1. 为什么"给Agent加技能"不是把API塞给它那么简单
过去一年里我接触过不少号称"Agent落地"的项目,尝鲜阶段大家做的事情几乎一样:把几个外部API封装成工具函数,在系统提示词里写一行"你是一个AI助手,你可以调用以下工具",然后就宣称Agent已经"会"这些能力了。跑到真实场景里一测,结果通常很惨:Agent要么在工具选择上犹豫半天,要么不按约定的参数格式传值,要么明明调用成功了却把结果用错地方。
问题出在哪?出在我们把"技能"理解成了"工具"。
工具是单向的——给它输入,它返回输出,仅此而已。但技能是Agent能力的一种内化形态,它包含了三样东西:触发条件(什么场景下这个技能该被激活)、使用规范(参数怎么传、结果怎么解读、中途出错怎么办)、边界意识(哪些情况是它处理不了的,需要把控制权交回去)。没有这套完整定义,你塞给Agent的只是一堆冷冰冰的函数签名,并不是它能自如运用、可信交付的能力。
这篇就围绕agent-skills这个命题,聊聊我在实际项目里搭建Agent技能体系的完整过程——从技能如何定义、注册、编排,到测试中踩过的坑,再到怎么让Agent从"会调用"进化到"真正学会了这个技能"。适合那些正在做Agent应用开发、被工具调用不稳定问题反复折磨的工程师,也适合刚入行想系统性理解Agent技能机制的读者。
2. 先拆清楚:技能不是Prompt,也不是单纯函数注册
2.1 三者的区别先从一次翻车开始
我最早做客服场景的Agent时,团队里最简单粗暴的方案,就是把查订单、退换货、优惠券核销这些接口全塞进一个大Prompt,然后期待模型自己理解哪个场景该调哪个。上线第一周,订单查询接口的调用成功率不到70%,但系统日志显示Agent调用的次数远超真实订单量——它把什么都往这个接口上试。
后来我总结出问题的根源:工具调用本质上是在"被命令时执行",而技能是"在正确的场景下自主启用"。这两者对Agent的要求完全不同。
具体来说,我把这三层做了个对照:
| 维度 | Prompt指令 | 函数注册(工具) | Agent技能 |
|---|---|---|---|
| 核心载体 | 自然语言描述 | 函数签名+JSON Schema | 技能元信息+使用规范+学习样例 |
| 触发方式 | 模型自由揣测 | 模型自主选择但无约束 | 规则+上下文判断双通道触发 |
| 失败模式 | 静默出错 | 参数校验报错,Agent不知所措 | 技能自身携带异常处理与回退策略 |
| 可进化性 | 改Prompt全局漂移 | 改函数全局影响 | 技能独立迭代、版本化、可评估 |
站在工程角度看,如果你只做Demo,函数注册就够了。但凡是面对真实流量、真实业务,Agent需要的是一个技能层——一个把"能力边界""使用条件""学习样本"打包在一起的独立单元。
2.2 一套完整技能定义应该包含什么
我当前在项目里用到的技能定义模板,大致包含固定字段。这里用一个"查快递物流"的技能作为例子说明:
skill_name: wms_track_query description: 查询订单物流轨迹,支持快递单号、店铺订单号两种入口。 trigger_conditions: - "用户明确提供了快递单号或店铺订单号" - "用户表达查询物流进度的意图(如'到哪了''帮我查下物流')" - "结合上下文可确定某个订单就是用户所指的物流对象" required_parameters: tracking_no: type: string description: 快递单号,优先获取用户原文中的数字串 order_id: type: string description: 店铺订单号,当快递单号缺失时使用 output_interpretation: status_mapping: in_transit: "在途" delivered: "已签收" exception: "运输异常" fallback_response: "未查询到物流信息时,引导用户核对单号" error_handling: invalid_no: action: "向用户索要正确单号,不重复尝试" upstream_timeout: action: "告知稍后重试,切换为等待用户下一步指令" negative_examples: - query: "你们几点下班" reason: "不属于物流查询请求,不应触发该技能" learning_samples: - query: "我的快递到哪了" expected_action: "询问或提取订单号后调用"你别小看这份定义,它帮我在后面的框架设计里省掉了很多麻烦。最关键的是:技能元信息不是给模型读的,是给"技能注册中心"和"技能筛选器"用的。模型仍然只看到精简版的技能调用说明,完整的元信息用于路由判断和冲突消解。
3. 技能怎么设计:原子技能加复合技能的拆分策略
3.1 老话重提但确实好用:单一职责原则
Agent技能拆分的颗粒度,是整个体系架构里最需要拿捏的事。
我在项目早期犯过一个错:把"售后处理"做成了一个巨型技能,里面包含了退款判定、物流拦截、补偿方案推荐、工单创建四个子能力。后果就是Agent每次调用时都在犹豫到底要不要走到退款环节,一个简单问题被模型复杂度放大成一大段操作。
后来我把技能拆成了原子技能(Atom Skill)和复合技能(Composite Skill)两层,设计原则有这么几条:
- 原子技能只做一件事,参数不超过5个,输出结构明确
- 复合技能是固定流程的编排,内部按顺序调用多个原子技能,但对外暴露为一个入口
- 判定逻辑尽量留在编排层,不要让模型自己临时决定调用哪个步骤
举例来说,"退款处理"就是一个复合技能,它内部固定了这样的顺序:先鉴权、再查订单状态、再查售后策略、最后提交退款单。每个子步骤各自是一个原子技能。Agent只需要决定"该不该发起这个流程",而不用在流程中途随机跳步。
这么设计的理由很朴素:模型的强项是意图判断和语言生成,而流程的稳定性不应该依赖模型的临场发挥。把流程写死,把决策点收窄,出错的概率会小一个数量级。
3.2 现实中怎么梳理技能清单
我第一次做技能盘点时,对着一个后台系统的几十个接口发愁——不知道哪些该做成技能,哪些保持普通工具就行。后来我按这个思路收敛:
第一类必须做技能:有状态、与用户长期目标绑定的事务性操作。比如"预约改期""退款进度跟进"。这些操作影响用户资产的变动,必须走技能流程,不能只做接口透传。
第二类建议做技能:需要结合上下文解释结果的查询类操作。比如"订单物流""库存查询"。Agent不仅要调接口,还要把结果转换成语义正确的自然语言答复。
第三类保持工具即可:无副作用、参数完全由用户直接提供、结果无需加工的基础查询。比如"获取当前时间""汇率换算"。这类能力做成技能反而增加了不必要的Prompt开销和路由延迟。
你可以在自己的项目里画一张二维表,横轴是副作用等级(读写状态的程度),纵轴是语义判读复杂度(结果需不需要领域知识加工)。落在右上角的,放心做成技能;落在左上角的,做成普通工具就行;右下角通常很少存在,因为高风险又无脑的操作一般会被上游直接限制掉。
4. 技能注册与调度的工程实现细节
4.1 注册中心与技能清单的下发机制
技能定义完成之后,下一步就是工程接入。我在这里用了一个非常轻量的方案:技能注册表存YAML配置,运行时加载为JSON,再按需拼进Prompt上下文。
核心逻辑如下:
from typing import Any, Dict, List import yaml import json class SkillRegistry: def __init__(self, config_path: str): self.skills: Dict[str, Dict[str, Any]] = {} self._load_config(config_path) def _load_config(self, config_path: str) -> None: with open(config_path, "r", encoding="utf-8") as f: raw_config = yaml.safe_load(f) for skill in raw_config["skills"]: self.skills[skill["skill_name"]] = skill def list_skills(self) -> List[str]: return list(self.skills.keys()) def get_skill_brief(self, skill_name: str) -> str: skill = self.skills[skill_name] # 精简版技能说明,控制token占用 return json.dumps({ "name": skill["skill_name"], "desc": skill["description"], "params": skill["required_parameters"], }, ensure_ascii=False)这里有一点非常关键:不要把所有技能的完整定义全部塞给模型。原因很简单,GPT级别的模型对超长上下文里的信息利用效率并不是均匀的,技能定义冗长会稀释真正重要的指令信息。所以在技能超过5个之后,必须做筛选策略。
4.2 技能筛选器:减少模型的选择负担
我的做法是加一层轻量级的技能预筛逻辑,让Agent只面对可能相关的技能子集。
class SkillRouter: def __init__(self, registry: SkillRegistry): self.registry = registry self.ruleset = self._build_rules() def _build_rules(self): # 每个技能声明若干关键词与正则规则 return { "wms_track_query": { "keywords": ["物流", "快递", "到哪了", "签收"], "regex": [r"\d{12,16}"] }, "refund_flow": { "keywords": ["退款", "退钱", "退货", "售后"], "regex": [] } } def route(self, user_query: str) -> List[str]: matched = [] for skill_name, rule in self.ruleset.items(): if any(kw in user_query for kw in rule["keywords"]): matched.append(skill_name) continue if any(re.search(pattern, user_query) for pattern in rule["regex"]): matched.append(skill_name) return matched or self.registry.list_skills()规则命中时优先推荐,规则没命中时回退到全量技能列表。注意,这里筛选器不是硬性拦截,而是做"排序+剪枝"——把明显无关的大概率从候选里去掉,同时给模型保留呼吸空间。这样做之后,我的Agent在意图识别准确率上提升了一个台阶,因为候选技能从15个降到了3个左右,模型不用再大海捞针。
这个思路和人类做事很像:你手里有一把瑞士军刀,正常情况下你不会每次都用指尖感受一遍所有刀头的形状,而是扫一眼知道哪把刀在哪个槽位,直接抽出来用。技能路由做的事情,本质上就是把这个"扫一眼"变成确定性的程序逻辑。
5. 让Agent真正掌握技能:从调用到内化
5.1 调用正确不等于技能掌握
我在项目第二个阶段遇到了一个颇具迷惑性的现象:技能的调用参数规范达标率已经很高了,但用户满意度并没有跟着涨。后来一查对话记录,发现了几个"每次都对但整体很蠢"的操作:
- 用户问"这个订单能不能改地址",Agent确实调用了订单查询技能,但查询完只回复了"可以修改"两个字,没有继续引导修改流程
- 用户说"帮我退了这个,顺便看看他家有没有别的替代品",Agent完成了退款技能,但完全没接住"推荐替代品"这个并行意图
这个现象揭示了一件事:Agent对技能的使用,停留在"触发-执行-汇报"的机械化阶段,没有把技能结果融入对话目标。真正掌握一项技能,需要模型理解这个技能在业务场景中的位置、它产出的结果如何服务于用户最终诉求。
为了解决这个问题,我在系统提示词层面做了两件事:
第一,在技能结果返回后增加了结果语义化阶段,要求Agent先解读结果再组织回复,不允许直接把JSON串丢给用户。
第二,给技能设置了后继动作链。比如"订单查询"技能之后,合法的跟随动作包括"改地址""申请退款""催发货";如果用户有明显后续意图,Agent必须推进流程,而不是停在原地。
5.2 学习样本:给模型看"正确姿势"
光靠文本约束还不够,我在技能定义里增加了learning_samples字段。每个技能配了2到3组"用户输入到技能选择与使用"的示例对话,最好是真实的线上数据脱敏而来。
你可能会问:这不就是few-shot吗,和写Prompt有什么区别?
有区别。few-shot通常堆在系统提示词最前面,一视同仁地给所有对话场景参考;而技能内的学习样本只会在该技能被命中时才注入。这样做的好处是节省token,更重要的是不会对其他技能的判断产生干扰。我在测试中发现,把A技能的错误示例放在全局Prompt里,会莫名拉高模型对B技能相关场景的误触率,因为某些表述是跨场景复用的。把样本收敛在技能内部,相互隔离,反而各得其所。
实际操作时,如果用的是OpenAI函数调用或类Anthropic工具调用格式,可以在system消息里对候选技能附带各自的learning_samples,模型会明显更稳定。这是我在对比了有无样本注入两组实验后的明确结论——技能调用稳定性提升了大约8个百分点。
5.3 通过"最小成功案例集"给技能做质量门槛
技能体系建起来容易,但怎么证明一个技能达到可上线标准,这是另一道关卡。我在项目里给自己定了一个"最小成功案例集"的要求:每个技能必须准备至少10个真实场景的输入,其中覆盖正常路径、边界输入、明显不该触发的负面输入。上线前,Agent在这组案例上的表现必须达到如下标准:
- 正常案例:技能触发率和参数正确率均为100%
- 边界案例:技能能正确拒绝或引导澄清,不硬调用
- 负面案例:技能不得误触发
这里有个容易被忽略的点:负面案例不触发和正面案例能触发同样重要。很多项目死在误触发上——模型把什么都当成技能调用请求,导致业务系统被打出一堆无效操作。我甚至为此单独建立了一个名为"NotToTrigger"的技能约束清单,在候选技能生成阶段就明确写入哪些场景禁止调用哪个技能。
6. Agent技能实战中频繁翻车的四个诊断点
6.1 超时与长时间任务:技能执行状态必须可观测
我的第一个线上事故就和超时有关。技能调用的是一个比较慢的物流接口,最慢要5秒多,而Agent平台的对外响应上限是10秒。技能执行期间,模型在整个等待过程中既不能继续对话也不能主动采取措施,超时后用户只看到转圈圈,体验非常差。
后来我调整了技能封装方式:把外部接口调用和Agent的对话环节物理分离,异步任务挂在后台执行,Agent先把"正在查询"的过渡回复发给用户,等结果回来后基于状态再补一条完整回复。技能因此多了一个状态机字段:pending(执行中)、succeeded(成功)、failed(失败),Agent在每轮对话前先检查待定技能的状态。
这里分享一个经验:如果技能内部的时间预算超过2秒,就要认真考虑异步化设计,别让Agent的对话循环被一个网络请求卡死。
6.2 上下文污染:一次调用的JSON会把Agent带偏
技能返回的结构化数据,直接拼进对话历史后会有个隐患:模型会被JSON里的字段名带歪。尤其当字段名恰好和用户问题里的词语有语义重叠时,模型容易把JSON里的数据当成对话的一部分来推理。
比如订单查询接口返回了{"status": "pending", "reason": "fraud_check"},模型下一轮回答里居然开始讨论"fraud",以为用户在进行欺诈相关的对话。这个问题的解法是:技能结果在进入对话上下文之前,先经过一个压缩器,转换成一句话摘要。比如上面的JSON转换成"订单状态:处理中,原因:风控审核中,当前不建议催发货"。给模型看的永远是可以直接参与推理的干净文本,而不是生硬的接口数据结构。
6.3 多个技能互相竞争:路由冲突要显式消解
当系统里同时有"订单查询"和"售后入口"两个技能,用户说"我要投诉这个订单"时,两个技能都会被规则命中。这类场景我的处理方式是增加一个技能优先级公告,明确在技能清单中标注priority字段。冲突发生时,优先执行优先级更高的技能,低优先级技能降级为备选。同时,在Prompt里写明:"如果某技能已经执行完毕但未解决用户诉求,允许且鼓励从备选技能中再次选择。"
消解冲突的逻辑要放在路由层而不是模型层。模型层的随机性在这种场景下是一种风险,不值得赌。
6.4 技能组合后的降级路径:再完美的编排也有兜底
复合技能在真实运行中的失败概率不是各个步骤失败概率的简单相加,还引入了编排链路中断的问题:第二步成功后第三步失败,这时候怎么回滚?我的实践经验是,复合技能必须自带回退逻辑声明,指定哪一步成功之后失败需要撤销已造成的影响。比如退款流程中"锁定订单"成功但"创建退款单"失败,就要触发解锁订单操作,这一步不能交给模型自行商量,要在技能定义里一并写死。
7. Agent技能的可进化机制:让技能从静态清单长成活的体系
7.1 线上日志驱动的技能表现看板
技能上线的第一天就要建立反馈闭环。我在项目中维护了一张"技能运行健康度"表,每项技能盯四个指标:
| 指标 | 计算方式 | 预警线 |
|---|---|---|
| 触发率 | 技能被选中并执行次数 / 对应场景出现次数 | 明显低于预期 |
| 成功率 | 技能完整执行成功并返回预期结果次数 / 触发次数 | 低于85% |
| 误触发率 | 在不该触发的场景中被触发次数 / 触发次数 | 高于5% |
| 回退率 | Agent技能执行后转而求助人工坐席次数 | 与业务基线比对 |
这些指标能精确地告诉我哪个技能定义出了问题。比如误触发率高,就去补negative_examples;成功率低,优先看参数校验是否过于严格,再看上游接口的稳定性。
7.2 技能版本迭代的发布节奏
技能不是一次性资产。业务规则变了、接口字段变了、模型版本升级了,都会影响技能的可靠性。我的做法是技能配置和主Prompt共同纳入版本管理,每轮调整都做一次AB对比测试:旧版技能跑线上A组流量,新版技能跑B组流量,观察前面说的四类指标。
一个值得注意的细节:技能改名是一个成本极高的操作,因为技能名会被历史对话上下文反复引用,改名后老对话中的技能引用会失效。所以技能命名一开始就要慎重、稳定,迭代时优先更新描述和规则,而不是改名换姓。
7.3 从会话经验到长期技能沉淀
最后聊一点我个人比较推崇的方向:让Agent在会话中产生的有效路径,能够反向沉淀回技能库。做法是保留每次技能执行的完整轨迹——触发时的用户输入、技能参数、中间状态、最终回复、用户是否满意。定期人工review这些轨迹,把重复出现但当前技能没有覆盖的模式提炼成新的learning_samples或规则补充进技能定义。
这个过程相当于让技能库随着真实流量自然生长。上线三个月后,我的客服Agent的技能数量和最初相比增加了40%,但误触发率没有上升,核心原因就是每次增量都经过人工review与案例集验证,而不是被模型临时行为带跑偏。
我自己做了这么多Agent项目之后,最大的体会是:技能体系的建设不是一次性的编码任务,它是一个持续演进、持续度量的工程。别想着第一天就把技能库设计到完美,先把闭环跑起来,让线上数据给你指路,这项能力会越用越顺手。