如果你正在做AI智能体,大概率已经绕不开 Function Calling、Tool Use 这些东西了。而 agent-skills 这个项目,本质上就是把零散的 Tool 升级成一套带描述、带参数、带执行逻辑、带兜底策略的完整技能单元,让智能体既能听懂指令,又能真正把活干完。我最近完整推进了这个项目从设计到落地的全过程,过程中踩了不少坑,也沉淀出一套可以复用的方法论。
这篇文章不聊大道理,就讲我怎么理解 agent-skills、怎么设计技能模型、怎么实现调用链路,以及那些文档里不会写的坑是怎么排掉的。项目做得不多,但每个环节都是真金白银堆出来的经验,希望能帮你少走几步弯路。
1. 技能的本质:把“会说话”变成“会干活”
1.1 大模型最擅长的是生成,不是执行
先想清楚一个问题:大模型凭什么能把活干完?答案是,它本身不会执行任何动作,它只会根据输入的上下文,生成一段“接下来应该调用哪个函数、参数是什么”的结构化文本。真正去查数据库、调接口、改配置的是外部系统。
这里就藏着一个关键认知:你喂给模型的不是“能力”,而是“能力的说明书”。模型必须知道存在哪些技能、每个技能是干什么的、需要什么参数、返回什么格式,它才能做出正确的选择。如果说明书写得含糊,模型就只能靠猜。
agent-skills 这个词拆开看,就是两个层面的东西:
- agent 层:负责感知任务需求、规划执行顺序、调用合适的技能;
- skills 层:负责把具体动作封装成标准化的可执行单元。
也就是说,agent 是大脑,skills 是手脚。大脑负责决策,手脚负责落地。我在设计这个项目时,第一件事就是划清这条界限——大脑不要碰执行细节,手脚不要做决策。一旦出现越界,系统就会出各种莫名其妙的 bug。
1.2 把 Prompt 当成代码写,会烂得特别快
没做技能系统之前,很多团队喜欢把所有业务逻辑写进 System Prompt。最初几个需求确实跑得动,等需求一多,问题就全出来了:
- Prompt 越来越长,三四千字都打不住,模型上下文空间被严重占用;
- 新增一个功能就要改 Prompt,线上微调成本极高并且容易引发“回归”;
- 同一个能力在多个场景需要复用时,只能复制粘贴,改一处漏一处;
- 模型输出的稳定性全看运气,稍微换一下措辞就可能触发错误分支。
我自己跑过一个测试:把某个客服场景的 20 条业务规则一股脑写进 Prompt,结果模型经常在几条纠缠规则之间反复横跳,答错之后怎么调措辞都压不住。后来把这些规则拆成了 5 个独立技能,每个技能只负责一小块逻辑,准确率一下就上来了。
这背后其实是个注意力问题。技能越多,单次推理时模型需要关注的无关信息就越少,决策自然越准。
1.3 技能系统到底解决了什么
归根结底,agent-skills 想解决三件事:
- 能力的复用:一个技能可以在多个 agent 场景里被反复调用,不用每次重写。
- 能力的可观测性:技能有名字、有描述、有参数约束、有返回结构,每一次调用都可以被记录和追踪。
- 能力的可测试性:单个技能可以独立压测、单测、回归,而不是只能整体跑黑盒。
所以你看,技能系统不是花架子,它是把“大模型应用”从“实验性脚本”推向“工程化产品”的一座桥。没有这座桥,项目规模一大就撑不住了。
2. 整体设计思路:技能建模是地基
2.1 先盘点现有能力,再做抽象
做技能系统的第一步不是写代码,而是盘点业务里到底有多少“动作”。我习惯用一张简单的表来梳理:
| 能力域 | 能力名称 | 高频场景 | 输入要素 | 输出结果 | 依赖系统 |
|---|---|---|---|---|---|
| 订单域 | 查订单状态 | 用户问物流到哪 | 订单号 | 订单状态+物流轨迹 | 订单服务 |
| 订单域 | 修改订单备注 | 用户要求改备注 | 订单号+新备注 | 成功/失败原因 | 订单服务 |
| 营销域 | 计算优惠 | 用户想用优惠券 | 用户ID+商品列表 | 优惠后金额 | 促销服务 |
| 用户域 | 查会员等级 | 用户问自己有啥权益 | 用户ID | 等级+权益列表 | 用户中心 |
这里的经验是:不要一开始就抽象大而全的通用接口,而是从真实高频场景反推。哪个动作被反复提起,就优先把它技能化。当某个技能描述开始出现“如果……就……否则……”这种绕弯的句式,说明它需要再拆一层。
2.2 技能描述怎么写才不被模型误解
我个人认为,技能描述是整个技能系统里最容易被低估的部分。描述写得好不好,直接决定模型能不能选对技能。
重点有这几个:
- 说明“能做什么”,也要说明“不做什么”。比如“查订单”技能,如果不在描述里写明“仅用于查询状态,不处理退款”,用户问退款问题时模型很容易误调用。
- 写清楚触发条件,包括关键词、用户意图、前置条件。
- 参数类型和取值范围要精确。为布尔参数写明布尔里到底表达什么,为枚举参数写明取值范围。
- 描述保持简洁,不要放置执行逻辑的历史包袱。描述是给大模型当导航用的,不是存放旧逻辑的垃圾堆。
举个例子,初始技能描述写的是:
查订单。查询订单信息,比如订单状态、物流、商品列表。
这样的描述过于宽泛,模型可能拿它当万能接口。优化之后:
当用户需要了解订单当前状态、物流进度时,调用查订单技能。仅支持订单状态查询,若用户表达退款、改签等需求,请改用其他技能。
效果会好很多。这类优化在项目里反复出现过,属于投入产出比最高的改善手段。
2.3 技能粒度怎么定:拆太碎是灾难,拆太粗是摆设
拆分粒度是另一个反复折磨人的点。我一开始把“查询订单”和“查询物流”拆成两个技能,结果模型经常在两个技能之间犹豫不决。后来把订单查询和物流查询合并成一个技能,反而更顺了。
我总结出的规律是:
- 技能不应该对应“单一接口”,而应该对应“完整用户意图”。比如“查看我上次买的手机现在走到哪了”,底层可能要查订单、查物流、查商品名称,但模型只应该看到一个“查订单全貌”的技能。
- 粒度太细,模型需要多步协调,每一步都可能出错,累积下来的错误率很吓人;
- 粒度太粗,技能内部塞一堆互不相干的逻辑,复用时反而受限制。
我现在的判断标准是:如果一个技能被调用的场景高度重复,并且输入的参数基本一致,说明粒度合适;如果同一个技能要加三个以上的“能力标志位”去区分不同行为,说明该拆了。
3. 核心实现:从注册到调用的完整链路
3.1 Schema 先行:把参数模型定清楚
先看一个我实际用过的技能 Schema 例子。别急着抄,重点看结构:
from enum import Enum from pydantic import BaseModel, Field class OrderStatus(str, Enum): PENDING = "pending" PAID = "paid" SHIPPED = "shipped" COMPLETED = "completed" REFUNDED = "refunded" class QueryOrderSkillParams(BaseModel): order_id: str = Field( description="订单号,格式为 8 位字母数字组合。" ) include_items: bool = Field( default=True, description="是否返回商品明细,默认返回。" ) class QueryOrderSkillResult(BaseModel): status: OrderStatus = Field(description="订单状态") express_trace: list[str] = Field( default_factory=list, description="物流轨迹,按时间正序排列。" )这套 Schema 的意义在于:
- 强类型校验。模型生成参数时经常手滑,给订单号传个 null 或者传个长度超标的字符串,pydantic 一拦就能快速反馈,不至于让一个错误的参数流入后端,把订单服务打挂。
- 字段描述即提示词。每个字段的 description 都会在运行时拼接进发给模型的工具说明里。字段注释写得准确,模型生成参数的准确率会显著提高。
- 可序列化。技能执行结果可以被转成结构化数据,作为下一次模型推理的上下文。
3.2 执行体编写:把“生成”变成“执行”
我设计执行体的核心原则是:执行体只做确定性的事情,不做任何让模型理解成本变高的事情。
换句话说,技能内部可以查数据库、调接口、做计算,但不要自己再套一层小 Prompt 让另一个模型做决策。每多一层模型调用,延迟、成本、失败率都会叠加。我的习惯是,执行体里的逻辑要么是纯代码,要么只允许调用内部规则引擎这种确定性的东西。
代码层面,技能执行体我建议保持轻量:
def execute_skill(skill_name: str, params: dict) -> dict: skill_registry = { "query_order_info": query_order_info, "calc_discount": calc_discount, "update_order_remark": update_order_remark, } if skill_name not in skill_registry: raise SkillNotFoundError(f"技能 {skill_name} 不存在") return skill_registry[skill_name](**params)在引入复杂框架之前,先用字典做注册表是最可靠的。我在项目早期试过动态加载模块、插件系统、事件总线等各种花花肠子,最后发现一个简单的字典加几行分发逻辑,反而是所有方案里最稳的。框架可以后加,但第一版一定要能随时看清调用链。
3.3 注册与发现机制:让模型知道“有什么可用”
注册的时机和入口也值得讲究。我建议把技能元信息在 agent 启动时集中加载,并生成一份统一的能力清单。清单里每个技能对应一块结构化的工具描述,模型推理时会看到全部或经过筛选后的子集。
这里有个性能细节:当技能数量超过一定阈值(我自己测下来大概是 30 到 50 个),把全部技能描述一次性塞进上下文,不仅浪费 token,还会让模型决策变慢。所以你需要一个“预筛”步骤。
我的做法是给每个技能打标签,比如{"domain": "order", "requires": ["user_id"]},在把技能清单发给模型前,先用规则或者一个小的 embedding 模型,根据当前会话意图做一次粗筛,只把相关的十来个技能放进上下文。
这一步简单的规则匹配就能见效。比如用户消息里出现“退”“款”,优先召回订单域和售后域的技能;出现“优惠”“券”,召回营销域技能。不必一开始就上复杂的意图分类模型。
3.4 调用路由与异常兜底
技能执行不是调用完就结束,后面的异常处理才是真正体现工程成熟度的地方。
我总结了一套兜底策略:
- 参数校验失败——把校验错误信息拼回上下文,提示模型重新生成参数,最多重试两次;
- 执行超时——设置单技能超时时间(比如 10 秒),超时后立即返回“技能不可用”的占位结果,不让 agent 无限等待;
- 业务返回为空——结果为空时不给模型发挥空间,直接返回一个明确的“未查询到数据”标记,避免它脑补不存在的结论;
- 未知技能调用——路由层拦截,记录日志,返回可读错误。
最容易被忽略的是第 3 条。模型在拿到空结果后经常倾向于自我发挥,编造一个看起来合理的答案。明确告知“没有数据”,既是给模型吃定心丸,也是给它划定边界。
3.5 上下文压缩:别让历史记录撑爆窗口
技能调用多了之后,上下文里会堆积大量工具返回结果。有些结果对当前会话早已没有价值,还白白占用 token 窗口。我处理的方式是在每次技能调用结束后做一次轻量摘要:
- 如果技能结果是列表型数据,保留前 3 条,其余截断,并附注“共 N 条,已截断”;
- 如果技能结果已经作为核心回复发送给用户,将其压缩成一行状态摘要,保留在短期记忆里;
- 如果同一技能在连续三轮内被反复调用,只保留最近一次的结果。
这套策略能让上下文占用率下降 50% 左右,模型响应速度的提升非常明显。
4. 实操过程:从一个真实技能库的搭建流程说起
4.1 场景定义:先立一个小目标
我在项目里选了个“订单助手”场景作为试点。目标很简单:用户输入自然语言,系统能识别意图,调用技能返回真实业务数据,并生成友好回答。
把一个场景做成闭环,比把十个场景做成半吊子重要得多。所以第一版我只做了三个技能:查订单、查物流、算优惠。
技能清单如下:
| 技能名 | 输入参数 | 输出 | 说明 |
|---|---|---|---|
| query_order | order_id | 状态+商品列表 | 查主订单 |
| query_express | order_id | 物流轨迹 | 查物流 |
| calc_discount | user_id, product_list | 优惠金额 | 计算优惠 |
三个技能串起来的链路非常简单:先是 query_order 拿到订单主体,再按需查物流,遇营销需求时就调 calc_discount。
4.2 最小闭环先跑通
搭建顺序是:先写最底层的服务接口,再用 pydantic 包一层参数校验,然后做技能注册和路由,最后接一个大模型作为“决策器”。
第一轮跑通的时候问题一堆,但最大的收获是技术选型都被验证过了。我测试用的模型支持 function calling,但我不想被某个单一供应商绑死,所以在设计上留了兼容层:把技能描述转成模型厂商的 tool schema 格式,这样切换模型时只需要换一个适配器。
如果项目周期紧张,我个人建议别从零实现兼容层,可以直接基于现成的 Agent 框架来做,把技能作为工具注册进去。框架能帮你处理对话状态、模型调用、函数分发这些脏活。但框架的选择也要结合项目复杂度和团队熟悉度,不要盲目追新。这个我在文末经验里还会展开。
4.3 让模型“更会选技能”的迭代技巧
跑通之后,最耗精力的不是让技能正常执行,而是让模型在混乱的用户表达里也能选中正确技能。
举几个真实迭代例子,感受一下:
- 用户说“我的快递啥时候到啊”,最初模型选的是 query_order,返回订单状态后并没回答物流问题。调整 query_order 的描述和 query_express 的触发条件,问题就消失了;
- 用户说“帮我看看还能不能便宜点”,模型无法正确关联到 calc_discount。后来我在 calc_discount 描述里增加了一句“包含所有与优惠、降价、折扣相关的场景”,准确率明显改善;
- 用户说“取消订单”,但系统还没做这个技能,模型就会随机调用相邻技能并胡乱回答。后来我在技能清单外增加了一个“fallback”说明,明确告知模型“当前范围不支持取消订单,请礼貌告知用户”,情况立刻收敛。
所以,技能系统的迭代不是写代码,而是写描述、写边界、写兜底。模型行为不对,先别急着改底薪结构,先用描述调一调,又快又安全。
5. 常见问题与排查技巧实录
5.1 模型选错技能,问题到底出在哪里
选错技能这事,很多人第一反应是“模型不行”,但我测下来的结论是:八成出在描述和场景语义不匹配,只有两成是模型本身能力不足。
排查思路是:
- 先检查该技能描述里是否写清了触发条件和排除条件;
- 再检查是否有其他技能与它语义相近,导致被同时召回;
- 最后再看模型推理日志里当时看到了什么,而不是凭感觉猜。
我在这个环节里最大的收获就是养成了看日志的习惯。无论多快的推理错误,多离谱的行为,都能在链路日志里找到依据。技能调用链的可观测性,一定要从第一天就建立起来,落后了再补会非常痛苦。
5.2 参数校验失败的三种解法
模型传参失败的概率其实远高于大多数人预期。尤其是枚举值,模型经常把“pending”传成“等待中”之类的中文描述,这会在校验层直接炸掉。
我现在常用的处理方式有三种:
- 在描述里写示例:每个字段 description 都带上真实示例,比如“order_id:例如 ORD20250101”,模型照葫芦画瓢的成功率会高很多。
- 在校验层做同义映射:把“等待中”、“待支付”之类的常见别名映射回枚举值。
- 失败后重试机制:第一次校验失败,把错误信息返回给模型,让它自行修正参数,再试一次。很多小问题模型自己就改对了。
5.3 重复调用与循环调用
模型在复杂任务中,有时会在两个技能之间来回跳,形成循环。最初我以为跑进了死循环,后来发现是上下文信息不足,模型每次做出的决策都一样,于是来回尝试。
我的规避办法:
- 限制单个会话的最大技能调用次数(比如 6 次),超出则强制结束转为人工兜底;
- 增加“已尝试过该技能”的历史标记,让模型看到重复调用没有效果;
- 分析日志找出反复调用的套路,针对性调整对应技能的描述。
5.4 上下文爆炸
技能返回的数据量一旦大了,上下文很快就不够用。特别是批量查询结果带列表的场景,一次会话就可能吃掉几万 token。
建议从源头控制:在 Schema 层面就支持分页或者数量限制参数,比如max_items=5,避免后端一次性返回全量数据。同时配合前文提到的上下文压缩策略,双管齐下,就能把上下文占用压到可控范围。
5.5 测试技能库的三种姿势
技能库的质量不能只靠肉眼判断,我把测试分成三层:
- 单元测试:单个技能的参数校验、业务逻辑、异常分支,纯粹跑 Python 层面的测试,速度快、反馈直接;
- 评估测试:准备几百条用户话术,让 agent 走完整链路,统计技能选择准确率、参数生成准确率、最终回复满意度。这个环节最接近线上真实效果;
- 回归测试:每次修改技能描述或新增技能后,跑一遍全量测试集,重点观察旧场景有没有被新描述干扰。
回归测试是很多人容易忽略的。改了一个技能的描述,另一部分场景可能就受到牵连,没有回归测试兜底,迟早要踩线上事故。
6. 结尾:说几句真心话
项目走到今天,我最大的体会是:做 agent-skills 这类系统,核心难点不在技术栈,而在对“大模型行为边界”的理解。你以为你在写代码,其实你在给一只话痨鹦鹉准备一本清晰的操作手册。手册写得好,它才接得住话、干得成事。
另一个深刻的体会是,永远别把全部希望押在一个单体模型上。我实际推进时发现,不同模型对 function calling 的支持和表现差距明显,有的在工具选择上确实更强,但消耗也大;有的模型虽然通用能力弱一些,但在简单技能调用上反而简洁高效。所以技能层的业务逻辑尽量与模型解耦,做好兼容适配,这样切换模型时才能进退自如。
最后分享一个小技巧:给每个技能加一个“成功率”指标,统计每次调用的失败率、超时率、空结果率,定期清洗优化。这套数据不仅指导具体迭代方向,还能在你和业务方对需求时拿得出硬指标,用事实说话总比空口讲道理有用得多。
agent-skills 这条路,技术边界还在被不断拓宽,但地基逻辑已经比较清晰了。希望我的这些经验,能帮你搭建自己项目的技能体系时,少踩几个前人已经踩过的坑。