如何在 LangChain RAG 中同时支持聊天历史与源文档返回

星涛同学_1463

星涛同学_1463

2026-05-24

278人浏览

原创

如何在 LangChain RAG 中同时支持聊天历史与源文档返回

本文详解如何在 LangChain 构建的 RAG 系统中,无缝融合对话历史(chat history)与检索源(sources),通过 Runnable 组合、自定义 Prompt 模板和上下文注入实现可追溯、有记忆的问答 API。

本文详解如何在 langchain 构建的 rag 系统中,无缝融合对话历史(chat history)与检索源(sources),通过 `runnable` 组合、自定义 prompt 模板和上下文注入实现可追溯、有记忆的问答 api。

在 LangChain 的现代 RAG 实践中,单纯使用 ConversationRetrievalChain 已不被推荐(v0.1+ 中已弃用),官方更鼓励使用函数式、可组合的 Runnable 接口。而用户常遇到的核心矛盾是:既要保留多轮对话状态(如上一轮问题/回答),又要显式返回支撑答案的原始文档来源(source metadata)——这两者在默认链路中天然存在张力:retriever 仅作用于当前问题,而 chat_history 需参与 prompt 构建,但又不能干扰检索逻辑。

解决方案的关键在于解耦“检索”、“历史注入”与“生成”三个阶段,并通过 RunnableParallel + assign 显式控制数据流。以下是经过生产验证的结构化实现:

✅ 正确的数据流设计(推荐模式)

from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, SystemMessage

# 1. 定义带历史占位符的 Prompt 模板(关键!)
SYSTEM_TEMPLATE = "你是一个专业的产品顾问。请基于以下上下文回答问题,并在结尾注明所参考的产品信息。"
HUMAN_TEMPLATE = "问题:{question}\n历史对话:{chat_history}"

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", SYSTEM_TEMPLATE),
    MessagesPlaceholder(variable_name="chat_history"),  # ← 支持动态插入历史
    ("human", HUMAN_TEMPLATE),
])

# 2. 构建带历史感知的 RAG 链
def format_docs(docs):
    return "\n\n".join([
        f"- {doc.metadata.get('name', '未知')}, "
        f"{doc.metadata.get('brand', 'N/A')}, "
        f"{doc.metadata.get('country', 'N/A')}"
        for doc in docs
    ])

rag_chain = (
    # 并行执行:检索 + 透传问题 & 历史
    RunnableParallel({
        "context": retriever,           # 当前问题触发的检索结果
        "question": RunnablePassthrough(), 
        "chat_history": RunnablePassthrough(),  # 历史作为独立输入传入
    })
    # 注入格式化后的 context 到 prompt
    .assign(context=lambda x: format_docs(x["context"]))
    # 组装完整 prompt 并调用模型
    .assign(answer=(
        chat_prompt 
        | model 
        | StrOutputParser()
    ))
    # 最终输出 answer + 原始 context(含 source metadata)
    .pick(["answer", "context"])  
)

✅ FastAPI 接口集成(支持状态管理)

from typing import List, Dict, Any
from langchain_core.messages import get_buffer_string

# 模拟内存存储(生产环境应替换为 Redis / DB)
chat_histories: Dict[str, List[Dict]] = {}

@app.post("/invoke")
def invoke_rag(
    question: Question,
    session_id: str = "default"  # 用于区分不同用户会话
):
    # 获取并格式化历史消息(转为字符串或 Message 对象)
    history = chat_histories.get(session_id, [])
    # 转为 LangChain 兼容的 Message 列表(如需结构化处理)
    # 或直接转为字符串:get_buffer_string(history) 

    # 执行链(传入 question + history)
    result = rag_chain.invoke({
        "question": question.question,
        "chat_history": history
    })

    # 更新历史(追加用户问题 + AI 回答)
    if session_id not in chat_histories:
        chat_histories[session_id] = []
    chat_histories[session_id].extend([
        {"role": "user", "content": question.question},
        {"role": "assistant", "content": result["answer"]}
    ])

    return {
        "answer": result["answer"],
        "sources": [
            {
                "product_name": doc.metadata.get("name"),
                "brand": doc.metadata.get("brand"),
                "country": doc.metadata.get("country"),
                "page_content_preview": doc.page_content[:100] + "..."
            }
            for doc in result["context"]
        ]
    }

⚠️ 注意事项与最佳实践

  • 不要滥用 ConversationRetrievalChain:它内部硬编码了 prompt 和 memory 逻辑,难以定制 source 格式与历史处理方式,且与新版 Runnable 生态不兼容。
  • MessagesPlaceholder 是历史注入的核心:必须在 ChatPromptTemplate 中显式声明,否则 chat_history 不会被渲染进 prompt。
  • Source 返回需保留原始 Document 对象:retriever 输出的 Document 包含完整 metadata(如 source, uri, page_number),切勿仅返回 page_content 字符串而丢失溯源能力。
  • 流式响应兼容性:若需 stream(),请改用 RunnableLambda 封装分块逻辑,并确保 pick() 前不触发最终解析(避免阻塞)。
  • 安全提示:FAISS.load_local(..., allow_dangerous_deserialization=True) 存在反序列化风险,生产环境务必改用 Chroma、PGVector 等安全向量库。

该方案已在多个 FastAPI + LangChain RAG 项目中稳定运行,兼顾可维护性、可调试性与可扩展性。核心思想是:让每个 Runnable 只做一件事,用 assign 显式传递中间态,用 pick 精确控制输出契约——这正是 LangChain v0.1+ 函数式范式的精髓所在。

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
OpenJDK JVM参数怎么调优
OpenJDK JVM参数怎么调优

Java应用运行缓慢,如何调整JVM参数?本专题介绍OpenJDK JVM性能调优的基本流程,涵盖性能指标确认、运行数据采集、内存配置和垃圾回收分析,帮助你先定位瓶颈,再通过对照测试验证效果,避免直接套用参数而影响应用稳定性。

2026.10.16

0

10

OpenJDK怎么配置Java开发环境
OpenJDK怎么配置Java开发环境

怎样用OpenJDK搭建开发环境?本专题介绍版本选择、下载安装、环境变量配置和编译运行验证,帮助你理清JDK的作用及基本配置流程,并通过简单程序检查环境是否可用,为后续项目开发做好准备。

2026.10.16

0

10

OpenJDK怎么部署Java应用
OpenJDK怎么部署Java应用

Java项目开发完成后,怎样部署到目标环境并正常运行?本专题介绍OpenJDK应用部署流程,涵盖运行环境准备、版本兼容检查、应用打包、启动参数配置和日志查看,帮助你理清部署前后的操作顺序,减少环境差异造成的运行问题。

2026.10.16

0

10

Revel框架模板渲染与表单验证
Revel框架模板渲染与表单验证

本专题围绕Revel框架视图层开发,介绍模板语法、变量输出、布局复用、表单提交、参数绑定与验证规则,帮助开发者完成安全规范的页面交互。

2026.10.16

0

15

Revel框架MVC路由与控制器
Revel框架MVC路由与控制器

本专题聚焦Revel框架MVC核心机制,讲解路由定义、控制器Action、参数绑定、请求响应和反向路由,并结合常见场景梳理接口请求处理流程。

2026.10.16

0

15

Revel框架入门与安装配置
Revel框架入门与安装配置

本专题从Revel框架环境搭建讲起,系统介绍命令行工具安装、项目创建、目录结构、配置文件及热代码重载,帮助开发者快速完成首个Go Web应用。

2026.10.16

0

15

Linux 新手先学哪些命令
Linux 新手先学哪些命令

Linux新手命令专题汇集终端基础、文件目录操作、文本查看、软件安装和权限管理等入门文章,帮助按实际任务练习常用命令并建立命令行操作习惯。

2026.10.15

20

20

MySQL 忘记密码怎么办
MySQL 忘记密码怎么办

MySQL忘记密码专题汇集不同版本与系统下的root密码重置方法,覆盖MySQL 5.7、8.0、Mac、Linux及本地集成环境,并提示恢复权限与重新登录的检查步骤。

2026.10.15

20

18

Vue.js 中 ref 和 reactive 的区别
Vue.js 中 ref 和 reactive 的区别

Vue.js中ref和reactive的区别专题汇集两种响应式API的定义、访问方式、对象解构、侦听与性能取舍等文章,帮助结合实际状态类型选择合适写法。

2026.10.15

0

20

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.7万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 133.7万人学习