同学,如果你正在做Agent项目,迟早会碰到这么一种尴尬:功能越加越多,提示词越来越长,工具函数散落在各个文件里,想加一个新能力就像在改一座摇摇欲坠的积木塔。agent-skills这个词,直译过来是“智能体技能”,背后其实是一套工程方法——把Agent能做的事,从“代码里的分支判断”升级成“有元信息、有校验、有版本、能路由的独立技能单元”。它解决的核心问题不是“让模型更聪明”,而是让能力变得更加可控、可复用、可组合。
我搭建agent-skills这层专项能力之后,最大的感受是:项目复杂度终于和业务复杂度解耦了。以前每加一个能力都要动主流程,现在只是往技能库里多放一个技能。这篇文章我会从为什么需要技能机制开始,讲到一套技能的标准结构、编排路线,再给出一份可以直接照做的搭建步骤,最后把我实操半年踩过的坑一次性讲清楚。适合正在做智能体开发、研究AI Agent落地,以及手里已经积累了一大堆“能力碎片”想整理成库的朋友,看完就能动手。
1. 为什么Agent项目需要一套Skills机制
很多刚起步的Agent项目,第一版都是把能力直接揉进系统提示词里。比如“你是一个助手,你可以用工具A,你可以用工具B,当用户提到C的时候你要调用工具A……”这套方案在最早期是能跑的,技能就两三个,怎么揉都不乱。但等到技能到了十个以上,问题就开始冒头了。
1.1 从提示词模板到可复用技能
提示词模板不是不够好,而是不适合做“能力管理”。它有几个天然缺陷:第一,能力描述和业务指令混在一起,模型对语境的理解越深,越容易在不需要的时候调用某个能力;第二,没有统一的元数据结构,工具函数、外部接口、文本处理规则各有各的写法,想对一个能力做版本回退、灰度下线,根本无从谈起;第三,每个能力的边界靠“人记住”而不是“系统约束”,谁改提示词谁负责,久了就变成灾难。
所以我在中期项目里换了个思路:把每一个能力抽象成一个技能单元,用一套固定格式去描述“这个技能什么时候用、输入什么、输出什么、失败怎么办”。技能单元本身不关心整个业务流程,它只对自己的能力边界负责。这个抽象看起来简单,实际解决了三个问题——描述标准化、加载可配置、调用可监控。从此新增能力和改动主流程之间几乎没有耦合。
1.2 技能机制在Agent架构里的位置
我们把Agent的工作流分成三层:感知层负责接收用户输入和上下文;决策层负责判断当前任务应该调用什么能力;执行层负责真正把能力跑出结果。技能机制就是执行层的核心。
决策层通常是大模型,它擅长做选择和规划,但不擅长稳定地执行重复动作。技能层则相反,它不需要“聪明”,它需要“可靠”。同一份数据处理规则用函数实现,十次结果都一样;如果交给模型每次现想,十次可能有五种写法。技能机制的定位,就是让“决策”和“执行”彻底分离:让大模型只做粗粒度判断,具体怎么做交给技能里的固定逻辑去完成。
我见过很多团队在决策层做大量工作,却忽略了执行层。结果就是模型换了几版、提示词调了一轮又一轮,效果波动依旧很大,因为出问题的往往是技能执行细节,不是模型的判断能力。
1.3 什么样的项目需要它,什么样的项目不需要
技能机制也不是银弹,小项目上了反而增加维护负担。我自己的判断标准是这样的:
- 能力数量少于5个:不需要。直接写函数调用,或者全部揉进提示词都行,怎么简单怎么来。
- 能力数量在5到20个之间:需要一套最基本的技能注册表,至少要统一技能配置的数据格式,明确每个能力的输入输出边界。
- 能力超过20个,或者需要多人协作:必须上系统的技能库方案,包括版本管理、依赖声明、检索策略、测试回归。
- 团队规模超过三个人:哪怕能力少,也建议从第一天就定好技能命名规范和数据格式,否则后面收不回来。
说白了,技能机制解决的是复杂度管理问题。它不会让单个技能跑得更快,但能让整个Agent项目在能力数量增长的时候不失控。
2. 技能结构拆分:一份能跑的agent-skills到底长什么样
说清楚为什么,接着聊怎么设计。技能不是一个prompt模板那么简单,它是一组完整的能力定义。我建议每个技能至少包含五个部分:元信息、触发条件、执行体、输入输出校验、失败回退。下面逐个拆开讲。
2.1 技能元信息设计:给模型看的接口注释
元信息是技能的门面,尤其那个description字段,直接决定了模型会不会正确地调用它。很多开发者写description喜欢用散文风格,写得像产品宣传语,这是大忌。模型的调用判断依赖的是这个文本的“区分度”和“稳定性”,描述里的每一项信息都要能帮助模型做决策。
我常用的最小元信息结构大致是这个样子:
name: summarize_logs version: 1.2.0 owner: agent-toolkit description: | 接收一组工作日志文本,提取其中的关键进展、阻塞点和明日计划。 仅在用户要求总结日志、整理周报材料时使用。 不用于生成新的工作计划,也不用于分析代码仓库。 trigger: keywords: [日志, 周报, 总结, summarize] inputs: logs: type: array items: string description: 日志条目列表 outputs: summary: type: object properties: progress: array blockers: array next_plan: array depends_on: [] retry_policy: max_retries: 1 idempotent: true你注意看description里那两句“不用于”的描述,这是负例。我踩过的坑是,刚开始把description写得特别“正面”,结果模型在用户随口说“帮我看看这周情况”的时候也把它当成日志总结来调用。加上负例之后,误触发率立刻降了一大截。负例是防止模型把技能泛化理解的重要手段。
2.2 触发条件与意图匹配:防止技能迷雾
技能的触发有三种思路:关键词规则匹配、向量检索匹配、模型意图分类匹配。很多教程一上来就推荐向量检索,但我在实践中发现,如果技能数量在几十个这个量级,关键词规则匹配反而更稳定,而且好调试、可解释。向量检索适合技能数量很大、触发条件描述模糊的情况,但它有个问题——召回的候选技能没有稳定边界,调试成本高。
我现在的方案是“两段式路由”。第一段先用轻量规则做粗筛,比如通过trigger关键词把候选技能从几百个缩到五六个;第二段把缩出来的候选技能描述交给决策层,让它精挑细选。这样做的好处是,决策层每次看到的内容都是可控的,不会因为在几千个技能里做选择而迷路。这也顺便解决了技能列表越来越长导致的“技能迷雾”问题——模型拿到全量技能列表时,注意力会被无关能力分走,两段式路由把这个风险限定了范围。
2.3 执行体的三种形态
技能的执行体不只是一种形式,按用途不同分成三类:
| 形态 | 适用场景 | 优点 | 需要重点管控的方面 |
|---|---|---|---|
| 提示词模板型 | 文本加工、格式整理、报告生成 | 易于修改、不需要编译打包 | 输出格式不稳定,需要校验输出结构 |
| 函数执行型 | 计算、数据转换、文件读取 | 稳定可复现、可做严格校验 | 参数校验、边界处理、异常捕获 |
| 外部API型 | 对接业务系统、第三方服务 | 能力边界大、能接入真实世界数据 | 鉴权、配额、超时、错误码归一化 |
我自己用得最多的是函数执行型。原因很简单,凡是能写成确定逻辑的,就不要让模型每次临场发挥。比如“提取本周日志里所有阻塞点”这件事,用代码来处理的结果比模型即时处理稳定得多,尤其是处理数据量大的时候,函授型技能几乎不会出错。提示词模板型适合那些需要模型语言生成能力的场景,比如写摘要、润色、生成结构化报告,但使用这种类型一定要配合输出校验,否则模型偶尔会给出非约定的格式,下游任务很头疼。
2.4 前置条件、后置条件与失败回退
技能不能只有正常路径,异常路径同样重要,甚至更重要。因为在实际运行里,技能调用失败的频率远高于预期。我见过不少技能在成功时返回数据很漂亮,但一旦失败,直接抛一个英文报错,整个Agent跟着断掉。这个问题在设计时必须想清楚。
失败回退要做到结构化:返回错误码、返回人类可读的失败原因、返回可以给到决策层的修正建议。举个例子,外部API型技能请求超时的时候,不能直接返回“timeout”,应该返回类似这样的结构化信息:“请求上游服务超时,错误码E002,建议稍后重试或降低查询范围”。这样决策层拿到后才能做出合理判断,是重试、换参数,还是告诉用户当前能力不可用。另外,技能最好在元信息里声明是否幂等。幂等的技能可以放心重试,非幂等的技能重试前必须确认上一次调用是否已经生效,否则容易制造重复数据。
2.5 目录结构与加载机制
到项目后期,技能会组织成目录结构。我推荐这样组织:
skills/ summarize_logs/ skill.yaml execute.py prompt_template.md tests/ case_01.json case_02.json format_report/ skill.yaml execute.py read_files/ skill.yaml execute.py加载机制上,建议“懒加载加索引缓存”。只把技能元信息和索引结构在启动时加载进内存,执行体代码等真正被调用时才加载。很多框架喜欢应用启动时把几百个技能全量加载,这种方案在技能数量超过五十个之后性能明显下降,而且新技能发布以后还要重启整个应用,非常难受。懒加载之后,新增技能只需要刷新索引或者调用一个reload接口重建缓存,不用重启应用进程。
3. 编排与路由:让技能从“单个能用”变“组合好用”
单个技能能用只是第一步,Agent项目真正有挑战的是把多个技能串起来,完成一个复杂任务。编排这块,我从实践里总结了三件事:上下文控制、路由策略、依赖管理。
3.1 为什么不能把全部技能一次性塞进上下文
有些开发者偷懒,启动Agent时把所有技能定义全部放进上下文,让模型自己选。技能数量少的时候这没问题,数量大了就是在给上下文增加噪音,模型的选择准确率下降,而且每次都把全部技能定义传给模型,运行成本会随着技能数量线性上涨,这种效率是不可接受的。
我自己早期栽过这个跟头。项目里有二十几个技能,全部塞进上下文后模型偶尔会调用一个完全没关系的技能。排查后发现,它误读了一个相似描述,而那个相似描述恰恰夹在它关注的路径附近。从此我改成两段式路由后,这个问题基本绝迹了。
3.2 检索与路由策略的三种落地级别
技能路由不用一上来就搞复杂的向量库,我推荐阶梯式路线:
- 级别一,规则路由。每个技能配置一组触发关键词,用户输入命中哪组关键词就优先推荐对应技能。适合技能数量少、触发场景清晰的阶段,可解释性最强。
- 级别二,规则加向量混合。先按关键词粗筛,再用向量检索在粗筛结果里排序。适合技能数量多、描述相似度高的阶段,效率高但引入了向量库的维护成本。
- 级别三,模型路由。用一个专门的轻量子模型来做人选决策,定期用测试集评估它的选路质量。适合技能数量极大、已经形成技能依赖图谱的成熟项目,但评估成本最高,不推荐早期使用。
我在项目中期长期停留在级别二,理由很简单:向量检索召回的结果虽然语义相关,但会出现“语义像不代表业务上该用”的情况,必须靠关键词和规则兜底。你如果刚起步,从级别一开始,几个星期也许就能摸到合适的配置手感。
3.3 技能依赖与调用链的编排流程
技能与技能之间不是孤立的。比如“生成周报”这个任务,涉及到读取日志、总结要点、渲染报告、发送邮件四个步骤,其中每一步都对应一个或两个技能。这些技能之间存在清晰的上下游依赖关系。
依赖管理两条建议。第一,在技能元信息里显式声明depends_on字段,应用可以基于它构建技能依赖图,检测循环依赖。第二,每个任务的技能调用要记录完整的调用链,包括调用顺序、每个技能的开销、耗时、输出摘要。有了调用链日志,排查问题的时候可以按trace_id把一次任务的完整轨迹拉出来,只看对话记录,看不出是哪一个技能出了毛病。
我自己会为每个复杂的任务建一份简单DAG模式。执行的时候按依赖顺序逐个调用,前一个技能的输出作为后一个技能的输入,每个节点都有超时和失败重试策略。不用过度设计,保持“技能能独立调,也能被依赖”这两个特性就行。
3.4 监控、评估与性能成本
最后是监控体系,我维护五个核心指标:技能命中率、误触发率、技能成功率、端到端任务成功率、单链路token消耗。前四个衡量质量,第五个衡量成本。每次技能变更后,我都会拿同样的测试语料重新跑一轮回归,对比这几个指标的Diff。不要只靠肉眼感觉“好用了一点”,要落到数据上。
另外要建一个评估语料集,用历史真实对话里抽取的典型请求和维护好的期望技能链。这个语料集不需要很大,一两百条就能回归出大部分问题。每次技能库变动以后跑一次回归,比线上发现问题再去排查省太多力气了。
4. 从零搭建一套agent-skills的实操路径
理论部分讲完了,接下来给一套可以照着走下来的路径。我这套路径不需要任何特定框架,纯手动也能做,重点是把步骤和背后的选择逻辑说清楚。
4.1 第一步:盘点业务流程,拆成技能清单
先不写代码,先把业务流程拆成技能候选。拿最简单的“周报助手”举例,整个任务可以拆成这样的清单:
| 子任务 | 对应技能 | 主要输入 | 主要输出 | 技能依赖 |
|---|---|---|---|---|
| 读取工作日志 | read_files | 文件路径 | 日志文本 | 无 |
| 提取日志要点 | summarize_logs | 日志文本 | 结构化摘要 | read_files |
| 生成周报正文 | render_report | 结构化摘要 | Markdown周报 | summarize_logs |
| 发送周报 | send_message | 周报文本、接收人 | 发送结果 | render_report |
拆的时候要问自己三个问题:这个子任务是不是具备独立输入输出的最小单元?同样的输入输出会不会在别的任务里被重复请求?这个子任务的执行体是确定逻辑还是需要模型生成?如果前三步有任何一个问题答不清楚,说明拆得不够干净。
4.2 第二步:先写三个核心技能,跑通最小闭环
不要一上来就把清单里的所有技能做完,先挑三个组成最小闭环的:读取文件、总结要点、生成报告。跑通一次端到端之后,再补发送这个动作,完全来得及。
最小闭环的价值在于:它能在短时间内暴露整个链路里的结构问题。比如技能定义格式不合理、参数传递方式有歧义、输出校验缺少兜底,这些问题早在三个技能的时候就能看到,等做到三十个技能时再看就晚了。我接手过的项目里,有团队是一口气做了四五十个技能之后才考虑编排的,结果数据结构不统一,全部推翻重写,那才叫真正的浪费。
4.3 第三步:编写技能清单和最小执行体
技能清单用YAML还是JSON,取决于团队习惯,关键是格式一定要严格可校验。YAML可读性好,适合维护,我推荐它。执行体代码保持轻量,入口函数接收一个入参字典,返回一个字典,内部做参数校验、正常逻辑、失败回退。
一个最小化的函数型技能执行体大概是这样的:
def execute(request: dict) -> dict: logs = request.get("logs", []) if not isinstance(logs, list) or len(logs) == 0: return { "status": "error", "error_code": "EMPTY_INPUT", "message": "日志列表为空,无法生成摘要", "suggestion": "请先调用read_files技能读取日志,再调用当前技能" } # 这里放置真正的业务逻辑:去重、聚类、提取关键信息 result = build_summary(logs) return { "status": "ok", "error_code": None, "message": "摘要生成完成", "summary": result }这段代码看着简单,但它体现了几个原则:入参有类型和空值校验,失败返回结构化信息且带修正建议,成功返回也带status字段方便上层做判断。skill.yaml里的inputs定义要和execute函数的校验逻辑保持一致,两边都要维护,这是最容易漏的地方。
4.4 第四步:维护测试用例,用数据评估上线
每个技能旁边建一个tests目录,放几个典型输入输出对和典型的反面用例。反面用例尤其重要,它确保模型不会在这个技能不该被调用的时候误调用。就比如summarize_logs,反面用例就是“用户只问现在几点”,期望结果是模型不调用任何技能、不会把这句话当作日志总结。
上线评估方面,我用几个硬指标:技能命中率要大于90%,误触发率要小于5%,技能成功率要大于95%,端到端任务成功率要大于80%。不达标的技能不允许进生产环境,没有例外。这些阈值不需要多精细,它们的作用是让每一个改动能被“证伪”,而不是靠感觉说话。
4.5 版本管理与团队协作
技能库要当作正式代码来管,每个技能用语义化版本,major版本代表不兼容的变更,minor版本代表新增能力,patch版本代表描述或实现细节的修复。变更日志要记录清楚每条改动的背景和预期影响。团队里可以约定:谁改了技能,谁负责更新description、测试用例和变更日志,缺一不可。
如果团队里有多个人同时维护技能,建议把技能库放到独立仓库管理,应用层通过锁定版本号进行依赖引用。这样升级技能时可以先在测试环境锁新版本验证,通过后再切生产,比所有人直接改一个共享代码库稳得多。
5. 实操半年踩过的坑:排查技巧与避坑清单
最后这部分,我把在项目里真实遇到的问题整理成一个清单,每个问题都附带排查思路。这些坑在教程里一般看不到,但一旦踩上,非常浪费时间。
5.1 坑一:技能描述写得太“人性化”,模型频繁误调用
一开始我给summarize_logs写的description是“精准洞察日志中的关键信息,让每一份材料都有价值”。这个描述好看但没用,模型经常在“帮我整理一下思路”“总结一下情况”这种场景调用它。后来改成“接收日志文本,提取关键进展、阻塞点、明日计划。仅在用户要求总结日志或整理周报材料时使用”,误触发率立刻降了。
教训很直接:技能描述是给模型看的接口注释,不是给人看的宣传语。要准确、要具体、要带触发边界,还要带负例。如果拿不准自己的description写得好不好,就看测试集里的反面用例能不能全部拦下来。
5.2 坑二:参数校验缺失,模型幻觉直接传给执行体
早期有过一个日期查询技能,模型在用户问“上周的日志”时传了一个错误的日期范围,执行体没有校验,直接拿去查库,结果查出来一片空数据。更麻烦的是决策层看到空数据后没有意识到是参数错了,继续拿空数据往下游推理,整条链路的结果都失真了。
修复方案有两层:第一层是skill.yaml里用JSON Schema约束参数类型和取值范围,执行体入口再做一次防御性校验,不合规直接返回参数错误;第二层是在错误信息里附上修正建议,告诉决策层可能是日期格式不对、需要转成什么格式,让它有机会自我纠正。从此之后,我把“参数校验”当成技能的基本盘来看待,宁可不做业务优化,也不能不要校验。
5.3 坑三:技能库“烟囱化”,各人一套风格
团队协作最头疼的问题就是每个人用自己习惯的方式建技能。A同事的技能用Python命名风格,B同事的技能用业务命名风格,参数命名有的用缩写,有的用完整单词,依赖版本也不统一。到后面技能多了,交叉引用的时候谁都搞不清楚对方的接口约定。
根治方案两件事:一是定技能命名规范,比如技能名统一小写下划线;二是定公共依赖版本,基础类型库和常用工具函数统一维护,不允许各写各的。代码评审里把技能元信息和接口格式当作首要关注项,业务逻辑反而是次要的。接口错了,业务再对也没用。
5.4 坑四:找不到技能时的兜底逻辑没设计
总有那么一些用户请求,在现有技能库里找不到对应的能力。模型这个时候容易“自由发挥”,要么硬套不相关的技能,要么东拉西扯拼出一个答案。这两种情况都比真实地告诉用户“当前没有这个能力”更危险。
我的方案是,给Agent接一个技能未命中的兜底处理:当路由阶段没有找到置信度足够高的技能时,返回统一话术“当前能处理的范围限于……,如果这是你的高频需求,可以提交反馈帮助完善”。同时,把每次未命中记录到日志里,定期整理这些未命中请求,判断要不要补技能。这个机制既保护了用户体验,也给技能库的扩展提供了数据依据。
5.5 坑五:重试导致副作用,幂等问题被忽视
技能执行超时后,模型经常会自动重试一次。如果技能本身不是幂等的,可能造成重复提交。遇到过真实案例:发送通知技能超时后自动重试,结果用户收到了两遍通知,这类问题在业务系统里影响很大,会造成实打实的损失。
解决办法是在技能元信息里增加幂等声明:幂等技能可以放心重试,非幂等技能重试前必须检查上一次是否成功。对外部API型技能,尽量在参数里带上幂等键,服务端用幂等键去重;对自己实现的技能,也要在错误信息里给出“上一次调用是否可能已经生效”的上下文,让决策层能够做出合理判断。
5.6 坑六:性能优化不到位,技能库越大越卡
技能库刚建好的时候没觉得慢,后来技能数量超过五十个,每次会话加载全部技能配置的耗时越来越长,用户体验明显变差。排查后发现是加载策略的问题——全量加载了所有技能的定义和依赖配置,每开一次会话都要重复做一遍。
改成懒加载加索引缓存之后,启动时只扫描技能元信息,构建索引;执行体代码在技能实际被调用时再加载。新增或修改技能后,调用reload接口重建缓存,不用重启应用。这个改动让应用启动时间从几十秒降到了几秒,效果非常明显。建议做技能库的时候,加载机制从一开始就按懒加载来设计,别等服务量长上来再重构。
我这里最后再分享一个体会:技能库这套东西,最难的不是写第一个技能,而是技能数量到了几十个之后还能保持边界清晰、依赖干净。如果你手里的Agent项目刚好到了“越加能力越乱”的阶段,不妨花点时间把上面这六个坑逐条对照一遍,大概率能帮你避掉大部分隐患。