news 2026/10/18 10:21:14

告别木偶Agent:用分层任务拆解构建可靠智能体系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别木偶Agent:用分层任务拆解构建可靠智能体系统

做Agent开发这几年,我踩过最大的坑就是亲手造出一个又一个木偶Agent——demo阶段看起来什么都会,工具调用行云流水,一丢到真实场景就崩得稀碎。后来我把这些翻车案例翻来覆去地复盘,发现根源几乎一致:大多数人都在用一个系统提示词加一堆工具函数,把大模型当万能执行者,让它自己决定怎么规划、怎么执行、怎么收尾,中间没有任何分层和约束。所以要告别木偶Agent,真正要做的是分层任务拆解:把智能体的决策链拆成意图识别、任务规划、原子执行、结果校验四个层面,每层只做一件事,层与层之间用结构化协议通信,最终得到一个可预期、可观测、可干预、可恢复的可靠智能体系统。

这篇文章我会把完整的拆解思路、每层的关键实现、从0到1的代码骨架,以及实战中反复踩到的坑全部写出来。适合正在做Agent应用、或者已经在生产环境里被Agent折腾得焦头烂额的开发者参考。先把话说在前面:分层不是给系统加复杂度,而是把不可控的模型行为,变成可控的工程边界。

1. 先诊断:木偶Agent为什么必崩

1.1 木偶Agent的长相:一个提示词加上一堆工具

木偶Agent的标准造型很容易辨认:有一个写满规则的系统提示词,罗列了几十个工具函数的说明,然后告诉模型"你可以使用以下工具完成用户的请求",剩下的就看模型临场发挥了。表面上看,模型确实会自己选工具、自己拼步骤、自己给结果,像模像样。

我把这种架构叫木偶Agent,是因为它本质上没有自己的骨架,只有一套华丽的皮肤。模型说要查库存,它就去查库存;模型说要发邮件,它就去发邮件。看起来是模型在做决策,实际上决策质量完全取决于提示词怎么写、模型当天的状态好不好、工具描述是否足够详尽,以及最要命的——上下文窗口里有没有被无关信息干扰。

我之前给一个做售后工单的团队做过技术咨询,他们的第一版Agent就是这种架构。用户问一句"我的打印机上周买的,现在不工作了,能换新吗",这个Agent真的会自己决定先去查订单、再去查保修政策、然后去查退货规则、最后生成回复。demo里演示了无数次都没问题,一上线就翻车——它会在查到订单后直接跳到退款,完全不看保修条款;也会在用户还没有提供故障证据时,就自信满满地告诉用户"可以换新"。

这不是模型蠢,是架构让模型背了不该背的锅。你让一个系统提示词同时承担意图理解、任务规划、工具编排、结果判定这四个完全不同的职责,它怎么可能稳定?

1.2 三个致命问题:上下文稀释、错误放大、无法回滚

木偶Agent的可靠性崩塌,通常沿着三条路径。

第一条是上下文稀释。系统提示词、工具说明、用户输入、历史消息、中间工具返回结果,全挤在一个上下文窗口里。模型需要从一大锅信息里找到当前该干什么,注意力被严重分散。工具说明写得越长,模型越容易忽略关键约束;历史消息越多,模型越容易忘记最初的用户目标。我在生产环境里见过最夸张的case,是模型在第五轮工具调用时,已经完全不记得用户要的是"查询保修状态",开始自顾自地聊起了打印机型号。

第二条是错误逐级放大。木偶Agent没有分层的校验机制,模型在前面的一个微小误判,会沿着后续步骤一路放大。比如它第一步把"订单号"理解错了,后面查到的所有信息都是错的,最后生成的结论自然离谱。更麻烦的是,每一步的错误都没有独立被发现的机会,因为整个流程是一团乱麻——你根本不知道是哪一步开始歪的。

第三条是无法回滚。一个复杂任务执行到一半,发现路径不对,木偶Agent只能继续硬着头皮往下跑,或者直接给你一句"抱歉,我无法完成这个请求"。它没有"回到上一步换一条路径"的概念,因为系统压根没记录每一步的状态。出了问题只能从头再来,而再来一次可能又在同一个地方翻车。

1.3 我们说的"可靠",到底是什么

很多人一听"可靠"就以为是要做到100%成功,这个目标本身就不现实。大模型是概率系统,没有任何架构能保证它每次都完美执行。我们追求的可靠,应该是四件事:可预期、可观测、可干预、可恢复。

可预期是指同类输入能得到同类结果,不会这次走A流程、下次走B流程;可观测是指任何一步出了岔子,你能明确知道是哪个环节、哪条链路出的问题;可干预是指系统在关键时刻能停下来,等人工确认后再继续;可恢复是指某个步骤失败时,能回到最近的有效状态换个方案重试,而不是整体崩溃。

想通这一点之后,我再看Agent架构就完全换了个视角。我不再去想"怎么让模型更聪明",而是去想"怎么让系统在模型犯傻的时候也能兜得住"。分层任务拆解,就是在这个思路下被验证过无数次的答案。

2. 给Agent装上骨架:分层任务拆解的整体设计

2.1 四层模型:意图层、规划层、执行层、校验层

我常用的分层方案是四层,每一层解决一类独立的问题。很多人也管这个叫"编排-执行-校验",实际是一个东西,只是叫法不同。

层级职责输入输出
意图层判断用户想干什么,决定要不要进入Agent流程原始用户消息、会话上下文意图标签、置信度、是否触发Agent
规划层把用户目标拆成可执行的任务步骤序列用户目标、可用工具清单、上下文摘要结构化的任务描述符列表
执行层逐个执行原子步骤,每个步骤有明确入参和出参单个任务描述符结构化执行结果
校验层验收执行结果,判断任务是否完成,决定继续/重试/终止执行结果、步骤预期校验结论、下一步动作

这四个层级的核心思想是职责单一。意图层只做入口判断,规划层只做拆解,执行层只做落地,校验层只做验收。每一层都短小精悍,不需要理解全局所有细节,它只需要吃透自己的输入,做好自己那件事,然后输出给下一层。

为什么把意图层单独拿出来?因为真实场景里,用户的请求有很大概率根本不需要Agent介入。用户可能只是问一句"你们几点下班",或者"我想换个地址",这种请求走标准会话或者固定流程就好,没必要让模型去规划任务。如果一上来就让规划层拆解,等于每句话都要过一遍大模型,既慢又贵,还容易在无关请求上胡言乱语。

执行层单独分离,是因为所有跟外部系统打交道的行为都有副作用。查个库存还行,但写数据库、发邮件、创建工单这类操作,一旦执行错了就很难撤销。把执行层做成一个个独立的、可被校验的原子操作,才能在出错时精准定位、局部重试。

校验层放在最后,是给整个系统兜底。模型的规划再合理,执行结果也可能跟预期对不上。这时候必须有个不带感情色彩的质检环节来说"这个结果不合格,重新来"。

2.2 层间通信:用结构化的任务描述符

分层最难的地方不是定义层数,而是层与层之间怎么说话。很多团队分层之后依然翻车,就是因为在层间传递大段自然语言文本——规划层写了一篇小作文传给执行层,执行层读得似懂非懂,结果还是乱。

正确的做法是定义一套结构化的任务描述符,相当于工厂里的工单。上层只下发工单,下层只回传回执,双方不需要理解对方的完整内部逻辑。

一个任务描述符通常包含这些字段:任务ID、目标描述、需要调用的工具或服务、输入参数的来源或具体值、预期输出结构、成功判定的标准。比如"查询订单状态"这个步骤,任务描述符会写成:调用订单查询服务,入参是订单号,出参加订单状态、商品列表、购买日期,成功标准是返回码为0且订单状态为有效。

这样做的好处很明显。第一,执行层不需要理解全局目标,它只看自己的工单,降低了对模型理解力的依赖;第二,每个步骤的结果可以被单独校验,出了错能精确定位到是哪个步骤;第三,整个流程可以被录音、回放、追踪,因为每一步的输入输出都是固定结构。

在层级之间传递结构化数据还有个隐藏价值:它天然形成了一种"接口契约"。规划层必须按契约生成步骤,执行层必须按契约消费步骤。任何一方越界,比如规划层生成了契约之外的步骤,或者执行层返回了不符合契约的结果,系统都能立即发现并拦截。

2.3 贯穿全文的案例:售后工单助手

后面的所有代码、排查经验,我会围绕一个具体的业务场景来讲,方便对照。假设我们要做一个企业售后工单自动处理助手,用户通过网页或IM提交售后诉求,Agent需要自动完成一部分工作,比如查订单、查保修政策、判断是否符合换新条件、生成回复、必要时创建换新工单。

这个场景非常有代表性:它涉及多个外部系统(订单系统、政策库、工单系统),有明确的业务规则(保修期、换新条件),有需要人工介入的边界场景(退款争议、政策灰色地带),还要求所有操作可追溯。任何一个把Agent当demo玩的人,碰到这种业务都会头疼。

用四层架构来解这个题会非常清晰。意图层判断用户是"咨询"、"报修"还是"申请换新",只有后两种才进入Agent流程;规划层决定先查订单还是先查政策,按什么顺序推进;执行层逐个调用订单查询、政策匹配、工单创建这些服务;校验层检查每一步结果是否合理,比如"用户申请换新但没有提供订单号,那查询步骤就是失败的"。

后面的章节我还会反复回到这个案例。它不复杂,但足够说明分层的价值。

3. 每一层怎么做,才不算"伪分层"

3.1 意图层:先学会拒绝和降级

意图层是整个系统的第一道闸门。最直接的实现方式是用大模型做分类任务,让它从固定的意图枚举里选一个,并给出置信度。把意图分类从开放式的任务规划里独立出来,是因为分类是模型的强项,比让它做多步骤规划稳得多。

但我建议加几道保险。第一,意图必须做成枚举值,不能凭空生成。模型只能在预设的意图列表里选,比如"咨询、报修、申请换新、投诉、无关闲聊"。第二,必须输出置信度,低于阈值时不允许进入Agent流程,直接降级到人工客服或者标准话术。第三,对"无关闲聊"这个意图要特别敏感,用户聊闲天绝对不能让Agent去执行任何工具。

我在实际项目里见过一个很典型的翻车案例:用户只是吐槽了一句"你们这个打印机真是气死我了",意图层给分到了"投诉",Agent直接调用了工单创建接口,给用户自动开了一个投诉工单,还把工单号发给了用户。用户一脸懵,运营团队一脸懵。后来我们把意图分类的置信度调高,并且给"负面情绪表达"单独设了一个意图,不给它挂任何工具,才解决这个问题。

意图层的另一项职责是抽取关键实体。用户说"我上周买的打印机不工作了",系统需要从这句话里抽取出产品名、购买时间范围、故障描述。这些实体后续会作为规划层的输入。实体抽取可以和意图分类放在同一个模型调用里完成,返回一个结构化JSON,效率更高。

但要注意,调用一次模型就要求它同时完成意图分类和实体抽取,这会增加输出的不确定性。我的经验是最好还是分成两步或者给模型更结构化的输出约束。如果担心延迟,用两个小并行调用也可以,关键是别让一次模型调用干太多事,否则它又会开始"自由发挥"。

3.2 规划层:把大任务写成可验收的工单

规划层的输入是一个"用户目标",输出是一串"任务描述符"。这个环节是整个架构里唯一需要模型发挥创造力的地方,也是最需要约束的地方。

我看到新手做规划器最常犯的错误,是让模型自由输出一段方案文本:"第一步查询订单,第二步分析保修政策,第三步生成回复"。这种输出没法被执行层消费,因为它不是结构化的,同一个步骤可能因模型表述不同产生无数种变体,解析出来全是坑。

正确的做法是让规划器输出严格的JSON结构。我常用的做法是在系统提示词里明确给出一个步骤模板,要求模型按模板生成。每个步骤包含:step_id、name、description、tool、input_schema、success_criteria。其中tool必须在给定的可用工具清单里选,input_schema要说明入参从哪里来,success_criteria要有可判断的标准。

还有一个非常关键但容易被忽略的点:规划器生成的步骤数量一定要控制在合理范围,我一般限制在5到8步。步骤太少说明拆解粒度太粗,执行层还是得靠模型自由发挥;步骤太多则产生大量中间产物,系统复杂度和出错概率都会飙升。如果规划流程很大,应该再做一层抽象:把任务拆成多个子Agent或者子流程,而不是把几十个步骤平铺在一个规划器里。

规划层还必须有能力处理"无法拆解"的情况。模型发现目标信息不足、或者用户请求超出了自己的能力范围,应该直接返回"需要更多信息"或者"无法完成"的结论,而不是硬编出一个错误的步骤序列。我在代码实现里会专门检查规划器输出的步骤数,如果逻辑上无法闭环,就拒绝进入执行阶段。

规划层对模型的温度参数设置也很讲究。我的习惯是设成0,关闭模型的随机性。规划是一个需要稳定性的环节,不需要模型发挥创造性,同一个目标每次生成的步骤序列应该尽量一致。否则同样的请求,这次先查订单、下次先查政策,测试都没法测。

3.3 执行层:能不用模型,就不用模型

执行层是系统里最不需要"智能"的地方,也是最需要用工程手段加固的地方。我的原则是:确定性操作绝不用模型,只有真正需要判断的环节才用模型。

什么叫确定性操作?查询订单状态、调用库存接口、写入数据库、调用工单系统创建工单,这些都是有明确入参和出参的操作,直接用代码函数实现即可。一个工具注册表,把工具名映射到对应的执行函数,执行层拿到任务描述符,去注册表里找到对应工具,传入参数,执行,返回结果。

那什么时候才需要在执行层用模型?只有在结果生成类任务里才需要。比如根据订单和保修信息生成一段给用户的回复话术,这个属于生成式任务,必须要用模型。再比如判断"用户提供的故障描述是否足够明确",这也可以交给模型判一下。但这类模型调用应该被封装成"一个特殊的工具",放进注册表里,由执行层统一调用,而不是让模型自己在流程里穿插。

执行层的另一个职责是参数绑定。每个步骤的入参不能由执行层自由发挥,而应该由规划层在任务描述符里明确指出从哪里取。比如"查询订单状态"的入参是"用户消息中抽取的订单号",执行层就只负责从上下文中把这个值取出来,填进工具调用里。如果取不到对应参数,执行层要做的事是返回"参数缺失",而不是自作主张猜一个值填进去。

我在项目里见过最多的问题,就是执行层拿到一个参数缺失的任务后,模型自作聪明地造了一个假订单号去调用系统。调用是成功了,但拿到的是别家用户的订单信息。这种情况在我们明确要求执行层"参数缺失必须报错"之后,才彻底消失。

执行层还要统一处理工具调用的异常:超时、网络错误、返回值格式不对、业务异常。每个异常都应该转换成标准化的错误码,返回给上层。上层拿到错误码才能判断是重试、换方案还是终止。

3.4 校验层:给系统装一个不带感情色彩的质检员

校验层是分层架构里最容易被偷工减料的环节。很多人觉得规划器和执行器都写好了,任务跑通就算完事,校验层随便写个"如果返回码不是0就报错"糊弄过去。但真正的可靠性,恰恰是靠这一层撑起来的。

校验层要做三件事。第一是结构校验,检查执行结果是否符合预期的schema,字段是否齐全,类型是否正确。第二是语义校验,检查结果在业务上是否合理。比如售后退换货场景,用户申请换新,系统查出用户的保修状态是"已过保",那结论就必须是"不符合换新条件";如果执行层返回的结果是"可以换新",这就是语义上的矛盾,必须拦截。第三是流程校验,检查当前步骤的产物是否满足进入下一步的条件。

语义校验怎么做?可以用一个独立的小模型调用,把执行结果和预期标准放在一个提示词里,让它判断"是否符合要求"并给出原因。这个判断任务本身非常简单,模型不容易出错,而且它和整个Agent的主流程是解耦的,就算偶尔判断失误,也只是多一次重试,不会污染主流程。

校验层还有一个非常重要的职责:循环检测。如果一个执行步骤连续两次产生完全相同的状态,或者规划器生成的新步骤序列和上一次一模一样,那基本可以判断系统进入了死循环。校验层需要记录每一步的"状态哈希"(把关键字段拼成一个字符串算哈希),一旦发现重复,立即终止并通知上层走人工兜底。

3.5 横切关注点:状态、记忆与日志

分层的系统如果不在状态管理上设计好,跑起来仍然会乱成一团。我给每一层都定义了一个简单的状态字段:pending、in_progress、success、failed、terminated。每个任务描述符都会有自己独立的生命周期,上层协调器只需要根据各步骤的状态来决定下一步动作。

记忆管理也是分层的。很多木偶Agent崩溃的根源,是它试图把所有的上下文全部塞给模型。分层之后,每层应该只看到自己需要的那一部分上下文。执行层做"查询订单"时,不需要看到用户三个小时前跟客服聊了什么;规划层做拆解时,只需要知道用户的当前目标、相关实体、已有的执行结果摘要,不需要具体的工具返回原文。

日志系统在分层架构里尤其重要。我给每个请求生成一个trace_id,贯穿所有层级的输入输出,每层执行完都把结构化日志写下来。出现问题时,按trace_id一拉,从意图分类到最终回复的每一层输入输出都清清楚楚,排查效率比木偶Agent高一个数量级。

记忆窗口的设计我倾向于用摘要化存储。不是把每轮对话原文都留给后续模型,而是每完成一个阶段,就把这个阶段的关键信息压缩成摘要,存进一个结构化状态对象里,供规划层和后续步骤使用。这样既保留了必要信息,又不会无限拉长上下文。

4. 从0到1:搭一个分层的售后工单Agent

4.1 定义核心数据结构

代码层面我习惯先用数据类把整个系统要传递的对象定义清楚。这一步看起来不起眼,但它是把"分层"真正落地的关键。没有清晰的数据结构,写着写着就会退化成用字典到处传,最后又变成一锅粥。

from dataclasses import dataclass, field from typing import Optional, Any from enum import Enum class StepStatus(str, Enum): PENDING = "pending" IN_PROGRESS = "in_progress" SUCCESS = "success" FAILED = "failed" TERMINATED = "terminated" @dataclass class TaskDescriptor: """任务描述符:规划层产出,执行层消费""" step_id: str name: str # 步骤名称,比如"查询订单状态" tool: str # 要调用的工具名,必须在注册表里存在 input_schema: dict # 入参说明,比如{"order_id": "来自用户消息的抽取结果"} success_criteria: str # 成功判定标准,比如"订单状态为有效且返回购买日期" inputs: dict = field(default_factory=dict) # 实际入参值,执行前由参数绑定填充 status: StepStatus = StepStatus.PENDING result: Optional[dict] = None # 执行结果 error: Optional[str] = None # 错误信息 @dataclass class IntentResult: """意图层输出""" label: str # 意图标签,必须在预设枚举里 confidence: float # 置信度 0~1 entities: dict # 抽取到的实体,比如{"product": "打印机", "order_id": "HD20240501"}

这两个数据类是整套系统的地基。TaskDescriptor在整个架构里流通,意图层负责产出IntentResult,规划层把IntentResult转成一个或多个TaskDescriptor,执行层逐个消费TaskDescriptor,校验层检查它的status和result。

4.2 意图层与入口路由

意图分类这块我不会让模型自由发挥,而是先有一个预设意图枚举,再让模型从枚举里选。阈值设成0.8,置信度低于这个值直接走人工客服通道。

INTENT_LIST = ["咨询", "报修", "申请换新", "投诉", "无关闲聊"] def classify_intent(user_message: str, history: list) -> IntentResult: prompt = f""" 判断用户的售后诉求属于哪个意图。只能从以下列表中选择一个:{INTENT_LIST}。 同时抽取与售后相关的关键实体:产品名称、订单号、问题描述、期望解决方式。 返回JSON格式: {{"label": "意图标签", "confidence": 0~1, "entities": {{"product": "", "order_id": "", "issue": ""}}}} 用户消息:{user_message} """ resp = call_model(prompt, temperature=0) data = parse_json(resp) return IntentResult(label=data["label"], confidence=data["confidence"], entities=data.get("entities", {})) def route(intent: IntentResult, user_message: str, session): if intent.confidence < 0.8: return {"action": "transfer_to_human", "reason": "意图置信度不足"} if intent.label == "无关闲聊": return {"action": "normal_chat", "reply": "您好,请问有什么可以帮您?"} if intent.label in ("报修", "申请换新"): return {"action": "start_agent", "intent": intent} if intent.label == "投诉": # 投诉不走Agent自动处理,直接转人工,避免误操作 return {"action": "transfer_to_human", "reason": "投诉需要人工介入"} return {"action": "normal_chat", "reply": "您好,请描述一下您遇到的问题。"}

这里注意一个细节:投诉没有给Agent任何工具权限,直接转人工。因为投诉场景里用户情绪占比很高,Agent自动触发工具大概率会激化矛盾。这算是业务规则对架构的反向约束——不是所有的意图都适合走Agent,意图层必须有能力把这些入口挡在外面。

4.3 规划层:让模型生成结构化步骤

规划器会在意图层确认进入Agent流程后被调用。它拿到用户目标和抽取出的实体,生成TaskDescriptor列表。关键点有两个:限定规划器可用的工具集合,以及强制JSON输出。

TOOL_SCHEMA = { "query_order": {"params": ["order_id"], "desc": "根据订单号查询订单状态、购买日期、产品型号"}, "query_policy": {"params": ["product_type", "issue"], "desc": "查询某类产品的保修和换新政策"}, "check_evidence": {"params": ["issue"], "desc": "判断用户的故障描述是否明确,返回是否充分"}, "generate_reply": {"params": ["order_info", "policy_info"], "desc": "生成给用户的最终回复话术"}, "create_exchange_order": {"params": ["order_id", "customer_message"], "desc": "创建换新工单"}, } def plan(intent: IntentResult, context: dict) -> list[TaskDescriptor]: prompt = f""" 用户目标:{intent.label} 已抽取实体:{intent.entities} 可用工具及其参数:{TOOL_SCHEMA} 请将上述用户目标拆解为3~6个步骤。必须遵守以下规则: 1. 每个步骤的tool只能从可用工具中选择,禁止编造不存在的工具。 2. 每个步骤的输入要么引用用户实体,要么引用前序步骤的输出,禁止凭空假设参数。 3. 每个步骤必须写清楚成功判定标准。 4. 直接输出JSON数组,不要输出任何解释。格式如下: [{{"step_id": "s1", "name": "查询订单", "tool": "query_order", "input_schema": {{"order_id": "实体order_id"}}, "success_criteria": "查询成功且返回订单状态"}}] """ resp = call_model(prompt, temperature=0) steps_json = parse_json(resp) # 硬性校验:工具必须存在,步骤数必须在合理范围 steps = [] for i, raw in enumerate(steps_json): if raw["tool"] not in TOOL_SCHEMA: continue # 或者标记为非法步骤,让上层决定是否重新规划 steps.append(TaskDescriptor( step_id=f"s{i+1}", name=raw["name"], tool=raw["tool"], input_schema=raw["input_schema"], success_criteria=raw["success_criteria"] )) if not 3 <= len(steps) <= 6: return [] return steps

我在这段代码里特意加了一个"非法步骤直接丢弃"的逻辑。实际项目中,如果丢弃后剩余步骤数量不合理,我建议直接触发一次重新规划,让模型看到"你刚才给的步骤不合法,请修正"。这样可以避免执行阶段踩到规划阶段埋的坑。

4.4 执行层:工具注册表与参数绑定

执行层的核心是一张工具注册表,外加一个参数绑定模块。参数绑定的职责是,从上下文对象里取出任务描述符需要的实际参数值。

def bind_arguments(step: TaskDescriptor, context: dict) -> dict: """把input_schema里的参数说明,映射为上下文中的实际值""" bound = {} for param, source in step.input_schema.items(): if source.startswith("实体"): key = source.replace("实体", "").strip() if key in context.get("entities", {}): bound[param] = context["entities"][key] else: raise ValueError(f"缺少实体参数: {key}") elif source.startswith("前序输出"): # 从前序步骤的结果里取值,这里简化处理 prev_step_id = source.split(":")[1] bound[param] = context["step_results"][prev_step_id]["data"][param] else: bound[param] = context.get(source) return bound TOOL_REGISTRY = { "query_order": query_order_service, "query_policy": query_policy_service, "check_evidence": check_evidence_service, "generate_reply": generate_reply_service, "create_exchange_order": create_exchange_order_service, } def execute_step(step: TaskDescriptor, context: dict) -> TaskDescriptor: if step.tool not in TOOL_REGISTRY: step.status = StepStatus.FAILED step.error = f"未知工具: {step.tool}" return step try: args = bind_arguments(step, context) except ValueError as e: step.status = StepStatus.FAILED step.error = str(e) return step step.status = StepStatus.IN_PROGRESS try: result = TOOL_REGISTRY[step.tool](**args) step.result = result step.status = StepStatus.SUCCESS except Exception as e: step.status = StepStatus.FAILED step.error = f"{type(e).__name__}: {e}" return step

这里面的generate_reply和check_evidence实际上内部会调用模型,但从执行层的视角看,它们只是两个普通工具。这个抽象非常关键——整个Agent流程不知道也不关心最终话术是模型生成还是模板拼出来的,它只需要知道这个步骤是否成功、结果是否符合预期。想替换实现的时候,只要保证输入输出契约不变,里面的逻辑随便改。

4.5 主流程:把四层串成闭环

主流程是一个循环:规划器产出步骤,执行器逐个执行,校验器检查结果,有问题就决定是重试、终止还是重新规划。

MAX_STEPS = 10 STATUS_HASH_SET = set() def run_agent(user_message: str, session): # 第一步:意图层 intent = classify_intent(user_message, session.history) action = route(intent, user_message, session) if action["action"] != "start_agent": return action # 第二步:规划层 context = {"entities": intent.entities, "step_results": {}} steps = plan(intent, context) if not steps: return {"action": "transfer_to_human", "reason": "无法生成有效规划"} # 第三步:执行层+校验层循环 for round_no in range(MAX_STEPS): all_success = True for step in steps: if step.status == StepStatus.SUCCESS: continue step = execute_step(step, context) context["step_results"][step.step_id] = step.result or {} # 校验层介入 if not validate_step(step): step.status = StepStatus.FAILED step.error = "校验未通过" # 失败两次就终止,走人工兜底 if step.retry_count >= 2: return {"action": "transfer_to_human", "reason": f"步骤{step.step_id}多次失败: {step.error}"} step.retry_count += 1 all_success = False continue step.status = StepStatus.SUCCESS # 死循环检测:把当前所有步骤的状态哈希一下 state_signature = hash(tuple(f"{s.step_id}:{s.status}:{s.result}" for s in steps)) if state_signature in STATUS_HASH_SET: return {"action": "transfer_to_human", "reason": "检测到循环状态"} STATUS_HASH_SET.add(state_signature) if all_success: return {"action": "reply", "content": steps[-1].result["reply"]} return {"action": "transfer_to_human", "reason": "超过最大执行轮数"}

这段代码是简化版,省略了重试次数字段的定义和validate_step的具体实现,但整体骨架就是真实项目里能用的一版。它把每一层的职责都卡得死死的:意图层负责判断,规划层负责拆解,执行层负责干活,校验层负责挑刺。主循环只是把四个环节像齿轮一样咬合起来。

5. 实战中的坑:为什么分层了还会翻车

5.1 规划器"脑补"出不存在的工具

即使你明确给了工具清单,规划器在复杂场景下还是有概率生成一个根本没注册过的工具名。比如用户说"帮我把故障图片发给维修中心",它可能自行脑补出一个send_image_to_repair工具。这个工具在系统里压根不存在,执行层就会直接报错。

我们的对策是三层保险。第一,在规划器的提示词里反复强调"只能使用给定工具";第二,在代码解析层直接丢弃不在注册表里的工具调用;第三,如果解析后发现步骤序列里有非法工具,不回执行层,直接重新规划一次。实测下来,第二层保险能拦截掉80%的脑补,剩下20%会被第三层重新规划消化。加了这三道保险之后,基本不会再出现莫名其妙的工具名。

5.2 工具返回结果太长,污染后续上下文

这是所有Agent工程都绕不开的问题。一次订单查询可能返回一大串原始数据:物流轨迹、订单备注、促销活动信息、优惠券记录。如果把这些原文全部传给规划层或者生成回复的模型,上下文窗口很快就会爆炸。

我们的做法是给执行结果做摘要化。每个工具的结果都经过一个处理函数,只保留后续步骤需要的核心字段。比如query_order的结果只保留:订单状态、购买日期、产品型号、是否在保修期、是否符合换新条件。其他原始信息宁可丢掉,也不塞给模型。

这个取舍刚开始做的时候会很纠结,觉得丢信息可惜。但实际验证过之后会发现,绝大多数原始字段对业务判断毫无帮助,保留它们只会稀释模型的注意力。数据的精简程度,决定了Agent的稳定性。

5.3 模型在规划阶段"自由发挥",输出不合法JSON

规划器用模型生成JSON,最怕的就是它偶尔输出一段带注释、带解释文字、甚至格式直接的伪JSON。我在项目里遇到过模型直接输出一段"以下是步骤:1. 首先……2. 然后……"然后硬塞到JSON解析器里,直接报错。

解决办法有三步。第一,提示词里写死"只输出JSON数组,禁止任何解释";第二,用容错解析器,先尝试标准解析,如果失败,用正则把json代码块剥出来再解析;第三,如果还是解析失败,就当作规划失败,走重新规划。配置好这三步之后,规划器的有效输出率能稳定在98%以上。

代码示例里的parse_json函数,我实际用的是先剥代码块、再标准解析、再修复常见缺逗号问题的三层方案。这些小技巧看着不起眼,但都是实打实提升系统稳定性的东西。

5.4 死循环:同一个方案反复执行

分层之后死循环依然存在,但容易被检测得多。最常见的死循环场景是:某个步骤校验失败,重试一次还是失败,系统重新规划,结果规划器又生成了和原来一模一样的步骤序列,再次执行,再次失败,周而复始。

我们的解法是在主循环里维护一个"状态哈希集合"。每个轮次结束时,把当前所有步骤的ID、状态、关键结果字段拼在一起算哈希,如果发现这个哈希已经出现过,说明系统没有在往前走,直接终止并转人工。

这个方案不是完美的,它没法做到100%识别所有循环形态,但足以拦截最典型的那一种。后来的实践里我又加了一条规则:同一个业务步骤失败重试次数不超过2次,一旦超过就转人工。两条规则叠加之后,死循环基本绝迹。

5.5 人工兜底:Agent系统的安全网

再好的分层架构,也不能保证100%自动闭环。尤其是售后场景,涉及用户权益、退款金额、政策灰色地带,这些情况在业务上天然需要人工决策。所以整个系统的最后一道防线,必须是一个设计良好的人工介入通道。

我们的人工兜底设计是:当校验层连续失败、规划层产出空步骤、或者循环检测触发时,系统生成一封包含完整trace_id、用户原话、每一步的输入输出、失败原因的结构化摘要,推送给人工客服。客服打开后台,点进trace详情,能看到从意图分类到最终失败的完整链路,直接定位问题,不用像以前那样靠猜。

这套兜底通道看起来简单,但它给整个系统赋予了"试错的安全边际"。运营团队愿意先让Agent跑50%的流量,也是因为确定有人在末端接着,出了问题不会造成业务事故。

5.6 测试与回归:录下每一次翻车现场

分层架构让Agent系统变得可测试,这件事常常被忽略。木偶Agent你是没法做测试的,因为每次运行路径都不可控;分层之后,每个环节都是确定的接口,可以逐层做单元测试。

我常用的方法是构建一个"回放测试集"。每次线上出现问题,就把这条会话的完整请求数据、各层输入输出录下来,归入回归测试集。改完代码后,批量回放这些样本,确认之前的翻车场景不再出现。这个方法操作起来成本不高,但效果非常显著——它能防止你在修复一个bug的同时,把另外几个场景搞坏。

状态哈希集合也可以保留下来用于对比。每次测试运行后,把最终状态序列和预期状态序列做对比,任何偏差都会暴露出来。这个做法在Agent系统里特别有价值,因为它能让你像测普通软件一样,对智能体系统做自动化回归。

最后说点实在的

这些方法不是从理论框架里推出来的,是我和团队在真实业务场景里一遍遍踩坑踩出来的。从最早的木偶Agent翻车,到后来强行分层但层间传自然文本继续翻车,再到把层间通信改成结构化任务描述符之后才真正消停,整个过程让我确信一件事:做可靠智能体系统,靠的不是让模型变得更聪明,而是用一个明确的结构,把模型的每一次“灵光一现”都约束在安全的边界里。

如果你现在正准备做一个Agent系统,我的建议是先从“给每个步骤写验收标准”开始。别急着写代码,先把你要处理的业务拆成一张步骤表,每个步骤写清楚工具、入参、出参、成功标准。这张表写好了,分层架构自己就会浮现出来;写不好,那你用再漂亮的框架也还是会翻车。这是我掏心窝子的经验,希望你能少走几步弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 10:19:40

Java牙科诊所管理系统:轻量级课设实战方案

简介&#xff1a;本资源是一套完整的Java毕业设计项目——私人牙科诊所管理系统&#xff0c;面向计算机专业本科生、Java初学者及医疗信息化实践者&#xff0c;聚焦病患管理、预约挂号、医生排班与财务统计等真实诊所运营场景&#xff0c;助力掌握企业级Java应用开发全流程。压…

作者头像 李华
网站建设 2026/10/18 10:18:50

Oracle 11.2.0.4 PSU补丁升级实战:从版本号解析到opatch应用与回退

简介&#xff1a;该资源为Oracle数据库11.2.0.4版本的Patch Set Update补丁包&#xff0c;补丁编号p33477185&#xff0c;发布于2022年1月&#xff0c;专为Linux x86-64平台设计&#xff0c;面向需要为Oracle 11g数据库打补丁的DBA与运维人员&#xff0c;用于修复安全漏洞、优化…

作者头像 李华
网站建设 2026/10/18 10:15:25

摩天大楼电梯仿真:从建模到调度的完整实践指南

简介&#xff1a;基于Java实现的摩天大楼电梯类型与调度算法仿真项目&#xff0c;模拟真实电梯运行&#xff0c;覆盖单向、双向、观光、高速、消防等电梯建模&#xff0c;并实现FCFS、最近楼层优先、最少行程时间、预调度和群控算法。通过随机生成乘客流量&#xff0c;可对比不…

作者头像 李华
网站建设 2026/10/18 10:14:20

基于CNN卷积神经网络的蔬菜识别图像分类实战详解

做毕业设计那会儿&#xff0c;我选了“深度学习基于CNN卷积网络的蔬菜识别”这个方向。一句话讲&#xff0c;就是拿一张蔬菜照片&#xff0c;交给一个卷积神经网络模型&#xff0c;让它判断这是黄瓜、西红柿还是茄子。任务听起来简单&#xff0c;但实际做下来&#xff0c;从数据…

作者头像 李华
网站建设 2026/10/18 10:11:52

Llama 3 架构精读与 FP8 量化长上下文扩展实战

1. 为什么值得花时间精读 Llama 3Llama 3 发布之后&#xff0c;我身边不少做推理部署和应用开发的朋友都在第一时间拉取了权重做测试。但真正把技术报告逐页读完、把模型结构拆到注意力头级别的人其实不多。大多数人停留在“跑了个 demo&#xff0c;效果还行”的层面&#xff0…

作者头像 李华
网站建设 2026/10/18 10:11:31

NLP文本分类实战:从数据清洗到模型调优的完整流程

做了快三年的自然语言处理落地项目&#xff0c;我最深的感触是&#xff1a;教科书教的是从干净数据到漂亮模型&#xff0c;生产环境给你的却是一堆乱得离谱的原始数据。这个差距&#xff0c;正是"硬核实战"和"课堂Demo"之间真正的分水岭。最近我完整地从零…

作者头像 李华