☰
Agent技能体系agent-skills:让大模型从会说到会做
2026/10/21 18:33:34 网站建设 项目流程

最近在重构一套多智能体协作系统时,我发现一个特别扎心的现象:同样的一个工具函数,在服务A里被硬编码进Prompt,在服务B里重新封装了一遍,到了服务C又换了个参数格式。三个服务各写各的,谁也没法直接复用。模型经常“会说不会做”——不是它不会调API,而是我们压根没给它一份结构化的、可检索的“能力说明书”。我们内部把这套能力标准化方案叫做agent-skills,它不是什么新框架的专有名词,而是一套把LLM可执行能力抽成标准化模块的设计思路。

这篇文章会完整拆解agent-skills的落地过程:技能的最小描述单元长什么样、注册中心怎么管生命周期、多技能之间怎么路由和组合、以及我在实测中踩过的一堆坑。整体偏工程实践,适合正在用大模型做提效工具、想把零散函数变成体系化能力的开发者,也适合刚接触多Agent编排、想搞懂“技能注册”到底在解决什么问题的同学。

1. 为什么我的Agent总是“会说不会做”:三个让我下决心做技能体系的痛点

先说说我为什么非要折腾这套东西。如果你只是接一个模型接口,让模型做做文本总结,那确实不需要技能体系。但一旦你的Agent要真正去操作业务系统、查数据库、发通知、做计算,事情就开始失控了。失控的方式很典型:

1.1 痛点一:工具代码散落在各个服务里,越复制越乱

最早我们做智能助手时,每个Agent服务里都有一把私有函数。有的叫query_inventory,有的叫getStockInfo,干的其实是同一件事,但参数名、返回结构、错误码全不一样。到了第二个项目,开发同学图省事,直接把上一个服务的函数拷过来改两下就用,结果文档没跟代码走,Prompt里写的调用示例早就过时了。

这种散落不只是维护成本高的问题,更致命的是——你没法让多个模型统一使用这些能力。一个能力没有标准定义,没有统一注册入口,模型每次面对的都是“开发者在Prompt里临时描述的一个函数”,它只能靠猜。

1.2 痛点二:提示词和逻辑纠缠不清,改一个参数要动一轮Prompt

现在很多人写Agent还是这套套路:在系统提示词里写上“你有以下工具可用:1. 库存查询,入参是sku_id……”,再把JSON Schema贴进去。看起来能用,实际上特别脆弱。只要接口参数稍微变一下,比如超时时间从3秒改成5秒,或者新增了一个筛选条件,你就得回头改Prompt。Prompt一旦变长,模型对工具的选择准确率就开始下降,逻辑和文本耦合得越深,系统就越难扩展。

这个痛点在多Agent场景里会更明显:一个Agent要给另一个Agent提供能力,如果各家的能力只是写在各自的Prompt里,跨Agent调用根本无从谈起。

1.3 痛点三:多Agent协作时,能力边界是一笔糊涂账

去年我们做了一个模拟运营系统,里面有多个Agent角色:一个管库存、一个管订单、一个管售后。刚开始每个Agent各写各的Prompt,各调各的接口,看似分工明确。一跑起来发现,售后Agent需要查订单状态,它不知道该调谁的哪个函数,只能让用户“把订单号发我”。这种体验离“自动化”差得很远。

我当时就在想,如果所有Agent能共享一份“能力清单”,每个技能都带着清晰描述、参数Schema、权限标签,任何Agent都能通过清单发现和调用,那跨Agent协作的问题就解决了一大半。这就回到了一个很朴素的思路:把能力从Prompt里抽出来,变成一个个可以注册、检索、编排的模块——这就是agent-skills的起点。

技能体系不是银弹,但它把“模型能不能调对工具”这个玄学问题,变成了“我们有没有把技能描述写清楚”这个工程问题。

2. 技能的最小描述单元:从一段函数到一份可被LLM理解的Schema

技能体系的核心不是代码,而是“结构化的描述”。你要让一个语言模型理解“这个技能是干嘛的、什么时候用、参数怎么传、返回值是什么”,不能靠注释,必须靠一份机器可读、模型可检索、人也可维护的定义文件。

2.1 技能描述应该包含哪些字段

我把一个技能的最小描述单元拆成十一个字段,不是越全越好,而是每个字段都有明确目的:

字段说明为什么必须有
id技能唯一标识,如inventory.query_stock路由和日志追踪都靠它
name人类可读的名称,如“库存实时查询”模型在推理时依赖自然语言名称
description一句话到两句话说清适用场景和边界条件模型决定“什么时候该用”主要看它
tags能力标签,如库存、只读、高并发支持系统侧路由和权限过滤
input_schemaJSON Schema 严格定义入参减少模型幻觉参数,静态校验依赖它
output_schema返回结构的结构化定义调用方Agent能预知结果形态
executor真正的执行函数或服务端点描述归描述,执行归执行
permissions所需权限域,如inventory:read多Agent场景强制做权限隔离
timeout_ms超时阈值防止某个慢技能拖垮整个编排
version版本号支持灰度发布和回滚
enabled是否启用动态摘除不健康能力

你可能会问,name和description是给模型看的,input_schema是给参数校验用的,那executor给谁用?给运行时用。模型永远不直接看到executor的实现细节,它只看到结构化描述,真正执行由技能管理器统一调度。这个分离非常重要,它保证了模型和真实系统之间有一层可控的防火墙。

2.2 一个技能的完整定义示例

下面是我实际使用的一个YAML定义,功能是语义搜索内部知识库。写这个示例是为了让你直观看到每个字段长什么样:

id: knowledge.semantic_search name: 知识库语义搜索 description: | 当用户想要查询内部知识库中与某个主题相关的文档时使用。 支持模糊语义匹配,不依赖精确关键词。适合问题类、经验类、 流程类查询。不适合做精确账务查询。 tags: [知识库, 搜索, 只读] version: 1.2.0 enabled: true timeout_ms: 3000 permissions: - knowledge:read input_schema: type: object required: [query, top_k] properties: query: type: string minLength: 2 top_k: type: integer minimum: 1 maximum: 10 default: 5 scope: type: string enum: [all, order, inventory, after_sale] default: all output_schema: type: object required: [items] properties: items: type: array items: type: object required: [doc_id, title, score] properties: doc_id: { type: string } title: { type: string } score: { type: number }

配套的executor可以是一个函数,也可以是一个HTTP服务地址。我更建议在初期用函数注册,后期再拆成独立服务,这样技能管理器可以统一处理超时和重试。

2.3 描述文本和参数约束:LLM能不能调对,全看这两项

很多人把精力全花在写executor上,却把description写得很随意,比如“用于知识库搜索”。这话太模糊了——“什么时候用、什么时候不用、边界在哪”全没讲。模型看到这种描述,只好凭感觉。我调试的时候发现,同一套技能,描述写得具体不具体,调用准确率能差30%以上。

写description有个小技巧:要同时写清楚“该用的场景”和“不该用的场景”。比如知识库语义搜索,我会特意写明“不适合做精确账务查询”,这样模型遇到一个需要精确数字的问题时,就不会误选这个技能。这种“负样例”经常被忽略,但实测特别管用。

参数约束也一样。input_schema里的required和enum字段要尽量收紧。你给模型的自由度越大,它越容易发挥“创造力”。把可枚举的值写成enum,把必填项写成required,既能降低参数幻觉,也能让后端的参数校验变成一个纯机械动作。

这里分享一个自查方法:把技能描述定义好以后,先不要接模型,把描述的文本打印出来,自己像第一次见面的人一样读一遍——如果三秒钟内你能准确说出这个技能什么时候该用、什么时候不该用,那描述基本合格了。

3. 技能管理器:注册、校验、索引、热更新的统一出口

有了技能定义之后,下一步就是建一个统一的管理出口。我把它叫技能管理器(Skill Registry),它负责技能的注册、校验、索引和动态生命周期管理。所有Agent都只跟这个管理器打交道,而不是直接去调函数。

3.1 管理器的四个核心职责

职责做的事我踩过的坑
注册接收技能定义,解析为内部运行时对象没有做格式校验,结果脏数据进了索引
校验校验Schema合法性和executor可调用性忽略了对执行器端点的连通性探测
索引建立描述、标签、名称的三维度检索没有索引,每次路由都在全量遍历
生命周期管理处理启用、禁用、版本更新、热切换直接替换实例导致正在执行的请求出错

这四件事听起来简单,但每件都有细节。比如校验,不只是校验YAML语法,还要解析input_schema的合法性,把minLength、maximum这些约束提前算好,注册时就拦截无效配置,而不是到运行时才报错。

3.2 注册流程与代码骨架

我写了一个简化版的管理器,它足够支撑大部分业务场景:

import json import threading import yaml class SkillRegistry: def __init__(self): self._skills = {} self._lock = threading.RLock() def register(self, skill_def: dict, executor: callable) -> None: skill_id = skill_def["id"] with self._lock: if skill_id in self._skills: # 版本逻辑单独处理,这里先严格去重 raise ValueError(f"skill {skill_id} already registered") self._validate(skill_def) skill_obj = { "definition": skill_def, "executor": executor, } self._skills[skill_id] = skill_obj def _validate(self, d: dict) -> None: # 必填字段检查 for field in ["id", "name", "description", "input_schema"]: if field not in d: raise ValueError(f"missing field: {field}") # input_schema 必须是合法 JSON Schema try: json.dumps(d["input_schema"]) except TypeError as e: raise ValueError(f"input_schema not serializable: {e}") def get(self, skill_id: str) -> dict: with self._lock: return self._skills.get(skill_id) def search(self, query: str, tags: list[str] | None = None) -> list[dict]: # 简易检索,实际可以替换成向量检索 with self._lock: results = [] for skill_id, skill_obj in self._skills.items(): d = skill_obj["definition"] if not d.get("enabled", True): continue if tags and not set(tags).issubset(set(d.get("tags", []))): continue if query and query not in d["description"] and query not in d["name"]: continue results.append(d) return results def update(self, skill_id: str, new_def: dict, new_executor: callable | None = None) -> None: with self._lock: if skill_id not in self._skills: raise ValueError(f"skill {skill_id} not found") old = self._skills[skill_id] current = dict(old) current["definition"] = new_def if new_executor: current["executor"] = new_executor self._skills[skill_id] = current

这段代码看起来像玩具,但它包含了核心思路:所有读写操作加锁保护,注册时就做校验,检索时可以按标签过滤。生产级实现在此之上再加日志、指标和配置热加载,但骨架结构不会变。

3.3 生命周期管理:启用、禁用、版本和热更新

技能不是一个“注册完就不管”的静态配置。线上服务随时可能出问题:某个技能突然超时率飙升、新版本参数不兼容、合作方接口需要临时下线。这时候你不可能去找开发改代码发布,你得有动态管理能力。

生命周期我分了四个状态:

  1. Registering:注册中,校验定义和executor
  2. Enabled:正常对外提供服务
  3. Disabled:手动摘除,路由层直接跳过
  4. Updating:更新版本,切换期间请求正常

热度切换是最容易出问题的点。我一开始直接在注册表里替换executor引用,结果正在执行的请求拿到的可能是旧函数、也可能是新函数,状态就乱了。后来改成“孤儿实例”方案:更新时保留旧实例继续处理在途请求,等旧请求全部结束后再物理移除。实测下来干净很多。

这个过程的代价是内存里会短暂存在两个技能实例,但对现代服务来说完全可接受。

4. 技能路由与组合:从单技能调用到多Agent协作的流水线

技能定义和注册解决的是“有什么能力”的问题,路由和组合解决“怎么用能力”的问题。这是agent-skills体系里最见功力的部分。

4.1 命中路由:LLM选技能和系统选技能怎么分工

技能路由有两种思路,很多团队会纠结选哪种。我的结论是两者不冲突,各管一段:

路由方式谁在做决定优点缺点
LLM路由模型根据描述和当前对话自主选择技能灵活,能处理模糊意图延迟高,可能选错,成本随调用量上升
系统路由按规则、标签、历史命中率直接决定技能确定性强、快不灵活,无法覆盖所有语义变体

我的做法是“系统先行,LLM兜底”:先让系统按用户意图分类、知识类型做一轮粗筛,如果系统能确定命中某个技能就直接调;如果系统拿不准,再把技能描述列表交给LLM做细粒度选择。这样既保证了大部分请求的稳定性,又保留了模糊场景的灵活性。

比如用户说“最近仓库里的蓝牙耳机还有多少货”,系统路由通过实体识别(仓库、蓝牙耳机)已经能直接锁定inventory.query_stock这个技能,完全不需要模型参与。如果用户说“帮我看看耳机还能不能补货”,意图模糊,系统找不到精确技能,才把候选技能列表交给模型裁决。

4.2 组合编排:串行、并行、条件分支

单技能调用只解决单点问题,真正的Agent场景往往需要多个技能按顺序或条件组合。我在编排层引入了一个轻量级的任务描述格式,类似流水线定义:

workflow: report.daily_sales steps: - skill: inventory.query_stock as: stock_data - skill: order.query_sales as: sales_data params: start_date: ${{ date.yesterday }} - skill: report.generate params: stock: ${{ steps.stock_data.result }} sales: ${{ steps.sales_data.result }}

这个编排的意思很直白:串行执行两个查询技能,把结果作为参数传给报告生成技能。实际跑起来时我会根据技能类型决定执行方式:

  • 纯只读技能而且彼此无依赖,并行执行,等所有结果回来再继续
  • 有依赖关系的技能,串行执行,每一步拿到上一步输出再下一步
  • 条件分支,用上一步返回的某个字段决定下一步走哪个技能

并行执行对延迟的优化非常明显。我有一组流程原来串行要6秒,改成并行后压到2.5秒。前提是技能必须是线程安全的,这个细节我后面在坑里细讲。

4.3 冲突处理:重名技能、模糊描述、权限不一致

技能一多,冲突必然出现。我整理了三类常见冲突和对应策略,供你参考:

冲突场景具体表现处理策略
技能重名两个模块都注册了report.generate强制命名空间前缀,按模块隔离;广域功能用module.function全名匹配
描述模糊两个技能描述高度相似,路由无法区分在description里增加负样例和明确边界,比如“本技能只处理已付款订单”
权限不一致Agent请求的技能需要更高权限技能管理器按Agent身份做权限校验,无权直接返回permission_denied,不把错误暴露给模型

权限不一致这个问题在初期最容易被忽略。我们一开始没做权限隔离,结果一个只读Agent也能调用批量删除技能,差点出事故。现在每个技能定义里的permissions字段都会和Agent的授权token强制绑定,技能管理器在路由层做前置校验,不通过就拦截,根本不会走到executor那一步。

5. 实测中踩过的坑和排查思路:给正在做技能库的同学参考

这个章节全是我在实际运行里踩过的坑,不是理论推演。每个坑都花过真实的时间去定位和修复,我把排查链路也写出来,希望你遇到类似问题时能少绕弯子。

5.1 坑一:参数Schema写得太宽,模型开始“瞎猜”

有段时间我的知识库搜索技能命中率暴跌,日志显示模型调用时传了一个search_type参数,但我Schema里根本没定义这个字段。好家伙,模型直接发挥了。因为后端的参数绑定是按定义严格处理的,多出来的字段被忽略,结果搜索行为完全不是模型预期的那样,反馈自然差。

修复方式就是收紧Schema:所有可枚举的值全部用enum锁死,非必填项立即标注,没有默认值的字段不写default。改完之后,参数幻觉明显减少。我现在的原则是:Schema能收紧就收紧,没有确定情况的字段一律不给模型发挥空间。

5.2 坑二:技能并发执行时状态污染

这个坑藏得很深。我早期把技能对象做成单例,所有请求共享同一个技能实例。技能内部为了性能缓存了一些数据在实例变量里,结果并发一高,多个请求互相覆盖,返回结果完全错乱。这个问题在本地压测时不一定暴露,因为并发量小,一旦上线流量上去就炸了。

排查链路是这样的:先看到错误率飙升,然后看异常堆栈发现是在技能执行器内部数据不一致,最后定位到实例变量被并发修改。修复方案是:技能实例不再全局共享,改为每次请求时从技能管理器获取一个“干净的上下文镜像”,所有可变状态都放进上下文中。这条我建议你在设计阶段就决定好,不然后期重构很痛。

5.3 坑三:注册顺序影响路由结果

系统路由刚上线时,我按注册顺序遍历技能列表,返回第一个描述命中的技能。测试时发现,同样一句话,有时选A技能,有时选B技能,后来发现是注册顺序变了。注册顺序完全是个偶然因素,怎么能影响路由结果呢?

我后来把路由从“第一个命中”改成了“评分制”:对候选技能的描述、标签、历史调用频率做加权评分,取最高分。虽然简单,但至少跟注册顺序无关了,确定性好了很多。如果要做得更精细,可以用向量检索对描述做语义匹配,但业务量没到那个程度,用加权评分已经足够。

5.4 排查链路示例:一次技能命中率下跌的定位过程

这是某一次真实的问题排查,我觉得很有代表性。现象是:一个订单查询Agent的准确率从92%掉到70%,幅度很明显。我开始怀疑是模型升级导致路由行为变化,后来一看不是。

一步步查下来:

  1. 查路由日志:发现大量请求被路由到了order.summary而不是order.detail。这不是模型自己选的,是系统路由根据关键词“订单详情”同时匹配了两个技能,按照旧逻辑取第一个,而order.summary排在前面。
  2. 查技能定义:两个技能的description都写了“查询订单信息”,没有边界区分。
  3. 查调用结果:order.summary返回的是聚合数据,而用户要的是单条订单项数据,形态完全不符。

定位到根因后,修复分两步:一是把两个技能的description边界写清楚,明细查询强调“返回单条订单项”,汇总查询强调“返回聚合统计、用于报表”;二是在系统路由层增加了“意图置信度”判断,置信度低于阈值时提升触发LLM判断路由,而不是直接选第一个。

这套双重调整之后,准确率回到了94%以上,而且比之前更稳定。这次排查的经验是:先看路由日志,再看技能定义,最后才考虑模型问题——大多数时候是描述写得不够好,而不是模型变笨了。

最后再说一个我个人的体会:做技能体系,最花时间的不是写executor,而是打磨description和Schema。你可以把技能描述当成产品文案来写,每次上线前拿一组固定的探测问题跑一遍,看模型会不会选错技能。这个动作看起来笨,但它能救你于真实调用时的各种玄学问题。我现在的团队里,技能上线前必须先过这个“探测问题集”,也算是在这次重构里最值得的一笔投入了。

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

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

立即咨询