
1. 为什么 Agent 需要技能化1.1 从模型直接回答到模型调用工具的演进我最早接触 Agent 时用的还是纯提示词方案把所有业务逻辑写进 System Prompt让大模型记住该怎么做。小规模 demo 还好一旦业务复杂起来就彻底失控——Prompt 动辄几千字模型经常记错步骤偶尔还会凭幻觉编造执行结果。那段时间我最大的感受是模型不是数据库也不是流程引擎你没法靠叮嘱让它稳定地完成多步骤操作。后来转向工具调用Function Calling方案情况有所好转。把一个个独立操作封装成函数模型自己决定什么时候调用。这个思路的核心是把模型不确定的部分交给代码而该做什么的决策权仍保留给模型。实际跑下来能明显感觉到凡是封装成工具的环节准确率大幅提升而留在 Prompt 里的逻辑依然容易出岔子。但工具方案也有它的天花板。工具之间是平级的模型每次只能选一个来调用缺少多个工具按顺序配合的机制。比如我想让 Agent 完成查询订单→判断是否超时→发送催付消息这个流程工具化之后 Agent 可能只调了第一个工具就停住了。这不是模型笨而是工具本身没有流程感——单个操作很清晰组合逻辑却没人管。真正的转折点是我开始尝试技能化。把一组相关的工具、执行逻辑、参数规范、使用场景打包成一个可以被 Agent 调用的整体单元这就是我理解中的 agent-skills。技能不是工具的替代品而是工具之上的组织层一个技能可以包含多个步骤、多个工具调用甚至嵌套其他技能。它把模型要做什么和模型该怎么做彻底分开了。1.2 技能与普通工具、插件到底有什么区别很多人分不清技能、工具、插件这三者我在项目初期也踩过概念混淆的坑。简单说工具是最小可执行单元通常对应一个函数或一次 API 调用插件是工具的集合包偏重集成技能则是面向任务的逻辑封装描述的核心是何时用、怎么用、用完之后要产出什么。用一个生活化的类比来理解工具像螺丝刀插件像一整套工具箱而技能像换灯泡操作手册。手册告诉你需要拿螺丝刀工具、需要先断电条件、拆几个螺丝步骤、装好后怎么验证结果。Agent 调用技能时其实是在执行一份操作手册而不是单纯举着螺丝刀到处拧。这个区别带来两个关键变化。一是抽象层级不同工具调用只解决怎么执行单个动作技能解决怎么完成一个目标。二是决策负担不同工具模式下模型需要自己规划每一步调用顺序容易出错技能模式下模型只需决定这个任务匹配哪个技能执行细节由技能内部代码接管。实测下来后者对长尾任务的成功率提升非常明显。1.3 技能化解决的核心痛点我总结下来技能化主要解决了四个让我头疼很久的痛点。第一个痛点是提示词膨胀。以前每个任务都要往 Prompt 里塞一堆步骤说明、边界条件、输出格式塞多了模型反而不听话。技能化之后这些细节从 Prompt 移到了技能定义文件里Prompt 只保留哪些技能可用的索引。Prompt 从 3000 字缩到 300 字模型选择准确率反而上升了。第二个痛点是逻辑复用困难。很多任务之间存在公共步骤比如获取当前用户上下文在多个 Agent 流程里都会用到。普通工具虽然也能复用但整个如何基于上下文判断下一步的逻辑没法复用。技能允许嵌套把公共逻辑抽成子技能改动一次全局生效。第三个痛点是测试没有边界。没有技能化之前改一个环节要回归整个流程测试成本极高。技能化之后每个技能独立测试再测技能之间的集成关系边界一下子清晰了。第四个痛点是可观测性差。技能内部有明确的步骤结构每一步的执行时间、成败、输入输出都能记录排障时不用再对着模型的一整段输出瞎猜。2. 技能库整体设计从入口到调用的分层架构2.1 技能描述层让 Agent 看懂技能我早期做技能化时只写实现代码结果发现模型根本不理解这个技能什么时候该用。后来意识到技能必须先解决可被理解的问题——模型看到技能列表时需要快速判断哪个技能匹配当前任务。我把这部分设计成独立的描述层和实现代码完全解耦。描述层包含技能名称、用途说明、参数模式、返回格式、使用约束、示例。难点在于用途说明怎么写才有效。写得越抽象模型越容易误匹配写得越具体覆盖的场景就越窄。我试过几十种描述风格最后总结出一句话经验描述里要写清楚预期效果和触发条件必要时给一个真实输入输出样例。比如一个订单催付技能与其写处理订单催付不如写用于订单已支付但未发货且距支付时间超过 48 小时的情况调用后发送催付消息并记录通知状态。参数模式也要在描述层明确。我习惯为每个参数标注类型、必填性、取值范围、示例值。这里最容易踩的坑是模型调用时会把自然语言里的数字转换成参数值如果参数定义含糊它就会传错格式。有一个真实教训某时间参数我定义成了字符串类型没有限定格式结果模型一会儿传2024-03-15一会儿传十五号中午下游解析直接崩了。后来统一改成标准时间戳格式并在描述里反复强调问题才消失。2.2 技能实现层可编排的执行逻辑描述层解决该不该用实现层解决用了怎么执行。我把实现层设计成有限状态机加步骤管线一个技能内部由若干步骤组成步骤可以是代码函数、外部 API 调用、甚至是另一个技能。步骤之间支持顺序执行、条件分支、循环和并行分发。状态机的优势在于每个步骤的输入输出都明确Agent 拿到节奏的掌控感。比如催付技能内部可能分为校验订单状态→生成催付文案→选择发送渠道→发送→记录结果。每一步都可能失败我需要在设计上明确失败后的处理策略是直接中断并返回错误还是走降级分支。这里我强烈建议不要让技能内部静默吞掉错误一定要把失败原因结构化地抛给上层。实现层的另一个设计重点是上下文传递。步骤之间共享哪些数据、哪些数据需要隔离都要提前想清。我吃过一次亏两个并行步骤同时读写同一个全局字段结果出现数据竞争Agent 返回的结果时不时就串了。后来的方案是每个步骤声明自己的输入上下文和输出上下文由调度器负责安全地传递和合并并行步骤之间只能通过显式声明的共享区交换数据。2.3 技能注册与索引层动态发现与版本管理技能数量少的时候全量加载到 Prompt 里没毛病。一旦技能超过 20 个Prompt 塞不下了模型的选择准确率也会明显下降。解决思路是加一层技能注册与索引每个技能注册时带上索引标签模型先通过检索找到候选技能再在候选集内做选择。我用的是双阶段流程粗粒度标签过滤 细粒度语义匹配把候选技能控制在 5 个以内。索引标签的设计直接影响召回效果。我给标签分了几类领域标签如电商、客服、数据、动作类型标签如查询、发送、计算、实体标签如订单、用户、商品。实际使用中发现标签体系不要太追求完美覆盖宁可多打几个粗标签也不要漏打关键标签。漏标签意味着技能永远不会被检索到等于白写了。版本管理这块我吃过亏。迭代技能时直接覆盖定义结果线上 Agent 的行为忽然变了还找不到原因。后来我强制引入语义化版本号技能定义里的每个变更都要升级版本并在注册中心保留历史版本。某个版本出问题可以快速回滚。另外上线前我会用一组金标用例来回归测试用例集固定不变技能版本更新后必须跑通全部用例才算合格这个习惯帮我挡住了不少低质量变更。3. 实操定义你的第一个技能3.1 技能定义文件怎么写先说结论技能定义最好用独立文件管理不要硬编码在代码里。我目前采用的格式是 YAML原因是可读性好非开发同事也能参与维护。一个技能定义文件的核心字段包括name、description、version、author、input_schema、output_schema、steps、tags、examples。其中steps是技能的核心逻辑其余是元信息和契约。一个最简单的例子定义一个获取商品详情技能name: get_product_detail description: 根据商品ID获取商品详情包含标题、价格、库存、上架状态。适用于用户查询商品信息或下单前核对商品状态的场景。 version: 1.2.0 tags: [电商, 商品, 查询] input_schema: product_id: type: string required: true description: 商品唯一标识形如 SKU-2024-001 output_schema: success: type: boolean title: type: string price: type: number stock: type: integer on_sale: type: boolean steps: - id: validate_id type: function function: check_product_id_format - id: fetch_detail type: api endpoint: /api/products/{product_id} method: GET - id: format_result type: function function: format_product_payload examples: - input: 查看商品 SKU-2024-001 的信息 output: 返回商品标题、价格、库存、上架状态这个文件直接告诉模型三件事这个技能做什么、需要什么参数、执行完给什么结果。模型不需要理解内部fetch_detail到底怎么请求只要知道传入product_id就能拿到结构化商品数据。实际操作中有一个很重要但容易被忽略的点description的措辞不要和别的技能太像。我曾经定义过两个技能一个叫获取用户近期订单另一个叫查询用户购物记录描述高度相似结果模型频繁选错。后来把两者合并成同一个技能并加了维度参数准确率立刻回升。3.2 参数设计要注意的细节参数设计是技能可用性的命脉我在这一块吃的亏最多。总结几个值得反复检查的细节。第一参数名要直白。product_id一眼能看懂但pid、prodId这种缩写就很容易让模型困惑。虽然模型能猜但猜就有出错的可能。技能是给模型用的接口命名风格要贴近自然语言而不是贴近程序员习惯。参数名和描述中的用词保持一致模型定位参数的语义准确率会高很多。第二必填参数尽量少。模型传参的本质是从对话中抽取信息信息缺失时它要么编造要么报错。如果某个参数只是增强功能就给它一个默认值不要设为必填。例如查询订单接口的time_range参数默认近 30 天即可。第三枚举值一定要显式声明。比如send_channel参数只有sms、email、in_app三个可选值就一定要在 schema 里写成enum。不写的话模型可能传一个push_notification进去下游匹配不上直接抛异常。显式枚举还有一个附带好处模型对候选选项的理解比自由填空更可靠这也是某种意义上的选择困难缓解。第四参数描述要写业务含义不要只写类型。product_id的描述写成商品唯一标识形如 SKU-2024-001比商品ID准确得多。模型靠描述做语义映射描述越具体映射越稳定。3.3 技能间依赖与复用技巧技能可以嵌套调用这是它比工具强很多的地方。我有两个习惯一是把高频公共逻辑沉淀为底层技能让上层技能引用二是用依赖声明明确技能间的关系避免隐式调用导致的混乱。依赖声明很简单在技能定义里加一个depends_on字段列出依赖的技能和版本范围。调度器在加载技能时检查依赖是否满足不满足直接拒绝注册。这个机制帮我省掉了很多莫名其妙的线上故障——以前某个底层技能改了参数格式调用它的 20 个上层技能全部悄悄报错排查了整整两天才定位到。加了依赖版本声明后底层技能升级会触发上层技能的回归测试提醒问题能在发布前被发现。复用技巧方面我强烈建议把决策类逻辑作为子技能抽出来。比如一个判断订单是否满足催付条件的技能内部封装了超时判断、金额判断、用户偏好判断上层技能只需要调用它并拿到should_pay_remind布尔值。这样做的好处是判断逻辑只维护一份不同上层技能得到一致的行为。模型也更容易理解——它不用自己推理复杂的条件组合只需要调用一个很会判断的技能。依赖过深也有风险。我遇到过最长的嵌套链路有 5 层一次调用牵动几十个子技能单次执行耗时撑了近 20 秒。后来我给技能调用加了最大深度限制并提请设计者反思链路超过 3 层时是不是该把某些中间层合并技能粒度不是越细越好核心标准是这个技能是否具备独立的业务语义。4. 技能编排让 Agent 学会组合拳4.1 线性编排与条件分支技能编排是我后期投入精力最大的部分。最初每个技能只做一件事Agent 经常完成了第一步就停下来没有接下来该做什么的意识。后来我意识到必须把多步动作本身也交给技能来做于是开始设计编排型技能。最基础的是线性编排步骤 A → 步骤 B → 步骤 C顺序执行。这种模式适合步骤之间没有依赖分支的流程。我在定义文件里用steps的数组顺序表达每个步骤引用一个子技能或内部函数。一个典型的客户投诉处理技能就是线性编排读取投诉内容 → 核验订单 → 判断责任方 → 生成回复。条件分支稍微复杂一点。我增加了condition表达式和branch定义让技能根据中间结果走不同分支。举一个实际例子售后审核技能里如果订单金额小于阈值走快速退款分支如果金额较大走人工复核分支。条件表达式的判断结果来自前序步骤的输出字段调度器会基于这些字段决定下一步走向。条件分支有个设计陷阱分支条件必须互斥且完备。我遇到过两个分支条件重叠的情况同一笔订单同时满足超时和可退款两个条件调度器不知道该走哪个分支最后默认走了第一个而第一个分支的逻辑恰好是错的。解决方案是给条件表达式增加优先级并在设计阶段强制检查所有分支条件的联合覆盖是否等于全集这样就不会出现无路可走的输入。4.2 递归调用与子技能编排里最有价值的设计之一是递归调用。一个技能可以调用自己这在处理树形结构任务时特别有用比如组织架构查询、多级菜单遍历、评论回复链路分析。给你一个场景用户问这个商品包含哪些子规格。商品体系有多级类目直属子类和孙类结构不同。我设计了一个展开子类目技能它先查询当前类目的直接子类再对每个子类递归调用自身。递归深度到叶子节点后返回最后把所有层级的子类汇总。这样技能代码只需写一次却能处理任意深度的类目结构。不过递归调用一定要写终止条件。我最开始忘了限制最大深度结果商品类目里有个循环引用管理员手动配错导致的技能直接陷入死循环。后来我加上两条防线一是在调度器层面限制全局最大深度的硬编码二是在技能实现里显式检测已访问节点集合发现重复节点立即终止并返回错误。这两条防线缺一不可因为调度器的最大深度只能在极端情况下兜底而业务层的重复检测才能真正阻止逻辑错误蔓延。子技能还有一种用法是并行扇出。比如要批量核对 100 条订单状态如果逐条调用耗时是串行的我把它设计成一个编排技能内部将订单列表分组后并行调用单条核对子技能全部完成后汇总结果。这个优化把处理耗时从 300 秒压缩到 40 秒左右收益非常可观。注意并行扇出的子技能之间必须没有数据依赖否则就得改成有依赖关系的两轮并行。4.3 编排冲突的处理循环检测与超时编排技能多了之后最怕出现循环依赖和调用冲突。我曾经设计了一套自动推荐编排先调获取用户兴趣技能再根据兴趣调推荐商品技能而推荐商品又调了获取用户兴趣作为辅助信息。这在逻辑上并不是死循环因为后一次调用只是取缓存但还是让我惊出一身冷汗。实现层我再三确认后决定在调度器里统一做循环检测用有向图记录所有技能调用关系每次部署前跑一次拓扑排序发现环就拒绝上线。超时机制也必须有。每个编排型技能都应设置总体超时时间超时后统一返回执行超时错误而不是让调用方无限等待。我常用的是超时阈值 预估正常时长的 3 倍保证足够宽裕又不会卡死整个流程。子技能也应有自己的超时且子技能超时要能被父技能捕获并触发降级分支而不是直接崩掉整个编排。我还遇到过一个很有意思的问题Agent 在技能编排中产生了选择冲突。同一个任务被两个技能都声称可以处理模型犹豫不决返回了模棱两可的响应。排查后发现问题出在技能描述上——两个技能都写了适用于用户退款相关咨询。我给其中一个技能加上更明确的边界条件描述并降低另一个技能的匹配优先级冲突就消失了。这个经验值得记住技能描述里使用仅当……时使用句式可以显著减少模型的选择摇摆。5. 技能测试与评估别让 Agent 乱调5.1 单技能单元测试技能化之后测试变得前所未有的清晰。每个技能都是独立单元可以给它构造输入、断言输出。我的测试策略分两层第一层是纯逻辑测试不经过模型直接调用技能实现函数第二层是模拟模型调用测试用预设的自然语言输入作为 Agent 用例验证描述层的匹配是否准确。单元测试的用例设计要覆盖几种典型路径正常输入、边界值、缺失必填参数、非法枚举值、依赖技能抛错。尤其要测参数缺失时的报错信息是否友好因为模型可能会在漏信息时收到这个报错清晰的报错能让模型自己纠错并重新调用。我在报错信息里加上了可操作的纠正建议比如缺少参数 channel请从 [sms, email, in_app] 中选择。实测下来模型看到这种错误后自动重试并成功的概率非常高。模拟模型调用测试是另一道保险我去真实对话环境里采集了一批用户问题作为固定测试集输入给技能选择器看模型最终选择哪个技能、传入哪些参数。测试不关心答案内容只关心两点选择是否命中预期技能、参数是否映射正确。这个测试集每次技能版本更新都会全量跑一遍跑不过就不允许发布。5.2 多技能集成测试单个技能正常不代表组合起来正常。集成测试主要验证三件事技能间数据传递是否正确、依赖版本是否兼容、编排路径是否按预期执行。数据传递问题最常见。上层技能调用子技能后需要从子技能的输出里提取字段。字段名不匹配、类型不一致都会导致下游步骤失败。我在集成测试里专门加了一层契约检查每个子技能调用完成后自动校验输出是否符合被调方声明的output_schema不符合就直接标记测试失败。有了这层契约检查字段名写错的问题基本能在集成阶段暴露。依赖版本兼容测试也值得投入。我会在测试环境里把底层技能升级到最新版本然后跑一遍所有依赖它的上层技能测试集。如果某个上层技能断言失败就说明两个版本的契约发生了破坏性变更需要排查是底层技能改了输出格式还是上层技能用了旧字段。这个流程保证技能可以放心升级不会被隐藏的兼容性问题绊倒。编排路径验证上我用了追踪记录的方式测试执行时把每个步骤的实际走向记录下来跟预期步骤序列做比对。例如预期售后审核技能应该走快速退款分支但实际走向了人工复核测试就会捕获这个差异。这种方式能帮我快速发现条件表达式逻辑问题尤其是边界条件判断错误。5.3 评估指标成功率、召回率、误用率光靠测试用例还不够产品上线需要衡量技能调用的整体表现。我习惯跟踪这四类指标技能选择准确率、参数映射准确率、任务完成率、误用率。技能选择准确率 正确命中的技能数 / 总请求数。这个指标反映描述层写得好不好。参数映射准确率 正确映射参数的请求数 / 命中技能的总请求数反映input_schema的定义质量。任务完成率 用户问题得到有效解决的请求占比可以通过用户的显式反馈或运营回访来估算。误用率 技能被调用但实际不符合应用场景的请求占比这个指标非常关键因为 Agent 一旦乱调技能虽然后续还能兜底但用户体验会明显变差。我拿过一组真实数据优化描述层之前选择准确率只有 78%误用率高达 15%。我把描述重写了一遍改用仅当……时使用的句式并补充真实样例准确率提升到 93%误用率降到 4%。这说明大部分问题出在模型理解偏差而不是模型能力不够。好的技能描述就是模型的照明灯越具体越亮越含糊越容易撞墙。评估不是一次性任务。我建议把评估作为日常流程固定下来每两周跑一次全量测试和指标统计对比上次结果找出回退项。回退通常意味着某次修改引入了隐性副作用比如改了一个子技能的输出格式导致依赖它的父技能逻辑失效。有了固定评估节奏这类问题最长两天内就能暴露。6. 常见问题与排查实录6.1 Agent 不选技能描述和触发条件问题遇到最多的线上问题是Agent 明明有技能可用就是不调用。排查这个问题的思路要分三步先看技能是否被成功检索到再看描述是否让模型觉得当前任务匹配最后看调用示例是否有说服力。检索不到的情况常见于标签打了太多没人搜的词。比如电商场景里打标签fulfillment但用户问题提的是发货模型语义检索时匹配不到。解决办法是标签贴近真实业务用词并且做同义词扩展。描述不匹配的情况也很多尤其当描述写得过于抽象比如处理订单生命周期相关操作模型可能搞不清楚催付到底算不算订单生命周期的一部分干脆不调用了。我把描述改成包含具体动作的词如催付需要给已下单但未付款的用户发送提醒消息并跟踪发送状态模型识别准确率立刻改善。还有一个很容易忽略的原因技能调用的前置条件没交代清楚。模型倾向于在它判断该做的时候调用技能如果技能描述里没写清楚使用的前提条件模型可能觉得自己手头信息不足就退缩了。我的做法是在技能描述里明确写出当出现……情况时使用本技能如果……则不要使用这能大幅减少模型的犹豫。6.2 技能粒度太粗或太细技能粒度是设计层面的老难题。粒度太粗一个技能包罗万象内部代码里全是 if-elseAgent 调用时也搞不清它到底能做什么。粒度太细技能数量爆炸每个技能描述都很短模型选择时信噪比太低检索也困难。我的判断标准是如果一个技能的内部逻辑已经需要大量条件分支去区分不同子任务就该拆了。如果有人改了这个技能要小心翼翼担心影响别的场景也说明该拆了。反之如果两个技能经常被 Agent 同时调用以完成同一目标就该考虑合并成一个编排技能来代替它们各自独立出现。举一个实际案例早期我的技能库里查询订单和查询售后单是两个独立技能但几乎所有售后场景里 Agent 都需要同时调用它们来对比信息。后来我新增了一个售后订单全景查询技能内部一次查询两个单据并合并输出。这之后模型选择更干脆了相关任务的一次性成功率提高了不少。技能粒度调整是常态化优化不是上线时就定型的事。6.3 上下文爆炸与结果污染技能调用会带来上下文管理问题主要体现在两方面一是技能返回结果过大塞进上下文把最重要的信息挤掉二是不同技能返回的同类数据格式不一致互相污染。上下文爆炸的处理经验是所有技能输出都要瘦身。设定输出 schema 只保留必要字段杜绝整表返回。一个查询订单的技能如果默认返回 30 个字段而真正需要的只有 5 个其余 25 个字段就是噪音。我统一做了字段裁剪并把完整原始结果放在可查询的存储里需要时再通过另一个详情查询技能取回而不是一次性全塞给模型。结果污染问题的根源是字段命名冲突。两个技能都返回status字段一个表示订单状态一个表示用户状态上层编排的步骤从两个子技能取数据时就很容易取错。我在设计规范里强制要求每个技能的输出字段加上业务前缀比如order_status和user_status并在output_schema里注明来源。这样字段名一旦冲突测试阶段就能发现不会等到线上才出问题。我还遇到过技能之间通过全局存储间接污染的情况A 技能写了某个缓存键B 技能读取时拿到了 A 写入的中间值导致 B 的输出漂移。排查时极其隐蔽因为两个技能表面上没有依赖关系。解决方案是给所有技能中间数据加上命名空间隔离并且默认禁止读写未声明过的共享键。这个强制规范实施之后类似的隐性数据污染问题就再没出现过。7. 最后分享几点做 agent-skills 的经验前前后后折腾了挺久踩了不少坑有几条比较深的体会想单独说一下。第一技能化不是银弹。它解决的是模型能力边界和业务流程的匹配问题但如果业务流程本身混乱、字段没定义清楚技能化只会把混乱结构化地放大。我建议在开始技能化之前先花时间梳理业务步骤和数据结构哪怕用文档手写出来都比直接写代码强。第二技能描述值得花最多的心思打磨。我见过不少人把精力全放在技能实现和代码优化上却忽略了对模型说话的那几行字。实际上模型能不能正确使用技能描述质量起着决定性作用。我的习惯是每写一个技能先拿 20 条真实用户问题做模拟选择测试选择准确率不过 90% 就继续改描述直到满意再写实现。第三从少数几个高频技能开始不要贪多。技能库里前十个技能往往能覆盖 80% 的高频场景先把这些技能做到极致比追求技能数量重要得多。技能一旦超过几十个维护成本会指数级上升评测、回归、版本管理全都变重。我后来有意识地合并同类项把相似技能收编成带参数的更抽象技能让技能库保持精简。第四日志和追踪是排障的生命线。每个技能调用、每一步执行、每个分支选择、每次参数映射都应该有结构化日志。初期我觉得麻烦没做全出了一个难查的现场事故之后我补全了全链路追踪。从那以后再遇到问题基本能在几分钟内定位到具体技能和具体步骤效率提升非常多。特别是当你准备把技能库逐渐开放给更多 Agent 共用时全链路追踪几乎是必需的基础设施。最后再分享一个小技巧技能定义文件里加一个changelog字段记录每次版本变更的原因和时间。这个字段不需要给模型看但对你维护技能库非常有帮助。半年后回头看某个参数为什么被改成这样那个字段会给你答案。