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 已帮你封装好的对话链。它默认用 history 和 input 这类变量组织对话。
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,
)
这段代码做了三件事:
- 自定义回答风格:尽可能详细,不知道就明确说不知道。
- 明确了历史对话位置:
{history}。 - 修改了 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)
- 读取
letter.txt。 - 使用
CharacterTextSplitter切分文本。 - 使用 Qwen embedding 模型把文本转成向量。
- 使用 Chroma 建立向量索引。
- 对用户问题做相似度检索,得到相关文档片段
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_documentshuman_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_key 和 input_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")
判断是否正确,只看两件事:
memory_key是否和 Prompt 里的历史变量名一致。input_key是否和本轮用户输入字段一致。
如果 Prompt 里写 {chat_history},Memory 就应该输出 chat_history。如果调用时传的是 human_input,Memory 就应该知道用户输入来自 human_input。
8 代码运行时的观察重点
多处设置:
python
verbose=True
重点观察 verbose 输出里的完整 Prompt。只要看到历史对话已经进入 Prompt,就能理解 Memory 的工作机制。
调试时重点看:
- 本轮输入字段名是否正确。
- Prompt 变量是否全部被填充。
chat_history或history是否出现历史内容。- 多参数 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,两者一起帮助模型回答。