如何在 LangChain 中实现 PDF 聊天机器人的多轮对话记忆功能
本文详解如何基于 langchain + fastapi 构建支持上下文感知的 pdf 智能问答系统,重点解决“无法记住历史提问”问题,通过 conversationbuffermemory 实现真正的多轮对话连贯性。
本文详解如何基于 langchain + fastapi 构建支持上下文感知的 pdf 智能问答系统,重点解决“无法记住历史提问”问题,通过 conversationbuffermemory 实现真正的多轮对话连贯性。
在构建 PDF 智能问答服务时,一个常见痛点是:单次提问能返回准确答案,但后续追问(如“它提到的日期是多少?”、“这个结论的依据是什么?”)却无法关联前文——系统“失忆”了。根本原因在于原始代码中每次调用 /process/ 都新建独立链(RetrievalQA),未共享对话状态。解决方案不是简单启用 memory 参数,而是将 ConversationBufferMemory 与 load_qa_chain 深度集成,并配合自定义 Prompt 模板显式注入聊天历史。
✅ 正确实现多轮记忆的关键步骤
-
全局初始化内存实例(非每次请求新建)
memory = ConversationBufferMemory( memory_key="chat_history", input_key="human_input", return_messages=True # 返回 Message 对象而非字符串,兼容新版 LangChain )⚠️ 注意:必须声明为模块级变量(如 global memory 或使用依赖注入),否则每次请求都会重置。
-
构造带历史上下文的 Prompt 模板
模板需明确预留 {chat_history} 占位符,并确保其位置在问题之前,让 LLM 理解对话流:template = """You are a helpful assistant answering questions about a document. Context from document: {context} Previous conversation: {chat_history} Human: {human_input} Assistant:""" prompt = PromptTemplate( input_variables=["chat_history", "human_input", "context"], template=template ) -
使用 load_qa_chain 并传入 memory 和 prompt
替代已弃用的 ConversationalRetrievalChain(v0.1+ 中行为不稳定):chain = load_qa_chain( llm=OpenAI(temperature=0.0), chain_type="stuff", memory=memory, prompt=prompt ) response = chain.invoke({ "input_documents": docs, "human_input": question }, return_only_outputs=True)
? 完整工作流程示例
假设用户上传一份《2023 年公司年报.pdf》,随后发起以下对话:
小门道AI
小门道AI是一个提供AI服务的网站
下载
- Q1: “公司去年营收是多少?” → 返回 “¥1.2B”
- Q2: “同比增长多少?” → 模型结合 chat_history 中的“去年营收 ¥1.2B”及文档中“2022 年营收 ¥1.0B”,正确回答 “20%”
这依赖于 memory 自动将 Q1 的问答对存入 chat_history,并在 Q2 请求时通过 Prompt 注入上下文。
⚠️ 重要注意事项
- 避免 global vector_store 的线程安全风险:生产环境应改用依赖注入或缓存服务(如 Redis)存储向量库,当前代码仅适用于单用户本地测试。
- return_messages=True 是关键:LangChain v0.1+ 要求 memory 返回 BaseMessage 列表(如 AIMessage, HumanMessage),否则 Prompt 渲染失败。
- Embedding 与 LLM 的一致性:确保 OpenAIEmbeddings() 与 OpenAI() 使用相同模型(如 text-embedding-3-small + gpt-3.5-turbo),避免语义空间错位。
- 调试技巧:打印 memory.load_memory_variables({}) 可实时查看当前存储的对话历史。
✅ 总结
实现 PDF 多轮问答的核心不在于“加个 memory”,而在于三者协同:
① 全局可复用的 ConversationBufferMemory 实例;
② 显式包含 {chat_history} 占位符的 Prompt 模板;
③ 使用 load_qa_chain(而非 RetrievalQA)并正确传递 memory 和 prompt。
如此,你的 FastAPI 后端即可像人类一样“记住上一句”,真正理解 follow-up 问题中的指代与隐含前提。