1. 为什么需要给 Agent 做“技能模块化”
1.1 裸 Prompt 的失控现场
先说说我看到的实际现象。很多人第一次接触 Agent 开发时,第一反应是把所有指令写进一个超大 Prompt 里——“你是一个全能助手,能帮我写周报、也能管理日程、还能分析数据,当用户提到周报时你要先……提到日程时你要……”这种写法在小规模演示里确实跑得通,模型也能勉强应付两三个任务,但一旦任务超过五个,或者你开始对接真实业务数据,整段 Prompt 就会变成一个谁也管不住的黑箱。
最典型的问题有三个:一是任务描述之间互相覆盖,比如“整理会议纪要”和“提取待办事项”这两条指令经常被模型混着执行,输出结构时好时坏;二是一次对话里塞入太多上下文,模型容易“忘记”早期指令,尤其是在长对话里,后面的输入一多,前面的约束就被稀释了;三是任何一处小改动的副作用不可控,你以为只是加了一句话,结果模型在另一个任务上的表现莫名其妙变差了,而且你还不知道是哪句话导致的。这些都是我在项目里实际踩过的坑,不是理论推演。
那时候我就在想,能不能把“让 Agent 做某件事的能力”本身当成一个独立的、可插拔的模块来管理。就像后端开发里把数据库访问封装成 Repository,把外部接口封装成 Client 一样——每个模块有明确的输入输出、有独立的版本、能单独测试、也能组合复用。这就是我从 2024 年年底开始在我的项目里落地 agent-skills 的初衷。所谓 agent-skills,就是在 Agent 应用里以标准化结构封装的一组“可复用技能单元”,每个单元负责一类具体的任务能力,比如“提取待办”、“生成周报”、“分析报表”,并且通过统一的接口协议被 Agent 调度和执行。
1.2 Skill 到底解决了什么问题
把技能从 Prompt 里拆出来之后,最直观的感受是:开发方式从“写提示词”变成了“定义能力”。这两者有本质区别。写提示词是在跟模型沟通,希望它“理解”你的意图;而定义能力是在给模型提供一套清晰的执行框架,它只需要学会“什么场景调用哪个能力”就行。前者依赖模型的临场发挥,后者把不确定性收缩到了一个可控的范围内。
具体来说,一个设计良好的 Skill 能带来四件实打实的好处。第一,行为稳定。同一个 Skill 每次执行的输出结构是固定的,你可以用 JSON Schema 约束它,下游代码不需要写一堆防御逻辑去猜测返回格式。第二,可测试。每个 Skill 是独立单元,你可以单独喂给它一组输入,验证它返回的结果是否符合预期,回归测试也只需要跑这个 Skill 的用例集,不用把所有场景重新过一遍。第三,可复用。同样一个“网页内容清洗” Skill,既可以用在舆情采集流程里,也可以用在文档归档流程里,只要输入输出协议一致,就能无缝接入。第四,可迭代。你对某个技能有优化想法时,只改这个 Skill 的版本就够了,不用动整个 Agent 的业务逻辑,升级和回滚都很干净。
1.3 Skill 和 Function、Tool、Plugin 到底是什么关系
这里有必要澄清几个概念,因为我在社区里经常看到有人把 Skill、Function Calling、Tool、Plugin 混着说。我的理解是:Function Calling 是模型调用外部函数的一种协议能力,它是底层机制;Tool 是暴露给模型的一个可调用函数的封装形态,通常包含函数名、描述和参数 Schema;Plugin 更偏向产品层,是一组功能和 UI 的整体打包。而 Skill 是一个介于 Tool 和业务逻辑之间的概念,它是一个“完成某类任务”的能力单元,内部可能调用多个 Tool,也可能只有自身的 Prompt 和推理逻辑,最终向 Agent 暴露一个统一的入口。
打个比方:如果 Tool 是一把螺丝刀,Skill 就是“用螺丝刀把面板拆开再换掉故障零件”的整套标准作业流程。它不只是知道怎么拧螺丝,还知道先拆哪颗、后拆哪颗、拆完怎么装回去、装完怎么验证。理解了这层关系,你就知道为什么说“给 Agent 多加点 Skill”不等于“多注册几个 Function”了。Function 解决的是“能做什么动作”的问题,Skill 解决的是“怎么把一件事做完整”的问题。
2. Skill 的结构设计与定义规范
2.1 一个 Skill 的标准组成部分
要落地 Skill 体系,第一步是把结构定下来。参考我在多个项目里的实践,一个设计完整的 Skill 应该包含六个部分:元信息(Meta)、输入协议(Input Schema)、输出协议(Output Schema)、执行逻辑(Execution Logic)、依赖声明(Dependencies)、测试用例集(Test Cases)。这六个部分不是可选项,而是我在项目里验证过的最低配置,缺任何一环都会在后续迭代时付出代价。
元信息是 Skill 的身份证,包括唯一标识符、版本号、作者、描述、标签。其中描述字段非常重要,因为它直接决定了 Agent 的调度器能不能在合适的时机把合适的 Skill 选出来。我见过很多团队在写描述时非常随意,就写一句“处理文档”,结果模型根本不知道这个 Skill 到底能处理什么文档、在什么场景下用、输出长什么样、跟其他 Skill 有什么区别。这样的描述等于没写。一个合格的描述应该包含触发场景、适用对象、可执行的典型任务、以及不适用的情况,尽量用两到三句话把边界描述清楚。
输入协议和输出协议决定了这个 Skill 对外部的接口契约。输入通常用 JSON Schema 定义,输出建议也尽量用结构化数据,即使你的业务最终需要自然语言报告,也应该拆成“结构化中间结果 + 自然语言渲染层”两部分。这样做的原因是结构化输出方便后续程序继续处理,自然语言只是给人看的。如果整个链路都是自然语言,那下游任何一步想做程序化判断都会变得非常痛苦。
2.2 输入输出协议:把接口定清楚
输入协议的设计是 Skill 成败的分水岭。我强烈建议在设计 Skill 之前,先把所有输入字段的意义和边界列出来,宁可多花半天时间做字段梳理,也不要边写边加。因为协议一旦被多个调用方使用,改字段名、改类型、改必填项都是破坏性变更,牵一发而动全身。
举个例子,我在设计“会议纪要整理”这个 Skill 时,最初只定义了 transcript(访谈原始文本)这一个输入字段。后来发现实际使用中有不同来源的纪要需要处理:有人直接粘贴录音转写文本,有人上传 PDF 会议记录,有人只给了一份待整理的事项列表。这三个场景对字段的要求完全不同。如果不区分场景统一走一个输入字段,Skill 内部就要写大量判断逻辑,而且很难兼顾。后来我把输入协议改成了这样:
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "source_type": { "type": "string", "enum": ["transcript", "pdf_document", "structured_notes"] }, "raw_content": { "type": "string" }, "options": { "type": "object", "properties": { "language": { "type": "string", "description": "输出语言", "default": "zh-CN" }, "include_decision": { "type": "boolean", "description": "是否提炼决策项", "default": true } } } }, "required": ["source_type", "raw_content"] }这样设计的好处是调用方很清楚自己该传什么,Skill 内部也可以根据 source_type 走不同的预处理逻辑。输出协议也一样,我会固定一个顶层结构,比如包含 summary、decisions、action_items、risks 这几个字段,每个字段定义好类型和描述。这样下游无论是存数据库还是渲染成报告,都有稳定的数据抓手。
2.3 依赖声明与资源管理
Skill 不只是 Prompt 和代码,它在执行时可能依赖外部资源。常见的依赖有几类:模型依赖(比如某些 Skill 需要支持长上下文的模型才能跑)、工具依赖(比如需要调用浏览器工具、代码解释器)、数据依赖(比如需要读取某个内部 API 或者本地数据库)、第三方服务依赖(比如需要访问某个外部服务)。这些依赖如果不显式声明,在 Skill 迁移或者多人协作时就会出大问题。
我建议在每个 Skill 的 meta 里增加 dependencies 字段,用数组列出所有需要的资源标识。同时在执行层做依赖检查:一个 Skill 在被调度之前,先校验它依赖的资源是否就绪,如果没就绪就直接返回一个明确的错误码,而不是执行到一半才报错。这个设计我在早期没做,结果有一次某个 Skill 的模型依赖被悄悄换成了一个更小的模型,部分功能因为上下文不够直接静默降级了,排查了很久才定位到问题。后来我加了依赖声明和启动检查,这类问题基本绝迹。
另外要提醒一点:Skill 内部尽量不要硬编码环境相关的信息,比如 API Key、数据库连接串、文件路径这些。正确做法是全部通过统一的配置注入。原因有两个:一是安全,硬编码等于把密钥写进代码库,泄露风险极高;二是可移植性,同一个 Skill 要在测试环境、预发布环境、生产环境间迁移,硬编码路径会让迁移变成噩梦。我自己的实践中,所有外部依赖都通过一个 environment context 对象传入,Skill 内部只声明“我需要什么”,不关心“这个依赖从哪里来”。
2.4 Skill 的命名与描述规范
命名和描述看起来是小事,实际上是整个 Skill 体系最容易翻车的隐形工程问题。名字起不好,描述写不清楚,直接后果就是 Agent 在技能路由阶段选错技能、或者压根不选中任何技能。这个问题在 Skill 数量超过 20 个之后会变得尤其明显,我见过有团队做了 30 多个 Skill,结果模型频繁把一个叫“generate_report”的 Skill 当成“analyze_data”来用,就是因为两者的描述里都出现了“数据分析”“报表输出”这些词,边界没有划清。
我自己的约定是:Skill 名称统一用动词开头,比如“extract_action_items”“summarize_meeting”“classify_ticket”,这样模型一看名字就大概知道这个技能是干嘛的。描述部分采用三段式:第一段说清楚适用场景,第二段给出典型任务示例,第三段说明不适用的情况。最后一段非常关键,它能帮模型做负向排除。比如“extract_action_items”的描述可以写:“适用于从会议记录、聊天记录或需求文档中提取明确的任务事项。典型任务包括:从会议纪要中提取待办、从邮件中提取责任人、从需求文档中提取交付物。不适用于回答一般性问题、生成总结报告。”你别说,这段“不适用”的描述在减少技能误调上的效果比其他任何参数都明显。
3. 从零落地一个 Agent-Skills 套件
3.1 场景选择与技能拆解
理论讲再多,不如亲手跑通一个例子。我拿一个实际做过的场景来说明:搭建一个“销售周报助手”Agent,它接收销售团队一周的零散工作记录,自动整理成一份结构化的周报。在这个场景里,我先带着团队做了一次技能拆解会议,最终把整个流程拆成了四个 Skill。
第一是“extract_work_items”,负责从原始文本里提取一条条的工作事项。输入是一大段杂乱的记录,输出是结构化的条目列表,每条包含时间、工作内容、涉及客户、成果描述。第二是“classify_work_type”,把提取出来的条目按预定义类型打标签,比如“客户沟通”“方案制作”“内部协调”“培训学习”。第三是“score_priority”,根据紧急程度和重要程度,给每条工作打一个优先级分,规则是团队内部商量好的。第四是“compose_weekly_report”,把前三个 Skill 的输出汇总起来,生成一个符合周报模板的自然语言报告。
这次拆解让我印象最深的点是:不要试图让一个 Skill 一口气完成从“原始输入”到“最终报告”的整个流程。很多人会觉得“我直接把所有逻辑写在一个 Skill 里不就行了?”短期看确实行,但一旦你的报告模板变了、或者你想在中间环节插入一个人工审核步骤,你会发现一个大而全的 Skill 几乎没法改。拆成四个小 Skill 之后,每一步都能独立优化、独立测试,中间环节也随时可以替换。
3.2 定义数据模型和 Schema
技能拆解完之后,紧接着要做的是把每个 Skill 的输入输出数据模型定下来。这一步我建议先画一张“数据流转图”,不用画得很复杂,就在白板上列出每个 Skill 的输入字段和输出字段,然后用箭头连起来,看看上游的输出是不是能天然成为下游的输入。如果两个 Skill 之间的字段对不上,趁现在调整一定是最省成本的。
以我的周报助手为例,四个 Skill 的数据流是这样的:extract_work_items 输出一个 work_items 列表,列表里每项包含 task_time、description、related_customer、result 四个字段;classify_work_type 的输入是 work_items,输出是加了一个 work_type 字段的增强列表;score_priority 的输入是带类型的工作列表,输出是加了 priority_level 和 priority_reason 字段的列表;最后 compose_weekly_report 接收完整的工作列表,输出的是 { title, summary, sections, suggestions } 四个字段的最终报告对象。
这里有一个非常重要的实践细节:每个 Skill 的输出字段,必须在 Schema 里写明每个字段的枚举值或取值范围。比如 priority_level 的取值只有三个:high、medium、low,不允许模型自由发挥写“非常紧急”之类的文字。这样下游做排序、过滤、统计的时候才有确定性。我在做这个项目的时候,团队成员第一次把 priority_level 设成了自由文本格式,结果模型输出了“重要且紧急”“一般”“先放一放”等十几种漫无边际的说法,后面做统计匹配极其痛苦,后来改成枚举类型,一次解决。
3.3 实现 Skill 执行层
数据协议定好之后,下一个问题是怎么“执行”一个 Skill。在我的实践里,执行层分为两种形态:一种是不需要写代码、完全靠模型推理的 Prompt-based Skill;另一种是需要调用工具、代码、外部 API 的 Tool-based Skill。刚开始做的时候,我建议优先把 Skills 实现成前者,也就是“一个定义良好的 System Prompt + 一组输入输出 Schema + 少量校验逻辑”,等跑通之后再逐步把涉及外部资源的环节替换成 Tool-based。
这里我分享一个实现 Prompt-based Skill 的内部模板,经过多次迭代后已经很稳定了。整体结构分四段:角色定位、执行步骤、输出约束、反例提示。角色定位告诉模型“你在执行一个什么任务”;执行步骤是给模型提供可遵循的操作流程,降低发挥空间;输出约束把输出格式卡死;反例提示告诉模型“这些情况不要做”。举一个 compose_weekly_report 的例子:
"你是一名销售运营助理,负责把团队一周的工作记录整理成周报。请严格按以下步骤执行:1. 通读输入的 work_items 列表;2. 按 work_type 字段分组;3. 在每组内按 priority_level 从高到低排序;4. 生成总结段落,说明本周主要进展和关键指标;5. 输出 JSON,格式必须符合 Output Schema。注意:不要在报告中添加输入数据中不存在的信息;不要臆造客户名称和数字;如果输入列表为空,请在 summary 字段直接返回'本周暂无工作记录'。"
注意这里每一步都是显式指令,模型没有太多自由发挥空间。有人担心“这样写是不是太死板了”,我的经验是,Agent 任务的稳定性永远优先于创造性。你需要创造性的是 Skill 的设计过程,而不是 Skill 的执行过程。
3.4 技能路由与选择策略
有了多个 Skill 之后,Agent 怎么知道当前这个用户请求该用哪个 Skill?这就是技能路由要解决的核心问题。在实践中我见过三种主流方案,各自适用不同阶段。
第一种是规则路由,维护一张关键词和规则表,命中后直接绑定 Skill。优点是简单可控、零模型开销,缺点是泛化能力极差,换个说法就失灵。第二种是语义路由,把用户输入和每个 Skill 的 description 一起丢给模型,让模型判断该调用哪个。这种方式是最推荐的,成本低而且效果不错,元信息里的描述是否写得清楚,就直接决定这条路的效果。第三种是向量检索路由,把用户输入和 Skill 描述都向量化,在向量数据库里做相似度匹配,返回 Top-K 候选,再交给模型或规则做最终决策。这种方式适合 Skill 数量很大、超过 100 个的场景,但实现和维护成本也高。
我的实践建议是:如果只有 10 个以内的 Skill,直接用语义路由就够了,用一个模型调用把所有 Skill 的描述传进去,让它输出一个选择决策。注意这里的决策信息一定要结构化输出,包含 selected_skill_id、confidence、reason 三个字段。confidence 低于阈值时,Agent 应该采用兜底策略而不是硬着头皮执行。我在这上面吃过亏,当时模型以 0.5 的置信度选了一个技能,我以为它能行,结果输出完全跑偏,后来加了置信度阈值校验,0.7 以下直接走“澄清问题”分支,明显稳了很多。
3.5 测试与回归机制
Skill 体系最大的优点之一是可独立测试,但前提是你真的把测试建起来了。我推荐每个 Skill 维护一个 cases 目录,里面至少包含三组用例:正向用例(happy path,正常的输入输出对)、边界用例(空输入、超长输入、缺失字段、枚举值之外的输入)、负向用例(明确应该拒绝的场景)。
正向用例的意义是确信“这个功能没退化”。我会把每个 Skill 在迭代关键版本时的输入和输出快照存下来,之后的改动都拿这批数据回归。边界用例的价值在于防止系统偶发崩溃。我印象最深的一个案例是某个 Skill 接到的输入字段是空字符串,模型当时的处理方式是把空字符串当成了“没有输入”,然后生成了完全编造的答案。后来我在执行层加了校验逻辑——如果关键输入字段为空,直接返回错误码 ERR_EMPTY_INPUT,不允许模型临场发挥。
负向用例则是你把控安全性的最后一道防线。比如“销售周报助手”里的 extract_work_items,如果输入内容是一段与销售完全无关的闲聊,我希望它返回“无法识别到有效工作事项”,而不是硬凑几条看起来像样实则编造的内容。这个能力必须靠负向用例反馈给模型,让它在设计中就知道什么情况下要“拒答”。没有这一步,你会在大规模应用时被模型幻觉问题打一个措手不及。
4. 常见问题与排查技巧实录
4.1 Agent 不会正确调用 Skill 怎么办
我收到过最多的咨询就是“我的 Agent 根本不会调用 Skill,或者总是调错”。面对这个问题,我先不让你去检查代码,而是让你先检查 Skill 的描述和名称。我在 2.4 节里强调过的内容,在线上环境里会成倍地放大。排查路径是这样:先看模型的输出日志里选中的 skill_id 是什么;再看这个 Skill 的名称和描述是什么;最后想一下在模型眼里,当前这个用户问题和这个 Skill 描述之间是否真的存在强关联。如果描述写得含糊不清,模型调错不怪模型,怪你自己。
另外一个高频坑是同时注册的 Skill 过多。早期的 Agent 框架里,所有工具都一次性传给模型,模型面对的候选越多,选择错误率越高,同时 prompt 变长还会挤压真正的上下文空间。我的处理办法是引入“预筛”流程:先用向量检索把几百个 Skill 粗筛到 3 到 5 个候选,再把候选描述丢给模型做最终选择,这样错误率和上下文开销都显著下降。这套方案在几十个 Skill 的中型项目中已经足够,不必一开始就上复杂的企业级路由框架。
4.2 Skill 之间职责重叠怎么处理
Skill 一多,边界一定会模糊。比如“analyze_data”(分析数据)和“generate_report”(生成报告),在很多人的定义里几乎就是一个东西,但在我这套体系里,我应该让“analyze_data”侧重统计计算和结论提炼,“generate_report”侧重把分析结论按模板展开成完整文档。边界切的越清晰,模型选错的概率越小。
遇到重叠问题时,我的做法是先从描述上做负向排除——在各自的 description 里明确写“这个任务不在本技能范围”。如果负向排除之后还是频繁选错,就要考虑是不是该合并 Skill,或者把公共部分抽成子模块。比如多个 Skill 里都需要做“文本清洗”,我会把清洗逻辑抽成一个共享子工具,而不是在每个 Skill 里各写一份,这样既减少了重复代码,也避免了各版本行为不一致的问题。
4.3 技能执行失败如何降级
线上环境里,Skill 不可能永远执行成功。模型超时、返回格式不合法、校验不通过、外部依赖挂了,这些都是家常便饭。我的原则是:每个 Skill 在失败时都必须返回结构化错误码,而不是抛一个异常或者返回一段自然语言的“我失败了”。错误码要尽量具体,比如 ERR_MODEL_TIMEOUT、ERR_INVALID_OUTPUT、ERR_DEPENDENCY_UNAVAILABLE,这样上层调度才能根据错误码决定是否重试、是否换一个 Skill、还是直接告知用户稍后再试。
另外一个经验是:重试时不要原样重试,而是做输入扰动。比如模型超时可能是并发太高,重试时可以降低模型温度;校验失败可能是模型输出格式漂移,重试时可以在 Prompt 里追加一句“请严格注意输出格式,上一次格式不合法”。我观察过,第二次带反馈的重试成功率比盲试高很多。这是很多人的盲区——失败后只顾着重发请求,却没有给模型任何额外的帮助信息。
4.4 版本更新踩过的坑
Skill 版本管理看起来很基础,但做起来容易掉进两个隐坑。第一个是没有“缓存失效”机制。模型输出的结果会被缓存,但 Skill 更新了之后,旧缓存还在,用户拿到的还是旧逻辑结果。你要确保 Skill 内容更新时,相关缓存的 key 也要带上 skill version 字段,不匹配就自动失效。第二个是跨 Skill 的兼容性。上游 Skill 输出协议改了,下游 Skill 的输入协议如果没有同步更新,整条链路会在运行时才报错。强烈建议在 CI 里加一道协议兼容性检查——每次更新 Skill 时,自动比对所有相邻 Skill 之间的输入输出 Schema 是否有破坏性变更。
我自己的习惯是:每个 Skill 的版本号就写在它的 meta 文件里,每次改动必须更新版本号并附上 changelog。虽然这套流程在项目早期看起来有点重,但等你的 Skill 积累到几十个、团队也开始有几个人协作的时候,没人能在没有版本记录的情况下说清楚“这个行为是哪个版本引入的”。那时候你再回头看,会觉得这套版本管理机制帮你省下了大把排查问题的时间。
5. 经验总结与工作流沉淀
如果说这段实践之旅教会了我最重要的一件事,那就是:Agent 应用开发的核心瓶颈,不完全是模型能力,而是工程化水平。你给 Agent 配了多强的模型,都不如把它需要做的每个动作封装得清晰、稳定、可维护、可测试。agent-skills 本质上是一种工程手段,它把“让模型做好一件事”拆成了“定义清楚一件事”和“让模型按约定完成一件事”两个更可控的问题。
我给正在尝试落地的朋友一个建议:不要贪多求全,先挑一个你最常用的业务场景,拆出三到五个 Skill,把每个 Skill 的输入输出协议和描述规范打磨到位,再把测试用例建起来。你自然会发现,Agent 的整体表现会上一个台阶,而且这个体系会越长越顺。等你积累足够多的技能之后,你会发现原来零散的工具函数正在聚合成一张越用越懂得如何协作的任务网络,这才是 agent-skills 真正有趣的地方。