把AI对话记忆存储测试从手工改成Playwright+pytest,覆盖率从20%提到96%,回归时间缩短90%

凌晨三点被电话轰醒,老板在群里贴出截图:用户问"昨天我们聊到哪儿了?",AI却像喝了孟婆汤一样回复"请重新描述您的问题"。线上事故,记忆存储丢了。研发查了半天,最后定位到一个冷门的时序竞态:消息顺序错乱导致记忆写入失败。手工测试压根没覆盖多会话交错写入的场景。那晚我边修 bug 边下决心:这套聊天记忆的 E2E 测试,必须整成自动化的,而且要能覆盖所有变态场景。

问题拆解:为什么 AI 记忆存储的测试那么难搞?

AI 对话记忆不是说简单的"存个字段",它的核心流程是:用户 A 说话 → 生成回复的同时提取记忆 → 存入持久化存储(Redis/Postgres) → 下一轮或新会话时召回记忆构建上下文。测试需要模拟:

  • 多轮对话中正确提取和更新记忆;
  • 跨会话的记忆持久化(关掉浏览器再打开还能记得);
  • 并发请求下记忆覆盖、错位;
  • 前后端交互中异步写入的时机。

手工测试通常靠人点:在聊天框发几句话,关掉标签页,再打开,看看是否记得。容易漏掉关键路径,而且耗时惊人------一次全量回归要 4 个多小时,还没人敢保证覆盖完整。我们太需要一个能模拟真实浏览器交互、断言精准、还能跑在 CI 里的自动化方案。

方案设计:Playwright + pytest,不选别的理由

我们最终选了 Playwright 作为浏览器驱动,配合 pytest 做用例管理和断言,输出 Allure 报告,整体跑在 GitHub Actions 上。

为什么没选 Selenium?慢。Selenium 需要单独安装 WebDriver,等待机制全靠隐式/显式封装,经常因为网络抖动假失败。Playwright 内置自动等待、网络拦截、trace viewer,调试体验甩几条街。Cypress 呢?它虽然开发体验好,但不支持多标签页和多浏览器并行,而跨会话测试必须开新标签或 browser context,直接被劝退。pytest 则比 Mocha/Jest 更契合 Python 技术栈,参数化、fixture 管理状态顺手。

架构上我们做了三层抽象:

  1. Page Object:封装聊天页面元素和操作(send_message, get_last_reply...)
  2. browser fixture:管理 browser context,隔离每个测试的存储状态(storageState)
  3. memory validator:一个可复用的断言工具,轮询检查记忆召回是否生效,处理异步写入。

核心实现:写出真能跑的记忆存储测试

先解决全局状态和登录态问题------用 conftest.py 提供一个持久化的认证上下文,避免每次用例重复登录。

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

@pytest.fixture(scope="session")
def browser_context() -> BrowserContext:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)  # CI 用 headless
        # 从文件加载已保存的登录态,避免重复登录,节省 8 秒/用例
        storage_state = "auth.json"
        context = browser.new_context(storage_state=storage_state)
        yield context
        context.close()
        browser.close()

第一个关键用例:跨会话记忆召回

这段代码模拟用户在一个会话中说"我叫小明",另一个会话中问"我叫什么",验证 AI 是否真的记住了名字。

python 复制代码
# test_memory_cross_session.py
from playwright.sync_api import Page, expect

def test_memory_remembered_across_sessions(browser_context):
    # 会话 1:建立记忆
    page1 = browser_context.new_page()
    page1.goto("https://chat.example.com")
    page1.fill('[data-testid="chat-input"]', "我叫小明,喜欢喝咖啡")
    page1.click('[data-testid="send-btn"]')
    # 等待回复出现,证明消息已处理
    expect(page1.locator('[data-testid="msg-bubble"]').last).to_contain_text("喜欢喝咖啡", timeout=10000)

    # 会话 2:在新 tab 中验证记忆召回
    page2 = browser_context.new_page()
    page2.goto("https://chat.example.com")
    page2.fill('[data-testid="chat-input"]', "你还记得我叫什么吗?")
    page2.click('[data-testid="send-btn"]')

    # 关键:异步记忆写入可能有延迟,不能直接断言,需要智能等待
    expect(page2.locator('[data-testid="msg-bubble"]').last).to_contain_text("小明", timeout=15000)

第二个用例:多轮更新记忆不丢失

这是最初线上出事的场景:连续快速发送消息,记忆提取是否会错乱。

python 复制代码
def test_rapid_conversation_memory_update(page: Page):
    page.goto("https://chat.example.com")
    # 模拟用户快速发送三条信息
    messages = [
        "我今年30岁",
        "我住在杭州",
        "我的猫叫豆包"
    ]
    for msg in messages:
        page.fill('[data-testid="chat-input"]', msg)
        page.click('[data-testid="send-btn"]')
        # 等待每条消息的服务器回执,避免队列堆积
        expect(page.locator('[data-testid="msg-status"]').last).to_have_text("sent", timeout=5000)

    # 用一个问题召回全部记忆
    page.fill('[data-testid="chat-input"]', "总结一下你记得关于我的所有信息")
    page.click('[data-testid="send-btn"]')
    last_reply = page.locator('[data-testid="msg-bubble"]').last
    # 断言三个关键信息都还存在
    expect(last_reply).to_contain_text("30岁")
    expect(last_reply).to_contain_text("杭州")
    expect(last_reply).to_contain_text("豆包")

踩坑记录:那些文档没教的事

坑 1:聊天框藏在 iframe 里,选择器全废

现象:一切 selector 都报 Timeout 30000ms exceeded,但手动打开 DevTools 明明能找到元素。原因:我们的聊天组件是独立部署,通过 <iframe> 嵌入主站。Playwright 的 page 对象默认只在主文档查找。解决:用 frame_locator 强行切进去。

python 复制代码
chat_frame = page.frame_locator('#chat-iframe')
chat_frame.fill('[data-testid="chat-input"]', "你好")

官方文档只演示了最常见的单 frame 场景,没提 iframe 里还有 Shadow DOM 时怎么链式定位,这花了我一整个下午。

坑 2:记忆异步写入,新会话"秒查"永远失败

测试脚本开新页面后立刻发问,AI 经常回"我还不知道你的名字"。追踪链路发现:记忆提取服务收到消息后把任务丢入了消息队列,消费者写入存储有 200~800ms 延迟。最初的 page.wait_for_timeout(2000) 硬等既慢又不稳定。后来改用 Playwright 的 expect().to_pass() 做轮询断言,超时设 15 秒,只要记忆完成写入就立刻通过,平均等待仅 400ms,再也没因时序假失败过。

坑 3:CI 中 headless 渲染与本地不一致导致截图对比失效

本地 Mac 跑得好好的视觉回归,到 GitHub Actions Ubuntu 镜像全挂。差异根源是系统字体渲染不同。后来直接放弃像素级截图对比,改为基于 DOM 文本的 to_contain_text 断言,同时保留截图仅当事后复查用,不再作为通过标准。

效果验证:数据不说谎

维度 手工测试 自动化后
单次全量回归耗时 4 小时+ 10 分钟
用例覆盖率 约 20% 96%
跨会话记忆覆盖 0 个用例 12 个用例
假失败率(flaky) 不适用 < 3%
CI 集成 GitHub Actions 每次 PR

现在每次提交代码后,自动跑 30+ 条记忆存储相关 E2E 用例,3 个月内拦截了 7 次记忆丢失/错位的回归,凌晨告警彻底消失。

直接拿去用的命令

bash 复制代码
# 克隆 test 套件,一行跑起所有记忆存储用例
git clone https://github.com/yourorg/ai-memory-e2e.git
cd ai-memory-e2e && pip install -r requirements.txt
playwright install chromium
pytest tests/ --browser chromium --headed --alluredir=./allure-results

#Python #Playwright #端到端测试 #AI测试 #自动化测试 #pytest


关于作者

一个专啃 AI 应用层硬骨头的后端/测试架构师,常年出没于凌晨的 CI 日志堆里。

GitHub: github.com/baofugege

Sponsor: github.com/sponsors/ba... --- 如果这篇文章让你今晚少熬两小时,欢迎请我喝杯咖啡

提供服务:Python 后端性能优化 / 自动化测试体系搭建 / 技术咨询,联系 Telegram @baofugege

相关推荐
kyriewen1 小时前
别再这样写TypeScript了——Code Review中最常见的8个反模式
前端·javascript·typescript
名字还没想好☜1 小时前
Next.js ‘use client‘ 到底加在哪:Server/Client Components 边界与常见报错
开发语言·前端·javascript·react·next.js
午安~婉1 小时前
GitHub Token/ GitHub Stats统计显示图异常
前端·github·vercel·github token
码云之上2 小时前
AI Agent 工程化总览篇:从 Prompt 到 Harness
前端·人工智能
2501_926978332 小时前
以说明书 DNA 为模板——完整 AGI 的结构图景
前端·人工智能·经验分享·笔记·ai写作
IT_陈寒2 小时前
Vite静态资源引用这个坑我踩得有点疼
前端·人工智能·后端
程序员黑豆3 小时前
鸿蒙应用开发:AttributeModifier 使用教程
前端·harmonyos
codeniu3 小时前
TRAE Work 实战 | 从"有个想法"到线上Demo,我全程只靠对话就完成了
前端·trae
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十):增加管理员角色组管理
前端·后端·ai编程