大约半年前,我接手了一个有点头疼的内部项目:一个要自动处理报表汇总、会议纪要和周报生成的智能助手。第一版我做得很“标准”——把每个功能的执行步骤全部写进系统提示词,然后告诉模型“看到A就做B,看到C就做D”。初版演示效果确实唬人,但随着业务方不断加需求,提示词从几百字膨胀到几千字,模型开始频繁“自作主张”:漏步骤、乱调用、甚至把草稿直接当成邮件发出去。那段时间我几乎每天都在怀疑人生:是我指令写得不清楚,还是模型本身就不适合这么干?
后来我彻底换了一套思路:不再费劲教模型“怎么做”,而是给模型一个“工具箱”,让它自己判断什么时候该用哪把工具。这个方案就是我们内部叫agent-skills的技能体系。简单说,就是把各种原子能力封装成可复用、可注册、可调度的“技能包”,模型只负责理解用户意图并从技能清单里选一个合适的来调用,具体执行交给代码,而不是交给自己脑补。整个重构过程和后续踩坑,我觉得挺值得拿出来聊聊。
如果你也在做类似的应用,比如智能客服、自动化办公助理、能操作各种工具的多步骤助手,这篇文章应该对你有用。我会把自己从设计到落地、再到翻车修复的完整链路写出来,包括技能怎么拆、描述怎么写、调度怎么设计、权限怎么卡,以及那些文档里不会写但实操中一定绕不开的坑。
1. 我为什么放弃了“把逻辑全塞进提示词”这条捷径
1.1 提示词越写越厚,模型越用越“飘”
先说具体的问题。第一版助手的系统提示词里,我写了密密麻麻的规则:“如果用户提到汇总,先读取文件,再清洗数据,最后生成表格;如果用户提到周报,先检查时间范围,再逐项填充,注意不要写废话……”看上去逻辑自洽,实际上模型天然是概率推理,不是规则引擎,一旦规则之间存在交集,它就会开始“表演”。
印象最深的一次,用户让“写一封会议邀请并保存到草稿”,模型做了三件事:自动调出通讯录、选择了参会人、然后直接点了发送。当时我盯着日志看了五分钟,找不到任何一行代码让它“发送”,但模型就是觉得“这是最完整的处理方式”。这类问题不是靠多写两行提示词能解决的,规则越多,互相干扰越严重,而且新需求一加,老行为就跟着漂移。
另一个隐藏成本是 token。几千字的提示词,每次会话都要完整传入,响应变慢、费用变高,而且模型在长文本里找关键指令,就像让学生在一百页考纲里找考点,找对了算运气,找错了才是常态。
1.2 从“教模型做事”到“给模型一把工具箱”
转折点出现在一次线下讨论。当时大家聊到一个问题:如果新来一个实习生,你会怎么带他?很少有人会事无巨细地教他“点哪个按钮、填哪个字段”,我们更倾向于告诉他“遇到什么情况就找哪个负责人”。这个“负责人”就是一个能力单元。模型其实也一样——它最强的是语义理解和意图判断,最弱的是稳定执行多步骤操作。
所以重构时我给 agent-skills 定了一条核心原则:模型负责决策,代码负责执行。把“读文件”“解析表格”“发送邮件”“调用某个查询接口”这些行为封装成独立模块,每个模块有名字、有描述、有输入输出协议。模型拿到用户诉求之后,不是自己琢磨怎么一步步干,而是从技能清单里选一个匹配的,然后把参数填好,交回给执行层。
这套思路后来被很多主流大模型 API 所支持,在当时的语境下更准确地说,是一种“工具调用”模式。但把它工程化落地,尤其是整理成一套可持续扩展、可观测、可管控的技能体系,才是真正的难点。我到现在都记得,第一次跑通“用户说需求→模型自动选中技能→执行代码返回结果→模型接着回答”全链路时,那种“我好像找到了正确方向”的感觉。
2. 技能拆解:构造一个稳定的最小能力单元
2.1 技能粒度的把握:太大会失控,太小会碎成渣
做技能体系,第一个绕不开的问题是:一个技能到底应该多大?我一开始特别容易做得“大而全”,比如封装一个“处理报销单”技能,里面一口气做了票据识别、金额校验、审批流程发起、消息通知四件事。听起来很方便,实际用起来非常难受:模型根本没有足够细的信息去控制内部逻辑,只要其中一步出错,整个技能就跟着失败,而且失败原因极难定位。更麻烦的是,下次其他场景只需要“报销单识别”这一个子功能,你又得复制一份代码。
后来我们把技能拆成了四层原则:一个技能只做一件完整的事;输入输出边界必须清晰;技能之间不要互相调用,如果有复合需求,交给模型编排而不是代码编排;公共基础操作(比如字符串格式化)不要做成技能,那是函数库的职责。按这个原则,“处理报销单”被拆成了“提取报销信息”“校验票据有效性”“生成审批单”“发送状态通知”四个技能,各自独立测试、独立部署、独立描述。效果立竿见影——模型的选择准确率上来了,因为每个技能的触发场景非常明确。
当然,也踩过拆得太碎的坑。有一阵子我把“读文件”拆成“读Excel”“读CSV”“读JSON”,理由是参数结构不同。后来发现模型经常在JSON文件场景下选了读Excel,因为用户说“数据太乱”它就开始猜。最后我还是合并成“读取表格文件”并允许自动探测格式,让参数里的格式字段可填“auto”。这个教训让我明白:粒度不是越细越好,判断标准是“模型能否根据用户的自然表达,准确命中该技能”。
2.2 技能描述是给模型看的“说明书”
这是整个 agent-skills 体系里最容易被低估、也最影响效果的部分。很多开发同学默认描述只是给人看的注释,随便写一句“文件复制功能”就完事了。但你要意识到:在模型调用时,它看到的不是技能源码,而是“技能名 + 描述 + 参数定义 + 返回结构”这一小段文本。它完全依赖这段文本来判断“现在该用哪个”。
我整理过一个直观对比:
- 差描述:
文件复制功能 - 中描述:
将文件从源路径复制到目标路径 - 好描述:
当用户需要把本地文件复制到远程主机,且目标路径尚未存在时,调用此技能完成一次性复制;若目标路径已存在,不要使用本技能,改用 update_file 技能。
差描述为什么不行?因为“文件复制”这个词太泛,模型在用户提到“把资料传到服务器”“同步一下配置文件”时,根本不确定该不该用。而好描述把触发条件、前置条件、边界情况都写清楚了,模型判断起来非常省力。后来我们总结了一套描述模板:触发场景、前置约束、执行动作、返回值含义、不该使用的情况。尤其是“不该使用的情况”,很多人不写,结果模型经常把相似技能张冠李戴。
写描述还有两个细节。第一,不要在描述里出现“强大的”“智能的”“全能”这类形容词,模型会被带偏。第二,描述要用模型容易理解的自然语言,而不是堆术语。一个准确的描述抵得上几千行提示词,这句话我现在完全信了。
2.3 参数与返回值的协议设计
技能描述决定模型“选不选”,参数和返回值决定模型“用得顺不顺”。我们统一要求每个技能使用类似 JSON Schema 的方式声明参数,字段名、类型、必填、默认值、枚举范围都要写清楚。有一个常见的坑:模型提取参数时特别喜欢“自由发挥”,比如数字单位、日期格式经常错。用户说“一周内”,模型可能就顺手填空“2025-01-01 到 2025-01-08”,这就是必须要在参数层做收敛的原因。
返回值我设计了统一的三段式结构:状态码、结果数据、附加信息。状态码只有三种大类:success表示成功,retryable表示这次失败但可以重试,fatal表示条件不满足或永久失败。这个设计在后来的错误处理里帮了大忙,因为它给了模型非常明确的下一步指令——成功就基于结果组织回答,可重试就换种方式再试,永久失败就直接告诉用户无能为力。
还有一个很容易忽略的点:技能返回给模型的数据同样占用上下文,如果返回一个几千行的 JSON,模型后面的推理质量会断崖式下降。所以我们在执行层做了一次“结果摘要”:对超大结果只返回统计信息和前 N 条样例,模型需要更细内容时再主动调另一个查询技能。这一步让整套系统的稳定性和速度都提升了不少。
3. 技能注册与发现:让 Agent 知道自己会什么
3.1 技能清单与注册中心
当技能数量超过十几个以后,你不可能每次都把全部技能的完整描述塞给模型。一方面 token 成本受不了,另一方面模型面对一大堆选择,误选率会明显上升。这时候就需要一个“注册中心”:每个技能在服务启动时注册自己的元数据,系统按业务域分组,会话开始后根据用户的初始意图和上下文,只把相关领域的一小撮技能加载进模型。
这个发现机制听起来简单,做起来需要反复调。第一版我们直接用关键词匹配,用户提“邮件”就加载邮件域的全部技能,效果一般,因为用户表达太灵活,关键词覆盖不住。后来改成两段式:先让模型对用户需求做一次粗分类,输出可能的领域标签,再根据标签加载对应技能清单。分类本身也是一个小模型调用,成本不高,但准确率提升非常明显。
加载技能清单之后,也不是每轮对话都原样传给模型。我们只会在“模型需要决定是否调用技能”的回合传入技能列表,如果当前只是闲聊或补充信息,就直接走普通对话,不传工具定义。这个看似不起眼的优化,把 API 的 token 用量省了差不多三分之一。
3.2 元数据里除了描述还需要放什么
技能注册时,我要求每个技能必须带上这几类元数据,缺一不可:
| 字段 | 作用 | 示例 |
|---|---|---|
| name | 技能唯一标识,模型调用时使用 | send_email |
| description | 给模型看的触发条件说明 | 触发条件、边界条件 |
| parameters | 参数 Schema | JSON key、类型、枚举 |
| permission | 权限等级:read / exec / dangerous | 写文件至少 exec,删数据必须 dangerous |
| timeout | 单次调用的超时上限 | 10s / 60s / 300s |
| version | 技能版本号 | 1.2.0 |
| deprecated | 是否已弃用 | true / false |
权限等级在注册时就写死,而不是等到执行时才判断,好处是可以直接在注册中心做拦截。比如一个技能的权限等级是 dangerous,而当前会话没有经过人工鉴权,那这个技能压根不会出现在模型的候选清单里。这样既省了模型一次错误的调用机会,也避免了执行层反复校验的复杂度。
版本号这个字段我一开始没太当回事,直到后来踩了“会话人格分裂”的坑(后面会讲),才意识到它至关重要。现在我们的规则是:技能一旦发布不可变,更新必须发新版本,旧会话继续用旧版本,新会话默认用新版本。这个机制给线上排查省了无数时间。
3.3 技能冲突与命名规范
技能多了之后,必然会出现两个技能都想处理同一意图的情况,比如“创建日历事件”和“创建待办事项”,用户一句“提醒我下周一开会”,模型就可能在两者之间犹豫。我们解决冲突的方法有两个:一是在描述里写清“不该使用的情况”,把场景边界切干净;二是给技能设计优先级,如果模型输出多个候选技能,执行层按优先级和上下文相关性选一个。
命名规范也值得立规矩。我们规定技能名为动词开头的蛇形命名,比如read_report_file、approve_expense,禁止用纯名词。原因很直接:动词开头的名称天然包含“做什么”的信息,模型看到approve_expense就知道这是审批动作,而看到expense可能会误当数据查询。这种细节听起来像洁癖,但实际运行中确实明显降低了误调用率。
4. 调度执行链路:从模型选择到动作落地
4.1 模型如何“想到”调某个技能
也许有读者会问:模型到底是怎么“决定”调用技能的?从工程角度看,主流大模型 API 会在每次对话请求里接收可选的工具列表,列表里的每一项就是技能的 name、description、parameters。模型推理时,如果认为当前用户需求与某个工具的描述匹配,就会在输出结果里返回一个结构化的“你想调用这个工具,参数是这些”信号,而不再直接输出普通文本。我们这边再对信号做解析、校验、处理,最后把执行结果以“工具返回值”的身份重新传回对话上下文,让模型基于真实结果继续回答。
这套过程实现起来并不复杂,真正麻烦的是要让模型“想得对”。我们在测试阶段见过太多匪夷所思的选择:用户明明在问统计报表,模型却去调用发送邮件技能,因为它觉得“用户可能是想发报表”;用户让查一下文件里有没有敏感词,模型却调用了删除文件技能。这些误选择大部分不是模型笨,而是我们的技能描述提供了错误或模糊的引导,调度器只是忠实地执行了错误的决定。
所以我在设计调度层时加入了一个“选择确认”环节:当模型选出的技能属于高危动作,调度器不会直接执行,而是把“模型想调用某某技能,参数是什么”推给用户确认。这个环节在生产环境里救过我们好几次,因为它把模型犯错的影响范围从“直接破坏数据”降级成了“一次错误但可撤回的提议”。
4.2 参数提取、校验与执行隔离
模型给出的参数几乎不可能一次到位。我们统计过,即使描述写得很清楚,模型输出的参数仍然有很高的比例存在类型偏差、缺字段、单位错误等问题。所以执行层的第一个动作永远不是跑业务代码,而是参数校验和规范化:把模型给的字符串日期转成标准格式、把不规范的数字单位补齐、把缺失的必填项标记出来。
校验失败的反馈方式也很关键。如果直接把异常扔回去,模型通常一脸懵,只能胡编一个答案。规范做法是返回一段结构化的错误信息:哪个字段有问题、期望什么格式、你刚刚给的是什么。模型看到这段信息,往往自己就能修正参数后重新发起调用。我在实际运行中发现,一次参数错误大约有七成概率能被模型自己修正,剩下的三成才是真正的用户需求表达不清,需要追问。
执行隔离是另一个必须从第一天就做的事。每个技能调用都应该跑在独立的临时目录、独立的进程或容器里,至少要有独立的工作文件夹。我们早期没有隔离,两个 Agent 同时调“读取上传文件”技能,结果互相覆盖了对方的临时文件,导致某次线上演示当场翻车。从那以后,所有技能执行都带上了会话 ID 加调用 ID 的隔离目录,用完即清。
我给调度器画过一张简表,帮助团队理解执行链路:
| 环节 | 输入 | 输出 | 失败处理 |
|---|---|---|---|
| 意图分类 | 用户消息 | 领域标签列表 | 忽略标签,加载全部技能 |
| 技能选择 | 用户消息+技能清单 | 技能名+参数 | 无匹配则普通对话 |
| 参数校验 | 模型参数 | 规范化参数 | 返回修正提示,让模型重试 |
| 执行调用 | 规范化参数 | 结构化结果 | retryable / fatal |
| 结果回填 | 结构化结果 | 模型最终回复 | 生成错误解释再回填 |
4.3 错误处理:让 Agent 学会“承认失败”
很多第一次做 Agent 的人,最害怕的就是模型“硬撑”——明明技能执行失败,它却编一个似是而非的成功结果。要避免这个,必须从底层切断“它能编”的可能。我们的做法是:执行结果回给模型之前,先强制转换成标准结构,并且只要状态码不是success,就把原始错误摘要放在最前面,让模型没法忽略。
对于retryable错误,我们允许模型在同一轮里最多重试两次,但每次重试前都必须调整参数或换策略,不能原封不动再发一次。配合超时控制,能有效防止死循环。对于fatal错误,我要求模型做的不是再次尝试,而是明确告知用户“这个事我办不了,原因是……”,并给出下一步可选路径。让模型学会承认失败,比让它假装成功更有价值,因为假装失败的代价通常是用户自己去填补所有不确定性,那才是最差体验。
5. 权限边界与安全隔离:技能一旦放开,约束必须前置
5.1 最小权限原则,拒绝“万能执行”
给 Agent 挂技能时,我最担心的从来不是功能不够,而是权限失控。一个能正常跑报表的 Agent,如果它的技能执行账号同时拥有删除文件的权限,那一次误调用就是一次灾难。后来我们强制所有服务套用最小权限:每个技能挂一个专用账号,只授予完成该技能职责所需的目录和资源权限。写文件技能的账号只能写指定工作目录,读数据库技能的账号只有只读权限,绝不能一个技能走“管理员权限”一路通吃。
权限收敛之后,模型误调用造成的影响被压到了最小。即使它错误地调用了“清理临时文件”技能,最多也只能删掉隔离目录里的临时文件,碰不到正式产物和数据库。我把它理解成“给 Agent 穿上了防爆服”——它依然有可能做错事,但伤害范围已经被控制住了。
5.2 高危技能的双人复核机制
不是所有技能都适合让模型一键执行,删除、转账、对外发布这类动作,我们统一挂到了高危名单里。模型可以把它们选进候选,但真正执行前,调度器会生成一条待确认任务,推送到人工审批队列,由业务方确认后才落地。这套“人机协同”的模式一开始被团队吐槽太重,我们自己也嫌慢,但上线三周后发生了一件事:模型在一个会话里连续三次想把测试环境的数据同步脚本打到生产目录,如果不是双人复核挡着,后果很难收拾。从那以后,再没人抱怨这个机制多余。
5.3 审计与配额,让每个动作可追溯
最后是审计与配额,这属于上线前就必须准备好的部分。每个技能调用都要记录:哪个会话、哪个用户、什么时间、传了什么参数、返回了什么结果、花费多少 token、耗时多久。审计日志不是用来摆设的,它在两类场景里特别有用:一是线上问题复盘时,能快速还原整个上下文;二是排查模型异常行为时,能发现那些“看起来成功但选项偏离”的隐性误选。
配额则是防止 Agent 在异常循环里烧掉所有资源。我们对每个会话设置了调用次数上限和 token 上限,一旦达到上限,Agent 会直接停止自动执行并要求人类接管。有一次线上事故就是模型陷入了“重试—失败—重试”的循环,三分钟里调用了上百次外部接口,如果没有配额,那个月的高额账单就要落在我们头上了。
6. 实测中的踩坑记录:那些重构时才发现的隐藏问题
6.1 描述越花哨,模型越容易误选
技能的描述文案里,我吃过一次很具体的亏。某个内部数据查询技能,为了让“显得专业”,在描述里加了一句“这是一个强大的万能数据分析工具,适用于任何场景”。结果上线之后,模型的误选率直接翻了一倍:用户问天气,它调数据分析技能;用户问菜谱,它也调数据分析技能。原因是那句“万能”给了模型一种幻觉——好像什么需求都能交给它。后来我把描述改回克制、具体、边界清晰的版本,误选率才降下来。这个坑非常有代表性:不要用营销话术给 AI 写说明书,它会当真。
6.2 同名技能在不同会话里的“人格分裂”
技能迭代是常态,但版本管理跟不上就会出现“人格分裂”:同一个技能,新会话用的是新逻辑,老会话因为上下文里还残留着旧技能描述,模型依然按旧思路调用,导致同一个用户在不同时间问同一个问题,得到的处理方式完全不同。有一次业务方投诉“这个 Agent 是不是换了个人”,排查半天才发现是技能版本没有在会话维度锁定。解决之后我们明确了一条规则:会话创建时快照当前技能版本,后续更新只影响新会话,旧会话保持兼容运行。
6.3 并发调用导致共享资源被冲掉
没有隔离之前,两个 Agent 同时处理任务,只要它们都调用“保存中间结果”这个技能,就会写到同一个共享路径,后写入的直接把先写的冲掉。我们在一次压力测试里发现,并发一上来,任务成功率啪地掉到一半以下,排查时一度以为是模型变笨了,后来才定位到是共享文件在打架。从那以后,所有技能执行都带独立隔离空间,临时文件加会话 ID 前缀,并且目录权限互相不可见。这个问题属于那种“文档不会提醒你,但生产环境一定会教你做人”的典型坑。
6.4 模型强撑——再好的技能也救不了错乱的提示词
还有一次问题出在我们自己身上。某个复杂业务场景里,为了引导模型做多步操作,我在系统提示词里写了非常长的“主线流程”,结果模型反而被这些流程文字带偏,频繁跳过技能调用,直接自己“脑补结果”。后来我把主线流程从提示词里删除,改成只保留目标描述,让模型依赖每一步的实际返回结果来动态决定下一步,正确率立刻回升。这个教训的普适版本是:提示词越想把控制权抓在手里,模型越容易发挥失常。把控制权交给技能机制本身,反而更稳。
7. 技能库的持续演化:让 Agent 从“能用”到“好用”
7.1 用请求日志反哺技能设计
技能体系上线只是开始,持续演化才见真功夫。我们每周会拉一次技能调用日志,重点关注五类指标:各技能调用次数、调用成功率、平均耗时、模型误选率、以及从未被调用的“僵尸技能”。很多技能开发时觉得很刚需,实际两个月都没被模型选中一次,那就要么是描述不准确,模型根本没发现它,要么是需求场景不存在。前者我们改描述,后者我们直接下架,免得干扰模型选择。
7.2 像优化代码一样优化技能描述
技能描述是消耗品,要持续迭代。我们给每个高频技能建了一个“描述修改记录”,每一次改动都记录原因。比如某个技能常被模型在“目标文件不存在”的场景下选中,我们就在描述里加了一句“仅当目标文件已存在时调用”,连续观察两周,误选率确实下来了。这种优化方式和修代码 bug 没什么本质区别,一个修行为,一个修文案,但后者成本更低、见效往往更快。
7.3 定期重演与回归测试
技能库一旦超过几十个,最怕的就是改了 A 技能,影响了 B 技能的选择。所以我们建了一套回归语料库,把典型用户问题、预期应该调用的技能、预期参数范围都录进去,每次技能描述或调度逻辑变更后,批量跑一遍回归。它可能不会 100% 覆盖线上变化,但至少能挡住最明显的回归问题,比如“改完描述后,模型再也不选这个技能了”。
7.4 从技能库走向“能力市场”
最后聊一句远期方向。agent-skills 这套体系真正走向成熟,是当它允许不同团队各自维护技能、统一注册、按需订阅之后。某个团队做了一个日期识别技能,其他团队可以申请调用;某个技能质量下滑,评分机制会让它慢慢淡出候选清单。这已经不是单纯的技术框架,而是一个组织内部的能力市场。这段时间做下来,我越来越确信:让模型理解并调用技能这件事,价值不在“接入多少个大模型 API”,而在底层这套技能设计和管理的功力。基础打牢了,上面换什么模型、加什么功能,都是搭积木的事。
如果你正准备做一个 Agent 项目,我的建议很简单:开工前别急着写执行函数,先花两三天把技能的清单、边界、描述文本像产品一样逐条打磨。agent-skills 这个项目走到今天,我觉得最值钱的资产不是那些执行函数,而是那份越改越清晰的“技能说明书”。一次调用选对技能,比模型事后补救省出十倍的时间和沟通成本。先写到这儿,后面积累了新的教训,我再来更新。