☰
agent-skills实战:构建可扩展的Agent能力体系
2026/10/14 9:40:20 网站建设 项目流程

1. agent-skills 到底在解决什么问题

做 Agent 开发有一阵子的人,几乎都会撞上同一个尴尬时刻:代码里堆了几十个 if-else 分支,每一个分支都在处理“用户说了什么 → 我该调用哪个能力”,最后这个 Agent 变成了一个谁也改不动的大泥球。刚开始加一两个能力还挺开心,加到第十个的时候,你已经开始怀疑人生了——加一个新函数要小心翼翼,生怕碰坏哪个老逻辑,测试用例越写越长,真正做事的代码反而没多少。

我去年接手的一个项目就有这个问题。当时某跨平台系统里跑着一个客服 Agent,代码里散落着十几个“能力入口”,有的写在主流程里,有的挂在某个回调函数下面,还有两个藏在工具类里。新同学入职第一周,光是把这些入口找齐就花了两天。我当时就想,如果再这么写下去,这个 Agent 迟早要重写。

后来我们做了一次重构,核心思路就一句话:把 Agent 的每一个能力都封装成独立的“技能”,每个技能自带描述、输入输出规范和调用逻辑,Agent 本体只负责理解意图和调度。这就是 agent-skills 的雏形。说白了,它不是某个稀奇古怪的框架,而是一种组织 Agent 能力的方法论加工程实践,让 Agent 的扩展从“改主流程”变成“新增一个技能文件”。

这个方案适合谁?如果你正在写 Agent,不管是聊天机器人、自动化工作流引擎还是企业内部的知识助手,只要你的 Agent 已经出现以下信号——新增一个功能要改动多处代码、能力边界模糊、测试困难、多人协作互相踩脚——那 agent-skills 这套思路就是为你准备的。它能帮你把 Agent 从一座越来越乱的毛线团,变成一张清晰的功能菜单。

2. 技能设计的核心拆解

2.1 技能的颗粒度:一个技能到底该管多大范围

这是所有人在设计技能时第一个纠结的问题。“查询天气”是一个技能,“查询今天的天气并生成穿衣建议”算一个还是两个技能?

我的原则是:技能是能独立完成一次完整交互的最小单元。判断标准很简单——这个能力能否被用户单独要求执行而不需要额外的上下文解释。用户可以说“帮我查下天气”,也可以说“帮我把一个 PDF 压缩”,这两件事本身都能独立成立,所以它们各自是技能。但“查询天气并生成穿衣建议”明显是两个动作的串联,前者是获取数据,后者是结合数据做推演,任何一环失败都不影响另一环独立运作,因此应该拆成两个技能,再由更上层的编排去串联。

颗粒度太粗,技能内部会迅速膨胀成一个小系统,违背了“易扩展”的初衷。颗粒度太细,又会出现一个尴尬场景——Agent 光在技能之间做路由都忙不过来,每个技能干的事又太薄,调度开销反而比实际执行还高。

我自己的习惯是拿到一个需求先写一版技能列表,然后问自己三个问题:

  1. 这个技能能被单独表述成一个完整的用户请求吗?
  2. 这个技能内部的输入输出是否自洽,不依赖外部状态?
  3. 如果未来有类似的请求,我是愿意新增技能还是复用现有技能?

第三个问题尤其关键。当你犹豫“这个新需求能不能用已有技能改改实现”时,答案通常是可以,那就先不要新建。技能是服务用户的,不是用来凑数的。

2.2 技能描述:写给模型看的说明书

技能描述是整个 agent-skills 设计里最容易被低估的部分。很多人以为描述就是写给人看的注释,其实在 LLM 驱动的 Agent 架构里,技能描述是模型决定“该不该调用这个技能”的唯一依据。

一个合格的技能描述至少包含三块内容:这个技能做什么、什么时候应该被调用、什么时候不应该被调用。注意,第三块很多人会漏掉。但实操中我见过太多误调用,根源都是没写“什么时候别用”。比如你有一个“生成二维码”技能,一个“生成图片”技能。如果“生成二维码”的描述里只写“生成二维码”,模型完全有可能收到用户“给我做一个图片”的需求时,先去看看“生成二维码”的输入参数合不合用。如果你在描述里加一句“仅在显式要求二维码或可扫码的图形码时使用,不用于普通图片生成”,误调用的概率会大幅度下降。

另外,描述里一定要写清楚输入参数的约束。比如“生成二维码”技能接收一个 text 参数,如果描述里不写清 text 的最长长度限制,模型可能把一个两万字的文档整个塞进来,然后你的二维码生成库直接抛异常。这些约束写在描述里,模型在生成参数时就会主动做截断或者提示用户。

一个实测定下来的描述模板长这样:

技能名称:convert_pdf_to_text 功能描述:将 PDF 文件内容提取为纯文本。 触发条件:用户明确要求从 PDF 中提取文字、将 PDF 转文本、或对 PDF 内容做文本分析。 不触发条件:用户仅提到 PDF 元信息(页数、大小)、或需要保留原始排版的场景(应转 Word/图片)。 输入参数: - file_path:PDF 文件路径,必填。 - password:加密 PDF 的密码,可选,如文件无密码可不填。 输出:纯文本内容,按原文档分页顺序排列。

这套写法看着啰嗦,但在真实运行里,它的准确率比那种只写一句话的技能描述高出一大截。因为模型本质上是靠文字做推理的,你给它越清晰的决策边界,它就越少做让你意外的选择。

2.3 输入输出规范:技能之间通信的通用协议

技能不是孤岛。在真实项目里,“查天气”的技能查完数据可能要交给“穿衣建议”的技能用,“读取文件”的技能可能要给“内容总结”的技能喂数据。技能之间能不能顺利协作,取决于它们的输入输出是不是对得上。

我在项目里定了一条硬规矩:所有技能的输入和输出必须使用 JSON 结构,并且每个技能都要声明自己的输出 schema。为了一条规矩,团队里当时吵过一轮,有人说“技能内部返回类对象不是更省事吗”,后来大家统一了——用 JSON 不是为了性能,而是为了通用性。

LLM 生成的参数天然就是 JSON 格式,技能输出的结果如果也是 JSON,那么下一个技能可以直接把输出当作上下文利用,不用额外做序列化转换。更关键的是 JSON 带 schema 的话,可以随时校验输出是否符合预期,在调试时一眼看出问题出在哪个环节。

实际项目中一个标准的技能输出模板:

{ "status": "success", "data": { "content": "提取出的文本内容" }, "meta": { "page_count": 12, "file_size": 348921 } }

字段越少越好,但是状态字段必须保留。因为调度层要判断技能是否成功执行,靠的是 status,而不是有没有返回值。很多新手容易犯的错是技能失败了就什么都不返回,结果调度层拿着 null 去处理,整个链路懵了。

2.4 技能的注册与发现机制

技能写好了,怎么让 Agent 知道它有这个技能可以用?这里有一个注册与发现机制的问题。

最简单的方案是把所有技能的描述放在一个列表里,一起塞进系统提示词。技能少的时候——比如十几个——这个方案完全可行,模型选择起来也快。可当技能数量涨到几十个上百个,全塞进去有两个问题:提示词被撑爆,模型的注意力被稀释,选错技能的概率反而升高。

所以当技能数量超过二十个,我建议做一层技能检索。它的原理很简单:把所有技能的名称和描述向量化,用户的请求进来先做一次相似度检索,选出与该请求最相关的 N 个技能,只有这些技能的完整描述才被拼进提示词。这实际上是把“模型在几百个技能里做选择”的问题,降维成“模型在五六个技能里做选择”,准确率提升非常明显。

技能注册表我习惯用一张表来维护:

技能名称描述摘要输入 Schema输出 Schema启用状态版本
convert_pdf_to_textPDF 转纯文本file_path, passwordcontent, page_countenabled1.3.0
summarize_document长文摘要text, max_lensummaryenabled2.0.1

这张表就是 Agent 的能力清单。每一次新增技能只需要往表里加一行,不需要动任何主流程代码。

3. 从零落地一套 agent-skills 体系

3.1 最小可运行版本:先跑通再优化

现在把上面的设计落成代码。这里我以一个 Python 实现为例,因为 Python 生态在 Agent 领域最成熟,大家也最容易看懂。先定义技能基类:

from abc import ABC, abstractmethod from typing import Any, Dict, Optional import json class BaseSkill(ABC): """技能基类,所有技能需继承此类。""" name: str = "" description: str = "" parameters: Dict[str, Any] = {} output_schema: Dict[str, Any] = {} @abstractmethod def execute(self, params: Dict[str, Any]) -> str: """执行技能,返回 JSON 字符串。""" pass def to_manifest(self) -> str: """生成技能清单条目,供调度层决策。""" manifest = { "name": self.name, "description": self.description, "parameters": self.parameters, } return json.dumps(manifest, ensure_ascii=False)

然后拿一个真实技能练手。做个刚才提到的 PDF 转文本技能:

import pdfplumber import json class ConvertPdfToText(BaseSkill): name = "convert_pdf_to_text" description = ( "将 PDF 文件内容提取为纯文本。" "当用户明确要求从 PDF 中提取文字、转为文本、或对 PDF 内容做文本分析时使用。" "不要用于获取 PDF 的元信息,如页数、文件大小。" ) parameters = { "type": "object", "properties": { "file_path": { "type": "string", "description": "PDF 文件路径,必填。" }, "password": { "type": "string", "description": "加密 PDF 的密码,可选。" } }, "required": ["file_path"] } def execute(self, params): file_path = params["file_path"] password = params.get("password", None) try: with pdfplumber.open(file_path, password=password) as pdf: pages = [page.extract_text() or "" for page in pdf.pages] content = "\n\n".join(pages) return json.dumps({ "status": "success", "data": {"content": content}, "meta": {"page_count": len(pages)} }, ensure_ascii=False) except Exception as e: return json.dumps({ "status": "failed", "data": {}, "meta": {"error": str(e)} }, ensure_ascii=False)

注意到 execute 的返回值统一是 JSON 字符串。这是有意为之,让技能调用方只需要处理字符串,不需要关心 Python 对象,也不要求所有技能用同一种语言实现。将来你用 Node.js 写某个技能也可以,只要它遵循同样的 JSON 契约。

3.2 技能管理器:注册、检索、调度都在这里

技能有了,需要一个管理器来统一维护。这个管理器要做三件事:注册技能、根据用户请求选择合适的技能、把请求委派给技能执行。第一版先实现一个简单版本:

from typing import Dict, List, Optional, Tuple class SkillManager: def __init__(self): self._skills: Dict[str, BaseSkill] = {} def register(self, skill: BaseSkill) -> None: if skill.name in self._skills: raise ValueError(f"技能 {skill.name} 已存在,请更换名称。") self._skills[skill.name] = skill def get_manifest(self) -> str: """生成全量技能清单,用于简单调度。""" manifests = [skill.to_manifest() for skill in self._skills.values()] return json.dumps(manifests, ensure_ascii=False, indent=2) def execute(self, skill_name: str, params: Dict[str, Any]) -> str: if skill_name not in self._skills: return json.dumps({ "status": "failed", "data": {}, "meta": {"error": f"未找到技能 {skill_name}"} }, ensure_ascii=False) return self._skills[skill_name].execute(params)

这个版本可以作为最小可运行基础。把它接到你的 Agent 上,用户请求进来后,你把 get_manifest 的内容塞进系统提示词,让模型输出它想调用的技能名和参数,然后调用 execute 执行,把结果再返回给模型生成最终回复。

跑通这个链路之后,你就已经具备 agent-skills 的基本骨架了。后续所有优化都是在管理器层面做文章。

3.3 引入技能检索:技能多了才懂它的好

刚才说过技能数量多的时候要加检索。这里补一个使用向量检索的改造版。假设技能数超过 20 个,全量清单会让模型的决策质量下降,于是给管理器加一个检索方法:

import numpy as np from sentence_transformers import SentenceTransformer class SkillManagerWithRetrieval(SkillManager): def __init__(self, top_k: int = 5): super().__init__() self._encoder = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") self._embeddings: Dict[str, np.ndarray] = {} self.top_k = top_k def register(self, skill: BaseSkill) -> None: super().register(skill) self._embeddings[skill.name] = self._encoder.encode(skill.description) def retrieve(self, query: str) -> List[str]: """返回与用户请求最相关的 top_k 技能名。""" query_emb = self._encoder.encode(query) scores = { name: np.dot(query_emb, emb) / (np.linalg.norm(query_emb) * np.linalg.norm(emb) + 1e-9) for name, emb in self._embeddings.items() } ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [name for name, _ in ranked[: self.top_k]] def get_manifest(self, query: str) -> str: """根据 query 动态生成局部技能清单。""" names = self.retrieve(query) manifests = [ self._skills[name].to_manifest() for name in names if name in self._skills ] return json.dumps(manifests, ensure_ascii=False, indent=2)

这里用了一个开源的轻量级 embedding 模型,对中文和多语言的支持都不错。每次查询做一次向量化,成本很低,换来的是模型每次只面对 5 个左右技能的描述,选错的概率明显下降。

有一点提醒,向量检索召回的技能不一定覆盖所有必要选项,所以我的做法是检索结果里固定留一个位置给“通用能力”——比如系统内置的“对话回退”技能,防止模型在检索不到合适技能时只能硬答。这部分在第四节的常见问题里会再展开。

3.4 技能编排:从一个技能到一条工作流

单个技能只能响应单一请求,但真实场景里很多请求需要多个技能协作。以“帮我把这周的销售周报生成摘要并保存到云端”为例,它至少涉及三个步骤:解析附件或读取数据源、生成摘要、保存到云端。

如果这个流程每次都要模型一步步规划,既慢又不稳定。更务实的做法是把固定流程固化为一个“编排技能”——它本身也是一个技能,只不过它的 execute 内部会调用其他技能。

class WeeklyReportPipeline(BaseSkill): name = "weekly_report_pipeline" description = ( "处理销售周报:读取周报数据、生成摘要、保存到知识库。" "当用户提到销售周报、周报摘要、周报归档等场景时使用。" ) def __init__(self, manager: SkillManager): self.manager = manager def execute(self, params): report_id = params["report_id"] # 第一步:读取数据 read_result = self.manager.execute("read_report_data", {"report_id": report_id}) read_data = json.loads(read_result) if read_data["status"] != "success": return read_result # 第二步:生成摘要 summarize_result = self.manager.execute( "summarize_document", {"text": read_data["data"]["content"], "max_len": 500}, ) summary_data = json.loads(summarize_result) if summary_data["status"] != "success": return summarize_result # 第三步:保存到知识库 save_result = self.manager.execute( "save_to_knowledge_base", {"title": f"周报摘要-{report_id}", "content": summary_data["data"]["summary"]}, ) return save_result

这个写法最巧的地方在于:编排技能本身不关心每个子技能内部怎么实现,它只依赖 manager 的 execute 接口。如果以后 read_report_data 从本地文件改为读取数据库,只要输入输出不变,编排技能一行都不用改。

其实这就回答了一个很多人纠结的问题——该不该让编排逻辑也走 LLM 决策。我的经验是分成两类:如果流程非常固定,比如上面的三步流程,就用代码写死,省时省力;如果流程本身变化多端,比如“帮我整理一下这些材料”,那应该交给模型动态规划技能调用顺序,而不是硬编码。两条路各有适用场景,关键是别用错地方。

4. 局部技能描述与全局策略的配合

4.1 写技能描述时容易忽略的决策边界

技能描述写得好不好,最直接的体现是模型误调用的频率。前面提到要写“不触发条件”,这里补充几个实战中踩过的细节坑。

第一个坑是参数的默认值描述不清楚。比如有个技能“send_email”,参数里有一个 is_urgent 的布尔值。如果你在描述里不写“默认 false,仅在用户明确表示紧急时设为 true”,模型极有可能在用户说“帮我把这份文档发给王工”时,自作多情地加上 is_urgent=true。一次两次没问题,发多了你就知道痛了。

第二个坑是输出里的字段命名随意。技能输出可能被另一个技能或模型读取,字段名如果起得不够直白,后续理解成本会上升。我统一过一套命名规范:输出字段一律小写加下划线,禁止缩写。比如 content 不写成 cnt,page_count 不写成 pc。保证人看得懂,模型也看得懂。

第三个坑是描述里夹带业务黑话。团队里有几次把“T+1”“SLA”“排期”这类词直接写进技能描述,结果模型在调度时对用户的一般化表述完全对不上。后来规定,技能描述面向模型,必须使用通用语言解释业务术语,比如“T+1”要说成“数据在次日生效”。

4.2 兜底策略:Agent 找不到合适技能时怎么办

无论你的技能清单多完整,总会有用户需求超出能力范围。这个场景如果不做兜底,Agent 可能会做一件很危险的事:硬选一个不匹配的技能来执行。

我见过一个真实事故。某自动化 Agent 没有兜底逻辑,用户说“帮我把 PPT 转成 Word”,Agent 居然选中了“图片转文字”技能,然后一本正经地“提取了”PPT 的文字内容,返回给用户。用户看到结果当场无语。

兜底策略其实很简单。在调度提示词里固定加一段话:

如果用户请求不在任何已提供技能的适用范围内,请明确告知用户“我暂不支持该功能”,并推荐用户使用最接近的能力,或引导用户将请求拆解为可支持子任务。切勿选择不匹配的技能强行执行。

这段话看似普通,但配合技能清单使用,能减少至少一半的“乱来”行为。还有一种兜底是技能层面提供一个 fallback 技能,专门处理无法路由的请求,返回一个固定的结构化提示,让前端可以展示友好的引导界面。

4.3 先小范围试运行再逐步放量的节奏

最后提一下落地的节奏。一套 agent-skills 体系刚上线时,千万不要一次性放开所有技能。我的做法是用灰度策略:先选 3 到 5 个核心技能跑两周,记录模型选择技能的准确率、误调用次数、用户反馈。

这两周足够你发现一批描述上的问题。比如某个技能经常被选中但执行成功率不高,那大概率是触发条件写得太宽;某个技能几乎从没被选中,可能就是描述写得和用户语言对不上。修正之后,再逐步开放下一批技能。

这个“小步快跑”的方式比一上来全量上线稳妥得多。全量上线最麻烦的是问题交织在一起——你说不清楚到底是描述问题、检索问题还是执行问题,排错要花好几倍的时间。

5. 技能评估:凭什么说这套体系是好的

5.1 一套轻量级的技能质量打分方法

技能上了线,总得知道它好不好用。这里分享一套我在项目里实际用的评估方法,不需要额外搭测试平台,只需要一个测试脚本加一份精心设计的测试用例。

测试用例每组包含三个字段:用户请求、期望调用的技能名、期望返回的 JSON 参数。比如:

用户请求:帮我提取一下合同扫描件的文字 期望技能:convert_pdf_to_text 期望参数:{"file_path": "contract.pdf"}

把几十组用例跑一遍,统计三个指标:

  • 技能选择准确率:模型选中的技能名称是否和期望一致,占比多少。
  • 参数生成准确率:技能选对了,参数是否符合 schema,占比多少。
  • 执行成功率:技能 execute 是否返回 status=success,占比多少。

这三个指标中,选择准确率依赖描述质量,参数准确率依赖 schema 的严谨度,执行成功率暴露的是技能内部实现问题。任何一环有问题,一眼就能定位到环节。

5.2 用评估结果反哺技能描述

评估最大的价值不在于看数字,而在于用数字驱动改进。举一个真实的例子,我们最初“convert_pdf_to_text”的选择准确率只有 78%,一批用户请求“帮我把 PPT 转成文字”也被路由到了这个技能。看了失败用例,发现是描述里没有写明“不处理 PPT、Word 等非 PDF 格式”。加上这句话后,准确率提升到 94%。

所以我的建议是每次调整描述后都重新跑一遍测试集,把准确率变化记录下来。描述写得好不好,不要凭感觉,拿数据说话。

评估频率上,每次新增技能或者修改描述都跑一遍全量测试。技能没变动时,每周跑一次回归就够了。不要求复杂,关键是坚持积累测试用例,用例库本身就是你团队的一笔资产。

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

6.1 模型总是选错技能?

这是最让人头疼的问题。排查思路按照下面这个顺序来:

先看技能描述里有没有明确的不触发条件。八成的情况是触发条件写得太宽,模型把相似但不同的请求都路由过来了。给每个技能都明确写清“不要用什么场景”,很多误调度直接消失。

如果描述已经没问题,再检查技能之间是否存在描述重叠。两个技能如果描述里有相似的触发场景,模型就会随机选一个。这时候要么调整技能边界,让两个技能的场景完全分离,要么考虑把它们合并成一个技能,在内部按参数分流。

最后一步才是怀疑模型能力。如果前面都没问题但准确率还是低,可以换更强的大模型试试。不过说句实话,大多数项目到这一步的准确率已经 90% 以上了,真正需要靠换模型解决的问题非常少。

6.2 技能执行成功了,但结果不是用户想要的

这种情况很隐蔽,因为从日志看一切都正常——技能被正确选中,参数也合法,执行也没抛异常,可用户就是不满意。我遇到过一次,原因是技能内部实现理解错了需求。

比如“summarize_document”技能,接收 text 和 max_len 两个参数,内部返回了第一段话的前 500 字——这能算摘要吗?代码没报错,但产出完全不能用。这种问题靠自动化测试很难发现,需要靠人工抽样检查输出质量。

解决方法是给摘要、生成、改写这类“产出型”技能增加简单的内容质量校验。比如摘要技能可以校验返回内容的长度是否在期望范围内、是否和原文重复度太高。这些校验规则不复杂,但能挡住一部分明显低质量的输出。

6.3 技能越来越多,注册表越来越乱

如果团队里有好几个人都在往注册表里加技能,“重复造轮子”和“命名混乱”就是一定会发生的事。我们项目里出现过两个技能“get_stock_price”和“query_stock_quote”,功能几乎一样,就因为一个人写了英文名一个人写了半英文半拼音。

后来立了几条规矩,这个问题基本绝迹:

  1. 技能命名统一用动词开头的小写英文,如 fetch_user_info 而不是 user_info。
  2. 新增技能之前必须检索现有技能描述,如有功能重合,要么复用要么先合并。
  3. 注册表加一个 review 环节,由固定的人负责审核新技能的描述与 schema。

这三条规矩没有一条是技术难度大的,但对长期维护的帮助极大。技能体系越往后越拼规范,不拼技术。

6.4 向量检索召回不准怎么办

如果你的技能数量特别多,向量检索召回不准确的问题就会暴露出来。排查步骤也很有套路。

第一步,检查技能描述是否足够长且独特。描述只有一句话的技能,向量表示的信息量太少,任何请求都可能和它有相似的余弦相似度。把描述扩充到包含触发场景、输入约束、功能说明的三段式结构,召回质量会立刻改善。

第二步,检查 embedding 模型是否适合你的语言。有些模型对英文效果好但对中文效果一般。如果项目以中文为主,选多语言模型。

第三步,人工检查几个失败例子的召回结果,看看排在前面的技能和用户请求差在哪。如果是语义理解问题,可能需要换更强的模型。如果是技能描述本身有问题,那就补描述。

实际上,把描述写充分之后,90% 的召回问题都能解决。向量模型本身的差异在技能文本质量面前,优先级并不高。

7. 我的一些实操心得

这套 agent-skills 的体系从最初一个粗糙的技能列表,到后来变成项目里所有 Agent 的统一底座,前后迭代了三四个月。回过头看,我最大的体会是:这个方案的难点从来不在技术实现,而在设计和约束。

技术上,注册一个技能、调用一个技能,半小时就能学会。真正花时间的,是想清楚每个技能的边界在哪里、描述怎么写才能让模型少犯错、不同技能之间怎么协作才能不互相干扰。这些东西都是磨出来的,不是一蹴而就的。

给准备上手的朋友几个建议:

第一,不要追求一次设计完美。先挑两三个场景落地,跑起来你才会发现哪里描述不清、哪里边界不对。改两轮之后,你对“技能该怎么拆”会有完全不同的理解。

第二,测试集要从第一天就开始积累。每遇到一个模型答错的案例,就把它加入测试集。这个测试集会越变越值钱,将来任何一次调整都可以用它来验证。

第三,技能描述是面向模型的,不是面向文档评审的。下次写描述的时候,试着把自己想象成一台语言模型——只看这段文字,你会不会知道什么时候该调用这个技能?

这个方向后续还能继续扩展。比如技能之间的依赖关系自动检测,技能质量的数据化看板,甚至是技能描述的自动生成。但无论如何扩展,核心的思想不会变:Agent 的能力建设,应该像搭积木一样,简单、清晰、可组合。这也是我对 agent-skills 最大的期待。

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

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

立即咨询