前阵子在重构项目里的Agent模块时,我盯着一个个散落的工具函数发了好一阵呆。它们什么都能干,但谁也不听谁的话:有的要JSON输入,有的只吃字符串;有的会自己记状态,有的每次都要把上下文从头传一遍。新来的同事想加一个功能,得先读半小时代码才能搞清楚该调谁。那段时间我一直在想一个问题——我们到底是在写Agent,还是在给Agent挖坑?
后来我把这套东西重新梳理了一遍,把每一个可以被模型调用的能力都收拢成结构化的“技能”,也就是标题里说的agent-skills。这篇文章不是讲某个具体的框架怎么用,而是把我整理这套技能体系时的完整思路、踩坑记录和经验沉淀写出来,包含技能如何定义、如何编排、如何管理、如何测试,以及一套可以直接拿来改的模板。适合刚接触Agent开发、或者正在被“Agent代码越写越乱”困扰的开发者参考。
1. 先搞清楚agent-skills到底在解决什么问题
1.1 一个重新审视Agent的能力单元
之前有次跟朋友聊天,他说自己用大模型写了个自动化脚本,效果还行,就是每次加需求都像拆炸弹。我想了想,这不怪他,因为很多人一开始上手Agent都是从“给模型一个提示词,再挂两三个函数”开始的。那时候代码体量小,怎么折腾都行,等工具函数超过十个、任务链路变长,问题就来了。
问题的根源在于:我们一直在用“函数”的思维管理Agent的能力,而不是用“技能”的思维。
函数是给程序调用的,特征是参数严格、返回确定、边界清晰。但Agent调用能力时不是这样的,模型需要在一次对话里动态判断“此刻该用哪个能力”“这个能力需要什么信息”“用完这个能力之后下一步是什么”。这些决策需要的不只是函数签名,而是围绕每一个能力单元的完整描述、约束、依赖和失败处理。这套完整封装,就是我说的agent-skills。
1.2 为什么说技能库是Agent工程的“地基”
如果你只是写个Demo,把几个函数塞进工具列表就够了。可一旦你的Agent要处理真实业务,比如自动查库存、生成报价单、跟进客户回访记录,情况就完全变了。模型选错工具、传错参数、调用顺序乱掉,任何一个失误都会直接体现在业务结果里。
把能力整理成技能,最大的好处是让“能力”本身具备了可维护性。每个技能是独立的,改动一个技能不影响其他技能;每个技能是自解释的,模型看到技能描述就知道什么时候该用、怎么用;每个技能是可测试的,你可以单独验证技能的行为是否符合预期。这跟软件工程里“模块化”“单一职责”是同一个道理,换到Agent的世界里,技能就是最小的能力单元。
当然,技能体系带来的另一个隐形成本是管理复杂度。你需要思考命名的规则、版本的迭代、依赖关系的处理、甚至技能的召回问题,这些说起来都是细节,但每一个细节都能让你在线上环境里付出代价。这篇文章后面很大篇幅都在讲这些事。
2. 技能定义的四个关键层次
2.1 描述层:让人和模型都能“看懂”技能
我见过很多人写技能描述,一句话带过:“获取天气信息。”这种描述放在工具列表里,模型确实能知道它可以获取天气,但遇到更复杂的场景就露馅了。比如这个技能实际要求输入城市的中文名还是拼音?是返回实时温度还是预报全天?是否需要额外参数如语言偏好?这些信息描述层不写清楚,模型就只能靠猜。
一个合格的技能描述应该包含几部分:这个技能是干什么的、什么场景下调用、输入的限制条件、输出的格式、以及典型的调用示例。别小看调用示例,模型对示例的敏感度远高于抽象描述,一个准确的正例经常比三行说明文字更有效。我自己写描述时还会把“什么情况下不要调用”也放进去,这能明显减少模型乱调用的现象。
2.2 输入输出层:严格的结构化契约
如果说描述层是给模型看的说明书,那输入输出层就是给程序定的契约。我的经验是:所有技能的入参和返回值都必须用JSON Schema定义清楚,字段类型、是否必填、枚举范围、嵌套结构,一样都不能少。
为什么这么严格?因为模型天然会在参数上发挥创造力。你定义了一个startDate字段,模型可能传“2024-05-01”,也可能传“五月一号”,还可能传一个时间戳。没有校验和转换,下游程序迟早要爆炸。我通常在技能内部做三层防护:第一层用Schema校验入参,不合规的直接拒绝;第二层做类型宽松转换,比如允许字符串形式的数字自动转成数值;第三层对枚举值做映射,把模型常用的口语化表达转成程序需要的标准值。
返回值也一样,我坚持让所有技能返回统一的JSON结构,不要返回一串格式化好了的文本让上层去解析。文本最终用于展示可以,但内部流转必须用结构化数据。这个规则一开始觉得麻烦,坚持下来会发现调试和扩展都轻松很多。
2.3 执行层:逻辑与工具的边界
技能的执行层是真正干活的代码,但它不是随便写写就行的。我在这一层最看重的是“确定性”:同一个输入,在相同环境里必须产生相同的输出。你可能会问,Agent调用外部API,结果本身就是不确定的,这个怎么保证?我的意思是逻辑路径的确定性。API返回什么我们控制不了,但拿到返回之后怎么处理、怎么组装响应、怎么处理错误,这些逻辑必须是稳定可预期的。
另一个边界问题是:一个技能该多“大”?拆得太细,模型要调五六个技能才能完成一个任务,上下文和耗时都受不了;拆得太粗,技能变成一个大杂烩,复用的可能性就没了。我常用的判断标准是“业务动作的自然边界”。比如“创建订单”是一个技能,“计算订单总价”是另一个技能,“发送订单确认邮件”再单独一个。前一个的产出是后一个的输入,链路清晰,每个技能都可以独立被其他需求复用。
2.4 元信息层:依赖、权限、成本、版本
最后这层是最容易被忽略、但线上问题最多的部分。技能要跑起来,往往不只是“调用一个API”那么简单,它可能依赖某个内部服务、需要某个身份权限、消耗一定的费用、还可能有执行超时限制。这些信息我全部塞进技能的元信息里。
举个具体的例子,我们有一个技能需要调用第三方的短信服务,每条短信都有成本。如果模型在循环里反复调用它,账单就会很难看。后来我在技能元信息里加了成本等级和每日调用上限,又在描述里写明“仅在用户明确要求发送短信时才可调用”,问题就解决了。权限控制也是同理,不是所有技能都该让模型自由执行,涉及写操作、支付、删库的技能必须有额外的确认机制。
3. 技能编排:从逐个调用到组合协同
3.1 线性编排与条件路由
单个技能定义好之后,真正的复杂度出现在编排层。最简单的编排是线性链路,比如“查询天气→决定穿搭→生成建议”,前一个技能的输出作为后一个技能的输入,顺序基本固定。这种链路实现起来最简单,用代码硬编码流程都行。
但现实需求很少这么乖。用户一句“帮我安排这周末的出行计划”,可能涉及天气、导航、餐饮、景点门票等多个技能,而且顺序因人而异。这时候就需要条件路由:模型根据当前上下文判断下一步调哪个技能,或者你的编排引擎根据技能间的依赖关系自动决定下一步。我实现路由时主要看两个信息,一是当前任务的进度状态,二是各技能声明的前置依赖和产出能力。比如某个技能声明自己需要“目的地信息”,而上一步恰好有技能产出了“目的地信息”,路由就自然指向它。
3.2 上下文窗口与记忆管理
编排过程中最头痛的问题之一:上下文太长。每个技能调用时,你都得把相关的历史信息塞给模型,技能一多,Token很快就爆了。我试过把完整对话历史一直带着,效果是模型什么都记得,但响应速度和服务成本一起飙升。
后来我改成两层记忆:短期记忆存当前任务的完整执行轨迹,长期记忆只存关键结论和结构化摘要。技能调用时,短期记忆完整喂给模型,长期记忆则按需检索,只有跟当前步骤相关的部分才加载进来。这套做法牺牲了一点点模型的“全知视角”,但换来了可接受的成本和速度,实际使用中模型的判断质量并没有明显下降。
还有个细节,技能本身的输入输出如果很长,没必要全部塞进对话历史。我会在技能返回后从上下文中摘除冗余的中间结果,只保留结构化摘要。这样既不影响后续技能对关键数据的访问,又省了大把Token。
3.3 ReAct循环里的技能调度
说到编排,绕不开ReAct模式,也就是“推理→行动→观察→再推理”这个循环。在我维护的技能体系里,每个技能的调用其实就是一次“行动”步骤,模型的推理文本中会声明要调用哪个技能、传什么参数,执行完的结果作为“观察”继续喂回去。
这里我踩过一个坑:模型在推理文本里声称要调用技能A,结果JSON参数里写的却是技能B的参数,导致执行器直接报错。排查了很久,发现问题出在我给的技能定义格式不统一,有的有明确的JSON调用示例,有的只有描述没有示例。统一格式之后,模型的调用准确率高了不少。你可以把每次调用都做成“技能标识+参数对象”的标准结构,所有技能一律这样调用,模型适应起来非常快。
4. 技能库的工程化管理
4.1 命名规范与版本管理
技能一多,命名就先乱起来了。我见过有人管技能叫“get_user_info”、“fetchUserData”、“查询用户”,三个名字说的是同一件事,这种混乱在Agent场景下很致命,因为模型是通过名字来识别技能的。如果同名技能有不同实现,模型可能随机选中一个,哪里出错都不知道。
我后来定了一套命名规则:一律小写字母加下划线,前缀按领域划分,动词开头描述动作。示例:order_create、order_query、user_profile_get、message_send。同时每个技能必须有语义稳定的唯一ID,ID不允许变更,即使内部实现升级了也保留同一个ID,靠版本号区分差异。版本管理我用的是语义化版本,每个技能独立打版本号,升级时保留旧版本一段时间,方便灰度回滚。
4.2 技能缓存:让重复调用不再烧钱
技能调用是有成本的,不只是钱,还有时间。有些技能比如“查询用户最近订单”,如果用户短时间内反复触发,每次都去查数据库就太亏了。我在技能执行层加了一个可选的缓存机制,key由“技能ID+入参哈希”生成,缓存过期时间按技能类型单独配置。查操作可以缓存几秒到几分钟,写操作禁止缓存。
缓存这块我最想提醒的一点:一定不要缓存包含敏感数据的技能结果,比如用户身份证号、支付信息。就算内部网络再安全,缓存数据多一份留存就多一份风险。安全起见,涉及隐私查询的技能我干脆禁用了缓存。
4.3 测试与回归:技能也要跑CI/CD
很多人写Agent代码不做测试,理由是大模型行为不确定,没法测。这话我只认同一半。模型的行为虽然不确定,但技能本身的执行逻辑是确定的,完全可以测。我在每个技能旁边放一个测试文件,包含正常输入的用例、异常输入的用例、边界值的用例,跑通了才算数。
更重要的是一旦技能升级,所有依赖它的编排链路都需要回归。我搭了一个最小回归环境:把常用的编排路径做成脚本,输入一些典型的用户请求,检查技能调用的顺序、参数、产出是否符合预期。模型选错技能这类问题不一定能完全通过代码测试拦截,但至少能和之前的表现做对比,偏差特别明显的时候能尽早发现。回归测试跑完,我基本心里就有底了。
5. 常见问题与排查经验实录
5.1 模型频繁调用错误技能
这个是我遇到最多的问题,模型放着精确匹配的技能不用,偏要选一个看起来相关但实际不对口的。排查时我先看技能的描述是不是有歧义,再看是不是缺少更合适技能的“不适用场景”描述。有一次是模型的调用示例不匹配,每次都在示例里传了多余字段,技能校验拒绝了,模型又带着这个失败信息继续尝试其他技能,搞得日志一长串报错。
最终的解决办法是给每个技能补充了正反两面的调用示例,正面示例写清楚正确的入参格式,反面示例标注哪些情况不该用这个技能。改完这一轮之后,误调用的频率肉眼可见地降了下来。
5.2 技能并发执行的资源冲突
有段时间我们让Agent同时执行多个技能,结果数据库连接被占满了。查了半天,问题出在几个热门技能共用同一个数据库连接池,并发一高就排队。后来把技能和底层资源的关系理了一遍,高频技能单独分配连接池,低频技能走公共池,并且给每个技能设了并发上限。超过上限的调用直接返回“暂时繁忙”,让Agent稍后重试。
这件事给了一个很重要的教训:技能库不只是逻辑层面的抽象,也必须是资源层面的治理单元。每个技能对资源的消耗你得心里有数,不然上线之后随时可能爆。
5.3 上下文被技能输出撑爆
有个场景是技能把一份很大的报表全文放进返回值,结果下一轮对话时模型直接把报表内容复述了一遍。你说它错吧,它确实回答了;但Token花得让人心疼。我的做法是把大体积返回内容存储到外部缓存,技能只返回一个引用ID和摘要,模型需要详细内容的时候,由另一个专门的技能按ID去拉取。这样一来,主上下文的长度基本保持稳定,费用问题也大幅改善。
另外两个小经验:第一,日志里记录每个技能调用的输入输出大小,方便定位哪些技能是Token消耗大户;第二,技能返回尽量精简,只返回必要的数据字段,别把底层API的原始响应原封不动透传出去。
5.4 技能升级导致旧链路不可用
有一次我优化了一个技能的入参结构,把原来必填的字段改成了可选,结果另一条编排链路立刻报错。后来一看,那条链路在编排时依赖了那个字段做条件路由,字段一可选,路由逻辑就失去了依据。
所以升级技能时,我会先对所有引用它的链路做一次依赖扫描,看有没有哪些字段被外部当作关键判断依据。简单暴力的做法是“只加字段、不改字段、不删字段”,新版本开头几版尽量保持向后兼容,确认稳定之后再考虑清理废弃字段。
6. 一套可以直接落地的技能模板
6.1 从示例开始的推荐结构
讲了这么多原则,你上手的时候还是需要一盘模板。下面这个结构是我目前在用的,你完全可以直接复制过去改。
{ "id": "order_create", "version": "1.2.0", "name": "创建订单", "description": "根据用户选择的商品和收货信息创建订单。仅当用户明确表示要下单时使用。如果用户只是询问价格或库存,请使用 order_query 或 stock_query,不要使用本技能。", "tags": ["order", "trade", "write"], "input_schema": { "type": "object", "properties": { "product_ids": {"type": "array", "items": {"type": "string"}, "minItems": 1}, "address_id": {"type": "string"}, "coupon_code": {"type": "string"} }, "required": ["product_ids", "address_id"] }, "output_schema": { "type": "object", "properties": { "order_id": {"type": "string"}, "total_amount": {"type": "number"}, "status": {"type": "string", "enum": ["pending", "confirmed"]} }, "required": ["order_id", "total_amount", "status"] }, "cost_level": "medium", "timeout_ms": 5000, "permission": "user_confirmed", "cache_policy": { "enabled": false, "reason": "创建订单属于写操作,禁止使用缓存" }, "examples": [ {"input": {"product_ids": ["SKU1001", "SKU1002"], "address_id": "addr_001"}, "output": {"order_id": "ORD20240501", "total_amount": 299.00, "status": "confirmed"}} ] }字段的含义你应该能猜个大概,我再说几个要特别注意的。cost_level是给调度器做决策用的,费用高的技能会在描述里被弱化,模型不那么优先调用;permission字段我常设成user_confirmed,这意味着技能执行前必须拿到用户的明确确认,防止Agent自作主张;cache_policy对写操作一定要关掉。
6.2 用一层抽象统一异构的Agent框架
最后说一个扩展方向。不同公司的Agent底层用的框架可能不一样,有的基于ReAct,有的基于Plan-and-Execute,但它们需要的技能能力是类似的。后来我加了一层适配器,把技能库本身和具体的Agent框架解耦。技能库里维护的是标准化的技能定义和标准执行接口,框架通过适配器把技能翻译成自己理解的工具格式。
这样一来,同一套技能库可以从一个框架迁移到另一个框架,模型换了、Prompt模板换了,技能本身不用重写。这也是我认为agent-skills这个方向最值得投入的地方,它让Agent的能力资产沉淀下来,而不是每次都从零开始。
如果你现在手头已经有一堆Agent工具函数,不妨挑一个高频场景,试着按上面的模板整理成第一个技能。从那个瞬间开始,你会明显感觉到Agent的代码开始变成可以维护、可以演进的东西了。