如果你也一直在做大模型 Agent 相关的项目,大概率会有同感:Demo 阶段怎么调都顺,一旦进入真实业务,Agent 就变得有点“不听指挥”。问题往往不在模型本身,而在于我们塞给 Agent 的那堆能力太乱了。今天想和你聊聊我最近在整理的 agent-skills 这个方向,简单说就是给智能体做一套可复用、可管理、可扩展的“技能库”,而不是继续零散地堆函数和提示词。
这套思路适合谁?如果你是刚接触 Agent 开发,想跳过“先跑通再返工”的弯路;或者你已经做了几个 Chat 类应用,正在头疼多轮任务里工具调用频繁出错,那么这篇内容应该能给你一个可以直接参考的落地框架。我会从需求拆解讲到技能结构设计,再给出一份完整的实现记录和踩坑清单,全部基于我在实际项目里反复迭代过的方法,不是那种停留在概念层的建议。
1. Agent Skills到底在解决什么问题
1.1 从“临时工具”到“长期技能”的转变
先回想一下我们最开始怎么做 Agent。拿到一个需求,比如“帮用户查天气”,我们会写一个get_weather(city)函数,注册到大模型的 tools 列表里,然后模型在合适的时候调用它。这没什么问题,但当一个项目里陆续出现十几个、几十个这样的函数时,问题就来了。
首先是选择困难。大模型面对一长串函数定义,有时候会选错,尤其是两个函数描述相似的时候。其次是逻辑混乱。有些函数调用的不是外部接口,而是另一组函数,这个过程在纯 tools 模式下没人帮你管理,只能靠提示词强行约束。再往后是维护成本,改一个通用参数,可能要把十几个函数的定义全部同步更新一次。
agent-skills 解决的是把这个过程“体系化”。一个技能不只是“一段可调用的代码”,它是一套完整的最小能力单元,包含元信息、参数契约、执行逻辑、边界条件。你可以理解为:工具是给 Agent 递一把螺丝刀,技能是给 Agent 一本岗位手册,上面写了什么场景用这把螺丝刀、怎么用、用的时候要注意什么、用完怎么收尾。
从实际效果看,把工具改造成技能后,我遇到的最大改善是选型准确率。因为技能描述可以写得比函数注释详细得多,模型在规划阶段就能根据任务目标做“粗筛”,再在粗筛结果里做精确匹配,而不是对着平坦列表硬选。
1.2 技能和工具、工作流、插件的边界在哪里
这里我们得把概念理清楚。很多人把技能当作工具的代名词,其实不太准确。
工具(Tools)是最底层的一类原子能力,比如“发一个 HTTP 请求”“执行一段 SQL”“读取一个文件”。它不关心业务流程,只负责把一件事做对。插件(Plugins)通常是指一组打包好的工具加配置,比如某个第三方平台的 SDK 接入包。工作流(Workflows)则是把多个工具按固定顺序编排起来,处理一个特定场景,比如“每天早上拉取数据、清洗、生成报表、发送邮件”。
技能(Skills)的位置在它们之间。技能可以封装单个工具,也可以编排多个工具,但它和固定工作流有个关键区别:技能的调用时机和组合方式是由 Agent 在运行时根据用户意图动态决定的,而不是预先写死。同一个技能,既可以被单独调用,也可以被另一个技能内部复用。
用一个表格来对照会更清楚:
| 概念 | 核心特点 | 举例 | 是否由Agent动态选择 |
|---|---|---|---|
| 工具 | 单一、原子、无状态 | 发送请求、读写文件 | 是 |
| 插件 | 工具+配置的打包组合 | 支付平台接入包 | 通常是 |
| 工作流 | 固化步骤、顺序执行 | 每日数据报表流程 | 否 |
| 技能 | 原子操作或轻量编排,带完整语义 | 文档格式转换、订单状态推断 | 是 |
实际开发中,我的经验是:能写成工具的,先写成工具;工具多了之后,按“业务语义”把成组的工具收拢成技能;同一个技能如果被反复以固定顺序组合使用,再把它提升为工作流。这样分层,每个层级的职责都很清楚,不会出现一个技能里面塞了几十个步骤,变成不可维护的“神技”。
1.3 一套技能体系最少要包含哪些要素
我在设计自己的技能库时,发现最少需要四个要素才能让技能真正稳定运转:技能的唯一标识、给大模型看的行为描述、参数契约、执行体。
唯一标识很简单,就是技能的名字,但它必须稳定,一旦发布就不能轻易改名,因为 Agent 的上下文和日志里到处都会引用它。行为描述是整个技能里最重要的一份文本,它直接决定了模型在什么场景下会选中这个技能。参数契约是 JSON Schema 形式的入参定义,它决定了模型能不能生成合法调用。执行体是真正的代码逻辑,可以是本地函数、远程API调用,也可以是另一段提示词驱动的子过程。
很多人忽略的是“边界描述”。技能描述里最好写明“这个技能不做什么”。举个实际例子,我做一个“订单状态查询”技能时,描述里写了“本技能只负责查询,不处理退款或改地址”,模型就很少越权调用。加上边界描述之后,误用率明显下降,因为大模型在做规划时往往会过度乐观地分配任务给它拿到的工具。
2. 设计技能库的四个关键决策
2.1 技能粒度:拆到什么程度才算合适
技能粒度的把控,是整个技能库设计里最容易翻车的环节。拆得太粗,一个技能包含太多隐含逻辑,代价是复用性差,Agent 在无关场景下也可能误选它。拆得太细,技能数量会爆炸,每次让模型从上百个技能里做选择,选型精度必然下降。
我用的判断标准是“一句话能否说清它的职责”。如果一个技能需要两句话才能解释清楚它做了什么,那大概率太粗了。反过来,如果一个技能描述短到和函数名没区别,那说明它就是伪装的工具,没必要单独做成技能。
举个例子。我之前做过一个文档处理项目,最初把“把收到的文档转成PDF并提取关键信息”合并成一个技能,结果模型在只需要提取信息的场景里也调用了它,白白浪费一次转换操作。后来拆成convert_doc_format和extract_doc_meta两个技能,各自只负责一件事,问题就消失了。
这里有个实用方法:每个技能可以先按“动词+对象”命名,比如“转换文档格式”“查询物流轨迹”“解析招聘信息”。如果发现命名里出现“和”“或者”,说明粒度不对,要继续拆分。
2.2 描述即接口:给LLM看的说明书怎么写
技能描述是给大模型看的接口文档,不是给人看的注释。它的写法直接决定了模型的调用成功率。我见过太多团队的技能描述写得像代码注释,比如“该函数用于转换文档格式”,这种描述对模型几乎毫无帮助。
一份合格的技能描述要包含三部分:触发场景、执行内容、预期结果。触发场景要写“当用户需要……时”“如果上下文里出现……”,执行内容写具体操作,预期结果写调用后能返回什么。还有一点容易被忽略:写清楚和它相近的技能之间的区别。否则模型会在两个相似技能之间犹豫。
我自己常用的描述模板是这样:
技能名称:convert_doc_format 触发场景:当用户需要把 Markdown、HTML 或纯文本文件转换为 PDF、DOCX 或 HTML 格式时。 执行内容:读取指定路径的源文件,调用底层转换引擎生成目标格式文件并保存到指定目录。 预期结果:返回生成文件的绝对路径和文件大小。 注意:本技能只负责格式转换,不负责内容编辑和排版优化。这个模板写起来不长,但信息密度很高。模型读完之后基本能判断该不该调用。每多花两分钟把描述写清楚,就能少花两小时去排查“为什么模型就是不调用”。
2.3 参数定义:让大模型稳定生成合法入参
参数契约是整个技能调用里最容易被忽视、但出错率最高的环节。大模型生成 JSON 参数偶尔会不按 schema 来,比如把枚举值写成相近词,或者漏掉必填字段。这不是模型太笨,而是参数 schema 设计得不够友好。
我在设计参数时坚持三个原则。第一,尽量用枚举和默认值引导模型,而不是开放字符串。比如转换格式,就把target_format定义为枚举[pdf, docx, html],模型选错的概率会低很多。第二,每个字段的描述要带上取值范围或格式示例,比如“input_path:源文件路径,必须是本地存在的文件路径”。第三,必填字段越少越好,能设默认值的都设默认值,减少模型遗漏必填参数的概率。
对于调用前校验,我会在技能执行体里再做一次兜底,而不是完全相信模型生成的参数。参数不合法时,先把错误信息返回给 Agent 重试,而不是直接抛异常导致整个任务中断。这个兜底机制在后面还会详细讲。
2.4 执行层抽象:让技能定义与代码解耦
技能库里所有技能都共用同一个执行入口,而不直接绑定到具体函数。也就是说,技能的元信息里写的是“handler 指向哪个执行器”,执行器负责解析参数、加载依赖、调用逻辑、返回统一格式的结果。
这么做的好处是,技能定义和执行逻辑可以独立演化。改技能描述不用动代码,换执行逻辑也不用大改描述。而且,不同团队可以各自负责不同技能,只要遵循统一协议就能接入同一个技能库。比如一个技能可能是 Python 写的本地函数,另一个技能可能是一个远程服务的 API 封装,但对 Agent 来说,它们呈现出来的调用方式完全一致。
统一结果格式也很重要,我会要求执行体返回一个 JSON,至少包含status、result、message三个字段。这样 Agent 拿到结果后,无论哪个技能,都能用同样的方式理解执行结果。你可以在语言模型提示词里写“调用技能后,根据返回的 result 字段组织回复”,而不用为每个技能单独写一套返回逻辑。
3. 从零实现一个技能库:完整实操记录
3.1 技能目录与元信息定义
我一般在项目里用一个skills/目录来维护所有技能,每个技能一个子目录,包含一份元信息文件和一份执行体。这样既方便单独做版本管理,也方便新成员通过目录名快速了解技能库的全貌。
一个典型的目录结构长这样:
skills/ convert_doc_format/ skill.yaml handler.py query_order_status/ skill.yaml handler.py summarize_text/ skill.yaml handler.py registry.json技能元信息我倾向于用 YAML,比 JSON 可读性好,而且能写注释。下面是一份我实际用过的技能元信息:
name: convert_doc_format description: | 当用户需要把 Markdown、HTML 或纯文本文件转换为 PDF、DOCX 或 HTML 格式时使用。 读取指定路径的源文件,调用转换引擎生成目标格式文件并保存到输出目录。 返回生成文件的绝对路径和大小。 注意:本技能只负责格式转换,不负责内容编辑和排版优化。 version: 1.2.0 handler: python:handler.convert parameters: type: object properties: input_path: type: string description: 源文件路径,必须是存在的本地路径 target_format: type: string enum: [pdf, docx, html] description: 目标格式 output_dir: type: string description: 输出目录,默认是当前目录 default: "." required: - input_path - target_format注意registry.json的作用:它保存每个技能的名称、版本、入口描述,是技能库对外检索的索引。启动时先加载它,再按需加载每个技能的执行代码,避免一次性把所有技能全部 import 进内存。
3.2 实现一个示例技能(文件格式转换)
执行体是最容易写的一部分,核心是把它做成与技能元信息解耦的纯函数。下面是一个简化版的handler.py,实际项目里还需要考虑大文件处理、编码识别、依赖环境检查等问题。
# handler.py from pathlib import Path import json def convert(input_path: str, target_format: str, output_dir: str = ".") -> dict: src = Path(input_path) if not src.exists(): return {"status": "error", "result": None, "message": f"源文件不存在: {input_path}"} supported = {"pdf", "docx", "html"} if target_format not in supported: return {"status": "error", "result": None, "message": f"不支持的格式: {target_format}"} out_dir = Path(output_dir) out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / f"{src.stem}.{target_format}" # 这里调用真实的转换引擎,示例代码省略具体实现 # engine.convert(src, out_path, target_format) return { "status": "ok", "result": { "output_file": str(out_path.resolve()), "size": out_path.stat().st_size if out_path.exists() else 0 }, "message": "转换完成" }代码里刻意强调了对源文件存在性和目标格式合法性的校验。因为大模型生成的参数偶尔会带着幻觉,比如提供一个根本不存在的文件路径,这种校验能把错误挡在调用真实转换逻辑之前。
同时要注意返回格式和元信息里“预期结果”保持一致,这样后续让 Agent 整理回复时,它能直接引用result.output_file这个字段。
3.3 技能注册与检索逻辑
技能注册的核心是一个加载器。我设计了一个SkillLibrary类,启动时扫描skills/目录下所有skill.yaml,把它们放进一个字典;需要时再根据用户输入做检索。
# skill_library.py from pathlib import Path import yaml class SkillLibrary: def __init__(self, skills_dir: str = "skills"): self._skills = {} self._load_all(skills_dir) def _load_all(self, skills_dir: str): for skill_yaml in Path(skills_dir).glob("*/skill.yaml"): meta = yaml.safe_load(skill_yaml.read_text(encoding="utf-8")) self._skills[meta["name"]] = meta def get(self, name: str) -> dict | None: return self._skills.get(name) def list_skills(self) -> list[dict]: return [ {"name": s["name"], "description": s["description"]} for s in self._skills.values() ]检索方式我试过两种。纯关键词匹配不好用,因为用户的表达和技能描述的用词差异很大。后来我直接先把所有技能的“描述”用向量化接口转成 embedding,用户请求到达时先做一次相似度检索,取 top 5 技能候选,再把候选列表和用户请求一起发给大模型做最终选择。
这么做的好处是:大模型不需要在上百个技能里做选择题,而是从 5 个里选 1 个,准确率高很多,token 消耗也少。你也可以用粗筛加精排的思路,只要候选集足够小,模型的选择能力就足够可靠。
3.4 让Agent自主完成技能调度
有了技能库之后,Agent 的 Prompt 里就不再需要写冗长的工具函数列表了。我会在 System Prompt 里告诉它:你拥有一个技能库,下面只列出技能名称和一句话简介,完整描述你可以在需要时查询。这样上下文窗口能省下大量空间。
调度流程大致是这样:
- Agent 收到用户请求,先调用技能检索接口,得到候选技能。
- Agent 根据候选技能的完整描述判断是否匹配。
- 匹配则输出技能调用意图,包括技能名和参数。
- 执行器调用技能,得到统一结果 JSON。
- Agent 根据结果组织最终回复。
整个流程可以用一段简单的伪代码描述:
user_input = "把这份文档转成PDF" candidates = skill_lib.retrieve(user_input, top_k=5) plan = llm.plan(user_input, candidates) if plan.has_skill_call(): result = executor.execute(plan.skill_name, plan.params) reply = llm.finalize(user_input, result)这里最容易翻车的地方在于,执行器必须使用严格的 JSON 解析,对模型输出的内容做完整性校验。我遇到过模型返回的调用参数末尾多了一个逗号这种 JSON 解析失败的情况,所以后来直接让模型输出固定格式的调用标记,再从中提取参数,而不是让它输出一段独立的 JSON。
4. 实战中的高频问题和排查思路
4.1 Agent宁可自己编也不调用已注册技能
这是最让人头疼的问题。代码里明明注册好了技能,描述也写得清清楚楚,但 Agent 在回答时就是不用,硬是凭自己的知识来回答,甚至编造一个不存在的接口。
我排查下来发现,原因通常出在三个阶段。第一是技能列表没有进 Prompt,或者被上下文截断了。第二是描述太抽象,模型根本不知道这个技能什么时候该用。第三是 Agent 根本不知道有“技能调用”这个动作,这常见于没有在 System Prompt 里明确规定任务分工的模型。
针对第二点,我的办法是给每个技能描述加“触发条件”段落,明确写“只在本条件满足时调用,否则不要调用”。你甚至可以加负面描述,比如“不要在没有提供源文件路径时调用”。这能明显压制模型自己发挥的冲动。
4.2 技能粒度过粗导致复用困难
把一个完整业务流程封装成一个技能,是一个很自然的想法,但到后面你会发现它几乎没法重用。比如“处理客户投诉”这个技能,里面包含了读订单、查聊天记录、生成回复草稿、发送通知四步,结果另一个业务场景只想复用“生成回复草稿”,却不得不为此调用整个技能。
我现在的做法是反向设计:先列出一批原子操作,再把常见的两两组合作为技能。原子操作相当于乐高积木,技能相当于拼好的小模型。你去维护得多花点时间,但 Agent 的组合能力会得到极大释放。
关于“一个技能里的代码行数”,我给自己定了一个参考线:超过 80 行的技能逻辑,你要仔细想想是不是该拆成多个技能。这不是硬性规定,但超过这个规模,往往意味着它在做不止一件事。
4.3 参数生成不规范导致的执行报错
大模型生成的技能参数偶尔会跑偏。最常见的有:字符串写成带引号的形式、数字和字符串混用、枚举值不在给定范围内、必填字段缺失。这些错误直接传给执行体,会报出难看的堆栈。
我处理这类问题分两层。第一层是在执行器入口做 JSON Schema 校验,不合法就返回结构化错误信息给模型重试。第二层是在关键字段上做“模糊兜底”,例如枚举值不匹配时,尝试做一次标准映射再决定是否拒绝。比如模型传入format: markdown,而 schema 只接受pdf/docx/html,可以把它当作无法识别的未知格式,而不是直接报错,让模型重新生成。
实践经验告诉我,90% 的参数错误都能通过“提示模型先阅读技能描述中的参数约束,再生成 JSON 调用”来解决。这个提示放在 System Prompt 里就有效,根本不需要复杂的后处理。
4.4 技能执行失败时的兜底策略
技能执行失败是常态,关键是失败后怎么处理。我见过很多团队直接把异常抛给大模型,结果模型也开始胡说八道,说“系统被攻击了”“文件被删了”之类的解释,完全偏离事实。
我的兜底策略是设计一个统一失败返回:{"status": "error", "message": "具体的错误说明"}。模型拿到这个结果后,被明确要求如实向用户说明“当前技能执行失败”,并询问是否需要换个方式继续。这里的关键是,不要让异常信息里包含庞杂的堆栈内容,那会让模型不知所措。
对于一些可恢复的错误,比如“文件被占用”“输出目录无权限”,我会在执行体内自动重试 1~2 次,仍然失败才返回错误。这种自动重试在真实项目里的收益很明显,网络抖动和临时锁冲突大部分都能被自动消化掉。
4.5 多人协作时技能命名和版本冲突
当技能库从一个人维护变成多人协作,命名冲突是早晚的事。我这里踩过很多坑。有人习惯用中文名,有人用英文名,同名不同义的技能甚至同时出现了两份。
我最终确认的规范是:技能名必须使用小写英文连字符命名,且全局唯一;多人协作时,统一在仓库级 registry 文件里登记,新技能必须先登记后开发;变更技能行为必须升版本号,不覆盖旧版本。虽然听起来很工程化,但Agent技能库一旦超过几十个,不规范就是给自己埋雷。
版本管理方面,我会在 registry 里保留旧版本入口一段时间,给调用方足够的迁移期。避免“改个描述老调用方全部失灵”的情况。你可以用一个简单脚本,启动时检查技能库内是否存在重复名称或缺失依赖,有问题直接拒绝启动,这比运行到一半再报错要好太多。
5. 给技能库建设者的几点维护建议
5.1 给每个技能配上可回归的测试
技能库不是写完就完事的,它需要像普通代码一样有测试。我现在的习惯是,每个技能目录下带上一个test_cases.json,里面放 5 到 10 组典型的调用输入和期望输出。每次改动技能描述或执行体后,跑一遍回归测试,确认技能还能在预期场景下被正确触发并返回正确结果。
测试技能描述的方法也比较直接:构造一组用户问题,让模型在只有该技能描述的情况下判断该不该调用,再记录准确率。这个准确率比代码层面的测试更能反映技能的“可用性”,因为它直接衡量了给模型看的接口文字质量。
我在项目里测试过一个只有一句话描述的技能和一个按触发场景模板写的技能,在同一个测试集上的选择准确率差了接近两倍。这件事让我对“写描述比写代码更重要”这句话特别有感触。
5.2 用调用数据反推技能描述优化
技能上线之后,别急着写新技能,先看看已有技能的调用数据。我用的监控指标主要有三个:调用次数、调用成功率、平均延迟。调用次数极低的技能,要么描述没写清楚,要么根本没人需要。长得好看但没人用的技能,对 Agent 来说是负资产,因为每个技能的存在都会增加选型难度。
还有一些技能调用次数高,但成功率不高,这类是优化重点。我一般会翻成功和失败两种情况下的模型调用参数,看看是不是失败都集中在某个常见参数上,如果是,就在描述里加一句该参数怎么填的提醒。
数据驱动的优化,好过凭空猜测。我保留了一个技能修改记录表,每次改描述都会写下“为什么改、期望解决什么”,方便事后回看哪些改动真正起了作用。这种沉淀习惯,比记忆靠谱得多。
5.3 技能下沉:从一次性任务里提取通用技能
最后建议,在日常开发过程中时刻想着“技能下沉”。每接到一个需要写临时脚本的任务,先思考其中有没有可以复用在其他场景的小能力。如果有,就直接按技能规范写成正式技能,然后让 Agent 在后续任务里调用它。
这是知识沉淀价值最高的一个习惯。我很多实用的技能,都是这样从一次性的报表生成、临时数据清洗、单独页面信息抓取里提炼出来的。久而久之,你的技能库就变成了一个“只见过的业务问题”的活档案,Agent 的能力上限也随之上移。
不过也要注意,不要为了下沉而下沉。一个技能如果只在特定项目里有效、跨项目复用价值很低,留在原项目的工具层就可以了。技能库的规模和质量,需要找到一个平衡点。
我个人在实际操作中的体会是,技能库更像一个持续养成的系统,前期的结构设计决定它的上限,后期的维护习惯决定它的下限。你不用在一开始就把所有技能想得完美,更重要是搭好这套“技能化”的方法,让每一个新能力都能以统一的方式沉淀下来。等你积累到几十个技能之后再回头看,没有这套框架和坑位记录,Agent 项目基本是寸步难行。