llm-universe 如何用 RunnableBranch 为检索问答链添加聊天记录并压缩指代不清的 query
2026/9/15 16:47:48 网站建设 项目流程

llm-universe 如何用 RunnableBranch 为检索问答链添加聊天记录并压缩指代不清的 query

【免费下载链接】llm-universe本项目是一个面向小白开发者的大模型应用开发教程,在线阅读地址:https://datawhalechina.github.io/llm-universe/项目地址: https://gitcode.com/GitHub_Trending/ll/llm-universe

在 llm-universe 教程的第四章中,我们已经用 LangChain 搭出了一条单轮的检索问答链:把向量知识库的召回结果拼进 prompt,再交给大模型回答。但它有一个明显缺陷——模型不记得之前的对话。当用户接着上一轮追问“南瓜书跟它有什么关系?”时,query 本身语义不完整,直接用它去查向量库很难召回相关内容。本文的任务就是在这条已有的检索问答链上扩展出多轮对话能力:用ChatPromptTemplatechat_history传给模型,并用 LangChain 的RunnableBranch在检索前先用 LLM 根据聊天记录压缩、补全指代不清的最新 query,再用补全后的 query 去检索。全部代码和验证方式均来自 docs/C4/C4.md 的 4.2.5 与 4.2.6 节,可在 C4.ipynb 中对照执行。

准备条件

按文档要求,开始之前需要满足三个前提:

  1. 已建好的向量数据库。文档要求此处必须使用和构建时相同的 Embedding,仓库中示例使用智谱 Embedding,加载路径为data_base/vector_db/chroma(Chroma 持久化目录)。
  2. 封装好的 Embedding 代码。文档要求将 zhipuai_embedding.py 放到当前 notebook 的同级目录下(教程 notebook 中通过sys.path.append("../C3 搭建知识库")引入)。
  3. 环境变量与依赖。在.env中配置OPENAI_API_KEY(用于ChatOpenAI)和ZHIPUAI_API_KEY(用于ZhipuAIEmbeddings),并用load_dotenv(find_dotenv())加载。依赖版本以 requirements.txt 为准:langchain==0.3.0langchain-core==0.3.0langchain-openai==0.2.0langchain-chroma==0.1.4python-dotenv==1.0.1zhipuai==2.1.5.20250106等。

加载向量数据库并构建检索器

这一步沿用文档 4.2.1 节的写法:用与建库时相同的 Embedding 加载 Chroma 库,再用as_retriever构造检索器。

import sys sys.path.append("../C3 搭建知识库") # 将 C3 目录放入系统路径中 from dotenv import load_dotenv, find_dotenv import os _ = load_dotenv(find_dotenv()) # 读取本地 .env 文件 from zhipuai_embedding import ZhipuAIEmbeddings from langchain.vectorstores.chroma import Chroma # 定义 Embeddings(必须与建库时相同) embedding = ZhipuAIEmbeddings() # 向量数据库持久化路径(notebook 内的相对路径,以你的工作目录为准) persist_directory = '../../data_base/vector_db/chroma' # 加载数据库 vectordb = Chroma( persist_directory=persist_directory, embedding_function=embedding )

加载成功后可以用一个简单问题验证检索器可用:

question = "什么是prompt engineering?" retriever = vectordb.as_retriever(search_kwargs={"k": 3}) docs = retriever.invoke(question) print(f"检索到的内容数:{len(docs)}")

文档示例中,k=3时输出“检索到的内容数:3”(文档示例,具体召回数量取决于你的知识库)。这一步确认了后文RunnableBranch两条分支最终汇入的retriever是可用的。

构造带 chat_history 的问答 prompt

文档 4.2.5 节的思路是:聊天模型的接口基于消息而非纯文本,ChatPromptTemplate可以接收聊天消息历史记录,与当前问题一起传给模型,从而把历史加入上下文。问答链的系统 prompt 和模板如下:

from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableBranch, RunnablePassthrough from langchain_openai import ChatOpenAI # 4.2.3 节定义的 LLM llm = ChatOpenAI(model_name="gpt-4o", temperature=0) # 问答链的系统 prompt system_prompt = ( "你是一个问答任务的助手。 " "请使用检索到的上下文片段回答这个问题。 " "如果你不知道答案就说不知道。 " "请使用简洁的话语回答用户。" "\n\n" "{context}" ) # 制定 prompt template qa_prompt = ChatPromptTemplate( [ ("system", system_prompt), ("placeholder", "{chat_history}"), ("human", "{input}"), ] )

其中("placeholder", "{chat_history}")是关键:历史消息会被展开为完整的消息列表插在 system 与当前用户输入之间。可以先单独验证这一点,不带历史记录和有历史记录各调用一次qa_prompt.invoke

# 有历史记录 messages = qa_prompt.invoke( { "input": "你可以介绍一下他吗?", "chat_history": [ ("human", "西瓜书是什么?"), ("ai", "西瓜书是指周志华老师的《机器学习》一书,是机器学习领域的经典入门教材之一。"), ], "context": "" } ) for message in messages.messages: print(message.content)

文档示例输出依次打印出系统提示、“西瓜书是什么?”,其 AI 回复,以及最新的“你可以介绍一下他吗?”,说明历史消息已按顺序进入消息列表(文档示例)。到这里,回答环节已经具备多轮对话能力,但检索环节还没有。

用 RunnableBranch 压缩指代不清的 query

问题出在检索这一环:像“你可以介绍一下他吗?”这样语义不全的最新对话,直接查向量库几乎召回不到有效内容——它其实想问的是“你可以介绍下周志华老师吗?”。文档 4.2.6 节的解法是信息压缩:先让 LLM 根据历史记录完善用户的问题,再用完善后的 query 检索。分支选择用RunnableBranch完成:

# 压缩问题的系统 prompt condense_question_system_template = ( "请根据聊天记录完善用户最新的问题," "如果用户最新的问题不需要完善则返回用户的问题。" ) # 构造压缩问题的 prompt template condense_question_prompt = ChatPromptTemplate([ ("system", condense_question_system_template), ("placeholder", "{chat_history}"), ("human", "{input}"), ]) # 构造检索文档的链 # RunnableBranch 会根据条件选择要运行的分支 retrieve_docs = RunnableBranch( # 分支 1: 若聊天记录中没有 chat_history 则直接使用用户问题查询向量数据库 (lambda x: not x.get("chat_history", False), (lambda x: x["input"]) | retriever, ), # 分支 2: 若聊天记录中有 chat_history 则先让 llm 根据聊天记录完善问题再查询向量数据库 condense_question_prompt | llm | StrOutputParser() | retriever, )

两个分支的执行路径要分清:

  • 分支 1:输入字典里没有chat_history或其值为假(如空列表)时,取x["input"]原样交给retriever,不产生额外的 LLM 调用;
  • 分支 2:存在聊天记录时,先用condense_question_prompt | llm | StrOutputParser()把 query 补全为独立成句的问题,再交给retriever检索。

组装支持聊天记录的检索问答链

有了retrieve_docs,接下来把它接入问答链。注意这里combine_docs需要相对单轮版本做一处改动:检索结果现在被RunnablePassthrough.assign存到了键"context"下,所以整合函数要改成从docs["context"]取值:

# 重新定义 combine_docs def combine_docs(docs): return "\n\n".join(doc.page_content for doc in docs["context"]) # 将 docs 改为 docs["context"] # 定义问答链 qa_chain = ( RunnablePassthrough.assign(context=combine_docs) # 使用 combine_docs 函数整合 qa_prompt 中的 context | qa_prompt # 问答模板 | llm | StrOutputParser() # 规定输出的格式为 str ) # 定义带有历史记录的问答链 qa_history_chain = RunnablePassthrough.assign( context = (lambda x: x) | retrieve_docs # 将查询结果存为 context ).assign(answer=qa_chain) # 将最终结果存为 answer

整条链的运行顺序是:输入{"input": ..., "chat_history": [...]}RunnableBranch按有无历史选择压缩或直查,得到文档列表并存为contextqa_chaincombine_docs拼出上下文、带上chat_history交给 LLM,得到answer

验证链是否生效

文档给出了两组对照测试。第一组不带聊天记录,走的是分支 1:

# 不带聊天记录 qa_history_chain.invoke({ "input": "西瓜书是什么?", "chat_history": [] })

文档示例返回的字典中,context是从南瓜书 PDF 召回的若干Documentanswer为“西瓜书是指周志华老师的《机器学习》一书,它是机器学习领域的经典入门教材之一。”(文档示例)

第二组带聊天记录,验证压缩后的 query 能否检索到正确内容、LLM 能否正确理解指代:

# 带聊天记录 qa_history_chain.invoke({ "input": "南瓜书跟它有什么关系?", "chat_history": [ ("human", "西瓜书是什么?"), ("ai", "西瓜书是指周志华老师的《机器学习》一书,是机器学习领域的经典入门教材之一。"), ] })

文档示例中,context召回了南瓜书中把西瓜书作为前置知识的段落,answer为“南瓜书是对西瓜书中较难理解的公式进行解析和推导细节补充的书籍。它以西瓜书的内容为前置知识,帮助读者更好地理解和学习西瓜书中的内容。”(文档示例)

文档对成功现象的判断是:LLM 准确地判断了“它”是什么,说明历史信息被成功传递;召回的内容中包含问题的答案,证明信息压缩策略也起到了作用。

限制说明与下一步

  • Embedding 必须一致:加载向量库时必须使用与建库时相同的 Embedding,这是文档 4.2.1 节明确要求的前提,否则召回结果不可用。
  • 两处文档措辞不同:4.2.6 节压缩 prompt 用“请根据聊天记录完善用户最新的问题,如果用户最新的问题不需要完善则返回用户的问题”,而 4.3.2 节 Streamlit 应用中用的是“请根据聊天记录总结用户最近的问题,如果没有多余的聊天记录则返回用户的问题”。本文主路径按 4.2.6 节执行;两种措辞在文档中均给出,实际效果以你自己的知识库测试为准。
  • Chroma 弃用警告:文档示例中出现The class 'Chroma' was deprecated in LangChain 0.2.9 ... import as 'from langchain_chroma import Chroma'的提示(requirements.txt 已包含langchain-chroma==0.1.4),属于警告信息,不影响示例链的运行。

文档 4.3.2 节给出了这条链的后续用法:把同样的RunnableBranch逻辑封装进get_qa_history_chain函数,再用 Streamlit 以st.session_state保存多轮messages、以chain.stream流式输出,本地运行命令为streamlit run "notebook/C4 构建 RAG 应用/streamlit_app.py"。对应源码可直接查看 streamlit_app.py,细节以 docs/C4/C4.md 的 4.3 节为准。

【免费下载链接】llm-universe本项目是一个面向小白开发者的大模型应用开发教程,在线阅读地址:https://datawhalechina.github.io/llm-universe/项目地址: https://gitcode.com/GitHub_Trending/ll/llm-universe

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询