打开我的项目目录,看到agent-skills这个文件夹的时候,我其实愣了一下。这是我最近在折腾的一套智能体技能库,起因很简单:我前前后后搭了十几个 Agent 应用,每个都是从零开始写工具调用、写提示词、调参数,做完一个换一个场景,之前沉淀下来的能力基本全废了。这种感觉就像你天天做饭,但每做一道菜都要重新发明一次火候和刀工,太蠢了。
所以我把所有 Agent 该有的能力全部抽出来,整成一个统一的技能层,这就是 agent-skills 的由来。它解决的问题很直接:让 Agent 具备可复用、可编排、可测试的模块化技能,而不是每次都在 system prompt 里堆一大堆指令。这篇内容我尽量说得实在一些,把技能体系的拆分思路、接口设计、注册调用机制,还有我在实际跑项目中踩到的一堆坑都捋一遍。
1. 先把“技能”这件事拆清楚:Agent 会什么,模型才知道
很多人在做 Agent 的时候,对技能、工具、提示词这三个概念的边界是模糊的。工具是一个函数,一个 API 接口,或者一段可以执行的代码;提示词是模型被调用时接收的指令和上下文。而技能,是这两者之上的封装——它是有名字、有描述、有输入输出约束、有执行逻辑的完整能力单元。
1.1 技能、工具与提示词的边界
我见过最典型的情况是:团队成员把工具函数写在代码里,然后在 system prompt 里用特别长的段落告诉模型“你可以使用这些工具,比如当你需要查询天气时可以调用 xx 函数”。这确实能跑通 demo,但一旦工具数量超过十个,prompt 就会变得臃肿,模型的注意力会被稀释,经常出现该调工具的时候不调、不该调的反而调了。
技能的思路不一样:它把“工具是什么”和“什么时候用这个工具”这两件事剥离开,并且用结构化的方式表达。一个技能包含三部分:
- 元信息:技能名称、描述、适用场景、标签、版本号;
- 调用契约:输入参数 schema、输出结果 schema、错误码定义;
- 执行体:一段代码、一个脚本、一个外部服务请求,甚至是一段固定流程的 prompt 模板。
用一个通俗的类比,工具是工具箱里的扳手和螺丝刀,而技能是一张“换轮胎操作卡”。操作卡上写着什么时候用千斤顶、什么时候用扳手、用什么扭矩标准,而扳手本身并不关心你是在换轮胎还是在修自行车。
这个区分在工程上的意义非常大。工具的粒度太小,模型很难判断该用哪个;技能的粒度是面向任务的,模型只需要判断“当前这个任务属于哪个技能的场景”,决策路径就短得多,误调用的概率也会显著下降。
1.2 为什么技能必须独立建模
把技能从业务逻辑里抽出来独立管理,最直接的好处是三个:可复用、可测试、可观测。
可复用最容易理解。我数据抓取技能、文本摘要技能、图表生成技能做成技能包之后,无论是做一个内容聚合助手、一个市场舆情看板,还是做一个周报自动生成器,都能直接挂载使用。不同 Agent 之间共享同一套技能,不需要把同样的逻辑抄三遍。
可测试意味着每个技能都能单独跑一遍输入输出验证。技能是一个独立组件,我能给它造一组固定的测试用例,跑完就知道描述写得好不好、参数校验有没有漏洞、执行体够不够稳健。这种测试粒度在单体 Agent 里是不可能实现的,因为所有逻辑纠缠在一起,出了问题只能靠全局日志慢慢排查。
可观测则是运维侧的收益。每次技能被调用,都会留下结构化的调用记录:谁调的、什么参数、花了多久、返回了什么、有没有报错。这些数据一方面能帮我做调用统计和成本分析,另一方面也是优化技能描述的重要依据——如果某个技能经常被选中但经常报错,说明它的描述和实现可能不匹配。
1.3 技能描述写不好,Agent 就“看不见”技能
技能描述这件事,我一度严重低估了。第一次设计技能库的时候,我写了一个技能叫“执行代码”,描述只有一句话:“运行给定的 Python 代码。”结果模型在实际任务里疯狂误用,动不动就把别的事情拐到这个技能上,后来排查发现,问题就出在描述太宽泛上。
模型的技能选择机制基于“语义匹配”——它把当前任务目标和技能描述做比对,哪个描述相似度高就调哪个。所以技能描述的写法,本质上是在给模型铺路:什么时候该选我,什么时候不该选我,选了我之后能拿到什么。
我现在写技能描述,基本遵循一个模板:
- 场景触发:什么类型的任务会用到这个技能,尽量列出任务的关键词动作;
- 输入说明:调用这个技能需要提供哪些信息,以及这些信息的形式;
- 输出说明:执行完成后会返回什么,数据以什么结构呈现;
- 排除场景:哪些情况不该调用本技能,这可以大幅减少误选。
举个例子,我有个“中文实体抽取”技能,描述里专门加了“仅适用于从非结构化中文文本中抽取人物、组织、地点等实体,不适用于判断实体间关系,不适用于英文文本”。加了这两句排除之后,误调用率直接降了差不多三成。别嫌描述啰嗦,这里多花的几十个字,能换来后面少踩无数个坑。
2. 技能体系的设计思路与分类
技能不是一个笼统的概念,它应该有清晰的分类。我把实践中沉淀下来的技能分成三类:认知型、执行型、复合型。分类的意义不是搞学术研究,而是让我在新增技能的时候能快速定位设计思路——这个技能是让模型更会思考,还是让模型更会动手,还是把一堆动作编排成一个完整流程。
2.1 认知型技能:拆解、反思、规划
认知型技能不调用外部工具,它调用的对象是模型自身的推理能力。最常见的有三类:任务拆解、反思校验、方案规划。
任务拆解技能,输入是一个模糊的大目标,输出是一组有序的小任务。比如“帮我准备一份市场分析报告”,拆解技能会把这件事分成明确的任务清单:确定分析范围、采集竞品信息、整理数据、生成图表、撰写报告各章节、最终校对。听起来简单,但如果不做拆解,模型经常东一榔头西一棒槌,答到一半发现自己漏了环节,又回头补,效率很低。拆解技能本质上是一种“思考脚手架”,让模型按照固定顺序推进。
反思校验技能更实用。它的逻辑是:模型完成一份输出后,触发一个专门的校验技能,把输出从头到尾审一遍,寻找事实性错误、逻辑矛盾、格式问题。很多情况下,第一遍生成的内容质量一般,但经过一次反思后就有了明显提升。这个技能的实现方式也不难,就是一段结构化的复核 prompt,告诉模型“你现在是一个审查者,逐条检查以下方面”,然后把原始输出作为输入。
认知型技能的核心是“用工程化的方式让模型的思考路径稳定下来”。没有这些技能,模型每次处理同一类任务时的思考路径都是随机的;有了它们,至少在同一条任务线上,模型的思路是收敛的。
2.2 执行型技能:代码、文件、API
执行型技能是 Agent 的“手脚”,也是大多数人最先想到的技能。代码执行、文件读写、HTTP 请求、数据库查询都属于这一类。每个执行型技能背后对应一个具体的函数或服务,Agent 的任务是在恰当的时机选中它,然后把参数填对。
设计执行型技能时有一个容易忽略的点:安全边界。代码执行技能必须跑在沙箱环境里,文件读写技能必须限制在指定目录内,HTTP 请求技能必须走完白名单域名校验。我最初做技能设计的时候图省事,Agent 阶段性地要读写文件,就直接给了一个“文件操作”技能,参数里有路径和内容。结果某次测试中,模型把路径参数写成了用户的 home 目录,导致读了一堆不该读的配置。后来我把“文件操作”拆成几个更细的技能:读文件、写文件、列出目录、删除文件,每个技能里都硬编码好了允许访问的基础目录,参数只能传相对路径。这既是安全加固,也是减少误用。
另一个容易忽略的点是幂等性。同一个技能用同样的参数调用多次,结果应该是一致的。尤其在 Agent 的任务链路比较长、网络超时会触发重试的场景下,如果技能本身不幂等,很容易产生重复数据。我在自己项目里遇到过:一次“写入日志记录”的技能因为超时被系统自动重试,结果日志表里出现了两条重复记录。后来我给写操作类的技能统一加了一个 request_id 参数,每次请求带一个唯一 ID,执行时先查重再写入,从根上解决这个问题。
2.3 复合型技能:把原子能力编排成工作流
有了认知型技能和执行型技能,接下来就是把它们编排成复合型技能。复合型技能解决的是“完成一个完整任务”的需求。
以“生成项目周报”为例,这个复合技能的流程是:先调用“读取提交记录”技能拿到项目仓库最近一周的 commit;再用“代码变更归类”技能把这些提交按功能、修复、重构、文档等类别分组;然后调用“生成周报模板”认知技能组装内容;最后调用“发送消息”技能把周报推送到协作群。整个流程里,每个环节都是一个独立的原子技能,但这些技能组合起来,才是一个对使用者真正有价值的能力。
复合技能的价值在于“确定性”。单靠模型自己在运行时决定先干什么后干什么,执行路径是不确定的;而把常用流程固化成复合技能,执行路径就是确定的。虽然模型内部的思考还是概率性的,但从使用者角度看,这个技能的输入和输出是稳定可预期的。
复合技能的编排方式有两种。一种是代码编排,我老老实实地把流程写在代码里,每个步骤调一个技能;另一种是让模型在运行时动态编排,我给一个任务目标和可用技能列表,由模型自己决定先调啥后调啥。代码编排稳定但灵活性差,动态编排灵活但容易出错。我的做法是:高频常见流程全部固化,低频个性化场景放开给模型自由编排。这样兼顾稳定和灵活,也是技能体系落地的务实选择。
2.4 技能库的结构和组织方式
说完了分类,再讲讲技能库本身怎么组织。我把技能库看成一个独立的代码仓库,根目录下每种技能放一个子目录,每个技能目录里包含一个描述文件和一个实现文件。
描述文件是技能的灵魂,记录了技能名、描述、输入输出 schema、标签。我给标签这件事的重视程度,是直到技能数量超过三十个之后才真正体会到的。最初标签体系没搭好,技能少的时候靠描述匹配也能凑合,技能一多,模型的选择准确率明显下降。后来我把标签分成几类维度:按能力类型(认知、执行、复合)、按业务域(数据、文本、图表、通信)、按数据属性(结构化、非结构化)。检索时这些标签会跟描述拼接起来一起做向量匹配,效果比纯描述匹配好不少。
技能包的组织形式类似共享库:每个技能有版本号,升级旧技能时保留旧版本,通过注册表里的路由来控制哪个版本对外生效。这个灰度思路是从后端服务部署实践里借鉴来的,虽然 Agent 领域还很早期,但技能版本管理这件事建议趁早做,否则后面改一个技能影响了一堆下游 Agent,定位问题会让人怀疑人生。
3. 动手搭一个技能系统:注册、调用与反馈闭环
设计是一回事,跑起来是另一回事。这一章我讲实操,包含技能接口怎么定义、注册表怎么设计、调用链路怎么走,以及如何让技能结果成功反馈并形成闭环。这部分的代码我尽量简化,重点在思路。
3.1 先定技能接口:输入输出契约必须严格
做技能系统第一步,先把接口契约钉死。我的技能接口通常用 JSON Schema 来定义输入输出,这样不仅能校验模型生成的参数,还能省掉一部分重复的字段对齐工作。
我用一个“数据表格汇总”技能举例。它接收一组结构化数据条目和需要计算的指标列表,返回汇总统计结果。输入 schema 长这样:
{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "additionalProperties": { "type": ["number", "string"] } }, "description": "待汇总的数据条目,建议传入10~1000条" }, "metrics": { "type": "array", "items": { "type": "string", "enum": ["sum", "avg", "min", "max", "count"] }, "description": "需要计算的指标列表" } }, "required": ["items", "metrics"], "additionalProperties": false }几个设计要点:required字段明确必填项,模型生成参数时就少了偷懒的余地;enum限定可选值,防止模型发明一个不存在的指标;additionalProperties设为 false,逼着模型按契约来,不让它随意发挥。参数校验这一步决不能省,我在后续排查章节会讲到,模型生成参数时出现幻觉的情况并不罕见,如果接口层没有严格校验,脏参数会一路穿透到执行体里,轻则报错重则写坏数据。
3.2 技能注册与发现机制
接口定义好之后,下一步是注册。我的做法是:启动时扫描技能包目录,解析每个技能的描述文件,注册到内存注册表里。注册表的核心字段包括技能名、版本、描述、标签、输入 schema、执行入口。
技能数量少的时候,把全部技能信息传给模型让它在里面选,是可行的。但当技能数量超过五十个,全部塞进模型上下文的成本和干扰都会失控。这种情况下需要引入检索机制:把技能的描述和标签向量化,任务目标进来时先做一次语义相似度检索,选出 top-k 个候选技能,再把这部分信息交给模型做决策。
这里可以初始化一个技能注册表结构:
class SkillRegistry: def __init__(self): self.skills = {} self.vectorizer = None self.index = None def register(self, skill_meta): self.skills[skill_meta["name"]] = skill_meta def search(self, task, top_k=10): query_vec = self.vectorizer.encode([task]) scores, indices = self.index.search(query_vec, top_k) return [self.skill_names[i] for i in indices[0]]这个搜索引擎用现成的向量库就能实现,不必重复造轮子。我实践下来的体感是:召回环节不需要太精妙,保证“正确答案大概率在候选集里”就够了,真正的决策交给模型。如果召回的候选里没有正确答案,后面怎么写提示词都没用。所以宁可 top_k 大一点,多消耗一点 token,也别因为召回太紧导致正确技能被过滤掉。
3.3 一个完整的技能调用示例
技能系统的核心就是一个“参数生成 → 校验 → 执行 → 结果返回”的闭环。模型发出技能调用请求后,系统先校验参数,失败则把错误信息反馈给模型,让它重新生成;校验通过就执行技能,执行结果再以结构化格式回传给模型,让它继续后续任务。
我用 Python 伪码把这个调用流程写出来,这是一次完整调用 skill 的骨架:
def invoke_skill(skill_name, model_generated_args): skill = registry.get(skill_name) if skill is None: return error("skill_not_found") errors = validate_args(skill.input_schema, model_generated_args) if errors: return error("invalid_args", details=errors) try: result = skill.execute(model_generated_args) return success(result) except SkillTimeoutError: return error("timeout") except SkillExecutionError as e: return error("execution_error", details=str(e))实际的技能执行可能会调用外部 API、写文件、跑数据库查询,这里只展示统一入口。注意成功和失败都使用结构化返回,失败时错误信息必须足够具体,这样模型才能知道怎么修正参数或换一种方案。
这里有个技巧:错误信息不要只给一个“调用失败”,要把校验失败的字段名和期望格式也带回去。比如模型生成了metrics: ["total"]但 schema 只允许sum/avg/min/max/count,返回的错误里就明确写“字段 metrics 的值 total 不在允许范围内,允许的值有 sum, avg, min, max, count”。有了这种反馈,模型下一轮大概率能自纠,整个链路不用人为介入。
3.4 技能的结果反馈与自适应调整
技能执行完成后,返回结果要打包成标准化的反馈格式,里面有状态、数据、耗时、日志摘要。这不仅仅是为了给模型继续推理提供上下文,更是为了后续优化收集数据。
反馈格式大概是这样的:
{ "status": "success", "execution_time_ms": 842, "result_summary": "汇总完成,共处理128条数据,指标 sum=35620, avg=278.3", "detail": { "sum": 35620, "avg": 278.3 } }每次调用产生的记录会落到调用日志里。我定期对这些日志做统计分析:哪些技能被调用频率最高、哪些技能经常报错、哪些技能模型选错了但没有及时发现、平均调用耗时是否异常。这些统计结果直接驱动下一个迭代:调用频率高且经常报错的技能,优先检查描述和实现的匹配度;调用频率为零的技能,要么删掉,要么重新优化描述让它更容易被选中。
这套反馈闭环是技能体系能持续变好的核心动力。没有它,技能库就只是一堆写死的函数,不会根据使用情况自我进化。有了它,每一次模型的实际调用都在为技能库的优化提供线索。
4. 实操中的几个大坑与排查技巧实录
技能体系看着清爽,实操中坑不少。我把印象比较深的几个问题整理一下,每个都附上排查思路和目前的解法。这些内容是从一次次的失败和排查里攒出来的,希望能帮你少绕点弯路。
4.1 技能召回不准:该选的不选,不该选的乱选
我一开始给 Agent 挂了大概二十个技能,跑了一阵子后发现一个规律:模型面对简单任务时表现还行,一旦任务描述偏口语化或者涉及多个场景重叠,技能召回就开始乱。比如用户说“帮我把这几个表的数据合并一下”,模型可能不去选“数据表格汇总”技能,而是去选了“代码执行”技能打算自己写脚本处理。理论上也能实现,但执行路径完全是随机游走,极不稳定。
排查的思路是先看召回阶段有没有问题。我把用户请求和所有技能的 description 全部拉出来看了一遍相似度分数,发现“数据表格汇总”的描述写得偏向工程技术语言,而用户的自然语言表达偏口语化,向量空间里两者的距离确实很大。优化方式是在描述里加入口语化触发词,比如“合并表格”“汇总数据”“算一下平均值”“统计一下总数”,让语义检索更容易命中。同时给技能增加一个别名机制:除了描述里直接写明的关键词,还能配置一组同义词扩展。
另一种有效的解法是任务预处理。我在 Agent 正式技能选择前加了一步轻量的意图归一化:先让模型把用户的原始请求改写成一个不含语气词、目标明确的“任务指令”,再用这个改写后的指令去做技能检索。实测下来召回准确率提升明显,代价只是多了一次几百 token 的模型调用。
4.2 参数幻觉:模型开始编造根本不存在的东西
参数幻觉是技能体系里最让人头疼的问题之一。模型选对了技能但参数填得乱七八糟:把枚举值写错,把必填字段漏掉,甚至编造一个不存在的字段名。这个现象在长链路任务中特别明显,任务执行到后期模型上下文占用量大了,注意力分散,生成参数的准确度就会下降。
我试过靠提示词解决,比如在系统提示里反复强调“严格按照 schema 生成参数,不要遗漏必填项”,但这只是缓解,不能根治。真正有效的还是工程手段:校验关卡前置,并且把校验失败的提示设计得足够好,让模型能低成本自纠。
针对不同参数类型的处理策略也不同。数值类型我给宽松范围加默认值,模型不传就取默认值;枚举类型在返回错误时附带完整可选列表;字符串类型最长长度必须限制,防止模型生成一篇小作文当参数。另外我把所有技能参数都加上additionalProperties: false,模型一旦生成 schema 之外的字段会被直接拒绝。这一条看着严苛,但它能有效阻断幻觉参数的扩散。
4.3 技能冲突与循环调用:两个技能在打架,模型在原地转圈
技能库大了之后,不可避免地会出现边界重叠。我有两个技能:一个是“内容改写”,负责润色文章;一个是“风格转换”,负责把文章从一种文风改成另一种。这两个技能在场景上有大量重叠,模型经常在两个技能间反复横跳:先调了“内容改写”,觉得效果不理想,又调“风格转换”,结果风格转换的结果又触发了“内容改写”,循环往复,白白消耗了大量 token。
解决思路有两条。第一条是物理隔离:如果一个新技能和旧技能的场景重叠度太高,就不要新增,而是把新场景并入旧技能的参数体系里。第二条是运行时检测:在技能调用链路上加一个“调用历史记录”模块,如果检测到模型连续多次在几个固定技能之间来回调用,就中断循环并提示模型“检测到技能冲突,请合并任务目标后一次性调用”。
还有一个更简单的技巧:给容易混淆的技能加“互斥声明”。在技能 description 的末尾写上“如果任务目标是改换文风,请选择风格转换技能而非内容改写技能;如果任务目标是优化表达,请选择内容改写技能而非风格转换技能”。模型看到这类线索后,决策的稳定性会明显提高。
4.4 技能失败后的兜底策略:报错不等于结束
技能执行失败是常态,关键是失败后怎么兜底。我最初的设计是技能执行失败就返回错误,交给模型自行决定下一步。但模型处理错误的方式不稳定:有时候它能找到替代路径,有时候它会在同一个技能上反复重试同样参数,直到任务崩溃。
正确的做法是给技能调用加一层兜底策略。第一步是重试,但必须保证重试参数一致,不是让模型重新生成参数,而是原样重放原参数,专门应对瞬时网络抖动这类问题;第二步是替换技能,如果主技能连续失败两次,就触发一个备选技能,比如“查询数据库”失败则尝试“查询接口服务”;第三步是降级输出,如果所有技能都失败,就返回一个结构化的失败说明,让 Agent 直接告知用户“当前能力不可用,建议人工处理”,而不是让用户面对一堆不可理解的执行日志。
兜底策略里还有一个容易忽略的细节:失败处理也要计入成本。日志里记录重试次数、失败原因、最终兜底结果,这些数据是优化技能可靠性的第一手来源。我梳理出来的一条经验是:高频失败通常不是偶然的网络问题,而是技能本身的设计缺陷,比如输入边界没写好、依赖了不稳定的外部服务、或是在特定数据形态下直接不可用。找到根因去修技能,才是一劳永逸的办法。
4.5 怎么评测一套技能体系:光看成功率远远不够
技能体系的评测和单体 Agent 评测不一样。单体 Agent 只需要看最终任务是否完成,而技能体系需要分层评测,否则你根本分不清问题是出在技能描述、参数契约、执行体还是编排逻辑。
我的评测分三层。第一层是单技能评测,给每个技能准备至少十个典型的输入样例,直接跑函数并核对输出是否合理,关注成功率、失败率、平均耗时。第二层是技能选择评测,给定一批任务描述,看模型能否选对技能,关注准确率、召回率、误用率。第三层是端到端评测,跑完整任务链路,关注最终任务完成率和完成质量。
层与层之间的数据要联动看。如果端到端完成率低,先看是哪一层掉的链子:技能选择错了,去修 description 和召回;参数校验总失败,去修 schema 和提示模板;执行体报错多,去修代码和外部服务。这套分层评测机制推广后,我定位问题的速度提升非常明显。以前排查一个 Agent 的 bug 可能要翻几百行日志逐段推理,现在直接看哪一层指标异常,有的放矢。
技能库的维护是个持续工作,不是上线就跑通一套就不管了。每隔一段时间,我会重新跑一遍评测集,看看指标有没有变化。尤其是当某个技能被更新后,必须回归测试所有依赖它的 Agent,否则经常出现“单个技能指标正常,但某个下游 Agent 表现变差了”的诡异现象。根本原因往往是新技能输出的数据结构有了细微调整,而下游流程还按旧结构解析。这种问题不跑回归测试很难发现。
一个值得分享的收尾经验
技能系统做到现在,我最大的体会是:一开始别贪大求全,先把两三个最常用的场景技能做扎实,跑通完整的注册、调用、反馈闭环,再去扩展技能数量。技能库不是越大越好,每加一个技能,模型的决策空间就多一个维度,误选风险也会随之上升。先把核心链路的稳定性打牢,再逐步加量,是性价比最高的推进方式。
最后再分享一个小技巧:给每个技能目录建一个 README 文件,记这段技能的迭代历史,比如描述改过什么、参数做过什么调整、遇到过哪些典型的误选案例。这些资料看起来琐碎,但当你面对几十个技能、需要回头复盘某个诡异行为的时候,一份好的技能档案比任何日志都好用。我也是在某次连续排查了几个小时技能冲突之后才开始养这个习惯,从那以后,技能体系的维护体验完全不一样了。