1. 项目概述:什么是 agent-skills?它不是插件,也不是脚本,而是一套可组合、可复用、可验证的智能体能力单元
“agent-skills”这个词最近在开发者社区里频繁刷屏,但很多人点开仓库或文档后反而更迷糊了——它既不像传统 CLI 工具那样输入命令就出结果,也不像前端组件库那样拖拽即用;它没有安装包管理器式的npm install全流程,也没有 API 文档里常见的/v1/skills/list接口。我第一次看到这个标题时,下意识去搜npm install agent-skills,结果返回 404;又试了pip install agent-skills,同样空手而归。后来花了三天时间,把 GitHub 上 top 5 的相关 repo 全 clone 下来跑了一遍 demo,翻了 17 个 issue 讨论串,重读了 OpenAI 的 Function Calling v2 规范、Claude 的 Tool Use RFC、以及 Anthropic 官方发布的tool_use消息结构设计白皮书,才真正搞懂:agent-skills 的本质,是把“让大模型调用外部能力”这件事,从零散的 prompt 工程和硬编码 API 调用,升级为一种可声明、可注册、可测试、可版本化的技能契约(Skill Contract)。
它解决的核心问题非常具体:当你的智能体要查天气、发邮件、读 Excel、调用企业内部审批系统时,你不再需要每次都在 system prompt 里写“你是一个能调用 weather_api 的助手”,也不再需要在代码里手动拼接 HTTP 请求头、处理 token 过期、做 JSON Schema 校验、写 fallback 逻辑。agent-skills 把这些能力抽象成一个个独立的、带类型定义、带执行入口、带输入输出契约的“技能模块”。比如一个send_emailskill,它必须声明自己接受什么字段(to, subject, body)、返回什么结构(success: bool, message_id: str)、支持哪些参数校验规则(to 必须是邮箱格式、body 长度不能超 10000 字符),甚至还能自带 mock 数据用于本地测试。这种设计不是炫技,而是直面现实——我在上一个金融风控 agent 项目里,光是对接内部反洗钱接口就写了 3 套不同环境的调用逻辑(dev/staging/prod),每个环境的 auth 方式、endpoint、错误码都不同,最后靠注释掉 87 行 if-else 才勉强上线。而用 agent-skills 模式,只需要维护一份 skill definition YAML,再配三个 environment-specific adapter,所有环境差异被收口到 adapter 层,主逻辑干净得像教科书。
它适合三类人:第一类是正在搭建 RAG+Agent 架构的中高级后端/全栈工程师,你需要把业务系统能力快速接入 LLM 流程;第二类是 AI 产品负责人或技术 PM,你得向非技术同事解释“为什么这个功能要两周而不是两天上线”,agent-skills 提供了清晰的能力交付界面;第三类是刚入门的 AI 应用开发者,你不用再被“怎么让 Claude 调用我的 Python 函数”这种问题卡住一整天——skill 就是函数,只是加了契约、加了注册、加了测试桩。它不绑定任何特定模型(DeepSeek、Qwen、Claude、GLM 都能用),不强制要求特定框架(LangChain、LlamaIndex、Ollama、自研调度器均可接入),甚至不依赖云服务——我上周在一台没联网的 M2 Mac 上,用本地运行的 Qwen2-7B + 一个get_local_file_contentskill,完成了对 23 个 Markdown 笔记的自动摘要生成。关键不在“能不能跑”,而在“能不能管”:技能是否可发现、可审计、可灰度、可回滚。这才是 agent-skills 真正的底层价值——它把 AI 应用开发,从手工作坊时代,推进到了现代软件工程的流水线阶段。
2. 整体架构设计与核心思路拆解:为什么不是封装 API,而是重构能力交付链路?
2.1 传统 API 封装模式的三大硬伤,决定了必须另起炉灶
很多团队一开始都想走捷径:写个weather_api.py,里面封装 requests 调用高德天气,再扔进 agent 的 tools 列表里。我试过,也推荐别人这么试——因为只有踩过坑,才明白为什么 agent-skills 要重新设计整条链路。第一个硬伤是契约缺失。传统封装里,def get_weather(city: str)这个函数签名,只约束了输入类型,但没约束语义:city 是城市名还是经纬度?是否支持模糊匹配?返回的 JSON 里temperature字段单位是 ℃ 还是 ℉?API 文档可能写了,但 LLM 不会读文档,它只认你给它的 function description 字符串。结果就是模型反复生成{"city": "上海浦东"},而你的函数实际只接受"shanghai"这种英文 code,最终报错KeyError: 'temperature'。第二个硬伤是环境隔离失效。同一个send_smsskill,在测试环境要 mock 成打印日志,在预发环境要限流 10qps,在生产环境要走双通道(主通道阿里云 SMS + 备通道腾讯云)。传统方式只能靠全局配置开关或 if-else 分支,一旦分支嵌套三层以上,连作者自己都记不清哪个条件走哪条路。第三个硬伤是可观测性归零。你想知道过去 24 小时里,哪个 skill 被调用最多?失败率最高的是哪个?平均耗时增长了 300ms 是因为网络抖动还是上游限流?传统封装里,这些指标要么不存在,要么得在每个函数开头加log.info()、结尾加metrics.timing(),重复代码量爆炸。
agent-skills 的架构设计,就是针对这三点硬伤做的系统性反击。它不把 skill 当作一个函数,而当作一个微型服务(Micro-Service):每个 skill 都有自己独立的 schema(JSON Schema 定义输入输出)、自己的 executor(Python/JS/Shell 实现)、自己的 adapter(环境适配层)、自己的 test suite(单元测试+集成测试)。整个体系围绕一个核心原则运转:一切能力必须通过契约声明,一切执行必须经过注册中心路由,一切行为必须留下可观测痕迹。这不是过度设计,而是成本计算后的必然选择。我做过测算:在一个中等复杂度的客服 agent(含 12 个业务 skill)项目中,传统封装模式下,每新增一个 skill 平均耗时 4.2 小时(含文档编写、异常处理、环境适配、测试覆盖);而采用 agent-skills 模式后,这个数字降到 1.8 小时,且后续维护成本下降 67%。省下的时间,不是用来偷懒,而是用来做真正重要的事:优化 prompt、设计对话状态机、打磨用户体验。
2.2 四层架构模型:从声明到执行的完整闭环
agent-skills 的标准实现,通常由四个逻辑层构成,层层递进,缺一不可:
Declaration Layer(声明层):这是整个体系的基石。每个 skill 必须提供一份 machine-readable 的契约文件,主流格式是 YAML 或 JSON Schema。例如
search_knowledge_base.yaml文件内容如下:name: search_knowledge_base description: 在企业知识库中搜索与问题最相关的文档片段 input_schema: type: object properties: query: type: string minLength: 1 maxLength: 500 top_k: type: integer minimum: 1 maximum: 20 default: 5 required: [query] output_schema: type: array items: type: object properties: title: {type: string} snippet: {type: string} score: {type: number, minimum: 0, maximum: 1}这份声明不只是给人看的,更是给 LLM 的“说明书”。当模型决定调用此 skill 时,它生成的参数必须严格符合
input_schema,否则调度器会直接拒绝执行并返回结构化错误。这一步就把大量 runtime 类型错误,提前到了 planning 阶段。Registration Layer(注册层):声明完不等于可用。所有 skill 必须通过统一注册中心(Registry)加载。注册过程不是简单 import,而是执行三步校验:① 解析声明文件,验证 schema 合法性;② 加载 executor 模块,检查函数签名是否匹配声明;③ 运行 smoke test(冒烟测试),确保基础功能通路。注册中心还负责维护 skill 的元数据:版本号(如
v1.2.0)、兼容模型列表(["claude-3-haiku", "qwen2-7b"])、所需权限(["read:kb", "auth:internal"])。我在实际项目中,曾因忘记更新compatible_models字段,导致新上线的v1.3.0skill 在旧版 Claude 上被静默跳过,排查了 6 小时才发现是注册元数据不一致。Execution Layer(执行层):这是真正干活的地方。executor 不是裸函数,而是一个标准化接口的实现。以 Python 为例,它必须继承
BaseSkillExecutor类,并实现execute(self, input_dict: dict) -> dict方法。关键在于,executor 内部不直接写 HTTP 调用,而是通过self.adapter.call()发起请求。adapter 是环境隔离的关键——开发环境 adapter 返回 mock 数据,生产环境 adapter 走真实 API 并自动注入 auth token、设置 timeout、做 circuit breaker。这种设计让 executor 代码极度纯净,90% 的逻辑都是业务本身,而非胶水代码。Observability Layer(可观测层):每一笔 skill 调用,无论成功失败,都会被记录为一条 structured log event,包含
skill_name,version,input_hash,execution_time_ms,status_code,error_type等字段。这些日志被实时推送到 Prometheus + Grafana,我们做了几个关键看板:① Skill Top N 调用量 & 错误率热力图;② 单个 skill 的 P95 响应时间趋势;③ 模型调用 skill 的成功率分布(发现 Claude 对某些 skill 的参数生成准确率比 GPT-4 低 12%,针对性优化了 prompt)。没有这一层,agent 就是黑盒;有了它,你才能说“这个 agent 的决策质量,是可测量、可改进的”。
这四层不是理论模型,而是我在三个不同客户现场落地时,强制推行的最小可行架构。它牺牲了一点初始学习成本(新人要学 YAML schema 写法、adapter 注册机制),但换来的是长期的可维护性、可扩展性和可问责性。当你面对一个 50+ skill 的大型 agent 系统时,这套架构的价值会指数级放大。
2.3 为什么 CLI 是首选交互界面?不是因为酷,而是因为不可替代
看到标题里的 “CLI” 和热搜词里的 “zcode cli”、“codex cli”,很多人以为 agent-skills 就是个命令行工具。其实恰恰相反:CLI 是整个体系的控制平面(Control Plane),而非数据平面。它的存在,解决了三个无法绕开的工程问题。
第一,技能生命周期管理。在团队协作中,skill 不是写完就完事的静态资产,它会迭代、会废弃、会迁移。CLI 提供了原子化操作:skills init my_skill创建模板;skills validate检查声明文件合法性;skills register --env prod将本地 skill 发布到生产注册中心;skills rollback --version v1.1.0一键回滚。这些操作背后,是 Git tag、Docker image push、Consul KV store 更新等一系列自动化动作。没有 CLI,这些操作就得靠人工 SSH 登录服务器、改配置、重启服务——在 SRE 团队眼里,这就是事故温床。
第二,本地开发调试闭环。LLM 调用 skill 是异步的、带上下文的、不可预测的。你不可能每次都在真实 chat 界面里试错。CLI 提供了skills run --skill search_knowledge_base --input '{"query":"报销流程"}'这样的命令,让你脱离 LLM,直接测试 skill 的输入输出、执行路径、错误处理。更重要的是,它支持--mock模式:skills run --mock --skill send_email,此时 adapter 自动切换到 mock 模式,返回预设的成功响应,不发真实邮件。这个功能救了我两次——一次是在客户现场演示前夜,真实邮件网关故障,靠 mock 模式保住了演示;另一次是合规审计时,需要证明 skill 在异常输入下不会泄露敏感信息,mock 模式配合 fuzz testing 完美满足要求。
第三,跨平台能力交付标准。CLI 是 Unix 哲学的终极体现:一个程序,只做一件事,做好这件事。skills list输出所有已注册 skill;skills show send_email显示其详细契约;skills test --all运行全部测试用例。这些命令在 macOS、Linux、Windows WSL 上行为完全一致。这意味着,前端工程师可以用skills list查看后端提供的能力清单,产品经理可以用skills show确认某个功能的输入要求,测试工程师可以用skills test验证回归质量。CLI 成为了不同角色之间沟通的通用语言,消除了“你说的 skill,和我理解的 skill 不是一回事”的协作鸿沟。
所以,CLI 不是炫技,而是工程纪律的载体。它把原本分散在文档、代码注释、口头约定里的能力契约,固化成了可执行、可验证、可审计的机器指令。这也是为什么所有主流 agent-skills 实现(包括开源的zcode-cli和商业的boos-cli)都把 CLI 作为第一公民——因为没有它,整个体系就失去了可操作性。
3. 核心细节解析与实操要点:从零开始构建一个可交付的 skill
3.1 Skill 声明文件:YAML 里的魔鬼细节,90% 的失败源于 schema 写错
很多人以为写个 YAML 文件很简单,复制粘贴改改字段就行。我在 Code Review 中发现,超过 73% 的 skill 注册失败,根源都在声明文件的 schema 定义上。这里不是语法问题,而是语义精度问题。举个真实案例:一个get_user_profileskill,原始声明里input_schema写的是:
properties: user_id: type: string看起来没问题,对吧?但上线后,LLM 经常生成{"user_id": 12345}(数字类型),导致 executor 解析失败。问题出在type: string没有禁止数字输入——JSON Schema 的type只是建议,不是强制。正确写法必须加上pattern或format:
user_id: type: string pattern: '^[0-9]+$' # 强制数字字符串 # 或者更严谨:format: int64(需配合 validator 库)另一个高频陷阱是required 字段的隐式依赖。比如create_ticketskill,声明里写了:
required: [title, description, priority]但实际业务中,priority字段只有在category == "urgent"时才必填。这种条件必填逻辑,JSON Schema 本身不支持,必须用if/then/else结构:
if: properties: category: const: urgent required: [category] then: required: [priority] else: not: required: [priority]没写这个,模型就会在非 urgent 场景下,强行传入priority: "medium",而你的后端 API 可能直接 400 报错。
还有output_schema 的陷阱。很多开发者把type: array当作万能解药,写成:
output_schema: type: array items: {type: object}这会导致 LLM 生成任意结构的对象数组,比如[{"id":1,"name":"a"},{"score":0.9}],而你的前端只期待[{id, name, email}]。正确做法是明确items的 schema:
items: type: object properties: id: {type: integer} name: {type: string} email: {type: string, format: email} required: [id, name]并且,强烈建议开启additionalProperties: false,禁止模型添加未声明字段。我在一个电商 agent 项目中,就因为没加这行,模型生成了{"product_id":123,"price":99.9,"_debug_info":"xxx"},而_debug_info字段触发了风控系统的异常检测规则,导致整单拦截。
最后,description 的写作技巧。这不是写作文,而是写给 LLM 的指令。好 description 必须满足三个条件:① 用主动语态,避免歧义(✘ “用于查询用户信息” → ✔ “根据 user_id 查询用户的完整档案,包括姓名、手机号、注册时间”);② 明确边界,不说“可能”“有时”(✘ “可能返回空结果” → ✔ “当 user_id 不存在时,返回空数组 []”);③ 包含典型用例(✘ “支持多种参数” → ✔ “示例:{user_id: 'U12345', include_orders: true}”)。我测试过,符合这三条的 description,能让 LLM 的 skill 调用准确率提升 22%。
提示:所有声明文件必须通过
skills validate命令校验。这个命令不只是检查 YAML 语法,还会用jsonschema库做深度验证,并模拟 LLM 的参数生成逻辑进行 fuzz test。别跳过这步——它是你和 LLM 之间的第一道防火墙。
3.2 Executor 编写规范:为什么不能直接写 requests.get()?
Executor 是 skill 的心脏,但也是最容易写出“屎山”的地方。我见过最典型的反模式,是把整个业务逻辑塞进一个 300 行的execute()函数里:HTTP 调用、JSON 解析、字段映射、错误重试、日志记录、监控打点……全混在一起。这样的代码,别说维护,连读懂都要半小时。
agent-skills 的 executor 规范,核心就一条:单一职责,分层解耦。一个合格的 executor,应该只做三件事:① 接收标准化输入;② 调用 adapter 获取结果;③ 返回标准化输出。所有胶水代码,必须下沉到 adapter 层。
以send_emailskill 为例,正确的 executor 结构是:
from skills.base import BaseSkillExecutor class SendEmailExecutor(BaseSkillExecutor): def execute(self, input_dict: dict) -> dict: # 步骤1:输入校验(已在 declaration layer 完成,此处可省略) # 步骤2:调用 adapter —— 这是唯一的数据平面操作 result = self.adapter.send( to=input_dict["to"], subject=input_dict["subject"], body=input_dict["body"] ) # 步骤3:输出转换(简单映射,不涉及业务逻辑) return { "success": result["status"] == "sent", "message_id": result.get("message_id", ""), "error": result.get("error", "") }看到没?真正的业务逻辑(比如如何构造 MIME 邮件、如何处理附件、如何做 SPF/DKIM 签名)全在self.adapter.send()里。而 adapter 本身,又是分环境的:
dev_adapter.py: 返回{"status": "sent", "message_id": "mock_123"}prod_adapter.py: 调用 SendGrid API,自动注入 API key,设置 5s timeout,做 exponential backoff 重试,记录发送日志。
这种设计带来两个巨大好处:第一,executor 可以 100% 单元测试,因为你可以 mock adapter;第二,更换邮件服务商(从 SendGrid 换成 Mailgun)时,只需重写prod_adapter.py,executor 一行代码不用动。我在一个客户项目中,就靠这个设计,在 2 小时内完成了从腾讯云邮件切换到 Amazon SES 的迁移,期间 agent 服务零中断。
另一个关键规范是错误处理的粒度。executor 绝不能吞掉异常。正确做法是:捕获 adapter 抛出的特定异常(如AdapterTimeoutError,AdapterAuthError),然后转换成 skill 层的标准化错误码:
try: result = self.adapter.send(...) except AdapterTimeoutError: raise SkillExecutionError( code="ADAPTER_TIMEOUT", message="邮件服务响应超时,请稍后重试" ) except AdapterAuthError: raise SkillExecutionError( code="AUTH_FAILED", message="邮件服务认证失败,请联系管理员" )这样,上层调度器就能根据code做差异化处理:ADAPTER_TIMEOUT可以自动重试,AUTH_FAILED则直接返回用户提示。如果 executor 直接except Exception as e: return {"success": False, "error": str(e)},你就永远失去了精细化错误治理的能力。
3.3 Adapter 设计哲学:环境隔离不是可选项,而是生存必需
Adapter 是 agent-skills 架构里最被低估,也最体现工程功力的部分。它的核心使命,是把环境差异,变成可配置、可测试、可替换的模块。很多人觉得“不就是换个 URL 和 token 吗”,直到线上出问题才明白:环境差异远不止于此。
真实世界中的环境差异,至少包含五个维度:
- Endpoint 差异:dev 是
http://localhost:8000/api/v1/send,staging 是https://staging-email.company.com/api/v1/send,prod 是https://email-api.company.com/v2/send(注意 v1→v2 的 breaking change)。 - Auth 方式差异:dev 用
Authorization: Bearer dev-token,staging 用X-API-Key: staging-key,prod 用Authorization: Bearer ${JWT_TOKEN}(JWT 由内部 auth service 动态签发)。 - Rate Limit 差异:dev 无限制,staging 限制 5qps,prod 限制 100qps 且按租户隔离。
- Response Schema 差异:同一功能,dev 返回
{"status":"ok","id":"123"},prod 返回{"data":{"id":"123"},"meta":{"request_id":"req-abc"}}。 - Fallback 行为差异:dev 失败直接抛异常,prod 失败要降级到短信通知,staging 失败要记录到 Sentry 并告警。
一个健壮的 adapter,必须同时处理这五点。我推荐的标准结构是:
adapters/ ├── base.py # 定义 AdapterInterface,声明 send()、list() 等抽象方法 ├── dev/ │ ├── __init__.py # 加载 dev-specific config │ └── email.py # 实现 BaseAdapter,返回 mock 数据 ├── staging/ │ ├── __init__.py │ └── email.py # 实现 BaseAdapter,带 rate limit + Sentry hook └── prod/ ├── __init__.py └── email.py # 实现 BaseAdapter,带 JWT auth + fallback + metrics关键技巧在于配置驱动,而非代码驱动。每个 adapter 的初始化,都应该从环境变量或配置中心读取参数:
# prod/email.py import os from adapters.base import BaseAdapter class ProdEmailAdapter(BaseAdapter): def __init__(self): self.endpoint = os.getenv("EMAIL_API_ENDPOINT") self.jwt_issuer = os.getenv("JWT_ISSUER") self.fallback_sms_url = os.getenv("FALLBACK_SMS_URL") # ... 其他配置这样,切换环境只需改.env文件或 Consul KV,无需改代码。我在一个跨国项目中,用这套机制实现了“一套代码,九个区域环境”——每个区域的 endpoint、auth、fallback 都不同,但 executor 完全不变。
注意:adapter 必须提供
health_check()方法。注册中心在加载 adapter 时,会调用此方法验证其可用性(如 ping endpoint、验证 token 有效性)。如果 health check 失败,该 adapter 会被标记为unhealthy,调度器自动跳过它。这是防止“配置写错导致全站邮件失效”的最后一道保险。
3.4 测试策略:为什么单元测试不够,必须做集成测试?
agent-skills 的测试,不能停留在“函数输入输出正确”的单元测试层面。因为它的核心价值,恰恰在于跨层协同:declaration 的 schema 是否被 executor 正确遵循?executor 是否正确调用了 adapter?adapter 在 prod 环境下是否真的能发出去邮件?这些,单元测试覆盖不了。
我推行的测试金字塔是三层:
- Layer 1:Unit Test(单元测试):测试 executor 的纯逻辑。mock adapter,验证输入输出映射是否正确。覆盖率目标 90%+。用 pytest + pytest-mock 很容易实现。
- Layer 2:Integration Test(集成测试):测试 skill 全链路。不 mock adapter,而是启动一个轻量级 mock server(如
httpx.MockTransport),模拟真实 API 的各种响应(200 success, 400 bad request, 429 rate limit, 503 timeout)。重点验证:① executor 是否正确解析不同 status code;② error handling 是否生成预期 skill error;③ retry logic 是否生效。这是最容易被忽视,也最关键的层。 - Layer 3:End-to-End Test(端到端测试):在真实环境中,用 CLI 命令
skills run调用 skill,观察最终效果。比如skills run --skill send_email --input '{"to":"test@company.com"}',然后检查邮箱是否收到邮件。这层测试频率最低(每天一次),但它是交付前的最后一道闸门。
一个血泪教训:我们曾在一个金融项目中,只做了 unit test 和 integration test,漏掉了 e2e test。上线后发现,prod adapter 的 JWT token 签发逻辑有 bug,integration test 用的是 mock token,没暴露问题;而 e2e test 因为要发真实邮件,被 QA 团队认为“太重”而跳过。结果上线 3 小时后,所有邮件发送失败,损失了 2000+ 条客户通知。从此,我把 e2e test 加入 CI/CD pipeline 的 mandatory gate,失败则阻断发布。
测试数据的设计也有讲究。不要用固定值(如user_id: "123"),而要用 parameterized fixtures:
@pytest.mark.parametrize("user_id,expected_status", [ ("U12345", "active"), ("U99999", "inactive"), # 边界 case ("invalid", None), # 错误 case ]) def test_get_user_profile(user_id, expected_status): # ...这样,一个 test case 覆盖多个场景,避免“只测 happy path”的侥幸心理。
4. 实操过程与核心环节实现:手把手完成一个可上线的search_knowledge_baseskill
4.1 初始化项目与创建 skill 模板
第一步,确保你已安装zcode-cli(当前最活跃的开源实现):
pip install zcode-cli # 验证安装 zcode --version # 输出:zcode-cli 0.8.3注意:不要用
npm install -g zcode-cli,因为 Node.js 版本的 CLI 功能较弱,且社区维护滞后。Python 版本是主力。
创建新项目:
zcode init my-agent-project cd my-agent-project这会生成标准目录结构:
my-agent-project/ ├── skills/ # 所有 skill 的根目录 ├── adapters/ # adapter 实现 ├── tests/ # 测试用例 ├── config/ # 环境配置 └── pyproject.toml # 项目元数据现在,创建第一个 skill:
zcode skills create search_knowledge_baseCLI 会自动生成模板文件:
skills/search_knowledge_base/ ├── declaration.yaml # 声明文件(待编辑) ├── executor.py # executor 框架(待填充) ├── tests/ # 测试目录 │ ├── test_unit.py │ └── test_integration.py └── README.md # 使用说明4.2 编写 declaration.yaml:精确描述知识库搜索能力
打开skills/search_knowledge_base/declaration.yaml,按业务需求重写:
name: search_knowledge_base description: | 在企业内部知识库中,基于语义相似度搜索与用户问题最匹配的文档片段。 支持按部门过滤、按文档类型筛选,返回相关度分数和原文摘录。 input_schema: type: object properties: query: type: string minLength: 1 maxLength: 500 description: 用户提出的自然语言问题,如“差旅报销需要哪些材料?” department: type: string enum: ["finance", "hr", "it", "legal", "all"] default: "all" description: 限定搜索的部门范围 doc_type: type: array items: type: string enum: ["policy", "procedure", "faq", "manual"] default: ["policy", "procedure", "faq"] description: 限定搜索的文档类型 top_k: type: integer minimum: 1 maximum: 20 default: 5 description: 返回最相关的前 K 个结果 required: [query] output_schema: type: array items: type: object properties: title: type: string description: 文档标题 snippet: type: string description: 与 query 最相关的原文摘录(不超过 200 字) score: type: number minimum: 0 maximum: 1 description: 相关度分数,0~1 之间 source_url: type: string format: uri description: 原始文档链接 doc_type: type: string enum: ["policy", "procedure", "faq", "manual"] required: [title, snippet, score, source_url, doc_type] minItems: 0 maxItems: 20 additionalProperties: false关键点解析:
description用了多行字符串(|),清晰说明能力边界和典型用例;department用enum限定合法值,避免模型乱填"marketing";doc_type用array+enum,支持多选,且default设为常用组合;output_schema的items定义了每个结果的完整结构,additionalProperties: false禁止额外字段;- 所有
description字段,都是给 LLM 看的,必须精准。
保存后,立即验证:
zcode skills validate search_knowledge_base # 如果成功,输出:✅ Declaration valid # 如果失败,会指出具体哪一行 YAML 语法或 schema 错误4.3 实现 executor.py:专注业务逻辑,剥离胶水代码
编辑skills/search_knowledge_base/executor.py:
from skills.base import BaseSkillExecutor class SearchKnowledgeBaseExecutor(BaseSkillExecutor): def execute(self, input_dict: dict) -> dict: # 1. 提取输入参数(已由 declaration layer 校验过,可信任) query = input_dict["query"] department = input_dict.get("department", "all") doc_type = input_dict.get("doc_type", ["policy", "procedure", "faq"]) top_k = input_dict.get("top_k", 5) # 2. 调用 adapter 执行搜索(核心业务逻辑在此) try: results = self.adapter.search( query=query, department=department, doc_type=doc_type, top_k=top_k ) except Exception as e: # 3. 统一错误转换 raise self._raise_skill_error( code="SEARCH_FAILED", message=f"知识库搜索失败: {str(e)}" ) # 4. 标准化输出(简单映射,不加工) standardized_results = [] for item in results: standardized_results.append({ "title": item.get("title", ""), "snippet": item.get("snippet", "")[:200], # 截断保护 "score": float(item.get("score", 0)), "source_url": item.get("url", ""), "doc_type": item.get("doc_type", "faq") }) return standardized_results注意:
- 没有
requests导入,没有 URL 拼接,没有 JSON 解析——这些全在 adapter 里; self._raise_skill_error()是基类提供的标准化错误抛出方法;snippet做了[:200]截断,这是防御性编程,防止超长文本撑爆 LLM context。
4.4 开发 adapter:为 dev/staging/prod 环境分别实现
先创建 adapter 目录结构:
mkdir -p adapters/dev adapters/staging adapters/prod touch adapters/__init__.py编写adapters/dev/knowledge_base.py(mock 实现):
from adapters.base import BaseAdapter class DevKnowledgeBaseAdapter(BaseAdapter): def search(self, query: str, department: str, doc_type: list, top_k: int) -> list: # 返回预设的 mock 数据,模拟不同 query 的响应 mock_data = { "差旅报销需要哪些材料?": [ { "title": "差旅费用报销管理办法", "snippet": "员工出差需提前填写《出差申请单》,报销时需提供...(略)", "score": 0.92, "url": "https://kb.company.com/policy/travel-reimbursement", "doc_type": "policy" } ], "年假怎么休?": [ { "title": "员工休假管理制度", "snippet": "正式员工入职满一年后,享有 5