☰
Agent技能抽象设计模式:从工具调用到可编排技能系统
2026/10/28 13:46:02 网站建设 项目流程

1. 从工具调用到技能抽象:agent-skills 解决的真实痛点

做 AI Agent 开发做到一定程度,很多人都会撞上一堵墙:功能好像都有,但 Agent 就是"不会干活"。功能都有,但 Agent 就是"不会干活"。你在代码里定义了几十个 function,把它们的 name、description、parameters 一股脑塞给模型,模型也确实能按格式调用。可真当一个任务需要连续操作文件、查询数据库、再调用外部 API 时,各种问题就全冒出来了。工具描述写得越细,提示词越长,模型的选择就越不稳定;工具之间隐式的依赖关系全靠人工在 system prompt 里维护,改一个字段就要全局排查。我去年维护的一个 Agent 项目就是这样,工具函数从 12 个涨到 40 个之后,单次任务的平均 API 调用量翻了将近三倍,成功率反而降了约两成。

"agent-skills" 这个概念正是冲着这个问题来的。它的核心思路是:不要继续在"函数调用"的粒度上组织 Agent 的能力,而是把能力提升到"技能"的粒度。一个技能不是单纯的"执行某个函数",而是"完成一件有明确目标的事情"所需要的完整行为模板——包括该做什么、不该做什么、用什么工具、按什么顺序、出错了怎么处理。说得直白点,工具调用是让 Agent 学会"伸手",技能系统是让 Agent 学会"做动作"。这两者的差异,决定了 Agent 只能处理单步请求,还是能自主完成一个多步骤的交付任务。

这篇文章基于我在几个实际项目里落地 agent-skills 设计模式的经验整理而成,适合正在纠结"为什么我的 Agent 有工具却很蠢"的开发者,也适合想从零搭一套可插拔 Agent 能力的架构师。我会先讲清楚技能抽象背后到底改变了什么,再给出技能文件的定义规范、注册加载机制、编排执行链路,最后分享我在真实场景里踩过的三个比较深的坑,以及一套完整的最小可行落地方案。

先说清楚一个容易混淆的概念:技能(skill)和工具(tool)到底有什么区别。工具是最小可执行单元,通常对应一个函数或 API,输入输出都是结构化参数,本身不包含"决策逻辑"。技能则是带决策逻辑的工作流模板,它可能封装了多个工具的调用顺序、前置条件、备选路径和终止条件。举个具体的例子,单个"查天气"API 是工具,而"根据城市名规划周末出行方案"这个能力就是技能——它内部会先查天气、再查交通、再结合时间段做推荐,三个步骤之间有依赖关系,需要按序执行、异常回退。用函数调用去硬编码这套逻辑会非常痛苦,但用技能声明来描述就自然得多。

另一个背景是 2024 年后各大 Agent 框架加速推动的"Skills"概念落地。这套模式逐渐形成几个共识性的组成部分:技能的声明式描述文件(通常叫 SKILL.md 或类似格式)、技能的注册与发现机制、技能的选择策略,以及技能执行时的失败恢复策略。下面几个章节,我会逐一拆开讲,并附上可以直接拿来改的代码和配置。

2. 技能的核心载体:SKILL.md 声明式描述与注册机制

2.1 为什么需要一份"技能说明书"

我在最开始的设计里走过一段弯路:把技能直接写成 Python 类,每个技能一个类,类的 docstring 里写描述,方法里写调用逻辑。这样做的第一个问题是,Agent 的大模型只能看到 model 层面的文本描述,它不认识你的 Python 类结构,所以你必须把所有关键信息转成文本喂给模型——那就等于又回到堆提示词的老路了。第二个问题是,技能之间相互引用的元信息(比如"这个技能依赖上一个技能的输出格式")散落在代码里,根本无法做静态分析和动态编排。

现在更成熟的做法是把技能文件拆成两个部分:一份人类可读、模型可读的声明文件,以及一份可执行代码。声明文件通常采用 Markdown 格式,核心字段包括技能的 name、description、when_to_use、dependencies、main_workflow、fallback 等。我见过不少团队一开始觉得这个文件多余,直接只在代码层写注释。但实测下来,声明文件有两个不可替代的作用:第一,它是模型做"技能选择"时的唯一事实来源,模型不需要理解代码,只需要能读懂这份说明就能判断"当前任务该不该调用这个技能";第二,它可以独立做校验、版本管理和热加载,不需要重新部署 Agent 主进程。

这里直接给出一份我实际项目里用过的 SKILL.md 模板,字段不多,但每个字段在模型决策链路里都有明确用途:

--- name: weekly_report_generator description: 根据项目工作日志生成周报,支持按团队过滤、统计工时、输出 Markdown 或 Excel 格式。 when_to_use: 当用户要求生成周报、汇总近期工作内容、统计团队产出的场景。 dependencies: - skill: project_log_reader version: ">=1.2" - tool: excel_exporter version: "^2.0" main_workflow: - step: 读取指定时间范围内的项目日志 use: project_log_reader - step: 按团队成员分组并统计工时 use: internal_calculator - step: 按目标格式输出报告 use: [markdown_renderer, excel_exporter] fallback: - when: 项目日志接口超时 then: 尝试读取本地缓存目录中的最近一次日志备份 - when: 报告格式参数冲突 then: 默认输出 Markdown 格式并提示用户手动选择 ---

注意 description 和 when_to_use 这两个字段,它们直接决定了模型会不会选错技能。我见过太多团队把 description 写成了"该技能用于生成周报"这种泛泛描述,结果遇到"帮我总结一下这周干了啥"这种请求时,模型举棋不定,在多个技能之间反复试探。好的描述应该写"输入需要什么、输出是什么、处理的大致过程",而 when_to_use 要写清楚触发场景,甚至可以加反例,比如"不要在会议记录整理场景使用,那是 meeting_minutes_sumarizer 的职责"。

2.2 注册表与动态发现:如何让 Agent 知道自己会什么

有了技能文件,下一步是把它们注册到一个 Agent 能感知的地方。这里有两种主流路线:静态注册和动态发现。静态注册就是在启动时把所有技能文件路径写进配置;动态发现则是让 Agent 在运行时扫描指定目录,读取每个 SKILL.md 的元数据,构建一个技能索引列表。我强烈建议走动态发现,原因很实际:技能文件会越来越多,你不可能每次加一个技能都要改主配置然后重启服务,动态扫描目录的方式让新增技能的成本几乎为零。

我的做法是建一个 skills/ 目录,每个子目录放一个技能,结构大概是这样的:

skills/ weekly_report_generator/ SKILL.md main.py requirements.txt project_log_reader/ SKILL.md main.py excel_exporter/ SKILL.md exporter.py

启动时写一个简单的 SkillRegistry,扫描目录、解析 SKILL.md 的 frontmatter、构建技能清单。这个清单实际是一段增强后的文本,会被拼接进 Agent 的 system prompt 里。这里要特别注意控制清单的长度:技能数量超过 20 个时,全量拼接会让模型输入 token 快速膨胀,选择准确率反而下降。实测下来,更合理的做法是对技能描述做编码向量化,在每次请求到来时先做一次 embedding 相似度检索,只把 top-k(比如 5 到 8 个)候选技能描述注入上下文。这套"检索式技能召回"是后面选择准确率稳定在 90% 以上的关键。

下面是一段简化版的注册与召回代码,用的库是开源的,只有核心逻辑示意:

# skill_registry.py from pathlib import Path import yaml import re class SkillRegistry: def __init__(self, skills_root: Path): self.skills_root = skills_root self._skills = [] def scan(self): for skill_dir in self.skills_root.iterdir(): skill_md = skill_dir / "SKILL.md" if not skill_md.exists(): continue content = skill_md.read_text(encoding="utf-8") meta = self._parse_frontmatter(content) meta["path"] = str(skill_dir) self._skills.append(meta) return self def _parse_frontmatter(self, content: str): match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) if not match: raise ValueError("SKILL.md 缺少 frontmatter 元数据块") return yaml.safe_load(match.group(1)) def list_descriptions(self): return [f"技能名: {s['name']}\n描述: {s['description']}\n适用场景: {s['when_to_use']}" for s in self._skills]

召回侧我用的是 sentence-transformers 里的一个轻量模型做向量化,存储直接扔进内存列表就够用,不用上向量数据库,因为技能数量通常不会超过几百个。每次请求的流程是:先对用户输入做 embedding,然后和所有技能描述的 embedding 算余弦相似度,取最高的前几个作为候选,再把候选的完整描述注入 system prompt。这个过程的延迟大约在几十毫秒,相对模型调用来说完全可以忽略。

2.3 技能描述的质量控制清单

技能描述是整条链路上唯一同时影响"选择准确性"和"执行正确性"的文本,我把自己的检查清单列在这里,每一条都是从失败案例里反向总结出来的:

  • description 里必须写清楚输入要求和输出格式,例如"输入是项目 ID 列表,输出是 JSON 数组,每个元素包含项目名、负责人和本周工时总和"。
  • when_to_use 要写触发场景和反例,宁可啰嗦不要含混,我见过的最好的写法是列三个正例加一个反例。
  • dependencies 字段里引用的技能和工具要标注版本范围,否则技能升级后可能出现接口不兼容导致执行中报错。
  • main_workflow 里每一步都要标注 use 的目标技能或工具,这不仅是给模型看的执行计划,也是后面编排引擎做依赖解析的依据。
  • fallback 必须写"什么情况触发 + 替换方案是什么"的二元结构,缺失 fallback 的技能相当于没有保险丝,一出错整条任务就崩。

提示:如果你看到模型频繁调用一个技能但执行结果不对,十有八九是 description 里没写清楚输入要求,模型以为它会把完整数据传进去,实际上技能要的是结构化参数。先把 description 里的输入输出规范写死再调参数,不要急着改模型温度或换大模型。

3. 技能编排与执行链路:一个任务如何被拆成一组技能

3.1 技能选择的决策路径:模型自主选 vs 规则路由

技能系统跑起来之后,第一个要回答的问题是:某个任务来了,到底由谁决定调用哪个技能?我见过两种极端方案,一边是完全交给模型自由选择,一边是完全用 if-else 规则硬编码。两种我都试过,都有明显问题。

完全交给模型的问题是,模型对技能的"适用边界"判断并不稳定,尤其在技能描述相似度高时,模型经常挑错技能。举例来说,我定义了一个"深度内容总结"技能和一个"要点提取"技能,两者 description 都涉及"总结",结果模型遇到一批需要"逐条概括并保留逻辑关系"的文档时,选到了只输出 bullet list 的要点提取,产出质量完全不对。后来我加了 when_to_use 反例描述,情况好一些,但依然有差不多 8% 的选择错误率。在任务失败会带来明显成本的生产环境,这个概率还是偏高。

完全规则路由的方案则走向另一个极端。规则一旦写多,很快退化成"每个技能一个 if",技能之间的组合逻辑根本表达不出来。比如"先读日志再生成周报"这个流程,用规则你得专门写一个"周报任务"分支,里面再套子分支。如果将来加了"读日志生成日报"呢?"读日志写汇报邮件"呢?每多一种组合就要写一条规则,代码很快就膨胀到不可维护,这恰恰是当初用技能抽象想解决的问题。

现在验证下来比较稳的折中方案是"场景分类器 + 技能候选召回 + 模型最终选择"三层决策。第一层用一个轻量分类器(或者一个规则优先的粗筛)把请求归到几个大类,比如"报告生成类""数据查询类""内容处理类";第二层在各类型对应的技能池里做 embedding 召回 top-k;第三层才把候选技能描述交给大模型做最终选择。这样的好处是模型面对的不是几十个技能而只有几个,选择空间变小,准确率会明显提升。我在某个内部工具上做了对比实验,只用模型自由选择时准确率是 82%,加了两层预筛后提升到 94%,误调用的概率下降了接近 60%。

3.2 技能执行时的上下文管理:跑起来才知道的组合问题

选对技能只是第一步。技能真正执行起来,比选错更常见的问题是两个技能之间的数据传递。在纯函数调用时代,上一个函数的返回值直接作为下一个函数的输入,这个链路是显式写死的。但技能执行时,模型会根据实际情况调整执行顺序,甚至跳过某一步,这时候如果上下文里的数据结构对不上,整个链路就崩了。

给你看一个我踩过的真实案例。我的周报生成技能第一步依赖"项目日志读取"技能的输出,日志读取技能返回的是一组记录对象,字段是{"project": "支付中台", "hours": 6.5, "detail": "完成了退款链路压测"}。而周报技能内部写死了要读取字段log_items数组,里面每个元素只有title和time_consumed两个字段。两边对不上,执行到中间步骤就报 KeyError。这个问题的根源不是代码,而是两个技能由不同开发者编写,双方的"数据契约"没有对齐。

解决这个问题,我现在的做法是在 SKILL.md 里强制增加一个output_schema字段,并且在注册时做静态校验:凡是main_workflow里声明dependencies的技能,启动时必须校验上游技能的output_schema和本技能的输入字段能否匹配。校验逻辑不复杂,本质上就是 JSON Schema 的兼容性检查。这个约束条件带来的收益非常大,它把运行时才暴露的接口不匹配问题提前到了启动时,一次部署前就能发现五六个隐患。我建议任何技能数量超过三个的项目,都必须把这一步纳入 CI 流程。

# schema_validation.py 片段 from jsonschema import Draft7Validator def validate_dependency(skill_meta, registry): for dep in skill_meta.get("dependencies", []): upstream = registry.get_skill(dep["skill"]) if upstream is None: raise RuntimeError(f"依赖技能 {dep['skill']} 不存在") upstream_schema = upstream.get("output_schema") local_input_schema = skill_meta.get("input_schema") # 校验本地输入是否是上游输出的子集,这里简化成非空校验 if not upstream_schema or not local_input_schema: raise RuntimeError(f"技能 {skill_meta['name']} 与 {upstream['name']} 缺少 schema")

除了静态契约,运行时还需要一个"技能执行上下文对象",本质是一个类似 dict 的容器,记录每一步输出了什么、存在哪个 key 下,并且支持嵌套读取。我会在所有技能代码里统一用ctx.get("$last_output")和ctx.set("report_data", data)这样的方式做数据传递,代替直接写 Python 变量名。这样技能与技能之间唯一的耦合点就是上下文 key,而不是具体的数据类,后续替换某个技能时只需要对齐 key 就能接入新实现,不需要改其他技能的代码。

3.3 失败恢复策略:为什么 fallback 比重试更可靠

技能执行不可能每次都顺顺利利。API 超时、上游返回空数据、参数校验失败,这类错误在生产里经常出现。很多人的第一反应是给整个链路加重试:执行失败就再来一次。我的经验是,无差别重试是性价比最低的方案,因为很多失败是"确定性的"——比如输入数据格式不对,重试一万次还是不对。你要做的是把失败分类,然后分别设计恢复策略。

我按"可重试"和"不可重试"把常见故障分成三类:第一类是瞬时故障,比如网络超时、服务端 503,这类可以自动重试,但重试次数要限到两次以内,且第二次和第三次之间要有指数退避;第二类是数据问题,例如上游返回了空列表或者字段缺失,这类重试没有意义,应该直接触发 fallback 逻辑,例如读取缓存或换一个上游数据源;第三类是技能逻辑内部抛出的业务异常,比如参数冲突、依赖文件不存在,这类需要把错误信息结构化地返回给模型,让模型决定是换一个技能还是直接面向用户说明失败原因。

我强烈建议在 SKILL.md 的 fallback 字段里把这些策略写清楚,而不是把恢复逻辑全写进代码。因为模型在执行链路中是需要了解 fallback 信息的,它得知道"如果第一步失败了,接下来是继续还是终止"才能做出正确的下一步决策。拿生成周报这个技能来说,fallback 写明"日志接口超时则读取本地缓存目录 /data/log_cache 下的最近文件",模型在第二步执行时发现日志读取失败,就能自主去尝试缓存路径,而不是傻傻地把同样的请求重新发给接口。

4. 实测中躲不开的三个坑:命名冲突、描述冗长与失败恢复

4.1 命名冲突与版本漂移:技能多了之后必然遇到

技能系统最容易被低估的问题就是"命名空间"。"搜索"这个技能,可能在内容团队的项目里叫web_search,在数据团队的项目里叫data_search。两个技能如果同时注册进同一个 Agent,模型在选择时会非常困惑,因为它们的 description 可能都写了"搜索相关资料"。我在一个多团队共建的 Agent 平台上就吃过这个亏,两个技能的名字和描述高度相似,结果模型总选错,导致搜索结果完全不是用户想要的。

解决办法是建立统一的技能命名规范,强制带上领域前缀,比如内容类叫content_search,数据类叫data_query_search,同时要求 description 的第一句就写明领域和边界。这个规范要写进团队的技术文档,并且做成注册时的强校验:名字必须匹配^[a-z]+(_[a-z]+)*$且不允许和已注册技能同名。第二步是在注册表里加一个"别名冲突检测",扫描新注册技能的 description 和已有技能的 description 的文本相似度,超过阈值就报警提醒人工确认。

版本漂移是另一个容易踩的坑。技能升级后,老技能的调用方可能还按旧版的输入格式传参数,导致运行时报错。我遇到过最严重的一次是日志读取技能升级了输出字段,从project_name改成了project,结果下游七个技能里有两个没同步更新,线上任务直接失败。现在我的做法有两个:一是所有技能注册都必须带版本号,并且在 SKILL.md 里用version字段声明;二是提供"兼容层"机制,技能升级后保留一段时间的旧输出别名,例如输出对象里同时写project_name和project,等所有下游完成后才删掉旧字段。虽然有点笨,但稳定压倒一切。

4.2 技能描述过长导致的选择偏差

开头我提到了提示词变长会导致模型选择不稳定,这里展开讲一个反直觉的现象。我给"项目风险识别"技能写过一个特别详细的描述,把每一步逻辑、每个判断规则、每个输出字段都写了进去,想着这样模型一定最清楚什么时候用它。结果是,其他技能的选择率整体下降,而这个技能无论面对什么任务都容易被选中。后来分析才知道,描述过长会让模型产生"这个技能内容最丰富、最可靠"的错觉,本质上类似注意力机制里长文本会占据更多权重。

解决这个问题分两步:第一,把技能的"选择描述"和"执行细节"分离。选择描述只保留简短的 name、description、when_to_use、dependencies 和 main_workflow 的步骤概览,强制控制在 800 字以内(我实际验证的阈值,超过 1000 字准确率会明显下滑)。执行细节挪进技能代码里的注释或专门的 docs 文件,只在执行阶段由执行引擎加载,不进入模型的选择上下文。第二,在描述里专门加一个"排除性提示",明确写"此技能不适用于哪些场景"。这看似矛盾,但实测能把误选率降低一半以上。核心逻辑是,给模型一个"边界感",它才知道什么时候不该用这个技能,而该用技能本身的效果是让 Agent 从"撒网式尝试"变成"脚手架式执行"。

4.3 失败恢复的"静默失败"陷阱

我在真实场景里调试过很多次 Agent 任务的失败,发现最危险的不是报错失败,而是"静默失败"——进程没有报错,任务状态被标记为完成,但产出内容根本不对。最典型的一种是日志读取技能在数据源没有匹配记录时返回了一个空列表,周报生成技能拿到空列表后没有报错,而是直接生成了一份"本周无工作内容"的周报。对于用户来说,这个结果是荒谬的,但系统层面所有状态码都是成功的。这个问题暴露之后,我才意识到失败恢复策略里最容易被忽视的一点:你必须给"预期为空"和"非预期为空"设置明确的判定标准。

现在我的技能模板里强制加入了一个empty_output_policy字段,写明哪些输出字段允许为空、哪些必须非空,以及为空时要执行什么动作——是跳过、填充默认值还是整体中断并通知用户。周报生成技能的配置就写了:log_items 为空时报错并在回复里提示用户"未找到任何日志记录,请检查时间范围或数据源"。这个字段不会进入选择描述,只在执行阶段被读取,但它保证了链路不会在用户不知情的情况下产出垃圾结果。我建议所有从外部数据源读取数据后再做加工的技能,都必须配置这个策略,宁可失败得大声,也不要错误得安静。

5. 从零搭建最小可用技能系统的完整方案

5.1 最小可用技能集怎么定:先走通端到端再横向扩展

搭建 agent-skills 的第一个项目,最容易犯的错误是贪多。我见过一个团队第一天就定义了十六个技能,结果一周过去没有一个跑通了端到端流程。我自己的经验是,初始技能集不要超过三个,而且这三个必须能串成一个完整的业务闭环。举例来说,如果你想要一个"自动生成周报"的 Agent,最小技能集就是三个:项目日志读取、工时统计/汇总、报告渲染。这三个技能一条链路走通,你才算真正掌握了技能系统的注册、选择、编排、恢复整套机制,之后再加技能都是顺水推舟的事。

技能目录和代码骨架,我建议用下面这个最小结构起步:

agent_project/ skills/ project_log_reader/ SKILL.md main.py summary_calculator/ SKILL.md main.py report_renderer/ SKILL.md main.py registry.py orchestrator.py main.py

main.py 里就是最精简的 Agent 主循环:接收请求 -> 召回技能 -> 模型做选择 -> 按工作流执行技能 -> 返回结果。把这条主链跑通之后,再考虑加缓存、加审计日志、加技能热更新这些外围能力。不要本末倒置——技能系统最核心的价值是"结构的确定性",这个确定性在你只有三个技能时就能充分体现。

5.2 测试与评估:怎么知道一套技能真的可用

很多人对技能系统的测试,止步于"每个技能单独跑一次看看能不能返回结果"。这远远不够。技能系统真正的风险在组合,不在单体,所以评估的重心要放在链路上。我常用的方法是建一组端到端的"黄金用例集",每个用例包含一段用户请求、期望调用的技能序列、期望的输出结构。每次改动技能或配置后,跑一遍黄金用例集,统计三个指标:选择准确率(模型选对了技能吗)、执行成功率(技能跑通了吗)、结构符合率(输出 schema 和期望的对得上吗)。

这三个指标里,我要特别提醒结构符合率的重要性。技能系统的输出如果每次都长不一样,下游的对接成本会持续高涨。我在自己的项目里把输出比作"快递单",无论搬运的包裹内容怎么变,单子上的收件人、地址、电话字段必须永远不变。所以每个技能的核心输出都用 Pydantic 模型或 dataclass 定义死,并且单独写 schema 测试,任何字段变更都必须显式改版本号,不允许无声无息地改结构。

另外要给技能系统加日志可观测性。每个技能执行时要把"选择了哪个技能、输入了什么、输出了什么、耗时多少、有没有走 fallback"都记录下来,存成结构化日志。这不是可有可无的运维功能,而是技能调优的数据基础。我最初发现模型总选错技能,就是靠日志发现某些请求普遍倾向于命中同一个技能,再结合具体输入分析才定位到是描述边界不清。没有日志,这类问题只能靠用户投诉才知道,代价差太多了。

最后聊一个容易被忽视的性价比问题:技能描述质量的提升对选择准确率的影响,远大于换一个更大的模型。我在同一套技能系统上做过对比,用原来的小参数模型,配合优化后的技能描述和三层决策链路,任务成功率比裸用大参数模型还高出一截。所以投入精力的优先级应该是:先固化技能描述规范,再做召回优化,最后才是升级模型。这个顺序反了,你会花大价钱买来更聪明的模型,却依然喂给它一堆模糊不清的技能清单,结果只是把错误犯得更快。

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

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

立即咨询