如果你最近在折腾 AI Agent,大概率会遇到一个绕不开的问题:模型本身很聪明,但它不会主动去查天气、翻文档、执行代码。这些能力不能靠“再训练”,只能靠外挂。而这个“外挂层”,就是业界常说的 agent-skills,也就是 Agent 的技能系统。
这篇内容想聊聊技能系统到底在解决什么问题、核心机制有哪些、以及怎么从零搭一套能跑通的极简实现。你可以把这篇文章理解成一份“技能系统从设计到落地的思路笔记”,适合正在做 Agent 应用的开发者、AI 产品负责人,也适合准备做智能体方向毕业设计或开源项目的同学。读完后你会对“技能”这个概念有更具体的感知,也能照着代码做出一个最小演示。
1. 为什么要给 Agent 做一套技能系统
1.1 单体式 Agent 的困境
早前做 Agent 应用时,最常见的思路是把所有能力都写进系统提示词里。比如“你是一个个人助理,你可以查询任务列表,任务列表存在本地文本文件中,查询的时候请用 Python 读取文件并返回结果,同时你可以记账,记账需要调用本地脚本……”
这种方式在只有两三个能力的 demo 阶段完全没问题,一旦能力数量多起来,问题就非常明显。
最直接的是提示词膨胀。每增加一个能力,就要在提示词里追加一大段说明。模型在长上下文里很容易“迷失重点”,尤其是多个能力描述之间存在语义重叠时,模型选错工具的概率显著上升。其次,可复用性基本为零。换个项目,代码要重新组织,能力逻辑要和业务逻辑耦合在一起,很难拆分。再有就是调试困难,你无法确定模型为什么调用了这个工具而不是另一个,也不能独立验证某个工具本身是否正常工作。单体式 Agent 在早期确实高效,但维护成本随着能力数量呈指数级上升。
实际项目里,这个问题会在能力超过五个左右时集中爆发。一个典型的表现是:模型反复调用同一个工具,或者面对相似描述的多个工具时出现选择混乱。比如系统里同时存在“查询用户信息”和“查询用户详情”这两个功能,模型经常选错。如果技能描述不加以约束,这类问题会频繁出现。
1.2 技能化 Agent 的核心差异
技能化 Agent 的思路是把“能力”从 Agent 主逻辑中剥离出来,每个能力独立定义、独立注册、独立维护。Agent 本身只负责理解用户意图、规划执行路径、调用对应技能。
我做一个比较直观的对比:
| 维度 | 单体式 Agent | 技能化 Agent |
|---|---|---|
| 提示词长度 | 随能力增长无限膨胀 | 保持精简,只需维护路由逻辑 |
| 能力复用 | 项目内耦合,难迁移 | 技能可跨项目复用 |
| 独立测试 | 困难,需整链调试 | 每个技能可单独测试 |
| 扩展方式 | 改提示词和主逻辑 | 新增技能文件即可 |
| 失败排查 | 需回溯完整对话 | 可独立验证技能本身 |
不是说所有场景都需要技能化。如果你的 Agent 只有两个固定动作,老老实实写在一个函数里反而更省事。但一旦你面对的是“能力会持续增加、需要在不同场景下动态组合”的情况,技能化是唯一能让系统长期健康演进的方式。
从实际感受看,技能化还有一个潜在的好处:它让 Agent 的能力边界变得清晰。某公司内部做过一个智能客服系统的重构,旧的单体实现里,所有工具判断逻辑混杂在提示词里,每次新增业务功能都要小心翼翼,生怕影响已有行为;技能化之后,每个技能独立上线、独立回滚,回归测试只需要验证涉及到的技能链路。效率提升确实非常明显。
2. 设计一套技能系统前,先想清楚这几件事
2.1 技能的本质:把能力契约化
一个技能,本质上不是一个函数,而是一个“行为契约”。它告诉 Agent:这个技能是干什么的、什么时候该用它、需要提供什么参数、会返回什么结果。
拿手机 App 来做类比。你装应用的时候,系统会告诉你它要用到哪些权限。Agent 的技能也是一样,它要显式声明自己的输入输出格式和调用边界。如果一项能力没有这些声明,那它就不应该被当作技能注册进系统里。
一个完整的技能定义通常包含以下要素:
- 技能名称:唯一标识,建议用英文短横线命名,如 search_document
- 技能描述:说明技能的用途、适用场景、限制条件
- 参数 Schema:描述这个技能需要哪些输入,每个参数的类型、必填性、取值范围
- 执行函数:实际运行的逻辑
- 返回格式:结构化数据,方便 Agent 读取
其中参数 Schema 是最容易被忽视但又最关键的部分。Agent 不是人,它不会看到你函数内部对参数做了哪些格式清洗,它只会依据你给它的 Schema 来生成调用参数。如果 Schema 写得含糊,模型就会“自由发挥”,生成 LLM 幻觉参数的概率会大幅上升。
举个真实的例子。某个项目里,开发者设计了一个“查询订单”技能,参数定义为 status: string,没有说明取值范围。结果模型在调度时生成了 status="已完成的订单",直接把查询逻辑搞崩了。之后把 Schema 改成 status: enum["pending","shipped","completed"],问题迎刃而解。这告诉我们:给模型的自由度越小,系统稳定性越高。
2.2 如何给模型写一份“看得懂”的技能描述
技能描述是给模型看的,不是给人看的。这是很多开发者容易混淆的一点。你写描述时要站在模型的角度去想:它在看到一个用户问题时,能不能根据这段描述快速判断该不该用这个技能?
描述通常遵循一个三明治结构:
- 这个技能做什么
- 在什么情况下使用
- 使用时需要注意什么
我举个例子。假设你要注册一个“获取今日天气”的技能:
差的描述:
技能:查询天气。输入城市名,返回天气。好的描述:
技能:get_weather 用途:获取指定城市当前以及未来几天的天气信息。 使用场景:当用户询问天气、温度、降水概率、是否需要带伞等问题时使用。 参数: city(必填):城市名称,中文格式,如"北京市"。 unit(可选):温度单位,可选值为 celsius、fahrenheit,默认 celsius。 注意:如果用户没有指定城市但提供了地理位置信息,请先确认后再调用。区别在哪?差的描述只回答了“做什么”,好的描述回答了“为什么用、什么时候用、怎么用、有什么坑”。模型本质上是一个模式匹配器,你给它足够多的约束条件,它才能在你的预期范围内完成调度。
描述也不宜过长,一条技能描述控制在 100–200 字即可。过多描述会让模型把注意力分散到非关键字段上,反而降低选择准确率。如果技能数量较多,建议在描述开头加一个“关键词”或“标签”字段,便于模型快速匹配。比如“技能标签:天气、气温、降雨、出行”,这样模型能更快建立联想。
2.3 技能的数量与命名规范
关于技能数量,很多刚接触的人会问“一个 Agent 最多能挂多少技能?”这个问题没有标准答案,但有一个经验值:在上下文窗口有限的前提下,技能数量超过 20 个后,模型的选择准确率会出现明显下降。
这时有两种处理策略:第一种是给技能做分组,将同类技能收敛到一个“技能组”里,先由路由层选出技能组,再由组内逻辑精确定位具体技能;第二种是利用向量数据库做技能召回,将全部技能描述向量化,每次请求先做语义检索,把最相关的 5–8 个技能注入上下文。后一种方案更适合技能库特别庞大的场景,本文后面的设计和实现,基于的是技能数量适中的场景。
命名规范也要统一。全小写加下划线或短横线,避免出现大小写混用或空格。模型在生成调用目标时,倾向于严格按照你提供的名称输出,如果命名不规范,执行器会频繁遇到技能不存在的异常。命名还与日志分析直接相关,统一的命名规范能让统计报表做得更顺利。
3. 技能系统的三个核心机制
3.1 注册与发现:技能怎么进系统
技能注册是整个技能系统的入口。它解决的问题是:系统怎么知道有哪些技能可用、每个技能的元数据是什么。
最基础的设计是“注册中心+装饰器模式”。在 Python 中,可以利用装饰器非常优雅地完成技能注册。每个函数加上一个 @skill 装饰器后,自动将技能元数据注册到全局技能表中。这样做的好处是业务逻辑与注册逻辑完全解耦,新增技能只需要写一个普通函数,不需要改动主程序。
下面是一个最小技能注册器的代码示例:
import inspect import json from typing import Callable, Dict, Any SKILL_REGISTRY: Dict[str, Dict[str, Any]] = {} def skill(name: str, description: str, parameters: dict): def decorator(func: Callable): SKILL_REGISTRY[name] = { "name": name, "description": description, "parameters": parameters, "function": func, "signature": inspect.signature(func) } return func return decorator你会发现,这里把函数对象也存到了注册表里。这意味着,任何模块只要执行了这个装饰器,就能把自身能力贡献给全局技能库。项目里模块多了之后,这个特性非常重要。各业务方可以直接在自己的代码目录里维护技能模块,系统启动时自动加载,不需要互相协调去改一份公共文件。
技能发现机制则是一个查询操作。系统在每次需要决策时,将注册表中所有技能的名称、描述、参数 Schema 序列化成一段文本或结构化的工具列表,交给模型选择。这部分在后续讲调度的章节会展开。
3.2 调度与执行:模型是怎么决定调哪个技能
技能调度是整个系统的核心决策链路,也是 agent-skills 这个目录下最容易被误解的部分。
很多人以为 Agent 的“思考过程”是系统预先写好的逻辑。实际上,主流实现都依赖于大语言模型的 function calling 能力。整个过程可以拆分成四个动作:
- 把可用技能名单和参数 Schema 发给模型
- 模型根据用户指令生成调用决策(调用哪个技能、传什么参数)
- 系统校验并执行对应技能
- 把执行结果返回给模型,模型生成最终回复
需要特别说明的是,模型在第二步输出的不是自由文本,而是一段结构化的 JSON。这段 JSON 通常包含两部分:技能名称(name)和参数值(arguments)。系统的调度器需要解析这段 JSON,找到注册表中的对应函数,并把参数传给它。
下面是一个简化的调度器实现:
import json def dispatch(tool_call_json: str) -> str: call = json.loads(tool_call_json) name = call.get("name") arguments = json.loads(call.get("arguments", "{}")) if name not in SKILL_REGISTRY: raise ValueError(f"技能 {name} 不存在,请检查技能名称") skill_meta = SKILL_REGISTRY[name] result = skill_meta["function"](**arguments) return json.dumps({"name": name, "result": result}, ensure_ascii=False)注意这个调用契约背后的一个重要点:模型生成的参数永远是不可信的。调度器在执行技能之前,要依据注册时记录的参数 Schema 做一次参数校验。常见做法是用 JSON Schema 校验工具(如常见的 jsonschema 库)检查模型生成参数的类型、必填项和枚举值。校验不通过时,系统可以带着错误信息让模型自行修正参数,也就是“重试一次”。很多生产级 Agent 的稳定性,靠的就是这一层校验和重试机制。
3.3 技能编排:单个技能不够用怎么办
真实场景里,用户的一句话往往需要多个技能协作才能完成。比如“查一下我昨天在项目 A 上花了多少时间”,背后至少需要两个能力:查任务列表的技能、计算工时统计的技能。这就是技能编排问题。
编排有两种常见模式:
顺序编排:一个技能的输出作为另一个技能的输入。执行器维护一个执行队列,按序调用。
条件编排:根据前置技能的执行结果决定后续调用哪个技能。比如查询技能返回的结果为空,则转而调用创建任务的技能。
实现层面,编排逻辑可以放在 Agent 主体里,由模型动态规划执行路径;也可以预置为“技能链”或“工作流”,由系统统一管理。前者更灵活,适合开放式场景;后者更稳定,适合高频固定流程。
我倾向于在系统早期采用“模型动态规划+技能链兜底”的双轨策略。也就是模型可以自由组合技能,但当系统识别到用户的请求属于某个高频场景时,直接走预设的技能链,避免模型反复权衡带来的延迟和不稳定。这种组合既能保留泛化能力,又能保证核心体验。
4. 实操:一个可运行的极简技能系统
4.1 目标与准备
前面讲了不少理论,接下来动手搭一个最小可运行的系统。这个小系统的目标是:
- 支持技能注册
- 支持模型调用决策(此处用模拟数据理解流程)
- 支持技能执行并返回结构化结果
我会用标准的 Python 环境来实现,不依赖重型框架。核心依赖只有一个 jsonschema 库,用于参数校验。如果你本地没有,可以用 pip 安装。
这个系统要解决的场景设定为一个“工作日志助理”。它能记录用户的工作任务、查询历史任务、统计某一天的工时。听起来简单,但足够展示技能系统的完整链路。
4.2 技能注册与实现
首先是技能实现的代码。每个技能独立定义,用装饰器挂载到注册中心:
import json from datetime import datetime, date # 用一个本地字典模拟任务数据库 TASKS = [] TASK_ID = 0 @skill( name="add_task", description="记录一条工作任务。当用户需要新增任务、写下待办事项时使用。", parameters={ "type": "object", "properties": { "task_name": {"type": "string", "description": "任务名称"}, "project": {"type": "string", "description": "所属项目"}, "hours": {"type": "number", "description": "预估或实际花费小时数"}, "task_date": {"type": "string", "description": "任务日期,格式 YYYY-MM-DD"} }, "required": ["task_name", "project", "hours", "task_date"] } ) def add_task(task_name: str, project: str, hours: float, task_date: str): global TASK_ID TASK_ID += 1 task = { "id": TASK_ID, "task_name": task_name, "project": project, "hours": hours, "task_date": task_date } TASKS.append(task) return {"status": "ok", "task_id": TASK_ID} @skill( name="query_tasks", description="查询指定日期的工作任务列表。当用户询问某天做了什么、列出任务时使用。", parameters={ "type": "object", "properties": { "task_date": {"type": "string", "description": "查询日期,格式 YYYY-MM-DD"} }, "required": ["task_date"] } ) def query_tasks(task_date: str): result = [t for t in TASKS if t["task_date"] == task_date] return {"count": len(result), "tasks": result} @skill( name="sum_hours", description="统计指定日期的累计工时。当用户询问当天工作时长、总工时、投入时间时使用。", parameters={ "type": "object", "properties": { "task_date": {"type": "string", "description": "统计日期,格式 YYYY-MM-DD"} }, "required": ["task_date"] } ) def sum_hours(task_date: str): total = sum(t["hours"] for t in TASKS if t["task_date"] == task_date) return {"task_date": task_date, "total_hours": total}这段代码本身没有任何“新东西”,但它的结构表达了一个设计原则:技能与业务基础设施分离。这三段逻辑即使不接入 Agent,也可以作为独立的工具函数使用。接入技能系统后,它们才获得了被模型发现和调用的能力。
4.3 调度器与参数校验
接下来是调度器的实现。这里引入 jsonschema 做参数校验:
from jsonschema import validate, ValidationError def execute_skill(name: str, arguments: dict) -> str: if name not in SKILL_REGISTRY: return json.dumps({"error": f"未知技能: {name}"}) meta = SKILL_REGISTRY[name] try: validate(instance=arguments, schema=meta["parameters"]) except ValidationError as e: return json.dumps({"error": f"参数校验失败: {e.message}"}) func = meta["function"] try: result = func(**arguments) return json.dumps({"name": name, "result": result}, ensure_ascii=False) except Exception as e: return json.dumps({"error": f"技能执行失败: {str(e)}"})在上述代码中,参数校验被放在执行之前。这能拦截掉大量“模型生成了错误参数”的情况。我在另一个项目里做过统计,接入参数校验后,同一批测试案例的技能调用成功率提升了接近 30 个百分点。大多数失败并非技能本身报错,而是模型给出的参数缺字段、类型不对或者枚举值超出范围。
4.4 模型决策模拟与完整链路
真实环境中,模型的 function calling 由大模型 API 提供。这里为了完整演示决策链路,我用一段模拟的模型输出代替。假设用户说:
用户:昨天我在项目A上做了哪些事?模型分析后,认为需要先查询日期“2025-03-04”的任务列表,于是生成如下决策:
{ "name": "query_tasks", "arguments": "{\"task_date\": \"2025-03-04\"}" }系统执行对应技能后返回:
{ "name": "query_tasks", "result": { "count": 3, "tasks": [ {"id": 1, "task_name": "完成接口联调", "project": "项目A", "hours": 2.5, "task_date": "2025-03-04"}, {"id": 2, "task_name": "撰写技术方案", "project": "项目A", "hours": 1.5, "task_date": "2025-03-04"}, {"id": 3, "task_name": "代码评审", "project": "项目A", "hours": 1.0, "task_date": "2025-03-04"} ] } }如果用户继续问“那我昨天项目A一共花了多少小时”,模型此时可能会生成连续两个调用决策。第一个是 query_tasks,第二个是 sum_hours。系统按序执行,把两次结果合并成最终答案。
这个交互过程告诉我们一件很重要的事:Agent 的“智能”不是全部来自模型,很大一部分来自系统如何精准地引导模型调用正确的技能。技能定义得越清楚,系统的智能涌现效果就越稳定。
5. 落地过程中最容易踩的坑
5.1 常见问题速查表
实操中会遇到各种奇奇怪怪的问题。下面这张表是从多次项目实践中整理出的高频问题:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模型频繁调用错误的技能 | 技能描述语义重叠或不清 | 重写描述,增加触发条件和反例说明 |
| 模型生成参数时缺字段 | 参数 Schema 中 required 列表不完整 | 检查参数 Schema,必填项显式声明 |
| 技能执行报错但模型仍然继续 | 错误信息未明确传给模型 | 将异常信息结构化为模型可读格式 |
| 新增技能后系统响应变慢 | 技能列表超过上下文合理范围 | 增加技能分组或出发向量召回 |
| 模型“不记得”之前技能执行结果 | 技能返回体过大 | 精简返回结果,保留 task 汇总裁 |
5.2 几个值得注意的细节
第一个细节:技能返回体必须向模型端做结构化表达。
一些技能函数返回的数据如果不为模型做过裁剪,很可能会让模型“消化不良”。比如查询任务列表返回了 50 条记录全部塞给模型,既占用上下文,又容易让模型生成冗长回复。建议技能返回时先做摘要化处理,超长列表只返回前 5 条加上总数统计,并在结果里附带说明“如需查看更多,可调用分页查询技能”。
第二个细节:并行调用与顺序调用的选择。
部分大模型 API 支持一次性生成多个工具调用意图。系统此时需要判断这些技能是否相互依赖。比如 add_task 和 sum_hours 就不适合并行调用——如果并行执行,sum_hours 统计时可能还没有包含新增任务。而 query_tasks 和 sum_hours 虽然可以并行,但后者的输入一般依赖前者的数据,需要场景化判断。稳妥的做法是:默认按顺序执行,只有当技能之间明确无依赖时才使用并行模式。
第三个细节:技能的幂等性设计。
有副作用的技能(比如写文件、发送消息、更新数据库)必须考虑幂等性。如果模型第一次调用后网络中断,系统重试时会不会重复写入?我在实际项目里遇到过一个真实的教训:某工具发送了一封通知邮件,因为第一次调用后 API 响应超时,调度器重试了一次,结果用户收到了两封一模一样的邮件。从那之后,所有有副作用的技能都强制要求在参数里增加 request_id 之类的幂等键,系统端根据该键去重。
6. 技能的长期维护与扩展方向
6.1 从一个技能系统演化为技能生态
当你的项目里积累了二三十个技能后,单纯依赖脚本式的技能模块会越来越难管理。这时候需要考虑把技能系统升级为技能生态,比如增加以下能力:
- 技能版本管理:每次修改技能后保留历史版本,方便回滚。
- 技能独立部署:将技能做成独立的服务,通过网络接口调用,与主 Agent 进程彻底解耦。
- 技能权限模型:为不同使用角色配置不同的技能可用范围,系统内的敏感操作要单独授权。
- 技能日志:记录每次技能调用的事件轨迹,用于复盘分析。
这些能力说起来都是常规的后台管理功能,但一旦引入,整个项目的形态就会从“一个智能体脚本”进化为“一个可对外提供能力的智能体平台”。某团队就把他们的技能模块开放给了同部门另外三个项目使用,对方不需要关心底层实现,只要调用注册中心提供的能力标准格式即可。
6.2 技能与模型能力的边界
最后想聊一个容易忽略的认知。技能系统的存在,是为了补充模型做不到的事,而不是替代模型擅长的事。模型天然擅长文本理解、逻辑推理、语言生成;技能则应聚焦于那些需要确定性执行、数据访问、外部交互的环节。
一个健康的 agent-skills 设计,会清晰划分两条线:思考线(模型负责)和执行线(技能负责)。思考线决定“做什么”,执行线决定“怎么做”。两条线之间通过结构化的技能描述和返回结果协同工作。
在扩展系统时,不要什么都做成技能。比如“总结这段文字”就不是一个好技能,模型自己就能完成,强行注册一个技能反而增加决策负担。而“从 PDF 中提取指定表格并返回 JSON 数据”就是一个典型的好技能,因为它涉及确定性的文件解析和结构化输出,模型无法单独胜任。
最后一点实操心得
从我个人多次“从零搭建技能系统”的经验看,最大的建议是:不要在一开始追求大而全的框架,先用不到两百行代码把“注册—调度—执行—返回”这条闭环跑通。闭环通了之后,再去考虑分组、编排、版本管理这些进阶能力。
另外,技能描述的打磨是一个迭代过程。不要指望第一版描述就能让模型百分之百选对。每次实际运行中出现的误选,都是你优化描述的线索。建议在日志里把“用户的原始输入”“模型选择的技能”“实际应该选择的技能”三条记录对齐,定期分析,逐个修正。用不了几轮,技能选择的准确率就会明显上升。
agent-skills 这个方向的好处在于,它既有工程上的挑战,又蕴含着产品设计的巧思。技能定义得好,Agent 就像一个有章法的团队成员;定义得不好,再强的模型也会手忙脚乱。希望这篇内容能给你一些启发,也值得动手把第一版技能系统搭出来,跑一遍真实的任务链路,很多疑问会在这个过程中自然找到答案。