简介:检索增强生成(RAG)通过外部知识库提升大模型回答的准确性,但在智能客服场景中,单纯“检索+生成”难以应对多轮上下文、分支路由与兜底转接等复杂流程。LangGraph以图结构显式编排状态节点,让意图识别、参数抽取、知识检索、答案生成与人工介入形成可观测、可干预的工作流。从文档切块、向量检索、重排序、状态管理与条件路由等基础技术切入,结合真实客服问答场景,展示如何将RAG从一次检索升级为可编排的完整链路,并分享降低幻觉、实现引用溯源及兜底转人工的工程实践。适合正在探索RAG落地与复杂对话系统开发的开发者参考。 做智能客服这几年,感触最深的是:单纯把RAG堆上去,回答还是经常“翻车”。问题往往不在模型本身,而在流程——用户问一句,系统答一句,没有意图判断,没有上下文管理,没有兜底策略,任何一环出问题,整体体验就崩了。这也是我最终转向LangGraph做这套RAG智能客服系统的原因:它把RAG从“一次检索+一次生成”升级成了可编排、有状态、可干预的完整工作流。
本项目是一个基于LangGraph框架的智能对话代理系统,核心目标是用RAG技术解决客服问答中回答准确性和上下文相关性的问题。它不只是简单调用大模型接口做问答,而是把“意图识别→参数抽取→知识检索→答案生成→兜底转人工”这一整条链路用图结构显式表达出来,让每个环节都能被观察、被控制、被优化。适合正在做RAG落地、想从单链路由转向复杂工作流编排的开发者参考。
1. 项目概述:从一个“会检索的客服”到一个“会思考的客服”
1.1 这个项目要解决什么问题
传统客服机器人的实现方式很多,早期是关键词匹配,后来是FAQ命中,再后来大模型火了,很多人直接把知识库灌进向量数据库,用户问一句就检索一段拼进Prompt里让模型回答。这套路看似简单,实际跑起来问题一堆。
最常见的情况是:用户问“你们退款要多久”,系统只检索“退款政策”段落,生成的回答里没有结合用户下单时间、支付方式、是否已发货这些上下文,结果答非所问。更麻烦的是多轮对话——用户先问“你们有什么套餐”,再问“这个能不能开发票”,系统如果记不住上一轮说的“套餐”,就不知道“这个”指的是什么,回答自然跑偏。还有一类场景是答案没有依据,模型胡编乱造产品参数,这在客服场景里是致命的。
这个项目要解决的正是这几件事:第一,用RAG把回答锚定在知识库之上,模型只能基于检索到的内容作答;第二,用LangGraph把多轮状态管理起来,让系统记得用户说了什么;第三,把整个问答流程拆成可观测的节点,哪个环节出问题一目了然。
1.2 为什么选择LangGraph而不是直接上LangChain
很多接触过LangChain的朋友会问,Chain不也能做RAG吗,为什么还要多学一个框架?我刚开始也有这个疑问,实际对比过之后才明白两者的定位完全不同。
LangChain的Chain是线性结构,适合“固定顺序”的处理流程:加载文档、切块、向量化、检索、生成,一条道走到底。但客服问答天然不是线性的。用户问题可能不需要检索(比如闲聊、打招呼),也可能需要多轮追问,还可能检索结果置信度太低需要转人工,这些分支逻辑如果用Chain硬写,代码会变成一坨纠缠不清的if-else,而且会话状态只能靠外部变量维护,很难做到系统化。
LangGraph把流程建模成图,节点(Node)负责干活,边(Edge)负责流转,状态(State)在节点之间显式传递。这和客服场景天然契合。你可以把“意图识别”“检索”“生成”这些步骤画成图上的节点,再通过条件边决定下一步走哪条分支。整个流程是可视化的、可控制的,甚至可以在运行过程中停下来人工介入。说白了,LangChain适合写脚本,LangGraph适合写系统。
打个生活化的比方:LangChain像一条传送带,零件从一头进去固定顺序加工;LangGraph像一个带分拣机器人的车间,每个货物进来先识别、再分流,有的返工、有的直接打包,每一步都有记录。
1.3 系统整体架构拆解
这套系统的核心架构可以概括为一句话:LangGraph负责“流程怎么走”,RAG负责“知识从哪来”,两者通过State串联。
用户输入进来之后,先经过意图识别节点,判断是普通咨询、闲聊、售后还是投诉;接着进入参数抽取节点,把订单号、产品名、时间这些关键信息从对话里捞出来;然后进入RAG检索节点,基于用户问题和抽取出的参数去向量库检索相关文档;检索到的片段经过重排序和阈值过滤后,交给生成节点组装Prompt并调用大模型回答;如果检索置信度不足,系统不会硬答,而是进入人工转接节点,把对话交给人工客服。
| 模块 | 职责 | 关键点 |
|---|---|---|
| 意图识别 | 判断用户问题类型 | 决定后续路由,避免每个问题都走RAG |
| 参数抽取 | 提取订单号、产品名等实体 | 提高检索精度,支持多轮上下文理解 |
| RAG检索 | 从向量库召回相关知识片段 | 切块策略和检索策略决定回答质量下限 |
| 重排序与过滤 | 对召回结果二次打分 | 过滤噪声,保留与问题最相关的2-3个片段 |
| 答案生成 | 基于检索结果生成回答 | Prompt约束模型,防止幻觉 |
| 人工转接 | 低置信度时切换人工 | 智能客服的“安全阀” |
这套架构的好处在于每个模块可以独立迭代。比如你觉得检索效果不好,只需调整切块策略和重排序模型,不需要动流程编排;想让生成更稳定,改Prompt就行。LangGraph把模块解耦这件事落到了实处。
2. RAG部分:决定回答质量的下限
2.1 文档切块策略:最容易忽视的“命门”
RAG领域的有一句话很真实:检索质量是RAG的天花板,而切块策略决定检索质量。我见过太多人花大量时间调Prompt、换大模型,最后发现问题出在最开始的文档切块上。
切块有几种常见思路:第一种是固定大小切块,比如每500个字符切一块,简单粗暴,但很容易把一句话、一个表格、一个知识条目拦腰截断,检索召回的是残缺信息;第二种是递归字符切块,通过一系列分隔符(换行、句号、逗号)递归划分,尽量保住语义完整性,这是LangChain和LangGraph里最常用的方法;第三种是结构化切块,先按Markdown标题、HTML标签或PDF文档结构切出章节,再在章节内细分。第三种效果最好,因为知识库文档本来就有层级结构,遵循这个结构切块,检索时才能保持上下文连贯。
我在这套系统里用的是“结构化切块为主、递归切块兜底”的组合策略。具体来说,文档加载后先解析Markdown标题,每个二级标题下的内容作为一个大的语义块,如果这个块超过800字,再按400字、重叠80字的参数递归切分。这样既保住了章节完整性,又控制了向量检索时需要的粒度。
from langchain_text_splitters import RecursiveCharacterTextSplitter # 结构化切块:按标题分割为章节 section_splitter = RecursiveCharacterTextSplitter( separators=["\n## ", "\n### ", "\n#### ", "\n", "。", "!", "?"], chunk_size=800, chunk_overlap=80, keep_separator=True, ) # 对超大章节二次切分 sub_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", "。", ";", ",", ""], chunk_size=400, chunk_overlap=80, )切块参数需要测试而不是拍脑袋。chunk_size太大,检索召回的片段包含太多无关信息,生成时Prompt容易被带偏;太小则上下文不完整,模型看不懂片段在说什么。我实测下来,客服领域知识条目一般200-500字是甜点区,既够模型理解,又不会稀释注意力。
2.2 向量化模型与向量库选型
切好块之后就要向量化。中文场景下必须慎用面向英文优化的Embedding模型,很多模型中文语义理解一言难尽。我在项目中用的Embedding模型可以进行横向对比测试:一边用BGE系列中文模型,一边用开源中文Embedding模型,拿一套自己的业务问题集跑召回率,谁分数高用谁。
选向量库方面,项目初期可以用Chroma或FAISS快速跑通,数据量小、启动快、不占资源。但如果是企业级客服场景,数据量过百万条、需要高并发检索,建议直接上Qdrant或Milvus。这套系统我最初用Chroma做了原型验证,上线前迁移到了Qdrant。迁移成本没有想象中高,LangChain和LangGraph对向量库的封装比较统一,换个客户端配置就能跑起来。
向量检索时一个容易忽略的点是元数据过滤。客服知识库里往往有版本、产品线、生效日期等属性,检索时如果只做向量相似度搜索,不带元数据约束,很可能会召回“旧版本政策”或“其他产品线”的内容。我在检索节点里加了一层metadata过滤,先把候选范围缩到当前生效且对应用户产品的文档,再做向量检索,效果提升非常明显。
2.3 检索后处理:从“找到”到“用对”
RAG的常见误区是认为向量检索完就能直接丢给模型。实际上向量检索召回的Top-K个片段里,经常夹杂着相似但无关的结果。我见过一个典型场景:用户问“怎么注销账号”,检索到的片段里有“账号注册流程”“账号安全保护”“账号注销方法”三个高度相似的段落,向量分数很接近,如果全塞进Prompt,模型很可能从注册流程那段里编出注销方法。
解决这个问题的方案有两层。第一层是重排序(Rerank),用一个专门的交叉编码器模型对召回片段重新打分。向量检索负责“海选”,Rerank负责“决赛”。第二层是相似度阈值过滤,低于阈值的片段直接丢弃,宁缺毋滥。我用一个阈值0.45作为过滤线,同时结合Rerank分数取Top-3。
from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker reranker = CrossEncoderReranker( model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"), top_n=3, ) compression_retriever = ContextualCompressionRetriever( base_compressor=reranker, base_retriever=vector_store.as_retriever(search_kwargs={"k": 8}) )这里要注意,Rerank模型同样要选中文场景适配的,而且Rerank是额外算力开销,一次问答多一次模型推理。项目初期如果性能压力大,可以先做相似度阈值过滤加Top-K截断,等上线后看效果再加Rerank。RAG优化是逐步加码的过程,不要一开始就全上。
3. LangGraph工作流编排:把客服流程变成“状态机”
3.1 节点设计:把一条RAG链路拆成可复用的零件
LangGraph的核心概念是StateGraph,你要做的事情就是定义“有哪些节点”和“节点之间怎么连”。节点就是普通的Python函数,函数签名一般是(state) -> dict,返回的字典会更新全局状态。这套设计的好处是每个节点只关心自己的输入输出,不需要知道整个流程长什么样。
我在这套客服系统里设计了五个核心节点:intent_node负责意图识别和参数抽取;router_node根据意图决定下一步走向哪条分支;retrieval_node负责RAG检索并做重排序;generation_node负责组装Prompt和调用大模型;handoff_node把低置信度对话转给人工客服。每个节点内部逻辑完全独立,想替换意图识别模型,只改intent_node里面几行代码就行。
from typing import TypedDict, Literal, Annotated from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver from langgraph.graph.message import add_messages # 定义全局状态 class AgentState(TypedDict): user_input: str messages: Annotated[list, add_messages] intent: str entities: dict documents: list answer: str confidence: float need_human: boolState的定义是整个LangGraph工作流最重要的一步。它决定了哪些信息可以在节点之间传递,哪些信息需要被持久化。我刚开始做的时候犯过一个错:把所有中间结果都塞进State,导致状态越来越大、调试困难。正确的做法是只保留“后续节点需要的东西”和“需要记录的东西”,临时变量留在节点内部就好。
3.2 条件路由:让系统自己判断“下一步走哪”
LangGraph最有价值的能力是条件边(conditional edge)。普通的边是“走完A必定走B”,条件边是“走完A之后,根据当前状态决定走B、C还是D”。客服问答场景里到处都是这种分叉逻辑:闲聊问题不走检索、退款问题要查订单、低置信度要转人工。
我实现路由的方式是让intent_node在返回里带上intent字段和需要人工介入的标记,然后在router_node里读这个标记,做一个简单的条件判断。
def route_after_intent(state: AgentState) -> Literal["retrieval", "direct_response", "handoff"]: intent = state.get("intent", "") if intent == "chitchat": return "direct_response" if state.get("need_human"): return "handoff" return "retrieval" graph = StateGraph(AgentState) graph.add_node("intent", intent_node) graph.add_node("retrieval", retrieval_node) graph.add_node("generation", generation_node) graph.add_node("handoff", handoff_node) graph.add_edge(START, "intent") graph.add_conditional_edges( "intent", route_after_intent, {"retrieval": "retrieval", "direct_response": "generation", "handoff": "handoff"} ) graph.add_edge("retrieval", "generation") graph.add_edge("generation", END) graph.add_edge("handoff", END)这里有个设计细节值得展开:检索之后如果发现所有片段分数都很低,生成节点也可以选择不硬答。我在generation_node里加了一个置信度判断,如果低于阈值,state会更新need_human=True,然后在生成节点后加一条条件边转去handoff。相当于双重保险——意图阶段判断不了,生成阶段还能兜底。
3.3 状态管理与Checkpointer:多轮会话怎么记住人话
智能客服区别于一次性问答的关键点在于多轮对话记忆。用户说“我在上海,能上门吗”,下一句“那要加钱吗”,系统得知道“那”指的是“上门服务”。LangGraph处理这个问题的方式是Checkpointer机制:每个会话线程的State快照会被持久化保存,下次同一线程继续运行时,可以从历史State恢复上下文。
项目中我用MemorySaver做内存级Checkpointer快速验证,生产环境则换成Redis或数据库实现的持久化存储。编译图的时候传入checkpointer参数,调用时多传一个config包含thread_id,LangGraph就会自动维护会话状态。
from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() compiled_graph = graph.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "user_10023"}} result = compiled_graph.invoke( {"user_input": "你们退款要多久"}, config=config ) # 下一轮继续用同一个thread_id,系统能记住上一轮内容 result = compiled_graph.invoke( {"user_input": "那需要我提供什么材料呢?"}, config=config )用上Checkpointer之后,还有一个额外的好处:LangGraph里的“记忆”不只是传给大模型的历史消息,而是整个流程执行过程中的状态,包括意图、实体、检索过的文档等。这意味着同一用户在多轮对话里问“刚才说的套餐再讲一下”,系统可以直接从State里取出上一轮提取过的套餐实体,不需要重新检索。
3.4 人工介入:Human-in-the-loop机制
LangGraph还有一个杀手锏——interrupt机制。它能让图在某个节点暂停,等待外部人工确认或补充信息后再继续执行。这个能力在客服场景里非常有价值,比如系统在转人工前,可以先暂停,让人工客服看到机器人生成的未经确认的回答草稿,点击确认后再发给用户,避免AI的“自信胡诌”直接触达用户。
我实现了一个半自动审核模式:当置信度处于中等区间时,图运行到confirm_node会触发interrupt,前端收到暂停信号后展示问答草稿,人工客服在界面上点击“通过”或“修改”,操作结果作为Resume输入继续执行图。
from langgraph.types import interrupt, Command def confirm_node(state: AgentState): # 暂停图执行,等待人工反馈 feedback = interrupt({ "question": state["user_input"], "draft_answer": state["answer"], "sources": state["documents"], }) if feedback.get("approved"): return {"need_human": False} return { "answer": feedback.get("manual_reply", state["answer"]), "need_human": False, }这个机制上线后,客服主管给了一个很直观的评价:机器人不再是“无人看管的自动回复机”,而是“有审核环节的智能助手”。对有合规要求的行业来说,这个能力几乎等于刚需。
4. 智能客服场景的实战实现
4.1 多轮上下文理解的具体做法
多轮对话在LangGraph里处理起来,核心思路是把历史对话作为状态的一部分传递给模型。我的做法是把State里的messages字段设置为add_messages注解类型,LangGraph会自动把新增的消息追加到历史列表里。
具体到Prompt组装阶段,生成节点会把最近5轮对话记录、当前用户问题、检索到的文档片段一起拼成Prompt。这里有个细节:不是所有检索结果都值得喂给模型。如果用户是在追问上一轮的某个点,检索时应该优先用“当前问题+上一轮意图”去检索,而不是单纯用当前问题。我在retrieval_node里做了一件事:把上一轮的实体抽取结果拼到检索query后面,例如“退款怎么操作”加上“订单号12345”,检索命中率比单纯用用户问题高很多。
4.2 答案引用溯源:让AI说话有依据
智能客服在企业环境落地,最难让业务方放心的就是“答错了谁负责”。这需要在系统设计上给答案提供引用溯源。我在RAG检索到片段时,会保留每个片段的来源文档路径、标题和页码,生成Prompt时要求模型在回答末尾列出引用片段编号。
Prompt里是这样的约束:
请仅根据以下参考资料回答用户问题。引用资料时使用[1][2]标注。 如果参考资料无法回答问题,请直接回答“当前知识库中未找到相关信息”。 不要编造任何参考资料之外的信息。生成节点返回的answer里会带上引用标记,前端解析后展示成可点击的卡片。用户点开就能看到“这份回复依据的是《售后政策》第3.2.1节”。这个功能在客服质检环节价值极大,质检员能快速定位回答依据,不需要再逐条翻对话记录。
4.3 兜底策略与人工转接
任何RAG系统都不可能覆盖所有问题,兜底策略决定了用户体验的下限。在这套系统里我设置了三个兜底层级。
第一层是意图识别阶段的闲聊意图,这种直接走一个轻量回复节点,不消耗RAG检索。第二层是检索置信度不足,经过阈值过滤后检索片段为空,系统回复“当前知识库暂未覆盖您的问题,我已为您转接人工客服”,同时把对话session转交给值班客服。第三层是生成之后业务规则的校验,比如用户问“什么时候发货”,如果检索到的片段只提供了一般规则而没有订单数据,系统会要求用户提供订单号并触发订单查询工具的调用。
def generation_node(state: AgentState): if not state.get("documents"): return { "answer": "当前知识库暂未覆盖您的问题,我已为您转接人工客服。", "need_human": True, "confidence": 0.0, } # 正常生成逻辑...这个“三层兜底”的设计让系统没有一个问题会硬答。用户收到的永远是“有依据的回答”或者“明确承认不知道并转人工”,这比让模型强行生成一个错误答案要可信得多。
5. 部署运行与效果评测
5.1 环境搭建与项目结构
LangGraph生态更新比较快,建议直接用最新稳定版,同时注意langchain、langgraph、langchain-community这几个包的版本保持一致,否则容易出现API不兼容的问题。我在项目初始化时用了一个requirements.txt来锁定版本,避免“昨天还能跑,今天报错”的情况。
# 核心依赖 langgraph>=0.2.0 langchain>=0.2.0 langchain-community>=0.2.0 langchain-openai>=0.1.0 langchain-text-splitters>=0.2.0 chromadb>=0.4.0 qdrant-client>=1.9.0 sentence-transformers>=2.5.0项目代码结构上建议按职责分层,不要把图定义、节点函数、工具调用全塞在一个文件里。我的目录划分是:graph/放LangGraph的状态和边定义,nodes/放各个节点函数,retrieval/放向量库和检索逻辑,prompts/放所有Prompt模板,services/放外部服务调用(大模型、订单系统)。这样当节点数量增长到十几个的时候,代码依然能维护。
第一次跑通项目,建议先开一个最小可运行的例子:只保留intent和generation两个节点,用MemorySaver作为Checkpointer,向量库用Chroma。先确保基础链路通,再逐步加重排序、外部工具、人工介入这些复杂功能。
5.2 基于Graph的调试与可视化
LangGraph调试比普通LangChain链好的地方在于,它天然带有“运行轨迹”视角——每一步走到哪个节点、状态变成了什么,都可以事无巨细地拿出来看。在调试阶段,我习惯打开LangGraph自带的追踪日志,给每个节点加一个print输出,记录节点开始和结束时的关键状态字段,这样能快速定位“是哪一步丢的信息、是哪一步给的错误答案”。
LangGraph Studio是官方提供的可视化调试工具,可以像看流程图一样观察图的运行过程,在任意节点暂停并查看状态。遇到状态字段传错这类问题,可视化调试比看日志高效得多。
5.3 评测方法:没有评测就没有优化
RAG系统上线最重要的就是建立评测集。我的做法是从真实客服对话里抽取300个问题,人工标注标准答案和知识库出处,形成一个评测集,每次改切块策略、换向量模型、调Prompt,都用评测集跑一遍,看整体准确率和引用命中率的变化。这个评测脚本要定期跑,防止“优化了A,却破坏了B”的回归问题。
评测时常用指标有:答案准确率(人工评估)、检索命中率(Top-K是否包含相关文档)、引用正确率(引用片段是否能支撑回答)、兜底正确率(该转人工时是否转人工)。这套指标不是一次性的,项目迭代过程中要一直跟进。
6. 常见问题与排查技巧实录
6.1 问题排查速查表
以下是我在这套系统从开发到上线过程中,实测遇到并解决过的典型问题,整理成了一张速查表,方便对照排查。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 回答内容与知识库不符 | 检索片段相关性差 | 检查切块粒度,替换Rerank模型,增加元数据过滤 |
| 多轮对话中“这/那”指代错误 | 历史消息未正确组装 | 确认messages字段使用add_messages注解,检查thread_id是否一致 |
| 不同用户之间会话串线 | Checkpointer的thread_id重复 | 确保每个用户生成唯一thread_id,不用固定ID |
| 检索不到任何内容 | 切块过大或Embedding模型不匹配 | 调整切块参数,换中文适配Embedding模型 |
| 模型回答超长、答非所问 | Prompt约束不足 | 在Prompt中明确回答长度和引用要求,必要时用输出解析器 |
| 转人工流程卡住 | interrupt机制使用不当 | 确认Resume时传入的Command格式正确 |
| 并发场景回答耗时太长 | Rerank模型推理开销大 | 开启模型缓存,或仅在高置信度场景保留一个节点做Rerank |
6.2 三个最关键的避坑经验
第一个坑是检索query的构造。刚开始我直接把用户原始问题丢给向量检索,效果很差。后来把意图、实体和历史意图拼接成检索query之后,效果有了质的提升。比如用户问“怎么退”,如果携带实体“订单12345”和上一轮意图“发货问题”,检索到的“退货需先确认未发货”比单独搜“怎么退”要精准得多。
第二个坑是切块后没有保留元数据。前期做原型时我只切块向量化,没有存来源和章节信息。后面业务方要求引用溯源,我不得不返工重新生成向量库。建议从一开始就把来源、章节、更新时间写入chunk的metadata,不要等到要引用的时候再补。
第三个坑是置信度阈值需要动态调整。固定阈值在真实业务里并不好用,不同类目问题的向量相似度天然有差异,比如FAQ类问题普遍分数高,长尾问题普遍分数低。我后来改成按意图类别设置不同阈值,同时结合Rerank分数综合判断,转人工的准确率才达到可接受水平。这里的核心思路是:兜底策略本身也要依赖策略,不能一刀切。
6.3 上线后的持续优化方向
这套系统跑通之后,后续的优化空间还很大。LangGraph的社区里讨论最多的方向是Agentic RAG,也就是让系统不再只是“检索一次就回答”,而是能根据问题自主决定是否需要多步检索、是否需要调用外部工具,甚至能自己拆解复杂问题再汇总回答。比如用户问“对比一下两款套餐的退款政策”,系统可以先分别检索A套餐和B套餐的信息,再汇总成对比结果,这种能力在客服场景里非常实用。
长期记忆也是一个值得扩展的方向。LangGraph现在有记忆持久化机制,可以让系统跨会话记住用户的偏好、历史订单和沟通记录。试想一下,一位老客户回来咨询时,系统能直接说“您好,根据您之前的订单记录,这次需要办理什么售后?”,这种体验是纯粹靠Prompt无法做到的。
个人在实际操作中最深的一个体会是:不要把LangGraph当做一个“新玩具”,一上来就堆砌复杂节点和边。先把手头最痛的问题用最小图解决,跑通、验证、上评测集,再逐步增加节点和分支。图编排是一把双刃剑,设计得好是灵活可扩展,设计得不好就是多了一层抽象复杂度。保持每个节点职责单一、状态字段精简、路由逻辑可解释,这套系统的价值才会真正体现出来。
本文还有配套的精品资源,点击获取