写这篇的起因是我在某内部工作流项目里反复踩了同一个坑给Agent塞了一堆工具定义模型却总在错误的场景调用错误的工具。直到我把思路从堆工具改成搭技能——也就是把工具、状态、错误处理、使用说明打包成一个完整的能力单元——整个系统的稳定性才真正立住。如果你也在做Agent相关开发或者正在被工具一多就乱选折磨这篇会拆解一套可复用的技能设计方法包括元信息怎么写、参数怎么定、错误怎么兜底、多个技能怎么编排以及我在实测里踩过的高频坑和排查路径。它面向的是那些已经跑通单次工具调用的开发者想从能跑走向能稳定跑。1. skills到底解决了什么问题从工具堆砌到能力封装1.1 只靠提示词堆功能会让Agent失去判断力很多人一开始做Agent都走过同一条路把所有API说明、函数签名、使用注意事项全塞进system prompt觉得自己写得够细模型就能按文档调用。实际效果呢工具少于五六个的时候还行一旦超过十个模型就开始乱点查天气的API被拿去翻译文本下单接口被用来查订单状态最夸张的一次它把删除接口当成重置接口调了。问题根源不是模型不够聪明而是我们给模型的东西粒度不对。一个函数定义function/tool只描述了能做什么但没有说明什么时候该做做了之后会对系统产生什么影响失败之后下一步怎么走。模型在一个巨大的选择空间里做判断准确率必然下降。Skills这层抽象要解决的就是这个判断成本问题。它把一组函数调用、中间状态、前置条件、后置处理、错误恢复策略全部封装成一个技能单元模型只需要决定现在要不要启用这个技能而不是具体该调哪个函数。1.2 技能不是函数状态、循环与人的介入我第一次接触技能框架的时候也困惑这不就是函数吗用着用着才发现差别很明显。函数是无状态的给参数、出结果一次调用就结束了。技能不同它可以在一次执行中多次读取输入可以维护中间状态可以做循环甚至可以在关键节点停下来等待人类确认。举个例子一个报告生成技能内部包含数据拉取、图表渲染、文本摘要、格式校验四个步骤其中图表渲染失败时可以自动重试两次超过次数再请求人工介入。这种流程控制逻辑在一个普通函数定义里根本装不下。所以我在设计技能时会把它看作一个独立的小服务而不只是一个函数签名。它有自己完整的生命周期指令有明确的启动条件和退出条件这样Agent在编排多个技能时才不会把一个技能内部的细节错误当成决策失败来处理。1.3 一次技能调用的完整生命周期把技能的运行过程拆开看大概是这个流程意图感知Agent从用户输入中判断是否需要某个技能这一般靠技能的description命中。参数确认Agent从对话上下文抽取参数组装成技能能识别的结构化输入。技能执行技能模块自己内部跑函数调用、循环、重试等逻辑期间可以请求额外信息。结果返回执行完成后返回结构化结果和状态标记成功、部分成功、失败。闭环处理失败时技能内部先尝试自愈处理不了再返回可控的失败原因摘要给Agent做下一步决策。跟直接暴露函数给Agent的模式相比这个流程最大的变化是中间步骤对模型不可见。模型看到的只有执行前和执行后中间的细节被技能封装掉了。这意味着模型不需要理解底层每个函数怎么工作它的认知负担大大降低选错的方向自然也少了。2. 技能元信息设计名称、描述、参数三件套的写法2.1 名称不是给人看的是给模型做快速分类的技能名称看着简单其实是有讲究的。我见过有人用skill_001getData处理一下这种名字Agent看到这种东西基本等于没看到。技能名称应该遵循动作对象的结构让模型在字母匹配阶段就能建立初步语义联系。举个例子查询订单状态就比order_status_query更适合因为越是自然语言的表达模型在生成调用意图时就越容易命中。命名还有一个容易被忽略的点同一套Agent里技能名称之间要有足够的区分度。比如你已经有一个生成周报技能再来一个汇总周数据技能两个名字的语义边界模糊模型就很容易选错。我一般会检查一遍技能列表把名称重叠度高的技能合并或者明确划分边界——生成周报负责终稿输出汇总周数据只负责原始统计描述里说清楚职责范围。2.2 描述要写触发条件和交付物少写原理技能的description是Agent判断要不要用的核心依据但它恰恰是被我浪费最久的地方。早期我写的描述像产品说明书该模块基于XX算法实现文本向量化支持输入长文本……结果模型全程不带理它。后来我做了一轮修改把描述拆成三个部分When to use何时使用用户提到哪些关键词或者出现什么情境时你应该考虑这个技能。What it does能交付什么执行完成后会返回什么格式的结果包含哪些字段。What it does NOT什么情况别用明确排除边界比如查询天气请勿使用本技能。这样改写之后技能命中率提升非常明显。举个例子原描述本技能用于生成PDF文档支持自定义页眉页脚。修改后当用户要求把内容生成PDF、导出为PDF、下载打印版文档时使用。返回生成文件的路径和文件大小。若用户只是要求分享链接而非生成文件请勿调用此技能。描述里明确什么情况别用特别重要因为它可以抵消掉一部分模型在某些关键词上过度联想的问题。2.3 参数设计遵循最少字段与示例先行技能参数的结构直接影响Agent填参的成功率。我踩过的坑是为了让技能看起来通用设计了一大堆可选字段结果模型每次调用都试图填满所有字段漏填、错填的情况特别多。现在我的参数设计原则很固定只保留必填字段和最高频的1到2个可选字段其余场景参数尽量让技能自己去推导或轮询。比如一个发送提醒技能早期我设计了提醒内容、接收人、发送时间、渠道、优先级、是否抄送六个字段后来压缩成接收人、提醒内容、发送时间三个其余全部让技能内部根据配置处理。另外如果参数是嵌套JSON结构一定要在参数的描述里给一个完整的示例。模型对深嵌套抽象描述的理解能力非常有限有了一条真实示例后字段填错的概率至少降低一半。我甚至会把示例写成参数模板放进去效果很好。3. 从零构建一个可用技能一次完整的实操拆解3.1 选一个能快速闭环的切入场景第一个技能不要选太复杂的业务场景最好选一个输入输出明确、失败影响可控的任务。我当时选的第一个技能是长文本归档把对话里的长文本拆成章节、生成目录、按指定命名规则存成Markdown文件。选它的原因很简单逻辑固定涉及的状态少失败最多是存了个错文件不会影响核心业务。你也可以从这些类型里挑一个做练手文本转换类格式转换、摘要生成、查询检索类查状态、查数据、简单操作类建目录、发通知。这三种场景的技能边界清楚调试起来效率最高。3.2 定义接口输入输出要能一次性说清楚技能接口的输入输出应该遵循一种协议感——让Agent知道传进去什么就会得到什么。我常用的一种结构是{ input: { content: 待归档的原始长文本字符串类型, title: 归档标题必填, format: 可选默认markdown支持markdown/txt }, output: { status: success | partial_success | failed, file_path: 生成的文件路径, chapter_count: 生成的章节数量, error_hint: 失败时的简要原因供Agent决策 } }输出里的error_hint字段非常关键它是技能向Agent传递失败信息的唯一通道。很多初学者会把异常直接抛出来让模型读堆栈日志那基本等于是让模型当运维。技能应该自己消化异常只把一句数据源连接超时可能原因网络波动或凭证过期这样的提示交给上层。3.3 实现内部逻辑重试、超时、兜底要写在技能里一个技能内部至少要有三重保障超时控制、重试机制、默认兜底行为。以长文本归档为例我实现的时候做了这几件事拆分章节之前先做文本长度检查超过阈值直接报错不进入耗时处理流程。文件写入失败时自动重试一次间隔2秒重试仍失败才返回失败状态。如果章节数量为0不强行生成目录返回partial_success并提示文本结构识别失败可能输入内容不规范。这些逻辑如果不在技能内部处理它们就会变成Agent在前端反复尝试、来回追问的噪音。技能内部能兜住的事情越多上层Agent需要做的决策就越少。import time import json def archive_long_text(content, title, formatmarkdown): # 前置校验 if len(content) 10: return json.dumps({status: failed, error_hint: 文本内容过短无法归档}) # 核心处理流程章节拆分、目录生成等已省略 try: result perform_archive(content, title, format) return json.dumps({status: success, file_path: result[path], chapter_count: result[count]}) except TimeoutError: return json.dumps({status: failed, error_hint: 归档处理超时可能文本过大}) except Exception: time.sleep(2) try: result perform_archive(content, title, format) return json.dumps({status: success, file_path: result[path], chapter_count: result[count]}) except Exception: return json.dumps({status: partial_success, error_hint: 归档失败已保留原始文本缓存})3.4 注册与意图命中测试验证的不是代码是模型的调用决策技能写完先别急着集成到复杂流程里单独做一轮意图测试。方法是准备20条不同类型的用户输入一半是应该触发该技能的一半是语义相似但不应触发的逐个跑一遍看看Agent的调用命中率。我记得第一次测长文本归档的时候20条输入里该触发的10条对了9条但10条不该触发的里错触发了3条——因为描述里归档整理这种词太泛了模型把把聊天记录整理成对话摘要也理解成归档了。解决方式就是在描述了加一条反向说明仅当用户要求生成可保存的独立文件时使用。若用户只是需要对话中的内容被总结或列表化不涉及文件生成请勿调用。加完之后误触发降到1条。所以注册之后一定要做意图测试不然你以为描述写清楚了实际上模型的语义联想范围跟你想象的不一样。4. 多技能协同Agent手里有多个技能时怎么编排4.1 顺序执行、条件路由、并行分治当技能数量变多Agent面临的问题就不只是选哪个技能而是组合技能的方式。我在实际项目里总结出三种最常见的编排模式顺序流水线技能A的输出作为技能B的输入适合文本处理类任务。比如长文本归档技能输出文件路径紧接着由文件摘要技能读取路径生成摘要最后通知发送技能把摘要发出去。条件路由Agent根据用户的最终目标在多个技能里选一条路径。比如用户说帮我查账Agent先调用订单查询技能如果订单状态是已完成再调用账单下载技能如果是未支付则调用支付提醒技能。并行分治一个大任务拆成多个子任务并行跑最后汇总。典型场景是季度复盘统计销售数据整理用户反馈生成图表三个技能可以并行执行。序列模式最简单也最容易出问题如果A技能失败了B技能怎么处理我一般会在技能design阶段就定义好下游是否依赖上游的完全成功对于那种部分结果也能用的情况B技能要能接受partial_success状态继续执行否则整个链路会卡死。4.2 技能之间的信息传递保持窄进窄出多技能协同最忌讳的一点是技能之间的结果互相渗透。我见过有项目把技能A的中间数据直接塞给技能B做输入这样看起来省事实际上一旦A的返回结构发生变化B全链路崩溃。正确做法是每个技能只接受经过验证的input只输出自己约定好的output技能之间不共享内部状态。A和B之间如果需要传数据就通过一个显式的交接结构来完成甚至可以把交接结构单独定义成一个类型这样后面加新技能的时候不会牵一发动全身。我常用的一种设计是把技能返回结果规范化之后再传递给下一个技能def pass_to_next_skill(previous_output, next_skill_input_schema): normalized { source_status: previous_output[status], payload: previous_output.get(data, {}), } return next_skill_input_schema.validate(normalized)这一步多花的时间很少但能省掉大量排障时间。4.3 技能膨胀之后注册表、标签与动态加载当技能超过25个你很快会遇到一个新的问题Agent每次决策要考虑的技能太多了选择时间变长误选率也随之升高。我用的办法是给技能加一个轻量级的注册表并附上标签和分组信息。比如标签有文本处理数据查询业务操作通知触达四类。Agent在做决策前先根据用户意图定位到某个标签分组然后只在这个分组内选择技能。这相当于给Agent做了一个路由漏斗先粗筛再精选决策错误率显著下降。另外技能不用一次性全部加载。对于已有的Agent框架可以做一个懒加载机制只有技能被选中时才加载它的实现代码加载完驻留内存不再重复加载。这样既控制了上下文长度也减少了模型的选择困惑。5. 实测中的高频坑与排查路径5.1 描述写得太自然导致技能误调用描述太人性化是有代价的。比如我在另一个技能里写了当用户觉得操作复杂时可以调用简化助手技能结果模型在用户仅仅说这个怎么这么复杂的时候就直接触发了简化助手技能其实用户只是在抱怨界面并不需要执行任何操作。这一类问题的本质是描述里用了太多情绪类泛化类触发词。我最后的处理方式是触发词尽量限定为明确的动词短语业务对象比如简化操作替换为当用户要求重写流程步骤、生成简版操作说明、或省略步骤时触发。越具体的触发词越不会引发歧义。排查这类问题不要靠猜直接把Agent调用日志打开看它某次决策时读取的技能描述是什么就能定位是描述里哪个词诱导了它。5.2 严格Schema校验导致成功率骤降这个坑我是被现实好好教育过一次的。我把技能参数schema定义得非常严格字段类型强制校验、枚举值精确匹配觉得这样最规范。结果上线之后技能触发成功率一度降到六成。原因是模型在生成参数时本身就带有一定随机性它可能把recevier这种拼写错误交给校验层或者把日期格式从2025-01-01写成2025/01/01这些在人类看起来都不影响语义但严格的schema会直接拒绝导致Agent反复修复、反复调用最终陷入死循环。现在我的做法是校验层允许语义等价和可纠正偏差。数值类型错了自动转换枚举值出现同义词就映射到标准值只有完全无法识别的字段才报错。这样既保持了数据的可处理性也给了模型一定的容错空间。5.3 异常上抛给模型引发的重试风暴这是多技能协同里让我最头疼的一个问题。最初我把技能内部网络请求的异常直接用原始报错返回给AgentAgent一看失败就重试网络波动那段时间一个请求能硬生生重试六次把下游接口都几乎打崩。后来我设了一个原则技能内部只允许最多自动重试两次超过次数统一转为业务失败将失败原因归约为风险提示。比如第三方服务暂时不可用已保留原始请求数据Agent看到这个结果就不会盲目重试而是会换一个技能或者直接回复用户稍后再试。5.4 排查链路日志、干跑与对话回放技能出问题的时候最大的难点在于区分是模型的决策错了还是技能的实现错了。我花了很久总结出一条相对高效的排查链路看日志日志里必须记录技能调度前后的原始用户输入、Agent选择的技能名、技能返回的状态码。先看选没选对技能再决定继续查下层还是上层。干跑模式给技能加一个dry-run开关开启后技能只做参数解析和流程模拟不真正执行外部调用。这样可以快速验证参数解析是否正确。对话回放拿历史对话重新喂给Agent固定住用户输入反复跑同一个case看是偶发还是必现。必现大概率是逻辑问题偶发大概率是模型随机性或外部依赖波动。这套链路我用了很久至少能把问题的定位时间压缩一半。不是每次都要走全流程但养成这个习惯之后排障思路会清晰很多。6. 再进一步技能的记忆、反馈与进化6.1 让技能记住上一轮的事基础的技能是无记忆的每次调用都是全新的。但在真实业务里技能往往需要跨调用记住一些东西比如上次生成的文件路径、用户偏好的输出格式、上一轮的归档目录。我通常的做法是给技能增加一个状态槽把跨会话需要保留的信息序列化保存下来下次调用时自动读入。长文本归档技能就记住了用户偏好的Markdown风格和归档根目录用户不用每次重复说调用时Agent不再需要让用户重复提供这部分参数实际体验会顺畅很多。但这里有个边界要注意技能的记忆应该限定在这个技能自己的工作范围内不要试图让它记住整个Agent的全局状态否则记忆维护成本会迅速超过收益。6.2 把失败案例变成技能更新的素材每个失败case都不应该被浪费。我会把每次线上出问题的输入和对应失败原因整理成一个小型样本集定期回放给技能做回归测试。如果发现某个失败模式是反复出现的就考虑在技能的描述或内部逻辑里加对应的处理分支。举个例子归档技能早期经常把用户粘贴的URL链接当成正文导致生成的文件里全是无关链接。后来我在技能的前置处理逻辑里加了一步URL识别与剥离在描述里也补充了一句输入包含大量网页链接时先过滤链接再归档。失败样本驱动技能更新这个习惯让我后面维护技能的时候不再凭感觉修问题而是有一条清晰的迭代路径。6.3 从技能集合到技能市场当你的项目里积累了十几个稳定好用的技能之后继续按老办法零散管理就不划算了。这时候值得做一次技能盘点把技能按团队、场景、稳定性程度分好层级做成一个内部复用清单。属于同一业务域的技能尽量保持接口风格一致参数命名也统一这样后续新的Agent项目可以直接引用老技能不用重写。技能里的错误处理逻辑、超时阈值、重试策略这些横切关注点也尽量抽出来统一维护。我现在手上这套技能库已经做到了新增一个Agent时从库里直接引三四个技能就能跑通原型。早期那种每个Agent都从头堆工具的日子算是彻底回不去了。最后分享一个我一直在用的小技巧技能描述写完不要凭感觉判断够了拿它去跑一轮意图测试再改。往往第一版都会因为触发词太宽泛或者边界没说清而翻车改到第二版、第三版之后你会明显感觉到Agent的调用决策开始长脑子了。技能设计这件事七分靠定义三分靠打磨打磨的那三分全靠真实对话数据喂出来。