ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LangGraph构建RAG智能客服:从检索到工作流编排实战

LangGraph构建RAG智能客服:从检索到工作流编排实战 简介检索增强生成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_size800, chunk_overlap80, keep_separatorTrue, ) # 对超大章节二次切分 sub_splitter RecursiveCharacterTextSplitter( separators[\n\n, \n, 。, , , ], chunk_size400, chunk_overlap80, )切块参数需要测试而不是拍脑袋。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( modelHuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-base), top_n3, ) compression_retriever ContextualCompressionRetriever( base_compressorreranker, base_retrievervector_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_humanTrue然后在生成节点后加一条条件边转去handoff。相当于双重保险——意图阶段判断不了生成阶段还能兜底。3.3 状态管理与Checkpointer多轮会话怎么记住人话智能客服区别于一次性问答的关键点在于多轮对话记忆。用户说“我在上海能上门吗”下一句“那要加钱吗”系统得知道“那”指的是“上门服务”。LangGraph处理这个问题的方式是Checkpointer机制每个会话线程的State快照会被持久化保存下次同一线程继续运行时可以从历史State恢复上下文。项目中我用MemorySaver做内存级Checkpointer快速验证生产环境则换成Redis或数据库实现的持久化存储。编译图的时候传入checkpointer参数调用时多传一个config包含thread_idLangGraph就会自动维护会话状态。from langgraph.checkpoint.memory import MemorySaver checkpointer MemorySaver() compiled_graph graph.compile(checkpointercheckpointer) config {configurable: {thread_id: user_10023}} result compiled_graph.invoke( {user_input: 你们退款要多久}, configconfig ) # 下一轮继续用同一个thread_id系统能记住上一轮内容 result compiled_graph.invoke( {user_input: 那需要我提供什么材料呢}, configconfig )用上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来锁定版本避免“昨天还能跑今天报错”的情况。# 核心依赖 langgraph0.2.0 langchain0.2.0 langchain-community0.2.0 langchain-openai0.1.0 langchain-text-splitters0.2.0 chromadb0.4.0 qdrant-client1.9.0 sentence-transformers2.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模型推理开销大开启模型缓存或仅在高置信度场景保留一个节点做Rerank6.2 三个最关键的避坑经验第一个坑是检索query的构造。刚开始我直接把用户原始问题丢给向量检索效果很差。后来把意图、实体和历史意图拼接成检索query之后效果有了质的提升。比如用户问“怎么退”如果携带实体“订单12345”和上一轮意图“发货问题”检索到的“退货需先确认未发货”比单独搜“怎么退”要精准得多。第二个坑是切块后没有保留元数据。前期做原型时我只切块向量化没有存来源和章节信息。后面业务方要求引用溯源我不得不返工重新生成向量库。建议从一开始就把来源、章节、更新时间写入chunk的metadata不要等到要引用的时候再补。第三个坑是置信度阈值需要动态调整。固定阈值在真实业务里并不好用不同类目问题的向量相似度天然有差异比如FAQ类问题普遍分数高长尾问题普遍分数低。我后来改成按意图类别设置不同阈值同时结合Rerank分数综合判断转人工的准确率才达到可接受水平。这里的核心思路是兜底策略本身也要依赖策略不能一刀切。6.3 上线后的持续优化方向这套系统跑通之后后续的优化空间还很大。LangGraph的社区里讨论最多的方向是Agentic RAG也就是让系统不再只是“检索一次就回答”而是能根据问题自主决定是否需要多步检索、是否需要调用外部工具甚至能自己拆解复杂问题再汇总回答。比如用户问“对比一下两款套餐的退款政策”系统可以先分别检索A套餐和B套餐的信息再汇总成对比结果这种能力在客服场景里非常实用。长期记忆也是一个值得扩展的方向。LangGraph现在有记忆持久化机制可以让系统跨会话记住用户的偏好、历史订单和沟通记录。试想一下一位老客户回来咨询时系统能直接说“您好根据您之前的订单记录这次需要办理什么售后”这种体验是纯粹靠Prompt无法做到的。个人在实际操作中最深的一个体会是不要把LangGraph当做一个“新玩具”一上来就堆砌复杂节点和边。先把手头最痛的问题用最小图解决跑通、验证、上评测集再逐步增加节点和分支。图编排是一把双刃剑设计得好是灵活可扩展设计得不好就是多了一层抽象复杂度。保持每个节点职责单一、状态字段精简、路由逻辑可解释这套系统的价值才会真正体现出来。本文还有配套的精品资源点击获取
返回列表