LLM对话记忆测试踩坑实录:手工回归30分钟,自动化后2分钟发现3个隐藏Bug

凌晨两点,企业微信闪出一条消息:"哥,ChatBot又把用户名字忘了,第五轮说自己叫'小明',第八轮就变成了'王先生'。"我一个激灵从床上弹起来,翻看对话日志------果然,记忆在第七轮某个工具调用后断掉了。这种Bug,手工回归一次要对着浏览器敲30分钟,边敲边在心里骂"上次不是修好了吗"。更恶心的是,人肉验证总会漏掉那么一两轮,测完心里还是虚。直到我用 Playwright + Pytest 搭了一套记忆一致性自动化测试,2分钟跑完核心遗忘场景,还顺手揪出3个藏在流式响应里的隐藏问题。今天就跟你聊聊这个"下班拯救者"是怎么炼成的。

问题拆解:为什么LLM记忆测试让测试同学生无可恋

大模型应用里的记忆,不是简单的存个变量,而是把历史对话塞进Prompt,让LLM基于上下文回答。常见的实现比如LangChain的ConversationBufferMemoryConversationSummaryMemory,或者自研的滑动窗口。一致性测试的本质,就是在多轮交互后,验证LLM是否还记得之前明确过的信息

典型痛点:

  • 回归耗时到肉疼:一个完整的长对话场景(比如用户注册后聊20轮,中间穿插澄清、工具调用,再回头问名字),纯手工走一遍至少5-10分钟,10个场景就是一个小时。
  • 断言靠人眼,漏了白漏:第18轮回复里是否还带着"你的生日是5月20日"?盯久了谁都会走神,更别说流式输出的文字是一点点蹦出来的。
  • 常规接口测试不够:直接调LLM API的文字补全,没法覆盖前端渲染、网络抖动、流式中断等真实环境下才会暴露的毛病。

这就逼着我们不得不上端到端自动化,把浏览器操作、真实对话流、断言一把梭。

方案设计:为什么不选Selenium/ Cypress

我们技术栈是Python后端+React前端,自动化测试最好能让后端同学也能随手写、随手维护。对比下来:

  • Selenium :老牌,但API不够现代,等待机制需要手动写WebDriverWait,碰到流式渲染经常抽风,而且启动慢,不适合频繁回归。
  • Cypress:很棒,但JavaScript生态,对我们全栈Python团队来说,要多维护一套Node环境,学习成本陡增,而且对多页面、跨域的处理没Playwright灵活。
  • Playwright:原生支持async/await,auto-wait智能等待,自带trace viewer,还能直接拦截网络请求Mock API。配合Pytest的fixture和参数化,不同记忆场景可以写成独立用例,一键并发跑。

架构思路很直白:一个conftest.py负责初始化浏览器、封装"发送消息并等待回复完成"的工具函数;测试文件里每个函数就是一个记忆场景,模拟N轮带干扰的对话后,用expect().to_contain_text()断言LLM回复里保留着关键信息。另外,为了对抗前端频繁改版,我们强制要求前端同学给关键元素加上data-testid属性,让选择器不再脆弱。

核心实现:从打开浏览器到断言记忆

下面的代码分两层:公共fixture conftest.py,和测试用例 test_memory.py

这段代码解决:共享的浏览器和页面工具,让每个测试用例不用重复写启动逻辑。

python 复制代码
# conftest.py
import pytest
from playwright.sync_api import sync_playwright, Page, expect

@pytest.fixture(scope="session")
def browser():
    """
    session级别,整个测试会话只启动一次浏览器。
    """
    with sync_playwright() as p:
        # headless=False 能看到界面,便于调试;CI上换成 True
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    """每个测试用例独立的页面上下文,互不干扰"""
    context = browser.new_context(locale="zh-CN")
    page = context.new_page()
    yield page
    context.close()

@pytest.fixture
def chat(page):
    """
    封装对话操作:发送一条消息,并等待本次回复完整出现在页面上。
    返回本次AI回复的完整文本内容。
    """
    def _send_and_get_reply(message: str, timeout: int = 15000) -> str:
        # 输入框
        input_box = page.locator('[data-testid="chat-input"]')
        input_box.fill(message)
        # 发送按钮
        page.locator('[data-testid="send-btn"]').click()

        # 等待最后一条AI消息出现且不再变化------这里用独特的"消息气泡 + 完成图标"组合来保证流式输出结束
        # 前端约定:流式输出完成时,消息气泡内会多出一个 [data-testid="typing-indicator"],消失即完成
        last_msg = page.locator(
            '[data-testid="message-bot"] >> nth=-1'
        )
        # auto-wait:先等到元素可见
        expect(last_msg).to_be_visible(timeout=timeout)
        # 等待打字指示器消失,表示流式结束
        typing_indicator = last_msg.locator('[data-testid="typing-indicator"]')
        expect(typing_indicator).to_be_hidden(timeout=timeout)
        # 返回完整的文本内容
        return last_msg.text_content()

    return _send_and_get_reply

这段代码解决:具体的用户名字记忆场景,模拟多轮干扰后,断言记忆没有丢失。

python 复制代码
# test_memory.py
import pytest
import re

def test_user_name_persists_after_long_turn(page, chat):
    """
    场景:用户告知姓名,经过10轮无关对话和一次多工具调用后,
    再次询问自己的名字,系统应准确回忆。
    """
    page.goto("https://your-app.com/chat")
    
    # 第1轮:设定名字
    reply = chat("你好,我的名字叫王小明")
    assert "王小明" in reply, f"首次回复未包含名字: {reply}"
    
    # 第2-6轮:穿插各种无关对话,模拟自然聊天和可能的记忆挤压
    chat("今天天气怎么样?")
    chat("帮我订一张去北京的机票")
    reply = chat("可以推荐几个北京的景点吗?")
    # 这里复用reply断言景点包含长城,确保流程正常
    assert "长城" in reply or "故宫" in reply, "景点推荐未正常返回"
    
    chat("你能讲个笑话吗?")
    chat("谢谢,再帮我查下北京明天下雨吗")
    
    # 第7轮:直接询问名字,验证记忆
    reply = chat("对了,你还记得我叫什么名字吗?")
    # 核心断言:回复中必须同时出现"王小明"和与名字相关的提示词,避免LLM绕开问题说"您没告诉我"
    assert "王小明" in reply, f"姓名记忆丢失,回复内容: {reply}"
    # 增加宽松正则,防止模型说"您的名字是王小明"等变体,但不降低准确度
    assert re.search(r"(名字|叫|姓名)", reply), f"回复未涉及名字确认: {reply}"
    
    # 第8-10轮:进一步干扰后再次验证
    chat("我有点饿了,附近有餐厅吗?")
    chat("算了,直接帮我订一个酒店")
    reply = chat("我的名字,你还没忘吧?")
    assert "王小明" in reply, f"长时间记忆丢失,最终回复: {reply}"

你可以像这样继续增加偏好记忆、生日记忆等场景,利用Pytest的@pytest.mark.parametrize轻松覆盖更多数据。

踩坑记录:官方文档不会告诉你的三个坑

坑1:流式输出导致"假装断言成功,实则内容不全"

  • 现象 :断言明明通过了,日志却显示AI回复只有半截。原来Playwright的to_be_visible()对已经部分渲染的元素会立即返回True,但文本可能还在持续更新。
  • 原因 :我们的前端在流式输出时,消息气泡会先出现,文字逐字填入,最终才隐藏typing-indicator。如果只等气泡可见,可能捕获到流式进行中的不完整文本。
  • 解决 :双重等待------先等消息气泡visible,再等typing-indicator隐藏(如上面fixture实现)。这是对接前端规范后的自救,如果没有这么一个标记,就需要用expect(page.locator).to_contain_text(expected_partial, timeout=...),但注意text仍可能断在半截,最好抓取textContent()后在整个用例层面做延迟重试。

坑2:LLM回复的非确定性让==断言变成炸弹

  • 现象:昨晚通过的测试,今早挂了,因为模型把"您的名字是王小明"改成了"您告诉过我您叫王小明"。
  • 原因:大模型的输出本质是概率采样,相同Prompt下文字可能有轻微变化。直接字符串相等断言是测试中的"玻璃腿"。
  • 解决 :永远不要==,改用in + 关键词组合(例如"王小明"和"名字"同时出现)。更稳定的方案是用一个验证函数,通过几个必需关键信息(姓名、身份代词等)的共现来判定,甚至可以用一个小型的文本相似度模型,但对CI流水线太重,不推荐。

坑3:选择器写成div:nth-child(3) > span,维护起来想砸电脑

  • 现象:前端同学重构了消息列表的DOM结构,所有测试全红一片。
  • 原因:用Playwright录制生成的自动选择器脆弱如纸。
  • 解决 :强迫前端在关键交互元素上加data-testid,比如输入框、发送按钮、每条AI消息容器。这是我们用血的教训换来的,没有这些稳定锚点,E2E测试就是自我安慰。

效果验证:数据说明一切

指标 手工回归(10个记忆场景) Playwright自动化
单次回归耗时 约30分钟 约2分钟(并行跑)
覆盖完整度 依赖注意力,常漏断 每个场景精确断言,不漏
可重复性 下班后不想跑 随时跑,凌晨自动跑
隐藏bug发现 0(手工难发现流式Bug) 3个(流式中断记忆丢失、切换模型版本记忆错乱、前端渲染记忆信息缺失)

自动化不仅把我从30分钟的重复劳动里解放出来,还真正抓到了3个靠人眼根本揪不出来的Bug------比如流式输出被网络波动截断时,记忆信息只渲染了一半,而后端仍认为已发送完毕。

可直接拿去用的工具

最小化起步,把你的URL替换进上面的conftest.pytest_memory.py,然后一行命令开跑:

bash 复制代码
pytest test_memory.py --headed --base-url https://your-app.com

如果前端还没有data-testid,马上拉上前端把这份规范加进开发流程------比任何自动化脚本都值钱。


#Python #LLM #测试自动化 #Playwright #Pytest

关于作者 一个在LLM应用落地和自动化测试上反复折腾的后端开发,坚信"能自动的绝不手动"。

GitHub: github.com/baofugege (这里有更多LLM测试小工具)

Sponsor: github.com/sponsors/ba... --- 如果这篇文章帮你的团队省下了测试时间,欢迎请我喝杯咖啡。

提供服务:Python后端性能优化 / LLM应用E2E测试定制 / 技术咨询,联系 Telegram @baofugege

相关推荐
Ashley的成长之路2 小时前
前端性能优化实战手册·第2篇:资源加载策略全解
前端·性能优化·资源加载·http/3·性能优化实战·资源加载优化
浅水壁虎2 小时前
vue基础(第二章 )
前端·javascript·vue.js
界面开发小八哥2 小时前
界面控件DevExtreme v26.1新版亮点——支持Angular 22
前端·javascript·angular.js·devexpress·ui开发·devextreme
Getflare3 小时前
前端 + UI 设计 + AI:这不是三个工种,是一个新三角能力模型(附自检清单)
前端·人工智能·ui
oil欧哟3 小时前
我做了一个 Vibe Coding 术语学习站:VibeHub
前端·ai·agent·独立开发·vibe coding
审小匠OpenCPAi3 小时前
银行流水核查怎么自动化?单边匹配、双向勾稽与图聚类异常检测的工程对比
java·前端·人工智能·python·审计
Revolution614 小时前
一个公共表格组件,是怎么一步步失控的
前端·前端工程化
腻害兔4 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程