【AI测试功能3】每次模型更新,跑 40 分钟测试——AI系统测试分层实战
如果你的 AI测试只有 E2E 测试,那你的测试效率会低到无法接受。
2024 年 5 月,我们团队接手了一个企业知识库问答系统。
系统基于 RAG 架构,用户上传文档后可以向 AI 提问关于文档内容的问题。
前任测试团队留下了 30 条 E2E 测试,覆盖了”上传文档 → 提问 → 获得回答”的完整流程。
听起来很全面?
执行起来是另一回事。
每次模型更新后,跑完 30 条测试要 40 分钟。
更绝望的是,某条测试失败了,你根本不知道是哪一层的问题——
-
是文档解析错了? -
是检索没找到相关文档? -
是 Prompt 拼接错了? -
还是 LLM 回答错了?
有一次,我们排查了 2 小时,最后发现是 Prompt 模板渲染出了问题——把 {context} 写成了 {contextt},少了一个 t。
这个问题本来应该由单元测试在 3 秒内 发现。
那次之后,我们彻底重构了测试体系,把 30 条 E2E 测试拆成了 295 条分层测试(200 单元 + 80 集成 + 15 E2E)。
今天这篇文章,就是那次重构的完整复盘。
为什么你的测试效率低到无法接受?
我在帮团队搭建 AI测试体系时,发现一个普遍问题:
测试工程师一上来就写 E2E 测试——从用户注册开始,模拟完整用户旅程。
听起来很全面,但执行起来问题一堆:
-
跑一次要 20 分钟 -
失败后不知道是哪一层的问题 -
维护成本极高
传统软件测试有一个经典的分层策略叫测试金字塔:底层大量单元测试,中间适量集成测试,顶层少量 E2E 测试。
这个策略在 AI 系统上同样适用,但每层的含义和比例需要调整。
传统测试金字塔 vs AI测试金字塔:关键差异
光讲概念不够直观,下面这张表格把两种金字塔的核心差异列清楚。
|
|
|
|
|---|---|---|
| 单元测试比例 |
|
|
| 集成测试比例 |
|
|
| E2E 测试比例 |
|
|
| 单元测试内容 |
|
|
| 集成测试内容 |
|
|
| E2E 测试内容 |
|
|
| Mock 策略 |
|
|
| 执行频率 |
|
|
关键结论: AI测试金字塔不是”缩小版的传统金字塔”,而是重心不同的新金字塔。集成测试的比例从 20% 提升到 35%,因为 AI 系统的核心复杂度在模块协作,不在单个函数。
用一张图来看测试金字塔的结构:
graph TD
subgraph AI测试金字塔
E2E[E2E 测试 15%<br/>完整用户旅程<br/>每次发布跑]
Integration[集成测试 35%<br/>模块间协作<br/>每次 PR 跑]
Unit[单元测试 50%<br/>单个功能点<br/>每次提交跑]
end
E2E --> Integration
Integration --> Unit
style E2E fill:#ff9999,stroke:#cc0000,color:#000
style Integration fill:#ffcc66,stroke:#cc7700,color:#000
style Unit fill:#66bb6a,stroke:#2e7d32,color:#fff
图中从上到下,代表测试数量从少到多、执行速度从慢到快、定位精度从粗到细。你的测试策略应该是:底层大量快速测试、中层适量集成测试、顶层少量 E2E 测试。
实战案例:失败定位从 2 小时降到 5 分钟
场景:某企业知识库问答系统的测试分层
前置条件: 系统基于 RAG 架构,用户上传文档后可以向 AI 提问关于文档内容的问题。团队有 5 名测试工程师。
问题: 团队一开始只写了 30 条 E2E 测试,覆盖了”上传文档 → 提问 → 获得回答”的完整流程。但问题很快暴露:每次模型更新后跑一遍要 40 分钟;某条测试失败了,排查了 2 小时才发现是 Prompt 模板渲染出了问题。
解决方案:
单元测试层(~200 条): Prompt 模板渲染测试 30 条、JSON 输出解析测试 40 条、工具参数构建测试 50 条、输入验证测试 80 条。每次代码提交自动运行,2 分钟内完成。
集成测试层(~80 条): RAG 完整链路测试 30 条(检索 → 拼接 → 生成)、工具调用链测试 25 条、多轮对话测试 25 条。每次 PR 合并前运行,10 分钟内完成。
E2E 测试层(~15 条): 核心用户旅程测试 10 条、异常场景测试 5 条。每次发布前运行,20 分钟内完成。
效果: 测试总时间从 40 分钟(只有 E2E)变成 32 分钟(分层执行),但发现的问题数量从每次 2-3 个增加到 8-10 个。更重要的是,失败定位时间从 2 小时降到 5 分钟——单元测试失败直接定位到具体函数。
代码示例:三层测试架构
下面是三层测试的典型代码结构。完整可运行版本需要 pytest 和 langchain 依赖:
import pytest
from unittest.mock import Mock, patch
from langchain.prompts import PromptTemplate
from langchain.output_parsers import JSONOutputParser
# ===<span class="wx-em-red"> 单元测试:隔离执行,Mock LLM </span>===
# 单元测试的核心是"快"和"精确定位",不依赖任何外部服务
def test_prompt_template_rendering():
"""单元测试:验证 Prompt 模板渲染"""
template = PromptTemplate(
input_variables=["role", "task"],
template="你是一个{role},请{task}"
)
result = template.format(role="翻译专家", task="翻译这段文字")
assert result <span class="wx-em-red"> "你是一个翻译专家,请翻译这段文字"
def test_json_output_parser():
"""单元测试:验证 JSON 输出解析"""
parser = JSONOutputParser()
raw = '```json\n{"answer": "hello", "confidence": 0.95}\n```'
parsed = parser.parse(raw)
assert parsed["answer"] </span> "hello"
assert parsed["confidence"] <span class="wx-em-red"> 0.95
def test_input_validator():
"""单元测试:验证输入参数校验"""
from myapp.validators import validate_query
# 正常输入
assert validate_query("公司的请假政策是什么?") </span> True
# 空输入
with pytest.raises(ValueError):
validate_query("")
# 超长输入
with pytest.raises(ValueError):
validate_query("a" * 10000)
# ===<span class="wx-em-red"> 集成测试:真实模块交互,可调用真实 LLM </span>===
# 集成测试验证模块间的协作是否正确
@pytest.fixture
def rag_pipeline():
"""集成测试用的 RAG 管道"""
from myapp.rag import RAGPipeline
return RAGPipeline(
retriever=MyRetriever(vector_db_path="./data"),
prompt_builder=MyPromptBuilder(),
llm=MyLLMClient(api_key="test-key") # 可用真实 LLM 或高质量 Mock
)
def test_rag_pipeline(rag_pipeline):
"""集成测试:验证 RAG 完整链路"""
query = "公司的请假政策是什么?"
# 1. 检索相关文档(真实检索器)
docs = rag_pipeline.retriever.search(query, top_k=3)
assert len(docs) <span class="wx-em-red"> 3
assert any("请假" in doc.content for doc in docs)
# 2. 拼接 Prompt(真实 Prompt 构建器)
prompt = rag_pipeline.prompt_builder.build(query, docs)
assert "请假政策" in prompt
# 3. 生成回答(真实 LLM 调用)
answer = rag_pipeline.llm.generate(prompt)
assert contains_keyword(answer, ["请假", "leave", "审批"])
def test_tool_call_chain():
"""集成测试:验证工具调用链"""
from myapp.agent import ToolAgent
agent = ToolAgent(tools=[search_db, calculate_score, generate_report])
result = agent.run("查询张三的销售额并生成报告")
# 验证工具调用顺序
assert result.tool_calls[0].name </span> "search_db"
assert result.tool_calls[1].name <span class="wx-em-red"> "calculate_score"
assert result.tool_calls[2].name </span> "generate_report"
# ===<span class="wx-em-red"> E2E 测试:完整用户旅程,所有组件真实交互 </span>===
# E2E 测试数量最少,但覆盖最核心的用户场景
@pytest.fixture
def e2e_client():
"""E2E 测试用的 API 客户端"""
from myapp.api import APIClient
return APIClient(base_url="http://test-server:8000")
def test_e2e_knowledge_base_qa(e2e_client):
"""E2E 测试:用户上传文档并提问"""
# 1. 用户上传文档
upload_result = e2e_client.upload_document("company_policy.pdf")
assert upload_result.status <span class="wx-em-red"> "success"
doc_id = upload_result.doc_id
# 2. 用户提问
response = e2e_client.chat(doc_id, "公司的年假有几天?")
assert response.status </span> "success"
# 3. 验证回答包含关键信息
assert "年假" in response.data["content"]
# 4. 验证回答有来源引用
assert len(response.data["sources"]) > 0
# 5. 验证多轮对话(追问)
follow_up = e2e_client.chat(doc_id, "那病假呢?", conversation_id=response.conversation_id)
assert follow_up.status == "success"
assert "病假" in follow_up.data["content"]
def test_e2e_anomaly_scenarios(e2e_client):
"""E2E 测试:异常场景"""
# 上传不支持的文件格式
with pytest.raises(Exception):
e2e_client.upload_document("malware.exe")
# 提问关于不存在的文档
with pytest.raises(Exception):
e2e_client.chat("non-existent-doc", "这是什么?")
代码说明:
单元测试层:用 unittest.mockMock 掉所有外部依赖,测试单个函数/类的正确性集成测试层:用 pytest.fixture创建测试管道,验证模块间协作,可调用真实 LLME2E 测试层:用真实 API 客户端模拟用户操作,覆盖核心场景和异常场景
7 个常见坑,踩中一个就白干
坑 1:别在单元测试里调用真实 LLM
单元测试的核心价值是”快”和”精确定位”。如果单元测试调用了真实 API,每次跑都要几秒,而且 API 波动会导致测试不稳定。用 Mock 或者固定回复。
坑 2:集成测试的 Mock 要高质量
集成测试需要真实模块交互,但 LLM 调用可以用 Mock。Mock 的质量很重要——如果 Mock 返回的格式和真实 LLM 不一样,集成测试通过了,上线照样崩。
坑 3:E2E 测试要少而精
E2E 测试数量应该是最少的(占总测试的 10-15%),但每条都要覆盖一个核心用户场景。别把 E2E 测试写成”把所有功能都点一遍”。
坑 4:分层比例不是固定的
如果你的系统工具调用很多,集成测试比例可以提高到 40%。如果你的系统主要是简单问答,单元测试比例可以降低到 30%。关键是分层思路,不是固定比例。
坑 5:测试数据要隔离
单元测试用内存数据,集成测试用测试数据库,E2E 测试用测试环境。别用生产数据跑测试,数据污染会导致测试结果失真。
坑 6:集成测试的 LLM 调用要可控
如果集成测试调用真实 LLM,设置 temperature=0 保证可复现。或者用高质量 Mock(记录真实 LLM 的输出,回放给测试用)。
坑 7:E2E 测试要包含异常场景
正常流程跑通了不代表系统没问题。上传错误文件、网络超时、LLM 服务不可用——这些异常场景才是用户投诉的重灾区。
常用工具一览
|
|
|
|
|---|---|---|
| pytest |
|
|
| unittest.mock |
|
|
| langchain |
|
|
| httpx |
|
|
| pytest-cov |
|
|
| Locust |
|
|
工具选择原则:单元测试用 Mock 隔离、集成测试用真实模块、E2E 测试用真实 API。每层用不同的工具,不要混用。
写在最后
测试分层不是”最佳实践”,是”生存必需”。没有分层,你的测试就会慢到无法接受、失败后无法定位、维护成本高到无法承受。
你现在的 AI测试体系中,单元测试、集成测试、E2E 测试的比例大概是多少?
有没有出现过”只有 E2E 测试导致排查困难”的情况?欢迎在评论区分享你的经历。
夜雨聆风