前面有两三个月,我一直在折腾一个 Agent 平台,最头疼的事情就是:模型表现飘忽不定。同一个任务,把 prompt 写长一点,效果好一点;换个表达,模型直接答非所问。后来我把思路从"把 prompt 调得更准"转成"给 Agent 配上可复用的技能模块"——也就是项目标题里那套 agent-skills 的玩法,效果立刻不一样了。
这个方向说白了就是:把 Agent 能做的每一类事情,都封装成独立的"技能"——每个技能有自己的描述、参数说明、执行逻辑和校验规则。Agent 接到需求后,先判断该用哪个技能,再按照技能的约定去执行。相比把所有指令塞进一条大 prompt,这种拆法让行为边界清晰得多。
这篇文章把我自己在技能定义、注册、调用、校验以及多技能协作上踩过的坑和沉淀下来的方法统一整理一遍,重点讲清楚"技能"和"普通工具函数"的差异、技能描述怎么写触发率才高、一次完整的技能调用链路长什么样、多技能之间怎么编排。正在做 Agent 应用、或者准备给现有系统接入"技能库"思路的开发者,应该能直接照着搭一套。
1. 为什么 Agent 需要一套"技能"体系
先聊一个很多团队会忽略的问题:Agent 本身并没有"能力"这个概念,它只有"模型生成文本"这个基本面。你给它一个 API 列表,它知道能调哪些接口;但你让它"自动完成数据分析并生成图表",它就得自己编排多步动作,而这些动作的编排逻辑如果只靠 prompt 约束,结果就非常不稳定。
技能体系要解决的就是这层"编排可控性"问题。一个技能不是单步工具调用,而是一条完整的、可复用的业务处理路径,里面可以包含模型推理、外部工具调用、结果校验、失败重试。用生活里的事情打比方:普通工具是一把螺丝刀,技能是"修门的标准化流程"——先检查门锁、再判断螺丝规格、然后使用对应工具、最后测试开合是否顺畅。前者只负责某个动作,后者负责完成一个目标。
1.1 技能与工具的本质区别
很多刚接触这个概念的开发者,会把技能的每一项都设计成"一个函数对应一个技能",比如"获取天气"是一个技能、"查询汇率"是另一个技能。这样做没有错,但并没有把"技能"的价值发挥出来。
我建议把技能定义成"面向目标的能力单元"。"获取天气"只是一个动作,它不需要模型做什么判断;而"根据天气安排出行计划"才是一个技能,因为它的内部会做条件判断、会调用天气接口、会根据结果生成方案。真正值得封装的,是这类"需要模型参与决策"的完整流程。
实际落地时,我会用三个维度来区分一个模块到底是不是技能:
- 是否包含意图判断逻辑。技能入口要能识别"用户当前想做的事是否属于本技能范围"。
- 是否有完整输入输出约定。参数、输出结构必须明确,不能靠模型自由发挥。
- 是否带结果校验和纠错机制。执行完不算完,要能检查结果是否符合预期,不符合就重试或报错。
如果一个功能只满足前两条、不满足第三条,那它顶多算一个"工具",可以直接挂在技能内部当子模块用,没必要单独暴露给 Agent。
1.2 技能描述决定 Agent 用不用它
同样一个技能,描述写得模糊还是精确,Agent 的选择结果天差地别。我之前做过一次对比实验:同一个"生成周报"技能,A 版本的描述是"生成周报",B 版本是"当用户需要总结本周工作并生成周报文档时使用,输入需要包含本周主要事项,输出为结构化周报文本,适合发送至工作群"。B 版本的触发准确率从 42% 提升到了 88%。
这个差距的原因在于,Agent 在大部分框架里的技能选择本质上是一次语义匹配。它拿到用户的话,结合每个技能的名称和描述,计算相似度或让模型做出选择。描述里如果缺少触发场景、输入要求、输出格式这些信息,模型就无法判断"现在该不该用它"。
描述写作我有几个比较实操的要点:
- 开头第一句话说清"什么场景下触发",比如"当用户需要...",不给模型留猜测空间。
- 中间写清参数要求,尤其是用户没直接给全参数时,模型是应该向用户追问还是自行推断。
- 结尾写清输出形式,比如"返回 Markdown 格式"、"返回参数名和值"。
- 描述里可以顺带说明该技能不会做什么,减少和其它技能的冲突。
2. 技能注册与调用链路的完整设计
聊完了概念,进入最关键的落地环节:技能库在工程上应该怎么组织。我的做法是把整个系统拆成四层:技能注册层、路由匹配层、执行调度层、验证反馈层。
这四层对应一次技能调用的完整生命周期。每一层都有独立的职责,不能混在一起。如果哪个团队把所有逻辑都塞在一个模块里,技能一多必然失控。
2.1 技能 Schema 定义与注册流程
先看技能本身的数据结构。我早期用的就是简单的 JSON 配置,后来发现字段不够用,逐步加成了下面这套结构:
{ "schema_version": "1.2", "skill_name": "weekly_report_generator", "display_name": "周报生成器", "description": "当用户需要总结本周工作并生成周报时使用,输入为本周工作内容,输出为结构化周报文本", "author": "agent-platform", "version": "0.3.1", "tags": ["report", "weekly", "automation"], "input_schema": { "type": "object", "properties": { "work_items": { "type": "array", "items": { "type": "string" }, "description": "本周完成的工作事项列表,每项一句话" }, "date_range": { "type": "string", "description": "周报所属时间范围,格式YYYY-MM-DD至YYYY-MM-DD" } }, "required": ["work_items"] }, "output_schema": { "type": "object", "properties": { "report": { "type": "string" }, "summary": { "type": "string" } } }, "execution": { "type": "local_python", "entrypoint": "skills/weekly_report_generator/main.py" }, "validation": { "type": "schema_check", "check_output": true } }注册一个技能,就是把这个结构注册到技能中心。技能中心本质上是一个带有索引能力的存储服务,它维护技能列表、技能版本、启停状态。Agent 启动时拉取技能列表,更新自己的可用技能集合,这个过程叫技能加载。
注册的时候有几个容易踩坑的地方:
- 技能 ID 一定要全局唯一。不要用容易重复的名字,比如"report_generator"太泛了,建议"weekly_report_generator"这种带业务属性的。
- 版本号要严格管理。技能逻辑更新后,老版本不能直接删,要保留一段时间,防止线上 Agent 还在用旧技能时突然崩掉。
- 描述里的特殊字符要转义。JSON 里如果描述包含引号,解析很容易出问题,最好在录入时做一次规范化。
2.2 调用时的路由匹配机制
技能注册好了,Agent 收到用户消息后,怎么知道该用哪个技能?这个环节我把它叫作路由匹配。目前主流做法是"模型调用 + 规则兜底"双通道。
先说模型调用通道。Agent 把用户消息、当前对话上下文、可用技能列表(每个技能的 name、description、input_schema)打包成一个选择任务,交给模型决策。模型返回一个结构化的技能选择结果,包括"技能名称+填充好的参数"。
这个过程在模型侧实际上做的是"意图识别+字段抽取"两件事。意图识别指从消息里理解用户要做什么,字段抽取指从消息里找出技能需要的参数值。比如用户说"把本周的工作整理成周报",模型应该理解意图是生成周报,抽取到 work_items 参数可能是"本周完成了A项目上线、B方案评审、C文档编写"——即使这些内容分散在几句对话里。
再看规则兜底通道。模型选择偶尔会失误,尤其是模糊请求或者用户没说清需求时。我在系统里加了一套轻量级规则匹配:提前给每个技能定义几个高频触发词,比如"周报"、"总结"、"汇总",当模型置信度低或者直接没返回结果时,用触发词做粗匹配,再让用户确认是不是要做这件事。
两条通道合起来的流程大致是这样:
- Agent 接收消息,先走规则通道,判断是否有强触发的技能。
- 有则直接锁定候选技能,无则交给模型进行语义匹配。
- 模型返回推荐技能和参数,系统做参数 Schema 校验。
- 校验通过则进入执行层,失败则向用户追问缺失参数。
- 执行完成后,进入验证层检查结果是否合理,不合理则尝试二次修正。
这套机制跑下来,技能选择的准确率基本能稳定在 90% 以上。对比之前纯靠模型选择的方式,稳定性提升非常明显。
3. 实操:从零实现一个高质量技能模块
前面讲的是设计框架,这一节用一个具体的例子,把技能从写代码到跑通的完整过程走一遍。我选的例子是"SQL图表生成技能",这个技能同时涉及外部数据查询、模型推理和结果格式化,能比较好地覆盖技能的各种典型细节。
3.1 技能内部结构拆解
假设我们要做的技能是:接收用户的问题,从数据库表 products 和 sales 中查询数据,生成分析结论,并用文本描述一张可视化图表。
这个技能的内部结构可以分为三个文件:
main.py:技能入口,负责参数接收、调用工具、组织输出。tools.py:内部工具模块,封装数据库连接、查询函数、图表配置生成函数。config.json:技能元数据,和前面注册用的 Schema 归为同一个文件,方便同步。
main.py 的简化实现:
import json from tools import query_database, generate_chart_config def execute(input_params: dict) -> dict: # 第一步:解析输入参数 question = input_params["question"] # 第二步:让模型把自然语言问题转换为SQL并查询数据库 sql = generate_sql_from_question(question) data = query_database(sql) # 第三步:让模型根据数据生成分析结论和图表配置 analysis = analyze_data(question, data) chart_config = generate_chart_config(data) return { "analysis": analysis, "chart_config": chart_config, "data_rows": len(data) }这里有一个关键设计:技能内部也会多次调用模型。一次执行里,模型既要做 SQL 生成,又要做数据分析,还要做图表设计。每一步都是独立的小模型推理任务,分开写会让技能更可控。这也是技能和普通函数最不一样的地方——函数是一步到位,技能是"多步推理+工具调用"的组合编排。
3.2 技能装载与动态更新
代码写好后,要放到技能目录里,由技能中心统一装载。我的项目里用的目录结构比较直接:
skills/ ├── sql_chart_analyzer/ │ ├── config.json │ ├── main.py │ └── tools.py ├── weekly_report_generator/ │ ├── config.json │ ├── main.py │ └── utils.py └── __init__.py装载逻辑可以理解成动态导入。技能中心扫描 skills 目录下的每一个子目录,读取 config.json,注册技能信息,然后把 main.py 里的 execute 函数绑定到技能 ID 上。
import importlib.util import json from pathlib import Path def load_skill(skill_dir: Path): config_path = skill_dir / "config.json" config = json.loads(config_path.read_text()) main_path = skill_dir / "main.py" spec = importlib.util.spec_from_file_location( config["skill_name"], main_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return { "config": config, "execute": module.execute }动态装载最大的好处是技能更新不用重启整个 Agent 服务,我只需要在技能中心点击"刷新"或者收到 Git 提交后自动重新加载。这个对线上推理服务特别重要,因为重启往往意味着中断现有会话。不过要注意一点:重新加载时要清理缓存,防止旧版本技能函数对象残留。我之前就遇到过一次,技能代码明明改了,线上跑的还是老逻辑,排查了半天才发现是 import 缓存没清干净。
3.3 技能测试与回归方法
技能写完之后一定要做测试。我见过很多开发者的技能逻辑本身没问题,但测试覆盖太少,导致上线后各种边界情况处理不了。
测试技能和测试普通函数不太一样,重点要覆盖三类输入:
- 正常需求输入。比如"帮我分析A产品最近30天的销售趋势",预期是返回包含销售结论和变化趋势的分析结果。
- 缺少必要参数。比如用户只说"分析销售数据",没说产品和时间范围,预期是技能返回参数追问,而不是直接报错。
- 意图不匹配。比如用户问"今天天气怎么样",预期是技能不触发,由路由层交给其它技能处理。
针对第一类输入,要检查返回的结论是否基于真实查询数据,而不是模型凭空生成的。实操中我经常发现:模型分析出"销量下降",但实际查询到的数据是增长。这种问题根源在于 SQL 生成或被截断、或查询条件写错。后来我在验证层里加入"结论与数据一致性检查",先让模型基于真实查询结果生成结论,再做一次反向核验,显著降低了错误输出率。
技能回归测试我用的是"固定测试集+人工抽审"的组合。固定测试集里放 30 条左右覆盖各种场景的输入,每次技能代码有改动就跑一遍,看输出结构、触发结果是否仍然正确。模型相关的结果没法做到全部自动化判定,就抽 20% 人工看一下质量。这套组合已经能满足大部分场景。
4. 多技能编排与协作细节
单个技能能工作之后,下一个要解决的是"多个技能一起干活"。复杂的用户需求往往不是一个技能能覆盖的,比如"分析本季度销售数据,并生成一份包含结论、图表的周会汇报文档",这至少涉及数据分析技能、图表生成技能、报告生成技能三个模块。
4.1 技能编排器的实现思路
我实现过一个轻量级的技能编排器,思路是把它当作一个"微型的任务分解器"。它不做实际执行,只做计划编排。编排器接收用户需求后,由模型拆分成子任务,每个子任务匹配到具体技能,技能之间通过一个共享的上下文传递数据。
实际用下来,编排器的拆分策略有两种:
- 串行拆分:上一个技能的输出作为下一个技能的输入。最适合"分析→生成报告"这种流程性任务。
- 并行拆分:多个技能互不依赖,同时执行最后汇总。比如"同时查询不同部门的销售数据",并行处理能大幅缩短用时。
无论是哪种拆分,模型都需要看到每个技能的输入输出接口约定。这要求技能的元信息在编排阶段就是可读的,而不是等到执行阶段才拉取。我在系统里把技能列表做成了可注入的上下文,编排器调用模型时,把这个上下文当成"可用资源"传进去,让模型基于资源做拆解。
4.2 技能间上下文传递与状态管理
技能之间的数据传递,我最开始用了简单的字典传递,后来数据量大了,字典结构越来越乱,就换成了带命名空间的上下文对象。每个技能执行后,把输出写入上下文的特定命名空间,比如ctx["sql_chart_analyzer"],其它技能通过这个命名空间读取。
命名空间规范我做得很严格:只能写入自己的技能名下,读取时可以跨技能读。这样避免了一个技能不小心覆盖另一个技能的数据。对于长期会话,上下文还要支持序列化和持久化,防止中途 Agent 服务重启导致对话状态丢失。
还有个小细节:传递的数据如果是表格类数据,一定要带结构描述。技能分析完数据后,输出的不能只是一堆数值,最好带上字段名和含义说明,不然下一个技能根本不知道这些数值代表什么。这个习惯能省掉很多后续的排查和沟通成本。
5. 常见问题与技能库维护建议
技能上线之后,真正的"运营"工作才刚刚开始。这里把我在技能开发和使用中遇到最多的问题整理成一张速查表,然后单独聊一下技能库的长期维护心得。
5.1 典型问题与解法速查
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 某个技能的触发率突然下降 | 新技能描述与旧技能描述语义重叠,模型被带偏 | 检查最近注册的技能描述,给重叠技能补充"不处理"边界说明 |
| 技能选中了但参数缺很多,反复让用户补齐 | 输入 Schema 描述不够明确,模型不知道哪些参数可以从上下文里推断 | 在参数描述里写明"如果对话上下文中包含该信息,请直接提取填充" |
| 技能执行成功但结果明显不符合用户需求 | 技能内部的模型推理步骤没有校验机制 | 给技能每一步模型推理加结果校验,输出不符合预期则重新推理一次 |
| 技能 A 执行后,技能 B 拿到的数据格式对不上 | 技能间上下文没有统一的结构约定 | 检查是否为技能输出定义了标准 JSON 结构,并正确使用命名空间 |
| 重新加载技能后老版本还在生效 | import 缓存或技能中心缓存未清理 | 确认技能中心是否按版本清理缓存,必要时使用不带缓存的动态导入方案 |
5.2 技能库长期维护的实操经验
技能库不是一次性搭完就能一直稳定运行的。随着技能数量增长,会遇到几个共性挑战:
一是技能数量膨胀。当技能超过 30 个之后,路由匹配的准确率会下降,因为模型在"看完"这么多技能的描述时,注意力会被稀释。这时候需要做技能分组和权限控制。我在系统里引入了"技能空间"的概念,不同业务空间挂不同技能集合,比如数据分析空间只挂分析类技能,汇报空间只挂文档生成技能,路由只在一个空间内做匹配。
二是技能版本演进。每个技能都应该有明确的版本日志。我的习惯是每次更新配一个简短的变更描述,比如"新增自动识别日期范围"、"修复数据量过大时的缓存问题"。这样回归测试出问题时能快速定位是新功能引入的 bug 还是和旧逻辑冲突。
三是技能废弃机制。长期不用的技能要标记为废弃,但不要立刻删除,至少保留一个发布周期。废弃的技能不再参与路由匹配,但历史会话要用到的旧逻辑还能找到对应版本的实现。
四是模型升级带来的影响。底层模型升级后,技能的表现可能跟着变化——通常是因为新模型对上下文的理解方式变了。所以每次换模型版本,我都建议做一次技能全量回归,重点关注触发率和输出格式,往往能发现一些描述需要微调才能保持和原来一致的效果。
我自己在维护技能库过程中体会最深的一点是:技能不是越多越好,而是越"准"越好。一个精心打磨、边界清晰、自带校验的技能,顶得上十个描述模糊的功能堆叠。而且技能的编写永远受"你希望 Agent 在什么边界内自主行动"这个目标的约束,它本质上是一种产品设计,不只是技术实现。如果你正在搭自己的 Agent 应用,建议从三个最核心业务行为开始封装,跑通之后再逐步扩张,这是我几次重构下来最稳妥的路径。