ARTICLE DETAIL

资讯详情

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

Agent 开发实战:从工具集合到稳定技能体系的完整方法论

Agent 开发实战:从工具集合到稳定技能体系的完整方法论 做 Agent 开发这几年我最大的一个体会是决定一个智能体上限的往往不是模型有多聪明而是挂在它身上的那串 agent-skills 设计得有多稳。把 prompt 写得更长、换更大的模型短期内确实能见效但越往后瓶颈越在技能层——怎么定义、注册、路由、观测、降级、迭代每一个环节都能决定生产环境是稳定运行还是频繁翻车。这篇文章会把我从第一个原型翻车到生产环境跑通的完整过程沉淀下来把想清楚的部分和踩过的坑都讲透适合正在搭 Agent 技能系统、或者想把自己手头那堆工具集合升级成技能体系的团队参考。1. 一次失败的原型为什么工具的集合不等于技能体系我第一次给 Agent 加技能是在一个内部客服助手上。当时的需求很朴素用户会问订单状态、退换货政策、物流时效偶尔还有发票补开的申请。第一版实现得特别天真——我把十几个 API 工具的定义全部塞进了 system prompt然后用 ReAct 循环让模型自己决定调哪个、按什么顺序调。1.1 prompt 越堆越长模型选择却越来越飘刚开始只有几个工具模型表现还算像模像样。等到工具数量超过二十个问题就开始集中爆发了。prompt 里工具说明文本越来越长光描述部分就占了三四千个 token模型开始出现拿错工具的情况——用户明明在问退换货政策模型偏偏调用了订单修改接口。最折磨人的是每加一个新工具都要重新调试好几轮新工具的表述总会和已有工具产生语义干扰按下葫芦浮起瓢。那段时间我一度以为是模型理解力不行换了当时更强的模型效果确实有改善但 token 成本直接翻倍而且只要两个工具描述稍微沾点边选择不稳定的问题照样复现。这个阶段让我非常受挫感觉自己在不断给一座摇摇欲坠的房子打补丁。1.2 问题出在把技能当成了工具的集合后来我停下来重新想这件事发现根子不在模型而在我自己对技能这个概念的认知。工具是什么工具是单一动作的封装比如查询订单接口计算运费技能是什么技能是带边界的、可以被独立触发和编排的能力单元它不止包含能做什么还要覆盖什么时候该用执行失败怎么处置用完之后会产生什么副作用。一个很直观的例子查天气可以是一个工具但帮用户规划出行是一个技能——它内部可能要查天气、查路线、算耗时、甚至查目的地附近有没有停车场。如果只把底层工具暴露给 Agent模型就得自己组合好几个动作任何一环的描述不清晰都会崩。可如果把规划出行沉淀成一个技能模型只需要触发一次内部流程由系统编排好模型的负担小得多稳定性也高得多。这次复盘让我确定了一个方向做 agent-skills 不是给 Agent 多挂几个 API而是围绕能力单元建立一套完整的设计规范、注册机制、路由策略和治理手段。后面所有的工作本质上都是在补这套体系。2. 技能定义的核心抽象能力描述、输入输出 Schema 与副作用声明想清楚技能不等于工具集合之后第一件事就是把技能的定义标准化。这一节我重构过至少三版描述结构最后沉淀下来一套相对稳定、也能支撑后续做权限和降级的字段体系。2.1 一条完整的技能描述应该包含什么我现在要求团队里每个技能都必须用结构化对象来定义而不是散落在注释和文档里。一个实用的技能定义大概是这个样子# skill_schema.py from typing import TypedDict, Literal, List class SkillIO(TypedDict): name: str description: str schema: dict # JSON Schema class SkillDef(TypedDict): name: str # 技能唯一标识如 order_refund summary: str # 一句话概述给模型快速了解用途 description: str # 完整触发条件与能力说明 input_io: SkillIO # 输入定义 output_io: SkillIO # 输出定义 side_effects: List[Literal[no_op, write, notify, costly]] timeout: int # 超时时间秒 fallback: str # 降级策略描述这套字段里side_effects和fallback是最容易被新手忽略的但它们在权限控制和异常处理这两个后续环节里几乎是救命的。技能定义不只是一份给模型看的说明它更应该是一份系统在运行时可以做决策的依据。2.2 description 才是模型做决策的判断依据很多人写技能描述时喜欢写调用订单模块的 refund 接口这个写法对工程师友好对模型完全不友好。应该翻译成模型能理解的自然语言当用户表达退款或退货意图且已确认订单信息时使用本技能发起退款申请。如果用户只是询问退款政策不要调用本技能请改用政策查询技能。有几个细节值得展开描述里要写清楚什么时候应该用同时还要写清楚什么时候不应该用排他性信息对模型帮助极大。描述里要写清楚用户完整表达了什么意图才触发防止只凭一个词就触发高风险动作。模糊的对话场景下宁可引导模型触发一个只读查询技能也不要让它触发写操作技能。写操作技能退款、下单、改密码如果没有清晰的触发边界模型极容易只凭一句话里的疑似意向就提前执行一旦执行就是不可逆的后果很严重。我在这个点上吃过很大的亏后面第 6 节会详细讲。2.3 输入输出 Schema 不要直接抄接口参数技能暴露给模型的 Schema不需要和底层 API 的入参完全一致。技能层要做的是语义化入参——把底层接口零散的参数打包成模型容易填写的字段。举个例子。底层订单查询接口需要传merchant_id、channel_code、biz_order_id、user_token四个参数对模型来说它只知道用户说了一个订单号。那么在技能层输入 Schema 就只定义成{ type: object, properties: { order_id: { type: string, description: 用户在对话中提供的订单号或电商单号 } }, required: [order_id] }模型能正确填出order_id的比率远比让它同时填四个底层参数的比率高得多。至于内部怎么根据order_id查出merchant_id和user_token那些都是技能内部实现不该暴露给模型。2.4 副作用声明如何影响调度与安全控制我在side_effects里定义了四类no_op只读查询、write会写数据、notify会给用户发消息、costly昂贵调用。模型侧其实不需要看到完整的副作用枚举但它需要从技能描述里感知到这是一个读操作还是写操作。在调度侧副作用声明有两个用处一是决定能不能走结果缓存——no_op类技能可以做缓存write类技能绝对不能二是决定是否需要插入二次确认——write和notify类技能在真正执行前系统可以自动加一个确认环节。这个设计让技能层和安全控制层彻底解耦权限规则不再散落在各个技能内部审计起来也干净得多。3. 技能注册与分发机制从字典硬编码到运行时技能总线定义完技能结构下一个问题是这些技能怎么被系统发现、怎么被 Agent 拿到、怎么在几十个技能并存时选出正确的那一个。这块的演进路径我建议你按自己的实际阶段来不用一上来就追求最复杂的方案。3.1 第一版字典注册简单但迟早不够用最朴素的做法是写一个注册表把技能名映射到处理函数# registry_v1.py SKILL_REGISTRY { order_query: order_query_handler, refund_apply: refund_apply_handler, policy_query: policy_query_handler, } def dispatch(name, params, context): handler SKILL_REGISTRY.get(name) if not handler: return SkillResult.fail(fskill {name} not found) return handler(params, context)这种方案在技能数量少于十个时完全够用但它的问题很快会暴露技能元数据描述、副作用、超时时间散落在代码的各个角落每次想调整技能描述都要改代码发版没有统一的加载时机和热更新机制想给技能做多版本、灰度发布、A/B 测试更是无从下手。3.2 第二版装饰器驱动的声明式注册我很快把注册方式改成了声明式用装饰器把技能定义和处理函数绑定在一起# skill_registry.py _skills {} def skill_register(skill_def): def decorator(fn): skill_def.handler fn _skills[skill_def.name] skill_def return fn return decorator skill_register(SkillDef( namerefund_apply, summary处理用户退款申请, description当用户表达退款或退货意图且已确认订单信息时使用本技能发起退款申请。..., input_ioSkillIO(...), output_ioSkillIO(...), side_effects[write], timeout10, fallbackrefund_apply_fallback, )) def refund_apply(params, context): ...这种方式比字典硬编码好了不少技能定义和处理逻辑放在一起查起来方便也更容易做静态检查。但到这一步它其实还只是一个更好维护的字典真正让它变成技能总线的是后面三件事运行时元数据加载、技能发现接口、以及按需组装技能视图。3.3 第三版运行时技能总线与按需裁剪的技能视图最终版本里我把技能仓库设计成一个独立的运行时组件它负责四件事注册、校验、发现、导出。注册启动时加载所有技能定义做 schema 合法性校验、副作用枚举校验、技能名唯一性校验。校验确认输入输出定义是合法 JSON Schemafallback指定的降级技能真实存在依赖的技能也在技能池里。发现根据当前对话上下文从技能池中召回一部分候选技能。导出把候选技能的描述文本拼进 prompt或者转成 function calling 的 tools 参数。这里有两个细节对线上效果影响非常大。第一技能视图必须是按需裁剪的。把全部技能都塞给模型一方面 token 成本高另一方面技能一多模型的选择精度就会掉。我在导出前加了一个召回层——用当前意图分类 关键词匹配 少量向量检索从技能池里筛出 Top K 候选再交给模型。实测技能池从 40 个技能缩小到每次 8~10 个候选之后工具选择的准确率提升非常明显token 消耗也降了将近一半。第二技能描述导出时要有统一的胶水格式。我建议所有技能导出给模型时都统一成技能名 一句话 summary 何时触发与何时不触发的说明。这个固定格式可以让模型更快地适应在多个技能之间做决策这个任务减少不同技能描述风格差异带来的干扰。3.4 技能数量的临界点从全量注入到按需召回如果你现在只有五六个技能全量注入完全没问题不用追求复杂设计那是过度工程。我的经验是当技能数量超过 15 个或者单条技能描述超过 300 个 token就该认真考虑引入召回机制了。给你一个更直观的类比全量注入是把一整本电话簿递给模型按需召回是只把当前对话最可能用到的几页递给模型。电话簿越厚模型翻错页的概率越大决策时间也越长。4. 多技能协同编排意图路由、依赖处理与上下文传递技能注册机制建立之后真正复杂的工作才刚刚开始——多个技能凑在一起怎么让它们协作得好。这个阶段处理不好技能再多也只是看起来丰富用起来还是四处漏风。4.1 显式编排与动态规划两条路线如何取舍我见过两类典型方案。一类是全动态路由让模型自由选择技能并自行决定调用顺序所有流程都写在模型脑子里。另一类是全固定流程预先用代码写死编排模板技能触发序列完全固定比如先查订单再查物流再给结论。我的实践结论是关键业务路径必须显式编排探索型场景可以动态规划。举两个真实场景。用户说帮我看看我那个订单什么时候到如果走全动态路由模型可能要依次触发订单查询、物流查询、时效计算三个技能任何一步选错整体结果就歪了。但如果我们预先定义好一个订单时效查询技能内部固定编排好三步模型只需要触达一次成功率会高很多。反过来用户说我想去厦门玩三天帮我想想怎么安排这种开放式任务你很难预先把所有组合写成模板动态规划反而更合适——模型可以在交通、住宿、景点、美食这几个技能之间自由跳转生成一个组合方案。所以我的编排设计原则是业务确定性越高编排越往代码侧下沉开放性越高编排越往模型侧上浮。两种能力都要有而不是只押注其中一种。4.2 技能依赖处理A 技能的产出如何成为 B 技能的输入多技能协作时最容易被忽视的是依赖关系。比如取消订单并退款这个组合动作里退款技能必须先拿到订单查询技能产出的订单金额和支付流水号。如果这些数据要模型自己记prompt 会变得异常复杂而且容易漏。我推荐在技能定义里显式声明依赖而不是让模型现场发挥class SkillDef(TypedDict): ... dependencies: List[str] # 前置技能名列表 injects: List[str] # 从前置技能输出中注入到本技能上下文的字段调度器在执行某个依赖型技能前先检查前置技能输出是否存在不存在就先触发前置技能并把需要的数据注入到当前技能上下文。这一步做扎实之后模型完全不需要在上下文里记住上一轮技能输出因为编排器已经把数据接好了。4.3 上下文传递的三类高频陷阱技能之间的上下文传递我在这上面翻过不少车总结出三类高频问题。第一类是对话历史的过度携带。有些技能其实只需要当前这轮用户输入开发时却图省事把整整十几轮对话历史都传给了技能内部调用的大模型结果技能执行变慢、成本变高。我的做法是给每个技能定义一个上下文窗口通常只注入最近 2~3 轮对话加上与该技能相关的系统状态其余历史不传。第二类是隐式状态的丢失。技能 A 执行完把结果写进了内存但技能 B 所在的计算单元如果是无状态的B 就拿不到 A 的结果。这个问题在微服务化之后尤其明显我最后的解法是在编排层引入一个显式的会话快照技能产生的结构化产物都落到快照里B 再从中取值。第三类是时区、单位、货币等隐含语义没有对齐。模型在技能 A 里输出了订单金额技能 B 里却把币种当错了这种错用户一眼就能看出来体验极差。技能层应该强制要求跨技能传递的一切数值字段必须携带单位、币种和时区声明不要在描述里默认大家都懂。4.4 超时与中断执行到一半用户反悔了怎么办模型调度技能执行期间用户随时可能插话或者修改意图。技能执行到一半新意图已经很明确不想继续了——这时如果继续硬跑完是浪费资源和成本如果直接中断又可能留下脏数据。我在编排器里设计了检查点机制每个技能内部划分成多个可中断步骤每个步骤执行前先检查当前会话是否有新的意图请求如果有就标记当前步骤为已取消并执行该技能定义里声明的 cancel 策略——回滚、等待完成、或者直接丢弃。这个机制看着简单但对线上体验的提升非常大尤其适用于耗时较长的技能比如生成式报告、批量处理任务。用户以为自己在打断系统实际上系统确实响应了打断而不是继续闷头跑完再给出一个无人关心的结果。5. 生产环境里的技能治理权限边界、可观测性与降级演练技能系统一旦上了生产环境你很快会意识到能跑和能长期稳定跑是两码事。这一节要讲的治理事项每一项都是线上事故换来的教训。5.1 权限边界技能能做什么必须在配置层显式声明很多团队在原型阶段让 Agent 使用一把全局 API Key所有技能共用同一套凭证。这个做法在原型阶段确实省事但生产环境必须拆开原因很简单你没办法在一个共享凭证上做审计也没办法限制某个技能不能访问另一个系统的数据。我的做法是每个技能单独声明所需的权限维度至少包含目标系统、操作类型读/写/删除、配额上限、敏感字段脱敏策略。权限校验发生在技能注册阶段和调用入口而不是在技能内部自行判断。这样审计的时候可以很清楚地回答这个技能到底被授权了什么。我踩过最疼的一个坑是某个查询类技能因为共用凭证误触发了另一个系统的删除操作。那次事故之后我强制要求所有技能必须显式声明最小权限甚至把权限声明纳入技能上线的 CI 检查——权限声明不通过技能就不允许注册。这个改动看着不起眼但它把人靠自觉变成流程强制。5.2 可观测性技能调用需要全链路追踪技能层的可观测性和普通 API 的可观测性不太一样你不仅要看耗时和错误率还要看这次调用的触发理由是什么。也就是说每次技能调用至少要记录三部分数据模型决策前看到的上下文摘要、模型选择该技能时输出的原始片段、以及技能入参与出参的摘要。我建议至少按这张表的结构来记录维度记录内容目的触发技能名、触发时的意图分类、模型原始输出片段定位误触发输入入参摘要、上下文快照引用复现问题输出出参摘要、是否降级、是否中断评估效果性能耗时、token 消耗、错误类型成本与稳定性这些数据直接决定了你能不能回答这个技能真的被正确触发了吗而不是只能回答这个技能被调用了几次。前者能帮你持续优化技能描述后者只能帮你做汇报。5.3 降级策略每个技能都要有 Plan B线上服务没有不挂的。第三方天气接口会限流订单系统会超时向量数据库也会有毛刺。你的技能系统如果没有降级方案一个底层服务抖动就会让整个 Agent 表现得像个傻子——要么一直报错要么模型自己编一个答案。我给每个技能都要求写fallback字段明确主能力不可用时怎么办。降级方案通常有三个层次直接返回可理解的失败话术比如暂时无法查询物流信息请稍后再试切换到同义能力的替代技能或者降级到半自动人工处理通道。关键点是降级不能是代码里偶发的 try-except而要在设计阶段就规划好。每次新技能上线前我会让团队成员手动模拟底层服务挂掉观察 Agent 的表现是否符合预期。这个降级演练应该像做备份恢复演练一样常态化而不是等出了事故才想起来。5.4 灰度发布新技能先进影子模式新技能上线最怕什么怕模型在还不了解它边界的时候被误触发导致线上用户遭遇不可预期的操作。我的做法是引入影子模式新技能在技能池里正常注册但调用时只记录如果当时不是在影子模式我会执行什么的日志不真正执行。观察一段时间统计它的触发频率、触发理由以及和已有技能是否产生抢占确认无误后才转为正式执行。这一招对写操作类技能尤其重要。影子模式本质上是在给技能做一场无人受伤的预演让误触发问题在造成真实影响之前就暴露出来。6. 我在生产环境里踩得最深的三类技能坑前面讲的是方法论这一节讲具体翻车现场。这三类问题几乎每个做 agent-skills 的人都会遇到而且踩坑路径高度相似我把排查和修复过程完整写出来。6.1 技能描述太技术化模型根本听不懂有个查库存的技能我第一版描述写的是通过 inventory service 的 get_stock 接口查询 sku 的可用库存。上线后马上发现问题用户说这个还有货吗这个还能买吗什么时候补货模型完全不触发这个技能反而去触发了商品查询技能。排查链路是这样的我先看调用日志确认模型确实收到了技能列表然后查看模型的原始输出发现模型在理解get_stock、sku这类词上明显犹豫最后选择了名称上更像查商品的技能。修复方式很简单把描述彻底改写成人话。当用户询问某商品的现货数量、是否有货、能否购买、何时到货时使用本技能查询实时库存。注意查询商品基本信息和价格时不要使用本技能。改完之后误触发率立刻降了一个量级。这件事给我的教训是技能描述是写给模型读的不是写给工程师读的所有内部项目代号、接口名、技术缩写都要翻译成模型能理解的自然语言。6.2 技能异常处理太笼统模型陷入重试死循环有一次线上事故让我印象特别深。某个技能因为底层服务限流返回了一个通用的system error模型拿到这个错误后以为是自己的参数填得不对于是反复重试同一个技能一连重试了五次把底层服务彻底打到熔断。问题根源有两个。一是技能返回的错误信息没有区分参数错误和服务暂不可用二是模型缺少遇到这类错误时应该停止还是换一个方案的指导。修复方案我做了三层。第一层技能内部统一封装错误码至少区分input_invalid、service_unavailable、timeout、forbidden四类。第二层在技能描述或系统提示里明确告诉模型如果收到 service_unavailable不要重试直接向用户说明服务繁忙请稍后再试。第三层在调度器里做重试熔断同一技能连续失败 N 次就暂停该技能一段时间从机制上杜绝异常循环。三层叠加之后类似事故再没有出现过。6.3 技能职责重叠模型选择不稳定技能一多职责边界天然会模糊。举个例子我有订单查询和售后进度查询两个技能用户问我的退款到哪一步了两个技能看起来都能答模型每次随机选一个返回值的风格还不一样。这个问题排查起来最费劲因为不是报错而是不稳定。用户不会说你错了只会觉得这个助手时灵时不灵。最后靠统计技能触发日志和模型交付结果对比才定位到职责重叠。根治办法是给技能画一张决策边界矩阵列出所有高频用户意图逐个确认该意图应该固定路由到哪个技能不允许出现都行的灰色地带。两个技能如果覆盖了同一个意图要么合并要么在描述里明确排他关系——当用户询问退款或售后进度时不要使用订单查询技能请使用售后进度查询技能。做完这轮梳理后模型选择稳定性提高了非常多而且这张矩阵本身也成了团队新人的入门文档。7. 技能系统还可以往哪里走组合模板、用户自定义与效果闭环聊完踩坑再说几个我们正在推进的扩展方向这些方向不一定每个团队都需要但可以给你一个参考坐标。7.1 把高频编排逻辑沉淀成组合模板显式编排和动态规划之间有一个经常被忽略的中间地带——组合模板。比如订单全流程查询可以是订单查询 物流查询 售后状态查询的组合出行助手可以是天气 路线 停车场的组合。把高频出现的技能调用序列固化成模板既能提高成功率也方便后续做 A/B 测试比较不同组合对用户满意度的影响。7.2 让部分用户自定义技能再往后可以尝试把技能系统的使用边界从开发者扩展到更有技术背景的用户。提供一套可视化的技能编辑器让用户自己配置触发条件 调用动作 返回话术本质上是在复用同一套技能注册、校验、路由、观测的基础设施只是换了一个入口。这个方向对平台型产品尤其有价值能让 Agent 的边界从团队自己扩展到整个用户生态。7.3 建立技能效果的反馈闭环每个技能上线后都应该形成效果闭环触发准确率、误触发率、降级率、单次调用的 token 成本、用户对返回结果的满意度。这些指标反哺到技能描述改写和决策边界矩阵的调整上才能让技能系统越跑越稳。技能系统不是一锤子买卖它需要像产品一样持续运营。我在实际运营中最大的感受是agent-skills 的价值一半在初期的设计规划另一半在长期的迭代治理。技能写得再好没有观测、没有降级、没有边界梳理早晚会在某个线上角落爆雷反过来只要治理到位即使技能数量增长很快系统也依然可控。希望这些从实战里长出来的经验能帮你少踩几个我已经踩过的坑。
返回列表