做Agent开发最头疼的是什么?不是模型选型,也不是算力,而是每次加需求的时候,都要往那条核心Prompt里再塞一段逻辑。prompt越写越长,日志越来越乱,模型开始答非所问,你还要对着几千字的system prompt逐行排查到底是哪句话把行为带偏了。
我经历过这个阶段,后来项目里引入了一套独立的技能管理机制,代码仓库名就叫agent-skills。它不是某个现成的框架,而是一套把Agent的“能力碎片”单独拆分、命名、注册、编排的工程化方案。这篇文章就是围绕这套方案的一次完整复盘,从为什么需要技能体系、怎么设计技能包,到一个技能从零开发、测试、部署上线的全过程,再加上我把系统跑出问题之后整理的避坑记录。想把Agent从“demo玩具”做成“可维护的系统”,这份资料应该对你有用。
1. 为什么Agent需要“技能”,而不是一条超长Prompt
先说个我自己的感受:prompt工程的边际收益衰减得特别快。前500字的效果提升很明显,到2000字以后每多写一句话,模型行为的不确定性反而加大。这不是玄学,背后有几个很实际的原因。
1.1 超长Prompt的三个隐藏代价
第一个代价是注意力稀释。Transformer的注意力机制虽然理论上能关注很长的上下文,但实践中模型对分散在长文本角落里的指令,响应稳定性会下降。你把“遇到退款问题先查订单状态”写在sys prompt第30行,它可能执行得很好;写到第300行,它就容易忘。
第二个代价是冲突式的指令覆盖。业务需求不可能永远一致。A场景要求语气简短,B场景要求详细解释,C场景要求带emoji。全堆在一个prompt里,模型会在冲突指令之间随机摇摆。你今天测出来是A,明天同一套配置可能是B,回归测试都跑不稳。
第三个代价是调试成本失控。一条超长Prompt出问题,你是改第80行还是第240行?改完之后是解决了眼前问题,还是又引入了两个新问题?当整个Agent等于一个“巨型字符串”的时候,任何局部修改都会变成全局风险。
1.2 技能(Skill)本质上是什么
我理解中的Agent技能,是把“某一种特定能力的完整处理流程”打包成一个独立单元。一个标准的技能包包含四样东西:
- 触发条件:什么时候该用这个技能。可以是意图描述、关键词、上下文状态,也可以由编排器主动调用。
- 输入Schema:调用这个技能需要哪些参数,缺了哪些可以默认补,哪些必须由外部提供。
- 执行逻辑:真正干活的部分。可以是一段模型Prompt、一段程序代码、一条外部接口调用链,或者三者混合。
- 结果校验:怎样判断技能执行成功。有明确的格式约束,还是需要模型自评,或者靠工具返回值判定。
把这四样东西固定下来,技能就是一套“定义良好的能力接口”。Agent本体不再负责所有事情,它只负责理解用户请求、路由到合适技能、把技能结果整合后返回。
1.3 技能、工具(Tool)和任务(Task)的区别
这块我一开始也混过,后来分了三个层次才理顺:
- 工具是最低级的原子能力,比如“能发HTTP请求”“能读本地文件”“能调用某个API”。
- 技能是面向业务场景的封装,内部可以编排多个工具,也可以夹带一段专门的Prompt策略,对外暴露的是“做完某件事”的能力,比如“把会议录音整理成纪要”。
- 任务是用户的一次具体诉求实例,技能是完成任务的手段。同一类任务反复出现,就可以沉淀成技能。
我用一个生活类比:工具是厨房里的锅碗瓢盆,技能是“煎一份五分熟牛排”的完整菜谱,任务则是“今晚做一份牛排给家人吃”。Agent不需要每次都从零组合锅铲和火候,它只需要知道此刻该激活哪份菜谱。
2. 技能体系的整体设计与目录结构
光写代码不设计目录,技能多了以后照样崩。agent-skills走到中期,我意识到技能不是越多越好,而是越规范越好。技能间的命名规则、目录约定、描述文档写法,直接决定了整个系统还能不能继续扩展。
2.1 技能命名与分类规范
我最初踩的坑是用功能命名,比如send_email、get_weather、summarize_doc。单个看没问题,技能到30个以后就发现了三个麻烦:
- 同义词太多:
summarize_doc和doc_summary是不是同一个能力? - 归属不清:
check_order到底属于客服域还是订单域?别的技能该怎么找它? - 路由困难:模型在意图识别阶段面对几十个扁平名称,选择准确率明显下降。
后来我改成“领域-动作-对象”的三段式命名,比如customer_service.refund_check、data_analysis.regression_apply。领域是最高分类,动作是做什么,对象是作用在什么东西上。这套规则还有额外好处:按前缀就能天然分组,做技能过滤和权限控制都方便。
分类上我按业务域走,示例:
| 领域前缀 | 说明 | 示例技能 |
|---|---|---|
communication | 对外沟通类 | 邮件撰写、消息应答、语气改写 |
data_analysis | 数据分析类 | 异常检测、回归拟合、报告生成 |
workflow | 流程编排类 | 审批推进、任务拆解、进度同步 |
storage | 数据存取类 | 档案检索、记录更新、缓存清理 |
2.2 标准技能包的目录长什么样
每个技能在仓库里都有独立目录,结构固定:
skills/ └── data_analysis/ └── outlier_detection/ ├── SKILL.md # 技能说明书,包含触发条件、参数Schema ├── schema.json # 参数的JSON Schema定义,机器可读 ├── execute.py # 执行逻辑,可以是脚本、调用链的入口 ├── prompt_template.md # 如果需要模型参与,把Prompt模板单独放 ├── tests/ │ ├── cases_positive.json # 正例测试集 │ └── cases_negative.json # 反例测试集 └── META.yaml # 版本号、作者、依赖技能、兼容性信息目录固定带来一个直接好处:加载器可以统一处理。扫描目录、解析SKILL.md、注册schema、挂载执行函数,完全自动化。不需要每个技能自己写加载逻辑,这是把“技能数量”从10个扩展到100个的前提。
2.3 技能描述文档的写法,重点在边界
SKILL.md是整个技能的灵魂。它不是写给人看的文档,很大程度上是写给路由模型看的。描述写不清楚,再好的执行逻辑也白搭。
我的写法遵循四个原则:
- 意图描述用例子说话,不用抽象定义。写“适用于订单退款场景的初步审核”就是抽象定义,模型不买账。改成这样:用户反馈退款慢、用户询问如何退钱、用户质疑订单被扣款,当这些表述出现时使用本技能。
- 显式声明不适用场景。描述里加一段“反例”:用户询问商品质量、用户要求人工客服电话,这类请求不要触发。给反例的价值在于抑制过度触发。
- 参数说明写全默认值。比如
notify_user缺省为true,描述里就得写清楚“默认会通知用户,如不需要请设为false”。 - 标注依赖的前置技能。比如退款审核依赖订单状态查询技能的结果,这件事写在
META.yaml里,编排器才能按序调度。
写完SKILL.md之后,我会做一个自查:把所有描述里的“情况”“相关”“等等”这类模糊词消灭掉。凡是出现这种词的句子,多半是描述没想清楚,必然会导致路由误判。
3. 核心实操:从零开发一个可复用的Agent技能
理论讲完,来点真格的。我以workflow.meeting_minutes这个技能为例,完整走一遍从需求拆解到测试落地的过程。这个技能的作用是:给一段会议原始文本,输出结构化的会议纪要,包括议题、结论、待办事项、负责人和截止时间。
3.1 第一步:先把需求边界画出来
开发之前先想清楚什么归这个技能管,什么不归它管。
这个技能负责:处理会议记录文本、语音转写的脏文本,输出结构化纪要。 这个技能不负责:录屏分析、参会人身份识别、生成会议邀请。
边界清晰之后,输入和输出就好定义了。输入是会议文本加可选参数,输出是固定结构的JSON。
3.2 第二步:定义Schema,把参数钉死
schema.json我这样写:
{ "name": "workflow.meeting_minutes", "parameters": { "type": "object", "properties": { "transcript": { "type": "string", "description": "会议原始文本,允许包含语气词、重复语句、口语噪声" }, "detail_level": { "type": "string", "enum": ["brief", "standard", "detailed"], "default": "standard", "description": "详细程度:brief只输出结论,standard包含主要论据,detailed保留完整发言人段落" }, "extract_action_items": { "type": "boolean", "default": true, "description": "是否提取待办事项" } }, "required": ["transcript"] } }参数有三个就够了。我之前犯过一个错误:参数设计得特别细,什么主持人、参会人数、会议时长都要传,结果在实际调用里大部分参数路由模型根本填不准,反而导致入口可用性下降。参数设计的尺度是:必填参数越少越好,可选参数带着合理默认值,让调用的成功率最高。
3.3 第三步:决定执行策略,模型和代码怎么分工
会议纪要这个技能,纯靠模型Prompt也能跑,但输出格式不稳定。我采用“模板约束+程序校验”的混合策略:
- 核心生成走
prompt_template.md里的模板,要求模型按JSON结构输出。 - 生成之后,
execute.py里写一段解析和校验逻辑,必须能正确读出JSON里的conclusions和action_items,读不出来的就判定执行失败。 - 对
detail_level等于detailed的情况,在模板里额外要求保留每个发言人的核心观点,并用“发言人-观点”格式串接。
模板里最关键的一段长这样:
请将会议文本整理为JSON,格式如下: { "topics": ["议题1", "议题2"], "conclusions": ["每个议题的结论"], "action_items": [ {"task": "待办描述", "owner": "负责人", "due_date": "截止时间或null"} ] } 规则: - 文本中未提到的信息,一律使用null,严禁编造。 - action_items提取必须基于明确的责任表述,例如“XX负责”“我来跟进”。 - 输出只包含JSON,不要有任何解释文字。给模型指定“未提到的信息一律null,严禁编造”,是降低幻觉的最有效手段。实测下来,加了这条以后action_items的幻觉率降了一多半。
3.4 第四步:写测试用例,正例反例都不放过
tests/cases_positive.json里放的是应该触发且应该处理好的样例:
[ { "input": "今天讨论了首页改版,结论是下周一上线。小王负责UI调整,老李负责接口适配。", "expect": { "topics_contains": "首页改版", "action_items_count": 2 } } ]tests/cases_negative.json里放的是不应该触发本技能的样例,比如一段纯聊天记录、一段技术方案讲座笔记。测试逻辑很直白:正例必须触发成功,反例触发就算失败。
我强烈建议把测试集接入流水线。每次改动技能描述,哪怕只是加了一句“适用于情况下”,都要跑一遍全量测试。我遇到过改了一个技能描述,导致三个相邻技能触发率异常的问题,要不是有回归测试,这个bug会在线上潜伏很久。
3.5 第五步:技能上线验证
上线前我会跑一次“盲测”:不只测正反例,而是拿过去一周的真实用户日志,把日志里的输入扔给技能路由,看命中率和命中质量。盲测通过之后,才允许把技能挂上线上环境的技能列表。
这里分享一个经验:新技能上线头两天,把它的路由日志单独拉出来盯一遍。看两个指标,一个是触发准确率,一个是执行成功率。如果触发准但执行老失败,问题在execute逻辑;如果执行成功但触发的案例牛头不对马嘴,问题在SKILL.md的描述。
4. 技能编排与动态加载:把技能变成可运营的资产
技能开发完只是第一步,真正的复杂度在“怎么被正确调用”。当技能数量超过30个,你就不可能靠Agent每轮全量思考来选择合适的技能,必须引入编排层。
4.1 路由选择的三条技术路线
我试过三种路由方案,各有适用场景:
- 关键词与规则路由:最快、最可控,适合强意图场景。比如用户输入包含“退款”,直接路由到退款技能。缺点是覆盖不了语气多变的表达。
- 向量检索路由:把技能描述和用户输入都转成向量,相似度超过阈值就命中。覆盖能力强,但偶尔会命中语义相似但本不该触发的技能,所以必须设阈值,还得定期校准。
- 模型路由:直接让模型从技能列表里选,灵活度最高。但当技能列表很长,模型会犹豫、会选错,速度也受影响。我的做法是先用规则粗筛,再把候选技能交给模型,让它做最后选择。
三种方案我目前是按“粗筛+精排”组合用:前缀规则锁定候选域,向量检索把候选缩小到3到5个,最后由模型从这几个里选一个。这样既不会让模型面对三四十个技能发懵,也不会因为规则过于死板漏掉长尾表达。
4.2 技能间的依赖与冲突处理
技能不是孤岛。workflow.meeting_minutes内部可能依赖storage.search_docs获取背景文档;customer_service.refund_check一定依赖order.query_status。这种依赖关系我用META.yaml显式声明:
version: 1.2.0 requires: - storage.search_docs - data_analysis.sentiment_analysis有了依赖声明,编排器就能做两件事:
- 生成DAG,按拓扑顺序执行。先查库、再判断、再输出。
- 检测循环依赖。A依赖B、B依赖C、C又依赖A,这种情况我遇到过,加载阶段直接报错总比运行阶段死循环好。
冲突处理是另一个坑。两个技能都想处理同一种输入时怎么办?我定的规则是:显式规则优先于隐式匹配。如果用户要求“简短回复”,那任何带有“详细输出”倾向的技能,无论匹配度多高都不触发。把这类优先级规则维护成一张独立的配置表,不要散落在各个技能的描述里。
4.3 上下文传递与状态管理
技能执行时产生的中间结果,要不要放回全局上下文?我的答案是:谨慎。全局上下文越大,越容易干扰模型的判断。
我的约定是:每个技能输入什么、输出什么都有明确Schema;技能内部的中间状态全部私有化,执行完了就销毁;只有输出结果可以写入一个“结果缓存区”,供下游技能引用。这个大原则能显著降低调试难度。我见过很多Agent系统跑着跑着行为变异,一问都是因为A技能留下的中间变量污染了B技能的判断。
还有一点:超时与重试。技能里凡是涉及外部API调用的,统一要设置超时时间。内部技能我设10秒超时,在线API技能设30秒超时。超时后由上一步的粗筛路由直接切换到备用技能。这一步被很多人忽略,但线上系统不设超时,你就是把Agent的命脉交给不可控的网络。
5. 真实踩坑记录与排查技巧实录
这一节写的都是我实际撞过的墙。早看到这些记录,我能省很多时间。
5.1 技能描述里的“不”字,远比你想的更难生效
我最初在SKILL.md里写过“该技能不适用于处理售后投诉”。实际测试发现,用户表达带一点“退款”“投诉”的意思,这个技能照样会被触发,反例形同虚设。
后来我明白了一个道理:模型理解反例的能力没有正例强。与其写“不适用于”,不如明确写“当用户表达以下意思时优先使用其他技能:……”。我甚至逐渐把反例全部改成正向引导,比如“此类请求请路由到customer_service.complaint_handle”。实测触发准确率比单纯写“不适用”高了不少。
5.2 技能执行成功,不代表用户满意
这一条让我最痛。workflow.meeting_minutes执行成功率很高,JSON也完全规范,但用户反馈说“纪要内容不对”。一查,发现模型把会议里的讨论性发言当成了结论,结论字段塞了一堆“有人建议……”“有人提出……”。
问题的根源不在于执行逻辑,而在于Prompt模板里的“结论”定义不够严格。我加了一条约束:“只有达成一致或明确拍板的表述才能写入结论,讨论过程和分歧意见不能写入。”加完这条,用户端反馈立刻改善。
这件事给我一个教训:输出合法和输出正确是两码事。每次上线新技能,都要找真实用户跑一轮满意度抽检,不能只看程序层面的执行成功率。
5.3 技能变多之后,性能开始拖垮Agent响应
技能加到60个左右时,光是向量化所有SKILL.md描述再逐条比对,就要多耗将近1秒。用户等得不耐烦,连接口都开始超时。
我的解法是把技能库分成两层:
- 热技能:高频使用的20个技能,常驻内存,描述短,检索快。
- 冷技能:低频长尾技能,元数据在内存,详细描述和执行脚本放远端存储,按需拉取。
热点不变化就定期跑一次访问统计,自动调整热冷状态。这个机制上线后,路由耗时降到了原来的三分之一。
5.4 调试Agent技能的实用小工具:调用链回放
最后推荐一个我自己写的小工具逻辑,它不复杂但极其好用:为每个技能调用生成一个追踪ID,把触发请求、路由决策、技能输入、执行中间步骤、最终输出全部追加到这个ID的日志里。
排查问题的时候,我不看模型输出,先看日志回放,定位是哪一步出了问题:
- 触发错了?看路由阶段的分值排序。
- 输入不对?看上游技能传过来的JSON。
- 执行出错?看execute.py的异常堆栈。
- 输出不合法?看模型生成结果和校验器报错。
有了这个回放机制,多技能排错的效率提高了不止一倍。没有它的时候,我排查一个跨3个技能的问题要靠猜,有它之后问题变成了体力活。
我也把日志格式定成了JSON Lines,每行一条事件。后面想接入监控、做统计都很方便。这也是整个agent-skills项目里最不起眼但回报最高的一笔投资。
做这套技能体系的过程中,我最大的感受是:Agent工程化真正的难点不在于让模型变得多聪明,而在于把聪明这件事拆成一个个能独立迭代、独立测试、独立运维的模块。技能就是一个很好的拆分单元。如果你也正在被超长prompt折磨,不妨从一个最简单的技能包开始,不用贪多,先挑三个高频场景做尝试,跑通“定义-开发-测试-观测”这个闭环,后面再慢慢扩充技能库。这套方法论本身,是比任何一个具体框架都值得沉淀的东西。