最近在复盘某个 Agent 项目时,我发现一个特别有意思的现象:底层模型的调用质量已经调得很稳了,但整体 Agent 的表现依然忽上忽下。后来把问题拆开看,瓶颈根本不在模型,而在我们围绕 Agent 构建的那套技能体系——工具(tools)是有的,但不知道怎么把它们组织成真正可复用的技能包(skills)。这个项目的代号就是 agent-skills,它最终改变了我们对 Agent 开发的理解方式。
这篇文章想聊的,就是 agent-skills 背后那套方法论:Skill 到底是什么,该怎么设计,怎么落地,怎么测试,以及我最开始在项目里踩过的那些坑。如果你正在做 Agent 相关的开发,或者准备把手头的工具调用整合成更稳定的智能体应用,这篇文章应该能给你一份可以照着做的清单。
1. 为什么 Skill 是 Agent 落地的真正分水岭
1.1 先厘清概念:Agent 和 Skill 到底指什么
先看 Agent。业内对 Agent 的定义五花八门,但落到工程层面,我觉得最朴素的理解是:一个能够自主规划、调用外部工具、根据执行结果进行多轮迭代,最终完成目标的系统。它和你写一个简单的 LLM 问答应用最大的区别在于,Agent 有行动能力——它能从"思考"进入"执行",再从"执行结果"回到"思考"。
那 Skill 是什么?它是 Agent 可以调用的某种"行为能力包"。一个 Skill 通常包含五个部分:
- 触发条件:什么场景下该用这个 Skill。
- 输入输出契约:调用它需要传什么参数,它会返回什么结构。
- 执行逻辑:内部调用哪些工具,按什么顺序执行。
- 约束条件:超时、重试、最大迭代次数、安全校验等。
- 退出状态:成功、失败、部分成功分别怎么上报。
一个 Skill 可以是一个工具的简单封装,也可以是多个工具的编排组合。拿"查天气并规划出行"来说,它可能同时调用了天气 API、地图 API 和时间计算逻辑,但对模型而言,它只需要暴露一个意图明确的技能入口。
1.2 从"会对话"到"会干活",差的是一整套行为协议
很多团队做 Agent 第一步都是"让模型能调用工具"。这个阶段的核心工作就是把几个 API 包装成 function,配上描述信息,然后期待模型在合适的时候调它们。跑通一两个 demo 确实不难,但真正进入生产环境后,问题会接踵而至:
- 模型不知道该在什么时机调用哪个工具。
- 工具返回了一堆原始数据,模型不知道怎么过滤和汇总。
- 连续执行多个工具时,状态混乱,参数传递对不上。
- 工具失败了,模型只会傻乎乎地重试,而不是换一种方案。
我见过不少项目卡在这里。不是模型不行,而是你只给了模型一堆"工具",却没有给它一套"使用工具的规范"。Skill 本质上提供的就是这套规范——它把"什么时候调用、传什么参数、怎么处理结果、失败了怎么办"都固化下来。一个没有 Skill 体系的 Agent 就像新手厨师面对一厨房的食材和刀具,知道每样东西是干什么的,但做不出一桌菜;而有了 Skill 体系的 Agent,手里拿的是一个个已经配好料、注明火候的菜谱。
我见过一个对比测试:同一个模型,同一个工具集,一个版本直接暴露 12 个 function,另一个版本按 Skill 封装后只暴露 4 个"能力入口"。结果后者的任务成功率提升了接近 40%,token 消耗反而降低了。原因很简单——模型做选择的空间变小了,做对的概率自然变大了。
1.3 Skill 之于 Agent,就像函数库之于程序
假设你写一个程序,从来不划分函数,所有逻辑全部堆在 main 里。前期可能还能跑,一旦需求复杂起来,代码的维护成本会指数级增长。你没法单独测试某一块逻辑,没法复用已有功能,每次改动都像在拆炸弹。
Agent 开发也是一样。如果你把工具调用、提示词、状态管理等所有逻辑都堆在编排层,最后一定是一团乱麻。Skill 的价值就是让 Agent 开发变成"搭积木"而不是"堆面条":
- 每个 Skill 是独立的单元,可以单独测试和调优。
- 一个 Skill 可以在多个 Agent 场景中复用。
- 修改一个 Skill 不会影响其他部分。
- 新增能力时,只需要新增一个 Skill,或者组合现有 Skill。
这也是 agent-skills 项目最核心的诉求:我们不希望每个新场景都从零开始造轮子,我们希望积累一个技能库,让新的 Agent 能像拼接积木一样快速组装起来。
2. Skill 设计的第一课:接口契约、状态边界与失败语义
2.1 接口契约:让概率系统变成可约束系统
大语言模型本质上是概率系统,同一个问题,你问十次可能有八种不同的表述。这种灵活性在聊天时是优点,但在调用函数时是灾难。你定义了一个search_products(keyword, category, max_price)函数,模型可能给你传max_price=便宜这种根本没法校验的值。
所以 Skill 设计的第一个原则就是:接口契约要足够严格。这里的严格不是指你要写一堆复杂的类型系统,而是你要用结构化方式把参数的边界定义清楚。下面是我在实际项目里常用的一种 Schema 设计:
skill_contract = { "name": "product_search", "description": "根据关键词和筛选条件搜索商品列表", "parameters": { "type": "object", "properties": { "keyword": { "type": "string", "description": "搜索关键词", "minLength": 1, "maxLength": 50 }, "category": { "type": "string", "enum": ["electronics", "clothing", "books", "home"], "description": "商品类目" }, "max_price": { "type": "number", "minimum": 0, "description": "最高价格(元)" }, "sort": { "type": "string", "enum": ["relevance", "price_asc", "price_desc"], "default": "relevance", "description": "排序方式" } }, "required": ["keyword"], "additionalProperties": False } }你看这个设计里有几个关键点:
- 能用枚举值约束的,绝不写自由文本。
category字段明确告诉模型只有四个选项可选,模型就很少会传一个奇怪的值进来。 - 对字符串限制长度。模型有时候会把一整个句子塞进一个本该是简短参数的字段里,限制长度可以逼它提炼核心信息。
- 给可选参数设置默认值。即使模型没有传
sort字段,后端也能用默认值兜底。
更重要的是,这个契约不只是给模型看的,它同时驱动后端的参数校验逻辑。模型传进来的参数,在你真正执行业务逻辑之前,要先用契约校验一遍,不符合要求的直接打回。
2.2 状态边界:别让 Skill 变成隐藏的全局变量
Skill 设计里最容易埋坑的就是状态。很多初学者写 Skill 的时候喜欢在函数外部挂一堆全局变量,比如current_user_id、last_search_result、session_data等等,结果 Agent 跑着跑着就出现各种诡异问题。
一个典型的问题场景是:同一个 Skill 被并发调用时,全局状态乱成一锅粥。你在代码里定义了一个global order_id,第一个请求的订单还没处理完,第二个请求就把它覆盖了。很多 Agent 框架在设计时是单线程、顺序执行的,这种问题不太容易暴露,但只要并发量稍微上来,立刻就会炸。
我的经验是遵循一个简单的原则:Skill 内部尽量无状态,所有需要跨步骤保留的数据,要么显式放在上下文中传递给下一个调用,要么放到外部存储里,并且带清晰的命名空间。Skill 的输入和输出应该是自我完备的——给定同样的输入,应该能预期同样的行为。
比如一个"下单"技能,它需要知道用户是谁、订单是什么、支付方式是什么,这些信息应该在调用 Skill 时就通过参数传入,而不是依赖 Skill 内部缓存的"上一步"数据。这样既方便测试,也方便在出问题时定位原因。
2.3 失败语义:Skill 一定会失败,关键是容忍失败
必须接受一个现实:Skill 的执行失败率不会为零。工具可能宕机,参数可能校验不通过,第三方接口可能返回畸形数据,模型可能把参数理解错。设计 Skill 的时候,不能只考虑"理想路径",必须把失败处理纳入核心逻辑。
我在 agent-skills 里把失败分成三类:
| 失败类型 | 具体表现 | 处理策略 |
|---|---|---|
| 工具级失败 | API 超时、返回 500、服务不可用 | 有限次重试,然后返回"当前服务不可用"状态 |
| 参数级失败 | 模型传的参数无法通过契约校验 | 尽量用默认值兜底,必要的参数缺失则明确提示模型纠正 |
| 策略级失败 | 工具执行成功,但结果不符合预期(比如搜索无结果) | 返回结构化信息,让模型换一种策略 |
这里最关键的是:Skill 的失败不应该直接中断整个 Agent 过程,而是要把失败原因作为可读的结构化信息返回给模型,让模型有机会重新规划。
你可以在系统里加一段这样的逻辑:
def execute_skill(skill_name, parameters): try: result = skill_registry.execute(skill_name, parameters) return {"status": "success", "data": result} except ValidationError as e: return {"status": "invalid_parameters", "errors": e.errors} except ToolTimeoutError: return {"status": "tool_error", "reason": "timeout"} except EmptyResultError: return {"status": "empty_result", "message": "no matching records"}这样模型收到"status": "empty_result"之后,就能理解"搜索方案没找到结果",进而决定是换关键词、放宽筛选条件,还是直接告诉用户搜不到。整个 Agent 的执行链路仍然是活的。
3. 从零写透一个 Skill:以联网检索为例
3.1 Skill 的目录结构设计
光说不练假把式。我拿一个最常见的 Skill——联网检索——来完整拆解一遍设计过程。这个 Skill 的核心功能是:给定一个查询,去外部搜索引擎或内容库检索,并把结果整理成模型能直接使用的信息。
我在 agent-skills 项目里使用这样的目录结构:
skills/ web_search/ metadata.yaml schema.json prompt.md router.py tools/ search_engine.py content_parser.py hooks/ post_process.py tests/ test_normal.py test_no_result.py test_fallback.py每个 Skill 自带元信息、契约、提示词、路由逻辑、工具封装和测试用例。这个结构的价值在于:任何一个 Skill 都是完整独立的,目录可以复制到任何新的 Agent 项目中直接启用,不需要改一行代码。
metadata.yaml 里面记录的是这个 Skill 的基本信息:
name: web_search description: 联网检索信息并返回经过整理的摘要结果 version: 1.2.0 tags: [search, retrieval] trigger_keywords: [搜索, 查询, 查一下, 找资料, 最新消息] max_retries: 2 timeout_seconds: 10触发关键词的存在是给模型一个"什么时候该用这个 Skill"的参考,它在最终生成的系统提示词里会起到引导作用。
3.2 工具路由:一个入口背后是三种检索策略
一个看似简单的"搜索"技能,内部其实需要根据查询类型做路由判断。我把它分成三种策略:
- 关键词搜索:查询是用户意图直接对应的明确关键词,直接调用搜索 API。
- 语义检索:查询是长句子或复杂描述,需要将查询向量化后走向量数据库。
- 结构化查询:查询涉及带有明确字段的实体(如"某个产品的型号参数"),需要生成结构化条件查数据库。
路由逻辑在 router.py 里实现,核心是先用一个轻量级分类器(或者直接用模型判断)确定查询类型,再分发给不同工具。这里有个小技巧:不要相信模型的第一次判断就完美,路由结果必须带置信度,低置信度时同时发起两种检索,最后按相关性融合。
def route_query(query): if contains_structured_entity(query): return "structured" if len(query) > 30: return "semantic" return "keyword"我在实际项目里发现,很多冷门领域的检索,关键词搜索和语义检索的结果差异很大。最稳的方案是默认走关键词搜索,如果结果量太少或相关度评分低,再自动触发语义检索兜底。
3.3 结果后处理与上下文压缩
检索工具返回的原始结果往往不适合直接丢给模型。搜索 API 返回的是一大堆 HTML 片段、广告位、无关页面描述,如果原封不动都塞进上下文,token 成本高不说,噪声还会干扰模型的判断。
我建议所有检索类 Skill 都要带一层 hooks/post_process.py,做下面三件事:
- 去重:同一来源的多个结果只保留信息量最大的一个。
- 截断:每个结果的标题和摘要控制在 120-150 个 token 以内。
- 排序:按照时间、来源可信度、相关性三个维度综合排序,把最可能有用前置。
经过后处理,原本来可能 3000 token 的搜索结果可以被压缩到 500 token,而且关键信息反而更容易被模型捕捉到。
3.4 可观测性与埋点
Skill 跑在生产环境里,最怕的是出问题时无从排查。我后来养成的习惯是:每个 Skill 必须埋点,至少记录以下几项:
- 触发时间、触发来源(哪个 Agent、哪个会话)。
- 接收到的原始参数,以及参数校验通过或失败的情况。
- 路由决策结果和置信度。
- 每个内部工具的调用耗时和返回状态。
- 后处理前后 token 数量的变化。
- 最终返回给模型的结构化结果摘要。
看起来记录这些东西很啰嗦,但当你调试一个成功率只有 80% 的检索 Skill 时,这些日志就是你的生命线。某次线上问题排查时,我靠日志里记录的"参数校验失败率异常升高"这条信息,定位到了模型升级导致的参数生成格式变化。如果没有这些埋点,这个问题可能要花好几个小时才能查出来。
4. Function Calling 与模型对齐:Schema 不是写了就完事
4.1 模型为什么不按你的 Schema 调用
我最初做 Skill 的时候,天真地以为把 schema.json 写得清清楚楚,模型就会规规矩矩地照着调用。现实给了我一记重拳:模型经常不按套路出牌。常见的几种"不乖"表现包括:
- 漏传必填参数,或者把必填参数写成 null。
- 枚举值传了一个不在列表里的内容,比如自定义了一个
"category": "digital"。 - 整段话塞进单参数字段里,导致搜索关键词变成一段完整描述。
- 明明你定义了
get_price(product_id),模型非得自己编一个get_price("iPhone 15 pro 256GB 蓝色")。
我一度以为是提示词的问题,反复修改工具描述,效果却不明显。后来才意识到:模型生成参数的逻辑,本质上是根据它对函数描述和参数描述理解,然后做模式匹配。描述写得再花哨,不如给出足够强的约束信号。
4.2 让 Schema 更稳的三个设计原则
经过多次迭代,我总结了三个实际的 Schema 设计原则,它们对提高模型调用的准确率很有帮助。
第一个原则:参数类型能写 enum 就不写自由文本。
这是整个 Schema 设计里最重要的一个原则。比如商品类目、排序方式、语言选项、日期格式,这类有边界的值,都要用 enum 明确列出来。枚举值本身不要给太多,5-10 个是理想区间。太少限制模型发挥,太多模型记不住,反而降低准确率。
第二个原则:字段描述控制在 10-20 个词。
很多开发者喜欢把字段描述写得很长,试图解释清楚这个参数适用于什么场景。但实际效果并不好,描述越长,模型越容易在其中迷失重点。保持简洁有力,直接把"是什么"说清楚就好。如果描述本身需要长篇大论来澄清,那说明这个参数的设计可能有问题——可能是该拆分成多个参数了。
第三个原则:每个参数必须有合理的默认值或兜底逻辑。
模型传递null或漏参的概率永远存在,设计时就要想好每个参数在缺失时的行为。是取默认值、忽略该筛选条件,还是返回参数错误让模型重新生成?不同选择会影响整个 Skill 的稳定性和不必要的重试成本。
4.3 给模型一个"调用范本"比规则更可靠
描述和枚举是"隐性约束",在 system prompt 里用少量示例做显式示范效果更好。我在实际项目里测试过,同样的 schema,不加示例的准确率可能在 70% 左右,加上 3 个示例后能提升到 90% 以上。
你是搜索助手,你可以调用 web_search 技能。请遵循以下示例格式: 用户问题:iPhone 15 pro 现在多少钱? 调用:web_search(keyword="iPhone 15 pro 价格", category="electronics", max_price=null) 用户问题:帮我找一本讲机器学习的书 调用:web_search(keyword="机器学习 书籍推荐", category="books", max_price=null)示例的价值在于它同时向模型展示了三件事:字段如何选择、参数如何格式化、模糊的问题如何被转化为精确的检索词。这是口头规则做不到的。大语言模型本质上是一个模式匹配引擎,给它越清晰的模式,它就越可能按照你要的模式执行。
5. 可别用"看起来能跑"交付:Skill 的测试与评估
5.1 Skill 测试的三个层面
很多做 Agent 的团队,测试停留在"跑几个 demo 看看效果"的水平。早期项目可以这么做,但在 agent-skills 这种技能库模式下,测试是刚需。我把 Skill 测试分成三个层面:
单元测试:Testing 技巧简单说就是分别验证内部工具、路由逻辑、后处理逻辑,独立于模型做确定性测试。用 mock 数据模拟工具返回,验证后处理逻辑是否输出预期结构。文本解析、结果截断、数据排重、参数校验等都是可以脱离模型直接测试的逻辑。
场景测试:这是真正体现 Skill 价值的测试级别。每个 Skill 至少准备 20-30 个覆盖不同场景的测试题,包括正常路径、边界路径和异常路径。
回归测试:每当 Skill 有变更——包括底层工具的 API 变化、提示词调整、契约字段更新——都要跑一遍历史用例集,确保没有引入回归问题。改一个参数引发五个旧用例挂掉的情况,我在项目里见过太多次了。
5.2 场景测试用例应该覆盖什么
下面是我常用的一个表格模板,每个新 Skill 都要填写进去:
| 场景类型 | 测试用例示例 | 预期结果 |
|---|---|---|
| 正常路径 | 查询"今天北京的天气" | 返回结构化天气信息,包含温度、湿度、风力和建议 |
| 边缘路径 | 查询"给我介绍一下不存在的内容" | 返回空结果,并说明建议调整关键词 |
| 参数边界 | 查询"帮我用最小值 0 搜商品" | 正确处理最小值为 0 的筛选逻辑 |
| 工具异常 | 模拟搜索 API 超时 | 返回工具错误,Agent 能给出合理兜底话术 |
| 多轮交互 | 先搜A再查B,验证状态不串 | 两次搜索结果互不干扰 |
| 内容冲突 | 搜索返回矛盾信息(如两处价格不同) | 能从多条结果中提炼共识,给出可信结论 |
场景测试的执行建议使用自动化框架,准备一个测试描述集,每条用例通过调 Skill 入口执行,然后用规则或轻量模型判断输出是否符合预期。人工跑场景测试一次两次还行,长期维护必须依赖自动化和典型的"影子模式"来辅助。
5.3 Skill 评估的关键指标
测试解决的是"会不会挂"的问题,评估解决的是"好不好用"的问题。我建议每个 Skill 都设定下面几个指标:
| 指标 | 定义 | 建议目标 |
|---|---|---|
| 任务成功率 | 端到端完成用户请求的比例 | 目标 ≥ 90% |
| 工具调用准确率 | Skill 选择正确、参数无误的比例 | 目标 ≥ 95% |
| 单次调用 token 消耗 | Skill 一次完整消耗的 token 数 | 目标比 baseline 低 20% |
| 端到端时延 | 从用户请求到响应的总耗时 | 目标 < 8 秒 |
| 失败恢复率 | 失败后通过重试/换路成功的比例 | 目标 ≥ 50% |
这些指标的基线怎么定?我建议先记录两周的线上数据,拿到真实分布,再设定可实现的提升目标。不要拍脑袋定一个数字,否则团队会很痛苦。
5.4 影子模式是安全上生产的重要一步
最后一个测试手段我觉得特别值得推荐:影子模式。就是在生产环境里把真实流量复制一份,打到新版本 Skill 上,但新 Skill 的响应不会真正返回给用户。实施方法是读取线上日志中的历史请求,并在离线测试环境中回放或将流量实时镜像到隔离环境。
这样你可以用真实线上分布的数据去验证新 Skill 的效果,不影响用户体验,又能提前发现大部分问题。等影子模式跑几天,成功率稳定了,再把流量缓慢切换过去。我习惯按 5% -> 20% -> 50% -> 100% 的节奏灰度,每次都观察指标回落是否正常。
6. 我在实际项目中沉淀的 Skill 设计清单
6.1 一份可以直接抄作业的检查清单
基于 agent-skills 项目里的多次重构和踩坑,我把经验沉淀成了一份清单。团队里每次新增或重构一个 Skill,都要过一遍这份清单:
- 接口契约里是否所有有界字段都使用了 enum?自由文本字段是否限制了长度?
- 每个参数都有明确的默认值或缺失兜底逻辑吗?
- 触发条件清晰吗?模型能在 2 秒内判断这个 Skill 是否适用于当前场景吗?
- Skill 目录是否包含 metadata、schema、prompt、tools、hooks 和 tests?
- 内部是否隐式依赖外部全局状态?能否只凭入参复现同样的输出?
- 失败处理是否覆盖了工具级、参数级、策略级三类失败?
- 是否埋了完整的调用日志,包括耗时、token 用量、失败原因?
- 是否有 20 条以上的场景测试用例,覆盖正常、边界、异常路径?
- 是否设定了任务成功率、token 消耗、时延的基线指标?
我把这份清单打印出来贴在工位上,每次设计新 Skill 都对着它逐条检查。
6.2 从单 Agent 到多 Agent:Skill 的下一步演进
agent-skills 项目做到后期,我们开始从单 Agent 走向多 Agent 协作。这时候原来的 Skill 设计思路需要做一些调整。核心变化是:Skill 不再只面向"任务执行",还要面向"协作通信"。
比如我们会为"任务分配"设计一个特殊 Skill,它的输出不是一个执行结果,而是一个已经格式化好的子任务描述。另一个 Agent 可以直接识别这个描述,触发它自己的对应 Skill。这就有点像把函数调用变成微服务之间的 RPC 通信,只是双方的"协议栈"是自然语言加结构化数据。
这个方向我还在探索,但有个体会是:如果前期把单个 Skill 的接口契约设计得足够稳,这个演进过程会平滑很多。每一个 Skill 都像一个定义良好的服务,随时可以被编排成更复杂的流程。
最后再分享一点我的个人体会。很多人以为把 Skill 包装好、写进框架就完事了,真正的难点在后续的持续迭代:模型升级了、工具 API 变了、用户提问方式变了,都会对 Skill 的效果产生冲击。没有测试和监控的 Skill 库,就像一个不打理的花园,初看挺美,几周后就杂草丛生。Skill 设计是一个持续维护的过程,而不是一次性的编码任务。希望这篇关于 agent-skills 的方法论能让你少走一些弯路。