目录

一个专注音视频领域的小圈子

前面的 RAG 示例都是单轮问答。

但真实使用时,用户经常会追问:

Python 函数式编程讲了什么?
上一轮提到的高阶函数,适合初学者先理解哪些?

第二个问题里的“上一轮”就是上下文引用。如果系统没有记忆,就很难知道用户在问什么。

这一篇就做一个最小多轮 RAG。

本文对应代码位于 code/langchain-demo/chapter09

本篇目标

第九篇要做两件事:

  1. 保存历史对话消息。
  2. 每一轮都根据当前问题检索资料,并把历史和上下文一起交给模型。

完整流程如下:

第 1 轮问题
  -> 检索上下文
  -> 生成回答
  -> 保存 HumanMessage / AIMessage

第 2 轮问题
  -> 读取历史消息
  -> 检索当前问题上下文
  -> 生成回答
  -> 继续保存历史

这一篇先用内存里的 list 保存历史,不引入数据库。

MessagesPlaceholder

LangChain 里可以用 MessagesPlaceholder 把历史消息插入 Prompt:

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "你是一个本地博客问答助手。"
            "回答时要结合历史对话和当前检索上下文。"
            "如果上下文不足,就明确说明。",
        ),
        MessagesPlaceholder("history"),
        (
            "human",
            "当前问题:{question}\n\n"
            "当前检索上下文:\n{context}\n\n"
            "请用中文回答,并给出参考来源。",
        ),
    ]
)

这里的 history 不是普通字符串,而是一组消息:

history: list[BaseMessage] = []

每轮回答后,把用户问题和模型回答都追加进去:

history.append(HumanMessage(content=question))
history.append(AIMessage(content=answer))

这样下一轮调用时,模型就能看到前面的对话内容。

每轮都要检索

多轮对话里有一个容易忽略的问题:有了历史,不代表就不需要检索。

历史消息解决的是“用户上下文”。

RAG 检索解决的是“知识上下文”。

所以每一轮仍然会执行:

relevant_chunks = retrieve(question, chunks, top_k=4)

然后把检索结果放进 prompt:

answer = chain.invoke(
    {
        "history": history,
        "question": question,
        "context": format_context(relevant_chunks),
    }
)

这样模型既能知道用户上一轮问了什么,也能看到当前问题对应的文章片段。

默认对话

第九篇写了两个默认问题:

DEFAULT_QUESTIONS = [
    "Python 函数式编程讲了什么?",
    "上一轮提到的高阶函数,适合初学者先理解哪些?",
]

第一轮是完整问题。

第二轮则故意写成追问,依赖上一轮回答里的“高阶函数”。

这能直观看到 memory 的作用。

运行 demo

code/langchain-demo 目录下运行:

uv run python -m chapter09.memory_rag

输出会按轮次打印:

Turn 1
Question:
Python 函数式编程讲了什么?
Sources:
...
Answer:
...

Turn 2
Question:
上一轮提到的高阶函数,适合初学者先理解哪些?
Sources:
...
Answer:
...

第二轮回答时,模型会结合第一轮对话,知道“上一轮提到的高阶函数”指的是 Python 函数式编程里的 mapreducefiltersorted 这些内容。

也可以传入自己的第一轮问题:

uv run python -m chapter09.memory_rag "FFmpeg 硬解码相关内容有哪些?"

程序会自动追加一个第二轮追问。

这种 memory 有什么限制

这一篇的 memory 很简单,只是一个 Python list。

它有几个限制:

  1. 程序结束后历史就没了。
  2. 对话很长时,历史消息会占用越来越多上下文。
  3. 没有摘要压缩。
  4. 没有按用户或会话保存。

不过作为入门学习,它已经足够说明一个重点:

多轮对话 = 历史消息 + 当前问题 + 当前检索上下文

后面做真实应用时,可以把 history 保存到数据库,也可以在历史太长时做摘要。

小结

这一篇补上了多轮对话能力。

到这里,本地博客问答已经可以处理追问,不再是每次都从零开始。

下一篇会把前面这些能力收束成一个简单 CLI,让它更像一个可以日常使用的小工具。

参考

  • code/langchain-demo/chapter09
  • LangChain MessagesPlaceholder
  • LangChain HumanMessage
  • LangChain AIMessage

原创文章,转载请注明来源:    LangChain 入门学习第九篇-多轮对话和上下文记忆