ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent技能体系搭建实战:从工具调用到技能内化

Agent技能体系搭建实战:从工具调用到技能内化 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, encodingutf-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_asciiFalse)这里有一点非常关键不要把所有技能的完整定义全部塞给模型。原因很简单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项目之后最大的体会是技能体系的建设不是一次性的编码任务它是一个持续演进、持续度量的工程。别想着第一天就把技能库设计到完美先把闭环跑起来让线上数据给你指路这项能力会越用越顺手。
返回列表