大家做Agent项目时,有没有遇到过这种情况:明明模型能力很强,工具也接了一大堆,但让它完成一个稍微复杂的任务时,就开始东一榔头西一棒子,要么漏掉关键步骤,要么把参数传得乱七八糟。问题的根源往往不在模型本身,而在“技能”这个层面上——你给Agent的只是一堆零散的工具,而不是一套组织好的、可复用的“技能”。我最近把整个项目里的Agent能力梳理重构了一遍,重点就是围绕“agent-skills”这套思路来做。这篇就把我在设计、落地和排查过程中积累的东西完整写出来,希望能帮到正在被同样问题折磨的开发者。
“agent-skills”说到底,就是把Agent能干的某件事,封装成一个结构化的、有明确输入输出的技能单元。它不是单纯指某个API接口或工具函数,而是包含“这个技能什么时候用、需要什么参数、内部怎么编排、输出什么结果”的完整定义。这样一来,Agent面对复杂任务时,能像搭积木一样组合技能,而不是每次都靠模型现场现想。适合正在做基于大语言模型智能体应用、对模型输出稳定性有要求、想把Agent做得更工程化的团队和个人开发者参考。
1. 技能体系的核心设计思路
1.1 为什么不能只依赖“工具调用”
很多项目一开始都是从“给Agent塞工具清单”起步的。你把函数列表和描述扔给模型,希望它自己决定调用什么、怎么调。早期Demo确实跑得通,可一旦任务复杂度上来,问题就密集出现了。我见过最典型的翻车场景:Agent要写一份数据分析报告,它先调了查询函数拿到数据,然后调画图函数生成图表,最后让它总结结论时,它突然开始编造数据;或者要执行一个多步骤操作流程,Agent在中间某一步漏掉了必要的参数校验,导致下游环节全线崩溃。
说到底,模型在上下文里“看到一堆工具”和“掌握一套技能”是两码事。工具调用讲究的是“这一下怎么调”,技能体系讲究的是“这件事怎么做”。技能会把完成一个目标所需的多个步骤、决策逻辑、异常处理全部封装起来,形成一个半自动化的流程。模型不需要每一步都做主决策,它只需要在技能之间做选择,然后把技能要求的参数补全即可。
拿生活里的事打比方:工具是你工具箱里的锤子、螺丝刀、电钻;技能不是锤子本身,而是“修好一把椅子”这整件事——用哪种工具、先拆哪个部件、怎么固定、加固到什么时候算合格。一个熟练木匠不会每次拿到椅子都从零想步骤,他脑子里存的是“修椅子”这个技能模板。
1.2 技能库的分层组织方式
在设计技能库时,我强烈建议按照“原子技能 - 复合技能 - 场景工作流”三层来组织。原子技能是最小可执行单元,对应一个明确的动作,比如“查询订单状态”“计算运费”“格式化日期”;复合技能由多个原子技能按固定顺序编排而成,比如“处理退货申请”就包含校验订单、审核资格、生成退货单、通知用户四个原子步骤;场景工作流则是一组复合技能的动态组合,面向一个完整的业务场景,比如“处理售后全流程”。
这个分层设计有实质好处。原子技能容易测试和维护,任何一个技能出了问题,你只需要修复这一层;复合技能沉淀了业务逻辑,不会因为模型随机性导致步骤错乱;场景工作流则保持了灵活性,模型可以根据用户意图自由编排复合技能的顺序。实际用下来,之前那种“Agent发挥不稳定”的问题大幅缓解,因为它不需要在每一步都“自由发挥”了。
还有一个容易被忽略的点:技能之间的依赖关系要尽量收拢。我见过不少项目把技能拆得特别细,结果一个业务流程要串七八个技能,中间任何一次模型判断失误都会断链。合理的做法是让复合技能承担更多封装责任,把“必须按顺序执行”的步骤锁死在技能内部,对外只暴露“开始”和“结果”这两个接口。
1.3 技能定义的三要素:描述、参数、返回
每个技能在注册到技能库时,需要写清楚三样东西。第一是技能描述,这东西最重要也最容易敷衍。模型是靠描述来判断“什么时候该用这个技能”的,写得太笼统,它就会在错误的时候调用;写得太啰嗦,又会挤占上下文空间。我的经验是描述控制在50到100字,包含触发场景、核心功能、输出结果三个信息,并用简短的示例句辅助说明。
第二是参数定义。这里必须规定每个参数的名称、类型、必填性、取值范围和含义说明。很多Agent翻车就是参数定义不严谨导致的,比如日期格式没写清楚,模型给你传一个“2024-7-8”,你的解析函数又只接受“2024-07-08”,直接就崩了。参数层面的约束越明确,模型成功率越高。我所有的技能参数都会写清楚枚举值或正则表达式约束,不给模型留“自由发挥”的空间。
第三是返回结果的规范化。技能内部可以产生各种中间数据,但最终对外返回的必须是一个结构化的、包含状态码、业务数据、错误信息的结果对象。这样上层Agent判断“技能是否有成功”时就有了机器可读的信号,而不是让模型去“读一段自然语言再自己悟”。
2. 核心技能类型的拆解与实操要点
2.1 单步工具技能:最基础的“手”和“脚”
单步工具技能是技能体系的最小单元,本质上是对一个外部工具调用的封装。但它和直接暴露工具给模型的区别在于:技能层会预先处理参数映射、格式标准化和错误兜底。比如一个“发送邮件”的工具,暴露给模型时可能要求传sender、recipient、cc、bcc、subject、body好几个字段,而封装成技能后,模型只需要传to、subject、body三个核心参数,其他字段由技能内部按默认策略补全。
实操过程中有几个容易踩的坑。第一个是参数名不要使用过于抽象或容易混淆的命名,比如args1、data这种,模型根本搞不清楚含义,正确做法是使用语义化命名并在描述里给字段加例子。第二个是不要漏掉必填参数的校验,技能内部必须先校验参数完整性再执行调用,否则报了晦涩错误,模型会一脸懵。第三个是超时和重试策略一定要有,外部工具不稳定时,技能内部先做一次重试,比让模型去“处理错误”靠谱得多。
关于错误信息,必须遵循一个原则:让模型看得懂、知道怎么修正。如果外部接口返回的原始错误是“500 Internal Server Error”,技能层要把它转换成“用户ID不存在,请检查后重试”这种语义化信息。模型就能基于这个信息调整策略,而不是在原地打转。
2.2 多步流程技能:把“过程”固化成“模板”
多步流程技能的核心价值在于把确定性逻辑从模型推理中剥离出来。什么时候该用多步流程技能?最典型的情况是任务步骤固定、顺序不能乱、中间有关键检查点的场景。比如“用户实名认证”这个流程,必须按“提交资料→公安校验→人脸识别→结果登记”的顺序执行,漏掉任何一步都不合规。
实现多步流程技能有一个关键设计模式:状态机思维。技能内部维护一个步骤状态,每完成一步更新状态,下一步根据当前状态决定动作。这样即使某一步执行失败,技能也能从失败点恢复,而不是整个流程推倒重来。我用一个简单的Python模式来落地这个设计:
class FlowSkill: def __init__(self): self.state = { "step": "init", "retry_count": 0, "context": {} } async def execute(self, params: dict) -> dict: try: while self.state["step"] != "done": handler = getattr(self, f"_handle_{self.state['step']}") self.state = await handler(self.state, params) return {"code": 0, "data": self.state["context"]} except SkillError as e: return {"code": e.code, "message": str(e), "state": self.state}这只是一个框架示意,实际的技能内部逻辑肯定更复杂。但它说明了一个重要原则:技能内部是命令式的确定性执行,模型只负责提供初始参数,步骤推进完全由代码控制。这样流程的稳定性和可测试性就有了保障。
2.3 技能与上下文管理
技能执行的时候需要访问业务上下文,比如用户ID、订单ID、当前的会话摘要。而“上下文”怎么传递给技能,是一个经常被忽视的工程问题。
最糟糕的做法是让模型把所有上下文都写在参数里传给技能。这有几个问题:上下文太长会超出模型上下文窗口,模型转述上下文时会丢失关键信息,还会产生大量无效的token开销。推荐的做法是引入一个共享上下文对象,由Agent框架统一维护,技能通过上下文句柄来读取与自己相关的数据片段。
我在项目中用了一种轻量级的做法:主Agent维护一个上下文库(Context Store),按命名空间隔离不同维度的数据,比如session.current_user、order.selected、report.generated_data。技能在执行前声明自己需要读取哪些命名空间,框架自动注入。技能内部还能主动写入新数据,比如“生成报告摘要”技能会把摘要写到report.summary,后续其他技能可以直接读取,不需要模型在中间倒一手。
这样做还有个额外好处:上下文体积可控。每个技能只读取自己需要的数据块,而不是把整个会话历史一股脑塞给技能处理,token消耗和延迟都明显下降。
3. 从零搭建一套可用的技能体系
3.1 技能注册与配置格式
技能管理的第一步是注册。我使用一份基于JSON的注册表来描述全部技能。这个文件相当于技能的“目录”,Agent框架启动时加载它,把每个技能的定义映射为可调用的执行单元。
{ "skills": [ { "name": "create_refund_order", "description": "当用户申请退款且校验通过后,创建退款单并通知财务系统。使用场景:售后退款、订单取消退款。", "type": "flow", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,格式如 ORD-2024-00001", "pattern": "^ORD-\\d{5}$" }, "reason": { "type": "string", "description": "退款原因,由用户提供,长度小于200字" } }, "required": ["order_id", "reason"] }, "returns": { "refund_no": {"type": "string"}, "amount": {"type": "number"} }, "flow": ["verify_order", "check_refund_policy", "create_refund", "notify_finance"] } ] }参数定义里有几个细节值得注意。每个字段都加了描述,并尽可能用正则或枚举约束取值;必填项通过required显式声明;flow字段列出了这个复合技能内部要依次执行的原子技能名称。框架在运行时读到flow字段,就知道该按哪个顺序编排内部的原子技能。
技能描述的质量直接影响模型选择技能的准确率。我测试过两种描述写法,一种是“创建退款单”,另一种是“当用户申请退款且校验通过后,创建退款单并通知财务系统。使用场景:售后退款、订单取消退款。”,后者的调用准确率高不少,因为模型是语义匹配的,触发场景写清楚,它判断起来的依据就充分。
3.2 技能编排路由器
有了技能注册表,还需要一个“技能路由器”来调度。路由器负责三步工作:根据用户意图从注册表中检索候选技能、把用户请求映射为技能参数、执行技能并处理返回结果。
检索阶段我用的是方案是先做向量召回再用模型重排。首先把每个技能的描述做向量化,用户请求进来后先算向量相似度,召回top 5候选;然后把候选技能连同用户意图一并交给模型做最终决策。这么做有两个原因:一是纯向量召回可能把语义相近但功能不同的技能误判为匹配,加入模型重排后准确率明显提升;二是模型不需要在一大堆技能里做全局搜索,token消耗和响应延迟都会降低。
参数映射阶段是另一个容易被忽视的坑。模型并不能天然知道“用户的订单编号是ORD-开头的那串字符”,它可能把订单ID和订单号搞混。这里需要在技能描述的参数说明里给出最典型的映射来源,比如“订单编号在用户请求中常以‘订单号’、‘订单编号’出现”。这样模型在抽取参数时就有了参照。
执行阶段的错误处理要统一约定返回值格式。我的约定是所有技能返回结构为{"code": 0, "data": {...}}或{"code": 4001, "message": "可读的错误描述"}。框架拿到code非0的结果后统一走错误处理流程,比如重新向模型询问缺失参数、换一个技能试试、或直接向用户展示错误提示。
3.3 技能测试与回归策略
技能的测试不是一次性的,而是要能反复回归。因为模型版本更新、技能描述调整、参数规则修改,都会影响效果。我建立了一套“技能测试用例集”,针对每个技能准备了多组输入。测试分两层:
第一层是静态测试,直接调用技能内部执行函数,验证在给定参数下能不能得到预期结果。这层测试不涉及模型,纯看代码逻辑,是保证技能本身不出问题的关键。第二层是端到端测试,模拟用户请求走完整链路,从模型意图识别到技能调用到结果返回都验证一遍。这层测试的目的是看模型是不是真的会在正确的场景下调用这个技能。
做端到端测试时,我发现一个很实用的技巧:维护一份“用户请求-技能调用预期”清单。比如,“用户说我要退掉刚买的那个订单”应该命中create_refund_order技能;用户说“查一下我买了什么”应该命中query_order_list技能而不是退款技能。每次调整技能库后,跑一遍这份清单,准确率的涨跌一目了然。
4. 常见故障与排查经验
4.1 技能调用不准确:八成是描述问题
如果你发现Agent频繁在错误的场景调用技能,或者压根不调用,第一个排查对象是技能描述而不是模型。描述写得太抽象、缺少触发场景示例,是这种情况的头号原因。
排错的步骤是这样:先复现出错误调用的对话;然后把这个对话里的用户请求单独拿出来,跟命中技能的描述对比,看两者语义相关性到底强不强;如果相关性确实不强,说明描述编写有问题,要补充更具体的场景词;如果描述已经够清晰但模型还是选错,可以考虑把容易混淆的其他技能名称改得更具区分度、在描述里加“不要用于XX场景”之类的负向提示,能帮助模型排除错误选项。
比较典型的一个例子是,我原来有一个“查询订单”技能和一个“获取订单详情”技能,描述分别写了“查询用户订单列表”和“获取单个订单详细信息”。模型经常在用户说“看看我的订单”时调用“获取订单详情”,因为用户消息里的“订单”和技能描述里的“订单”发生了模糊匹配。我把前者的描述改成“查询用户订单列表。适用场景:用户想看所有订单、想知道近期买了什么、想找某个订单的物流信息。当用户提到‘我的订单’‘订单列表’等关键词时优先使用本技能。单个订单详情查询请使用获取订单详情技能。”,准确率立刻提起来了。
4.2 参数错传:要在技能内部二次兜底
模型正确选择了技能,但传入的参数是错的,比如把用户输入的“去年买的那个东西”直接原样传给日期参数。这类问题不能只靠模型自我修正,技能内部要做兜底。
我的经验是给关键参数加“别名映射”和“值解析”两个处理层。别名映射解决“同一个东西在不同说法下怎么对应”的问题,比如用户说“今年”“本年度”时,映射为当前年份。值解析解决“格式不标准”的问题,比如日期参数接过来时可能是“今天”“下周”“2024年8月”,技能内部统一做一次标准化解析,转成标准格式。
实现时可以用这种策略:参数解析函数先尝试格式解析;不满足要求时,对值做意图化解析,比如结合当前时间的“相对时间解析”;再不行就直接向用户澄清,而不是带着错误参数往下走。把“寻求澄清”也纳入技能处理逻辑,能大幅减少因为参数不对导致的流程中断。
4.3 流程中断与超时:状态恢复是关键
在多步流程技能执行过程中,如果某一步因为外部接口超时或临时失败而中断,整个流程就可能卡死。我最初的处理方式是:让技能直接返回失败,由模型决定下一步。但模型在这种情况下往往只会重复尝试同样的事,或者给用户一个含糊的“暂时无法完成”。
后来我改成了一套更稳健的恢复机制。技能维护执行状态,每完成一个原子步骤就持久化一步状态;如果中断发生,先把当前状态写入技能日志;下一次相同技能被调用时,如果检测到已有未完成的执行状态,就允许从断点继续而不是从头开始。这个机制听起来简单,但对流程型Agent的稳定性提升很明显——用户不会因为一次网络抖动就被迫重头再来。
对于超时处理,我的默认策略是“至多重试两次、退避递增”。第一重试等待2秒,第二次等待5秒,如果仍失败则放弃并返回详细错误信息。这个阈值参数可以按技能的实际耗时调整,但原则是不要让用户等太久,也不要把错误掩盖在无限重试里。
4.4 性能与成本:技能缓存与并发控制
技能层的过度调用会直接体现在token消耗上。一个常见的问题是,同一个技能在上下文里被反复触发,比如用户连续追问多轮订单相关信息,每轮都重新调用“查询订单”技能。解决方案是做技能结果缓存——同一个技能、相同参数、在有效期内(比如5分钟)直接复用上次的结果,不再真正执行调用。
缓存粒度要注意:不应该按“技能名+全部参数”做缓存键,因为用户可能只是稍微改了一点查询条件;更合理的做法是利用技能内部的查询特征字段生成缓存键。比如订单查询技能,以“用户ID+订单ID”为缓存键,比以整个参数集合为键的命中率高得多。
并发控制也是技能层必须考虑的事。一些外部工具有速率限制,技能层如果不做限流,Agent并发一高就会被接口封禁。我在技能层统一加了一个简单的并发信号量,按照技能类型分别设置最大并发数,超过阈值的请求排队等待。这套限流逻辑在框架层实现,不污染技能内部逻辑,算是工程上比较省心的处理方式。
5. 进阶实践与个人经验总结
5.1 动态技能注册:让技能库可以热更新
技能体系最大的好处之一是可以“持续生长”。我把技能注册表放在独立配置中心,框架启动时加载,配置中心变更时自动热刷新。这样新增一个技能、调整一段描述、修改一个参数规则,都不需要重启服务。对于线上Agent来说,这点非常实用。
热更新时要处理一个问题:正在执行中的技能不要中断。我的做法是版本化管理技能定义,每个技能有版本号,热更新后生成新版本,执行中的实例继续用旧版本逻辑跑完,新调用一律使用新版。等旧实例全部结束后,再清理旧版本定义。
在新技能上线后,也要注意“新旧技能干扰”的问题。技能增多后模型更容易搞混,新技能刚上线时会有一段不稳定的观察期,建议先灰度发布,比如只对5%的流量开放新技能调用,观察几天确认没问题再全量放开。这个灰度机制可以通过配置中心的开关实现,成本很低但收益很实在。
5.2 人与Agent的共同改进循环
技能体系上线以后要持续迭代,不能当作一个一次性的开发任务。我把“线上问题反馈”和“技能迭代”直接打通:每次模型调用技能失败,都会记录进日志;每周复盘一次失败日志,看有没有因为描述不清晰导致误撞,有没有因为参数规则不合理导致频繁报错。
这个复盘循环是技能体系能持续变好、Agent稳定性越来越高的核心动力。如果只是建好技能库就不管了,过几周再看大概率又会出现新问题,尤其是你更新了模型版本、加了新工具之后。以周为周期做技能迭代,比时不时大改一通要稳健得多。
在我自己的项目里,这个体系跑了两三个月,Agent任务成功率肉眼可见地提升。更重要的是,出问题后的排查路径变得清晰了——要么是模型对技能选择错误,要么是参数映射不正确,要么是技能内部执行异常,每个类型的解决手段都不一样,但都有明确的处理方式。我心里始终有一条原则:尽量少依赖模型的“临场发挥”,把能确定的逻辑都放进技能里;模型只做规划、选择和表达,剩下的交给技能。这几句话虽然朴素,但确实是做agent-skills这件事最核心的方法论。