上次复盘 agent 项目时又遇到一个老问题:模型推理能力明明已经足够,可任务执行结果还是忽好忽坏。排查到最后,问题往往集中在同一个环节——技能设计。圈子里聊 agent 的帖子已经够多了,但大部分都在谈选哪个模型、用哪个框架、怎么调 prompt,真正把技能(skills)本身当成核心工程问题来对待的很少。我做了大半年 agent 落地,觉得有必要把这块拆开讲清楚。如果你也在做 AI 代理、助手、自动化流程这类东西,这个主题值得认真看。
1. Agent 技能到底是什么:先摆脱"工具即技能"的惯性认知
很多项目一开始只会用"工具"(tool)这个概念——给模型挂几个函数,能调 API 就行。但做着做着就会发现,工具只是技能的骨架,技能是一个完整的能力单元,把"模型应该怎么用这个能力"也包进去了。
1.1 工具是函数,技能是"函数加说明书加容错机制"
我在新项目里要求的技能定义必须包含三样东西:可执行的动作、模型侧的使用说明(description + 参数 schema)、以及失败时的兜底行为。纯工具函数只解决第一样,后两样完全交给模型自己猜。模型猜得对,那是运气好;猜错了,排查起来就是灾难。
拆开来看:
- 动作层:真正的 Python 函数、API 调用、数据库读写,负责执行。
- 说明层:告诉模型这个技能什么时候用、怎么传参数、参数代表什么含义、返回结果长什么样。
- 容错层:技能本身可能失败,失败之后应该返回什么信息、模型下一步该怎么处理。
很多团队把这三层揉在一个函数签名里,注释写两行就完事,结果模型在真实调用时根本不知道该传什么。说白了,技能设计就是给模型写"使用手册",手册写得越清晰,模型干活的稳定性就越高。
1.2 技能粒度:从"细粒度原子"到"工作流级技能"
技能还有一个很容易被忽略的维度——粒度。我把技能分成两类:
一类叫原子技能(Atomic Skill),比如"查询用户订单"、"获取天气信息"、"计算两个日期差多少天"这类单步动作,参数简单、边界清晰、可复用性强,适合被其他技能调用。
另一类叫流程技能(Workflow Skill),把多个原子技能按固定流程编排成一个整体,比如"完成订单退款"需要先查订单、校验状态、计算退款金额、执行退款、记录日志五步——这五步打包成一个技能,模型不需要逐步决策,只需要一次性调用流程技能就行。
这两类技能的比例需要刻意平衡。原子技能太多,模型决策负担重、上下文爆炸;流程技能太多,灵活性差、新场景不好适配。我目前的经验是,建模阶段先拆原子技能,上线阶段再根据高频路径封装流程技能。
1.3 技能和 Prompt、插件的关系
不少人问技能跟"提示词工程"到底什么关系。我的理解是:Prompt 是让模型在"没有外部工具"的情况下生成正确输出;技能则是让模型在"需要改变现实状态"的情况下完成动作。两者不是替代关系,而是递进关系——先把 Prompt 调稳,再把需要跟外部世界交互的部分抽出来做成技能。
插件(Plugin)这个概念更多是工程部署层面的东西,解决的是"技能如何分发、加载、更新"的问题。技能是逻辑单元,插件是部署单元。一个插件里可以包含多个技能,也可以只包含一个。
2. 技能设计的核心元件:声明、逻辑、兜底三件套
这一节是整篇文章最硬核的部分,我直接给出自己在项目里沉淀的"技能三件套"设计法,配合代码示例说明。这部分内容你可以在自己的项目里直接抄作业。
2.1 技能声明:名称、描述和参数 Schema 怎么写才不误导模型
我见过太多技能声明写成一坨:"get_order_info(order_id)",描述也只有一个单词。这在 demo 里没问题,一旦技能数量超过 20 个,模型就开始频繁选错技能。
我的做法是给每个技能写三段式描述:
- 第一段说明技能能做什么,第一人称叙述,比如"获取指定订单的完整信息,包括状态、金额、商品明细"。
- 第二段说明什么时候用,比如"当用户查询订单状态、核对订单金额、或者售后需要订单信息时使用"。
- 第三段说明什么时候不要用,比如"不要用本技能判断订单是否可退款,退款资格请用 check_refund_eligibility 技能"。
参数 Schema 方面,三个容易踩的细节:
- 每个参数必须写 description,严禁只写类型。模型不看 schema 类型猜值,它看的是描述。描述写"订单编号,用户在电商平台下单后生成的唯一ID",模型就懂该去聊天上下文里翻。
- 复杂参数必须给 enum 或 example。比如状态参数,你写 string 类型模型可能给你传"已发货"、"shipped"、"SENT"各种格式。给出 enum ["pending", "paid", "shipped", "completed", "cancelled"],模型就会严格照抄。
- 参数顺序也有讲究,必填项放前面,可选项放后面。虽然模型理论上不受顺序约束,但实测里,必填在前、可选在后能减少漏参率。
from pydantic import BaseModel, Field from typing import Literal class GetOrderInfoParams(BaseModel): """获取订单信息技能参数""" order_id: str = Field( description="订单编号,用户在电商平台下单后生成的唯一ID,形如 ORD-2025-0716-001" ) include_items: bool = Field( default=False, description="是否需要返回商品明细,默认不返回以节省 token,需要时设为 true" ) class GetOrderInfoSkill: name = "get_order_info" description = "获取指定订单的完整信息,包括订单状态、金额、下单时间。当用户查询订单、核对账单、进入售后流程时使用。不要用它判断退款资格。" async def execute(self, params: GetOrderInfoParams): # 业务逻辑…… return {"order_status": "shipped", "total_amount": 199.00}最后这个"不要用它做什么"的段落很多开发者会忽略,但实际上,阻止模型误用某个技能,比引导它使用某个技能更重要。因为技能误选的代价不只是该动作没执行,还会污染后续所有决策。
2.2 技能执行逻辑:纯函数优先,副作用收敛
技能内部的业务逻辑,我要求严格遵循"纯函数优先,副作用收敛"原则。什么意思?就是技能的输入输出尽可能可预测、无状态,所有外部副作用(发消息、写数据库、调用第三方 API)集中在明确标注的区块里。
这么设计有几个好处:
- 可测试性大幅提升,单测里可以直接 mock 副作用,验证核心逻辑。
- 故障定位更快,技能报错时能迅速判断是逻辑问题、参数问题还是依赖服务问题。
- 技能可以进行重试、补偿、回滚,为构建复杂工作流奠定基础。
实现上我常用一个装饰器来统一管理技能执行:
def skill_runner(retry_times=2, timeout=10): """技能执行的统一包装:超时、重试、日志、异常收敛""" def decorator(func): @functools.wraps(func) async def wrapper(params): start = time.time() try: # 这里可以做参数校验、埋点、限流 result = await asyncio.wait_for(func(params), timeout=timeout) return {"success": True, "data": result, "latency_ms": int((time.time()-start)*1000)} except asyncio.TimeoutError: return {"success": False, "error": "skill_timeout", "hint": "服务暂时无响应,请稍后重试或建议用户等待"} except Exception as e: return {"success": False, "error": f"{type(e).__name__}: {str(e)}", "hint": ...} return wrapper return decorator注意最后把异常收敛成一个模型能看懂的结构——hint 字段是给模型看的,让它知道"技能挂了之后下一步该怎么办"。很多技能失败后模型就彻底卡住,就是因为返回的错误信息全是 Python traceback,模型根本不知道该干嘛。
2.3 技能返回值的"最小足够原则"
技能返回值直接影响模型下一步的推理质量。我踩过最大的坑就是把执行结果全量塞给模型,token 花了一大堆,模型反而迷失在细节里。
规定技能的返回值只保留"模型做下一步决策所需的最小集合",同时额外带一个"next_step_hint",提示模型接下来可以做什么。比如订单查询技能返回:
{ "success": true, "data": { "order_id": "ORD-2025-0716-001", "order_status": "shipped", "total_amount": 199.00, "next_step_hint": "用户订单已发货,如需查询物流请调用 query_logistics 技能;如用户申请退款且订单已发货,请先引导用户确认收货后退款" } }这个 next_step_hint 是我后来加上的,实测可以让模型的下一次技能调用准确率提高不少,因为它相当于给了模型一个"决策的路标"。
而完整数据(比如商品明细、地址详情)放到另外的字段,让模型按需再调详情技能获取,不要一次全给。这个设计的核心思想是:上下文是珍贵的推理资源,不是存储容器。
3. 技能路由:让模型在几十个技能里选中正确的那一个
当技能的粒度已经比较合理、声明也写得足够清楚之后,接下来的挑战变成了:面对 30 个甚至 50 个技能的时候,模型怎么在没有"思维短路"的情况下做出正确的调用选择?这一节讲路由层的设计。
3.1 全量注入的"选择瘫痪"问题
第一版技能系统简单粗暴,把所有技能定义一股脑注入上下文。模型每次请求都带着几十个技能声明,效果如何?我只能说,技能数量小于 20 的时候还能勉强工作;超过 30 个,模型的选择准确率明显下滑,而且出现了"看到某个技能名里有关键词就用"的浅层匹配。
我做过一个统计:技能数量从 15 个增加到 40 个,模型选错技能的概率几乎翻了一倍,同时首字延迟因为上下文过长增加了约 30%。
所以必须做技能路由(Skill Routing)——在把技能列表交给模型之前,先用一个路由模块做预筛,把候选集压到 3~5 个。这个路由模块可以是一个小模型、传统检索算法、或者规则系统,按需选择。
3.2 基于向量召回 + 规则的混合路由
我最终采用的做法是"规则前置 + 向量召回 + 模型兜底"三层路由:
- 第一层规则前置:根据用户请求里的明显信号做硬匹配。比如请求里出现"查一下订单"、"我的快递到哪了",直接绑定相关技能组。
- 第二层向量召回:把技能的名称、描述、场景标注做 embedding,用户请求也做 embedding,算余弦相似度,召回 top K。
- 第三层模型兜底:将前两层的结果作为候选技能注入上下文,由主模型做最终决策。
class SkillRouter: def __init__(self, skills: List[SkillDefinition]): self.skills = skills self.embeddings = self._build_skill_embeddings(skills) # 预计算技能向量 def route(self, user_query: str, top_k: int = 5): # 1. 规则硬匹配 ruled = [s for s in self.skills if self._match_rule(s, user_query)] # 2. 向量召回 query_vec = embed(user_query) ranked = sorted(self.skills, key=lambda s: cosine(query_vec, s.vec), reverse=True)[:top_k] # 3. 合并去重 candidates = dedupe(ruled + ranked) return candidates这套方案落地之后,候选技能注入量平均下降 70%,技能选择准确率回到 95% 以上。注意这里路由召回的准确性依赖技能描述质量,所以前面技能声明的功夫绝对不能省。
3.3 技能链:从"选技能"到"编排技能流程"
单技能调用只能解决单个动作,真实用户请求往往是多步任务,比如"帮我退款并且通知我朋友"。这就涉及到技能链(Skill Chain)的概念。
技能链有两种实现路径:
- 固定编排:基于用户请求的模式,从预设模板里选择一条链路,比如"退款流程技能",内部按顺序调用多个原子技能,每一步由模型填充参数。
- 动态编排:不预设链路,模型每一步在候选技能里选一个,执行后根据结果再选下一个,相当于用模型做运行时调度。
我一开始追求全动态编排,结果链路执行成功率非常不稳定,经常在第二步选错技能,还不好排查。后来改成"高频场景用固定编排,低频场景用动态编排",成功率反而上来了,模型 token 消耗也下来了。
这是 agent 系统一个比较反直觉的结论:动态能力越强,系统越不稳定;固定的东西越多,成功率越高。真正的智能应该用在"该灵活的事"上,而不是"每一步都重新发明轮子"。
4. 复盘:从技能系统 1.0 到 2.0,我踩过的三个大坑
说完了理想设计,讲讲实际踩出来的坑。这些坑几乎都来自同一个认知偏差——把技能当成"给模型多一个函数",而不是"帮模型做对一个决策"。
4.1 坑一:参数 Schema 不给示例,模型输出格式漂移
技能 1.0 时代,我定义参数只写类型和简短描述。很快发现一个问题:模型在传日期时间参数时,有时传2025-07-16 08:30:00,有时传2025/07/16,有时传2025-07-16T08:30:00Z。后端解析直接崩溃。
排查了很久,最终发现是参数描述里没有给出明确的格式示例。修复方案很简单——在每个易错参数的 description 里加上形如...的示例。加完当天,日期时间参数的格式错误率从 30% 降到几乎为零。
这件事给我的启发是:模型对格式的理解依赖于"范例"而不是"规则描述"。你写一百遍"ISO 8601格式",不如写一个2025-07-16T08:30:00Z来得有效。后来我把这个原则推广到所有技能的参数描述中,凡是带格式要求的字段,一律给出示例值。
4.2 坑二:返回值过大导致的注意力稀释
某个技能返回的是用户近一年的行为记录,分页做得很粗糙,一次返回几百条。当时觉得"数据越全,模型判断越准"。实测结果完全相反——模型看过多的记录之后,反而抓不住最新几条关键信息,给出的判断经常拿旧数据说事。
我后来把这种大数据量返回改成两层:第一层返回最近 5 条摘要 + 聚合统计(总次数、最近活跃时间、偏好标签),模型若需要完整明细,再显式调用一个新的"行为明细导出"技能按时间段查询。这样一改,模型的决策质量明显提升,tokens 也省了。
这个坑背后有一个更普适的原理:上下文越长,模型对局部信息的注意力越容易被稀释。用技能时要刻意控制每个技能的"可见输出量",把模型当成一个"每次只能读一屏文档的实习生",一次只给够用的信息。
4.3 坑三:技能之间的边界模糊导致互抢调用
技能多了之后会出现一种烦人的情况:两个技能都能处理同一种请求,模型随机选择一个,导致结果时好时坏。比如"下单"和"预下单"两个技能,功能相近,描述相近,模型经常把只想要试算价格、不想真正下单的用户误引导成提交真实订单。
这个问题的根治办法不是改描述,而是做技能边界审计:
- 找出所有"描述中包含相同业务实体和相近动作"的技能;
- 如果边界确实模糊,合并为一个技能内部做模式分支;
- 如果不能合并,在描述里显式写"什么时候用 A,什么时候绝对别用 A,用 B"。
边界模糊的另一个副作用是评估难做。你根本分不清模型选 B 算不算错误,因为 B 从某种角度看也能用。后来我在每个技能定义里强制增加"sees_also / 不要用本技能的场景"字段,从机制上逼着自己想清楚边界。
5. 技能治理:从 50 个技能到 500 个技能的可控演进
技能系统做到后面,难点早就不是某个技能怎么写,而是整个技能库怎么持续健康地演进。这一节讲治理经验,更像"系统架构"而不仅是"agent 技术"。
5.1 技能注册中心与依赖管理
团队协作时,不同人开发的技能需要有一个统一的注册中心,否则会出现技能重名、参数冲突、互相覆盖的混乱。我用一个简单的注册表来管理:
每个技能必须有全局唯一名称、版本号、负责人、依赖列表和调用频率统计。技能的依赖关系必须是显式的,不允许技能通过隐式全局变量共享状态。这样当某个底层 API 挂掉时,可以迅速排查出有哪些技能受影响。
# skill_manifest.yaml skill_name: refund_order version: 1.4.0 owner: team_after_sale dependencies: - get_order_info - check_refund_eligibility - payment_gateway domains: - ecommerce - after_sale注册中心的核心价值不是"管理",而是"可观测"。你要能随时回答三个问题:哪些技能最常用?哪些技能几乎没人调?哪些技能开始频繁报错?
5.2 技能版本管理与灰度上线
技能本质上是代码,所以必须走正经的版本发布流程。技能 1.0 时代我只是改完直接替换线上,结果某次一个新版技能改了参数格式,导致链路中断了一整天才发现。教训是:技能必须有版本向量,并且要支持灰度。
我现在的做法是:
- 每个技能打版本号,线上模型默认调用稳定版本;
- 新版本先在 5% 流量上灰度,对比该技能的成功率、延迟、是否引发链路异常;
- 灰度观察 24~48 小时后再切换全量,同时保留旧版本作为回滚点。
这套流程听起来很重,但对于生产环境,它省掉的麻烦远超投入。
5.3 技能退化检测与自动下线
技能库膨胀到一定程度后,必然出现部分技能质量退化,或者因为业务调整而彻底废弃。定期检测不能靠人肉巡检,我做了三档质量指标:
- 采纳率:候选技能出现后,模型最终选用它作为最终行动的比例。
- 成功率:技能执行成功(不抛异常、返回合规结果)的比例。
- 人工接管率:某个技能引发的会话转移到人工客服/人工干预的比例。
这三个指标合成一个健康分。低于阈值的技能会进入"待优化"列表,优化不了就下线。我把这个机制称为"技能的自然淘汰系统"——因为只要业务在变化,技能库就一直在新陈代谢,不清洗只会变成一坨越来越难用的遗产。
6. 技能系统再往后走:我的三个方向判断
技能系统从 1.0 做到 2.0,其实已经比较稳定了,但这段时间我一直在思考它能往哪走。以下三个方向是我现阶段比较看好的,可能对你有参考价值。
6.1 技能的自描述与自动化注册
现在技能的定义和注册还需要人肉写描述、写参数 Schema、做 embedding 索引。成本不小,而且质量因人而异。
下一步我觉得可以让技能本身具备自描述能力——根据技能的代码逻辑自动生成描述和参数说明,再配合测试用例自动校验描述准确性。这相当于给技能系统做自动化单测:描述写得对不对,用一组"测试请求"跑一遍路由,看能不能命中目标技能。
6.2 跨技能依赖图驱动的自动编排
固定编排和动态编排两种模式长期并存,但编排规则都是人肉维护的。我想看到的是:从技能依赖图出发,自动生成候选链路的拓扑排序,然后引导模型在合法链路上做选择。这样做的好处是不会出现"先退款再校验资格"这种明显违规链路的幻觉。
实现上需要一套 DAG 约束系统,以及链路级的效果追踪。复杂度不小,但一旦做出来,agent 的执行可靠性与可解释性都会有质的提升。
6.3 技能市场与跨团队复用
团队里沉淀的高质量技能,不妨做成内部技能市场,按业务域、场景打标,并支持一键引入。这个方向的价值在于组织级知识复用:某个团队验证过的售后流程技能,另一个团队在构建"用户咨询"场景时可以直接复用,而不必重新设计、重新踩坑。
跨团队复用带来的最大挑战是上下文中的语义冲突——同名概念不同定义。这个需要一套统一的领域词表(Ontology)来对齐,也是我接下来准备动手做的事情。
做 agent 技能的这大半年,我最大的感受是:模型能力的天花板比很多人想象的要高,决定实际效果的反而是"给模型配备的技能体系有多顺手"。技能设计不能靠临场发挥,它值得有一套像样的工程方法论。这篇帖子把我实战中验证有效的做法和踩过的坑都写出来了,如果你也在给 agent 配技能,欢迎照着一试,有问题评论区交流。