AI Agent技能系统实战:从工具调用到可复用技能库的演进
我是在连续开发了好几个AI agent原型之后才真正意识到agent-skills这个概念的价值的。当时的情况很典型我需要让智能体完成一些看起来并不复杂的任务——整理周报、提取合同关键条款、给销售线索打标签。每个任务单独写prompt都能跑通但只要任务一多系统就乱成一团prompt互相干扰、输出格式不稳定、换一个数据源就要改一遍逻辑。最让我崩溃的是每个agent都在重复发明轮子同一个提取时间范围的逻辑我在三个不同的agent里写了三遍每次调试还得从头看一遍prompt。后来我把这套能力抽出来做成可复用的技能skills整个系统的稳定性和开发效率才发生了质变。这篇文章不打算讲大而全的框架理论而是从我自己搭建agent-skills的实际经历出发聊清楚几个问题技能和工具调用到底有什么区别、一个技能单元应该怎么定义、技能系统跑起来之后会遇到哪些真实坑、以及怎么让技能库随着使用自动进化。适合正在做AI agent应用、或者已经在用函数调用但总觉得差口气的朋友参考。1. 先说清楚agent-skills到底解决了我什么痛苦1.1 没有技能系统的agent本质上是个记性很差的实习生我早期做的agent结构非常简单一个系统prompt加上一段用户请求直接把整个上下文丢给LLM让它输出答案。单次调用看起来没什么问题但只要你让它持续工作问题就全冒出来了。最典型的表现是同一个任务每次处理方式都不一样。同一个从销售邮件里提取客户意向等级的需求今天跑出来的结果是A类/B类/C类明天可能就变成高意向/中意向/低意向。不是LLM不够聪明而是没有人告诉它应该用一套固定的、沉淀好的方法去处理这类任务。每次调用都是一次自由发挥等于让一个新来的实习生每天重新学一遍怎么做Excel透视表做完还不一定按同一个格式存档。另一个让我头疼的问题是长上下文的损耗。大模型在处理长对话时早期的信息会被逐渐稀释这是注意力机制的天然特性。如果agent的工作流里有七八个步骤执行到第五步的时候第一步的结果可能就已经模糊了。没有技能系统你只能把所有中间结果都堆在上下文里token消耗暴增效果还不一定好。我当时就在想人之所以越做越熟练是因为人会把重复性的工作固化成肌肉记忆或操作手册。agent为什么不行后来我才慢慢意识到这个操作手册就是技能skill而肌肉记忆就是技能的自动触发和稳定执行。1.2 工具调用不等于技能别把这两个东西混为一谈很多人对agent的第一印象是它会调用工具也确实OpenAI的function calling、Anthropic的tool use都很成熟了。我一开始也觉得自己用上了工具调用就等于有了技能系统但实际上这两个东西的抽象层级完全不一样。工具是原子的、单次的、无状态的。一个工具就是执行某个动作查天气、发邮件、算个加法。你给它参数它返回结果完事了。至于这个动作在什么场景下该触发、触发之前要做什么准备、触发之后结果怎么用工具一概不管。技能要解决的恰恰是工具管不了的那些事。一个技能包含了什么时候用输入长什么样执行逻辑是什么结果怎么校验失败了怎么办这一整套信息。它是有状态的、组合的、可以演进的能力单元。我举一个特别直观的例子你有一个生成销售周报的函数它接收客户数据输出一段周报文本。但真正的业务场景是agent需要先判断手里有没有足够的客户数据如果数据不够要先做数据补齐然后生成周报接着检查周报里有没有漏掉重要的商机变化最后还要按指定的格式保存到某个文档里。这一整套流程单靠一个function是装不下的需要的是一个生成销售周报的技能这个技能内部可以调用多个工具也可以有自己的逻辑分支。1.3 技能系统带来的三个直接收益稳定、复用、积累搭完第一版agent-skills之后我体会最深的三个变化可以概括成六个字稳定、复用、积累。稳定指的是固定路径替代了自由发挥。同一个任务走同一个技能输入参数经过校验执行逻辑可重复输出结果有结构。跑一百次和跑第一次行为基本一致这对生产环境来说是底线级的诉求。复用指的是技能从具体的业务场景里被抽象出来之后可以在多个agent之间共享。我在内容审核agent里写的识别PII个人隐私信息技能几乎没有任何改动就直接用到了客服agent里。这个抽象过程节省的开发时间怎么算都值。积累是指技能库本身是持续增值的资产。每处理完一类新任务就可以把它沉淀成一个新技能每次发现技能有缺陷就迭代一版。技能库越丰富agent能cover的场景越多这是指数级的效果不是简单的加法。2. 技能不是工具一个技能单元的内部结构与定义规范2.1 技能元数据名称、描述与触发条件一个都不能省我开始设计技能单元的结构时踩了不少坑最后稳定下来的一套字段大概是这样的。第一个是元数据包含技能名称、描述和触发条件。这三个字段看起来简单实际上是最难写对的。技能名称要短、要唯一最好用动词加名词的表达方式比如extract_contract_termsclassify_sales_lead。名称本身是给LLM和开发者看的原则是看到名字就能猜出大概用途。我不建议用过于抽象的命名比如process_data这种在技能多起来之后光是路由排查就能让你疯掉。描述字段是整个技能定义里影响最大的一个字段。LLM在决定当前任务应该用哪个技能时主要就是靠读技能描述来做判断。描述写得好不好直接决定路由准不准。我总结的经验是描述里要讲清楚这个技能做什么、在什么场景下用、以及特别重要的一点——在什么场景下不要用。提示不要用一堆营销式的形容词去堆砌描述比如高效智能强大这些词LLM看到这些词并不会更倾向选它。真正有用的是具体的业务关键词、目标对象、输出形式。触发条件这个字段是我后期才加进去的。没有它的时候经常出现两个技能描述相似、LLM选错的情况。触发条件明确了这个技能应该在什么样的任务状态下被激活相当于加了一道语义门槛能有效降低误路由。2.2 输入输出契约参数要扁平返回要统一技能的参数定义我推荐直接使用JSON Schema它对LLM有天然的亲和力各种agent框架也都支持。不过参数结构的设计上我有一个非常痛的教训嵌套层级不要太深。我早期有一个技能入参是一个多层嵌套的对象LLM在生成这类参数时非常容易出错不是少一层就是字段名写错。后来我改成扁平的多个字段配合枚举值和格式说明参数生成的准确率一下子提升了好几个百分点。举个例子。早期版本是这样的{ params: { type: object, properties: { filter: { type: object, properties: { date_range: { type: object, properties: { start: {type: string, format: date}, end: {type: string, format: date} } }, status: {type: string, enum: [new, contacted, lost]} } } } } }后来改成了这样{ params: { type: object, properties: { start_date: {type: string, format: date, description: 开始日期格式YYYY-MM-DD}, end_date: {type: string, format: date, description: 结束日期格式YYYY-MM-DD}, status: {type: string, enum: [new, contacted, lost]} }, required: [start_date, end_date] } }扁平化之后LLM要处理的结构更简单字段的语义也更明确参数幻觉的概率明显下降。而且每个字段都加了description告诉LLM这个字段到底该填什么尤其是枚举值必须给清楚取值范围。返回结果我建议统一封装成一个结构code、message、data。code是执行状态码message是可读性的辅助信息data是真正的业务数据。这样设计的好处是上层agent在拿到结果时不需要对每个技能做特殊的解析适配只需要统一读这三个字段。2.3 执行体纯代码、LLM驱动还是混合形态技能的执行体有三种形态选哪种取决于任务的确定性程度。第一种是纯代码技能。任务逻辑完全确定没有任何歧义比如计算两个日期之间的工作日天数把CSV转成JSON。这种技能直接用Python写就好快、便宜、可调试。我第一次实践时把大量重复性的字符串处理逻辑做成了纯代码技能直接省掉了每次调用LLM的token消耗。第二种是LLM驱动技能也叫prompt-as-code。任务本身没有标准答案需要理解语义、生成文本比如写一封催款邮件提炼客户的核心诉求。这种技能的执行体是一段精心设计过的prompt模板加上对输入输出的约束。它的核心在于prompt模板的质量一个好的模板能让LLM的输出稳定在可控范围内。第三种是混合技能也是实际业务里最常见的形态。先跑规则代码做前置处理再调用LLM做语义判断最后再用代码整理输出结果。比如从PDF合同里提取关键条款这个技能第一步是用代码把PDF解析成文本第二步是调用LLM按指定格式提取条款第三步是用代码校验和格式化结果。三段式结构每一段都是最简单可靠的方式。2.4 技能的自校验与后置条件防止垃圾结果回流我见过太多agent翻车问题不是出在技能本身而是出在没有对技能的输出做检查。LLM或者外部接口返回的结果有时候看起来合理但根本不能满足业务要求。所以我在每个技能里都加了一个自校验环节。校验分两层第一层是格式校验比如返回的JSON能不能被正确解析必填字段有没有缺失日期字段是不是合法第二层是业务校验比如提取出来的金额是不是正数分类结果是不是在给定的枚举范围内。后置条件这个概念是我从数据库的事务里借鉴过来的。它定义的是技能执行成功之后系统中必须为真的状态。比如发送邮件技能的后置条件是收件人、标题、正文都不为空更新客户状态技能的后置条件是目标记录真实存在。正常运行和真正闭环之间的差距就在这些细节里。3. 从会到用技能注册、路由与调用链路的完整拆解3.1 注册机制让每个能用的技能都被agent看得见技能写好了还只是代码工件必须经过注册进入技能清单agent才有可能在运行过程中发现它、调用它。这个机制听起来很基础但注册方式的选择直接影响整个系统的灵活性。我第一版的做法是静态注册也就是在配置文件里把技能列表写死。好处是简单、好排查坏处是技能一旦多了或者需要动态上下线每次都要重新部署。现在的主流agent框架里注册机制已经发展得很成熟了既支持启动时自动扫描技能目录也支持运行时通过API动态添加、禁用技能。我自己的实践是两层注册核心技能走启动加载长尾技能走动态注册。核心技能是系统稳定运行必需的比如身份识别、权限校验长尾技能是业务上新增的场景性能力比如某个客户新要求的报表格式这类技能随时可能上线也随时可能废弃放动态注册池更合适。注册时要做的另一件事是合法性检查。一个技能在被加载进清单之前至少要校验元数据是否完整、参数Schema是否合法、执行入口是否存在。我吃过一次亏有一个技能的描述字段为空注册时系统没报错结果在线运行时LLM根本不知道该在什么时候选它这个技能就变成了一个永远沉睡的死技能。所以在注册阶段把这类问题拦下来比线上排查省事得多。3.2 路由决策谁来决定当前任务用哪个技能技能注册完之后面临的核心问题就是路由——用户当前的输入到底应该激活哪个技能。主流的做法有两种。第一种是让LLM直接从技能清单里选这是function calling天然支持的路径。你把技能列表转成tools传给LLMLLM在推理过程中会自己判断是否需要调用技能、调用哪个、传什么参数。这种方案对单次任务、技能数量不多比如十几个以内的场景足够了。但技能数量一多每次调用都把所有技能的描述和参数定义塞进上下文的token开销会非常大而且LLM在大量候选之间做精确选择的能力也会下降。第二种是两阶段路由先用embedding召回候选技能再用LLM做精排。这是我自己实践下来最稳的方案。第一步把技能的名称、描述、触发条件拼接成一段文本预先embedding化存起来第二步收到用户请求后把请求内容也做embedding用向量相似度召回Top-K个技能作为候选第三步把候选技能的完整定义和用户请求一起交给LLM让它选择最终要用的技能。这个方案的优点很明显。embedding召回的效率高可以在技能数量上千的情况下快速缩小范围LLM精排只需要看几个候选选择的准确性也会高很多。缺点是多了几个环节但这点延迟换来的稳定性提升是值得的。3.3 参数绑定把自然语言准确映射成结构化参数路由确定之后下一个动作是把用户输入映射成技能参数。这是执行力层面最容易出错的一环。我见过的最典型的错误是让LLM自由发挥填参数不提供任何字段说明和格式约束。比如用户说帮我查一下上周三以后的销售数据如果技能参数里没有对日期格式做约束LLM可能会填出上周三而不是具体的日期或者填出一个空值。原因在于LLM不是数据库它不会自动把相对时间换算成绝对日期必须靠参数定义里的规则去约束它。所以参数绑定这个环节我总结了三个要点。第一每个字段都要写清楚的description告诉LLM这个字段的业务含义和填值规范。比如日期字段不仅要写格式YYYY-MM-DD还要写必须是具体日期不接受相对时间表达。第二对可以枚举的字段一律枚举。能枚举的就不给LLM自由发挥的空间。第三在进入技能执行体之前加一道规则校验。校验不通过时不直接执行而是让LLM重新生成参数或者向用户澄清。这叫fail-fast成本最低。3.4 结果回传执行完的输出要能接得住技能执行完返回结果不是塞给用户就完事了还要考虑结果如何回传给agent的主流程。如果后续还有步骤要依赖这个结果那结果就必须被翻译成主流程能读懂的格式。我见过不少agent实现的败笔技能返回了一个JSON结构但后续步骤还在原始用户输入里找关键信息结果当然是找不到。正确的做法是技能的结果经过格式化之后写入到一个统一的上下文状态区后续步骤从这个状态区里读取而不是回溯到最初的用户输入。这里还有一个容易被忽略的问题token消耗。如果技能结果非常大比如一个报表的完整内容直接全部塞回上下文就会占用大量token。我的经验是先做摘要化处理把结果压缩成后续步骤真正需要依赖的关键字段别舍不得删。比如生成周报技能跑完后回传给主流程的不是整个周报全文而是周报已完成保存路径是xxx销售额环比增长12%共有3个重点商机这样一层摘要。既够用又不浪费。4. 技能库治理检索召回、去重、版本演进4.1 技能多了以后检索不准是第一个浮现的问题技能数量突破30个之后我开始明显感觉到路由质量在下降。最直观的表现是用户问了一个问题agent命中的技能跟业务预期对不上或者两个描述相近的技能来回犹豫。这个问题的根源在于技能检索完全依赖文字描述。之前我觉得描述写得具体一点就够了但随着技能变多语义空间被填得越来越挤。新技能的描述和旧技能有重叠就产生了干扰。我的解法是给技能描述加定位语而不是重新发明检索算法。每个技能在描述里都要写清楚自己的独特定位明确划出与其他技能的边界。举个例子查询订单状态和查询物流轨迹这两个技能很多人的描述都会写成根据订单号查询相关信息这种描述在向量检索时区分度极低。正确的写法是各自强调自己独有的字段和业务场景查询订单状态强调订单当前生命周期阶段查询物流轨迹强调快递运输环节。边界划清楚了召回准确率自然上来了。4.2 去重与冲突两个技能看起来都能处理同一个任务技能库发展到中后期经常会出现两个技能都能覆盖同一类任务的情况。比如识别客户情绪和分析客户反馈这两个技能本质上都是在做文本情绪分析只不过一个偏客服场景一个偏调研场景。这种情况下如果不做处理路由就会随机波动。我的处理方式是在技能定义里增加一个适用性层级分为通用技能和垂直技能。当通用技能和垂直技能出现重叠时优先激活垂直技能因为垂直技能的触发条件更严格语义上下文更明确。还有一种情况是两个技能确实高度冗余这种情况建议直接合并。我后来在技能库里跑了一个定期的结构分析脚本计算每对技能描述之间的语义相似度相似度超过阈值的就拉出来人工审查是合并还是保留边界由人来决定。让AI自己决定去留风险太大不推荐。4.3 版本演进与废弃技能也会过时得有生命周期技能是有生命周期的。业务规则变了底层API变了甚至上游数据源变了都会导致一个技能从可用变成不可用。我早期对技能版本没有概念改了一把技能就覆盖部署结果一次调整把线上行为改崩了回滚还要靠git历史。后来我强制要求所有技能必须带版本号任何变更走新版本发布旧版本保留一个回滚期。这跟普通软件发布的思路完全一致只不过很多做agent的人把技能理解成了配置文件忽略了它也是一段需要管理的业务逻辑。对于已经废弃的技能我不建议直接删除。更好的做法是标记为deprecated在描述里明确说明当前任务请使用xx技能代替给LLM一个过渡指引。等确认线上没有任何调用后再清理掉保留一段学习记录对之后设计新技能也有参考价值。4.4 技能级联让一个技能调用另一个技能技能库做大之后还会出现一个新的需求技能之间的组合调用。比如生成客户体检报告这个技能内部需要先调用拉取客户基本信息拉取交易记录计算客户健康评分三个技能最后汇总成报告。我把这种技能叫做组合技能它本身不直接执行具体的原子操作而是编排其他技能的执行顺序和结果汇聚。在定义组合技能时重点在于编排逻辑的确定性。建议用代码写编排流程而不是靠LLM现场发挥因为组合技能的稳定性直接决定了上层业务能不能跑通。一个组合技能内部每个子技能的入参从哪来、出参给谁用、失败时是做重试还是做降级这些都要在定义时明确不能依赖LLM临场判断。5. 实测翻车记录误路由、参数幻觉与技能失效的排查链路5.1 误路由明明A技能更合适LLM却选了B有一次线上事故让我印象很深。用户输入的是一段客户的售后服务请求系统却把投诉升级技能给触发了。从日志来看LLM选择这个技能的原因是描述里出现了客户不满处理这些关键词恰好跟用户输入里的措辞匹配上了。排查过程是这样的我先打开了系统运行日志这一步必须有。我的日志里记录了每次请求的完整链路用户输入、命中候选技能列表、LLM最终选择的技能、传入的参数、技能执行结果。没有这套日志我根本无从下手。日志翻完之后我确认问题是出在投诉升级技能的description写得太宽泛了。它写了适用于客户不满意的场景但不满意这个词的边界太模糊而售后请求也是客户潜在不满的一种表现于是LLM就把它一并归入了投诉。修复方案不复杂把描述改精确明确投诉升级技能的触发条件是客户已经明确表达投诉意图或要求值班经理介入同时追加了一条负向触发条件如果客户只是咨询问题不要使用本技能。这个case让我明白一个道理技能误路由的根因绝大多数不是模型能力不够而是技能定义给了模型一个模棱两可的答案空间。5.2 参数幻觉LLM声称调用了技能但参数根本不能用参数幻觉是我搭建agent-skills过程中遇到最折磨人的问题。现象是LLM确实选中了正确的技能但生成的参数一塌糊涂。我排查过一个case用户说统计过去一周的订单量结果LLM给时间参数传的值是last_week而不是具体的日期范围导致后续的查询直接报错。问题出在参数Schema的描述不够严谨。我在Schema里写了start_date: {type: string}但没有说明格式也没有说明具体值要求。LLM在信息不明确的情况下会倾向于生成一个看起来合理但无法落地的值。修复做法我前面提过每个字段都加详细description和format约束并明确必须使用具体日期格式YYYY-MM-DD禁止使用相对时间。加上这个约束之后同类问题的出现频率大幅下降。除了修参数定义我在技能执行前还加了一道参数检查函数专门拦截相对时间枚举外值空值等非法参数发现非法参数就直接拒绝执行并返回提示让LLM重新生成。这道防线从源头上把参数幻觉挡在了执行体之外。5.3 技能失效底层API变了技能还在被调用还有一种翻车不是路由问题而是技能依赖的外部能力发生了变化。我当时有一个查询企业工商信息的技能底层对接的第三方API升级了鉴权方式但我在技能代码里没有同步更新导致所有调用统一返回401错误。这类问题最麻烦的地方在于LLM并不知道技能底层的API失效了它依然会觉得这个技能可用继续选择它继续失败。如果没有监控这个问题可能被捂着很久。我后来加了两道机制。第一道是失败次数熔断单个技能在短时间内连续失败超过阈值就自动把它从技能清单里下线不再参与路由选择同时触发告警第二道是技能健康检查定期对核心技能做拨测拨测失败的技能自动置为不可用通知开发人处理。这两道机制上线之后技能失效对线上业务的影响从长时间静默变成了分钟级发现和处理。5.4 排查方法论没有全链路日志一切排查都是扯淡把这三个翻车案例放在一起看它们的共性是都需要全链路日志才能定位。我最初做agent的时候也觉得日志不重要跑通了不就行了。后来线上出问题我对着屏幕干瞪眼才明白没有日志的agent系统就是一个黑盒出问题只能靠猜。我的做法是在agent的入口和出口两侧各打一条日志入口日志记录请求原文和路由决策出口日志记录最终结果。中间每个技能调用的输入参数和返回状态也全部记录。这样任何一个环节出问题我都能通过日志回溯到具体是哪个技能、哪一步、哪个参数出了差错。另外我维护了一个最小复现集。把历史出过问题的用户输入收在一个固定集合里每次修改技能定义之后用这个集合做一遍回归测试。不要小看这个做法它帮我拦下了至少一半的回归问题。6. 让技能库自己长出来从对话中自动化沉淀技能6.1 人工定义核心技能自动挖掘长尾技能技能库的搭建我亲身实践下来的有效路径是先人工定义核心技能再自动挖掘长尾技能两条腿走路。人工定义核心技能是因为核心技能关系到系统的稳定性和安全性不能交给自动化流程去自由发挥。这类技能通常比较少但每一个都被打磨得很细致。自动挖掘长尾技能是因为业务场景是长尾分布的靠人肉一个一个写技能速度和覆盖面都不可能跟上真实需求。6.2 从成功对话中抽取技能模板自动挖掘技能这个想法听起来比较理论实际做下来是完全可行的核心思路是从已经跑通的对话里把成功的处理路径抽取出来沉淀成可复用的技能模板。具体做法是在agent执行任务的过程中记录哪些请求走通了完整链路并且最终结果被用户接受或确认是合理的。我把这类请求标记为成功样本然后对成功样本做一次结构化分析用户输入是什么类型、agent调用了哪些功能、执行顺序是什么、关键输出是什么。把这些信息泛化成一个技能模板。泛化这个过程是关键需要用LLM把具体的业务细节抽象成通用的处理步骤。比如某个成功样本里写的是用户要求查看华东区的销售数据泛化后就是查看某个区域或多区域的销售数据参数里的华东区变成一个可配置的region字段。这个泛化后的模板再经过人工审核才能注册成一个正式技能。这套自动化流程的价值在于原本需要人肉去分析产品日志才能发现的高频场景现在系统自己就能沉淀成技能而且天然是从真实需求里长出来的不会有写了没地方用的问题。6.3 新技能上线前的回归验证能避免很多半夜响起的告警自动挖掘出来的技能不能直接上线。我吃过这个亏自动抽取过一批技能没有充分的验证就部署了结果其中好几个技能逻辑上有漏洞导致线上调用出错。现在我的流程是每个新技能上线前先跑一遍历史案例集。这个案例集里有两类数据一类是应该命中这个新技能的case验证它能不能正确处理另一类是明确不应该命中它的case验证它会不会误伤。两者都过关技能才会进入正式技能库。这一步看起来费时但是值得的。因为技能库是一个整体一个质量差的技能不仅影响自己的调用成功率还可能通过路由干扰到其他正常技能的选择。我自己运行这套流程一段时间之后最大的感受是技能库像一个生物体它会成长也会生病。成长靠的是从真实对话里持续挖掘新的能力点生病靠的是全链路日志和回归测试来及时发现和处理。这套机制跑顺之后agent的能力迭代速度才开始真正快起来。如果让我再分享一个最实用的经验那就是从一开始就为每个技能写好完整的description和参数说明别图省事。技能库越大这些前期的啰嗦就越值钱它会直接决定你的agent是稳如老狗还是天天抽风。