1. 从“会说话”到“会做事”Agent 技能到底解决什么问题做 Agent 相关项目的人迟早会撞上同一个路口模型已经能写出像样的回复了但让它真正“做成事”比如查库存、生成报表、操作内部系统却总是差一口气。我在某内部项目X里折腾了大半年逐步把各类业务能力整理成一套独立的“Agent 技能Agent Skills”体系才真正把模型从“聊天窗口”变成了“干活入口”。这个标题乍看是个普通开源项目名但它背后其实是 Agent 工程里最值得投入的一层设计。在动手写代码之前我想先聊聊为什么要单独做“技能”而不是继续把能力描述塞进系统提示词里。你如果已经有过类似尝试应该会懂我说的那种痛苦提示词越长模型越容易顾此失彼回答越来越模板化工具调用的准确率反而下降。把技能抽象成独立模块等于告诉模型你不需要知道每一步怎么做只需要告诉我你想做什么剩下的交给技能去执行。1.1 提示词越长Agent 越糊涂我见过很多团队的第一版 Agent是把所有能力说明写进一个超长的 system prompt 里比如“你可以调用查询订单接口参数如下……你可以调用发送邮件接口参数如下……”。这种方案在小规模试水时看着还行但一旦技能数量超过十个问题就全冒出来了。首先是 token 占用。每次请求都要把全部技能描述发给模型光是描述文本就可能占据好几千 token留给真正上下文的空间被压缩模型回答质量肉眼可见地下降。其次是选择困难。模型面对十几条格式相似的能力定义经常选错工具甚至自己“编造”一个不存在的接口名。我在测试中就见过模型一本正经地调用一个从未定义过的“getCustomerVipLevel”接口原因只是描述里提到过“vip”两个字。所以 Agent 技能体系的第一个核心价值是帮模型做“减负”。技能不是靠提示词硬堆出来的而是靠结构和选择机制让模型在合适的时机只看到合适的技能。1.2 Skill、Tool、Plugin 到底有什么区别聊技能之前先把这个概念区清楚否则后面所有设计都会含糊。这三者经常被混用但在 engineering 层面是有明确边界的。Tool 是最底层的能力单元通常是一个可以执行的函数或接口比如“发送 HTTP 请求”“读取文件”“调用某个 API”。它本身不携带业务语义只负责执行。Plugin 通常是一组相关能力的打包比如“Excel 处理插件”包含读取、写入、格式转换等多个工具。Skill 则更进一步它不只是工具集合而是面向某个任务目标的能力封装里面可以包含提示词模板、工具调用逻辑、参数校验规则、甚至子技能之间的编排顺序。用一个生活化的类比来说Tool 是一颗螺丝Plugin 是一盒带螺丝刀的套装Skill 则是一份“组装书桌”的完整工序说明里面会告诉你先拧哪颗螺丝、用哪把工具、最后怎么检查。Agent 技能体系真正要设计的是“组装书桌”这一层而不是单纯堆螺丝。1.3 我为什么把技能作为第一公民来设计在项目初期我只是把技能当成一个“可以调用的函数列表”后来发现这个定位太消极了。技能实际上是 Agent 的“肌肉记忆”是业务能力和模型能力之间的桥梁它应该有自己的生命周期、版本、属性和测试策略而不该是函数注册表里的一条记录。把技能作为第一公民意味着每一份技能都要有明确的名字、描述、输入 Schema、输出规范、错误码、依赖关系、版本号。Agent 拿到用户请求后先通过一个“意图路由”判断该调用哪个技能再由技能自己决定内部怎么执行。这样一来模型只做两件事理解用户意图、选择合适的技能。剩下那些有固定逻辑的步骤比如格式转换、参数校验、接口调用全部交给代码执行。这样拆分还有一个直接好处可测试性。纯靠模型“即兴发挥”的能力几乎无法自动化验证但技能一旦变成结构化组件就可以对每个技能单独跑单测、做回归、看成功率指标。后面我会详细讲测试和评估体系这里先记住一个结论技能设计得越结构化Agent 系统越能像普通软件工程一样被管理和维护。2. 技能系统骨架接口协议、注册中心与技能仓库确定“技能是一等公民”之后下一步就是搭建骨架。这一章我会讲我在项目X里具体落地的协议格式和工程结构你直接照着抄也能跑出一版能用的原型。2.1 技能描述文件让模型看懂也让代码看懂每个技能都有一个独立的描述文件通常是 YAML 或者 JSON 格式。这个文件同时服务两个读者一是模型它需要靠描述来理解技能的适用场景二是代码它需要靠结构化的字段来完成路由、校验、调度。描述文件里至少要有这几部分name技能唯一标识全小写下划线命名比如query_order_statusdescription一段面向模型的自然语言说明重点写清楚“什么时候该用它、什么时候不该用它”input_schema输入参数的 JSON Schema定义字段类型、是否必填、取值范围output_schema输出结果的结构化定义同样是 JSON Schemaversion语义化版本号升级不兼容功能时必须升主版本capabilities技能依赖的执行能力列表比如“需要访问订单数据库”timeout_ms最大执行时长防止技能卡死拖垮整个 Agentcost_hint成本提示比如是否调用大模型、是否有外部付费 API写这份描述文件的时候最容易犯的错是把 description 写得像功能说明书比如“该技能用于查询订单状态参数是订单号”。这远远不够。模型做意图判断基本只靠 description所以要写场景、边界和反例。比如一个订单查询技能更好的描述是“当用户询问订单是否发货、物流进展、签收状态时使用本技能。输入订单号返回当前状态与时间线。如果用户只是在问退货规则不要使用本技能那是另一个技能的责任。”这样的描述才真正帮助模型做决策。2.2 参数 Schema 为什么比自然语言描述更重要很多初学者会把参数说明也写在 description 里这是后来踩坑最多的地方。模型生成参数时不会去“读”自然语言描述里的细微约束但它会老老实实按照 JSON Schema 生成结构。所以参数定义必须足够严格严格到代码可以不依赖模型“自觉”。看一个对比。自然语言写法“订单号是一个很长的字符串通常是字母和数字混合注意不要漏掉前缀。”模型很容易生成一个缺失前缀、甚至带空格的参数。而换成 JSON Schema 的写法{ type: object, properties: { order_id: { type: string, pattern: ^ORD-[A-Z0-9]{12}$, description: 订单号格式为 ORD- 加 12 位大写字母或数字 } }, required: [order_id] }模型看到这个 Schema输出的准确性会大大提高更重要的是就算模型输出了不符合格式的内容代码层的校验器也会直接拦截而不是把错误参数发到下游系统。参数校验是技能体系的第一道安全防线一定不能省。2.3 技能注册与发现从一个装饰器开始描述文件只是元数据真正让技能生效的是注册机制。我的做法是让每个技能实现一个统一的 Python 接口核心就是一个装饰器加一个执行函数。skill( namequery_order_status, description当用户询问订单是否发货、物流进展、签收状态时使用本技能。 输入订单号返回当前状态与时间线。查询退货规则时不要使用。, input_schemaorder_status_input_schema, output_schemaorder_status_output_schema, version2.1.0, timeout_ms5000 ) def query_order_status(params: dict) - dict: order_id params[order_id] # 这里做实际的业务调用比如请求订单服务 return {status: shipped, timeline: [...]}注册中心启动时会扫描所有带skill装饰器的函数收集它们的描述文件构建一个技能索引。模型每次发起调用时实际拿到的是这份索引的子集而不是全部技能。这就回到了第一章说的“减负”逻辑无关技能根本不进入模型视野选择准确率自然上升。2.4 技能仓库的分层与命名规范随着技能数量增长我很快发现不能把所有技能平铺在一个目录里否则找起来和排查起来都很痛苦。项目X里的技能仓库分成了三层基础技能层与具体业务无关的通用能力比如 HTTP 调用、JSON 解析、时间格式化领域技能层面向某条业务线比如订单域、库存域、用户域这一层的技能通常会调用基础技能场景技能层面向完整用户任务的高层编排技能比如“处理售后申请”它会调度订单查询、退款计算、客服工单等多个领域技能命名上基础技能用domain_action的格式领域技能用domain_scenario_action的格式比如order_query_status、order_refund_calculate。这样从技能名就能反推出它属于哪一层、哪个域日志排查的时候省很多事。3. 技能编排与调度让多个技能协作不打架技能不是孤岛。真实的用户任务几乎都需要多个技能按顺序或并行协同完成。这一章我会讲编排层的几个关键设计决策。3.1 单技能调用链输入规范、超时与重试每个技能内部可以有清晰的执行步骤。以“处理售后申请”为例它的执行链是解析用户诉求 → 查询订单状态 → 查询退款规则 → 计算退款金额 → 写入申请工单。每一步都可能失败所以我在技能执行函数里不做“裸调用”而是统一走一个内部执行上下文自动带上调用链 ID、超时控制和重试参数。超时是必须显式声明的。一个查询订单的技能如果 5 秒没返回说明下游大概率有问题重试 1 次即可但如果是一个需要人工异步审批的技能就不该设置短超时。每个技能在自己的描述文件里声明timeout_ms编排层严格遵守绝不默认给所有技能同一个超时值。重试的时候要注意幂等性。查询类技能可以放心重试但写入类技能不行。一个稳妥的约定技能描述文件里设置idempotent: true/false编排层看到写入类技能时重试前必须生成一个新的幂等键并且把原始请求 ID 传给下游系统。3.2 多技能并行调度依赖关系与并发上限多个技能之间的依赖我画成了一个 DAG有向无环图。比如处理售后需要先拿到订单状态再并发调用退款计算和客服规则校验最后汇总结论。这里有两个我吃过亏的点。第一点不要一味追求并行。并行确实能省时间但如果两个技能并发操作同一个下游资源可能造成数据竞争。我的原则是无依赖且读多写少的技能可以并行存在资源竞争的技能必须串行。第二点并发优先级要分档。某个技能如果属于“快速失败”类型即失败后整体流程无需继续那它就要先跑让后续技能尽早被阻断。把最可能失败的检查放在最前面是编排层最重要的“省钱”策略因为模型每多走一步就多花一份 token。下面用配置表达一个简单编排规则workflow SkillGraph( after_sale_handle, nodes[ (query_order, dict(kindmust, timeout5000)), (query_refund_rule, dict(kindmust, timeout8000)), (check_customer_level, dict(kindbest_effort, timeout3000)), (calc_refund, dict(kindmust, timeout10000, depends_on[query_order, query_refund_rule])), ] )注意有个技能是best_effort类型意思是它失败不影响主流程比如查询用户会员等级只是为了给客服一个备注。这一招在成本控制上非常有效因为不是所有信息都值得让 Agent 等待。3.3 成本与“注意力预算”别让 Agent 一次带太多技能如果说超时控制是防拖慢那么注意力预算就是防“选择瘫痪”。我在项目早期让一个 Agent 同时持有 30 多个技能定义结果模型开始频繁在相似技能之间犹豫有时还会调用一个语义相近但并不是最优的技能。后来我引入了“注意力预算”机制每次对话开始先通过一个轻量的意图分类器粗筛出候选技能数量控制在 5 到 8 个之间再把这些候选技能的描述传递给模型做最终选择。粗筛本身可以是很简单的关键词匹配或者一个小的分类模型成本极低但它大幅提升了主模型的选型准确率。实测下来技能数量从 30 个降到 8 个后工具调用的准确率从 71% 提升到 94%。这个数字非常直观地说明了一个道理不是给模型的信息越多越好而是给得越精越好。3.4 技能回退与逃生通道再强的技能体系也有失败的时候。我学到的一个关键教训是永远要为用户保留一条不经过任何技能的“纯对话”路径。技能全挂的时候Agent 至少要能说出“当前系统暂时无法处理这个请求你可以稍后再试或者联系人工客服”而不是反复报错或者生成一个假成功。实现方式上我在编排层设置了全局 fallback技能执行失败超过一次后不再让 Agent“硬想”其他技能直接进入人工兜底分支。这里还有一个容易被忽略的细节用户可能在技能执行中途直接输入新意图。比如 Agent 正在处理退款申请用户突然说“算了我还是先问一下物流”。此时必须允许技能中断把当前上下文保存下来或丢弃而不是强行把对话拉回原来的执行流程。技能编排的灵活性往往体现在这些边界情况的处理上。4. 可观测性从“技能消失”到“一眼定位”技能体系一旦跑起来最大的噩梦就是用户在对话里说“刚才你查到的订单数据不见了”或者“你明明说要发送邮件但邮箱里什么都没有”。这已经不是模型能力问题而是系统可观测性不足。我在这部分投入的精力甚至比写技能本身还多。4.1 技能调用的标准日志格式每一份技能日志都必须是结构化 JSON而不是自由文本。项目X里统一了一套最小字段集{ ts: 2025-06-01T10:24:00.123Z, trace_id: tr_8f3b2e, skill: query_order_status, skill_version: 2.1.0, action: query_order_status, status: success, latency_ms: 342, input_digest: order_idORD-AB1234567890, error_code: , token_cost: 0 }看到一个技能报错只需要捞日志就能知道是参数问题、超时问题还是下游接口问题。input_digest字段是我后来加的用来快速定位“同一个用户反复报错是不是因为某个固定参数”而不需要把全量输入打到日志里兼顾排查效率和隐私安全。4.2 链路追踪要从入口就带上一次用户请求可能触发多个技能技能之间还有依赖。没有链路追踪的话出了问题根本没法回答“这么多技能调用里到底是哪一个失败导致最终结果不准”。我的做法是在对话入口就分配一个trace_id后续所有技能调用、模型调用、外部 API 请求都携带这个 ID。每一个技能内部再分配span_id父子关系通过parent_span_id连接。这本质上是一场微服务领域的 trace 实践用到 Agent 场景里同样成立。虽然不能用图表画出完整调用链但用文本树也能判断问题所在。最常用的排查动作是按trace_id搜出所有日志按时间排序找到第一个status ! success的记录基本就是问题源头。4.3 技能级指标成功率、延迟、Token 消费日志能帮你找原因指标能帮你抓趋势。我在监控面板上重点盯三个指标技能调用成功率按技能维度统计特别关注近端是否出现断崖式下跌技能执行延迟P50、P95 两条线一起看P95 上涨通常意味着某个下游接口开始变慢Token 消费按技能维度统计用来发现那些“偷偷多烧钱”的高耗技能Token 消费是最容易被忽视的。有的技能虽然成功率很高但它每次调用都要回传大量格式化文本导致整体账单一个月涨了 40%。后来我把这些“富文本”输出技能全部改成精简 JSON 输出把格式化工作挪到前端或者后续步骤成本立刻降下来。4.4 技能 CLS 与回归看板每轮迭代都会改技能描述或参数 Schema一不小心就会把一个原本表现得不错的技能改坏。我在项目X里维护了一组“金标任务”每个任务会指定标准答案和允许的偏差范围。每次技能变更后自动跑一遍金标任务对比本轮结果和上一轮结果任何回退都会被标记出来。这套回归看板不是说“AI 回答得顺不顺”而是把每个技能的关键行为量化。比如“query_order_status 必须返回 status 字段”“after_sale_handle 遇到不满足条件订单时必须返回 refusal 字段”。有了这些可验证的行为标准技能迭代才敢放开手脚。5. 测试、评估与可持续演进很多人以为 Agent 项目不需要“正经”测试因为模型输出是概率性的。这个想法在只有一两个技能时勉强说得通但在几十个技能协作的系统里完全不可行。技能是可编程的组件就理所应当接受工程级的质量保障。5.1 单元测试要写在 Agent 测试之前每个技能都有一组单元测试重点覆盖三类情况参数正确时的正常返回、参数非法时的错误信息、下游服务异常时的降级表现。这听起来像后端开发的基本功但 Agent 项目里很多人跳过了这一步直接对着对话框测效果。写技能单测时我不建议真的请求外部真实服务。用打桩的方式把依赖服务替换成本地假实现保证测试稳定可重复。测试用例里还会故意传入一些“边缘参数”比如空字符串、超长字符串、缺失必填字段确保参数校验器真的在起作用。不要试图把“模型理解能力”放进技能的单测里。那是评估体系的职责不是单测的职责。单测只验证代码逻辑模型能力验证交给下一节。5.2 用一组“金标任务”卡回归金标任务是我前面提到的回归看板的底座。构建金标任务时最重要的是“答案可判断”不要写“回答得好不好”这种泛泛标准而是明确约束“必须包含哪些字段”“必须调用哪个技能”“禁止输出哪些内容”。我给金标任务设计了三类断言技能调用断言某个场景必须命中指定技能禁止命中另一个相似技能输出内容断言返回结果必须包含指定字段或者必须不包含某些内容行为顺序断言多技能任务里某些技能必须先执行比如查询订单必须在计算退款之前这套金标任务每次技能升级后都会跑一遍。改技能描述之前先跑一轮得出基线改完再跑一轮对比差异。差异里有一部分是“行为变了但结果更好”需要人工确认接受但凡是结果变差的一律必须回滚或者修正。5.3 版本兼容与灰度升级技能是有版本的升级不能靠“改了代码直接上线”尤其是外部业务方依赖这些技能的时候。我采用的做法是同一份技能保留两到三个版本默认路由到新版本同时维护一个“仅面向内部测试”的旧版本通道。灰度升级时先让新版本承接 10% 的流量持续观察成功率指标和延迟指标如果没有异常再逐步提升到 50%、100%。一旦新版本出现明显回退就立刻把流量全部切回旧版。这个流程不需要复杂的平台支撑一个version_router配置就能搞定。version_router { query_order_status: { 2.0.0: 0.0, 2.1.0: 1.0 } }灰度期间出现的问题往往要结合链路追踪日志才能定位所以上一章讲的可观测性是灰度策略能够安全执行的前提。没有日志和指标就没有灰度因为它们分不清问题出在新版本还是无关因素。5.4 废弃技能与淘汰机制技能多了以后一定会出现“僵尸技能”上线时轰轰烈烈三个月后根本没人调用或调用量极低。僵尸技能占着注意力预算拖慢模型选型速度还偶尔会被模型误召。我给自己定了一条规则每两周看一次技能调用量统计调用量连续一个月低于阈值的技能进入“待废弃”状态不再参与粗筛和注意力预算分配再过一个月仍然没有恢复就从注册中心移除。移除前要检查历史对话记录里是否有人提到相关功能避免误删仍有需要但没走技能路径的场景。淘汰机制听起来像运营工作但它对系统的稳定性贡献非常大。每次清理一批僵尸技能后我都能明显感觉到模型在剩余技能上的选择准确率略有上升也许 1% 到 2%但积少成多值得坚持。6. 实操中踩过的坑与排查技巧最后分享一些我在项目X里真实踩过的坑。这些不一定能在教科书里找到但如果你也在做 Agent 技能体系应该能帮你少走很多弯路。6.1 模型就是不按描述文件调用技能现象技能描述写得清清楚楚参数 Schema 也严格定义但模型依旧我行我素要么不调用技能要么传错参数。排查第一步不是改提示词而是确认模型到底有没有“看到”这份描述文件。我遇到过几次原因是技能描述太长被 Agent 框架的上下文窗口截断了。后来给描述文件加了“前端摘要”字段核心决策信息放在前面详细示例放后面模型反而调用得更精准。如果模型确实看到了描述但还是不调用很可能是存在另一个技能在描述上“抢生意”。检查候选技能集合里是否有两个描述高度相似的技能给它们各自的 description 加“互斥条款”。这类互斥描述的效果比我反复强调“不要混淆”好得多。6.2 多个技能返回冲突Agent 自己吵起来现象用户问“订单什么时候发货”技能 A 返回“已发货”技能 B 却因为缓存数据返回“未发货”。Agent 最终把两个矛盾结论都告诉用户用户一脸困惑。这类问题的根源不在模型而在数据一致性。技能 A 读的是订单主库技能 B 读的是缓存两者有延迟。我最终的解决方式是在技能定义里加了一条“数据源优先级”注释并让技能 A 这类主数据查询技能标记为authoritative: true编排层在遇到冲突结果时直接优先采用权威数据源的结果。另一个方法更简单在技能输出 Schema 里加一个confidence字段模型在汇总多个技能结果时必须优先采用高置信度的数据。但要注意这个字段不能靠模型自己拍脑袋最好由技能代码根据查询目标和结果完整性来计算。6.3 技能数量一多响应延迟反而上升现象从日志看每个技能本身执行都很快但整个 Agent 响应却越来越慢。很多时候问题出在“潜在技能过多导致选择耗时”模型要在大量候选技能里做决策每增加一个候选都会显著增加决策时间。我把技能分成两层粗筛第一层用关键词和意图规则收窄范围第二层才把候选技能塞给模型。粗筛阶段不做语义分析只做字符串匹配和规则匹配成本几乎为零。实测效果极其明显首字响应时间从平均三秒降到一点二秒。还有一个隐藏原因技能代码初始化耗时。有些技能在 import 时就做了重资源加载比如建立数据库连接池、加载大字典文件。解决办法是把这些重资源移到首次调用时懒加载或者放到独立进程中预热。6.4 更新技能后旧会话还在用旧行为现象技能升级后存量对话的表现还是以前的行为新会话正常。原因是这些旧会话已经把旧的技能描述放进了上下文里新的技能定义没有被重新注入。解决办法是给每个会话记录一个skillset_version当注册中心发现技能集合版本变化后让存量会话在下一次请求时自动刷新技能列表丢弃旧的技能描述。这个机制的代价是旧会话会丢失一部分上下文但这个代价换来的行为一致性通常更值得。顺便提一个容易被忽略的点如果你在技能描述里写了“请返回 JSON 格式”但技能代码层面并没有强制格式化输出那模型偶尔还是会把回复写成自然语言。经验是所有确定性要求都必须在代码层校验和强制不能依赖模型的“自觉”。这可能是整个技能体系里最实用的一条经验。我在实际项目里最深的体会是Agent 技能体系的本质不是“给模型更多能力”而是“给模型更少的选择、更清晰的边界、更可靠的后端”。设计技能时多花在协议、可观测性和测试上的时间都会在线上稳定运行之后成倍地回报回来。这套东西后续还可以继续扩展比如让技能支持自主编排生成子流程、根据用户画像动态装配技能集合、或者在多 Agent 场景里共享一套技能库。但不管怎么扩展底层那套“结构化定义加严格校验加完善观测”的骨架始终值得先打牢。