ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent技能库设计实战:从提示词堆砌到可插拔技能组装

Agent技能库设计实战:从提示词堆砌到可插拔技能组装

过去半年我一直泡在 Agent 开发的一线,交了不少学费。2025 年如果还在纠结“怎么让大模型记住多轮对话”,那基本还停留在上一代玩具阶段;真正的分水岭,是“Agent 到底能不能稳定地执行一整套具体任务”。我自己的体会是,模型本身的能力差距正在快速缩小,决定一个 Agent 项目能不能从 Demo 走向生产的,往往是那一层最不起眼的“技能层”。这也是我今天想认真聊聊 agent-skills 的原因——它不是某个现成框架的名字,而是当前设计智能体时最该优先考虑的技术组织方式。

先说清楚它解决什么问题:假设你让一个 AI 助手帮你处理“整理项目周报”这件事,模型如果只是嘴上答应,然后直接吐一段泛泛而谈的文字,那不叫 Agent。真正的 Agent 应该是——能自己去读取 Git 提交记录、能调用内部文档搜索接口、能按固定模板生成 Markdown、最后还能把文件提交到指定目录。这一连串动作背后,每一类可复用的原子能力,就是一个“技能”。agent-skills 要做的,就是把这些原子能力体系化、标准化、可插拔,让 Agent 真正从“会聊天”进化到“会干活”。

这篇文章适合正在做 AI 应用落地、搞过 Agent 原型但卡在稳定性和复用性上的人,也适合那些刚准备入坑、想知道除了 Chat 以外还能做什么的开发者。我会从设计思路、核心细节、实操步骤、常见坑四个层面展开,尽量把底层的“为什么”也讲透。

1. 内容整体设计与思路拆解

1.1 从“提示词堆砌”到“技能组装”的转变

先说一个我观察到的普遍误区。很多人设计 Agent 时,第一反应是把所有指令写进 System Prompt,恨不得几千字全塞进去,让模型“自己看着办”。这种方式在只有两三个任务的 Demo 阶段勉强能跑,一旦任务数量上到两位数,提示词之间的互相干扰就会立刻暴露出来。今天这个场景下要模型表现 A 能力,明天换一个场景又要它启动 B 能力,模型在庞杂的上下文里经常“精神分裂”,行为变得不可控。

agent-skills 的思路刚好反过来:它不指望模型靠读提示词自我理解整套业务,而是把每个能力封装成独立的技能单元,由外部调度层决定什么场景触发什么技能。模型不再被要求“全能”,它只需要负责决策和编排——也就是在合适的时机选择合适的技能来执行。这样做的好处有三个:

  • 职责单一:每个技能只做一件事,内部逻辑可以高度优化。
  • 可插拔:新业务场景来了,加一个技能即可,不用推翻整个 Prompt。
  • 可测试:清晰的输入输出契约,让每个技能都能单独验证质量。

我自己感觉,这有点像把“写一大段即兴发挥的自由体操”改成了“设计一套由分解动作组合而成的规定动作”,稳定性大幅提升,表现力也没有明显损失。

1.2 筛选核心应用场景的四个标准

技术设计做得再漂亮,场景选错一样白搭。结合我自己的踩坑经历,判断一个场景适不适合用 agent-skills 这套体系来做,主要看四条标准。

第一条是操作仪式感要强。所谓“仪式感”,就是这个任务必须有稳定、可识别的步骤边界,不能是开放式的“随便聊聊”。比如“把销售日报从邮件里找出来,提取关键数据,生成可视化图表,发送到钉钉群”,这是清晰得多阶段任务;而“和用户谈谈心”这种,就不适合拆技能。

第二条是输入输出能结构化。技能和技能之间要靠数据说话,如果彼此传递的信息都是模糊的自然语言,那下游技能根本无从处理。凡是能用 JSON、表格、数据库记录来承接输入输出的场景,优先做;凡是全靠“理解上下文”的场景,暂缓。

第三条是具备高频复用性。一套技能体系是存在建设成本的,如果某个组合流程一年都用不了几次,那还不如每次单独写脚本。优先挑那些每周甚至每天都在重复的操作来沉淀成技能,比如“从 OCR 结果中提取身份证关键字段”“把 Markdown 批量转成 PDF”这类。

第四条,也是我后来才悟出来的——容错率要可控。技能是让 Agent 自主执行的,执行过程必然有失败概率。如果任务失败一次就会造成很大损失(比如直接对外发合同、扣款),那就需要在技能里加一堆防护逻辑,设计成本飙升;反之,像“整理周报”“生成卡片图”这类失败了重跑一遍就行的场景,就是最佳起步区。

1.3 方案比对:为什么用技能库,而不是全链路训练

可能有人会问:“既然场景这么明确,为什么不用传统软件工程直接写死流程,非要扯上 Agent?”这个问题我在实际项目里被问过很多次。答案在于:不少任务的中间步骤存在大量不确定性,传统代码遇到变化就要改逻辑,而 Agent 可以在技能边界内灵活选择组合顺序。

举个例子,给你一堆杂乱无章的 PDF 和图片合同,要求抽取关键条款并录入系统。传统脚本需要预先定义表格结构、写明关键词匹配规则,遇到新排版就得改代码。而 agent-skills 的思路是定义“解析文档”“提取实体”“格式校验”三个独立技能,由大模型负责根据文档实际情况调度组合。底层模型不擅长执行(因为执行依赖外部工具),但它擅长决策——在 A 技能失败时自动换 B 技能重试。这种编排能力是传统硬编码流程不具备的。

当然,这种灵活性也有代价:推理成本更高、运行时间更长、偶发不确定性变大。但从维护角度看,当业务流程变化时,你只需要新增或修改单个技能,而不是推倒整个管道,这个收益非常值得。

2. 核心细节解析与实操要点

2.1 技能的结构化定义:一份能落地的清单

正式动手之前,先得明确一个技能长什么样。我经历了三个版本迭代后才形成现在这套规范,核心思路是把“描述怎么做”和“实际怎么做”彻底分开。一个标准技能单元包含以下部分:

  • 名称:全局唯一的技能标识,比如extract_invoice_amount。
  • 描述:给调度模型看的一段话,说明该技能的职责边界和典型使用场景。
  • 输入 Schema:结构化定义调用该技能需要哪些参数,包括类型、是否必填、取值范围。
  • 输出 Schema:定义返回值结构,方便下游技能或调度器消费。
  • 执行逻辑:可以是一段 Python 代码、一个 API 调用脚本,也可以是一套提示词模板驱动的子流程。
  • 错误处理策略:定义执行失败时如何重试、回退或上报。

这里有一个容易被忽略的关键点:描述字段的重要性不亚于执行逻辑本身。因为大模型每次做技能选择时,读的就是这个描述。描述写得太抽象,模型不知道什么时候该调用;写得太啰嗦,关键信息被淹没,模型容易误选。我通常把描述控制在两三句话内,明确“什么时候用”“不要用来做什么”。

2.2 技能粒度控制:拆多碎才算合理

技能粒度是整个体系里最考验经验、也最影响体验的环节。拆得太粗,一个“技能”内部其实还是一个大杂烩,模型无法精细控制;拆得太细,技能数量爆炸,调度模型光做选择就要烧掉大量 Token,而且容易出现“能力交叉”导致的选择冲突。

我目前比较推荐的粒度标准是:一个技能应该对应一个完整的、可独立验证的子任务。比如“从网页抽取全部商品标题”,这就是一个合理粒度;而“采集整站商品数据”就不适合做成单个技能,因为这里面包含了翻页、解析、去重、存储,每个环节都值得独立定义和单独测试。反过来,“提取一段文本里的所有邮箱地址”这种则太细了,和“提取联系方式”合并成一个技能更合适。

实际操作中可以用一个简单标准自我检查:如果这个技能需要传入三个以上的业务对象才能描述清楚,说明它还应该再拆;如果两个技能内部代码有 80% 以上重合,说明该合并了。

2.3 一套“技能注册中心”的元数据模型

当技能数量达到十几个以后,靠硬编码 if-else 去分发已经不够了。你需要一个轻量的“注册中心”,本质上是一张元数据表,让调度层可以遍历、筛选、排序所有可用技能。我自己用得非常顺手的元数据字段包括:

字段类型说明
skill_namestring唯一标识,用下划线分隔的小写字母组合
descriptionstring给模型看的职责描述
input_schemadict基于 JSON Schema 的输入定义
output_schemadict输出数据结构定义
required_permissionslist需要的外部权限,比如文件读写、网络调用
timeout_secondsint超时上限,防止技能卡死
enabledbool是否对当前场景可见,便于灰度控制

这个注册表只需要支持最基本的增删改查即可,没必要引入复杂的服务框架。哪怕用一段 Python 字典加上一个 JSON 文件来维护都完全够用。真正重要的是让调度的 Prompt 明确知道:“你只能从以上列表中选择技能,且必须严格按照 Schema 传参。”

2.4 脚本接口与技能元数据的解耦策略

关于技能文件内部的组织方式,我踩过一个很深的坑。早期我把技能描述直接写在 Python 函数定义的 docstring 里,看起来方便,但在执行环境与调度环境隔离的时候(比如技能跑在沙箱容器里),调度器根本读不到文档内容,导致技能无法被发现。

现在的做法是:元数据文件与脚本实现文件彻底分离。每个技能目录下包含一个SKILL.md用于描述能力与调用方法,再配一个script.py用于实现执行逻辑,最后统一由一个入口脚本按目录扫描加载所有SKILL.md生成本节上面那个注册表。这样既方便开发时维护,也给运行时调度提供了干净的数据来源。这种组织方式我沿用了很久,硬是没再出过“技能失踪”的幺蛾子。

3. 实操过程与核心环节实现

3.1 从零搭建一个最小技能库的目录结构

先别急着写代码,动手前把目录结构搭好,后面会省非常多的事情。我推荐的最小可行结构如下:

agent-skills/ |-- registry/ | |-- skill_manifest.json |-- skills/ | |-- fetch_news/ | | |-- SKILL.md | | |-- script.py | |-- summarize_text/ | | |-- SKILL.md | | |-- script.py |-- runtime/ | |-- dispatcher.py | |-- loader.py |-- logs/ |-- main.py

skills目录下每一个子目录就是一个独立技能,各自包含一个声明能力边界与参数要求的SKILL.md,以及一个真正干活的script.py。runtime目录里的loader.py负责遍历skills目录并生成可用于调度的注册表,dispatcher.py则负责在当前会话内协调执行。简单项目里不需要引任何重量级框架,Python 标准库加一个requests就足够。

3.2 加载与注册全技能的过程带注释

接下来是核心的加载逻辑。我会写一个loader.py,它负责扫描所有技能目录、解析SKILL.md中的元信息、组装成统一字典。整个流程大概几十行,但把这几十行写扎实,后面扩展才会舒服。参考示意如下:

import json import os from pathlib import Path def load_skill_registry(skills_dir: str = "skills") -> dict: registry = {} for skill_path in Path(skills_dir).iterdir(): if not skill_path.is_dir(): continue manifest_file = skill_path / "SKILL.md" if not manifest_file.exists(): continue # 用简单约定解析元数据块,实际项目中可换成 yaml 或 toml with open(manifest_file, "r", encoding="utf-8") as f: lines = f.readlines() skill_meta = {"name": skill_path.name, "description": "", "input_schema": {}, "output_schema": {}} # 这里只做极简解析演示,生产环境建议定义专门的解析类 ... registry[skill_meta["name"]] = skill_meta return registry

注意,这段代码的核心目的是展示思路,而不是直接照抄上线。真正的生产级实现里你应该用PyYAML或tomli来解析SKILL.md的元信息头,而不是自己手写行级解析器——后者看起来省事,但一旦字段增多,各种边界情况就会让你怀疑人生。

3.3 让大模型学会“查目录挑技能”的调度 Prompt 模板

技能库建好了,还要让大模型学会用它。这一步是最需要反复调 Prompt 的环节。我会在 System Prompt 里做三件事:先说明角色边界,再贴出注册表内容,最后定义输出格式要求。核心模板参考如下:

你是一个任务编排助手。你的职责不是直接回答业务问题,而是根据以下可用的技能列表,为用户请求设计执行计划。 可用技能如下(JSON 格式展示): {registry_json} 要求: 1. 严格从技能列表中选择,禁止虚构不存在的技能。 2. 输出必须是合法的 JSON,包含 steps 数组。 3. 每个 step 必须包含 skill_name 和 arguments 两个字段。 4. 如果需要多个技能完成一个目标,请拆分步骤并按依赖关系排序。

实际跑下来的经验是:给模型贴多少个技能、按什么顺序展示,直接影响选择准确率。把高频技能排在列表前面、低频技能后面,模型误选率会显著下降。另外,每个技能的描述务必不要出现“如果……则……”这种条件句式,模型(尤其是当前主流模型)在这种开放式描述下特别容易乱选。

3.4 一个真实技能文件长什么样

为了让上面的流程更好理解,我贴一个精简但完整的示例:一个“抓取今日热榜”的技能定义。它的调用逻辑很简单,但足以展示元数据与实现代码如何配合。

--- name: fetch_hotlist description: 抓取指定平台今日热门内容列表,返回标题与访问链接。适用于舆情分析、内容运营、竞品监控场景。 input_schema: platform: string # 平台标识,如 news、tech、video top_n: integer # 返回条数,默认 10 output_schema: items: array # 元素包含 title、url、score timeout_seconds: 15 ---

配套的script.py大概就是搭一个接口请求并解析返回。这里不谈具体平台,只强调一个重要原则:技能内部绝对不能硬编码任何业务密钥或私有配置,所有环境变量统一从外部注入。这样同一个技能在不同项目、不同用户之间才能真正复用,而不是换一个部署环境就要改一轮代码。

3.5 技能执行与外部工具交互时的权限控制

最后一个实操要点是权限控制。Agent 技能跑在真实环境里,免不了要读写文件、请求外部服务。千万不要在技能内部写“万能函数”——比如让一个解析 PDF 的技能同时拥有写数据库的权限。我的做法是每个技能声明自己需要的权限类型,由调度层做统一审批和运行时注入。

例如,fetch_news技能的权限范围是“仅允许访问指定域名”,那么调度层就给它一个带限制的 HTTP 会话;write_report技能的权限范围是“仅允许写入reports/summary/目录”,调度层就给它一个锁定的文件句柄。这种最小权限原则可以防止单个技能被 prompt injection 之后突破边界,保住整个系统不被攻陷。在我的生产实践里,凡是跳过这一步的版本,最终都被各种异常调用折磨得不得不回来补课。

4. 常见问题与排查技巧实录

4.1 技能调不起来、方法报错

这是最基础的问题,但也最容易藏着掖着。技能调不起来,十有八九是技能元数据里的名称与函数实际名称对不上。前期为了图方便,我喜欢直接在注册表里手写函数名,结果重构时函数改了名,注册表没同步,模型按旧名字调用,自然全部报错。后来我改成从SKILL.md自动加载名称后,这类问题几乎绝迹。

另外一类原因是 Python 环境里装了两个版本的同名依赖包。某一次技能突然全部 500,排查半天发现是全局环境里某个库被另一项目升级到了新版本,接口签名变了,而我隔离环境里依然是旧版本,两边行为不一致。现在我的所有技能都跑在独立虚拟环境里,部署时锁定requirements.txt版本。

4.2 模型总是选错技能

这大概是 Agent 落地过程中最让人挠头的一类问题。我的排查顺序通常是:先看技能描述是否准确;再看是否有语义重叠的技能;最后看是不是输入输出 Schema 定义模糊导致匹配困难。比如有一次“总结文本”和“提取关键词”两个技能在描述上都出现了“分析内容”字样,模型有 30% 的概率会选错。我把描述重新改为“生成一段压缩后的自然语言摘要”和“返回一个包含关键概念词组的列表”,误选率立刻降了下来。

有时候问题出在模型本身的可控性上。如果你用的是小参数模型,连 JSON 输出都经常不按格式来,那再优化描述也没用。建议这种场景下要么换用更强的模型做调度,要么就在 Prompt 里加 few-shot 示例,把“遇到什么请求该选哪个技能”的典型 case 直接贴给模型看。

4.3 技能链路长、排查难定位

多技能串联执行时,任何一个环节出了 bug,整条链路都会表现为“最终结果不对”。早期我在日志系统里只打了“开始执行”“执行结束”两行,一旦中间出问题,根本无从定位。

现在的做法是“链路追踪 + 分段日志”。每次执行一个技能,就分配一个唯一的 request_id,同时记录技能的输入参数摘要、输出结果摘要、耗时、错误信息。排查时直接按 request_id 过滤日志,一眼就能看出是哪个技能卡住或返回了异常值。这个方法对单体脚本足够,再往上走才需要引入专业的链路追踪系统。

4.4 错误重试策略:何时重试、何时放弃

Agent 执行网络请求时不时失败,这非常正常。但“失败后怎么办”就有讲究了。如果技能内部无脑重试,弹药(配额)消耗极快;如果一失败就放弃,很多性能抖动类的偶发问题会让整个任务的体验变差。

我的重试策略总结成一张表:

失败类型判定方式处理策略
网络超时异常类型为 timeout间隔 2s、5s、10s 重试三次
接口限流返回 429 状态码等待 60s 后单次重试
数据校验失败返回值不符合 Schema立即停止,上报调度层
依赖服务不可用返回 5xx 状态码直接放弃,标记为“执行失败”

这张表看起来简单,但实战价值极高。以前我把所有异常都扔到同一个“重试三次”的处理逻辑里,结果遇到真不该重试的死循环,白白浪费大量时间和接口配额。分清“可恢复错误”和“不可恢复错误”,是 Agent 技能工程模块成熟度的一道分水岭。

4.5 案例复盘:一次多语言混合输入导致的链路崩溃

这个案例我觉得特别有代表性。当时一个技能负责把用户输入的英文合同条款翻译成中文并提取关键日期,另一个技能负责把提取出的日期写入企业内部系统。有一天线上突然收到大量报错,任务全部卡在第二步。

排查后定位到:用户在原文里混用了中英文日期格式,比如“March 5”和“3 月 5 日”同时出现。提取日期这个技能本身的规则只考虑了标准格式,遇到混写就直接返回空列表,导致下游写入技能拿到空值后报错。最终解法是在技能内部加了一个“格式归一化”前置步骤:不管输入什么格式,先统一转成 ISO 8601 标准字符串,再往下游传递。这个补丁不复杂,但只有经过完整链路梳理,才能发现数据格式在技能间的“隐式约定”有多么脆弱。

5. 从技能工程到 Agent 基础设施的演进建议

5.1 技能与模型共同演进时的兼容策略

做 agent-skills 的时候,心里要明白一个事实:底层的模型一代比一代强,技能的设计也必须跟着调整。早期我用小模型时,技能描述必须写得很“死板”,一步一步详细列操作,模型才能选对;但后来换了新模型,发现它完全能理解更抽象的描述,反而有了更强的泛化能力。

这种演进带来的挑战是:你不能把技能质量绑定在某个特定模型的强弱上。我的做法是给技能库打“兼容级别”标签,标注该技能“面向弱模型设计”“面向强模型设计”或“全部兼容”。每次切换模型版本后,跑一遍基础回归测试,看看哪些技能出现了选错、用错参数等问题,再针对性优化。

5.2 技能测试的自动化与可观测性建设

技能库越建越大,手工验证肯定不行。我现在保持着“技能即代码”的工程习惯:每个技能都配套至少一个最小调用用例,跑通后记录结果基线。任何一次对技能元数据或实现逻辑的修改,都触发一轮回归。

自动化之外,可观测性更是 Agent 后期迭代的生命线。技能的开始时间、结束时间、Token 消耗、使用频率、失败率这些指标,我都会落到本地存储里。每周看一眼,就能发现哪些技能实际调用频率极低,可以考虑关闭;哪些技能失败率异常升高,需要立即排查。没有数据支撑的技能库建设,本质上是在黑暗中自嗨。

5.3 组织内部怎么沉淀和共享技能库

当团队里多个项目都在做 Agent 时,技能库不应该各自为战,否则就是重复造轮子。内部可以搞一个简单的技能共享中心,每个技能提交时需要附带说明文档、测试用例、调用示例和安全边界说明。团队其他人调用时先审查字段释义,确认齐全后直接注册到本地。

但这里必须提醒一点:不同业务线对“技能”的语义理解往往有微妙差异。比如“生成报告”在财务线和运营线,输出的字段、格式可能完全不同。直接硬混在一起不但不会提升复用效率,反而会让调度模型无所适从。合理的做法是按业务域建命名空间,比如skills.finance.report_generator和skills.ops.report_generator共存,各自独立演进、独立维护。

6. 收尾:几条从实战中磨出来的经验

最后把话收回到个人体会。agent-skills 这条路走下来,我最大的感受是:技术方案本身并不复杂,真正的难点在于克制——克制给模型太多自由、克制把技能拆得过度细碎、克制不加边界地给技能赋权。

如果你准备开始在自己项目里实践这套思路,我有三个比较务实的建议。第一,先挑一个你每周至少做三次、且流程边界清晰的任务来试点,不要一上来就试图把整个业务全部技能化。第二,让模型以“最小可行技能集”起步,从三五个技能开始跑,跑顺了再增加,不要追求一步到位,技能太多反而会干扰调度。第三,技能巡检要常态化,哪怕版本稳定了,也要每周抽一点时间看日志,确认哪些技能被高频调用、哪些长期闲置,再做动态调整。

另外还有一点经验分享:日志里如果出现“技能返回内容不符合预期”,不要惯性怀疑是模型能力问题。大多数情况下,问题出在输入数据和你定义的 Schema 不匹配。把数据清理做好、把字段边界定义清楚,收益远大于升级模型。与其纠结模型聪明不聪明,不如踏实把技能基础设施打磨得稳一些。项目要扛住线上真实流量,靠的绝不是单点模型的灵光一现,而是这套技能体系每一步都走得可靠、可查、可修。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进