如果你亲手搭过几个AI Agent,大概率会遇到这样一个现象:模型单看很能打,可真让它连续完成好几件事,不是长上下文失忆,就是在一个任务上反复打转。我后来把项目里所有给Agent用的能力描述、工具说明、角色约束、示例对话统一收进一套“agent-skills”体系之后,才真正把这种漂移控制住。这篇文章不是概念科普,而是一份我在生产环境折腾了大半年的实践总结,重点讲清楚技能怎么定义、怎么组织、怎么调试、怎么迭代,适合正在做Agent编排、自研工作流或者接各种Agent框架的朋友。下面写的是我从多个项目里沉淀出来的通用做法,不是某个框架的官方规范,但换到不同平台上基本都能落。
1. agent-skills到底是什么:从“会聊”到“会干活”的那层胶水
先说一个容易被忽略的事实:大模型本身不具备“稳定完成一个任务闭环”的能力,它只具备“根据上下文生成下一个合理片段”的能力。你让它聊天、写文案很轻松,但让它连续做“查数据、做判断、输出结构化结果”这种带确定流程的活儿,它就会暴露问题:不知道什么时候该用什么工具,做到哪一步算完成,出错之后该怎么办。agent-skills补的正是这一层。
我给它下了一个比较朴素的定义:技能是Agent完成某个任务时需要的一组结构化指令、判定规则、工具调用序列和输出约束的集合。它把一次任务从“让模型自由发挥”变成“按流程办事”。对比一下就清楚了:工具是单个动作,比如调用一个API、执行一段SQL;技能是多个动作的组合,可能还包含分支、重试和失败处理。提示词只是“说话方式”,技能则是“做事的完整章法”。
1.1 技能不是插件,也不是提示词
不少团队一开始会把Agent能力拆成工具清单和提示词两块,但这两块都回答不了几个关键问题:模型知道“有哪些工具可用”,但它不知道“当前这个用户请求到底该匹配哪套流程”;提示词能约束语气,却约束不了判断顺序。插件更多是一个载体,强调的是代码包如何被加载和注册,而不是任务逻辑如何被组织。
所以在我的项目里,工具、插件、技能是三个不同层。工具永远是最小的执行单元,负责“把某件事做掉”;技能负责“围绕某个业务目标把步骤排好”;插件则承担“把一批工具和技能打包分发”的职责。用大白话打比方:技能是岗位上的SOP,工具是SOP里用到的螺丝刀,提示词是贴在工位上的沟通礼貌须知。螺丝刀再好,SOP混乱,活儿照样干不利索。
这个区分直接影响了代码结构。如果你发现自己某个技能文件里塞了大量工具实现,那说明你把工具和技能混在一起了。真正的技能应该只有“流程编排逻辑”和“对工具的描述与调用约定”,工具本身还是独立封装,这样技能才能在不同项目之间搬运。
1.2 一个技能该包含哪些东西
我习惯用一套五段式结构来写技能,不管业务复杂度如何,都先按这个框架起底:概述描述、触发条件、参数协议、执行流程、输出规范。
概述描述是给模型看的说明书,说明这个技能解决什么问题、适用边界在哪里。触发条件用来判断当前请求是否命中技能,这里不只写“什么情况该触发”,还要写“什么情况不该触发”。参数协议定义了调用技能需要哪些输入字段、类型、是否必填、默认值。执行流程是核心,必须拆成模型能理解的具体步骤,步骤之间要有明确的输入输出衔接。输出规范则包括格式、篇幅、引用要求,防止模型自由发挥。
实际写的时候最常见的错误是只给“你要做什么”的清单,没有给“什么情况算失败、失败后怎么处理”。我现在每个技能的执行流程里一定包含失败分支,比如查数据接口没返回时,是直接结束,还是换一个口径重试。没有这个分支,模型遇到异常就容易开始编。技能文件本身建议用YAML或JSON承载,因为结构化格式可以被编排器解析,自动化注册和校验,也能进版本库做diff。纯自然语言写的技能看起来友好,但没法做自动化检测,项目一大人就麻了。
2. 技能库的顶层设计:粒度、分层与复用边界
单点技能好写,真正麻烦的是当技能数量多起来之后,整个“技能库”怎么组织。如果只是把几十个技能文件丢到一个文件夹里,路由和选择很快就会变成灾难。这里我更愿意先讲顶层设计,因为它决定了后面所有工作的复杂度边界。
2.1 原子技能与复合技能怎么切
我在多个项目里反复调整后,最后稳定在两级拆法:原子技能 + 工作流技能。原子技能只做一件不可再分的事,比如“提取订单号”“查询订单状态”“格式化金额”;工作流技能负责把多个原子技能串起来,比如“订单售后处理”,先提取单号、查状态、判断是否在售后期、再生成回复话术。
拆得好不好,一个很实用的判断标准是:能不能用一句话说清这个技能的目标。一句话说不清,说明粒度太大;如果拆完发现好多技能之间频繁互相调用、边界重叠,说明原子层拆过头了,需要合并。
我还会拿真实历史对话做场景化检验:找十个典型输入去跑技能匹配,如果同一个输入经常同时命中多个技能,而且模型需要用户二次选择,就说明技能揉得不够;如果某个技能内部步骤超过五个,就往下拆一层。净凭感觉设计粒度,上线后一定会被真实的用户表达打脸。
2.2 技能库的分层管理:公共层、业务层、个人层
技能库不能做成一个大平层,至少分三层:公共技能、业务技能、个性化技能。公共技能跟具体业务无关,比如“信息检索”“文本润色”“代码格式整理”;业务技能绑定到特定场景,比如“电商售后处理”“医疗报告解读”,里面会带行业术语和业务规则;个性化技能则只服务特定用户或角色,比如“按小明的习惯把周报改成表格加批注”。
分层的主要目的是控制权限和影响范围。公共技能一改,所有Agent都会受影响,所以必须走严格的回归测试;业务技能影响范围收窄到单个子系统;个性化技能只对特定会话生效,可以快速试错。我踩过一个很典型的坑:把业务技能里的专有名词写进了公共层,结果另一个无关项目触发了一个预期外的调用,排查了很久才发现是技能内容互相污染。
用Git管理技能库时,我按目录分层,每个技能一个文件,命名里带版本号。这样用diff就能清楚看到这个技能的判定逻辑在哪个版本变了、为什么变,出了问题也能快速回滚。团队协作时,公共层由核心维护者合入,业务层由各业务owner维护,个人层完全自治,权责清晰。
3. 从零实现一个可落地的技能模块
讲完设计,进入实操。我会用一个非常常见的“查快递进度”场景做例子,带你把一个技能从定义文件写到被Agent调用。这里的关键不是业务本身,而是那种“既能被模型看懂、又能被编排程序解析”的写法。
3.1 技能清单格式:给模型和编排器都看一眼
我的技能定义文件用YAML,一个技能一个文件,结构大概长下面这样。你可以直接抄这个框架,后续再根据业务调整字段:
id: express_query_v1 name: 快递进度查询 description: 查询快递物流轨迹并生成简洁的进度摘要。只处理国内快递单号,不处理国际件。适合用户询问包裹到哪了、预计几天到。 trigger: - 包含快递单号且命中物流相关意图 - 用户直接说“查一下物流” exclude: - 用户同时询问多个包裹的运费对比 params: tracking_no: type: string required: true description: 快递单号 carrier: type: string required: false description: 快递公司简称,可为空,空则自动识别 steps: - name: 校验参数 action: validate rule: "单号长度在8-32位且由字母和数字组成" - name: 识别承运商 action: tool_call tool: carrier_detect input: "{tracking_no}" - name: 查询轨迹 action: tool_call tool: express_query input: "{tracking_no}, {carrier}" - name: 生成摘要 action: llm_response prompt: | 基于轨迹数据,用不超过3句话说明当前状态、最新动态和预计到达时间。 如果数据异常,明确提示暂无法确认,不要编造。 output: format: text max_length: 120 require_source: false fallback: - condition: "查询失败且原因不是参数错误" action: "换一种承运商口径重试一次" - condition: "重试仍失败" action: "告知用户稍后再试,并让用户核对单号"这里有两个读者:模型会按steps里的步骤执行,编排器会读取trigger、params、fallback做技能路由和参数填充。值得强调的一点是,description里不要堆太多内容。过长的描述反而会稀释模型的注意力,它只需要知道“解决什么问题、边界在哪、遇到什么情况别碰”就够了。
3.2 执行逻辑:从触发条件到反馈闭环
技能被触发后的第一件事不是执行步骤,而是做参数对齐。用户说“帮我看看快递”,但没给单号,这时不应该让技能内部暴露出“缺参”的错误,而是把会话状态切到技能的“待填参数”阶段,继续追问。
所有必填参数齐了之后才真正执行steps。步骤之间我用明确的参数名做衔接,比如第3步的input写成{tracking_no}, {carrier},这样无论是人看还是程序解析,都知道上一步产生了什么、下一步消费什么。
反馈闭环也容易被忽略。技能执行完,除了给用户最终答案,还要返回一段执行痕迹,记录调了哪些工具、哪些步骤走了fallback、耗时多久。这段痕迹的价值在可观测性和诚实回答。比如当结果来自备用接口时,Agent能主动说“这个结果不完全确定,因为我用的是兜底渠道”,比硬撑着给一个错误答案要靠谱得多。
3.3 注册与调度:让agent在正确时机拿起正确技能
有了技能定义文件,接下来要解决的问题是:Agent怎么在对话中知道该用哪个技能。做法分两个极端:一种是把所有技能定义全部塞进系统提示词,让模型自己按文本执行;另一种是走专门的技能调度器,做向量检索加规则路由,再由技能内部的steps驱动模型。前者简单但不可扩展,技能一多上下文就爆;后者工程重,但能承受生产负载。
我的生产项目用中间路线:轻量调度器先基于embedding相似度加trigger规则做初筛,把候选技能压到两三个,然后让模型在few-shot示例引导下做最终选择。初筛的核心价值是把决策范围缩小,而不是把几十个技能全塞给模型。
注册机制上,我维护一份技能清单索引,包含id、名称、描述、参数模式、版本号。每次服务启动时扫描技能目录,自动生成索引并打印注册数量。新增技能时只需放一个文件,不用改编排代码,这对后续扩展非常重要。
4. 踩过的坑:技能冲突、幻觉和上下文爆炸
这部分是生产环境里最值钱的记录。很多问题在demo阶段根本看不出来,只有用户量起来、对话变长之后才暴露。我按实际踩坑频率排一下,希望对你有帮助。
4.1 两个技能都想接管同一个请求怎么办
最早遇到的痛点是技能互相打架。用户说“帮我总结一下这几个文件”,结果“文档摘要”技能和“文件内容对比”技能同时命中,模型随机挑了一个,输出完全不符合预期。
解法有两个层次。第一层在技能定义里加“排除条件”,明确不适合处理的情况。比如在“文档摘要”技能的exclude里写“不处理两个及以上文件的对比,对比请走文件对比技能”。这看起来是语法边缘的补充,却对意图识别非常有效,因为模型做技能匹配时往往更依赖显式的边界信号。
第二层在调度器里做重叠检测。每次新增技能时,我会跑一遍已有技能描述和trigger的相似度,如果重叠度过高,就主动合并技能或调整边界。这样做之后,技能之间打架的概率至少降了一个数量级。
4.2 中间结果出了问题,根因经常在工具层
最隐蔽的错误是工具返回的数据本身是脏的,但模型不知道,直接把原始数据当成正确结果用了。比如快递查询接口返回“异常签收”字段,模型可能直接理解为“已签收”,但业务规则里“异常签收”意味着用户拒收或地址错误。
现在我会在每个工具返回值外面加一层schema校验和规则映射,把接口返回的status_code映射成技能内部统一的枚举值,比如success、retryable、failed。模型只接收映射后的值,不再直接看原始字段。这样技能执行逻辑和上游接口实现天然解耦,上游改字段名也不会让技能全线崩溃。
排查这类问题必须依赖执行痕迹。我会把每个步骤的工具入参、输出摘要、耗时、状态码写进结构化日志,出现问题先在痕迹里定位是第几步挂的,再决定是修技能逻辑还是修工具适配层。反复调提示词而不看数据,是在错误方向上努力。
4.3 上下文窗口不够用?把技能当压缩器
长任务场景下上下文爆炸几乎躲不掉。让模型边调接口边累积全文,它会很快忘记最早的业务前提。我的办法是把技能中间过程设计成“状态摘要”,而不是“原始记录”。
例如一个多轮数据清洗任务,每个步骤结束时不是把完整中间表追加进上下文,而是让技能生成一段紧凑的状态摘要,包含当前行数、已处理字段、异常计数。下一轮执行只基于摘要继续,原始过程放到外部存储。
这种方法等于把上下文折叠成状态机。代价是摘要可能丢细节,所以我在摘要模板里强制要求“可能影响后续决策的关键字段必须保留”。如果某个技能设计时没有想清楚后续依赖什么,那么这个技能就是不合格的,上线后迟早出问题。
4.4 常见问题速查表
| 故障现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 该触发技能时没触发 | 描述里缺少正向触发词,或意图被exclude误伤 | 检查exclude条件是否过宽;用真实用户query跑一遍embedding命中 |
| 不该触发时误触发 | 描述边界太宽,缺少“不能做什么” | 为技能补充exclude或“不处理”条件 |
| 技能内部步骤中途跳出 | steps逻辑太模糊,模型不知道该按什么顺序走 | 把模糊表达改成明确规则;减少单个技能步骤数量 |
| 结果不稳定,时好时坏 | tool入参或返回没有统一schema | 加参数校验和状态码映射,模型只消费固定枚举值 |
| 上下文在长任务中溢出 | 中间过程原始数据全部停留上下文中 | 改为步骤级状态摘要,原始数据外部存储 |
| 两个技能同时命中 | 技能边界重叠 | 加exclude,调度器做重叠检测,必要时合并技能 |
5. 技能库的维护、评估与进化
技能库一旦跑起来,就需要当产品来维护,而不是写完就完。这一节讲我怎么评估、发版和排优先级,都是比较朴素但可靠的做法。
5.1 评估一个技能好不好,不能只看成功率
很多人评估技能只看“任务完成率”,但完成率会掩盖很多问题。我现在增加三个指标:触发准确率、步骤稳定性、异常接管率。触发准确率看的是该触发时有没有触发、不该触发时有没有误触发;步骤稳定性是同输入跑十次,脚本路径是否一致;异常接管率是真正需要fallback时,兜底逻辑有没有生效。
每两周我会挑一批真实会话日志,把一个技能的所有命中记录拿出来人工标注,算这四个指标。这个工作确实耗时,但比离线评测集更贴近生产环境,因为真实用户说法跟测试集经常完全不同。
触发准确率低于80%,我会先怀疑描述里有没有误导内容;触发正常但步骤不稳定,重点查步骤顺序是否写得太模糊。像“适当处理”“根据情况判断”这类词,能不用就不用。
5.2 技能版本更新的正确姿势
技能受提示词、参数、工具逻辑共同影响,我已经把它纳入版本管理,同时遵守几条硬约定:不修改已发布技能的id,要改就新增v2;描述或trigger变化时同步升级版本号;发版前跑一次快速回归,至少五条代表性输入。
背后原因是Agent Skills和普通代码不一样,它的“逻辑”一部分是模型行为,会跟旧会话交互形成惯性。直接原地改线上技能,旧缓存和新行为混在一起,非常难查。新增版本号虽然会让技能库多出一些重复文件,但换来了可回滚、可对比、可观测。
我还会在技能文件里写一行changelog注释,例如“v2: 增加承运商自动识别规则,修复首条轨迹显示异常”。半年后再回来,至少不用考古。
5.3 我自己的落地顺序建议
如果你是从零开始建技能库,我的建议是“先窄后宽”。先挑一个高频、边界清晰的任务做成第一个技能,跑两到四周,把格式、调度、监控整条链路跑通,再逐步扩展相邻业务。
不要一开始就搭大而全的技能市场。技能库里每多一个技能,路由决策负担就多一分。我看到过一些项目,热情地建了七八十个技能,结果模型选技能的时间比执行技能还长。先有稳定的深度,再考虑宽度,是最务实的路径。
最后想说,技能设计不能只交给开发。一定让业务人员参与定义“什么叫执行成功”。我现在的项目里,每个技能立项都有一页“验收定义”,里面包含三个真实用户案例、一个负面案例、一个边界案例。这一页纸是后面所有技能配置的地基,花的时间完全值得。