☰
如何构建Agent技能库?agent-skills实战拆解与设计取舍
2026/10/13 4:47:47 网站建设 项目流程

开头:

我在第三个月开始整理一个代号叫 agent-skills 的技能库实践项目,起因很简单:Agent 光是"能对话"远远不够,真正决定它干不干活、干得好不好的,是它手里有没有一套可以稳定复用、边界清晰、可测试可观测的技能。用一句直白的话说,Agent 的智商是模型给的,但 Agent 的干活能力,是靠技能堆出来的。这篇文章不聊那种炫酷的 Demo,而是把我从零开始搭建 agent-skills 技能体系时踩过的坑、沉淀下的设计取舍、真实的目录结构和调度逻辑,从头到尾摊开讲一遍。如果你正在做 Agent 接入业务系统、自动化流程编排、或者让模型输出稳定可落地的操作,这篇内容应该能帮你少走不少弯路。

我用了一段时间把技能和普通"工具调用"区分开,是因为项目做到后面发现两者根本不是一回事。工具调用只是给模型一个函数,它调完就算完了;而技能是一个完整的执行单元:它包含触发条件、输入参数、执行逻辑、输出约束、异常处理,还有一段专门写给模型看的"什么时候用什么技能"的说明。agent-skills 的价值,就是把这些东西从模型提示词里剥出来,变成一个一个独立文件,让 Agent 主循环像查菜单一样按需选取。下面我从技能的定义开始,一步步拆开这套体系。

1. 为什么我把 Agent 能力拆成"技能"而不是继续堆提示词

1.1 第三个月,提示词膨胀到收敛不了

最初我做的 Agent 是那种很典型的"一个大 System Prompt 包打天下":模型角色、业务背景、工具说明、输出格式、注意事项全塞在一个超长上下文里。前两周还好,等到任务类型从五种涨到二十几种,提示词膨胀到接近两万字的时候,问题全冒出来了。模型经常在多个相似的操作说明之间犹豫,同一个动作在不同任务里翻来覆去地重复描述,偶尔改动一个业务字段,还不知道该改哪一段,改完又怕影响别的场景。

后来我把日志翻出来逐条看,发现一个规律:模型不是不知道要做什么,而是不知道该用哪一套操作方式来做。它面对的不是"会不会"的问题,而是"选哪个更合适"的问题。提示词里所有操作步骤是平铺的,没有边界,没有标签,模型每次都要从一长篇文字里自己"找路线",找着找着就找错了。这个阶段我意识到,继续堆提示词是一条死路,得把操作能力从"描述"变成"独立对象"。

1.2 技能让模型从"读说明"变成"选套餐"

技能化的第一步,是把原先提示词里描述的那些操作拆成独立单元。每个单元有名字、触发场景、输入参数、执行体,还有"给模型看的挑选说明"。Agent 主循环拿到用户目标后,先做意图分解,再把每个子目标拿去和技能列表匹配,命中哪个就执行哪个。区别很直观:以前模型要现场"阅读理解",现在它只需要"按目录检索"。

这个转变的本质,是把模型的注意力重定向到更小、更聚焦的上下文。我不需要让模型理解全部业务逻辑,只需要让它在几十个技能的清单里挑出最符合当前子目标的那个。挑完之后,技能自己带着完整参数和执行说明进入运行期,模型的上下文里只剩当前这一个任务的细节,认知负载一下降下来了。实测下来,意图分错的比例下降了大概四成,对 LeakDetection 这类对意图敏感的检测类任务,效果尤其明显。项目里当时正好有个环节专门做工具操作异常检测,把"识别异常"这件事做成了一个技能之后,模型只需要判断"要不要调用它",判断准确率直接上了一个台阶。

1.3 技能不是函数的简单包装

很多人以为技能就是把函数加一段描述,格式化成一个 JSON 给模型就行。我的经验是,这远远不够。函数描述解决的是"这个函数是干嘛的",而技能还需要解决"什么时候用、什么情况下不能用、参数从哪来、失败了怎么办、输出怎么收"。举个例子,我有一个生成周报的技能,它不只是调用一次文本生成接口,它内部还要拉取指定项目的进展数据、判断数据是否完整、按模板填充、最后再校验输出格式是否符合要求。这个执行链如果拆成一个函数给模型,模型可能第一步就不知道该传什么参数。

所以我把技能设计成四层结构:第一层是技能定义(元数据、参数模式、权限);第二层是计划文本(专门写给模型的挑选说明和调用前置条件);第三层是执行逻辑(代码或脚本,也可以组合多个小技能);第四层是返回处理器(把原始输出按契约整理成结构化结果)。四层全齐,才算一个完整的技能。agent-skills 项目里所有技能文件都按这个四层结构组织,后续新增能力时,只需要套模板。

2. 一份技能清单的自我修养:输入、输出与执行契约

2.1 技能签名的字段设计

技能清单最核心的部分是签名。签名定义得不好,后面全白搭。我最终沉淀下来的字段分成四组:身份字段(name、version、domain)、触发字段(description、when_to_use、when_not_to_use)、契约字段(input_schema、output_schema)、治理字段(permissions、timeout、risk_level)。

name 我用统一的 snake_case,格式是 domain_verb_noun,比如 data_validate_sql、content_generate_summary。domain 是技能域,verb 是动作类型,noun 是操作对象。这样命名有两个好处:一是技能库文件列表扫一眼就知道覆盖了哪些能力;二是检索阶段可以直接按 domain 快速过滤候选集合。version 字段我一般写语义化版本,技能内部逻辑只要发生不兼容变化就升主版本,只改描述说明就升次版本,方便后面做灰度对比。

输入参数我强烈建议直接用 JSON Schema,不要图省事写自然语言描述。原因很简单:JSON Schema 可以声明类型、约束、默认值,还能做运行时校验。模型按 schema 填参的时候,就算某个参数给错了类型,执行端也能在进入业务逻辑之前拦截下来,而不是让错误泛化到结果里。

2.2 描述文本才是给模型看的"销售文案"

技能描述和字段描述不是给人看的注释,是给模型看的销售文案。别小看这段文字,它直接决定模型在候选列表里点不点你的技能。我的经验是,description 里要讲清楚三个东西:这个技能能交付什么结果、它适合处理什么类型的输入、它不适合处理什么。

我见过一个失败的案例,某开发者把一个数据筛选技能写成了"对输入数据执行数据操作",模型完全不知道它到底能筛什么东西、筛完给什么样的输出。把他改成"从数据仓库表结构中提取字段级血缘关系,适用于数据血缘分析和字段审计场景,不适用于数据质量评分"之后,命中率立刻上去了。还有一点:when_not_to_use 不是可有可无的。它可以避免模型在技能相似度过高时反复横跳。比如我同时有 content_generate_summary 和 content_extract_keywords,两个都在摘要域,模型容易分不清,如果各自说明里写明"本技能只负责xxx,不负责yyy",冲突就少很多。

2.3 输出契约与异常契约

输出比输入更容易被忽略,但往往输出才是执行链终端的命门。我给每个技能定义了 output_schema,规定正常结果必须包含哪些字段。如果你做的是流程编排类 Agent,下游步骤依赖技能的输出,这时候输出结构不稳定,整个编排就变成踩西瓜皮。我还加了一个统一包装层,所有技能返回结构都是{status, data, error, trace_id},正常时 status 是 ok,data 里放业务结果;异常时 status 是 error,error 里放错误码和人类可读的消息。

异常契约也很重要。我在执行逻辑里统一捕获三层异常:参数校验异常、业务执行异常、资源权限异常。对应返回不同的错误码,Agent 主循环看到错误码后能决定是重试、换技能,还是明确告诉用户"这件事做不了"。没有异常契约之前,技能一报错就是把一段堆栈丢给模型,模型要么瞎猜重试,要么一脸懵。加了错误码体系之后,重试策略变得非常可预期。

3. 技能库的摆放方式:目录、命名与依赖关系

3.1 按领域和场景组织目录

技能文件一旦超过三十个,组织结构就成了新的问题。我一开始把所有技能文件平铺在一个目录里,找了半天找不到想要的。最后我按领域划分子目录,并且在技能清单上额外加了一个 scene 字段。目录结构大概是这样的:

skills/ common/ # 通用基础技能 get_system_time text_segment data_analysis/ # 数据分析域 query_warehouse validate_metric content/ # 内容生成域 generate_summary extract_keywords workflow/ # 编排类技能 weekly_report_agent data_issue_triage

scene 字段用于标注场景,比如"适合在日报生成链路中调用""适合在异常归因链路中调用"。这相当于给技能打了两层标签:一层是能力领域,一层是业务场景。检索阶段先按 scene 和 domain 联合过滤,得到少量候选,再把这几个候选的描述交给模型做最终选择。这个两级过滤策略比我一开始"把所有技能描述全塞给模型选"要稳定得多,候选多了模型真的会犯晕。

3.2 技能依赖与子技能调用

技能之间不是完全孤立的。有些编排类技能需要调用底层基础技能。我在技能清单里加了一个 requires 字段,声明依赖哪些其他技能和版本范围。加载技能库的时候,我会做一次拓扑排序,把技能按依赖顺序注册好,避免出现 A 依赖 B、B 又依赖 A 这种环。

有一次我偷懒没做这个检查,结果两个技能互相调用,执行的时候直接栈溢出,日志刷了一整屏。后来我加了一个独立的依赖检查器,每次仓库更新后先跑一遍"循环依赖检测 + 缺失依赖检测",通过之后才允许注册到线上 Agent。这个检查器本身也被我做成了一个 agent-skills 技能,叫 repo_validate_registry,效果还挺好,算是用技能来管理技能。

层级上,我严格控制技能内部调用深度不超过三层。超出三层,链路就很难观测和排错了。如果一个任务需要四层组合,我会倾向把它拆成一个独立编排技能,把中间过程封起来,对外只暴露一个简洁签名。这样表面上看技能数量变多了,实际上每个技能的可维护性都上去了。

3.3 技能描述和提示词分离的另一个深层原因

把技能从提示词里剥出来的过程,本质上是在给"能力"建立边界。提示词是给模型指方向的,技能是给模型干活的。两者混在一起,模型既要在方向层面理解用户意图,又要在执行层面记住操作细节,任何一个环节模糊都会互相污染。我把所有操作细节全部下沉到技能文件之后,System Prompt 只剩三类内容:全局安全约束、通用对话策略、以及技能清单的检索入口说明。整个提示词体积缩减到原来的四分之一,模型在复杂任务上的稳定性有了肉眼可见的提升。

我还做了一个尝试:给每个技能配上"现场演示"片段,就是一段很短的前人调用记录,包含典型输入和典型输出。实测下来,模型在匹配阶段更愿意选有演示的技能,因为演示样例降低了不确定性,尤其是当两个技能描述相似时,演示几乎成了决胜因素。当然,这个做法会增加技能文件体积,我只在容易混淆的领域加,不会无脑全加。

4. Agent 主循环里,技能是怎么被选中和调度的

4.1 注册、发现、选择、执行四个阶段

技能体系不能独立于 Agent 主循环存在,它必须和 Agent 的执行流程深度耦合。我的 Agent 主循环拆成四段:注册阶段、发现阶段、选择阶段、执行阶段。注册阶段在启动时完成,遍历技能库目录,解析技能清单,做依赖和合法性检查,然后构建两个索引:一个按 scene 索引,一个按 domain 索引。发现阶段根据当前子目标和上下文,先做候选筛选,把技能范围收窄到大概 3~5 个。选择阶段才轮到模型出场:把候选技能的描述、演示片段、当前子目标一起交给模型,让它输出选中的技能名和参数。执行阶段落到运行时,跑技能逻辑,收结果,做输出校验和日志记录。

这个流程的核心原则是:能不用模型判断的,就不用模型判断。筛选用规则和索引,选择才用模型。很多 Agent 项目把所有决策全交给模型,导致不可控、成本高、延迟大。技能化改造后,大量低价值决策被前置规则消化掉了,模型只在最有必要的时候做关键选择。整体响应里,选择阶段消耗的 token 占比从 80% 多降到了 30% 左右。

4.2 候选筛选规则和冲突处理

候选筛选规则要敢于写得机械化。比如场景严格匹配时,直接把不含该 scene 的技能筛掉;参数数量不匹配时,如果技能要求必填参数四个,而当前上下文只能提供两个且无默认值,那这个技能直接不进入候选。再比如风险等级高的技能,如果没有在当前会话里明确出现过用户授权指令,就不出现在候选列表里。这些规则做下来,模型面对的选择空间和质量都高多了。

冲突处理是我后来才加的一段逻辑。当两个技能在功能上有重叠,且模型第一次没有明确选择时,我不允许模型反复横跳,而是用一个 settle 机制:如果前一次已经选了技能 A,且技能 A 只是执行失败但错误码是 500(业务执行异常),并且技能 B 和它描述相似,那么自动触发一次基于 scene 和 output_schema 的相似度比较,取输出契约更贴合当前目标的那个。这个机制减少了 20% 左右的无效重试。

4.3 上下文到底传什么进去

执行技能时,上下文传递经常被忽略。我的原则是:技能执行体只接收签名里声明的参数,不接收完整对话历史。完整对话历史又长又杂,一旦技能逻辑里隐式依赖历史,执行结果就会变得飘忽。我把上下文分成两层:显式参数和场景元数据。显式参数由模型在选择阶段填充;场景元数据由主循环自动注入,包括当前用户功能模块、超时上限、trace_id、权限令牌,这些不入技能签名,但通过执行上下文接口传给执行体,执行体可以按需读取。

这么做还有个好处:技能可以很方便地做权限校验。权限令牌进入执行上下文后,技能内部先验证自己的 permission 声明和令牌里的权限集合是否匹配。比如一个技能声明需要数据表读取权限,而当前令牌只有数据表写入权限,那就直接抛权限错误,不碰任何数据。这套机制在多人共用同一个 Agent 系统的场景里尤其重要,避免越权操作发生。

5. 放技能进生产环境前,先想清楚测试、授权与观测

5.1 技能测试不能只在流程里"连调"

技能多了以后,靠 Agent 全流程联调来验证技能是不现实的,出错时你根本分不清是模型选错了,还是技能逻辑本身错了。我把技能测试拆成三级:单元级、契约级、场景级。单元级是直接跑技能执行体,传入准备好的 mock 参数,验证输出是否符合 output_schema;契约级是模拟 Agent 主循环的调用,检查技能在接收到模型填充的参数时,参数校验是否严格、异常返回是否符合错误码规范;场景级则是把技能放进完整的 Agent 链路里,用固定的回放数据跑同一批任务五次,观察结果稳定性。

三级测试里,最容易被忽视的是场景级测试。原因是有些技能,尤其是指令依赖较强的技能,单独测都是好的,放进链路里就被模型的随机性影响,表现忽高忽低。我后来给这类技能加了"执行前校验模块":如果输入参数置信度偏低,技能会先发一次确认请求,而不是硬着头皮往下执行。这个设计虽然多了一次交互,但明显减少了后续的错误修补成本。

5.2 授权模型:每个技能都要有访问边界

授权不能放在技能执行逻辑里临时决定,必须在技能清单里提前声明。我给技能设了三个权限维度:数据域、操作域、时间域。数据域标注技能可以访问哪些数据范围,比如"只能读公开指标,不能读用户明细";操作域标注它能执行哪些操作,比如"只允许读,不允许改";时间域标注它在什么时间窗口内有效,比如某些统计类技能只能查 T-1 以前的数据。技能执行体的入口统一做一次权限断言,不满足就抛 P403。

有一阵子我发现某个排查类技能偶尔返回了本不该它看到的内容,后来查清楚是因为技能内部调用了一个公共数据读取模块,而那个模块默认返回全字段。我给技能加了"字段级裁剪"配置,在技能清单里声明允许字段列表,执行体拿到的数据先经过裁剪再给模型推理。这个问题解决完之后,我又把所有涉及明细数据的技能都过了一遍同样的裁剪逻辑。经验就是:授权机制一定要越靠前越好,别等数据都发出去了再想边界。

5.3 观测体系:每次技能调用都要能回溯

观测这块我踩过不少坑,最大的教训是:技能执行日志如果只有成功失败两个状态,那排查问题时几乎没有任何帮助。我现在把一次技能调用拆成五个观测点:命中候选、模型选定、参数校验、执行耗时、返回结果。每个观测点都记录 trace_id,写入统一日志中心。排查问题的时候,我只要拿着 trace_id,就能还原出这个技能是从哪条链路被调起来的、候选池里有谁、最终为什么是它被选中、参数有没有被裁剪过、执行耗了多少时间。

耗时监控特别有参考价值。我给自己定了个阈值:简单技能超过 3 秒就要告警,复杂编排技能超过 10 秒就告警。设置完阈值之后,确实抓到了几个异常:比如某个文本摘要技能一直在等一个上游接口响应,而那个上游接口偶尔会挂起 15 秒才超时,导致整条链路卡顿。后来我在技能清单里给它配了独立超时和熔断参数,问题立刻缓解。每个技能的执行超时建议单独设置,别用一个全局超时一刀切,否则要么卡死链路,要么误杀长任务。

6. 踩坑记录:技能化改造中最容易翻车的五个细节

6.1 描述写得太抽象,模型反复选错

我第一次给技能写描述时,几乎都有同一个毛病:过度抽象。比如把一个"提取数据表主键字段变更"的技能写成"分析表结构变更信息",模型根本分不清它是分析变更还是生成 DDL。然后模型就经常在它和"生成建表语句"两个技能之间纠结,选错了还要再绕一圈。后来我强制要求在描述里写具体对象和交付物:这个技能到底接收什么、吐出什么、典型输入长什么样。我会在内部约定俗成地给自己每一份技能描述写句式模板:"当用户需要X时使用,输入是Y,输出是Z,它不会处理W"。这个句式虽然不是多高深的技术,但真的让模型选得更准。

6.2 把所有逻辑塞在一个技能里,一条路走到黑

有段时间我把一个周报生成链路做成一个"巨无霸技能":内部包含数据查询、文本摘要、模板渲染、格式校验,十几个步骤全塞在一段执行脚本里。它确实能跑通,但每次想调整某一环节,都得动整个脚本,而且运行时一旦中间一步出错,整个技能直接失败,错误定位也很费劲。后来我把它拆成一个编排技能和三个原子技能:一个查数、一个摘要、一个渲染。编排技能只负责串联调用和处理异常,三个原子技能各自独立测试、独立发布。这个重构带来的收益是维护成本大幅下降,改动某个环节时完全不影响其他环节。

6.3 参数用中文命名,模型填参时经常张冠李戴

一开始图省事,给技能参数直接写了中文名,比如"数据内容"“生成方式”。结果发现模型填参的时候,经常把多个中文参数的值搞混,可能是底层分词和语义理解的原因,中文参数名的匹配没有英文标识符稳定。我把所有参数的 key 改成英文 camelCase,参数描述用中文。比如sourceContent配上一句"需要处理的原始正文文本,支持最多 8000 字"。这一改,参数错填率下降得非常明显。我的看法是:底层模型的工具调用更习惯英文标识符,别在这个地方搞个性。

6.4 技能命中率低,先查候选池而不是先查提示词

有一次发现某个新上线的技能总是不被模型选中,我一开始以为用户意图分解有问题,调了半天还是没效果。后来我把选择阶段的日志拉出来,发现这个技能压根没出现在候选池里,因为它挂的 scene 标签和用户触发链路里的 scene 值不一致。问题根本不在模型,而在注册时的领域映射配置。这个排查经验后来被我固化成了"技能命中率分析模板":先看是否进候选、再看排名、最后才看模型选择,分层定位。以后每次技能命中率异常,我都先走这条链路,节省大量精力。

6.5 依赖关系不检查,循环调用导致执行栈溢出

前面提到过,我踩过技能循环依赖的坑。这里再补充一个细节:依赖检查不只是启动时做,还要在技能热更新后做。我有一次只改了其中一个技能文件的 requires 字段,热更新就直接上线了,结果两个技能瞬间形成循环调用,线上任务连续失败好几分钟。后来我在热更新流程里强制插入了一个依赖校验前置步骤,校验不通过就不允许发布到生产。这个步骤我原本觉得多此一举,现在成了我所有技能仓库的标配动作。安全起见,我还会在编译阶段提取所有技能依赖关系生成一张静态依赖图,每次发布前对比差异,任何结构变化都要过审。

最后分享两件小事

技能化改造做了大半年,我最大的感受是:不要想着一开始就把技能库设计得又大又全,先把手头最频繁、最重复的操作变成第一批技能,跑顺了再逐步扩展。一个新技能从诞生到稳定需要至少两周的观察期,这期间我会把它标记为 beta 版本,限制调用频率,等日志里错误率降到阈值以下才转正。

另外一个值得养成的习惯是,每次给技能写描述前,先模拟一下"完全不知道这个技能存在"的模型视角,看自己能不能仅凭描述判断出该不该用它。这个模拟成本很低,但非常能暴露描述模糊的问题。我在 agent-skills 项目里已经把这个习惯固化成了一条检查项,写进技能提交清单,后来凡是过审的技能,命中率都明显比早期随手写的高出一截。这套体系不算复杂,但每一步都是实践换来的经验,希望对你也有用。

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

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

立即咨询