把智能体技能拆成乐高积木:我设计 agent-skills 技能体系的完整复盘
动手做这个"agent-skills"之前,我刚从一个大模型应用的坑里爬出来。那个项目的智能助手表面上啥都能聊,一旦涉及具体操作——查库存、算报价、调度任务——大模型的回复就开始"一本正经地胡说八道"。不是模型不够聪明,而是我没有给它的能力划好边界、设计好工具。后来我把智能体的能力拆成一个个独立的"技能"模块,让模型学会在正确的时候调用正确的技能,效果立竿见影。这篇内容就是围绕 agent-skills 这个技能体系的设计思路、实操过程和踩坑记录,适合正在做智能体应用、大模型工具调用或者自动化流程的开发者参考,哪怕你刚入门,也能从中找到可以直接抄作业的落地方法。
1. 内容整体设计与思路拆解
1.1 为什么智能体需要独立的技能层
先聊一个本质问题:大模型本身能干很多事,但"能说"和"能做"之间隔着一整条工程链。举个例子,让模型写一段营销文案很容易,但让模型帮用户完成一次订单退款,中间涉及查询订单、校验权限、调用退款接口、发送通知,这串动作如果全塞进提示词里,提示词会膨胀到没法维护,模型的输出也会变得不可控。
我设计的 agent-skills 体系,核心思路是把智能体的能力抽象成一个个独立的技能单元。每个技能单元自包含三样东西:技能的描述说明、触发条件、执行逻辑。模型在对话中理解用户意图后,会在技能列表里做匹配,选中合适的技能再去执行。这个设计借鉴了函数式编程的思想——把复杂逻辑拆成小的、可组合的模块,每个模块只干一件事,但通过模块组合能干成复杂的事。
从实际效果看,这种设计解决了三个长期困扰我的问题。第一个是职责边界:以前模型什么都要管,现在它只管"决策",具体动作交给技能去执行,权限控制、参数校验都在技能层完成。第二个是可维护性:新增一个业务能力,只需要新增一个技能,不用改动对话主流程。第三个是可观测性:每次技能调用都有记录,出了问题能精确定位是哪个环节挂了。
1.2 技能分层与应用场景映射
在设计 agent-skills 的结构时,我参考了微服务架构里的分层思想,把技能拆成三层:原子技能、复合技能和编排技能。
原子技能是最小的不可拆分的动作单元,比如"查询订单状态""计算物流运费""获取用户信息"。这些技能通常对应一个API调用或者一个确定性的函数,输入输出都很明确,没有任何歧义。
复合技能是把多个原子技能按固定顺序串起来,完成一个相对完整的业务动作。比如"处理退货申请"这个复合技能,内部会依次调用"校验订单权限""查询订单详情""生成退货单""通知仓库"这四个原子技能。
编排技能是最高层的动态规划能力,它不固定执行路径,而是根据用户的具体需求,动态决定要调用哪些原子技能、以什么顺序执行。比如"帮用户完成一次完整的售后咨询",模型需要实时判断用户是要退货、换货还是退款,然后走不同的技能链路。
这三层设计让我在应对不同复杂度场景时都有了抓手。简单的查询需求走原子技能,效率高、延迟低;标准化的业务流程走复合技能,稳定可控;开放式的复杂任务走编排技能,灵活应变。下面这张表可以更直观地看到分层的好处:
| 技能层级 | 执行特征 | 适用场景 | 典型例子 |
|---|---|---|---|
| 原子技能 | 单次调用、确定性执行 | 独立查询、简单操作 | 查天气、查订单 |
| 复合技能 | 固定流程、按序执行 | 标准化业务流程 | 退货处理、报销审批 |
| 编排技能 | 动态规划、按需组合 | 开放式复杂任务 | 售后咨询、行程规划 |
2. 核心细节解析与实操要点
2.1 技能描述:让大模型准确理解你的技能
技能层的第一个关键环节,是技能描述。刚开始做的时候我觉得这步最简单,后来发现绝大部分技能匹配失败都是描述写得不够好。大模型不像人那样能靠直觉理解一个函数的用途,它完全依赖你给它的文本描述来做选择,描述质量直接决定了模型能不能在正确的时候选中正确的技能。
我总结出一套写技能描述的模板:做什么+什么时候用+不要什么时候用+关键参数说明。举个实际例子,我为"查快递"技能写的描述经过了四轮修改:
第一版:"查询物流信息"。太宽泛,模型经常在用户问"我的包裹到哪了"时没选中这个技能。
第二版:"用户查询包裹物流轨迹信息时使用"。好了一些,但没说明参数要求和边界。
第三版:"当用户询问包裹位置、物流进度、快递跟踪信息时使用。需要提供运单号或订单号。不要用于查询商品库存状态。"这时候匹配率才明显上来。
第四版加了边界情况说明:"如果用户只提供了收件人手机号但没有运单号,使用手机号查询接口;如果两者都没有,先反问用户获取。"
写技能描述的过程,本质上是在给模型画"触发地图"。描述里除了说明用途和触发条件,还要写清楚参数的约束条件和默认行为。模型判断是否调用技能时会对描述做语义匹配,描述越精确、边界越清晰,匹配的准确率就越高。我后来统计过一个数据:描述优化前后,同一批测试集上的技能匹配准确率从68%提升到了91%,这个提升完全不需要改模型,只改文本描述就能拿到。
2.2 技能注册清单与优先级设计
智能体的技能多了之后,会遇到一个新问题:技能列表太长,模型在选择时会"看花眼"。我手上有一个智能客服项目,技能数量一度膨胀到80多个,结果模型在意图识别阶段就频繁出错,经常把"查订单"和"查物流"搞混。
解决办法是给技能清单做分级和优先级设计。我把技能分成了"核心技能"和"扩展技能"两个池子。核心技能常驻在模型的上下文里,保证随时可以被调用;扩展技能不直接进入模型上下文,而是通过一个前置的意图路由检索出来。这个前置检索我用的是简单的关键词匹配加向量相似度的组合方式,先把候选技能从80个缩小到5~10个,再交给模型做精细选择。
这样做还有一个额外的好处:节省token。大模型的上下文窗口有限,每个技能描述大概要消耗100~300个token,80个技能全塞进去要占掉几万token,不仅贵,还会稀释模型对对话本身的注意力。做了分级之后,模型上下文里只保留10个核心技能,压力和成本都降下来了。
技能注册顺序也值得注意。我观察到一个有趣的现象:当多个技能都能匹配用户意图时,模型倾向于选择列表里靠前的那个。所以在注册清单里,我会把高频业务技能放在前面,低频的放在后面,也算是一种软优先级控制。
2.3 技能冲突检测与熔断设计
技能做多了之后,最怕的是冲突和雪崩。冲突指的是两个技能都能响应同一个用户请求,结果模型选了错的那个;雪崩指的是某个技能内部依赖的下游接口挂了,导致大量请求卡死。
针对冲突问题,我在技能注册表里增加了一个"互斥声明"。比如"创建订单"和"修改订单"两个技能,我明确声明它们不允许同时出现在候选列表里。当意图路由检索出多个互斥技能时,会触发一个仲裁机制,用额外的关键词权重去打破平局。这个机制虽然简单,但确实把模型误选率降低了大约30%。
针对雪崩问题,我在技能执行层加了一层熔断器。每个技能在连续失败超过阈值(比如5次)后,会自动进入"降级状态",这时候模型不会再调用这个技能,而是走一个兜底回复或者备用技能。熔断器的状态会定期探测恢复,服务健康了会自动回到正常状态。设计这个机制的时候让我想起了微服务治理里的Circuit Breaker模式,本质是一样的——保护整个系统不被一个故障点拖垮。
提示:技能熔断的阈值不是拍脑袋定的。我在生产环境里用的是"5次失败+30秒探测周期",这是根据平均接口响应时间和失败率算出来的。阈值太小会导致抖动误判,太大则保护失效,需要结合实际指标调整。
3. 实操过程与核心环节实现
3.1 定义技能Schema:从零搭建一个技能文件
我们从零开始搭一个完整的技能定义。我采用的是JSON Schema作为技能描述的标准格式,因为JSON有很好的可读性,也方便存储、校验和传输。一个标准的技能定义结构如下:
{ "name": "track_order", "description": "当用户询问订单物流轨迹、包裹位置、签收状态时使用。支持按订单号或运单号查询。不要用于查询商品库存。", "version": "1.2.0", "atomic": true, "tags": ["logistics", "order", "tracking"], "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单编号,格式为ORD开头的14位字符串" }, "tracking_number": { "type": "string", "description": "快递运单号,通常为12-15位数字" } }, "one_of_required": ["order_id", "tracking_number"] }, "execute": { "type": "api_call", "endpoint": "/api/v1/logistics/track", "method": "GET", "timeout_ms": 3000 }, "fallback": { "type": "reply_text", "content": "暂时无法获取物流信息,请稍后再试。" } }这个Schema里,name是技能唯一标识,description给模型看的触发说明,atomic标记技能层级,input_schema定义参数结构和校验规则,execute定义具体怎么执行,fallback定义失败时的降级动作。
框架层面,我基于Python写了一套轻量的技能注册与调度框架。核心代码结构是这样的:
class SkillRegistry: """技能注册中心:负责技能注册、检索与分发""" def __init__(self): self._skills = {} self._index = None def register(self, skill: dict): """注册一个新技能""" name = skill["name"] if name in self._skills: raise ValueError(f"skill {name} already exists") self._skills[name] = skill self._rebuild_index() def search(self, query: str, top_k: int = 10): """根据用户请求检索候选技能""" candidates = self._keyword_search(query) if len(candidates) < 3: candidates = self._vector_search(query) return candidates[:top_k] def dispatch(self, skill_name: str, params: dict): """执行技能并返回结果""" skill = self._skills.get(skill_name) if not skill: raise KeyError(f"unknown skill: {skill_name}") return execute_skill(skill, params)SkillRegistry这个类的职责非常单一:维护技能列表、响应检索请求、派发执行任务。实际的业务逻辑都放到execute_skill函数里,这个函数会根据技能Schema里的execute配置,决定走API调用、本地函数执行还是其他类型的动作。
3.2 技能调用链路的完整配置流程
有了技能定义和注册中心,接下来要解决的是整个调用链路怎么串起来。我设计的链路是"用户请求→意图路由→技能候选集→模型决策→技能执行→结果返回",每一步都有明确的输入输出和日志记录。
第一步,用户请求进来后,先做意图路由。这一步跑得很快,用的是关键词匹配加向量检索,目标是把技能候选集从几十个缩小到个位数。这一步不需要大模型参与,所以延迟很低,通常在几十毫秒内完成。
第二步,把候选技能的描述、参数Schema和用户原始请求一起拼成Prompt,交给大模型做最终决策。模型需要做两件事:一是判断是否需要调用技能,二是给出技能的具体参数。为了减少模型输出格式错误,我用的是结构化输出约束,让模型返回JSON格式的调用指令。
第三步,解析模型的输出,校验参数格式。这里有一个我踩过很多次的坑:模型会自动补全你没要求的参数。比如你只让它查"订单号12345",它可能自作主张把"用户手机号"也填上了。所以在参数校验环节,我做了严格的"允许参数白名单"过滤,超出声明范围的参数一律丢弃。
第四步,执行技能并拿到结果。如果执行成功,把结果结构化地拼回给模型,让模型基于结果组织自然语言回复;如果执行失败,触发降级策略,直接给用户返回兜底文案。
下面是这个链路中一次真实调用的日志摘要,可以看到每步的耗时和结果:
| 链路环节 | 耗时 | 结果 |
|---|---|---|
| 意图路由 | 35ms | 命中候选项:track_order, get_order_info |
| 模型决策 | 680ms | 选择track_order,参数完整 |
| 参数校验 | 5ms | 通过,order_id符合格式规范 |
| API执行 | 420ms | 成功,返回物流轨迹数据 |
| 回复生成 | 520ms | 模型组织自然语言回复 |
3.3 技能参数注入与上下文管理
技能执行不是孤立的,它需要理解对话的上下文。我的处理方式是把对话历史中的关键信息提取成"上下文变量",在执行技能时自动注入到参数里。
举个例子,用户先问"帮我查一下我最近买的那个扫地机器人到哪了",这里没有直接给出订单号,但上下文里可能有之前的订单信息。我在上下文管理模块里维护了一个"实体记忆库",从最近N轮对话里抽取订单号、商品名、用户ID这些实体,当技能参数缺省时,会自动从记忆库补全。
上下文管理的实现要注意一个边界:不是所有实体都适合自动注入。涉及用户隐私的信息,比如手机号、身份证号,必须显式询问用户确认后才能用。另外,实体的时效性也很重要——用户三个星期前提到的一个订单号,如果在当前对话里没有再次确认,不能直接拿来做参数。
从工程实现角度,我在技能执行前增加了一个"上下文适配器",它的职责是把上下文变量映射到技能Schema里的参数。适配器的映射规则可以预先配置,比如"上次提到的order_id -> input_schema.order_id"。这样技能本身保持纯净,不需要感知复杂的对话状态。
3.4 低成本验证技能效果的方法论
技能系统上线前,我用了一套低成本但行之有效的验证方法,不需要搭建完整的大模型推理环境,用一个脚本就能完成基本的准确性检查。
方法是这样的:准备一组测试用例,每个用例包含"用户输入+期望调用的技能+期望参数"。然后写一个测试脚本,把用户输入发到一个"模拟器"里。这个模拟器不做真实的API调用,只做技能检索和参数解析,然后返回命中的技能名和解析出来的参数。脚本自动对比模拟器的输出和期望值,统计准确率。
def test_skill_match(test_cases): total = len(test_cases) correct = 0 errors = [] for case in test_cases: user_input = case["input"] expected_skill = case["expected_skill"] candidates = registry.search(user_input, top_k=5) # 简化:直接判断期望技能是否在候选集中 if expected_skill in candidates: correct += 1 else: errors.append({"input": user_input, "expected": expected_skill, "candidates": candidates}) accuracy = correct / total print(f"技能匹配准确率: {accuracy:.2%}") return errors这套验证方案跑起来只需要纯CPU环境,成本几乎为零。我每次修改技能描述或者新增技能之后,都会跑一遍回归测试,准确率不降级才能合并到主分支。这个方法让我在后期的迭代中少踩了很多坑——很多技能改动看似合理,跑测试才发现对已有场景造成了干扰。
4. 常见问题与排查技巧实录
4.1 技能匹配不准的根因分析
在所有接入 agent-skills 的项目里,出现频率最高的问题就是"技能匹配不准",具体表现是用户明明想查物流,模型却调了查订单接口。我排查这个问题的思路是从三个层面逐层深挖:技能描述、意图路由、参数上下文。
第一层看技能描述。描述里有没有把触发条件和边界情况写清楚?比如"查物流"技能的描述里如果只写了"查询物流信息",那模型分不清"物流信息"和"订单信息"很正常。我前面提到过的第四版描述,就是针对这类问题做的优化。
第二层看意图路由。路由阶段的关键词匹配如果命中了错误的技能,模型就会在错误的候选集里做选择。我排查时会打印出路由阶段的候选集列表,看看是不是真实的意图对应技能被错误地排除了。
第三层看参数上下文。有些情况下技能选对了,但参数提取错乱导致执行结果不对。比如用户说"帮我查一下上个订单",上下文记忆库如果取不到"上个订单"的实体,就会导致参数缺失。排查这类问题时,我会重点看"实体记忆库"里到底存了什么内容。
4.2 一次典型的技能误调用排查实录
说一个我实际处理过的案例。某个智能客服项目上线半个月后,有用户反馈说"我没让机器人改地址,它却提示我收货地址已修改"。这明显是"修改地址"技能被误触发了。
排查过程从日志开始。我把那段时间的调用日志拉出来,定位到误触发的那条会话,发现用户当时说的是"我要去别的地方出差,这个快递怎么办"。模型把这个请求匹配到了"修改收货地址"技能上,然后调了改地址接口。从语义上理解,用户表达的是担心包裹没人收,最佳的技能应该"改派快递柜"或者"改配送时间",模型选错了。
问题的源头在技能描述不够精细。"修改地址"技能的描述里没有写清楚它的边界是"修改配送地址",没有说明如果用户本意是"修改配送时间"应该走哪个技能。我随后做了一轮技能描述补充,给涉及用户敏感操作的技能都补上了"边界警示"——在描述里明确写上"不适用于XX场景,此类情况应使用YY技能"。
这轮优化之后,类似的误触发没有再出现。我得到的教训是:技能的安全边界描述和技能功能描述同样重要,甚至更重要,因为涉及敏感操作的技能一旦误触发,损失的不只是用户体验,还可能造成实际的数据变更。
4.3 技能执行失败后的恢复策略
技能执行不可能永远成功,下游接口抖动、参数过期、依赖服务升级都可能导致失败。我的恢复策略按照失败原因分了三档处理。
第一档是瞬时类失败,比如网络超时。这类失败我会自动重试一次,重试间隔设置一个较短的延时。如果重试还是失败,就降级到兜底话术,不让用户感知到系统的不稳定。
第二档是参数类失败,比如订单号查不到、用户不存在。这类失败不是系统故障,而是用户的输入本身有问题。这时候技能的返回值会携带错误码,模型拿到错误码后会用话术引导用户检查输入。
第三档是依赖类失败,比如下游接口连续报错。这类失败触发熔断器,让技能进入降级状态,后续请求不再路由到这个技能。同时后台会记录告警信息,运维人员介入排查。
恢复策略的经验一句话总结:不要把恢复逻辑写死在技能里,而是要交给调度框架统一处理。我在每个技能定义里只声明"fallback"行为,具体什么时候重试、什么时候熔断,都走框架层统一的治理策略,这样才能保证行为一致。
4.4 高频踩坑点速查表
整理一个高频踩坑速查表,都是我实际踩过的坑,按频率排个序:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 模型总是选错技能 | 技能描述边界不清、候选技能过多 | 重写描述、增加边界警示、技能分级 |
| 参数解析错误率高 | 没有用结构化输出、Schema定义模糊 | 强制JSON结构化输出、严格参数白名单 |
| 上下文中旧实体干扰新对话 | 实体记忆未做时效控制 | 增加会话轮次上限、过期自动清理 |
| 新增技能影响已有技能 | 描述之间语义重叠 | 每次改动后跑回归测试、互斥声明 |
| 技能执行时间过长 | 技能内部串行调用太多接口 | 并行化改造、增加超时和熔断 |
5. 扩展思路:把技能体系变成组织能力
5.1 技能的可复用性与持续沉淀
agent-skills 体系运行一段时间后,我最大的感受是:技能是一种可以持续复用的组织资产。项目A里写好的查询技能,稍微调整下参数Schema,就能在项目B里复用。跨项目复用时,不需要改动技能框架和调用链路的代码,只需要重新梳理描述文本和API映射。
如何保证技能的可复用性?我的经验是时刻保持技能定义的"纯净"。一个技能只做一件事,不要把两个业务逻辑混在一个技能里。比如"查订单"和"申请退款"这两个动作,我很早之前把它们合在了一个技能里,导致后来模型经常在用户只要求"查一下订单"时,就误触发了"退款申请"的流程。拆分成独立技能之后,不仅误触发问题解决了,这两个技能也各自被其他项目复用了。
技能的沉淀还体现在测试用例上。每排查一个线上问题,我会把对应的测试用例补充到回归测试集里,防止同样的问题换个场景重新出现。这套测试集本身就是团队宝贵的知识库,新员工上手时跑一遍,就能理解每个技能的能力边界。
5.2 从技能到流程编排的进阶路径
当技能数量达到一定规模,纯靠模型在运行时动态编排所有技能,既不可控也不经济。我逐渐引入了一种"半自动编排"的思路:把高频的、标准化的业务路径固化成复合技能,只把真正的长尾场景交给模型自由编排。
以售后场景为例,"退款+退货"是一个固定流程,我把它固化为复合技能,里面依次调用"校验订单状态""校验售后时效""生成退货单""发起退款"四个原子技能。模型只需要决策"用户要走退货退款流程",具体怎么走,由复合技能自己控制,准确率和效率都大幅提升。
真正需要模型发挥编排能力的场景,是那些没有标准路径的开放问题。比如用户说"我的订单出了点问题,你帮我看下怎么办",这时候模型需要理解问题全貌,动态组合合适的技能链路。在这种场景下,我会给模型提供更大的编排自由度,但也会通过上下文注入"边界约束",确保它不会越权执行敏感操作。
5.3 给初学者的落地建议
如果你正准备在自己的项目里引入agent-skills式的技能体系,我建议从三个步骤开始。
先找一个清晰的业务场景,别贪多,选一个最高频的需求做第一个技能。把这个技能的描述文本打磨到极致,跑通完整的调用链路,验证准确率和稳定性。
再增加第二、第三个技能,观察它们在候选列表里会不会互相干扰。这个阶段重点学的是技能描述怎么写才能让模型分清楚边界。
最后逐步扩大技能规模,同时补上路由检索、熔断降级、回归测试这些治理能力。技能数量和系统复杂度是同步增长的,治理能力跟不上,技能越多系统越脆弱。
提示:如果你只是想做一个小演示项目,不需要一开始就铺开全套治理能力。先手工配置三五个技能,把链路跑通,再按需逐步加能力,避免过度设计。
6. 写在最后的个人心得
我在多个项目里验证过agent-skills的可行性,从智能客服到内部知识助手,技能体系带来的收益是一致的:模型输出更可控,业务逻辑更可维护,新增能力更快上线。但也有一个需要清醒认识的边界——技能体系解决的是"模型如何正确决策并执行"的问题,如果大模型本身能力不足、或者业务逻辑本身是混乱的,再好的技能框架也救不了。
我个人的体会是,设计技能体系的最难点不在于写代码,而在于梳理业务边界。你得清楚哪些操作是原子的、哪些流程是标准的、哪些任务需要动态编排,这本质上是对业务逻辑的一次深度整理。整理得越清楚,技能体系就越稳定。
最后再补一个小技巧:技能描述里使用"负面约束"通常比"正面描述"更有效。与其写"当用户询问物流信息时使用",不如同时写"不要在用户只询问订单金额时使用"。大模型对"不要做什么"的理解,往往比对"要做什么"的理解更精准,这个小细节能明显减少误触发。