这年头做 AI Agent 的,谁没被提示词工程毒打过几回。功能少的时候,一段 Prompt 加几个函数调用还能撑住;一旦任务稍微复杂点,提示词就变成上千行的“缝合怪”,改一个需求牵一发动全身,上下文里塞满了历史会话的边角料,模型开始前言不搭后语。我自己折腾 agent-skills 这个项目,就是被这种痛点逼出来的——把过去散落在提示词里的各种能力,重构成一套可复用、可编排、可独立升级的“技能系统”。它本质上解决的是 Agent 工程化的问题:让智能体不再是一个靠长文本堆逻辑的“一次性脚本”,而是一组有清晰接口、能按需组合的能力模块。
这篇文章不是纯概念科普,更多是记录我在落地 agent-skills 过程中踩过的坑、验证过有效的设计原则,以及一套可以直接照抄的实现路径。无论你是被复杂 Agent 需求折磨的开发者,还是刚接触智能体、想搞清楚“技能”和“提示词”到底差在哪儿的初学者,这篇内容应该都能帮你少走不少弯路。
1. agent-skills 到底在解决什么问题
1.1 当纯提示词工程扛不住复杂度
我先说一个很典型的场景:你想做一个能自动生成周报的 Agent。第一版很简单,一段 Prompt 告诉模型“根据我提供的邮件、日程、群消息整理本周工作”,跑起来效果不错。接着你发现周报里需要包含数据指标,于是往 Prompt 里塞了“如果消息里有访问量、转化率就单独列一个模块”;然后又希望周报结尾自动生成下周计划,于是又把“结合本周进展和公司目标推断下周安排”加进去。这时候 Prompt 已经将近 800 行,模型每次处理都要重新读一遍全部规则,响应变慢,而且经常出现某个模块的规则被另一个模块覆盖的情况——反复调 Prompt,调到自己都想吐。
这个问题本质上是逻辑复杂度已经超出了“自然语言直写”的管理上限。提示词本身是扁平文本,内部没有边界,没有作用域,没有版本控制。你想让不同功能之间互不干扰,靠的是模型对文本的理解能力,而不是结构性约束。可一旦能力变多,模型的注意力就有限,它不可能在每一次推理时都精准平衡所有规则。这时候你需要的是把能力拆开、封装、隔离——这正是 agent-skills 存在的理由。所谓技能,就是一段描述清晰的、可独立执行的能力单元,有明确的输入输出约定,由 Agent 在需要时动态选择调用,而不是把所有逻辑一次性塞进语境里。
1.2 技能不是函数,不是插件,也不只是工具调用
很多人一听“技能系统”,下意识想到的可能是 Function Calling,或者浏览器插件那一类东西。这里得掰扯清楚:技能和函数调用有重叠,但不在一个层面。函数调用解决的是“让模型能调用一个确定的外部操作”,比如查询天气、发送邮件,它是原子的、无状态的。而技能是复合的,它可能是一个流程,可能包含多步推理,可能内部还要自己决定调用几个工具。
举个例子,“生成周报”可以是一个技能。它的内部实现至少包括:读取邮件摘要、读取日程安排、筛选关键事项、分析数据趋势、输出结构化 Markdown。如果把它拆成函数,你至少要拆五六个函数,然后把它们如何组合的逻辑交给模型自行判断;而做成技能,Agent 只需要知道“遇到周报相关需求就用 generate_report 这个技能”,具体怎么组合、按什么顺序,是在技能内部的执行策略里定义好的。这就是“工具”和“技能”的关键区别:工具是零件,技能是“加工流水线”本身。
插件也有类似的问题。传统意义上的插件通常绑定特定的宿主应用或者框架,技能则更抽象一些——它强调通用的可移植性。我在设计 agent-skills 时,刻意让技能定义尽量跟底层模型解耦:同样一份技能描述,既可以被这个模型用,也可以被另一个模型用,只是执行效果各有差异。这个设计思路可以参考现成的开源协议框架,比如早期的 AgentSkills 规范讨论里就反复强调过可移植这点。
1.3 一套技能系统的核心组成
一个能落到生产环境的技能系统,至少要有四个部分:技能定义、技能路由、技能执行、状态管理。技能定义描述这个能力是什么、什么场景触发它、它接受什么输入、会输出什么;技能路由解决“当前对话交给哪个技能处理”的问题,通常是模型根据用户意图和技能描述做匹配;技能执行是具体跑逻辑的部分,可能是代码、可能是嵌套调用别的技能,甚至可能是数据库查询;状态管理保证技能的执行结果能在不同技能之间传递而不互相污染。
这四个部分缺一个,技能系统就容易变成“只是换了一种写法提示词”。我在做第一版 agent-skills 时就犯了执行部分拆得不干净的毛病:技能描述写得挺好,路由也正常,但所有技能的内部执行逻辑实际上还是直接拼进同一个 Prompt,等于没有真正隔离。后来才知道,技能要有独立的上下文窗口,用系统级 Prompt 描述角色和当前任务,再注入用户输入和必要的参考材料,这样才能避免 A 技能产生的中间结果干扰 B 技能的判断。
2. 技能怎么设计:从场景拆出可复用的能力
2.1 从业务场景倒推技能清单
技能设计的第一步不是写代码,而是先理清楚你手头的业务到底有哪几类高频任务。拿我自己做的一个内容运营 Agent 举例,我最初列了大约二十个需求点,包括选题策划、文章大纲生成、素材素材检索、初稿撰写、SEO 标题优化、竞品分析、排版建议、发布前自检等等。二十个直接做成二十个技能显然不合理,很多需求之间有重叠,而且粒度过细会导致 Agent 路由选择困难——它面对太相似的两个技能描述时会不知道选哪个。
我的做法是对需求做一次聚类。把“能复用同一种底层逻辑”的任务合并:比如 SEO 标题优化、大纲结构调整、初稿撰写,本质上都依赖“文章内容理解与改写”,可以归入“内容创作”大类,再在这个大类里根据不同输出格式拆出独立的技能。最终我把二十个需求收敛成了五个技能:意图理解、素材收集、内容生成、内容精修、发布排版。这里分享一个判断标准:如果两个任务在执行时需要读取完全不同的数据源,且输出结构完全不同,它们更适合拆开;如果只是输出格式略有差异,执行逻辑类似,则优先合并。技能不是越细越好,而是越“语义独立”越好。
2.2 技能粒度怎么定
我踩过比较深的坑是技能粒度过小带来的“链式风暴”。系统里设了“提取重点”“分析情感”“生成总结”“格式化输出”四个技能,看起来职责明确,可模型在处理用户请求时,经常一场对话里发起五六次技能跳转,每跳一次都有额外的推理消耗和延迟,还增加了上下文丢失的风险。后来把四个合并成“内容摘要”一个技能,内部规定好流程:先提取关键信息,再情感分类,最后按用户指定模板输出。效果反而更好,因为流程被固定在技能内部了,不依赖模型临场发挥。
反过来,粒度过大的问题也很要命。我之前试过把所有跟“数据分析”有关的需求全部塞进一个技能里,结果这个技能的描述得写两千字,内部实现光是分支条件就有十多个。模型每一次走这个技能,都像在一张巨型流程图上找路,未必走对。经验是:一个技能的执行步骤建议控制在三到七步之间,超过七步就要考虑拆分成子技能。子技能不代表路由层会看到它——子技能可以挂在父技能内部,用户在对话层感知不到这种拆分,但执行层能更稳定。
除了步骤数量,还需要关注技能的“触发范围”。好的技能描述应该让模型一眼看出“什么情况该用我”。比如一个“周报生成”技能,描述里不需要写“当用户问任何问题时可以使用我”,而应写成“仅当用户要求汇总一段时期(如一周)的工作内容并形成结构化报告时使用”。清晰的触发条件能显著提升路由准确率,这部分我会在后面的排查章节重点讲。
2.3 技能描述、参数与触发条件怎么写
技能定义的核心是三块:描述、参数模式、执行说明。描述是给模型看的,决定它会不会在正确场景下选中这个技能;参数模式决定你能从用户的请求里抽取出哪些信息来填充执行逻辑;执行说明则是给技能运行引擎看的,约束它该怎么干活。
我把描述部分拆成两个层级:上层“一句话概括”,下层“详细适用场景”。一句话概括控制在二十个字以内,类似“为内容运营生成结构化周报”,目的是让路由模型快速扫过就能建立印象。详细适用场景则列出五六个典型触发例子,比如“用户提到本周工作汇总”“用户上传了三天的聊天记录并要求整理成周报”等等。这里有一个细节:举例子的价值远大于抽象的规则描述。模型对具体例子的泛化能力比我们想象中强,你与其写“适用于任何带有时间跨度的工作汇总场景”,不如写“当用户说‘帮我写一份这周的周报’或‘周日总结一下这周做了什么’时启用”。
参数设计也需要克制。不要试图把所有信息都定义成参数,那些执行技能时用不到的信息只会增加路由和抽取的干扰。我习惯只用三到五个核心参数,其余信息全部塞进自由文本的补充字段。比如“素材收集”技能,核心参数只有“主题关键词”和“收集范围”,至于素材渠道、时间范围,都归入自由文本,让执行层的提示词自己去提取。参数定义太多,模型抽取时反而容易抓了芝麻丢了西瓜。
2.4 技能之间的依赖怎么处理
技能不可能完全独立,现实中总有一个技能需要另一个技能的产出。我最初天真地让技能之间直接互相调用,后来很快就乱了:周报技能调数据统计技能,数据统计技能又调数据库查询技能,数据库技能报错后,错误信息在链路里层层穿梭,最终传回用户时已经面目全非。后来我收敛了依赖策略:技能之间只允许数据依赖,不允许流程依赖。简单说,技能 A 可以接收技能 B 的输出结果作为输入,但 A 不能主动“要求” B 去跑一段流程,流程编排层统一在 Agent 主控里定义。
具体实现上,我会在技能定义里增加一个“依赖数据项”的字段,标注该技能运行前需要哪些外部数据。主控引擎负责检查这些依赖是否已经满足,不满足就自动触发上游技能,满足就直接进入执行。这样技能的依赖关系变成一个有向无环图,既清晰又可控。依赖图还能顺便做并行优化——多个没有依赖关系的技能可以同时启动,能显著缩短链路耗时。
3. 从技能定义到跑通全流程
3.1 最轻量的落地:YAML 技能包
不想搞重型框架的话,我建议直接用 YAML 定义技能包,结构清晰,易维护,也方便用 Git 做版本管理。一个基础技能包长这样:
name: weekly_report description: 为内容运营团队生成结构化周报 when_to_use: - 用户要求汇总一周的工作内容 - 用户上传了本周的聊天记录并要求整理周报 - 用户提到“周报”“本周总结”“weekly report”等关键词 parameters: - name: period description: 报告覆盖的时间范围 required: false - name: focus description: 周报的重点关注方向 required: false steps: - step: collect_materials description: 从输入中提取邮件、日程、聊天记录关键事项 - step: analyze_trend description: 如果输入中包含量化数据,识别变化趋势 - step: generate_report description: 按周报模板输出结构化 Markdown output: markdown这个 YAML 主要表达了技能的名字、适用场景、参数列表和执行步骤。真正跑起来的时候,引擎会把它翻译成一套内部指令,例如把 when_to_use 变成路由模型的匹配准则,把 steps 变成执行层的任务清单。推荐用 YAML 还有个好处:你可以在不同环境里维护同一技能的不同版本,比如灰度环境下放一个“实验版”,线上环境用“稳定版”,切换成本几乎为零。
3.2 运行时怎么让 Agent 学会用技能
技能定义写好了,运行时怎么让它生效?我在 agent-skills 里采用的是“提示词注入 + 路由选择”的双层结构。第一层是主控制器:系统提示词里说明“你是一个带技能系统的 Agent,以下是当前可用技能列表”,然后把所有技能的 description 和 when_to_use 逐个注入。第二层是路由:模型在收到用户请求后,先用一次轻量推理判断应该调用哪个技能,或者决定不需要技能、直接用通用能力回答。
这里有个性能优化细节:技能数量一多,全量注入会撑爆上下文。假设每个技能描述占 500 个 token,一百个技能就是五万个 token,还没开始干活模型就已经被提示词塞满了。我的做法是先做一次召回。用简单的关键词和语义匹配把候选技能压缩到五到十个,再把这几个候选的完整描述注入主控。召回层可以用 embedding 向量检索,也可以直接用规则匹配。如果技能描述里的触发词跟用户请求有重叠就优先选中,这个粗暴方法实测下来准确率已经不差。
关于模型选择,我目前的做法是路由决策用便宜快速的小模型,技能执行阶段用强推理的大模型。路由只需要判断“进哪个门”,不需要很深的理解能力;但执行阶段是有真实逻辑任务的,推理能力不足容易跑偏。这种分离还有一个额外好处:小模型路由失败的影响面可控,你在排查问题时能明确知道是选错了门,还是门里面干活出了问题。
3.3 编排:串行、并行与条件分支
拆技能只是第一步,把多个技能串成一条能解决实际问题的流水线才是真正出价值的部分。我最初直接在 Agent 主控里写死流程:意图理解技能跑完,素材收集技能跑,素材收集跑完,内容生成技能跑。写起来痛快,但遇到新需求就得改代码,跟“增加逻辑就要改提示词”的老问题没有本质区别。
之后我换成了声明式编排,把流程定义和数据流跟代码解耦。比如一个“自动写公众号推文”的流水线,编排配置大致长这样:
pipeline: - skill: intent_detection output: intent - skill: material_collection params: theme: ${intent.theme} - skill: article_generation params: materials: ${material_collection.result} style: "${intent.style || 'default'}"这种风格的配置一眼就能看出数据从哪个技能流到哪个技能。技能执行完,结果统一存入一个会话级的“共享数据区”,下一个技能按 key 取值。条件分支也在编排层处理,比如“如果素材不足则追问用户”这种逻辑,不会写死在技能内部,而是由编排引擎读取前一个技能的输出状态决定下一步动作。做好这一点后,新增一条流水线基本不用动代码逻辑,拼配置就行。
3.4 上下文隔离与状态管理
技能执行最大的隐形杀手是上下文污染。一个技能跑完后留下的中间推理、临时结论、内部报错,如果不加清理,就会混进下一个技能的上下文。模型对此非常敏感:上一技能提到“邮件数量偏少”,下一个技能给用户生成周报时,可能莫名其妙来一句“根据邮件情况,本周工作产出有限”——信息串味了。
我在项目里强制要求每个技能拥有独立的事件上下文:技能开始时从共享数据区读取自己需要的输入字段,执行过程中的全部中间量都留在技能的私有上下文里,只有声明过的输出字段会被写回共享数据区。换句话说,共享区是“接口层”,私有上下文是“实现细节”,两者严格分离。技术上实现也不复杂,每次技能执行前给消息序列起一个新根节点即可。
另一个值得注意的状态问题是会话的长期记忆。技能执行完之后,应该把哪些信息沉淀到长期会话里,是需要明确规定的。我一般只把最终输出结果和用户明确表达的偏好写入长期记忆,中间步骤的推理过程一概不保留。否则会话一长,历史记忆里全是技能内部碎碎念,核心信息反而被稀释了。
4. 实测中绕不开的坑与排查思路
4.1 选错技能:路由准确率怎么提
技能系统落地后,我遇到频率最高的错误就是路由选错技能。用户说“帮我把这段话改得正式一点”,系统却触发了“SEO 优化”技能,把好好的通知改成了一堆重复关键词的营销稿。排查发现,问题出在技能描述上:SEO 优化技能的 when_to_use 里写了“适用于改写文本以提升搜索表现”,而“改得正式一点”这个表述里包含了“改”,跟触发词撞了。
修复方式分两步。第一步,在技能的 when_to_use 里增加否定样例,明确写出“不要在用户仅要求调整语气或风格时使用本技能”。这个方法见效很快,能直接把这类误触发压下去。第二步,在路由层加一层“置信度阈值”机制:如果模型对技能选择的置信度低于设定阈值,宁可不让任何技能介入,直接用通用对话能力回应。这个兜底非常必要,技能不是必须每次都用的,选错还不如不选。
还有一个值得尝试的方法:针对高频误触发场景,单独加一个“风格改写”技能。既然模型分不清“SEO 改写”和“风格改写”,那就把两者的边界用更具体的技能描述拉开。有时候某个技能触发率异常偏高,不一定是你这个技能写得不好,可能是缺少一个更精准的“替代技能”来分流。
4.2 上下文污染与参数打架
第二种高频问题是上下文污染导致的输出错乱。症状表现为:技能 A 本来只负责收集素材,结果输出里混进了“下周建议”之类的内容;或者技能 B 明明只生成初稿,结果把上一轮对话里的用户抱怨也写进了正文。排查时我先看共享数据区,通常能看到一些没被清理的中间变量被后续技能读取了。
我的整改措施是把共享数据区升级成带 Schema 校验的结构化存储——每个技能只能读取声明过的字段,写入字段前先做一次格式检查,不合法就拒绝。这一步看起来好像是给自己找麻烦,但实际调试时价值巨大:数据贯穿链路后,你能准确判断是上游写错了还是下游读错了,而不是对着一段乱七八糟的输出瞎猜。
参数打架的另一种形式是用户原始请求里的信息被参数抽取逻辑破坏。比如用户说“本周和上周的数据对比一下”,两个时间段如果被参数化后只保留一个,另一个就丢了。后来我把“时间段对比”这类场景做成了独立技能,不再依赖通用参数抽取,专门处理多值输入的比对逻辑。记住一点:当某个技能的参数设计需要不断打补丁才能满足新场景时,优先考虑这根本不是参数问题,而是场景应该被拆成独立的技能。
4.3 技能版本失控
技能系统上线之后,一定会面临持续迭代。最原始的版本管理做法是直接改 YAML 文件,测试完就上线。听起来没问题,但实际运行一段时间就糟了:线上 Agent 还在处理会话,技能却已经被更新成新逻辑,部分用户侧上下文里的老数据和新版技能的输入格式对不上,响应质量瞬间下滑。
我现在坚持对每个技能单独维护版本,并且在技能调用时固定快照——也就是说,一个会话在哪个版本下启动,整个会话就固定用那个版本的技能定义,不跟随线上后续升级。这样可以保证单次会话内逻辑一致,不受发布影响。新版本通过灰度环境验证后再全量切流,遇到问题也可以按版本号快速回滚。
另外强烈建议给技能执行加日志埋点。每跑一个技能,记录它的版本号、输入参数摘要、输出摘要、耗时、路由置信度、是否触发兜底分支。有了这些数据,你才能准确回答“这周技能表现到底有没有变差”,不然排查问题全靠猜。
4.4 效果评估与回归
技能系统的效果评估,跟普通提示词工程不一样:你不知道是哪一个技能坏了,只知道整体表现下降了。所以我搞了一套针对技能的回归测试集。每个技能维护十条左右的典型测试用例,每条用例包含输入请求、预期选中的技能、预期输出里的关键内容点。每次改完技能定义或编排逻辑,就跑一遍全量回归。
评估指标上,我最关注的是三个:路由准确率(该调技能的时候有没有调对)、技能成功率(技能跑完有没有产生有效输出而不是报错或胡言)、链路总耗时。还有一个容易被忽略但非常重要的指标:技能误干扰率——用户压根没要求技能介入,系统却擅自调用了。这个指标可以直接反映你的技能系统是不是“过度积极”,我自己的经验是把误干扰率控制在 1% 以下才敢放生产环境。
真的跑起来之后你会发现,所有评测指标里最难优化的其实是误干扰率,因为它的样本往往要靠用户反馈才能发现。我会在每次技能调用时为用户提供“这不是我想要的”的操作入口,把这些负面反馈回流到测试集里,持续补强边界样例。技能系统的效果不是一锤子买卖,越用越准才是它的正确打开方式。
我在实际折腾 agent-skills 的过程中体会最深的一点是,这个方向真正难的从来不是写代码,而是能不能在“能力拆分”和“系统复杂度”之间找好平衡。技能拆得太细,路由和编排的压力会倍增;拆得太粗,技能内部就又变回了巨无霸提示词。任何你觉得别扭的边界划分,大概率都是手感在提醒你再想想清楚。如果这篇分享能让你在设计自己的技能系统时少踩两个坑,那就算值了。