凌晨一点,客户群里炸了------我们的 AI 客服 Agent 突然"失忆",用户刚说完自己的订单号,下一轮它就反问:「请问您的订单号是多少?」。层层排查后发现,是记忆存储逻辑在一次重构中被误改。那时我就下定决心:记忆必须被测试。万万没想到,用 Pytest 测试 LangChain 的记忆组件,竟让我又搭进去一整个下午。这篇文章就记录两个真实的大坑,让你别再掉进去。
问题拆解:记忆测试到底难在哪
AI Agent 的对话连贯性全看记忆。LangChain 内置了 ConversationBufferMemory、ConversationSummaryMemory 等一堆实现,但大多数时候大家只通过 Chain 间接使用,出了问题就靠手动跑几段对话来验证。手动测试有两个致命伤:
- 重复成本高:每次改完代码,你得从零模拟多轮对话,肉眼比对回复是否包含历史信息。
- 状态残留:记忆是有状态的,上一轮测试的对话会污染下一轮,你永远无法确定失败是因为记忆逻辑挂了,还是因为你上次没清干净。
我们真正需要的,是一套可重复、自动断言的测试方案,能直接验证记忆组件在各种对话序列下的输出,而不是把整个 Chain 拉起来做端到端侥幸检查。
方案设计:为什么不测 Chain,只测 Memory
我选择了 Pytest + LangChain Memory 接口 的组合,核心思路是隔离测试记忆对象本身。
-
为什么选 Pytest?
pytest.mark.parametrize太适合对不同记忆类型做同一套测试;fixture能优雅管理记忆实例的生命周期,避免状态污染。 -
为什么直接测 Memory 而不是整条 Chain?
Chain 里还包着 LLM,调用慢且不稳定。记忆本质就是一个"从对话历史提取上下文"的纯逻辑模块,用单元测试验证它的
load_memory_variables输出完全足够。只要它对了,再加上 LLM 的 Mock,上层就不会崩。 -
为什么不选 Unittest?
我要频繁创建、销毁记忆实例,Pytest 的 fixture 机制比
setUp/tearDown更灵活,而且参数化可以一键覆盖 6 种记忆类型,省去大量重复代码。
核心实现:从 0 搭建记忆自动化测试
1. 测试 ConversaionBufferMemory:验证"记住"了什么
这段代码测试最基础的缓冲记忆,模拟用户说了一句话,然后断言记忆返回的上下文是否包含这句话。
python
import pytest
from langchain.memory import ConversationBufferMemory
# fixture 保证每个测试拿到全新的记忆实例
@pytest.fixture
def buffer_memory():
# return_messages=False 让 load_memory_variables 返回纯字符串,方便断言
return ConversationBufferMemory(return_messages=False)
def test_buffer_memory_remembers_input(buffer_memory):
"""一轮对话后,记忆应包含用户输入"""
# 模拟一轮完整的对话上下文
buffer_memory.save_context(
{"input": "我的订单号是ABC123"},
{"output": "好的,已为您查询到订单信息"}
)
# 提取记忆变量
memory_vars = buffer_memory.load_memory_variables({})
# 断言历史字符串包含用户原话
assert "ABC123" in memory_vars["history"]
assert "Human: 我的订单号是ABC123" in memory_vars["history"]
2. 测试 ConversationSummaryMemory:验证摘要是否生成
摘要记忆依赖 LLM 自动压缩历史,通常我们 Mock 掉 LLM 来保证速度。这里用 LangChain 的 FakeListLLM 返回固定摘要。
python
from langchain.memory import ConversationSummaryMemory
from langchain.llms.fake import FakeListLLM
@pytest.fixture
def summary_memory():
# Fake LLM 直接返回预设摘要,不依赖真实模型调用
llm = FakeListLLM(responses=["用户询问了订单ABC123的情况"])
return ConversationSummaryMemory(llm=llm, return_messages=False)
def test_summary_memory_returns_fake_summary(summary_memory):
summary_memory.save_context(
{"input": "我的订单号是ABC123"},
{"output": "查询中..."}
)
memory_vars = summary_memory.load_memory_variables({})
# 摘要应为 Fake LLM 返回的内容
assert "用户询问了订单ABC123的情况" in memory_vars["history"]
这样,我们可以在毫秒级完成测试,完全不用调 OpenAI API。
踩坑记录:折磨我 4 小时的两个坑
坑 1:return_messages=True 让字符串断言全部瘫痪
现象 :
我把 ConversationBufferMemory 的 return_messages 设为 True,然后自信地写下一堆 assert "某个词" in memory_vars["history"]。测试跑起来,全红。打印 memory_vars["history"] 一看,显示的是 [HumanMessage(content='...'), AIMessage(content='...')] 对象列表,而不是字符串。
原因 :
return_messages=True 会将内存中的对话以 LangChain 消息对象(如 HumanMessage、AIMessage)的形式返回,不再是拼接好的文本。而 load_memory_variables 文档里对返回类型的说明只有一句话,很容易忽略。官方示例里,大多数情况用的都是默认 False,一旦你需要消息对象做更细粒度的断言,就很容易踩这个类型陷阱。
解决 :
统一测试策略------如果只是验证历史是否包含某些关键词,全部用 return_messages=False。如果确实需要消息对象,则改用属性断言:
python
messages = memory_vars["history"]
assert any("ABC123" in msg.content for msg in messages)
坑 2:Pytest fixture 的"半共享"状态导致记忆泄露
现象 :
我为多个测试编写了同一个 buffer_memory fixture,scope 用的是默认的 function,按理每个测试都应该拿到新实例。结果 test_A 执行后,test_B 却能记住 test_A 的对话历史,断言上下文长度时就崩了。
原因 :
排查半天发现,我在 fixture 里用了一个模块级的字典缓存,本意是为了避免重复创建 LLM 实例,结果随手把 memory 实例也放进了那个字典:
python
_memory_cache = {}
def get_memory():
if "buffer" not in _memory_cache:
_memory_cache["buffer"] = ConversationBufferMemory()
return _memory_cache["buffer"] # ❌ 永远返回同一个对象
fixture 虽然每次被调用,但内部逻辑却返回了单例对象,导致多个测试共享同一个 memory,历史消息越积越多。
解决 :
删掉缓存,或者只缓存无状态组件(如 LLM 配置)。如果确实需要复用 LLM,可以单独用一个 session 级别的 fixture 提供 LLM,而 memory fixture 每次重新构造:
python
@pytest.fixture(scope="session")
def llm():
return FakeListLLM(responses=["fake"])
@pytest.fixture # scope="function" 默认
def memory(llm):
return ConversationSummaryMemory(llm=llm) # 每次都新对象
官方文档没强调的一个点是:LangChain 的 Memory 对象内部持有 chat_memory,它是非线程安全的列表,测试里必须严格隔离。
效果验证:从 5 分钟到 2 秒
自动化测试落地后,我对 6 种记忆组件一共覆盖了 32 个用例,包括空历史、多轮追加、摘要阈值触发、token 限制裁剪等场景。结果如下:
| 指标 | 手动测试 | 自动化测试 |
|---|---|---|
| 单轮回归耗时 | ≈5 分钟 | 2.3 秒(全部) |
| 覆盖的记忆类型 | 2 种(顺手测) | 6 种 |
| 发现的隐藏 Bug | 0 | 3 个 |
| CI 集成 | ❌ 不可能 | ✅ 已集成 |
现在每次提交代码,Github Actions 都会自动跑这 32 个测试,再也没出现过记忆悄悄失灵的问题。
可直接用的代码
下面是一个开箱即用的记忆测试 fixture 模板,替换 memory_class 和预期值即可套用到你的项目里:
python
import pytest
from langchain.memory import ConversationBufferMemory, ConversationSummaryMemory
from langchain.llms.fake import FakeListLLM
@pytest.fixture(params=[ConversationBufferMemory, ConversationSummaryMemory])
def memory(request):
if request.param == ConversationSummaryMemory:
return ConversationSummaryMemory(llm=FakeListLLM(responses=["fake summary"]))
return request.param()
关于作者
我是宝富,一个专啃后端硬骨头的实战派架构师,喜欢用自动化测试给项目上保险。
GitHub:github.com/baofugege --- 后续会把 AI Agent 测试模板整理成开源工具。
Sponsor:github.com/sponsors/ba... --- 如果这篇文章帮你省下 4 小时,不妨请我喝杯咖啡。
提供服务:Python 后端性能优化 / 工具定制 / 技术咨询,联系 Telegram @baofugege
#Python #LangChain #AI Agent #自动化测试 #Pytest