ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI代理技能系统设计:从架构到落地,解决工具调用稳定性

AI代理技能系统设计:从架构到落地,解决工具调用稳定性 我调试过不少AI代理项目发现一个特别普遍的现象——初期Demo演示时效果惊艳一旦扔进真实业务场景代理的表现会断崖式下降。用户多问两句、业务流程稍微绕一点、工具返回的数据带上些脏格式代理就开始胡言乱语或者原地打转。很多人第一反应是换更大的模型但其实问题多半出在技能层代理手里没有一套真正趁手的技能再强的模型也只是个空有智商不会办事的秀才。所谓agent-skills正是围绕这个问题生长出来的系统化做法——把大模型能做和不能做的事拆解成可注册、可调用、可组合的技能单元让代理在遇到具体任务时有章可循而不是靠提示词硬扛。它解决的痛点很明确怎么设计技能、技能之间怎么调度、调用失败的链路怎么兜底以及技能多了之后怎么维护。这篇文章我会从架构主干讲到落地细节再拆一个真实案例最后把我踩过的坑一并交代清楚给正在做代理应用、被工具调用稳定性折磨的人一个可复用的参考。1. 为什么代理会卡壳技能缺失才是根因先别急着讨论技能怎么写我们得把代理为什么会卡壳这件事想透。很多团队遇到的问题是对话刚开始好好的用户稍微绕个弯子代理就分裂了。其实这不是模型变笨了而是它缺少某个关键环节的肌肉记忆。1.1 裸模型做任务的三个硬伤第一个硬伤是操作外部工具时缺乏稳定协议。裸模型拿到一个API文档它能写出一段能跑的代码但它不保证每次都按同样的方式写——有时候参数名记错了有时候漏掉鉴权头有时候把返回结果当成最终答案直接甩给用户。一次两次能用十次里必然翻车。第二个硬伤是状态管理天然薄弱。真实业务流程几乎没有一个请求一个响应的简单模式大多是查一下A如果满足条件再更新B然后给C发通知这种多步骤操作。模型在单轮里能推理得很漂亮但跨轮次之后就会忘记前面做过什么、当前处于流程哪个位置导致重复执行或跳过关键步骤。第三个硬伤是失败后不知如何收敛。API超时了怎么办权限不足怎么办数据库里查不到记录怎么办裸模型往往会自己编一个看起来合理的成功结果糊弄过去这是最危险的。人遇到异常会停下来确认模型不会。这三个硬伤靠提示词工程只能缓解不能根治。技能系统之所以有效是因为它把怎么做某件事从模型的临场发挥变成了预先定义的确定性逻辑——确定性逻辑负责兜底模型只负责判断该不该用、怎么组合各干各的问题边界就清楚了。1.2 技能不是函数也不是插件很多人一听到技能系统就会想到函数调用或者插件机制。这个类比方向没错但有个关键区别必须点破函数只是一段可执行的代码技能则是一段可执行的代码 触发条件 使用前提 失败兜底 输出约束的完整封装。举个例子。你用LangChain或类似框架里直接定义一个函数get_order_status(order_id)模型看到API描述后自主决定调用这叫函数调用。但技能系统里这个能力会被描述成——当用户询问订单状态、物流进展、签收情况且对话中已存在order_id时调用此技能内部先查缓存缓存未命中再查订单库若订单不存在则返回NOT_FOUND并触发追问流程。同样的底层动作函数调用把判断责任全扔给模型技能则把判断逻辑固化进了运行时。这样带来的好处是模型做选择题系统做填空题。模型只需要在技能清单里选一个最贴切的剩下的参数补全、调用顺序、异常处理都由系统兜住稳定性自然上一个台阶。1.3 什么场景最值得上技能系统不是所有代理应用都需要这套东西。如果你只是做个单轮问答或者简单的知识库检索函数调用完全够用。但凡是下面这些特征技能系统基本是刚需业务流程有明确的状态流转比如工单从待受理到处理中到已解决需要调用多个外部系统且互相之间有依赖关系先鉴权、再查数据、再写结果对错误率有硬性要求不能接受模型凭空捏造结果需要多轮对话维持上下文用户可能中途改变主意或补充条件。电商客服、IT工单、内部数据分析助手、政务咨询这类场景几乎全中。在后面的案例拆解里我会用IT工单助手这个场景完整走一遍设计过程。2. 技能系统的主干设计注册表、调度器与执行器一个能撑住真实业务的技能系统至少要有三层结构注册表负责有什么技能调度器负责该用哪个技能执行器负责技能怎么跑起来。三层各司其职代理的表现才会稳定。2.1 技能注册表让模型看得懂、选得对注册表的核心是一份份结构化的技能描述模型通过描述来决定是否调用、如何调用。所以描述文件的质量直接决定调度效果。我惯用的描述结构大致如下{ skill_id: query_order_status, name: 查询订单状态, description: 当用户询问订单当前状态、物流进度、预计送达时间时使用。需要对话中已存在有效的order_id。, input_schema: { type: object, properties: { order_id: { type: string, description: 用户在对话中提及的订单号若未提供必须向用户追问不得猜测 } }, required: [order_id] }, preconditions: [user_authenticated, order_id_is_valid], fallback: query_order_status_via_external_api, timeout_ms: 5000 }几个字段值得单独讲讲。description是最容易被忽略但最影响效果的字段。模型要在一堆技能里选一个靠的就是这段描述和目标用户请求之间的语义匹配。写作时要反着来——不是写这个技能能做什么而是写用户在什么场景下、说了什么话会需要这个技能。比如查询订单状态这种描述太泛模型可能该用的时候不调用改成当用户询问订单当前状态、物流进度、预计送达时间时使用就具体多了命中率明显上升。preconditions是很多人会漏掉的设计。它声明了技能运行前必须具备的前置条件。调度器在执行前会先检查这些条件缺了就触发补条件的流程而不是直接带着残缺参数去调用接口。最典型的就是登录态——技能内部做鉴权当然也可以但把鉴权放到前置检查里失败时能更早地引导用户登录用户体验完全不同。2.2 调度器的两种典型思路调度器就是那个决策中枢负责根据当前对话状态和用户意图从注册表里选出要执行的技能。我尝试过两种思路一种是纯模型驱动的语义匹配一种是规则约束下的模型决策。前者灵活后者可控实际开发中我倾向于混合使用。纯模型驱动的做法很直观把所有技能的description拼进系统提示词让模型输出技能ID和参数然后系统执行。它的优点是能处理没见过的表达方式缺点是技能一多提示词被撑爆模型开始丢三落四。规则约束下的模型决策则多一层护栏先用规则系统做一轮粗筛比如对话中检测到order_id必然触发query_order_status检测到退货关键词可能触发create_return_request粗筛结果再交给模型做最终排序和参数补全。这样既保留灵活性又不会让模型在20个技能里糊成一团。调度伪代码大致长这样def dispatch(user_input, dialog_state, skill_registry): # 第一层规则粗筛 candidates [] for skill in skill_registry: if skill.match_keywords(user_input): candidates.append(skill) # 第二层模型决策 if len(candidates) 1: selected llm_select(candidates, user_input, dialog_state) elif len(candidates) 1: selected candidates[0] else: selected llm_select(skill_registry, user_input, dialog_state) # 第三层前置条件校验 if not check_preconditions(selected, dialog_state): return build_condition_query(selected.preconditions) return fill_and_execute(selected, user_input, dialog_state)这个流程有个好处每一层都只需要做自己擅长的判断。规则擅长确定性匹配模型擅长模糊意图理解前置检查擅长兜底串起来之后整体鲁棒性比单靠模型高一个量级。2.3 执行器的三种形态技能查完了、选定了接下来怎么执行我见过三种执行形态各有适用场景。第一种是本地函数直调。技能背后就是一个Python函数跑在代理进程内直接查数据库、调内部服务。这是最简单、最可靠、最省成本的形态能用它解决的绝不上复杂方案。第二种是HTTP远程调用。技能需要访问外部系统时走这种方式。这里要注意的关键点是技能描述文件里应该写清楚接口的入参、出参、鉴权方式、超时时间执行器统一处理HTTP层的东西不把网络细节暴露给模型。第三种是子代理编排。某些技能本身就是一个完整的多步骤任务比如整理本月销售报表并生成分析摘要需要检索数据、计算指标、调用LLM生成文本等多个环节。这种技能内部可以再挂一个小代理由它去执行子技能序列。子代理的好处是可复用坏处是复杂度上了台阶建议只在确实需要灵活编排的地方使用。执行器的选择原则是从重到轻能直连不跳板能固定流程不要自由发挥。很多团队一上来就用子代理编排所有技能结果链路太长、排查问题特别困难。我的建议是尽量用前两种形态把第三种留给真正的流程型场景。3. 让技能真正可落地粒度、上下文与安全边界架构层面搭好之后真正的坑全在细节里。这一节讲三个最容易让代理在真实环境中翻车的问题技能粒度怎么切、上下文怎么管、安全和失败怎么兜。3.1 一个技能做一件事但别切得稀碎技能粒度是个玄学问题。切太粗技能变成一个大杂烩内部逻辑全是if-else模型选了它之后还是不知道该干什么切太细一个简单需求要调用五六个技能调度和编排成本飙升反而更容易出错。我自己的判断标准有三条技能是否对应一个完整可交付的结果。比如查订单状态对应一个明确结果查订单状态并推送物流动态给用户手机就不是一个自然结果得拆成查状态和推送通知两个技能。技能是否有独立的失败场景和独立的回退策略。如果某个步骤失败时的处理方式和另一个步骤不一样就应该拆开。比如查库存失败可以换个仓库再查创建订单失败就只能让用户确认信息不能重试这俩就不该揉一起。技能内部是否包含多个大模型需要独立决策的环节。如果一个技能里有两处根据用户输入判断走哪条分支基本说明它应该拆开。按这三条切出来的技能数量通常不会太夸张。一个中等复杂度的业务域二三十个技能已经能覆盖大部分场景超过五十个就要考虑是不是切太碎了。3.2 上下文状态从模型自己记变成系统托管状态管理是代理应用里最恶心的问题技能系统可以缓解但必须设计得当。我的做法是技能不直接信任模型的记忆而是把状态存到一个显式的上下文对象里。举例说明。用户在对话里先说帮我查一下上周的订单再问那笔4688元的单子到哪了——如果系统只在第一轮把order_id存下来第二轮模型大概率能对上但如果是帮我查第一笔订单不对我说的是今天下午那笔模型就很容易混淆。技能系统里应该把order_id、customer_id、current_step这样的关键状态统一放到上下文对象里由调度器决定何时写入、何时覆盖。class DialogContext: current_intent: str | None order_id: str | None customer_id: str | None step_status: dict[str, str] # 每个技能当前执行到的步骤 def extract_and_store(self, user_input): # 用规则模型双通道抽取关键参数 # 抽到新值就覆盖抽不到就保留旧值 pass状态托管的另一个好处是支持多轮补全。前置条件没满足时调度器会生成追问问题用户回答后由上下文对象更新字段再重新尝试执行技能。整个流程不需要模型额外记忆任何东西——所有信息都在上下文对象里躺着模型每次只负责读最新状态。3.3 安全边界与权限最小化技能系统给了代理更大的行动力也意味着更大的破坏力。权限和安全设计必须在架构第一天就考虑否则后面改起来成本极高。我的核心原则是权限最小化 危险操作二次确认。查询类技能放开权限没问题但执行写操作创建订单、修改配置、发送消息给客户的技能必须满足三个条件技能的描述里明确标注danger_level: high当调度器选中这类技能时系统强制进入用户确认模式必须由用户明确说确认执行才能继续。执行器内部对用户身份和角色做校验不是登录了就一定有权要细化到该用户是否有某类订单的操作权限。所有高危险技能的调用都留审计日志记录触发来源、完整参数、执行结果方便回溯。我知道有人觉得二次确认影响体验但真实业务里一次误操作带来的信任损失远比多点一次确认大得多。安全边界这种东西宁可过度设计也不能裸奔。3.4 失败兜底让技能在出错时死得体面技能执行一定会有失败重要的是失败后怎么办。我给每类技能都设计了三层兜底链路第一层是技术重试。超时、网络抖动这类瞬时错误用指数退避重试两次成本低、收益高。但重试要慎重写操作和幂等性不明确的接口宁可失败也不要重试否则可能重复下单。第二层是降级替代。主技能失败后调度器检查是否有fallback技能可以顶替。比如根据订单号查物流失败时可以降级为根据快递公司运单号查物流前提是能从上下文里补出这些参数。第三层是人工接管。当前两层都失败就直接返回显式的错误信息给用户并且明确说明当前无法完成该操作请稍后再试或联系人工客服。比让模型硬编一个假成功结果靠谱得多。这三层兜底加起来技能系统的可用性能做到比裸模型高很多。前提是每层兜底逻辑都写进技能定义里而不是运行时临时判断——临时判断等于把决策权又交还给了模型那还不如不设计。4. 案例拆解IT工单助手从零到可用理论讲了这么多还是要落到一个具体的例子上。我拿一个真实做过的IT工单助手来拆解让大家看看技能清单是怎么定、关键技能怎么实现、实测效果如何。这个场景非常适合技能系统因为工单流转天然有状态涉及查、创建、修改、通知等多种操作而且对错误容忍度极低。4.1 技能清单是怎么定出来的第一步不是写代码而是把业务场景里所有可能的需求列出来然后逐条映射到技能。我和产品、运营一起梳理了大概60条典型用户请求最后收敛成22个技能。核心的15个技能如下类别技能ID触发场景危险等级查询query_ticket_status用户询问工单当前处理状态low查询query_ticket_history用户想查看某工单的历史操作记录low查询query_sla_deadline用户问工单还有多久超时low查询query_knowledge_base用户询问故障处理办法low查询query_notification_history用户问通知有没有发出去low创建create_incident_ticket用户报障需要新建工单high创建create_asset_ticket用户申请资产新建资产类工单high变更reassign_ticket用户或流程要求转派工单high变更update_ticket_priority用户要求提高或降低工单优先级high变更add_ticket_comment处理人补充处理过程说明medium变更attach_evidence处理人上传截图或日志附件medium流转mark_ticket_solved处理人标记工单已解决high流转reopen_ticket用户反馈问题未解决重新打开工单high通知notify_requester给工单创建人发送站内信或邮件medium协同escalate_to_human超出代理能力转人工处理low这个清单有几个特点查询类占大头因为它们覆盖了用户80%以上的重复提问写操作几乎全是高危后面接二次确认escalate_to_human作为兜底技能排在列表最后保证代理遇到不确定场景时有退路。4.2 关键技能的实现细节挑一个最有代表性的技能create_incident_ticket展开讲讲。它背后的逻辑不算复杂但细节非常多。前置条件检查需要三项用户已登录、用户有报障权限、当前没有未解决的相同主题工单。第三项特别重要否则用户连续点两次提交就会创建两张重复工单。参数补全阶段需要从对话中抽取故障类型硬件/软件/网络/账号、故障描述、影响范围只有自己还是整个部门、紧急程度根据描述里影响办公无法登录这类信息自动推断。抽不到就追问一次只追问一个字段避免连续提问让用户烦。执行阶段系统调用内部工单API创建记录并把ticket_id写回上下文对象。后续用户问处理得怎么样了调度器就是通过读取上下文里的ticket_id直接命中query_ticket_status技能而不用让模型重新理解对话历史。def create_incident_ticket(params, ctx): ctx.assert_preconditions([user_authenticated, has_permission]) dedup_key f{ctx.customer_id}:{params.fault_type}:{params.summary} if cache.exists(dedup_key): return DuplicateTicketError(检测到相同工单请勿重复提交) payload build_payload(params) ticket_id ticket_api.create(payload) ctx.ticket_id ticket_id ctx.current_step waiting_assignment return {ticket_id: ticket_id, status: created}这段代码看着简单但每个细节都有目的。dedup_key防重复ctx.ticket_id保持多轮上下文连续ctx.current_step标记流程状态——这三行代码让代理在后续对话中的行为有了确定性。4.3 实测效果与一组关键数据这个工单助手上线后跑了三周我们对调度命中率、执行成功率和用户满意度做了统计。整体效果非常能说明问题。技能调度准确率92.6%。剩下的7.4%里一半是用户表述太含混需要追问一半是模型在相似技能之间选错主要发生在query_ticket_status和query_ticket_history之间。技能执行成功率97.1%。失败案例里网络超时最多其次是权限不足兜底策略基本都能接住。用户消息数相比之前的裸模型方案解决问题的平均用户消息数从7.3轮降到4.1轮。原因就是前置条件检查减少了来回追问上下文对象避免了状态错乱导致的重复沟通。真人工单率从19%降到8%。之前很多简单查询场景模型会乱答现在这些场景被查询类技能确定性接住了。这组数据最能说明问题技能系统的价值不是替代模型而是把那些模型本就不该碰的高风险推理交给确定性逻辑模型只做最擅长的理解和对话。4.4 执行链路中的成本控制一个容易被忽略的细节是成本。技能系统虽然稳定但多了一层调度如果每一步都让模型做选择token消耗会很高。我的做法是尽量用规则提前短路掉简单场景。检测到order_id匹配正则并且用户意图词是查进度到哪了这类明确词汇时调度器直接命中query_ticket_status完全不经过模型那一层LLM选型。我实测下来差不多60%的请求可以走这种规则短路token成本能压掉40%左右响应延迟也从900ms降到350ms。顺带说一句响应延迟对用户体验的影响被严重低估。350ms和900ms的区别在用户感知里就是秒回和转了两圈的区别直接影响用户是否愿意继续用这个助手。5. 实测踩坑技能膨胀、调度冲突与状态污染理论讲得再好不踩几个坑都不会有切身体会。我在这个项目里踩过的坑不少挑三个影响最大、也最有代表性的说给各位。5.1 技能超过30个之后描述开始互相打架项目中期技能从15个扩充到35个问题开始显现模型在相似技能之间频繁选错。比如query_ticket_status和query_incident_report——前者查单个工单状态后者查一段时间的工单报表功能本身不重叠但描述都提到用户想了解工单情况模型就容易混淆。排查过程特别痛苦。我们先是一个个看日志发现选错集中在特定描述语上然后做了个对照实验把两个技能的description同时展示给模型让它写区分理由结果发现它自己都说不出差异。最终方案分两步解决一是改写描述语强调触发场景而不是功能本身。query_ticket_status改成用户询问某一个具体工单当前的流转状态、当前处理人、预计完成时间query_incident_report改成用户想查看某统计周期内所有工单的数量、分类、超时情况汇总。二是在调度规则里加了互斥条件上下文里存在ticket_id时优先匹配前者不存在且出现时间范围词汇时优先匹配后者。这两步操作之后同类错误从每周十几次降到两三次证明了一个道理技能描述本质上是在给模型划分类边界写不好就等着模型瞎猜。5.2 上下文状态污染一个字段引发的血案有一次线上出了个诡异的问题用户明明在问A工单代理却回复了B工单的状态。查了半天发现是上下文对象里ticket_id字段被覆盖了——用户说了一句再帮我看看报修单的事系统把 报修单 当成新实体去抽取解析出一个错误的ID覆盖了原来的order_id。问题本质是参数抽取的覆盖策略太粗暴。正确做法应该给字段加来源标记字段是从哪一轮对话、哪句话、哪个技能调用结果里来的这样当新值尝试覆盖时系统能判断哪个信息更可信。我们的修复方案是给每个上下文字段加了confidence和source并且规定从结构化API返回里解析出的字段优先级最高从用户自由文本里抽取的字段优先级其次模型猜测的字段如果不能确认就不能写回。这个坑给我们的启示是上下文对象不是简单的KV存储它需要维护数据的血缘和可信度。尤其在多轮长对话里一个错误的状态覆盖会让整个对话崩盘。5.3 规则短路与模型决策之间的无人地带前面说规则短路能省token、降延迟但规则短路有个隐患它可能截胡掉本应该走模型决策的更优解。我们遇到过这样的场景用户说帮我查一下我上周所有工单的处理情况和超时风险——里面包含查工单这类关键词被规则直接路由到了query_ticket_status但这个请求的实际意图明显是查周期报表应该走query_incident_report。排查后发现是关键词匹配太宽泛。修复方案是给规则加了上下文语义快检规则命中后先用一个轻量模型当时用的是小模型摘要快速判断用户意图是否和命中的技能冲突冲突就放开走完整LLM调度。这里我学到的教训是规则短路适合单意图很明确的场景但只要用户表述稍微复杂一点宁可多花几十毫秒让模型确认也不要贪快。后来我们把规则条件从关键词匹配升级为关键词 槽位约束 否定排除词三重校验这类误路由才基本消失。5.4 高危险技能的误触发有一次测试用户随口说那你直接删了吧系统差点把一条重要工单标记为已解决——因为mark_ticket_solved的描述里写了用户确认问题已解决时使用而那你直接删了吧在语义上被模型归类为确认解决。这个问题非常隐蔽事后复盘发现二层防护都漏了调度器选中了技能前置条件也通过了就差执行前的那个用户明确确认环节。我们的修复方案双重加固描述里增加否定排除语仅当用户明确表达问题已解决不用了可以关闭等意图才使用本技能讽刺、假设、非正式表述一律不算。执行器侧增加意图确认弹窗高危技能执行前必须让用户再选一次确认执行或取消并且把原话附在确认信息里让用户看清系统理解成了什么。这套描述排除 执行确认的组合堵住了误触发。测试中不再出现用户说反话把工单关掉的情况。6. 把技能系统做厚版本管理、评测回归与自动沉淀技能系统最难的地方不是上线而是后续不断迭代。业务需求会变接口会变用户的表达方式也在变。让技能系统持续好用需要另外三块能力。6.1 技能版本管理与灰度发布技能修改是家常便饭。改一个技能最简单的是直接替换描述和逻辑但这样有个大坑你无法确定新版本在所有场景下都比旧版本好贸然全量替换可能让原本正常的场景挂掉。我的做法是给每个技能维护版本号并且支持按用户比例灰度。新版本先放给5%的流量观察命中率、成功率和用户负面反馈率没问题再逐步放开到50%、100%。这个流程看着笨但能防住绝大多数回归问题。版本管理落地上技能注册表本身存储的就是带版本号的JSON切换版本只是改一下路由配置。回退就更简单了随时把路由指回旧版本即可。6.2 离线评测集让每次技能改动都有据可依技能改动的最大难题是怎么知道改坏了。人工回归测试覆盖不了太多case所以我强烈建议搭一个离线评测集。评测集来源是真实线上对话日志——挑出几百条有代表性的请求每条标注期望命中的技能ID和期望抽取的参数。技能改动后跑一遍评测集看调度命中率、参数准确率、执行成功率的变化。我们当时建了个250条case的评测集覆盖了8成常见场景和2成边界场景。改描述语后跑一遍命中率曲线一目了然。评测结果还倒逼团队更严谨地做技能描述——谁也不想自己提交的版本让整体分数掉了三个点。离线评测集的建设成本不高收益却是长期的。它相当于给技能系统装了个体检仪让每次改动都变成可量化的决策而不是凭感觉上线。6.3 让代理从失败中沉淀新技能这个方向我目前还在探索但已经有了一些可用的轮廓。核心思路是当代理遇到一个当前技能清单覆盖不了的任务时系统不直接失败而是记录下这段对话让运营人员判断是否需要沉淀成新技能。具体落地分三步失败信号记录调度器在技能全不命中、技能执行失败、或用户明确表达不满时自动归档这段对话并打上标签。半自动生成技能草稿用LLM分析这段对话生成一个技能描述草稿包括触发场景、输入参数、建议执行动作人工审核后就能进入技能注册表。持续回流评测集每次新增技能后从归档对话中抽取相关case加入评测集保证新技能不是当前对话能用换个语境就废。这套闭环让技能系统有了进化能力。上线半年技能从22个增长到48个其中8个就是从失败对话中沉淀出来的。它们覆盖了最初没预想到的边界场景让系统的整体拦截率又上了一个台阶。写在最后给那些准备动手做技能系统的人如果你正准备给自己的Agent项目引入技能系统我个人的建议是先别急着写框架——把三个月后你期望的对话录进文档反复看哪些环节是模型不该碰的哪些操作失败的代价是不可接受的按这个标准去定技能边界。再补一条非常实际的技巧技能描述写完别急着上线找几个没参与过这个项目的人去考试——只给他看技能描述让他判断一段用户请求该匹配哪个技能。如果三个人里有两个人选不对那这个描述就是不合格的模型上线后大概率也会选错。技能系统做到最后本质上是在做两件事一件是把大模型从什么都得会里解放出来让它只做自己擅长的事另一件是给系统的确定性留一条清晰的、可控的路径。所有代码、框架、参数都是为这两件事服务的。
返回列表