1. 为什么我把Agent的能力拆成了一份"技能清单"
先讲个真实的经历。去年我接手了一个某跨平台数据采集与汇总项目,初期方案很简单:给大模型塞了二十多个函数定义,让它按需调用。结果不出三天就翻车了——模型经常在错误的场景调用错误的函数,明明该走清洗流程的时候直接去调了分析接口,参数格式更是五花八门,同一个字段今天传字符串明天传数组。更头疼的是,每次调试都要在几十条工具调用日志里翻来翻去找原因。
后来我换了个思路,不再把能力当作松散的函数列表丢给模型,而是按照"技能"的维度重新组织:每个技能自带触发条件、输入输出契约、执行逻辑、典型用例,甚至明确写了"什么时候不要用这个技能"。改完之后,同样的任务准确率明显上升,调试定位问题的速度也快了很多。"agent-skills"这种组织方式,本质上解决的并不是"模型不会调用工具"的问题,而是"模型如何在复杂任务里正确编排自己能力"的问题。
1.1 从"工具调用失败"说起
先聊聊常见的失败模式。很多人在给Agent接工具的时候,第一反应就是把后端接口一个个包成函数,再把这些函数塞进模型上下文。这种做法表面上是"给了模型工具",实际上只是"给了模型一堆按钮"。模型并不知道每个按钮背后是什么、适合什么场景、和其他按钮之间是什么关系。
我见过最典型的情况是这样的:模型在某个业务场景里,面对五个功能相似的工具,经常选错。比如有个智能客服Demo,同时挂了"查询订单""查询物流""查询售后进度"三个功能,描述写得很模糊,结果模型面对"我的包裹到哪儿了"这个问题,直接调用了订单查询接口,返回了一堆订单号,就是没有物流轨迹。后来我把三个功能合并成了一个大技能"查询客服全流程信息",技能内部自己做了意图分发,效果立刻稳了。
这类问题的根子在于:工具列表是"平的",模型需要在所有候选项里做全局匹配。而技能是"分层的",模型先选场景,再选动作,决策路径更短、更清晰。
1.2 Skills到底是什么:与普通函数的本质区别
很多人把"agent-skills"理解成一套函数库,其实不太准确。函数只有一个"名字+参数+返回值",技能却是一整套能力契约,通常包含:
- 技能标识与名称:比如
fetch_web_content、analyze_sentiment; - 触发条件描述:什么时候调用它,什么时候不要调用;
- 输入参数Schema:结构化定义每个参数的类型、范围、默认值、必填性;
- 执行逻辑:技能内部是直接调API、执行代码、还是组合多个子步骤;
- 输出契约:返回什么结构,模型拿到之后应该怎么理解;
- 典型用例与反例:给模型看1-2条正确调用和错误调用的示例。
这套设计的目标是:让模型像人类一样"按需调用技能",而不是"碰运气选函数"。函数定义解决的是"能不能调"的问题,技能定义解决的是"该不该调、怎么调、调完之后怎么接"的问题。
1.3 适合用Skills解决的典型场景
不是所有项目都需要引入Skills体系。如果你的Agent只有一个简单的问答功能、后端接口总共不超过三五个,那么老老实实写函数定义就够了。但如果你遇到下面这些情况,就值得认真考虑技能化改造:
- 同一个能力被多个业务流程复用,比如"数据清洗"既要在爬虫流程里用,又要在报表生成流程里用;
- 工具数量超过十个,模型经常在相似功能之间纠结;
- 同一个任务需要多个步骤串联,比如"抓取网页→抽取正文→生成摘要";
- 希望能力和业务解耦:某个技能内部实现大改,但对外暴露的契约不变,业务流程完全不受影响。
我在实际项目中总结出一个简单判断标准:如果你的工具逻辑里已经出现了"如果用户在问XX,就用A接口;如果用户在问YY,就用B接口"这类描述,那就说明你该把这些分支收敛成技能了。
2. 一个Skills包从零到一的完整定义
聊完了概念,接下来是实操。我以一个很常见的场景为例:做一个"网页内容结构化"技能,输入一个URL,输出结构化的文章标题、正文、作者、发布时间、标签。这个技能看起来简单,但完整定义下来涉及不少细节。
2.1 技能清单文件:把能力写成配置
我习惯把每个技能定义成一个独立的配置文件,统一放在skills/目录下,而不是把技能描述直接写死在系统提示词里。这样做的好处是:每个技能可以独立测试、独立版本化、独立维护,不会因为一段描述改动导致整个系统行为漂移。
一个标准技能定义文件大概长这样:
name: web_content_structurer description: > 将指定网页URL转为结构化数据,提取标题、正文、作者、发布时间与标签。 当用户提供一个网页链接并希望获取文章核心信息时使用。 如果用户只是泛泛地问"网页上有什么",不要使用本技能,先用 fetch 获取页面内容再做判断。 input_schema: type: object properties: url: type: string description: 待解析的网页完整地址,必须包含协议头 http/https。 extract_type: type: string enum: [full, brief] default: full description: full 返回正文全文,brief 返回压缩后的核心摘要。 required: - url output_schema: type: object properties: title: { type: string } content: { type: string } author: { type: string } published_at: { type: string, format: date-time } tags: { type: array, items: { type: string } } execution: runtime: python entry: structurer.py timeout: 15 examples: - user_request: "帮我把 https://example.com/articles/123 这篇文章的结构提取出来" agent_args: url: "https://example.com/articles/123" extract_type: "full" - user_request: "这篇文章大概说了啥" agent_args: url: "https://example.com/articles/123" extract_type: "brief"配置文件本身不需要太复杂,但有几个字段值得专门说明。
2.2 描述怎么写才管用:触发条件与负例约束
我之前写技能描述走过一段弯路。一开始只写"该技能用于提取网页结构化数据",结果模型在用户问"帮我看看这篇文章适合发哪个平台"时也调用了这个技能——因为模型觉得"看看文章内容"和"提取网页数据"有关系。
后来我把描述改成了三段式:
- 正向触发条件:什么输入下应该调用,写得尽量具体,最好包含"URL""链接""网页""文章"这类强信号词;
- 操作说明:调用后能拿到什么、不能拿到什么,明确能力边界;
- 负例约束:什么时候不要调用,这点非常关键。
负例约束是最容易被忽略的部分。模型调用工具的常见问题不是"不知道该用",而是"什么都想用"。你在描述里明确写一句"如果用户只是在讨论网页话题,没有给出具体链接,不要使用本技能",模型的判断准确率会立刻上一个台阶。
我做过对比实验:同一批500条指令,描述里带负例约束的技能触发准确率从78%提升到93%。4. 负例约束的原理并不复杂——大模型在生成工具调用时天然倾向于"过度使用",因为调用工具看起来比不调用更积极、更负责。你明确告诉它"有些时候不要调",相当于给了它一个决策锚点。
2.3 输入输出契约:让LLM学会"正确使用"而不是"自由发挥"
输入Schema设计是另一个容易翻车的地方。我见过不少技能定义,输入参数只写了url: string,其他全靠模型猜。结果模型有时传https://...,有时传example.com/articles/123,有时干脆把整个页面内容塞进去。原因很简单:string类型对模型来说约束力太弱。
要解决这个问题,需要在Schema层面严格要求:
- 每个参数说明里写清楚格式要求,比如"必须包含协议头 http/https";
- 枚举值可以穷举的,一定要用
enum限制,比如extract_type: [full, brief]; - 参数长度、范围、格式用
minLength、pattern等字段兜底; - 必填参数和非必填参数边界清晰,避免模型自作主张加参数。
输出契约同样重要。很多技能定义了输出格式,但模型拿到输出后不知道怎么接下一步。比如web_content_structurer返回了tags字段,如果后续流程里需要基于tags做分类,那就要在输出契约里额外写一句"tags字段是后续分类技能的输入依据",或者在输出结构里预留classification_hint字段。
一句话总结:输入契约决定了模型能不能正确发起调用,输出契约决定了模型能不能正确消费结果。两边都要管。
3. 技能编排:单技能到工作流的组装逻辑
单个技能定义好之后,真正的复杂度出现在多技能组合的时候。同样一段用户请求,可能需要先调用爬虫技能,再调用清洗技能,再调用分析技能,最后调用报告生成技能。这个"串联"的过程,叫技能编排。
3.1 顺序编排:数据管线的串联方式
最简单也最常用的编排模式是顺序执行。以"竞品动态监控"为例,完整链路可能是:
fetch_web_content抓取目标页面;extract_main_content抽取正文;summarize_text生成摘要;format_markdown输出日报。
在这种顺序链路里,最核心的问题不是"怎么调用",而是"上一个技能的输出如何安全地传给下一个技能"。我在项目里遇到过输出侧字段命名不一致导致下游解析失败的情况,比如清洗技能返回的是cleaned_text,而摘要技能读取的是text,模型在中间自己做了字段映射,结果偶尔映射错误。
解决方案有两个,我会同时用:
- 统一经过一个共享的上下文缓存区:每个技能的输出都先写入缓存区,下游技能从缓存区取数据,不直接依赖上一个技能的返回值;。
- 在技能定义里写明输入输出之间的衔接关系:比如摘要技能的描述里明确写"输入取上下文缓存区中的
cleaned_text字段,若不存在则报错提示上游技能缺失"。
后者能让模型少做很多无谓的猜测。
3.2 并行与条件分支:什么时候需要真正的"编排引擎"
有些场景天然适合并行。比如用户同时要求分析三篇文章,那么三个相同的技能实例可以并发执行。但注意,我在实践中发现一个问题:如果只是靠模型自己并发调用工具,执行顺序、资源竞争、结果合并都需要额外兜底,模型很容易搞乱。
这时候可以考虑引入一个轻量的编排层,用代码控制技能执行流程,而不是全交给模型。比如用asyncio并发跑多个技能,或者用一个规则引擎根据上下文决定走哪个分支。
不过这里有个原则要把握:编排层只负责任务调度和资源管理,不应该处理业务逻辑。业务逻辑仍然留在技能内部。否则编排层会越来越臃肿,最后变成一个无人敢动的"巨石"。
3.3 技能间上下文传递的三种机制
多技能协作时,上下文传递是绕不开的话题。我试过三种机制,各有优劣:
- 显式传参:A技能的输出直接作为B技能的输入参数。优点是把依赖关系放在明面上,缺点是碰到长链路时参数列表会变得很长,而且一旦中间某个技能改动输出格式,下游全部要跟着改。
- 共享上下文存储:所有技能读写同一个上下文对象,A技能写字段,B技能读字段。优点是灵活,缺点是字段名如果没有统一规范,很容易出现"有人写
user_name,有人读username"。 - 消息总线:技能通过事件消息通信,A技能发出一个"内容已清洗"事件,B技能订阅后自动启动。这种机制适合组件化程度特别高的系统,但小项目用起来有点重,而且事件追踪成本不低。
我在大多数项目中用的是第二种,共享上下文存储,因为它在灵活性和可维护性之间最平衡。关键是约定一套字段命名规范,比如统一用蛇形命名、所有技能输出都挂在以自身技能名命名的子目录下,这样字段冲突的概率会大大降低。
4. 调试中反复出现的三类问题与完整排查链路
技能化改造做完之后,真正的战争才开始——调试。这一节我总结三类我反复踩过、也反复在群里看到别人踩的问题,每类都给出完整的排查链路。
4.1 问题一:技能描述越长效果反而越差
很多人有个误区,觉得技能描述写得越长、越详细,模型理解得越准确。我在一个项目里把某个技能描述写到了800多字,结果效果反而不如之前的200字版本。
排查链路是这样的:
- 先确认是不是描述内容有问题:把技能描述单独拿出来,逐句读,看有没有自相矛盾的指令;
- 再看是不是上下文被压缩:800字的技能描述占掉了大量上下文窗口,模型在处理长输入时反而丢失了关键信息;
- 用A/B测试验证:同一个技能分别用长描述和短描述跑同一批测试用例,对比调用准确率。
实测下来,长描述效果变差的原因往往集中在两点:第一,描述里掺杂了太多"背景知识"和"内部实现细节",模型消化不了;第二,关键指令被淹没在冗长文本中,提取效率下降。
我的经验是:技能描述控制在一屏以内,核心指令用"必须""禁止"等强约束词明确写出,背景说明尽量放到内部文档而不是暴露给模型。
4.2 问题二:宽松的参数Schema导致幻觉填充
有一次我的技能输入Schema里有一个source字段,描述写的是"数据来源,可选",实际调用时模型经常填一些毫无根据的值,比如"根据用户提问推测"。
排查下来发现根因是字段本身可填可不填,但模型倾向于把所有可见字段都填满,表现出来就是"幻觉填充"。
解决方式有三个,按优先级排序:
- 非必填参数直接从Schema里移除,等真正需要时再动态加;
- 如果非填不可,就把允许值限定在枚举范围内,或者加
default值让模型不用猜; - 输出侧增加校验逻辑,发现参数值和输入上下文不一致时,强制丢弃并由下游逻辑重新推断。
这类问题最坑的地方在于,它不会稳定复现,有时候填对,有时候填错,极难在测试阶段发现。所以我后来养成了一个习惯:所有技能上线之前,用20条边界输入做参数填充压力测试,重点看模型在不明确的场景下会怎么补参数。
4.3 问题三:技能间的上下文覆盖与循环调用
多技能协作中最难查的问题就是上下文互相覆盖。我遇到过一个真实案例:A技能会把处理完的数据写进共享上下文的result字段,B技能在处理另一个任务时也写result字段,结果下游的C技能拿到的是B的产物,导致整个链路输出错乱。
复现这个问题的排查路径,我建议按下面几步走:
- 在日志里给每个技能的每次调用编号,记录调用开始时间、参数、读写字段列表;
- 按时间线把所有技能调用和字段读写画出来(可以用简单的日志分析脚本,不要手工画);
- 找到字段冲突的交叉点,确认是覆盖还是并发竞争;
- 修复方式很简单:给共享上下文的字段名加上技能前缀,比如
web_structurer.result和analyzer.result,彻底隔离。
循环调用更隐蔽。模型在处理复杂任务时可能出现"技能A调用技能B,技能B的结果触发技能A再次调用"的无限循环。我的对策是在编排层加一个调用深度计数器,超过3层就强制终止并回退到兜底策略。
4.4 通用排查链路:复现、留痕、最小化验证
总结一下,调试技能调用问题,我的通用链路是四步:
- 复现:不能复现的问题不值得修。先用固定测试集反复触发目标场景,确保问题可以稳定出现;
- 留痕:给所有技能调用打日志,至少要记录触发上下文片段(模型的推理轨迹可以截断保存)、传入参数、返回结果、耗时。没有留痕,排查就等于盲人摸象;
- 最小化验证:把场景缩小到单个技能、单个参数,逐步加回复杂条件,定位影响因子;
- 回归:修复后跑一遍完整的测试集,防止修一个问题引出三个新问题。
这套链路看着朴素,但非常管用。我甚至会在项目刚开始做技能化改造时,就先把日志规范定好,宁可前期多花半小时,也不要在出问题时面对一坨没有记录的调用数据。
5. 把Skills用出生产力的几个配置习惯
最后聊几个偏工程实践的习惯。技能定义好了、编排跑通了,但距离"好用"还差一层——可维护性、可评估性和团队协作。
5.1 版本管理与回归评估
技能定义是代码资产,应该纳入版本管理。我在项目里每个技能都有独立的版本号,改动技能描述、Schema或执行逻辑时,都会同步更新changelog并跑一遍回归测试。
回归测试集不需要特别大,但覆盖面要够。我的做法是每个技能准备20条标准指令、10条边界指令和5条负例指令,改动任何一个技能都必须全部通过。这套测试集平时看起来麻烦,但在频繁迭代时能救大命——有一次我改了某个技能的输出契约,以为影响范围很小,结果回归测试直接发现下游三个技能全部解析失败,及时止损。
5.2 命名规范与目录结构
技能多了以后,命名规范决定了一个项目能不能持续维护。我的建议是:
- 技能名统一用"动作+对象"的形式,比如
fetch_web_content,不要用process1这种无意义名字; - 同一模块下的技能用前缀区分,比如
data_clean_*、data_analyze_*; - 技能描述第一句话必须能概括整体职责,方便快速筛选。
目录结构我习惯这样组织:
skills/ common/ fetch_web_content/ extract_main_content/ data_pipeline/ data_clean_remove_noise/ data_clean_deduplicate/ analysis/ sentiment_analyze/ topic_classify/ output/ format_markdown/每个技能目录里必须有skill.yaml、执行脚本、测试用例、说明文档。没有文档的技能,三个月后连作者自己都未必看得懂。
5.3 不要过度设计:技能粒度的判断标准
讲一个反直觉的经验:技能不是越细越好。我见过有人把"字符串转小写""数组去重"都定义成技能,让模型去编排。结果模型在真正的业务任务里浪费了大量上下文在"选择技能"上,反而忽略了核心需求。
我的技能粒度判断标准是三条:
- 可独立验证:这个技能能不能单独测试、单独评估,如果可以,拆分才有意义;
- 有实际决策价值:技能之间是否存在"模型需要区分的不同行为",比如"提取摘要"和"生成情感标签"是两种行为,值得拆开;
- 复用潜力:是否至少被两个业务流程需要,如果目前只有一个地方用,先不做拆分。
以最后一条为例,我经常提醒自己:暂时不需要一个技能,等真正出现第二个使用场景时再拆不迟。过早拆分只会增加系统的复杂度和维护成本,而收益微乎其微。
我在实际使用中发现,当技能数量控制在20个以内、每个技能的描述足够紧凑、命名清晰可辨时,整个Agent系统会进入一种很舒服的状态——模型调用准确率高、调试定位快、迭代风险低。一旦技能数量超过这个阈值,就要开始考虑分层分组了,而不是继续在单一列表里堆叠。
最后再分享一个小技巧:技能定义文件里,最好每年定期做一次"减脂",把那些实际从未被触发过的技能找出来,先停用观察,确认没有业务影响后直接删除。多余的技能不只是占用上下文空间,还会在关键决策时干扰模型的选择。
我踩过几次坑之后,现在看一个Agent系统的工程质量,第一眼就是看它的技能清单长什么样。好的技能清单,读起来像一个熟练员工的岗位职责说明——边界清晰、指令明确、知道自己什么时候干活、什么时候不干活。做到这一步,后面的大多数问题都不会来找你。