最近一直在做 agent-skills 方向的东西,说白了就是给智能体搭一套“能干活的技能库”。很多人以为 Agent 的价值全在模型本身,但实际跑过几个项目就会发现,真正拉开差距的是技能体系:你怎么拆技能、怎么描述技能、怎么让模型在你定义的边界内稳定调用技能。这篇文章不聊概念,只聊我在落地 agent-skills 过程中总结出来的设计思路、踩过的坑、以及一套可以直接照抄的实操方法。适合正在做智能体应用的开发者、AI 产品经理,以及想把大模型接进真实业务流程的团队。
1. 先拆开看:Agent Skills 到底在解决什么问题
1.1 从“能聊天”到“能干活”,差距就在技能体系上
大模型的强项是自然语言理解和生成,但“干活”从来不只是说话。查订单、发邮件、改数据库、调接口,这些动作需要模型能够准确理解用户意图,然后调用外部工具,拿到结果,再组织成用户能看懂的回答。这个过程里,模型本身只是“大脑”,真正动手的是技能。
agent-skills 本质上就是把“模型能做什么”和“系统能做什么”之间的桥梁拆成可复用的单元。每个技能对应一个明确的能力边界,有清晰的输入输出、权限约束、超时策略和失败处理。比如你做一个售后客服智能体,订单查询是一个技能,物流跟踪是另一个技能,退货审核又是一个技能。它们各自独立,但可以被同一个 Agent 统一调度。
我见过不少团队第一次搭 Agent 时,什么功能都写在 Prompt 里,让模型自由发挥。结果就是同一句话今天能答对、明天答不对,换了一个用户口音就崩。问题不在于模型不行,而在于你没有一个可控的执行层。Skills 的意义恰恰是让不确定性收敛:模型只负责“决定做什么”,具体的“怎么做”交给注册好、测试过、可观测的技能去完成。
从影响范围来说,这套思路能覆盖的场景非常广。自动化办公、客服问答、数据分析、运维告警处理、内容生产,只要业务流程可以被拆成“输入-处理-输出”的步骤,就能用 agent-skills 的方式承接。它解决的核心痛点是:AI 应用从 Demo 走向生产环境时,必须具备的可维护性、可测试性和可治理性。
1.2 技能拆还是不拆?我用的三个判断标准
很多初学者最容易犯的错是把技能切得要么太粗、要么太碎。太粗的话,技能内部塞了大量条件分支,本质上又变回了一个函数;太碎的话,模型在一个任务里要来回调用七八个技能,上下文转换开销大到离谱。
我一般用三个标准判断一个能力要不要拆成独立技能:
- 这个能力会不会被多个流程复用?比如“发送邮件”几乎每个业务都会用到,拆出来就是划算的。
- 它有没有明确的输入输出边界?输入是几个参数,输出是固定结构,技能内不需要理解太多全局上下文,就具备拆出来的条件。
- 它是否需要独立的权限或成本控制?比如某个能力要调用第三方付费 API,单独封装才能做限流、鉴权和账单隔离。
反过来,如果一个动作只在某个特定流程里出现一次,或者它高度依赖前后几步的状态,那就不要硬拆。我之前在智能客服项目里把“生成回复文案”和“发送回复消息”拆成两个技能,结果每次都要把用户完整对话历史传给“生成文案”,再把生成结果传给“发送消息”,中间多了一倍 token,还经常因为上下文截断导致文案前言不搭后语。后来合并成一个“生成并发送回复”技能,反而稳定很多。
拆技能的核心不是“越小越好”,而是“边界越清楚越好”。一个技能哪怕内部逻辑很复杂,只要对外接口清晰、行为可预期,它就是好的技能。反过来说,一个只做了半件事的原子操作,如果没有完整的输入上下文,模型根本不知道怎么编排它。
2. 一次完整落地:技能清单、注册与编排
2.1 先从业务场景反推能力清单
我搭技能库时不会上来就写代码,而是先拿一沓便利贴,把业务场景里所有“用户可能会提的要求”列成动词短语。比如做一个“订单管理助手”,你可能会得到这么一串:查订单、改地址、催发货、申请退款、开发票、问库存、算运费、找客服…… 这些都是潜在技能。
收敛之后,我会再评估哪些动词是用户高频使用的,哪些是核心流程的关键节点。第一版技能库只保留 10 个以内技能,绝不贪多。技能太多,模型在路由阶段的选择成本会急剧上升,而且彼此间很容易出现描述重叠,导致模型调错。
下面是我常用的技能清单模板:
| 技能名 | 触发场景 | 输入参数 | 输出内容 | 依赖资源 |
|---|---|---|---|---|
| 订单查询 | 用户问订单状态、商品明细 | 订单号、手机号 | 订单状态、商品列表、金额 | 订单库只读权限 |
| 物流跟踪 | 用户问快递到哪里了 | 物流单号 | 物流轨迹节点、预计时间 | 物流接口 |
| 退款审核 | 用户申请退款 | 订单号、退款原因 | 审核结果、退款金额 | 财务系统权限 |
| 发票申请 | 用户要开发票 | 订单号、抬头、税号 | 发票信息或申请回执 | 开票服务 |
这个表的价值在于它强制你把每个技能的边界写下来。写不出来的技能,说明你还没想清楚,先不开发。
2.2 技能注册表怎么写:元信息、权限与依赖声明
技能落地时,我建议用中心化注册表统一管理,而不是散落在代码里。注册表本身可以是一个 YAML 文件、数据库表或者专门的配置服务。每个技能至少要包含以下字段:技能唯一标识、名称、描述、参数 Schema、权限、超时时间、是否幂等、依赖关系。
描述这一栏最容易被人忽略,但它恰恰是最关键的。因为大部分 Agent 是靠模型理解描述来决定是否调用技能的。描述写得太模糊,模型就会乱选;写得太具体,又会把技能限定死。以前端同事沟通,我会给一句标准:“描述要像一个新同事看这份文档,不需要再问你就知道什么时候该用这个技能。”
举个例子,一个订单查询技能的 YAML 定义:
- name: order_query description: > 当用户询问订单状态、订单内容、下单时间、支付金额等信息时使用。 不要用于查询快递物流轨迹,物流轨迹请使用 logistics_tracking。 parameters: order_id: type: string description: 订单号,通常是 17 位数字,必须来自用户输入或系统会话上下文 mobile: type: string description: 下单手机号后四位,用于二次校验,可空 permissions: - order:read timeout_ms: 3000 idempotent: true dependencies: []注意描述里明确写了“物流轨迹请使用另一个技能”,这就是在帮模型做路由消歧。参数 Schema 里我特别强调了“必须来自用户输入或系统上下文”,这会显著减少模型编造订单号的情况。
注册表写完之后,还要一个简单的技能引擎去加载它们。引擎启动时校验 Schema、注册路由、建立健康检查,运行时按需执行。这个过程我把它叫作“从代码里的函数到系统里的技能”的转变:技能不再只是方法,而是拥有元数据、权限和监控的独立服务单元。
3. 核心实现:上下文、路由与重试
3.1 技能路由:意图识别与参数抽取
技能路由是 Agent 每轮对话都会发生的动作。我现在的做法是“规则优先、模型兜底”的混合路由。对于高频、固定的表达,直接写正则或关键词匹配;对于长尾表达,再把候选技能列表和用户输入交给模型去选。
def route(user_input, candidate_skills): # 1. 先尝试规则匹配,命中直接返回 hit = rule_match(user_input) if hit: return hit # 2. 规则未命中,交给模型做意图筛选 selected = llm_select(user_input, candidate_skills) return selected这样做的原因很简单:规则路由成本低、响应快、可预期,能把 80% 的高频问题稳定拉住;模型路由只处理剩下的 20%,即使偶尔选错也在可控范围内。路由完成之后,紧接着做参数抽取。这一步和意图识别我建议分开做,而不是让模型一个动作同时完成。原因在于同时预测两件事会显著增加参数幻觉概率。
参数抽取时,我会把参数 Schema 和示例值一起给模型。比如“查订单”需要订单号,示例就写“订单号一般是 17 位数字,类似 20240315000123456”。一方面让模型知道格式,另一方面让它学会从用户原句中找值。抽出来之后,还要做格式校验,不合格就打回重抽或者要求用户补充。
3.2 上下文压缩:别把对话历史全塞给技能
我在早期项目里发现,技能调用的输入如果直接拼接整个对话历史,会带来两个问题:token 消耗暴涨、模型在长上下文里更容易忽略真正重要的字段。技巧是要为每个技能建立独立的“状态槽”,把对话历史里和当前技能相关的信息提取出来,槽位填充完毕后再调用技能。
例如“退款审核”技能需要订单号、退款原因、客服备注。对话历史可能有十轮,但真正需要的可能只在最早两轮里出现过。我会用一个轻量抽取器把这三个槽位从历史里找出来,找不到的槽位再通过追问补齐。这样传给技能的结构化输入通常不超过 200 token,但准确性比塞 3000 token 历史高得多。
上下文压缩还有一个原则:技能不该关心无关上下文。模型需要负责的是“从对话里理解用户要什么”,技能需要负责的是“把用户要的事情办成”。中间需要一个适配层,把用户意图翻译成技能参数。我在工程上把这个适配层叫做“技能前置处理器”,它和技能本身分开维护。
3.3 失败重试与降级策略
技能调用失败是常态,不设计好失败路径,Agent 就会变成复读机。我会把所有失败统一成一个结构化错误对象,里面带错误码、可重试标记、建议动作。
class SkillFailure(Exception): def __init__(self, code, message, retryable=False): self.code = code self.message = message self.retryable = retryable可重试的失败(超时、网络抖动、上游临时 5xx)使用指数退避重试,最多三次;不可重试的失败(参数非法、鉴权失败、业务规则校验不通过)直接返回给用户,并给出修正建议。降级策略则是在主技能不可用时,自动切换到一个备选技能或者返回缓存结果。比如物流接口挂了,可以先返回订单本身的发货状态,告诉用户“物流明细暂时获取不到,但订单已发货”,而不是让整个对话卡死。
降级方案一定要提前写在技能注册表的 dependencies 或者 fallback 字段里。我在某个跨平台系统里就吃过没有降级的亏:天气查询服务超时后,Agent 直接报错,用户体验极差。后来接了一个离线天气库作为 fallback,虽然数据不是实时,但至少用户不会面对一片空白。
4. 实战中踩过的坑:技能冲突、任务碎片化与参数幻觉
4.1 技能互相抢活,路由优先级怎么定
技能一多,最典型的坑就是两个技能描述重叠,模型随机翻牌。有一次我们的“订单查询”和“物流跟踪”同时能回答“我的快递到哪了”,结果同一句话有时候走订单接口,有时候走物流接口,返回的内容完全不一样。
解决思路有三个,可以同时用:
- 在技能描述里显式声明“不要做什么”,至少写清楚和容易混淆技能的边界。
- 在路由层维护一个优先级列表,当模型对候选技能置信度接近时,优先走规则表里指定的那个。
- 对高频混淆场景做一条规则兜底,比如用户提到“快递”“物流”“到哪了”就直接路由到物流跟踪,不让模型做选择题。
我还发现一个细节:技能描述里的位置会影响模型判断。描述放在注册表靠前位置的技能,被选中概率会偏高。所以不要把冷门技能放在热门技能前面,这也算一个优化点。
4.2 任务被拆太碎,Agent 反而丢掉全局
第二个坑是过度拆分。我们曾经把“生成周报”拆成“读取数据”“总结要点”“排版”“发送”四个技能,理想中模型应该依次调用。实际跑下来经常出现前后割裂:读取数据用的是 A 口径,总结时模型又自己脑补了 B 口径,最后周报里的数字对不上。
后来我把这种强流程型任务改成了“工作流模板”模式。技能仍然是原子能力,但原子能力之间不再由模型自由串联,而是由预先定义的 DAG(有向无环图)编排。模型只需要在入口处判断“用户要生成周报”,然后触发整条工作流。模板会显式定义步骤、每一步的输入来源、输出流向。
这个改变让我得出一个结论:agent-skills 体系里,“技能”负责原子动作,“工作流”负责流程编排,两者必须分开。模型擅长的是从一个候选清单里选一个技能,而不是在小步快跑的流程里做几十次决策。每多一次决策,就多一次出错的机会。
4.3 参数幻觉的排查思路
参数幻觉是 Agent 落地中最恼人的问题之一,典型表现是模型“一本正经”地编出订单号、日期、金额。排查时我会按顺序检查三件事:
- 参数有没有可能从系统上下文里拿到?如果能,就做“上下文回填”,优先覆盖模型生成值。
- 参数校验规则是不是太宽松?比如订单号应该包含数字和特定前缀,那就不能只校验非空。
- 模型是否被要求在同一个步骤里做了太多事情?如果它在一次调用里既要选技能又要抽 5 个参数,幻觉概率会大增。
我见过一个案例,用户说“帮我查一下上周的订单”,模型把“上周”当成了一个日期参数,生成了一个根本不在任何时间区间里的具体日期。后来我们把所有时间类参数都改成相对时间解析器,由代码去算日期范围,模型不再负责生成具体日期。参数幻觉的根因,很多时候是让模型做了本该由代码完成的事。
4.4 问题排查速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 模型不调用任何技能 | 技能描述不清晰或候选列表未传入 | 检查路由上下文,确认技能注册表已加载 |
| 调用技能但参数格式错误 | 参数 Schema 缺少示例 | 在参数描述里补充示例值 |
| 多个技能返回冲突结果 | 描述边界重叠 | 增加“不要使用”说明和路由优先级 |
| 任务执行一半中断 | 技能超时或未设置重试 | 检查超时时间,配置失败补偿动作 |
| 技能执行成功但回答错误 | 技能输出没有被正确映射 | 增加前置处理和后置格式化层 |
这张表我一般贴在项目文档最前面,出现问题时先对照排查,而不是盲目调 Prompt。
5. 可测试性与可观测性:让技能库能持续演进
5.1 为每个技能建一个“最小验证集”
技能库不是写一次就完了,它会随着业务变化不断改。改技能最怕的是改完一个,影响了另一个。所以我会给每个技能维护一个最小验证集,一般包含三类用例:正常场景、异常场景、边界场景。
以“订单查询”为例:
TEST_CASES = [ {"input": "我的订单 20240315000123456 到哪一步了", "expected_skill": "order_query", "expected_params": {"order_id": "20240315000123456"}}, {"input": "订单状态看不了", "expected_skill": "order_query", "expected_params": {"need_more_info": True}}, {"input": "快递送到哪里了", "expected_skill": "logistics_tracking", "expected_params": {"logistics_no": None}}, ]这些用例会被跑在一个回放环境里,使用 mock 接口,不请求真实上游。每次修改技能描述、参数 Schema 或者路由逻辑后,一键回放所有验证集。一旦有用例没通过,就说明这次的改动引入了回归。这个习惯救了我很多次,因为技能类改动不像传统 Bug 那样有堆栈报错,很多问题是静默的,只有回放才能发现。
5.2 观测指标怎么埋:成功率、调用链、资源消耗
技能上线后,必须能回答三个问题:技能跑得稳不稳?用户的问题有没有被正确路由?哪些技能正在消耗最多的成本和时间?
我会给每个技能记录结构化日志,字段固定为:技能名、参数摘要、返回码、耗时、重试次数、调用来源、模型版本。然后按天聚合以下指标:
| 指标名 | 含义 | 目标 |
|---|---|---|
| 技能调用成功率 | 成功次数 / 总调用次数 | 核心技能需 > 98% |
| 路由命中率 | 正确技能被选中次数 / 总次数 | 需持续监测趋势 |
| 参数抽取错误率 | 参数校验失败次数 / 总调用次数 | 新技能上线后重点观察 |
| 技能平均耗时 | 每次调用的响应时间 | 超过阈值触发告警 |
| 模型路由占比 | 规则路由 vs 模型路由的次数比例 | 模型路由占比过高时优化规则 |
这些指标不止是看板,更要设置告警。比如某技能成功率连续 10 分钟低于 95%,就立刻切到 fallback 方案,同时通知对应负责人。没有告警的可观测性,等于没有可观测性。因为技能链路的错误往往不表现为系统崩了,而是表现为用户默默流失。
另外,每次升级模型版本(比如换了一个新的底座模型),都必须在完整验证集上跑一轮回归。模型能力变化会影响技能路由的表现,哪怕只是模型“更聪明”了,也可能导致它不按描述的边界走。这个坑我踩过一次后才开始把模型版本纳入测试矩阵。
6. 按我的经验,搭建技能库最容易上手的顺序
如果你现在准备从零开始搭一套 agent-skills,我建议不要急着铺开几十个技能,而是按这个顺序走:
第一步,选第一个技能时,不要选“发邮件”“写代码”这类写操作,优先选一个只读、低风险、结果容易验证的技能。比如“查订单”“查天气”“查文档”。只读技能的好处是即便调用错误,也不会对业务数据造成不可逆影响,非常适合用来跑通注册、路由、执行、反馈的完整链路。
第二步,用中心化注册表把这一个技能定义好,包含描述、参数 Schema、超时和权限。然后接一个最简路由:规则优先,模型兜底。这个阶段的目标不是“智能”,而是“确定”。
第三步,跑通三到五个只读技能,建立一套最小验证集,然后才开始加写操作类技能。写操作类技能必须有更严格的权限校验、操作前确认、操作审计。我在模拟项目 X 里就是这样:前两周全部在做只读技能,流程稳定后第三周才接了“创建工单”和“发送通知”,出错率明显低很多。
第四步,每增加一个技能,就回到验证集跑一遍全量回归,并且把新技能和已有技能的边界写清楚。技能库最忌讳一次性扩张,一旦路由准确率滑坡,排错的复杂度会指数上升。
最后,用前面说的结构化日志和指标把系统跑起来。你会发现 agent-skills 的开发重点很快就从“写 Prompt”转向“做治理”:怎么让技能列表清晰、怎么让失败有章法、怎么让每一次调用都有迹可循。
我个人在实际操作中一个很深的体会是:不要把技能体系设计得过于复杂。注册表、路由、校验、重试、观测,这五件事做好,已经足够支撑大多数生产级应用。很多团队一上来就想做可视化编排、多智能体协作、自进化技能,结果连最基础的技能稳定性都没达标。先把一个技能做到 100 次调用不出错,再谈一百个技能怎么协同,顺序千万不能反。