1. 为什么多智能体架构正在重写智能客服的技术选型
1.1 从单模型问答到多智能体协作的演进逻辑
做过客服系统的人都有一个共同体会:单靠一个大模型接口加一段提示词,能做出演示效果,但一上生产就露馅。用户问“我上个月买的那台机器坏了,发票也找不到了,能不能换一台新的”,这里面同时包含订单查询、售后政策判断、发票补开、换货流程四个子任务,单模型要么一次性胡编一个答案,要么在长上下文里丢失关键信息。这不是模型能力不够,而是架构不对。
多智能体协作的核心思路,是把一个复杂的客服会话拆解成若干个职责单一的智能体,每个智能体只负责自己擅长的领域,由一个调度层负责分派任务、汇总结果、决定下一步走向。这套思路在工程上对应的就是LangGraph这类图编排框架——它把智能体之间的流转关系显式地建模成一张有向图,节点是智能体或工具,边是条件跳转逻辑。相比链式调用,图结构最大的好处是支持循环、分支和状态回传,这正是客服场景里“追问—澄清—执行—确认”这类多轮交互所需要的。
我自己的判断是,2024年之后新立项的智能客服项目,如果还在用“一个提示词打天下”的方案,基本等于给自己埋雷。原因很简单:企业客服的知识库是异构的(FAQ、工单、产品文档、订单系统),业务动作是有副作用的(退款、改地址、发验证码),这两点决定了你必须把“理解”“决策”“执行”三层分开,而多智能体加图编排是目前开源生态里最成熟的落地路径。
1.2 这套系统到底解决了哪些真实痛点
先把话说直白:这套开源方案不是给个人开发者做着玩的玩具,它的目标场景是中大型企业的客服中台。具体解决的痛点有这么几个。
第一是意图路由的准确率问题。传统方案用分类模型做意图识别,训练数据一变化就得重新标注重新训练。多智能体方案里,路由本身由一个轻量智能体承担,它读的是自然语言描述的路由规则,新增业务线只需要加一条规则和对应的子智能体,不用动模型。
第二是多轮对话的状态管理。LangGraph 内置了状态机语义,每一轮对话的上下文、已收集的槽位、当前所处的流程节点都保存在图的状态对象里,不会因为模型上下文窗口限制而丢失。这一点在退换货、投诉升级这类长流程场景里是刚需。
第三是人工坐席的平滑接管。热词里“智能客服转人工”出现频率很高,说明这是用户最在意的体验点。多智能体架构里,转人工不是一个异常分支,而是一个正常的图节点——当置信度低于阈值、用户明确要求、或触发了敏感操作时,调度智能体把当前完整状态打包推给人工坐席,坐席看到的是结构化的会话摘要而不是一堆原始聊天记录。
第四是可观测与可迭代。每个智能体的输入输出、每次工具调用的参数和返回、每个路由决策的依据,全部落在图执行的轨迹里。出了问题能定位到具体是哪个节点判断错了,而不是对着一个黑盒模型干瞪眼。
1.3 适合什么样的团队上手
这套东西不是零基础能直接吃透的。我的建议是,团队里至少要有一个人熟悉 Python 异步编程、了解 LangChain 的基本抽象(Tool、Retriever、Memory),并且对状态机或工作流引擎有概念。如果团队之前做过 RAG 知识库问答,那上手会快很多,因为检索增强那部分逻辑是复用的。
反过来,如果团队完全没有后端工程能力,只想拖拽出一个客服机器人,那这套开源方案反而会带来负担——你需要自己部署向量库、自己接业务系统 API、自己写前端对话界面。它给你的是骨架和大脑,血肉还得自己填。
2. 核心架构拆解:多智能体到底怎么分工
2.1 调度智能体与领域智能体的职责边界
一套能跑通的多智能体客服系统,通常包含这么几类角色。我用一张表把职责和典型实现方式列清楚,方便你对照自己的业务做裁剪。
| 智能体角色 | 核心职责 | 典型实现 | 是否必须 |
|---|---|---|---|
| 调度智能体 | 意图识别、任务分派、结果汇总、决定是否转人工 | LLM + 结构化输出 | 必须 |
| 知识问答智能体 | 基于企业知识库回答产品、政策类问题 | RAG 检索 + LLM 生成 | 必须 |
| 业务操作智能体 | 调用订单、退款、工单等业务 API | LLM + 工具调用 | 按业务定 |
| 澄清智能体 | 当信息不足时主动追问补全槽位 | LLM + 槽位校验 | 推荐 |
| 情绪安抚智能体 | 识别负面情绪并调整话术 | 分类模型 + 话术模板 | 可选 |
| 人工接管节点 | 打包状态、生成摘要、转坐席 | 图节点 + 消息队列 | 必须 |
调度智能体是整个系统的大脑,但它不应该什么都干。我见过不少项目把调度写成“万能提示词”,结果就是调度层越来越臃肿,最后退化成单模型方案。正确的做法是让调度只做三件事:判断用户这句话属于哪个领域、判断当前信息是否足够、判断下一步该走哪个节点。具体的知识检索和业务执行,全部下沉到领域智能体。
领域智能体之间不直接通信,所有流转都经过调度层。这个约束看起来增加了开销,但换来的是可观测性和可测试性——你可以单独对某个领域智能体做单元测试,也可以在图层面做端到端的轨迹回放。
2.2 LangGraph 图结构的设计要点
LangGraph 的核心抽象是 StateGraph,你需要先定义状态对象(State),再往图里加节点(Node)和边(Edge)。状态对象决定了智能体之间能传递什么信息,这是整个设计里最关键的一步。
一个客服场景的状态对象通常长这样:
from typing import TypedDict, Annotated, List from langgraph.graph.message import add_messages class CustomerServiceState(TypedDict): messages: Annotated[List, add_messages] # 对话历史 user_id: str # 用户标识 intent: str # 当前识别到的意图 slots: dict # 已收集的槽位信息 retrieved_docs: List[str] # 检索到的知识片段 tool_results: dict # 业务工具调用结果 need_human: bool # 是否需要转人工 confidence: float # 当前决策置信度这里有个细节值得展开:messages字段用了add_messages这个 reducer,意思是每次节点返回新的消息时,框架会自动追加而不是覆盖。这个设计避免了手动管理对话历史的麻烦,但也要注意,如果某个节点需要“重写”历史(比如人工坐席介入后要重置上下文),就得显式处理。
边的设计上,条件边(conditional edge)是灵魂。调度节点执行完后,不是固定跳转到下一个节点,而是根据状态里的intent和confidence动态决定。LangGraph 里用add_conditional_edges来实现,路由函数返回一个字符串,框架据此选择目标节点。
def route_after_dispatch(state: CustomerServiceState) -> str: if state["need_human"] or state["confidence"] < 0.6: return "human_handoff" if state["intent"] == "knowledge_query": return "knowledge_agent" if state["intent"] == "business_action": return "business_agent" return "clarify_agent" graph.add_conditional_edges("dispatch", route_after_dispatch)注意:路由函数必须是纯函数,不能在里面做网络请求或数据库查询。所有需要的信息都应该在进入路由前已经写进状态对象。我踩过的坑就是在路由函数里调了一次向量检索,结果每次路由都多花几百毫秒,而且因为路由可能被多次调用,检索结果还不一致。
2.3 状态管理与上下文传递的工程细节
多智能体系统里,状态膨胀是个隐蔽的杀手。对话轮次一多,messages列表越来越长,每次调用 LLM 都要把全部历史塞进提示词,token 成本飙升不说,模型注意力还会被稀释。
我的处理办法是分层管理上下文。短期上下文(最近3到5轮)完整保留,中期上下文(本次会话的槽位和意图轨迹)用结构化字段保存,长期上下文(用户历史偏好、历史工单)走检索按需注入。具体实现上,可以在调度节点里加一个“上下文压缩”步骤,把超过阈值的早期对话用 LLM 总结成一段摘要,替换掉原始消息。
另一个细节是槽位(slots)的校验时机。不要等到调用业务 API 的时候才发现缺参数,应该在澄清智能体里就做完整性校验。校验规则可以用 JSON Schema 描述,这样新增业务动作时只需要加一份 Schema,不用改代码逻辑。
3. 从零搭建:可复现的实操流程
3.1 环境准备与依赖选型
先把技术栈定下来。以下是我实测下来比较稳的一套组合,版本号建议锁死,避免依赖漂移。
python==3.11 langgraph==0.2.x langchain==0.3.x langchain-openai==0.2.x fastapi==0.115.x uvicorn==0.32.x pydantic==2.9.x redis==5.0.x # 会话状态持久化 pgvector==0.3.x # 向量检索(也可换 Milvus/Chroma)模型侧,调度和澄清这类需要强推理的节点用大参数模型,知识问答和话术生成可以用小参数模型或经过微调的领域模型。热词里“大模型微调”和“大模型选择”出现很多次,说明大家都在纠结这个问题。我的经验是:客服场景里,调度和工具调用对模型的指令遵循能力要求最高,这部分不要省;话术润色和摘要生成可以用便宜模型,效果差距不明显。
向量库的选择上,如果数据量在百万级以内,pgvector 足够用,而且能和业务库放在同一个 PostgreSQL 实例里,运维成本最低。超过这个量级再考虑专用向量库。
3.2 知识库构建与检索增强的落地方法
知识问答智能体的效果,八成取决于知识库的质量,而不是模型。我见过太多团队把 PDF 直接丢进向量库就完事,结果检索出来的片段驴唇不对马嘴。
正确的做法是分三步走。第一步是文档预处理,把 PDF、Word、网页统一转成 Markdown,保留标题层级,因为标题信息在切分时能作为上下文注入。第二步是语义切分,不要按固定字数切,而是按语义边界切——一个完整的政策条款、一个完整的操作步骤,应该是一个 chunk。LangChain 里的RecursiveCharacterTextSplitter配合自定义分隔符可以做到,但更推荐用基于标题层级的切分策略。第三步是元数据标注,每个 chunk 打上产品线、文档类型、生效日期等标签,检索时可以按标签过滤,避免把过期政策检索出来。
检索策略上,纯向量检索在客服场景里不够用,因为用户经常用产品型号、订单号这类精确词。我的方案是混合检索:向量检索召回 Top 20,BM25 关键词检索召回 Top 20,用 RRF(倒数排名融合)合并后取 Top 5 送给 LLM。实测下来,混合检索相比纯向量检索,在包含型号和编号的查询上准确率提升明显。
def hybrid_retrieve(query: str, top_k: int = 5): vector_hits = vector_store.similarity_search(query, k=20) keyword_hits = bm25_retriever.get_relevant_documents(query)[:20] # RRF 融合 scores = {} for rank, doc in enumerate(vector_hits): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) for rank, doc in enumerate(keyword_hits): scores[doc.id] = scores.get(doc.id, 0) + 1 / (60 + rank) ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [doc_map[doc_id] for doc_id, _ in ranked[:top_k]]提示:RRF 里的常数 60 是原论文的经验值,不用改。融合前要确保两个检索器返回的文档 ID 体系一致,否则融合逻辑会失效。
3.3 工具调用与业务系统对接的注意事项
业务操作智能体要调用真实系统,这里的安全边界必须划清楚。我的原则是:读操作可以自动执行,写操作必须二次确认。查订单、查物流这类只读接口,智能体可以直接调;退款、改地址、取消订单这类有副作用的操作,必须生成一个确认卡片让用户点击确认,确认后再执行。
工具定义上,用 Pydantic 模型描述参数,让 LLM 通过结构化输出生成调用参数。LangGraph 里可以用ToolNode配合bind_tools实现,但要注意工具描述的质量直接决定调用准确率。工具描述里要写清楚:这个工具做什么、什么情况下用、参数的含义和格式、返回值的结构。我见过调用失败最多的原因就是工具描述写得太简略,模型不知道什么时候该用。
from pydantic import BaseModel, Field class RefundInput(BaseModel): order_id: str = Field(description="订单编号,格式为 ORD 开头加12位数字") reason: str = Field(description="退款原因,必须是以下之一:质量问题、七天无理由、发错货、其他") amount: float = Field(description="退款金额,单位元,不能超过订单实付金额") @tool("create_refund", args_schema=RefundInput) def create_refund(order_id: str, reason: str, amount: float) -> dict: """创建退款工单。仅在用户明确要求退款且订单状态为已支付时调用。""" # 实际业务逻辑 ...工具调用的超时和重试也要处理。业务系统响应慢是常态,设置 5 秒超时,超时后返回“系统繁忙请稍后重试”而不是让整个图卡死。重试只对幂等的读操作做,写操作不自动重试,避免重复退款。
3.4 转人工流程的完整实现
转人工不是简单地把对话丢给坐席就完事。完整的流程包含四个环节:触发判断、状态打包、坐席分配、上下文同步。
触发判断上,我设置了三个条件:用户连续两次表达不满、调度置信度低于阈值、用户明确说“转人工”。这三个条件任一满足就触发。注意不要只靠关键词匹配“转人工”,用户可能说“我要找个人”“机器人听不懂”之类,这些要靠意图识别覆盖。
状态打包是把当前图状态序列化成一份结构化摘要,包括:用户基本信息、本次会话的意图轨迹、已收集的槽位、已执行的业务操作、检索到的关键知识片段、以及一段 LLM 生成的会话摘要。坐席看到这份摘要,能在几秒内了解上下文,不用翻聊天记录。
坐席分配可以用简单的轮询或按技能组路由。上下文同步通过 WebSocket 或消息队列推送给坐席工作台。这里有个细节:转人工后,图并没有结束,而是进入一个等待状态,坐席的回复通过一个特殊节点注入回图里,这样如果坐席解决不了又转回智能体,状态是连续的。
4. 上线后最容易踩的坑与排查手册
4.1 意图误判与路由死循环
路由死循环是多智能体系统里最典型的问题。表现是:调度把任务分给澄清智能体,澄清智能体追问后用户回答,调度又分给澄清智能体,来回循环。根因通常是澄清智能体没有正确更新槽位,或者调度判断“信息是否足够”的逻辑有漏洞。
排查方法是在图执行时打印每个节点的状态快照,对比进入澄清前后的slots字段有没有变化。如果没变化,说明澄清智能体没有正确解析用户回答。解决思路是给澄清智能体加一个槽位提取的校验步骤,提取失败时换一种问法,连续两次失败直接转人工。
另一个常见问题是意图边界模糊。比如“我要退货”和“退货政策是什么”,前者是业务操作,后者是知识问答。调度智能体如果分错,用户体验会很差。我的做法是在调度提示词里给每个意图配 3 到 5 个正例和反例,并且要求模型输出置信度,低于阈值时走澄清而不是硬猜。
4.2 检索召回不准的调优路径
检索不准的表现是:知识库明明有答案,但智能体回答“我不知道”或者答非所问。排查要按顺序来。
先看切分是否合理。把检索到的 chunk 打印出来,如果发现一个完整的政策被切成了两半,那就是切分策略的问题。再看 embedding 模型是否匹配语种和领域,中文客服场景用中文优化的 embedding 模型效果明显好于通用模型。然后看检索策略,纯向量检索对精确词不敏感,加上 BM25 混合检索。最后看提示词,检索到的内容有没有被正确注入到生成提示词里,有没有被其他指令挤掉。
我整理了一份速查表,遇到问题按这个顺序排查,基本能覆盖九成情况。
| 现象 | 可能原因 | 排查动作 | 解决方向 |
|---|---|---|---|
| 答非所问 | 检索片段不相关 | 打印 Top 5 chunk | 改切分策略或加混合检索 |
| 回答“不知道” | 召回为空或阈值过高 | 看检索得分分布 | 降低阈值或扩充知识库 |
| 答案过时 | 检索到旧版本文档 | 检查元数据过滤 | 加生效日期过滤 |
| 答案不完整 | chunk 太小 | 看 chunk 长度 | 增大 chunk 或加父子检索 |
| 型号查不到 | 向量检索对精确词弱 | 测试关键词检索 | 启用混合检索 |
4.3 工具调用失败的分类处理
工具调用失败分三类:参数错误、业务拒绝、系统异常。参数错误是模型生成的参数不符合 Schema,比如订单号格式不对、金额为负。这类要在工具层做校验,返回明确的错误信息让模型重新生成,而不是直接抛异常。
业务拒绝是业务系统返回的合法拒绝,比如“订单已发货不能退款”。这类要把业务系统的错误码和提示原样返回给模型,让模型据此生成解释话术。千万不要把业务错误吞掉,否则模型会编一个“退款成功”的假消息。
系统异常是超时、连接失败这类。这类要区分读操作和写操作,读操作可以重试一次,写操作直接返回“系统繁忙”,并记录日志人工介入。
实操心得:给每个工具加一个调用日志,记录入参、出参、耗时、是否成功。上线初期每天看一遍失败日志,能发现大量提示词和 Schema 的问题。这个习惯帮我省了至少两周的调试时间。
4.4 成本与延迟的平衡技巧
多智能体系统的成本主要来自 LLM 调用次数。一次用户提问可能触发调度、检索、生成、校验四次调用,token 消耗是单模型方案的三到四倍。延迟也相应增加,用户等待时间可能到 5 秒以上。
优化手段有几个。一是模型分级,调度和校验用大模型,生成和摘要用小模型。二是缓存,相同或相似问题的检索结果和回答缓存起来,客服场景里重复问题比例很高,缓存命中率能到三成以上。三是并行化,检索和槽位提取可以并行执行,LangGraph 支持节点并行。四是流式输出,生成节点用流式返回,用户看到首字的时间能缩短一半以上。
延迟这块,我的底线是首字响应不超过 2 秒,完整回答不超过 8 秒。超过这个阈值,用户就会觉得“卡”。如果业务复杂导致延迟压不下来,就在等待时给一个“正在为您查询”的中间态提示,体验会好很多。
5. 二次开发与能力扩展的方向
5.1 接入企业自有模型的改造点
很多企业出于数据合规考虑,要用自己部署的模型。改造点主要在 LLM 客户端的封装层。LangChain 的ChatOpenAI类支持自定义base_url,只要你的模型服务兼容 OpenAI 接口协议,改一个地址就能接上。不兼容的话,需要自己实现一个BaseChatModel子类,实现_generate和_stream两个方法。
微调模型的接入要注意提示词格式。微调时用的提示词模板要和推理时一致,否则效果会打折扣。热词里“大模型微调实战”和“大模型提示词工程”是关联的,微调不是替代提示词工程,而是补充。我的建议是先把提示词工程做到位,确认瓶颈确实在模型能力上,再考虑微调。
5.2 多模态能力的扩展思路
客服场景里图片是刚需,用户经常发截图问“这个报错什么意思”“这个按钮在哪”。扩展多模态能力,需要在状态对象里加一个images字段,在调度节点判断是否包含图片,包含的话路由到多模态理解节点,用视觉模型生成图片描述,再把描述注入后续流程。
这块的工程复杂度主要在图片的存储和传输。建议图片走对象存储,状态里只存 URL,需要时再下载。视觉模型的调用成本比文本高不少,要做好频率限制和缓存。
5.3 从客服到全场景智能助手的演进
这套架构的复用性其实很强。把领域智能体换一换,调度规则改一改,就能变成内部 IT 助手、HR 助手、销售助手。核心的图编排、状态管理、工具调用、转人工机制都是通用的。
我在实际项目里做过一次迁移,把客服系统改造成内部工单助手,只花了三天——换知识库、换工具集、调调度提示词,图结构基本没动。这也是我推荐这套架构的原因:它给你的不是一个固定功能的成品,而是一套可生长的骨架。业务变了,加节点加边就行,不用推倒重来。
最后分享一个我在调试期常用的小技巧:给图加一个“回放”模式,把历史会话的状态轨迹存下来,可以逐节点回放,看每一步的输入输出。这个功能在排查偶发问题时特别有用,因为很多问题只在特定对话路径下出现,靠日志很难还原。实现上就是在每个节点包一层装饰器,把状态快照写进 Redis 或本地文件,回放时按顺序读出来。代码量不大,但省下的调试时间是以天计的。