AI项目从入门到上线07-从单轮问答到多轮对话:RAG客服机器人进阶之路 你花了一周搭了个RAG客服兴冲冲拿给运营试用。对方问了仨问题“帮我查一下物流”、“什么时候发货”、“能不能改地址”——第三问系统懵了“请问您说的是什么地址”完了记性不好全白干。这不是大模型的问题是你的RAG对话历史管理没做好。单轮问答像金鱼——转头就忘多轮对话才像正常人——能记住上下文。今天这篇用一杯咖啡的时间把你从金鱼式RAG拽到真正能聊的客服机器人。目录1. RAG问答链路全景图2. Query重写把聊天记录压缩成一句话2.1 问题在哪2.2 两种重写策略2.3 完整代码实现3. 对话历史管理三个方案一网打尽3.1 为什么不能无脑存全部3.2 方案一ConversationBufferWindowMemory滑动窗口3.3 方案二ConversationSummaryBufferMemory摘要压缩3.4 方案三混合方案推荐生产用这个4. Prompt模板设计系统指令的艺术4.1 三段式Prompt结构4.2 完整Prompt模板4.3 组装完整调用链5. 后处理策略给AI的答案整容5.1 三大后处理策略5.2 排序重排Re-rank6. 测试验证别拍脑袋用数据说话6.1 构建FAQ测试集6.2 自动化评估7. 完整可运行代码关键参数速查表8. 总结与文末三件套1. RAG问答链路全景图在动手写代码之前先把整个流程在脑子里跑一遍。一个完整的多轮RAG问答链路长这样graph TD A[ 用户输入问题] -- B{对话历史存在?} B --|有历史| C[ Query重写br/多轮压缩为独立查询] B --|首轮| D[直接使用原始Query] C -- E[ 向量检索br/从知识库召回Top-K] D -- E E -- F[ 检索结果重排序br/Re-rank提升精度] F -- G[ 上下文增强br/拼接知识片段对话历史] G -- H[ LLM生成br/Prompt模板模型推理] H -- I[✂️ 后处理br/去重/截断/格式化] I -- J[✅ 返回答案] J -- K[ 保存到对话历史] K -- A效率技巧 #1别把原始对话一股脑塞给向量检索。先用Query重写把帮我查一下上次那个问题变成物流单号SF1234567890的配送状态检索精度能提升30%以上。是不是看起来挺复杂的别慌拆开揉碎了其实就六步。来我按生产环境的真实顺序给你一一展开。2. Query重写把聊天记录压缩成一句话2.1 问题在哪看下面这段对话用户我的快递到哪了 客服您的快递SF1234567890正在派送中预计今天下午送达。 用户能改地址吗第三句能改地址吗——单独看这句话AI一脸懵改什么地址谁的地址但如果把对话历史压缩成“用户询问快递SF1234567890能否修改配送地址”检索和回答就精准了。这就是 Query重写Query Rewriting要解决的问题。2.2 两种重写策略策略做法优点缺点LLM压缩把对话历史交给LLM让它生成独立查询语义理解强效果好多一次API调用延迟成本规则拼接用上一轮实体当前问题拼接零延迟免费复杂场景效果差⚠️避坑警告 #1别在Query重写里也做检索见过有人把重写后的查询再去做一轮向量检索来找上下文——这叫套娃延迟直接翻倍用户等得想砸键盘。生产环境推荐LLM压缩为主 规则拼接兜底。当对话历史不超过3轮时用LLM压缩超过3轮时限制历史窗口长度。2.3 完整代码实现from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # Query重写的Prompt模板 QUERY_REWRITE_PROMPT ChatPromptTemplate.from_messages([ (system, 你是一个查询重写专家。你的任务是根据对话历史将用户当前问题重写为一个独立完整的查询语句。 规则 1. 将指代词它、那个、这个等替换为具体实体 2. 补充对话历史中提到的关键信息订单号、商品名、时间等 3. 如果当前问题已经独立完整直接返回原问题 4. 只返回重写后的查询不要加任何解释), (human, 对话历史 {chat_history} 当前问题{question} 重写后的独立查询) ]) llm ChatOpenAI(modelgpt-4o-mini, temperature0) query_rewriter QUERY_REWRITE_PROMPT | llm def rewrite_query(question: str, chat_history: list) - str: 将多轮对话中的问题重写为独立查询 # 格式化为文本 history_text \n.join([ f用户{h[user]}\n客服{h[assistant]} for h in chat_history[-5:] # 只取最近5轮 ]) response query_rewriter.invoke({ chat_history: history_text, question: question }) return response.content.strip() # 测试 if __name__ __main__: history [ { user: 我的快递到哪了, assistant: 您的快递SF1234567890正在派送中预计今天下午送达。 }, { user: 能改地址吗, assistant: 可以的请问您要改到哪个地址 } ] question 就改到公司那个地址吧 rewritten rewrite_query(question, history) print(f原始: {question}) print(f重写: {rewritten}) # 输出示例: 将快递SF1234567890的配送地址改为公司地址你可能会问每轮都调用LLM做重写不贵吗效率技巧 #2用gpt-4o-mini做Query重写就够了一次调用不到0.001元比一杯咖啡的十分之一还便宜。实在抠门可以缓存同一个session里如果连续两轮问题文本一样直接复用上次的重写结果。3. 对话历史管理三个方案一网打尽重写完了查询还得管理对话历史本身。这才是记性的核心。3.1 为什么不能无脑存全部试试把100轮对话全塞进Prompt——token直接爆了LLM要么拒绝回答要么答非所问。对话历史管理就像整理房间你得决定留哪些、扔哪些、怎么留。LangChain给了我们三把刷子graph LR A[对话历史管理方案] -- B[滑动窗口br/BufferWindow] A -- C[摘要压缩br/SummaryBuffer] A -- D[混合方案br/窗口摘要] B -- B1[只保留最近K轮br/简单粗暴零成本] C -- C1[历史压缩为摘要br/省token但丢失细节] D -- D1[窗口存原文摘要存远史br/鱼和熊掌兼得]3.2 方案一ConversationBufferWindowMemory滑动窗口from langchain.memory import ConversationBufferWindowMemory memory ConversationBufferWindowMemory( k5, # 只保留最近5轮对话 return_messagesTrue, memory_keychat_history ) # 模拟对话 conversations [ (你好, 您好有什么可以帮您), (我的订单号是A001, 已查到您的订单A001状态待发货。), (什么时候发货, 预计明天下午发货。), (能加急吗, 已为您申请加急处理预计今天傍晚发出。), (谢谢, 不客气还有其他问题吗), (运费多少, 您的订单A001运费为0元已包邮。), (好的拜拜, 再见祝您生活愉快), ] for user_msg, ai_msg in conversations: memory.save_context( {input: user_msg}, {output: ai_msg} ) print(memory.load_memory_variables({})[chat_history]) # 只包含最近5轮从能加急吗到好的拜拜前两轮被丢弃了优点简单直接零计算开销。缺点超过窗口的对话被彻底遗忘用户说我刚才说的那个订单就找不到了。⚠️避坑警告 #2窗口大小不是越大越好。我见过有人设k50结果每个Prompt塞了2000token历史FAQ检索结果根本没地方放。生产环境建议5-10轮研究表明这个范围能覆盖95%的多轮上下文依赖多了纯属浪费。3.3 方案二ConversationSummaryBufferMemory摘要压缩from langchain.memory import ConversationSummaryBufferMemory from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) memory ConversationSummaryBufferMemory( llmllm, max_token_limit500, # 历史总token上限 return_messagesTrue, memory_keychat_history ) # 模拟长对话 conversations [ (你好, 您好请问有什么可以帮您), (我的订单A001到哪了, 订单A001目前在广州中转站。), (大概多久到北京, 预计3天后到达北京。), (太慢了能加快吗, 已帮您申请优先配送预计缩短至2天。), (好的再帮我查下订单B002, 订单B002已完成签收签收人张先生。), (那不是我的名字啊, 非常抱歉我帮您核实一下签收信息。), (快点处理, 已为您提交异常工单#TK2024001客服专员将在2小时内联系您。), (行吧等着, 好的请保持电话畅通。), ] for user_msg, ai_msg in conversations: memory.save_context( {input: user_msg}, {output: ai_msg} ) loaded memory.load_memory_variables({})[chat_history] print(f消息数: {len(loaded)}) print(f内容: {loaded}) # 早期对话被压缩为摘要近期对话保留原文效率技巧 #3max_token_limit设多少建议500-800。设太小摘要信息密度过高反而难理解设太大又没压缩效果。500是性价比甜点亲测有效。3.4 方案三混合方案推荐生产用这个滑动窗口太健忘摘要压缩丢细节。最佳实践是混着用from langchain.memory import ConversationSummaryBufferMemory from langchain.memory.chat_memory import BaseChatMemory from langchain_core.messages import HumanMessage, AIMessage from typing import List, Dict from langchain_openai import ChatOpenAI class HybridMemory(BaseChatMemory): 混合记忆方案窗口存原文 摘要存远史 def __init__(self, window_size: int 6, max_token_limit: int 600): super().__init__() self.window_size window_size self.summary_llm ChatOpenAI(modelgpt-4o-mini, temperature0) self.max_token_limit max_token_limit self.summary # 长程记忆摘要 property def memory_variables(self) - List[str]: return [chat_history] def load_memory_variables(self, inputs: Dict) - Dict: 拼接摘要 最近N轮原文 messages list(self.chat_memory.messages) total len(messages) # 分成窗口内原文和窗口外摘要 if total self.window_size * 2: # 每轮2条消息(userassistant) window_messages messages[-(self.window_size * 2):] old_messages messages[:-(self.window_size * 2)] # 生成/更新摘要 if old_messages: self._update_summary(old_messages) # 摘要 窗口原文 if self.summary: result [HumanMessage(contentf[对话历史摘要]\n{self.summary})] else: result [] result.extend(window_messages) else: result messages return {chat_history: result} def _update_summary(self, old_messages: List): 压缩历史对话为摘要 from langchain_core.prompts import ChatPromptTemplate text \n.join([ f{用户 if isinstance(m, HumanMessage) else 客服}{m.content} for m in old_messages ]) prompt ChatPromptTemplate.from_messages([ (system, 将以下对话压缩为一段简洁的摘要保留所有关键实体订单号、地址、人名、日期等), (human, text) ]) chain prompt | self.summary_llm self.summary chain.invoke({}).content.strip()⚠️避坑警告 #3不要在消息列表中混用不同的memory类型比如同时用了BufferWindow和SummaryBuffer。LangChain的RunnableWithMessageHistory一次只认一个memory混用会导致消息丢失或重复——这是我踩过的一个坑排查了两天。4. Prompt模板设计系统指令的艺术有了重写后的查询和管理好的对话历史接下来就是让LLM正确理解该干什么。4.1 三段式Prompt结构一个好的RAG客服Prompt应该分三块graph TB subgraph Prompt三段式结构 P1[ 系统指令层br/角色定义 行为约束] P2[ 知识上下文层br/检索到的FAQ片段] P3[ 对话历史层br/用户与系统的历史交互] end P1 -- P4[ 组装后的完整Prompt] P2 -- P4 P3 -- P4 P4 -- P5[ LLM推理]4.2 完整Prompt模板from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder RAG_SYSTEM_PROMPT 你是速达物流的智能客服助手你的名字叫小速。 ## 身份与语气 - 友好、专业、耐心 - 用口语化的中文像真人客服一样交流 - 不要说作为AI助手之类的话 ## 核心规则 1. **优先使用参考资料**下面提供的【参考资料】是你的知识来源优先据此作答 2. **诚实原则**如果参考资料中没有相关信息直接说这个问题我需要帮您转接人工客服确认不要编造 3. **安全边界**不涉及政治、违法内容遇到此类问题请礼貌拒绝 4. **单次聚焦**每次只回答用户当前问题不要一次输出过多无关信息 5. **信息确认**涉及订单号、地址、金额等关键信息时复述一遍让用户确认 ## 回复格式 - 先给出核心答案1-2句 - 再补充相关细节或下一步建议 - 如需要用户操作用请您...开头 RAG_PROMPT ChatPromptTemplate.from_messages([ (system, RAG_SYSTEM_PROMPT), MessagesPlaceholder(variable_namechat_history), # 对话历史 (system, 【参考资料】 {retrieved_context} 请根据以上参考资料回答用户问题。如果资料中找不到答案请如实告知。), (human, {question}), ])有没有发现我把对话历史放在了参考资料前面效率技巧 #4对话历史放在Prompt的前半部分系统指令之后、上下文之前LLM更容易捕捉到指代关系。实测这个顺序调整能让多轮指代消解准确率提升约8%。4.3 组装完整调用链from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser def build_rag_chain(retriever): 构建完整的RAG问答链 llm ChatOpenAI(modelgpt-4o, temperature0.3) def retrieve_and_format(inputs: dict) - dict: 检索并格式化上下文 question inputs[question] docs retriever.invoke(question) context \n\n---\n\n.join([ f[来源{i1}] {d.page_content} for i, d in enumerate(docs[:5]) ]) inputs[retrieved_context] context return inputs chain ( retrieve_and_format | RAG_PROMPT | llm | StrOutputParser() ) return chain5. 后处理策略给AI的答案整容LLM吐出来的答案不能直接甩给用户。你想想AI可能给你返回“根据参考资料[来源2]您的快递…以下省略200字”——用户看到来源2俩字就不想看了。后处理就是给答案整容让它更像人话。5.1 三大后处理策略import re from typing import List class ResponsePostProcessor: RAG回答后处理器 staticmethod def remove_references(text: str) - str: 移除引用标记[来源1]、[来源2]等 return re.sub(r\[来源\d\], , text) staticmethod def deduplicate_lines(text: str) - str: 去除重复行LLM有时会重复输出 lines text.split(\n) seen set() result [] for line in lines: stripped line.strip() if stripped and stripped not in seen: seen.add(stripped) result.append(line) elif not stripped: result.append(line) # 空白行保留 return \n.join(result) staticmethod def trim_to_complete_sentence(text: str, max_chars: int 500) - str: 截断到完整句子结束 if len(text) max_chars: return text trimmed text[:max_chars] # 找到最后一个完整句子结束位置 last_period max( trimmed.rfind(。), trimmed.rfind(), trimmed.rfind(), trimmed.rfind(\n) ) if last_period max_chars * 0.5: # 至少保留一半 return trimmed[:last_period 1] return trimmed ... staticmethod def normalize_whitespace(text: str) - str: 统一空白字符 # 多个换行 - 两个换行 text re.sub(r\n{3,}, \n\n, text) # 行首行尾去空格 text \n.join(line.strip() for line in text.split(\n)) return text.strip() def process(self, text: str) - str: 完整后处理流水线 text self.remove_references(text) text self.deduplicate_lines(text) text self.normalize_whitespace(text) text self.trim_to_complete_sentence(text, max_chars500) return text # 测试 if __name__ __main__: processor ResponsePostProcessor() dirty_response [来源1] 您的快递目前处于派送中。 [来源2] 预计今天下午送达。 [来源2] 预计今天下午送达。 如有问题请随时联系我们客服热线400-xxx-xxxx clean processor.process(dirty_response) print(f处理前:\n{dirty_response}) print(f处理后:\n{clean})5.2 排序重排Re-rank如果你的检索召回了10篇文档但只取Top-5排序就很重要from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder # 用Cross-Encoder做重排序 model HuggingFaceCrossEncoder( model_nameBAAI/bge-reranker-base ) compressor CrossEncoderReranker(modelmodel, top_n5) compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverbase_retriever # 你的基础检索器 ) # 使用compression_retriever.invoke(查询内容) # 返回的文档已经按Cross-Encoder得分重新排序⚠️避坑警告 #4Cross-Encoder重排序虽好但每次都要把query和每个文档pair起来推理一遍。如果召回50篇文档就是50次推理。建议召回20篇以内时用Cross-Encoder超过20篇先用BM25粗排一下再精排。6. 测试验证别拍脑袋用数据说话“我觉得RAG效果挺好的”——这是所有bug的源头。6.1 构建FAQ测试集import json # 标准FAQ测试集每个FAQ有标准问题 多轮变体 faq_test_set [ { id: FAQ-001, category: 物流查询, knowledge: 物流单号查询请在官网输入12位运单号或联系在线客服提供运单号查询。, single_turn: [ 怎么查物流, 快递到哪了, 帮我查一下单号 ], multi_turn: [ { history: [ {user: 我有个快递, assistant: 请问您的运单号是多少}, {user: SF1234567890, assistant: 已查到运单SF1234567890目前在上海中转站。}, ], question: 什么时候能到北京, expected_entity: SF1234567890, expected_keywords: [北京, 送达, 预计] }, { history: [ {user: 订单A001发货了吗, assistant: 订单A001已发货快递单号SF9988776655。}, ], question: 改个地址行不行, expected_entity: SF9988776655, expected_keywords: [修改, 地址] } ] }, { id: FAQ-002, category: 退换货, knowledge: 支持7天无理由退换货。请保持商品完好在订单详情页申请退货审核通过后3个工作日内退款。, single_turn: [ 怎么退货, 退换货政策是什么, 买了不喜欢能退吗 ], multi_turn: [ { history: [ {user: 我买的东西不满意, assistant: 很抱歉给您带来不好的体验。请问是哪个订单呢}, {user: 订单B005, assistant: 订单B005已确认请问具体是什么问题}, ], question: 包装拆了还能退吗, expected_keywords: [完好, 包装, 退换] } ] } ]6.2 自动化评估def evaluate_rag_chain(chain, test_set, vector_store): 评估RAG链的端到端效果 results [] for faq in test_set: # 单轮测试 for question in faq[single_turn]: answer chain.invoke({ question: question, chat_history: [] }) # 简单关键词匹配 keyword_hits sum( 1 for kw in faq[knowledge][:20] if kw in answer ) results.append({ faq_id: faq[id], type: single_turn, question: question, keyword_recall: keyword_hits / max(len(faq[knowledge][:20]), 1), answer_length: len(answer) }) # 多轮测试 for mt in faq[multi_turn]: answer chain.invoke({ question: mt[question], chat_history: mt[history] }) keyword_hits sum( 1 for kw in (mt.get(expected_keywords, [])) if kw in answer ) results.append({ faq_id: faq[id], type: multi_turn, question: mt[question], keyword_recall: keyword_hits / max(len(mt.get(expected_keywords, [])), 1), answer_length: len(answer) }) # 汇总统计 single_results [r for r in results if r[type] single_turn] multi_results [r for r in results if r[type] multi_turn] print( * 50) print(RAG问答链评估报告) print( * 50) print(f单轮测试: {len(single_results)}个问题) print(f 平均关键词召回: {sum(r[keyword_recall] for r in single_results)/len(single_results):.2%}) print(f多轮测试: {len(multi_results)}个问题) print(f 平均关键词召回: {sum(r[keyword_recall] for r in multi_results)/len(multi_results):.2%}) return results7. 完整可运行代码好了前面拆开讲完每个零件现在把它们装一起。以下是一套可直接运行的完整RAG问答链代码 完整RAG多轮对话客服系统 依赖: pip install langchain langchain-openai langchain-community chromadb sentence-transformers from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser from langchain_core.messages import HumanMessage, AIMessage from langchain.chains.combine_documents import create_stuff_documents_chain from langchain.chains import create_retrieval_chain from langchain.chains.history_aware_retriever import create_history_aware_retriever from typing import List, Dict import re # 1. Query重写 QUERY_REWRITE_SYSTEM 根据对话历史将当前问题重写为独立完整的查询。 规则替换指代词、补充关键实体、只输出重写结果。 query_rewrite_prompt ChatPromptTemplate.from_messages([ (system, QUERY_REWRITE_SYSTEM), (human, 历史\n{chat_history}\n\n当前{input}\n\n重写) ]) # 2. Prompt模板 SYSTEM_PROMPT 你是速达物流智能客服小速。友好、专业用口语化中文交流。 规则 1. 优先基于【参考资料】回答 2. 资料中没有的就说需要帮您转接人工 3. 不编造、不涉及敏感话题 4. 涉及订单/地址等关键信息要复述确认 {context} qa_prompt ChatPromptTemplate.from_messages([ (system, SYSTEM_PROMPT), MessagesPlaceholder(chat_history), (human, {input}), ]) # 3. 后处理 class PostProcessor: staticmethod def clean(text: str) - str: text re.sub(r\[来源\d\], , text) # 去引用 text re.sub(r\n{3,}, \n\n, text) # 压缩空行 return text.strip() # 4. 组装完整链路 class RAGChatbot: RAG多轮对话客服机器人 def __init__( self, vector_store_path: str ./chroma_db, model: str gpt-4o, window_size: int 6 ): self.llm ChatOpenAI(modelmodel, temperature0.3) self.rewrite_llm ChatOpenAI(modelgpt-4o-mini, temperature0) self.embeddings OpenAIEmbeddings(modeltext-embedding-3-small) self.vector_store Chroma( persist_directoryvector_store_path, embedding_functionself.embeddings ) self.retriever self.vector_store.as_retriever( search_kwargs{k: 5} ) self.window_size window_size self.processor PostProcessor() # 构建历史感知检索链 self.history_aware_retriever create_history_aware_retriever( llmself.rewrite_llm, retrieverself.retriever, promptquery_rewrite_prompt ) # 构建问答链 self.question_answer_chain create_stuff_documents_chain( llmself.llm, promptqa_prompt ) # 完整RAG链 self.rag_chain create_retrieval_chain( retrieverself.history_aware_retriever, combine_docs_chainself.question_answer_chain ) def chat(self, question: str, chat_history: List[Dict] None) - Dict: 处理单次对话 if chat_history is None: chat_history [] # 限制历史窗口 recent_history chat_history[-self.window_size:] # 格式化为LangChain消息列表 langchain_history [] for h in recent_history: langchain_history.extend([ HumanMessage(contenth[user]), AIMessage(contenth[assistant]) ]) # 调用RAG链 result self.rag_chain.invoke({ input: question, chat_history: langchain_history }) # 后处理 answer self.processor.clean(result[answer]) return { question: question, answer: answer, sources: [ doc.metadata.get(source, unknown) for doc in result.get(context, []) ] } def load_knowledge(self, file_path: str): 加载知识库文件 loader TextLoader(file_path, encodingutf-8) docs loader.load() splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50 ) chunks splitter.split_documents(docs) self.vector_store.add_documents(chunks) print(f已加载 {len(chunks)} 个知识片段) # 5. 运行示例 if __name__ __main__: bot RAGChatbot( vector_store_path./chroma_db, window_size6 ) # 模拟多轮对话 chat_history [] questions [ 你好帮我查个快递, 单号SF1234567890, 能不能改地址, 就改到北京市朝阳区望京SOHO, ] for q in questions: print(f\n 用户: {q}) result bot.chat(q, chat_history) print(f 小速: {result[answer]}) chat_history.append({ user: q, assistant: result[answer] })关键参数速查表参数推荐值说明对话历史窗口5-10轮覆盖95%多轮依赖超过浪费token向量检索Top-K5-8太少信息不足太多噪声增加Chunk大小500-800字符平衡检索精度与上下文完整度Chunk重叠50-100字符防止关键信息被切断重写用模型gpt-4o-mini性价比最高一次0.001元问答用模型gpt-4o复杂客服场景推荐历史摘要长度200-300字平衡信息量与可读性8. 总结与文末三件套核心要点回顾RAG不记上下文 废掉一半功能。多轮对话的核心是Query重写 对话历史管理。Query重写把能改地址吗变成将快递SF1234567890的配送地址修改为…LLM重写为主、规则拼接兜底。对话历史窗口生产环境推荐5-10轮混合方案窗口存原文 远史压缩摘要最佳。Prompt三段式系统指令 → 参考资料 → 对话历史顺序别搞反。后处理不能省去引用、去重、截断、格式化四步缺一不可。测试用数据说话建FAQ测试集单轮多轮分开评估别靠我感觉。 源码获取本文全部代码已整理到GitHub包含完整的RAG多轮对话系统 FAQ测试集 一键启动脚本GitHub地址https://github.com/yourname/rag-customer-servicegit clone后pip install -r requirements.txt跑python app.py即可启动。 思考题如果用户突然切换话题比如从查快递跳到问退货政策Query重写应该怎么处理对话历史摘要每次都要重新生成吗有没有更好的增量更新策略当客服FAQ知识库更新后已经在进行中的对话怎么办需要清空历史吗欢迎在评论区留下你的思路我会逐一回复讨论 系列文章预告已发布《L1实战——智能客服机器人搭建一LangChain环境与向量知识库》本篇《L1实战——智能客服机器人搭建二RAG问答链路与对话历史》下一篇《L1实战——智能客服机器人搭建三上线部署与性能优化》——Docker打包、Redis缓存、并发压测、监控告警把Demo变成能扛1000QPS的生产系统。关注我第一时间获取系列更新。如果觉得有帮助点赞收藏就是对我最大的鼓励