☰
Agent Skills实战:重构大模型任务代理,告别提示词万能论
2026/10/13 8:01:14 网站建设 项目流程

“agent-skills”这个概念真正钻进我脑子里,是在一次被任务代理搞到崩溃之后。当时我在维护一个模拟项目X,它是一个某跨平台系统里的任务代理,要同时处理消息整理、日程提取、文档汇总这些活。最初的实现方式特别简单粗暴:把所有规矩全写进系统提示词,让大模型自行发挥,结果连续好几周输出都不稳定。后来我把这套东西推倒重来,把所有能力拆成一个个自包含的技能模块,也就是 agent-skills 的思路,效果比我预想中好太多。这篇文章想把这次重构里真正学到的、关于技能化改造的完整理解写下来,包括技能到底是什么、调度怎么设计、验收怎么量化、上线时又踩过哪些坑。如果你正在基于大模型做任务代理,或者觉得“提示词万能论”到处漏风,那这篇应该能给你一套可直接落地的参考。

1. 为什么“什么都会聊”的大模型,落地到任务代理上却总是拉胯

1.1 一段让我决定重构的失败过程

模拟项目X刚上线的时候,我是照着“全能助理”的思路去设计提示词的。系统提示词里写了洋洋洒洒二十五条规则:如何处理日程冲突、如何提取会议纪要素、如何判断邮件优先级、如何组织日报格式,甚至还有“如果信息不足,不要补猜测”这类补充条款。头两周测试样例全都能过,样例集里六十多个典型问题,正确率能到九成,我当时还挺满意。

结果一放到真实请求上,问题像开闸一样涌出来。最让我印象深刻的三个场景是这样的。

第一个是规则互相覆盖。我在提示词后半段写了“如果日程中没有明确地址,不要补地址”,但在一个真实会话里,用户只是说“帮我把明天下午的会议整理一下”,模型居然从历史邮件里“聪明”地猜了一个地址填进去,彻底违背了前面的规则。第二个是上下文污染。同一个会话在前面聊过“要不要改日报标题”,等到后面用户再说“生成今天的日报”时,模型竟然把之前的修改意愿当成指令,自作主张改了标题,下游下游的推送流程整个乱了。第三个是输出格式漂移。我要求返回 JSON,前面一直正常,后来某天它突然开始返回 Markdown 表格,直接把消费方接口打挂。

这些问题的共同点,不是“模型笨”,而是我给了它一个过长的、线性排列的规则清单,却期待它像一个严格的执行引擎那样去逐条遵守。大模型本质上是概率生成器,在和用户对话时这反而是优点,但一旦承担“多步骤、多状态、多约束”的执行任务,规则一多就会互相打架,上下文一长又会把早期指令稀释掉。

1.2 问题的根子不在模型,而在任务的表达方式

如果让我用一个比喻来解释这个根子,我会说:这就像你有一个经验丰富的全能员工,你不给他任何标准化作业文件,只是当面口头交代一大段任务,还指望他不漏步骤。他个人能力当然没问题,但“一次性听完并完整执行”这件事,天然就超出了口头指令适合表达的复杂度。

Agent Skills 的思路,本质上是把这个过程转换成“SOP + 工具箱 + 分工人”的模式。先把能力拆成一个个自包含的技能模块,每个技能有自己的说明书、参数格式、执行步骤和兜底方案。大模型不再去“从头组织整个工作流”,它只需要做三件事:判断当前请求该用哪个技能、填好需要的参数、把技能返回的结果整理给用户。复杂度从“一段几百字的指令里做所有事”,降级成“一套可检索、可测试、可版本管理的技能列表里做选择”。

说白了,单纯优化提示词是没有办法根治这个问题的。提示词是线性文本,规则之间没有明确边界,没法单独测试、单独回滚。而技能是一个模块化实体,描述、参数、执行逻辑都可以独立演进。这是我在模拟项目X里第一次体会到“技能化”和“提示词优化”之间质的区别。

2. 技能的本质:把“能力”从提示词里搬出来,并给它一份说明书

2.1 技能、Prompt、Function、Plugin到底有什么区别

很多朋友一听到 agent-skills 就说:这不就是写好一点的 Prompt 吗?或者不就是 Function Calling 吗?我一开始也这么想过,实际落地之后发现差别挺大。它们之间的关系,我用一张表简单理一下。

维度PromptFunctionPluginSkill
本质一段文本指令程序函数接口一组功能的聚合包自包含能力单元,含描述、参数、执行、校验
适合谁阅读模型、人代码宿主框架模型做选择,人去维护
是否可复用低,换场景要重写长文可复用,但缺乏语义边界按类目复用,粒度偏大可注册、检索、版本管理,语义边界清晰
维护方式改提示词长文改代码和注册表依赖框架内置机制每个技能独立改版本、独立回归
大模型参与度大模型全流程组织大模型只负责填参大模型在一组插件里选择大模型负责“选技能+填参”,执行逻辑在技能内部

如果还觉得抽象,可以这么理解:Prompt 是告诉模型“你是一个会做日报的助手”,Function 是告诉模型“你可以调用一个叫做 generate_report 的函数”,而 Skill 是给模型递过去一张名片,名片上写着“这是一个专门做日报的工具,适合什么时候用、不适合什么时候用、需要哪些输入、输出长什么样、失败会怎么告诉你”。模型拿到技能列表时,不需要自己推理出怎么做日报,只需要判断该不该把请求交给它。

2.2 一份技能定义应该包含的五个要素

在模拟项目X里,我后面把技能定义统一成五个要素,缺一个都会在实际调用时出问题。这五个要素是:名称、说明、参数、执行体、校验与降级。

先说名称。技能名应该稳定、机器可读,例如 daily_report_generator。不建议用“生成日报”这种自然语言做内部标识,因为模型在输出调度结果时,短横线命名的歧义远小于中文自由文本。当然,展示给用户的友好名称可以另存一个字段。

然后是说明,这是给大模型看的自然语言描述。一段合格的说明至少包含四个信息:它能做什么、什么时候应该调用、什么时候绝对不要调用、输入数据从哪里来。很多初版技能吃亏就吃亏在“只写了能做什么,没写不能做什么”,导致误调用率居高不下。

参数部分用 JSON Schema 描述最稳妥。必填参数、可选参数、类型、格式约束都要写清楚。执行体则定义了技能的实现方式,可以是脚本、API调用链、数据库查询,也可以是一段带子工具的多步流程。最后是校验与降级:什么算执行成功、失败时返回什么占位结果、需不需要记录日志。下面这个 YAML 是我在实际项目里简化后的技能定义模板。

name: daily_report_generator description: > 根据某个日期范围内的原始记录生成结构化日报,适用于需要整理每日工作摘要的场景。 不要用于邮件发送;不要对缺少来源字段的输入做猜测性补全。 input: start_date: type: string format: date required: true end_date: type: string format: date required: true source_ids: type: array items: string required: false execute: - fetch_raw_records - merge_by_source_type - fill_summary_template output: report: type: object fields: [topics, details, action_items] fallback: on_error: "return REPORT_FAILED and log missing fields"

你可能注意到我在描述里特意写了“不要用于邮件发送”。这不是废话,而是真实发生过的事情。模型看到“生成日报”的描述,有时会把“把日报发出去”也当成隐含任务,结果调度错了技能。把边界写进描述,等于提前给模型划了一道围栏。

2.3 为什么技能必须“自包含”:边界清晰才能减少误伤

技能这个概念里,“自包含”三个字比“能做某件事”更重要。一个技能如果需要依赖外部环境里某个看不见的状态才能跑通,那它在大模型调度场景下就是一个不稳定炸弹。我在早期写过这样一个技能,名称叫 query_contacts,描述只写了“查询联系人信息”。看起来很简单,但它没有说明数据源是哪个系统,也没有说明联系人表里的哪些字段允许返回;实际调用时,模型经常把这个技能和“找人”“发消息”“查组织架构”混为一谈,甚至有一次,用户只是说“给张三发条消息”,模型就调了这个查询技能,而不是调用专门发送消息的技能。

后来我做了两个调整。第一,在描述里加“何时不要使用”,比如:“不要用于发送消息、不要用于查找群聊、不要用于修改联系人信息”。第二,把技能内部所需的数据源标识显式作为参数传入,而不是默认“系统里只有一个联系人库”。这两步做下来,误调用率降得非常明显。这说明一个道理:技能边界不是靠代码强制隔离出来的,而是靠结构化描述和参数约束共同把“使用范围”钉死。

3. 技能发现与调度:让大模型看到对的人、走对的路

3.1 技能发现机制:描述就是技能的名片

技能做出来了还不够,关键是让大模型在合适的时机“看见”它。这是 agent-skills 和普通函数库最大的不同:函数库靠程序员 import,技能库靠模型自己挑选。那模型靠什么挑?靠的就是技能描述。

我试用过两种技能发现方式,各有适用场景。

第一种是全集注入。技能数量很少的时候,比如二十个以内,直接把所有技能的描述塞进系统提示词的工具列表,让模型从里面选。这种方式零额外组件、零延迟开销,非常适合起步阶段。缺点是技能一多,描述会占掉大量上下文,而且模型选错概率会随着列表膨胀而上升。

第二种是检索注入。把每个技能的描述做向量化处理,用户请求进来时,先做一次轻量的意图摘要,再拿摘要去技能库召回最相关的几个技能,只把 TopK 个描述交给模型。在模拟项目X里,技能库扩充到七八十个之后,我做了这次切换。全量注入时一次对话首轮的工具描述开销将近一点一万个 token,换成检索注入后降到两千四左右,模型的选择准确率反而更高,因为候选集里不再全是干扰项。

如果你刚起步,我的建议是别一上来就搞检索。技能不超过二十个时全量注入最直接,先把业务跑通;等到技能数量多到让提示词明显臃肿时,再做检索注入不迟。检索组件本身也需要维护,过早引入只会让你的排查链路多一环。

3.2 从自由选择到规则兜底:我采用的两段式调度

把技能库交给大模型自由选择,是最灵活的方案,但也是我最不放心的地方。模型在长尾意图上确实灵活,但在高频简单意图上,它反而可能过度思考。比如用户明确说“把明天的日程发给领导”,绝大多数时候就应该直接转发日程摘要,不需要让模型在几十个技能里翻来覆去。

所以我最终采用了一个两段式调度:先规则路由,后模型决策。每个高频意图都维护一份触发规则表,如果用户请求命中了明确规则,就直接映射到固定技能,不经过模型判断。比如“转发会议邀请”“查询今天天气”“停止定时任务”这些动作,规则表里直接写死。如果规则没有命中,再走模型决策链路:召回候选技能描述,连同原始请求交给模型,让它输出技能ID和参数 JSON。

这样做的好处是,高频场景稳定不漂移,长尾场景灵活能覆盖。代价是要维护一份规则表,好在规则表不需要很大,覆盖住最常出现的三成请求就够了。还有一个额外收益:如果模型决策链路出了问题,规则路由还能作为降级通道继续工作,不至于整个任务代理直接瘫痪。

我把这个过程总结成了五步,你可以直接参考。

  • 第一步:接收用户请求后,先做一次轻量意图摘要,长度控制在三十个词以内。
  • 第二步:用摘要命中规则表,命中就直接返回固定技能和参数。
  • 第三步:未命中时,拿摘要去技能库做向量召回,取 TopK,K 我一般设 5 到 8。
  • 第四步:把召回技能的描述列表和原始用户请求一起发给模型,要求模型只输出一个 JSON。
  • 第五步:执行引擎校验技能名和参数 Schema,通过后再执行技能体。

3.3 调用协议里的细节:先选技能,再填参数

调度链路里最影响成功率的一环,其实是模型输出协议的约束。我刚开始设计时,直接让模型“输出你选择的技能和参数”,结果模型偶尔会输出一段自然语言解释,或者把技能名写成别称,参数格式也常常和技能内部的 Schema 不一致。

后来我把协议收紧成固定格式,并明确告诉模型:只允许输出一个 JSON,不要解释,不要附加文字。下面是我在模拟项目X里实际使用的调度提示词节选。

你是技能调度器。你会收到用户请求和候选技能列表。 只允许输出一个 JSON,格式为: {"skill":"技能名","args":{...}} 如果没有任何技能合适,输出 {"skill":"noop","args":{}} 不要解释,不要附加文字。

为什么要求模型先想清楚技能名、再填参数?因为如果模型先确定了技能标识,参数生成过程就会更贴近该技能的 Schema;反过来如果先写参数再套技能名,就特别容易出现参数对不上号的情况。另外,我要求模型在“没有任何技能合适”时输出 noop,而不是硬猜一个技能。这个小改动直接解决掉一类严重问题:以前模型为了完成对话会强行调用不相关技能,现在它有了一个合法出口,误用率大幅下降。

执行引擎收到模型输出后,我会先校验技能名是否存在于技能库,再用技能定义里的 JSON Schema 校验参数。校验不过就直接返回“参数缺失”的占位结果,而不是把错误参数硬传给技能体,这样既便于排查,也避免把脏数据带进内部系统。

4. 技能质量的验收标准:从“会做”到“能被稳定调用”

4.1 用一张测试卡量化技能质量

很多人写技能只管“能不能跑通”,不管“会不会被正确调用”。但 agent-skills 方案里,技能被误调用和技能执行失败一样危险。我在模拟项目X里定义了一张技能测试卡,每个技能上线前至少要跑一遍。下面是我给技能质量定的四个核心指标,以及对应的通过标准。

测试项通过标准我用的模拟测试方法
意图命中率期望触发该技能的请求中,被正确选中的比例不低于 95%准备一百条标注好的真实请求,跑调度链路
参数提取率从自然语言中抽出的必填参数,字段级命中率不低于 90%分别用完整和省略表达去请求,检查 args
边界拒绝率不该调用该技能的请求,被误选的比例不超过 2%把相似但不同意图的请求混入测试集
失败恢复率外部服务不可用时,依然返回兜底结果的比例达到 100%手动模拟接口超时和返回异常

这个测试卡不一定适合所有项目,但它提供了一种“可量化验收”的思路。技能不是写完就结束的东西,它是一个会被模型反复挑选、反复调用的产品表面;质量不过关,整个代理的表现就不可信。

4.2 描述文本里的四个常见Bug,以及改法

技能描述是最容易写出问题、又是最容易被忽略的部分。我整理了自己踩过、也帮朋友排查过的四个高频 Bug,每一个都有对应的改法。

第一个是描述过宽。比如技能描述写“获取信息”,模型会在邮件、日程、文档、通讯录各种场景下都选中它。改法是收窄宾语,明确数据来源和字段范围,写成“根据指定日期范围获取排班表原始记录,只允许读取日期、人员和班次字段”。

第二个是描述过窄。比如技能描述只写“生成日报”,用户说“帮我整理一下今天做了什么”,模型就不会联想到它。改法是在描述里补充同义触发场景,把“工作记录整理”“每日小结”这类说法也写进去。

第三个是参数名跟内部变量不一致。技能描述里写参数名 start_date,但执行脚本里读的是 date_begin,结果模型严格按照描述传参,脚本却取不到值。改法是让技能描述的参数名和内部实现直接对齐,或者做一层参数映射,减少这类低级事故。

第四个是隐含前提。描述里写着“从同步记录中生成摘要”,但没说清楚同步记录从哪里来,调用方也没有传入数据源标识。等到模型真的调用了,技能体才发现拿不到数据。改法是所有外部依赖都显式声明为参数,宁可在调用时繁琐一点,也不要让技能内部偷偷假设环境状态。

4.3 版本管理与回归:一个输出字段引发的线上事故

技能进入稳定期之后,最大的风险不是写不出新技能,而是改坏旧技能。我印象最深的一次事故,是在某跨平台系统工作时,A 同学为了统一输出规范,把一个技能输出字段从 text 改成了 summary,并且没有保留旧字段。看起来只是改了个名字,但所有依赖旧字段的自动化流程全部断掉了。那一次回滚花了将近两小时,问题就出在“原地修改”而不是“新增版本”。

从那以后,我坚持每个技能定义里带 version 字段和 compatibility 说明,破坏性改动一律开新版本,而不是覆盖旧的。下面是一个版本信息的例子。

name: summarize_doc version: 3.1.0 compatible_versions: ["3.0.0"] breaking_changes: - "output.text 重命名为 output.summary,旧调用方需迁移"

更重要的是建立回归机制。每次改动技能描述、参数 Schema 或执行体,都要重新跑一遍第一节里的测试卡,确认意图命中率和边界拒绝率没有退化。如果改动会破坏兼容性,还要在版本说明里写清楚旧版本调用方需要怎么迁移。这个习惯看起来消耗时间,但跟线上事故的代价比起来,很划算。

5. 一条可以直接照抄的技能开发工作流

5.1 从任务日志逆推技能边界,而不是拍脑袋设计

我最开始设计技能列表时,是照着“我觉得用户需要这些功能”的思路列的,列完才发现很多技能根本没被调用过。后来改成另一种做法:从任务日志逆推技能边界。

具体来说,我会拿过去一到三个月的真实用户请求,先按意图关键词聚类,看看大家到底在反复要求哪些动作。聚类结果出来后,把高频动作单独拎出来,针对每个高频动作去画技能边界。边界画的是三件事:触发条件是什么,不触发条件是什么,输入数据需要哪些字段。画完之后再开发最小可运行版本,而不是一开始就做全功能。

这个流程最大的价值是让技能和真实需求对齐。你可能会发现,自己以为很重要的“高级筛选”其实没人用,而反复出现的“把这下周的时间帮我排一下”才是真正值得固化的技能。

5.2 最小可运行技能的搭建模板

一个技能要能跑起来,至少需要三样东西:描述文件、参数定义和执行脚本。我在模拟项目X里习惯把所有技能放在统一目录下,比如 skills 下的一个文件夹一个技能,这样调度引擎可以很方便地扫描注册。下面是一个简化版的最小技能示例,技能名是 generate_todo_summary。

import json def run(args: dict): records = fetch_records(args["date"]) todos = [r for r in records if r.get("status") == "open"] return { "summary": f"今日还有 {len(todos)} 个待办", "items": todos[:10] }

对应的描述文件可以长这样。

name: generate_todo_summary description: > 生成指定日期的待办汇总,适合用户问“今天有什么待办”或“帮我整理一下任务”时使用。 不要用于删除待办事项,不要用于修改任务优先级。 input: date: type: string format: date required: true output: summary: type: string items: type: array

为什么不建议一上来就写复杂执行链?因为最小版本的技能可以更快上线、更早收集真实调用反馈。等确定这个技能确实被频繁使用,再往里面加更细的功能和更完善的兜底逻辑。技能开发是迭代的事,不是一步到位的事。

5.3 回归测试与灰度上线:先让小流量替你挨打

新技能写完之后,我不会直接全量开放。就算测试卡全部通过,真实请求的分布和测试集也可能存在差异。稳妥的做法是先跑一小段时间的灰度流量,比如让 10% 的请求可以命中新技能,剩余 90% 走旧的降级路径。这个期间重点盯三类指标:技能调用成功率、调度链路延迟、模型误用率。

指标全量前观察值说明
成功率不低于 98%排除外部服务本身的问题
平均延迟不超过旧方案 1.5 倍主要看检索和校验耗时
模型误用率不超过 3%关注是否被选在明显不该用的场景

灰度期间如果发现某个误用场景特别集中,我会优先回头修描述,因为没有比真实用户的表达更生动的测试样本了。等观察两三天没问题,再逐步放大流量到全量。这个流程看起来保守,但对线上稳定性很有效。

最后说一个我现在一直坚持的小习惯:每个技能描述里必须有“不要用于”段落。这个习惯是从一次线上误调用事故之后开始养成的,之后帮我避开了大量相似的坑。技能描述别怕啰嗦,边界写得越细,模型的选择就越稳。做 agent-skills 这件事,越到后面越会发现,真正的难点从来不是把技能写出来,而是让每个技能在合适的时候被选中、在不合适的时候被拒之门外。这个平衡点,只能靠一次次基于真实日志的调整慢慢磨出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询