1. 为什么同一个模型不同团队跑出的效果差距这么大先讲一个我最近的观察。在同类项目里好几个团队用的是同一个基座模型甚至系统提示词都相差不大但最终跑出来的Agent应用效果却天差地别。有的团队做出来的东西像“一个人”在干活知道什么场景该用什么能力什么话该接、什么话不该接有的团队做出来的东西却像“一个混乱的对话机器人”用户说一句它答一句稍微绕一点就开始瞎猜。这中间的差距很多时候不是模型能力的问题而是“skills”这一层没做好。这里的“skills”不是指个人技能而是AI工程实践里常说的技能模块——一组结构化的能力描述文件。它告诉模型你手上有哪些能力、每个能力在什么条件下可以用、调用之后应该按什么步骤执行、输入输出长什么样。你可以把它理解成“给模型一份岗位说明书”而不是“给它一个提示词”。早期我也走过弯路以为把工具函数写清楚、把系统提示词写得详细一些模型自然就会用。但实际上模型面对一堆工具时经常出现两个极端要么在无关场景反复调用某个工具要么在该调用的时候完全不触发。问题就出在“工具只描述了接口没有描述行为边界”。这篇文章我想完整聊一聊如何从零搭建一套可复用的技能体系覆盖技能文件的编写规范、辅助模块设计、回归测试、部署后的持续维护。内容基于我最近在某个跨平台Agent项目里的一次完整落地过程代码和配置会尽量给出可以直接用的版本。适合正在做Agent应用、提示词工程、工具编排或者想把自己手里的“一堆工具”整理成“一套能力”的开发者。1.1 “会写提示词”和“会设计技能”是两回事要理解技能模块的价值先得回到一个核心问题语言模型本身并不会“主动调用能力”它只是在生成一段文本按照概率决定下一个token是什么。所以当你在系统提示词里写“你可以调用工具A、B、C”模型并不会真正理解“什么时候该用A、什么时候该用B”它只是把这句话当作生成时的上下文参考。技能模块做的事情是把“何时用、怎么用、用到什么程度”这些内容从模糊的提示词里剥离出来变成结构化的、可解析的配置。模型在执行时会先读取技能描述根据用户输入去匹配某个技能的触发条件匹配上了才进入该技能的调用流程。这句话说起来简单实操中的差异非常大。我见过一个团队把所有技能说明都堆在系统提示词里结果上下文一长模型在生成时总是倾向于调用描述靠前的工具后面的工具基本成了摆设。还有团队把技能描述写得像“API文档”满篇术语模型根本抓不住触发信号。后来我们统一了一套写法每个技能要有独立的描述文件包含用途描述、触发条件、输入输出结构、示例请求外加可选的依赖声明和边界约束。这样模型在做触发判断时拿到的是干净、聚焦的文本而不是一大段混在一起的信息。1.2 skills与插件、工具、工作流的边界在哪里很多刚开始接触这个概念的读者会问技能模块和插件有什么区别和工具函数有什么区别和工作流又有什么区别我自己的理解是工具是“最小可调用单元”比如一个查天气的API封装输入城市名输出天气数据插件是“代码级的扩展包”可能包含多个工具、事件监听、UI组件工作流是“固定顺序的执行编排”比如先查天气再根据结果推荐穿搭技能则是“行为级的决策单元”它定义的是模型在什么状态下做什么决策决策之后才去调用工具或执行某个流程。可以用一个生活化类比来理解工具像一把螺丝刀插件像一套工具箱工作流像流水线上的工位布置而技能是“干这个活的标准手法”——它告诉你拧螺丝应该用多大力、什么时候换批头、拧完要不要做检查。这张表可以说明它们的区别维度工具Tool插件Plugin工作流Workflow技能Skill核心内容单个函数的输入输出功能扩展包代码任务执行路径编排行为决策与使用规范面向对象模型调用平台集成任务执行引擎模型决策是否含判定逻辑否部分弱强典型问题模型不会主动触发安装复杂、体积大路径固定、不灵活描述不规范则难生效把技能和工具分开管理之后最大的好处是底层工具可以保持稳定而技能层可以快速迭代。模型触发错了不需要改代码改技能描述文件就行。这也是这类配置体系在Agent项目里越来越流行的原因。2. 从零写一份合格的技能描述文件我刚接触技能体系时犯过的最大的错误是照抄别人的技能文件结构。结构确实没问题但里面的描述写得太抽象。比如用途描述写“帮助用户解决天气问题”触发条件写“当用户需要天气信息时”。这种描述模型读了等于没读因为它没有形成足够强的触发信号。后来我总结出一套比较稳定的写法这里拆开来讲。2.1 命名规范和用途描述的写法命名看起来是个小事实际上影响很大。模型在匹配技能时通常会把用户输入和技能名称、描述做语义匹配。如果技能名叫“helper_weather”描述里全是“provide weather service”那么用户用中文问“明天穿什么”技能大概率不会被触发。我的建议是技能名称面向“场景”命名而不是面向“接口”命名。比如“查天气并给穿搭建议”就比“get_weather”更容易被命中“整理周报并发送摘要”就比“weekly_report_generator”好用。命名要尽量包含场景关键词让名称本身就承担一部分触发匹配的作用。用途描述是触发判断的核心。写的时候有一个原则用两句话讲清楚“这个技能在什么场景下做什么事”。第一句讲场景要具体到用户行为的可感知特征第二句讲动作要说清楚技能会调用什么能力、产出什么结果。举个例子下面这个描述比“帮助用户解决天气问题”好得多当用户询问某城市某日或近期天气情况并可能希望获得穿衣、出行建议时主动触发本技能。技能会查询目标城市天气数据结合温度与天气状况输出简要建议。它的关键在于第一句给了模型一个可判断的触发条件城市日期/近期天气询问第二句给了输出预期数据查询建议。这样模型在生成时如果遇到“北京周末冷吗”这类口语化请求也能自动匹配。2.2 触发条件、输入输出与示例请求的设计触发条件写得太宽模型会频繁误触发写得太窄又容易漏触发。平衡点是“以用户视角写条件而不是以系统视角写条件”。常用做法是列出“必须满足”的条件和“可选加分”的条件。比如查天气技能城市字段是必须的日期是可选的如果没有日期默认当天。描述里就要写清楚这种默认逻辑避免模型纠结于“用户没给日期怎么办”。输入输出结构最好和底层工具接口保持一致但要注意技能的描述文件里写的是“用户意图层”的参数不是“代码层”的参数。比如用户说“明早出门要不要戴帽子”技能需要解析出城市、时间、意图再映射到底层天气工具。这个解析规则写在技能描述里比写在代码里更灵活。示例请求能显著提升触发准确率。我一般会在每个技能描述文件里放3到5个典型请求并且特意包含一个“不触发”的反例。例如查天气技能里加一条触发示例“北京明天天气怎么样”“上海这周适合户外活动吗”不触发示例“帮我讲个笑话”——这条明确说明不属于本技能范围这个“不触发”示例是很多教程里不会提但实战极其有效的小技巧。它能够在描述层面给模型一个负向信号降低误触发概率。2.3 一份可以直接抄的yaml模板下面是一个我实际用过的技能描述文件模板结构做了精简保留了核心字段。你可以直接复制到项目里改成自己的场景。name: weather_with_advice description: | 当用户询问某城市某日或近期天气情况并可能希望获得穿衣、出行建议时主动触发本技能。 技能会查询目标城市天气数据结合温度、降水与风力情况输出简要建议。 triggers: required: - 用户包含城市地点 - 用户包含天气或气温相关意图 optional: - 用户包含具体日期或“今天、明天、周末”等相对时间 negative_examples: - 讲个笑话 - 帮我写一首诗 inputs: city: type: string required: true description: 用户询问天气的目标城市 date: type: string required: false description: 可选默认当天支持相对日期表达 output: format: text summary: 返回目标城市天气附带一至两句穿衣或出行建议 dependencies: - tool: weather_api version: 1.2 mapping: city: city date: date limits: - 只读取天气数据不修改任何用户设置这个配置的关键字段是triggers尤其是required和negative_examples。我在多个项目里测试过加了这两项之后技能触发准确率通常能从70%左右提升到90%以上。这也是推荐先抄这两个字段的原因。3. 把人设、依赖、记忆这些辅助模块也纳入技能体系技能文件本身是核心但一套真正好用的技能体系一定不只是“一堆yaml文件”。它还包含人设文件、依赖声明、记忆模块和安全边界。这些辅助模块用得好技能库会越跑越顺用不好技能库就会变成一个“谁也不服谁”的大杂烩。3.1 人设文件不是摆设它是触发判断的底层上下文人设文件描述的是Agent整体行为风格它不直接参与某个技能的判定但会影响模型在模糊场景下的选择倾向。比如同一句“随便推荐一个”如果人设是“专业严谨的顾问”模型更可能触发推荐类技能并给出结构化选项如果人设是“话痨朋友”模型可能会直接聊起来而不调用任何工具。我当时在项目里处理这个问题时把人设文件放在技能体系的入口位置所有技能共享一份人设但人设文件里会声明“当遇到边界场景时优先选择用户明确需求对应的技能而不是直接闲聊”。这句约束看起来简单却减少了大量误触发。人设文件建议包含三块内容角色定位、回答风格、边界声明。回答风格要写具体比如“使用简洁的日常语言不使用术语堆砌”而不是“语气友好”。模板化的友好描述对模型生成倾向的影响其实很弱具体的行为约束才有效。3.2 依赖声明与记忆模块如何协同依赖声明解决的是技能之间的调用关系。设计技能体系时如果A技能和B技能共用同一个底层工具最好把公共能力提取成一个基础技能让A和B分别依赖它。这样后续改公共逻辑时不需要同时改多个描述文件。但依赖声明有一个坑版本冲突。我记得有一次一个技能升级了参数格式从“文本输入”改成“结构化输入”但两个依赖它的老技能没有同步升级结果线上出现了间歇性报错。后来我们规定所有底层工具和基础技能必须带版本号技能描述文件里的dependencies字段写清version变更底层接口时必须先兼容旧格式再逐步迁移。记忆模块和技能的配合就更微妙了。结构化的短期记忆可以记录用户刚刚提到的城市、日期、喜好技能在执行时读取这些记忆就能减少用户重复输入。不过要控制记忆的写入范围不是所有对话都要存。比如用户说了“我不喜欢下雨天”这个信息值得存到用户偏好记忆里但如果用户只是说“帮我查一下”那就没必要存。实操时我给记忆模块设了一个简单规则只有“可持续影响后续行为”的信息才写入记忆。判断标准是“换一个话题之后这个信息还能用吗”。能用的存不能用的不存。这个规则让技能系统少了很多莫名其妙的“记忆污染”也减少了模型在触发判断时被无关记忆干扰的情况。3.3 安全与边界技能越权的典型场景技能权限边界是这类体系里最容易被忽略、出事后果又最重的部分。所谓“越权”就是模型在某个技能的流程里执行了不该由这个技能执行的高风险操作。典型场景是一个“查询订单状态”的技能底层工具返回订单数据也暴露了“修改订单备注”的接口。模型在一次执行中因为用户说“顺便帮我改一下”就顺手调用了修改接口。系统层如果没有拦截这个操作就真的执行了。我们针对这个问题做了两条硬约束。第一每个技能文件里必须有limits字段明确列出“本技能禁止执行的操作”第二在系统层对所有写操作做二次确认用户输入中必须出现明确的指令模型不能因为推理性猜测就执行写操作。这两条结合之后技能越权的风险降到了很低的水平。我建议任何想搭建技能体系的团队都把这套边界约束从第一天就加进去。4. 技能不是写出来就完了回归测试和版本管理才是长期工程很多团队在搭建技能体系时前期热情很高把技能文件写得非常完整但上线之后就不再维护。结果一两个月过去技能库里的文件越来越多触发准确率却肉眼可见地下降。原因很简单没有回归测试也没有版本管理。模型更新、用户表达习惯变化、新技能加入都会让旧技能的触发判断出现漂移。4.1 维护一个最小可用评测集没有评测集就谈不上回归测试。这里的评测集不是给模型打分用的复杂测试集而是针对技能触发和输出的一份“场景题库”。我当时建的第一个评测集只有六十条请求但覆盖了三类场景正常场景、边界场景、干扰场景。正常场景就是典型用户请求比如“北京明天天气”“帮我查一下上海后天会不会下雨”。边界场景是那些容易让模型犹豫的输入比如“明天要出门穿什么好”——用户没有直接提天气但隐含了天气查询需求。干扰场景则是“讲个笑话”“帮我写一首歌”这类不该触发天气技能、但描述里有相似词汇的输入。每条评测数据要标注三件事期望触发的技能、不允许触发的技能、期望输出的结构。不需要写得太细能判断“触对了没有、输出对不对”就够了。建立评测集时建议团队成员各写一部分因为不同的人对用户表达的预设有差异合在一起能盖住更多盲区。4.2 回归测试、效果度量和常见陷阱评测集建好之后每次修改技能描述、人设文件、工具接口都要跑一遍评测。我习惯用一个简单的脚本把评测结果输出成表格测试项输入期望技能实际触发输出结构是否通过正常北京明天几点日出日出时间查询天气查询文本建议否这个表格看起来简陋但它能快速暴露出两个最常见的陷阱。第一个陷阱是“只测新技能不跑老技能”。某次我更新了一个搜索技能的描述新技能本身触发得很好结果连带影响了三个老技能的触发优先级如果不跑全量评测根本发现不了。第二个陷阱是“评测集太久没更新”。用户表达习惯会变尤其当你的应用上了新功能后评测集里没有对应的新场景回归测试就失去了参照意义。效果度量方面我主要看两个指标触发准确率和输出合格率。触发准确率指“该触发的触发了、不该触发的没触发”的比例输出合格率指“触发之后产出结果是否符合预期格式和内容”。这两个指标分开统计比只看一个综合分更能定位问题。比如触发准确率很高但输出合格率低通常是描述文件里的output字段和实际执行不一致反过来输出合格但触发准确率低问题大概率出在triggers字段。4.3 用版本号和实验记录管理技能迭代技能描述文件本质上和代码没有区别也应该用git管理。我在项目里为技能体系单独建了一个仓库每次修改都走“分支-评审-合并”的流程。技能文件的文件名里不加版本号版本信息统一放在文件头部或仓库的release标签里避免文件名越改越长。版本管理还要配套“实验记录”。每改一版技能描述我都会记录这次改动的假设、评测集结果、线上观测结果。比如“把触发条件加了 negative_examples认为可以减少‘讲个笑话’误触发评测集误触发率从12%降到3%上线后一周触发率稳定”。这份记录看起来费时间但真正到了“技能触发率莫名变化”需要回查的时候它会救命。灰度上线也值得做。技能描述文件的修改不像代码改动没有编译报错错误往往是静默的。所以我会让改动先作用于一小部分流量观察24小时触发准确率和用户反馈确认没问题再全量生效。这一步很多人觉得麻烦但经历过一次“新描述上线导致全站乱触发”的事故之后我再也不敢跳过它。5. 部署之后从触发率日志到技能库健康度技能体系上线不代表工作结束真正的维护工作才刚刚开始。我看到过太多“上线即跑路”的项目技能触发率掉了没人管日志里堆满失败调用新技能该加的没加老技能该退役的还占着位置。一套健康的技能库是需要像看业务报表一样定期打理的。5.1 监控什么、怎么分析技能相关的监控指标不应该只看“调用次数”。我一般会盯四类指标技能触发率、调用成功率、平均响应时长、用户侧反馈标签占比。触发率是“该技能被触发的次数”除以“评测集或业务流中该技能预期出现的次数”。如果触发率连续下降说明技能描述可能被更新影响或者用户的表达习惯变了。调用成功率是“技能完整执行成功”的比例失败原因通常是底层工具超时、参数解析出错、依赖技能版本不匹配这些需要结合日志分类处理。日志分析的关键是把“失败”拆开看。同样是“查询天气”执行失败可能是因为城市字段没解析出来可能是因为天气接口返回了异常数据也可能是因为模型在中间步骤擅自改写了参数导致下游工具收到错格式。这几种情况的修复方式完全不同前者改技能描述里的输入解析中间要改工具兜底逻辑后者则要去查模型为什么会改写参数。5.2 一次从失败日志反推技能问题的完整案例说一个我印象比较深的案例。某次上线一个新功能后我们观察到“查天气”技能的触发率在一周内下降了20%左右。最直接的表现是用户明明在问天气对话却直接进入闲聊模式没有任何工具调用。第一步是看日志。我们把“疑似应该触发天气技能但未触发”的对话样本拉出来发现大量出现同一类型的用户表达“明天那个天气能出门吗”。这句话里有“天气”关键词按理说应该触发但没有触发。第二步是回到评测集。我们把这句话加进评测集里重现了问题。然后开始二分定位先怀疑是新增技能抢占触发于是临时禁用新技能结果问题依旧再怀疑是人设文件改动影响了判断翻看git记录发现前一天有人把“使用简洁的日常语言”改成了“使用直接明快的表达”。这个改动看起来没有大问题但新表达里“直接”这个措辞让模型在某些模糊请求下更倾向于“直接回答”而不是“调用工具”。第三步是修复。我们把人设文件的表达重新调整同时给天气技能描述补了一条触发示例“明天那个天气能出门吗”。上线后触发率恢复并且评测集里的误触发率没有反弹。这个案例的启示是技能体系里所有文件都是相互关联的。人在解决一个问题时不能只看一把螺丝刀要顺着整条链路去排查。5.3 技能库演进避免“积灰技能”和“过度膨胀”最后聊一聊技能库的长期演进。技能库是会腐烂的新技能不断加入老技能没人用但也没人删到最后触发判断变得混乱因为模型要在越来越多的候选技能里做选择选择成本一高准确率就会下降。我定了一个“技能库体检”流程大概每三个月做一次。第一步拉出近三十天的触发率数据把“触发次数几乎为零”的技能列为候选退役对象。第二步对退役候选技能做一次评测如果评测集里还有它的场景说明是触发判定出了问题需要调整描述而不是删除如果评测集里已经没有它的场景直接归档把文件移到一个“deprecated”目录保留历史记录但不参与运行时加载。第三步检查技能之间的依赖如果有技能的依赖工具超过三个考虑合并或者把这些依赖包装成公共基础技能。新技能的加入也要有门槛。单项技能上线前至少要有一套评测数据、一次灰度观察、一次文档记录。没有这三样东西技能库就只是一个堆文件的文件夹。我现在回头看这套项目里最值钱的部分不是某个技能本身而是“知道每个技能为什么存在、什么时候撤掉”的那张地图。我自己这几年的感受是技能体系最大的敌人不是模型能力不够而是“没人对它负责”。如果你正在搭类似的东西建议你先别急着写满二十个技能先维护好三个技能、一份评测集、一套日志记录跑通了再逐步扩容。技术的背后都是笨功夫这功夫下够了模型自然会越用越顺手。