13-一文讲透 LangChain Memory:LLMChain、ConversationChain、CombinedMemory 与 RAG 实战

0 前言

LLM 本身没有会话状态,用户上一轮说过什么、名字叫什么、刚刚讨论到哪里,模型不会自动记住。LangChain 的 Memory 组件就是用来在多轮调用之间保存上下文,并在下一次调用 Chain 时自动把历史内容注入 Prompt。

本文主讲五块:

  • LLMChain 中使用记忆。
  • ConversationChain 中使用记忆。
  • 自定义 Prompt 覆盖默认对话模板。
  • 同一个链合并使用多个 Memory。
  • 给多参数问答链增加记忆。

可将 Chain 理解成一次"应用服务编排",把 Memory 理解成"会话上下文存储"。调用模型不是一次孤立 RPC,而是一条带 Prompt、模型、上下文和输出的业务链路。

1 在 LLMChain 中使用 ConversationBufferMemory

1.1 定义带历史记录的 Prompt

python 复制代码
template = """你是一个可以和人类对话的机器人.
{chat_history}
人类:{human_input}
机器人:"""

prompt = PromptTemplate(
    template=template,
    # chat_history:由 Memory 自动提供,里面是历史对话
    # human_input:本轮用户输入,由调用方提供
    input_variables=["chat_history", "human_input"],
)

最终传给模型的内容类似:

text 复制代码
你是一个可以和人类对话的机器人.
Human: 我叫 JavaEdge
AI: 好的,我记住了。
人类:你还记得我叫什么吗?
机器人:

模型之所以"记得",不是因为模型真保存了状态,而是因为 LangChain 在下一次调用时把历史对话重新拼进了Prompt。

1.2 创建 Memory、模型和 Chain

python 复制代码
memory = ConversationBufferMemory(
    # 这里必须和 Prompt 里的{chat_history}对上
    # 调用 Chain 时,Memory 会把历史记录放到chat_history变量,然后 PromptTemplate 再把它渲染进完整提示词。
    memory_key="chat_history",
)

chain = LLMChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True,
)

调用方式:

python 复制代码
chain.predict(human_input="我最新喜欢我的世界这个游戏,你还记得我叫什么吗?")

如果前面没有先告诉模型名字,这里通常答不出来;如果前面已经通过同一个 chain 调用告诉过名字,Memory 就会带上历史对话,模型可以基于历史回答。

2 使用 ChatPromptTemplate 和 MessagesPlaceholder

接着换成 Chat 模型更常用的消息模板。

普通 PromptTemplate 是一整段字符串,而 ChatPromptTemplate 更贴近聊天模型的输入结构:系统消息、历史消息、用户消息分别组织。

2.1 定义 Chat Prompt

python 复制代码
# 这段 Prompt 分三层:
prompt = ChatPromptTemplate.from_messages(
    [
        # 系统角色设定,告诉模型它是什么助手
        SystemMessage(
            content="你好,我是一个可以和人类对话的机器人",
            role="system",
        ),
        # 预留历史消息位置
        MessagesPlaceholder(
            variable_name="chat_history",
        ),
        # 本轮用户输入
        HumanMessagePromptTemplate.from_template(
            "{human_input}"
        ),
    ]
)

可以用下面代码查看渲染效果:

python 复制代码
print(prompt.format(human_input="你好", chat_history=[]))

此时 chat_history=[],说明没有历史对话。后面接入 Memory 后,这个列表会由 Memory 自动填入。

2.2 return_messages=True 的意义

python 复制代码
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True,
)

这里和前面最大的区别是 return_messages=True

如果是普通字符串 Prompt,历史记录可以是一段文本;但 ChatPromptTemplate 里的 MessagesPlaceholder 期望拿到消息对象列表,所以要让 Memory 返回消息格式,而不是纯字符串。

完整链路:

python 复制代码
llm = create_qwen_model(
    temperature=1,
    streaming=True,
)

chain = LLMChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True,
)

chain.predict(human_input="我叫 JavaEdge, 我是一个AI应用程序猿")

这次调用后,Memory 会保存一轮消息:

  • Human:我叫 JavaEdge,我是一个 AI 应用程序猿。
  • AI:模型的回答。

后续继续调用同一个 chain,这些历史消息就会进入 chat_history

3 ConversationChain:更适合对话的默认链

ConversationChain 可理解为 LangChain 已帮你封装好的对话链。它默认用 historyinput 这类变量组织对话。

python 复制代码
memory = ConversationBufferMemory(
    memory_key="history",
    return_messages=True,
)

chain = ConversationChain(
    llm=llm,
    memory=memory,
)

所以这里的 memory_key="history" 要和 ConversationChain 默认 Prompt 里的历史变量对齐。

调用方式:

python 复制代码
chain.predict(input="帮我做个一日游攻略")

这里参数名是 input,不是 human_input。这也是写 LangChain 代码时经常要注意的点:Prompt 里需要什么变量,Chain 调用时就要传什么变量;Memory 的 key 也必须和 Prompt 变量匹配。

4 自定义 ConversationChain 的 Prompt

覆盖 ConversationChain 的默认模板:

python 复制代码
template = """下面是一段AI与人类的对话,AI会针对人类问题,提供尽可能详细的回答,如果AI不知道答案,会直接回复'人类老爷,我真的不知道'.
当前对话:
{history}
Human:{input}
AI助手:"""

prompt = PromptTemplate(
    template=template,
    input_variables=["history", "input"],
)

chain = ConversationChain(
    llm=llm,
    memory=ConversationBufferMemory(
        ai_prefix="AI助手",
        return_messages=True,
    ),
    prompt=prompt,
)

这段代码做了三件事:

  1. 自定义回答风格:尽可能详细,不知道就明确说不知道。
  2. 明确了历史对话位置:{history}
  3. 修改了 AI 前缀:ai_prefix="AI助手"

ai_prefix 会影响 Memory 保存和展示历史对话时 AI 一方的名称。例如历史内容可能会显示为:

text 复制代码
Human: 你好
AI助手: 你好,我可以帮你什么?

调用:

python 复制代码
chain.predict(input="你知道我叫什么名字吗?")

如果当前 chain 的 Memory 里之前没有保存用户名字,模型就应该按模板要求回答不知道。

5 同一个链合并使用多个 Memory

本部分讲了一个更接近真实场景的问题:对话越来越长咋办?

只用 ConversationBufferMemory 会把所有历史对话原样塞进 Prompt。优点是信息完整,缺点是 token 成本越来越高,长对话还可能超过模型上下文长度。

所以引入两个 Memory:

  • ConversationSummaryMemory:对历史对话做摘要。
  • ConversationBufferMemory:保留最近的原始对话。

代码如下:

python 复制代码
summary = ConversationSummaryMemory(
    llm=llm,
    input_key="input"
)

cov_memory = ConversationBufferMemory(
    memory_key="history_now",
    input_key="input",
)

memory = CombinedMemory(
    memories=[summary, cov_memory],
)

summary保存的是摘要型记忆。cov_memory 保存当前原始对话,并且指定:

  • memory_key="history_now":这份 Memory 输出到 Prompt 的变量名。
  • input_key="input":用户输入字段叫 input

CombinedMemory 的作用是把多份 Memory 合并成一个 Memory 对象,交给 Chain 使用。

5.1 Prompt 同时接收摘要和当前对话

python 复制代码
TEMPLATE = """下面是一段AI与人类的对话,AI会针对人类问题,提供尽可能详细的回答,如果AI不知道答案,会直接回复'人类老爷,我真的不知道'.
之前的对话摘要:
{history}
当前对话:
{history_now}
Human:{input}
AI:"""

prompt = PromptTemplate(
    template=TEMPLATE,
    input_variables=["history", "history_now", "input"],
)

这里有三个变量:

  • history:来自 ConversationSummaryMemory,表示之前对话的摘要。
  • history_now:来自 ConversationBufferMemory,表示当前保留的原始对话。
  • input:本轮用户输入。

完整 Chain:

python 复制代码
chain = ConversationChain(
    llm=llm,
    memory=memory,
    prompt=prompt,
)

调用:

python 复制代码
chain.run("那ETH呢?")

这个问题本身很短,只有"那 ETH 呢?"。模型能否理解,要看 Memory 里之前是否讨论过 BTC、加密货币或投资主题。这里正好体现了 Memory 的价值:用户追问经常省略主语,必须依靠上下文补全语义。

对 Java 架构师来说,这类似客服系统里的"会话上下文 + 会话摘要"。短期窗口解决最近几轮追问,摘要解决长周期上下文。

6 给多参数问答链增加 Memory

最后一部分是更实用的 RAG 问答场景:先从文档里检索相关内容,再结合历史对话回答。

6.1 读取文档并构建向量库

python 复制代码
with open("letter.txt") as f:
    text = f.read()

    text_splitter = CharacterTextSplitter(
        chunk_size=20,
        chunk_overlap=5
    )
    texts = text_splitter.split_text(text)

    embddings = create_qwen_embeddings()

    docssearch = Chroma.from_texts(
        texts,
        embddings,
    )

    query = "公司有什么新策略?"
    docs = docssearch.similarity_search(query=query)
  1. 读取 letter.txt
  2. 使用 CharacterTextSplitter 切分文本。
  3. 使用 Qwen embedding 模型把文本转成向量。
  4. 使用 Chroma 建立向量索引。
  5. 对用户问题做相似度检索,得到相关文档片段 docs

参数说明:

  • chunk_size=20:每个文本块大约 20 个字符。
  • chunk_overlap=5:相邻文本块重叠 5 个字符,避免切分处丢失语义。
  • similarity_search(query=query):根据 query 查找最相关的文档片段。

6.2 构建带历史记忆的 QA Chain

python 复制代码
template = """下面是一段AI与人类的对话,AI会针对人类问题,提供尽可能详细的回答,如果AI不知道答案,会直接回复'人类老爷,我真的不知道',参考一下相关文档以及历史对话信息,AI会据此组织最终回答内容.
{context}
{chat_history}
Human:{human_input}
AI:"""

prompt = PromptTemplate(
    template=template,
    input_variables=["context", "chat_history", "human_input"],
)

这个 Prompt 同时包含三类信息:

  • context:向量检索得到的相关文档。
  • chat_history:历史对话。
  • human_input:本轮问题。

这就是 RAG + Memory 的基本形态:外部知识解决"模型不知道公司私有文档"的问题,Memory 解决"用户多轮追问上下文"的问题。

6.3 Memory 的 input_key 为什么要指定

python 复制代码
memory = ConversationBufferMemory(
    memory_key="chat_history",
    input_key="human_input",
    return_messages=True,
)

这个例子里 Chain 不是单输入,而是多输入:

  • input_documents
  • human_input
  • 由 Chain 内部生成的 context
  • 由 Memory 注入的 chat_history

如果不告诉 Memory 哪个字段是用户输入,它可能无法判断应该把哪个输入保存进历史。因此这里用:

python 复制代码
input_key="human_input"

表示本轮用户消息来自 human_input

memory_key="chat_history" 仍然要和 Prompt 里的 {chat_history} 对齐。

6.4 加载 QA Chain 并调用

python 复制代码
chain = load_qa_chain(
    llm=llm,
    memory=memory,
    prompt=prompt,
    verbose=True,
    chain_type="stuff"
)

chain({
    "input_documents": docs,
    "human_input": "公司的营销策略是什么?"
})

load_qa_chain 会创建一个问答链。这里 chain_type="stuff" 表示把检索到的文档内容直接塞进 Prompt 的 {context} 位置。

执行时传入两个核心参数:

  • input_documents: 上一步检索出来的文档片段。
  • human_input: 用户当前问题。

Chain 执行时会把 input_documents 转成 context,再加上 Memory 提供的 chat_history,最终生成完整 Prompt 给模型。

7 从 Java 架构视角理解这份代码

这份课件虽然是 Python notebook,但背后的架构概念对 Java 应用是通用的。

7.1 Chain 是应用服务编排

在 Java 里,我们经常写这样的服务:

text 复制代码
Controller -> Service -> Repository -> Remote API -> Response DTO

LangChain 的 Chain 可理解为:

text 复制代码
User Input -> Prompt -> Memory -> LLM -> Output

加入RAG就是:

text 复制代码
User Input -> Retriever -> Documents -> Prompt -> Memory -> LLM -> Output

它本质上也是编排,只是被编排的对象从数据库、RPC、MQ,变成了 Prompt、模型、向量库和记忆。

7.2 Memory 是会话上下文,不是业务数据库

ConversationBufferMemory 适合保存对话历史,例如:

  • 用户刚刚说自己叫什么。
  • 用户上一轮问的是 BTC,这一轮问"那 ETH 呢"。
  • 用户让助手按某个格式继续输出。

但它不适合保存强一致业务事实,例如:

  • 订单是否支付。
  • 用户会员等级。
  • 合同审批状态。
  • 库存数量。

这些仍然应该来自 Java 后端和业务数据库。Memory 只是帮助模型理解对话上下文,不应该替代业务数据源。

7.3 memory_key、input_key 是最容易出错的地方

本文多次出现 memory_keyinput_key

python 复制代码
ConversationBufferMemory(memory_key="chat_history")
ConversationBufferMemory(memory_key="history")
ConversationBufferMemory(memory_key="history_now", input_key="input")
ConversationBufferMemory(memory_key="chat_history", input_key="human_input")

判断是否正确,只看两件事:

  1. memory_key 是否和 Prompt 里的历史变量名一致。
  2. input_key 是否和本轮用户输入字段一致。

如果 Prompt 里写 {chat_history},Memory 就应该输出 chat_history。如果调用时传的是 human_input,Memory 就应该知道用户输入来自 human_input

8 代码运行时的观察重点

多处设置:

python 复制代码
verbose=True

重点观察 verbose 输出里的完整 Prompt。只要看到历史对话已经进入 Prompt,就能理解 Memory 的工作机制。

调试时重点看:

  • 本轮输入字段名是否正确。
  • Prompt 变量是否全部被填充。
  • chat_historyhistory 是否出现历史内容。
  • 多参数 QA 链里 input_documents 是否转成了 context
  • return_messages=True 是否和 MessagesPlaceholder 搭配使用。

9 总结

本文让你理解 LangChain 多轮对话的基本机制:

  • LLM 不会自动记住上一轮对话。
  • Memory 会保存历史,并在下一次调用 Chain 时注入 Prompt。
  • LLMChain 可以手动组合 Prompt、LLM、Memory。
  • ConversationChain 封装了常见对话链路。
  • ConversationBufferMemory 保存原始历史。
  • ConversationSummaryMemory 保存历史摘要。
  • CombinedMemory 可以把多份记忆合并使用。
  • 多参数链必须用 input_key 告诉 Memory 哪个字段是用户输入。
  • RAG 场景下,context 来自检索文档,chat_history 来自 Memory,两者一起帮助模型回答。
相关推荐
雮尘4 小时前
让AI更懂你:提示词工程5大框架完全指南
人工智能·llm
胡哈5 小时前
Langfuse JavaScript SDK 架构设计与实现原理
llm·aigc·agent
曲幽8 小时前
初探:用 FastAPI 搭建你的第一个 AI Agent 接口
python·ai·llm·agent·fastapi·web·chat·httpx·ollama
高木木的博客8 小时前
数字架构智能化测试平台(2)--AI DevOps测试流程框架
python·llm·fastapi·cicd
devpotato9 小时前
人工智能(十四)- 思维链(Chain of Thought, CoT)
人工智能·llm
@atweiwei10 小时前
LangChainRust Agent 引擎:Graph 构建到执行
rust·langchain·llm·agent·rag·langchaingraph
布朗克16811 小时前
大模型初步介绍:从基本概念到全球排行榜
人工智能·大模型·llm
冬奇Lab1 天前
RAG 系列(十):混合检索——让召回更全面
人工智能·llm
Mr.朱鹏1 天前
5.LangChain零基础速通-LCEL链式调用
python·langchain·django·大模型·llm·virtualenv