☰
LangGraph子图模式构建多智能体AI客服系统的实战方案
2026/10/4 5:39:46 网站建设 项目流程

做客服方向的大模型应用,大多数团队都会卡在同一个地方:单个智能体确实能跑通 demo,但一旦同时挂上订单查询、退换货、商品咨询、转人工这些能力,Prompt 和工具列表就迅速膨胀,模型开始分不清该调用哪个工具,业务规则也越改越乱。我在这个项目里换了一种组织方式,用 LangGraph 的子图模式把 AI 客服系统拆成“一个主图 + 多个能力子图”,主图只负责意图路由和状态分发,每个客服能力封装成独立的子图,内部再挂自己的工具和流程。

这套方案最终形成了一个可落地的多智能体协作框架:用户消息进来后,主图先判断意图,再按需调用订单查询、售后办理、商品咨询或人工转接子图,每个子图内部又是独立的 ReAct 智能体。项目全部基于 LangGraph 实现,模型走 OpenAI 兼容协议,所以换任意国产大模型都能直接跑;最后用 FastAPI 包一层 HTTP 接口就能上线。如果你正在搭智能客服、工单系统或售前导购这类多流程系统,或者已经试过 LangGraph 但总觉得链路乱、状态管理糊,这篇的建模思路、主图与子图挂载方式、路由节点写法和排查记录,可以直接拿来抄作业。

1. 为什么用子图模式做 AI 客服系统

1.1 单个智能体在客服场景里为什么撑不住

客服系统的第一特征是业务边界不清晰。用户说一句“我那个耳机没收到,想退款”,这句话同时涉及物流查询和售后办理两个意图,任何一个单独的智能体面对这种信息都会纠结先调哪个工具。我一开始图省事,把所有工具挂到一个 agent 里,Prompt 写了三千字还是压不住,实测十次里有两三次模型会选错工具,比如用户问“能开电子发票吗”,它反而去调了库存查询接口。

这类问题在单条 Prompt 里很难彻底解决。客服需求的另一个特点是规则密集,退换货有时效限制、运费险有赔付范围、某些品类不支持无理由退换,这些规则如果全塞进同一个 Prompt,或者是堆进同一个工具描述,改一次业务策略就要做一轮全量回归。而且单智能体链路意味着每一次修改的爆炸半径是整个客服系统,上线风险极大。把能力按业务域拆开之后,规则就散落到各个子图里,谁的业务谁维护,模型的工具选择空间也小得多,准确率自然上来。

1.2 三种多智能体协作模式,我为什么选子图

LangGraph 项目里做多智能体,目前主流有三条路:supervisor 模式、handoff 模式和子图模式。我做这个客服系统前把三种都试过一遍,放在真实场景里对比差异非常明显。

协作模式组织结构优点缺点典型适用场景
supervisor 模式一个主管智能体统一调度多个 worker编排灵活,能处理意图交织的动态任务主管上下文压力大,链路不透明,主管决策错了难排查需求变化频繁、需要动态规划的任务
handoff 模式智能体之间直接转移会话控制权会话交接自然,模拟真实坐席转接控制流不透明,容易互相“踢皮球”有明确会话转交语义的系统
子图模式主图按意图分发到独立能力子图边界清晰、状态隔离、子图可独立复用跨子图动态协同能力偏弱意图边界明确的业务系统,如客服

选子图模式的核心原因是客服系统的意图分布相对稳定,无非就是订单、物流、售后、商品咨询和转人工这几类,用户不会要求智能体现场编排出一个新流程。子图模式把每条业务线变成独立编译的图,主图只关心“这次请求该进哪个子图”,子图之间互相不感知,业务团队完全可以各自维护自己那部分。更直白一点,这就是把 AI 客服做成了电话总机加专席坐席:总机按按键分线,每线坐席只处理自己那一摊事。

1.3 项目架构与功能边界

整个系统的顶层结构非常清晰,一共就三层。最外层是 FastAPI 服务,负责接收 HTTP 请求、维护会话、返回结果;中间层是主图,承担意图路由和状态分发;最里层是四个子图,分别对订单查询、退换货办理、商品咨询、人工转接四个能力域负责。子图之下再挂工具,就是订单中心接口、售后工单接口、商品知识库这类业务系统。

FastAPI HTTP 服务 └─ 主图 main_graph ├─ intent_router 意图路由 ├─ order_agent 订单查询子图 ├─ after_sale_agent 退换货子图 ├─ consult_agent 商品咨询子图 └─ human_agent 人工转接 工具层:订单中心 API、售后工单系统、商品知识库

这个分层最大的意义在于,主图不感知任何业务细节,它不需要知道订单查询具体调了什么 API,也不需要理解退换货的七日限制;它只维护一个路由表,把带“订单”字眼的请求丢给订单子图,把带“退换”字眼的请求丢给售后子图。业务变复杂时,比如售后流程里加一个“运费险理赔”环节,我只需要修改售后子图内部的节点,主图和其他子图一行代码都不用动。这就是子图模式给工程维护带来的直接好处。

2. 先建模再写代码:状态与工具准备

2.1 LangGraph 的最小运行逻辑,五分钟说清

很多朋友第一次看 LangGraph 的代码会有点懵,其实它的运行逻辑就三句话:定义状态类型,声明节点,再用边把节点串起来。节点是一个普通函数,接收当前状态字典,返回一个字典表示要更新哪些字段;边定义执行顺序,条件边则会根据状态里的值决定下一跳走向。整个图编译之后,调用 invoke 就是在状态图上从起点走到终点。

子图模式之所以成立,是因为 LangGraph 里的图本身可以用来当一个节点。子图内部可以继续有自己的节点、边和工具调用,编译之后扔进主图的一个节点位置,对外部来说它就是一个带输入输出的黑盒。这种“图里有图”的嵌套结构,天然适合把复杂业务分层建模。我这里要强调一个容易踩坑的点:LangGraph 的状态更新不是自动累积的,除了少数用加法合并器处理的字段,返回的键值对会直接覆盖对应字段。所以设计状态时要想清楚哪些字段需要累积、哪些字段只需要存最近一次结果。

2.2 任务状态 KbState 定义

状态是贯穿整个系统的主线,多智能体协作最忌讳的就是每个子图自己搞一套状态定义,主图传不下去、子图回不来。我的做法是全系统共用一个 KbState,字段尽量精简,用哪些取哪些。下面这个定义是整个项目的核心数据结构:

from typing import Annotated, TypedDict from langgraph.graph import StateGraph, START, END, add_messages from langchain_openai import ChatOpenAI from langchain_core.messages import AIMessage, HumanMessage import os class KbState(TypedDict): messages: Annotated[list, add_messages] user_id: str order_id: str intent: str need_human: bool model = ChatOpenAI( model=os.getenv("LLM_MODEL", "qwen-plus"), api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), temperature=0, )

这里最关键的是 messages 字段用了Annotated[list, add_messages]。这个声明表示每当一个节点往父状态返回 messages 时,LangGraph 不是覆盖整个消息列表,而是把新消息追加到已有消息后面。你可以把它理解成一张不断加长的聊天记录纸,每个节点做的事情只是在纸的末尾追加内容。如果没有这个合并器,第二轮对话时子图返回的新消息会把第一轮的全部历史消息覆盖掉,客服系统立刻失忆。其余字段按业务需要定义即可,比如 need_human 用来标记是否要转人工,order_id 存当前关联的订单号,节点可以直接读写这些字段而不依赖大模型。

2.3 工具层:让智能体真正能干活的基础

模型自身不产生业务数据,所有订单状态、售后政策、商品信息都来自工具层。这一步我用 LangChain 的 @tool 装饰器定义函数,函数体内部在真实项目里就是调用订单中心 API 或读取数据库,这里为了可演示先返回模拟结果。有一点非常重要:@tool 下面的 docstring 会被作为工具描述原样交给模型,模型靠这个描述来决定何时调用工具,所以描述字段必须写清楚“这个工具是干什么的”“应该传什么参数”。

from langchain_core.tools import tool @tool def get_order_status(order_id: str) -> str: """根据订单号查询物流和订单状态。""" return f"订单 {order_id} 已发货,快递单号 SF1234567,预计 3 天内送达。" @tool def check_return_eligibility(order_id: str) -> str: """检查订单是否支持退换货。""" return "该订单支持 7 天无理由退换货。" @tool def create_after_sale_ticket(order_id: str, reason: str) -> str: """创建售后工单,传入订单号和售后原因。""" return f"售后工单 AF-{order_id} 已创建,售后同事会在 24 小时内联系您。" @tool def search_products(keywords: str) -> str: """从商品知识库中检索商品信息。""" return "这款降噪耳机支持 7 天无理由退换,售价 399 元,支持蓝牙 5.3。"

工具设计上有一条我实测得到的经验:工具粒度要按业务子图切分,而不是把所有工具堆到一个模型面前。订单查询子图只看得到订单和物流工具,售后子图只看得到退换货和工单工具,这样模型选错工具的概率会大幅下降。原因很简单,工具数量一多,模型在 function calling 阶段的 token 就长、注意力就分散,误调用的几率随工具数量上升。

3. 子图构建实战:三个坐席级智能体

3.1 订单查询子图:基于 create_react_agent 的轻量实现

每个子图本质上是一个独立的智能体。订单查询子图是其中最典型也最简单的一种,直接用 LangGraph 的 create_react_agent 就能构建。这个函数内置了 ReAct 循环:模型思考要不要调工具、调用工具、看结果、再回答,整个过程在主图里只体现为一个节点。系统因此有了“多智能体”的效果,每个子图都是独立决策的大脑,带着自己的工具和行动准则工作。

from langgraph.prebuilt import create_react_agent order_subgraph = create_react_agent( model, tools=[get_order_status], )

子图构建出来之后,不能直接在用的时候把大段历史一股脑塞给它。我这里做了一层包装函数,把主图里最近的几轮消息截取出来再传给子图,子图返回的答案再塞回主图的消息流。为什么要截取而不直接传全量?因为每个子图都是独立的 ReAct agent,每次触发都会重新处理输入消息,如果每次把这个会话几个小时的历史全部灌进去,token 成本会迅速失控,而且历史里其他子图的业务内容对这个子图是纯噪音。

def run_order_agent(state: KbState) -> dict: recent_messages = state["messages"][-6:] result = order_subgraph.invoke({"messages": recent_messages}) answer = result["messages"][-1].content return {"messages": [AIMessage(content=answer)]}

我这里截取最近 6 条消息是处理“用户说‘那个耳机呢’但前文提到过耳机”这类指代问题。如果只传最新一条用户消息,子图没有任何上下文,根本听不懂“那个”指什么;传最近一屏对话,一方面保留了指代信息,另一方面不至于把半小时前的维修工单记录也带进来干扰它。

3.2 退换货子图:把“资格校验+提交工单”封装成流程

退换货子图是整张图里业务最重的一块。它挂在两个工具上,一个是 check_return_eligibility 负责校验订单是否还能退换,一个是 create_after_sale_ticket 负责真正提交售后工单。这两个工具的组合让子图内部形成了一条隐性的业务链:先校验再提交,模型负责判断顺序。

after_sale_subgraph = create_react_agent( model, tools=[check_return_eligibility, create_after_sale_ticket], ) def run_after_sale_agent(state: KbState) -> dict: if state.get("need_human"): return {"messages": [AIMessage(content="正在为您转接人工客服,请稍候。")]} result = after_sale_subgraph.invoke({"messages": state["messages"][-6:]}) answer = result["messages"][-1].content return {"messages": [AIMessage(content=answer)]}

真实业务里退换货流程比这个复杂得多,可能涉及优惠券回滚、库存退回校验、审批流。更严格的工程化做法是在子图内部再用手写 StateGraph 做显式流程:一个节点做资格校验,返回 allowed 和 reason 两个字段,条件边根据字段走提交节点或拒绝节点。这样流程完全可控,不依赖模型的临场编程。我在项目里的建议是,涉及资金、优惠、库存流转的关键路径,尽量用显式 StateGraph 固定住步骤;只有类似“解释退换政策”这种开放对话,才交给 ReAct 的模型自由发挥。

3.3 商品咨询与人工转接子图:不是所有子图都需要大模型

商品咨询子图本质是一个轻量 RAG 入口。演示代码里我用 search_products 模拟知识库检索,真实环境换成向量库检索,把命中片段塞给模型生成回答就行。这类子图的 ReAct 循环不需要太复杂,一个检索工具加一个拼接 Prompt 的节点已经足够,因为商品咨询的回答大多来自知识库原文,模型主要做润色和口语化表达,不需要自己编造商品参数。

consult_subgraph = create_react_agent( model, tools=[search_products], ) def run_consult_agent(state: KbState) -> dict: result = consult_subgraph.invoke({"messages": state["messages"][-6:]}) answer = result["messages"][-1].content return {"messages": [AIMessage(content=answer)]}

人工转接子图就更特殊了,它完全不调用大模型。用户表达转人工意图后,人工转接子图要做的只是把 need_human 置为 True,然后给一个固定回复。很多项目把转人工也塞给模型生成,既浪费 token,又容易在用户情绪激动时生成不合适的回复。我的实现是把它做成一个普通节点函数,不经过任何 LLM 调用:

def run_human_agent(state: KbState) -> dict: return { "messages": [AIMessage(content="已为您转接人工客服,请描述您的问题。")], "need_human": True, }

这里也体现了一个设计原则:能用代码解决的问题不要浪费模型。子图本身只是一种组织方式,节点里可以是任意逻辑,可以是模型调用,也可以是普通函数。

4. 主图编排:多个子图协作的真正难点

4.1 子图挂载的两种方式和我的选择

LangGraph 官方支持两种把子图挂到主图的方式。一种是直接把编译好的子图对象传给 add_node,这是最省事的做法;另一种是把子图的调用包进一个普通节点函数,再把这个函数 add_node,也就是前文展示的做法。我在这里明确推荐第二种。

直接挂载很快,但有几个隐藏约束:要求子图状态字段必须是主图状态字段的子集,否则运行时会因为状态键对不上而报错;而且子图在执行过程中产生的中间状态容易污染主图,排查问题的时候很难分清楚哪个字段是哪一层写入的。包装函数则相当于给子图加了一层显式的输入输出边界,我可以控制传给子图的历史长度,可以拦截子图的返回结果做二次处理,还可以在节点这一层打日志记录每个子图的耗时和 token 消耗。对于正式上线的客服系统,这层拦截的价值非常大。

main_graph = StateGraph(KbState) main_graph.add_node("intent_router", intent_router_node) main_graph.add_node("order_agent", run_order_agent) main_graph.add_node("after_sale_agent", run_after_sale_agent) main_graph.add_node("consult_agent", run_consult_agent) main_graph.add_node("human_agent", run_human_agent) main_graph.add_edge(START, "intent_router") main_graph.add_conditional_edges( "intent_router", route_by_intent, { "order_agent": "order_agent", "after_sale_agent": "after_sale_agent", "consult_agent": "consult_agent", "human_agent": "human_agent", }, ) for node_name in ["order_agent", "after_sale_agent", "consult_agent", "human_agent"]: main_graph.add_edge(node_name, END)

这里有个重要的工程决策:四个子图节点都直接连到 END。也就是说在一次 invoke 里,主图只选择一条子图路径执行,执行完就结束,不会出现子图之间来回跳转。多轮对话的持续协作不靠一次 invoke 内的循环,而是靠下一次用户请求再次进入主图、通过 intent_router 重新路由到对应子图。这样做的好处是每轮请求的链路都是确定的,出了问题可以非常快速定位是哪一条路径。

4.2 意图路由节点的设计:先规则后模型

意图路由是多智能体协作的第一道闸门,它决定会话进入哪个子图。这里我用了一套基于关键词的规则路由,写起来直接,跑起来零成本,而且完全可测试:

def intent_router_node(state: KbState) -> dict: last_message = state["messages"][-1] text = last_message.content.lower() if any(word in text for word in ["人工", "转人工", "投诉", "经理"]): return {"intent": "human_agent"} if any(word in text for word in ["订单", "物流", "快递", "发货", "签收"]): return {"intent": "order_agent"} if any(word in text for word in ["退", "换货", "退款", "售后", "维修"]): return {"intent": "after_sale_agent"} return {"intent": "consult_agent"} def route_by_intent(state: KbState) -> str: return state["intent"]

为什么路由不直接用大模型?因为我实测发现,客服意图分类这种低频、高确定性的任务,用规则能覆盖九成以上场景,而大模型每次路由都会消耗 token 和延迟,并且偶尔还会把“我遇到骗子了想退款”这种消息错分到投诉子图。规则匹配的缺点是遇到语义变化会失灵,比如“我买的东西在路上了吗”虽然能命中“路上”但和 logistics 关系不大,所以我在规则后面留了一条升级路径:规则命中不了时,可以再挂一个轻量分类模型或小 LLM 做兜底。上线初期用纯规则,收集一批真实用户语料后,再决定要不要加模型兜底,这个节奏比较稳。

4.3 会话记忆:如何让多个子图共享上下文

多智能体系统最容易翻车的部分不是路由,而是记忆。用户第一轮说“帮我查一下 20241128 的订单”,第二轮说“它什么时候到”,这里的“它”必须能指回到 20241128。LangGraph 处理这个问题的标准做法是 Checkpointer 加 thread_id:Checkpointer 负责保存每一轮执行完成后的完整状态,thread_id 是会话的唯一标识,同一会话的多次请求会基于保存的状态继续执行。

from langgraph.checkpoint.memory import InMemorySaver memory = InMemorySaver() compiled_main_graph = main_graph.compile(checkpointer=memory) config = {"configurable": {"thread_id": "session-10001"}}

这里要克服一个常见误区:Checkpointer 保存的是主图状态里的 messages,但每个子图在包装函数里只接收最近 6 条消息,所以会话记忆的“长期性”体现在主图,而子图只做局部推理。这个分层很有好处,子图不需要知道完整的历史,它只要知道足够回答当前问题的上下文。生产环境里 InMemorySaver 只适合单机演示,正式服务建议把 Checkpointer 换成 Postgres 或 Redis 版本,这样即使进程重启,用户之前的对话上下文也不会丢。

5. 编译、调试和对外服务

5.1 编译主图并跑通一次完整调用

子图全部构建完、主图节点和边都接好之后,编译主图然后就可以调用了。编译这个步骤很像吹一口气把节点、边、状态定义绑定成一个可执行的图结构,Common errors 基本都集中在这一层暴露。跑通一次完整调用代码如下:

compiled_main_graph = main_graph.compile(checkpointer=memory) result = compiled_main_graph.invoke( {"messages": [HumanMessage(content="帮我查一下订单 20241128 到哪了")]}, config={"configurable": {"thread_id": "session-10001"}}, ) print(result["messages"][-1].content)

第一次请求会走 查询订单去向 的消息进入 intent_router,命中“订单”关键词后走到 order_agent 子图,子图里的 ReAct 循环调用 get_order_status 工具,最终返回物流信息。接着同一会话再问一句“它什么时候能送到”,注意这次的输入里没有订单号,但因为 InMemorySaver 保存了上一次的完整消息,主图状态里仍然存着 20241128 这个订单号,子图看到最近六条消息里包含上下文,就能正确回答。这种跨轮指代能力,是客服系统区别于普通单轮问答的核心体验。

5.2 上线前先看图,画出来的结构不会骗人

LangGraph 自带图可视化能力,编译后的图可以直接导出结构图,我每次改完图结构都会导出一张 PNG 贴到会议室白板上。这一步对排查链路特别管用,尤其是节点多、边复杂的情况,人眼看代码容易漏,看图的连接关系一眼就能发现某个节点没被任何一条路径引用,或者某个条件边映射到了不存在的节点名。

# 导出结构图,检查节点与边的连接关系 graph_image = compiled_main_graph.get_graph().draw_mermaid_png() with open("kb_graph.png", "wb") as f: f.write(graph_image)

我的调试习惯是分两段走。第一段用 mock 函数替代真实工具调用和大模型调用,只验证路由逻辑和状态流转是否符合预期;第二段才接上真实模型,逐步打开日志看每条链路的完整轨迹。如果直接一步到位跑全链路,一旦出问题,你很难分清是路由错、工具错、模型错还是状态更新错。线上的话建议把 LangSmith 或同类 tracing 工具接上,多智能体系统每次请求会涉及两三层嵌套调用,没有追踪日志,排查一个“用户转人工失败”的 case 可能要翻半小时代码。

5.3 用 FastAPI 包装成 HTTP 接口

AI 客服系统最终要暴露给前端或呼叫中心调用,我用 FastAPI 做了一层薄薄的 HTTP 接口。这里不需要把 LangGraph 的图对象暴露出去,后端只接收 session_id、用户ID和消息文本,内部用 thread_id 绑定会话,返回最后一条 AI 消息和是否需要转人工的标志。

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI(title="AI 客服系统") class ChatRequest(BaseModel): session_id: str user_id: str message: str class ChatResponse(BaseModel): answer: str need_human: bool = False @app.post("/chat", response_model=ChatResponse) def chat(req: ChatRequest): config = {"configurable": {"thread_id": req.session_id}} result = compiled_main_graph.invoke( {"messages": [HumanMessage(content=req.message)], "user_id": req.user_id}, config=config, ) return ChatResponse( answer=result["messages"][-1].content, need_human=result.get("need_human", False), )

启动服务后直接调 uvicorn main:app --port 8000,前端 POST 一个 JSON 就能拿到结果。如果需要打字机式的流式输出,可以把 invoke 换成 astream,接口改成 SSE 或 WebSocket;不过流式会增加不少复杂度,第一版建议先同步返回,把链路跑稳再说。

6. 常见问题速查表与工程优化建议

6.1 高频问题排查速查表

我把这个项目里踩过的高频问题整理成一张表,这些问题在 LangGraph 多智能体项目里基本都会碰到,直接对着查能省很多时间。

问题现象可能原因处理方式
报 Recursion limit reached子图内部 ReAct 循环没触发退出条件,或主图错误地把节点连回路由节点形成死循环检查子图工具是否返回足够明确的结束信号,检查主图条件边是否闭环
报 Invalid key 错误节点函数返回了 KbState 里没有的字段要么在状态定义里补上该字段,要么精简节点返回值,只返回已定义字段
多轮对话失忆,回答对不上前文没配 Checkpointer,或 thread_id 每次请求都在变compile(checkpointer=...),invoke 时用同一 thread_id
模型频繁调错工具工具数量太多,或工具描述写得太含糊按子图拆分工具列表,把 @tool docstring 写成具体、带边界说明的操作手册
消息出现重复包装函数里同时手动 append 了一条消息,主图又有 add_messages 合并器,两边一叠加就重复节点返回的消息只通过 add_messages 合并,不要手动修改 state["messages"]
API 名称找不到langgraph 不同版本之间的模块名有差异优先用当前稳定版本;常见的差异点是 MemorySaver 与 InMemorySaver、graph.message.add_messages 与 graph.add_messages

这里有个我反复强调的坑:不要在包装节点里既手动修改state["messages"],又通过返回字典用 add_messages 合并。手动 append 加自动合并等于消息写了两遍,子图下次接收上下文时会看到重复内容,模型的回答质量会肉眼可见地下降。正确的姿势是只做一件事,要么不清空 messages 只靠合并器追加,要么只在返回字典里给出要新增的消息。

6.2 成本与 token 优化

多智能体系统的 token 消耗比你想象中涨得快,因为同一个用户请求会经过路由判断、子图 ReAct 循环、工具结果回填等多个环节。我的第一条优化实践是路由尽量用规则或小分类模型,这一环节省掉的 token 非常可观。第二条是控制每次传给子图的历史窗口长度,不要图省事把完整 messages 全灌进去,历史每多一轮,每个子图的 ReAct 循环就要重新处理一轮,成本是叠乘的。

第三条建议是设计工具调用时“能先拦截就先拦截”,比如退换货场景里,先让 check_return_eligibility 工具确认订单资质,如果已经超期,就直接生成拒绝文案,不让模型再去走创建工单的流程。拦截逻辑放在工具里,比放在 Prompt 里可靠得多。对更复杂的跨天会话,还可以加一个“历史摘要压缩”节点,把半小时前的对话先让模型总结成一段摘要,后续请求只带摘要再加最近几轮原文,这样既保住了上下文语义,又把 token 控制在一个稳定区间。

6.3 从 Demo 走到生产的扩展建议

如果只是为了技术验证,这套架构已经够用了;一旦要真正上线,我建议再补几块。第一块是中途转人工的能力。目前的架构里,用户进入订单子图后,如果中途说“算了,你直接转人工”,子图内部还在按 ReAct 循环执行,外部很难打断。我在业务子图里额外挂了一个 handover_to_human 工具,让模型自己识别转人工的信号并主动调用,包装函数发现返回结果里带转人工标记后,直接跳到人工转接逻辑,整个链路就活了。

第二块是离线评测集。多智能体系统上线后最大风险不是崩溃,而是路由漂移,比如大模型底座升级后,意图识别行为发生变化。我建了一个意图路由评测集,里面放几百条真实客服语料,每次变更代码或者换模型就跑一遍回归,准确率掉了就不上线。第三块是子图复用。售后子图不仅可以被客服主图调用,也可以被工单系统、CRM 系统独立调用,因为子图本身是独立编译的,只要用不同的状态和上下文包一层就能复用到其他场景,这也是子图模式相比单一大 Prompt 最大的长期价值。

做这套系统我最大的体会是,LangGraph 的子图模式真正解决的并不是性能问题,而是组织问题。你把订单、售后、咨询、人工转接各自封装成独立子图之后,业务边界、团队成员分工、代码仓库结构全部跟着清晰了起来,真正的难点从写代码变成了画清楚业务边界。另外提醒一句,无论多智能体系统看起来多简洁,一定要保留完整的日志和追踪数据,否则链路一长,排查问题的成本会成倍增加。最后给你一个小技巧:给节点和子图起业务化的中文名,比如“售后办理子图”“人工转接节点”,LangGraph 导出的可视化图里这些名字会直接显示出来,贴到文档里比任何架构说明都直白。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询