乐于分享
好东西不私藏

【AI测试功能3】每次模型更新,跑 40 分钟测试——AI系统测试分层实战

【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测试金字塔:关键差异

光讲概念不够直观,下面这张表格把两种金字塔的核心差异列清楚。

对比维度
传统测试金字塔
AI测试金字塔
单元测试比例
70%(逻辑确定性,可大量自动化)
50%(核心逻辑不确定性,单元测试测不了)
集成测试比例
20%(接口稳定,集成点少)
35%(Prompt/工具/RAG 协作复杂,Bug 高发区)
E2E 测试比例
10%(用户旅程稳定)
15%(需要人工抽检用户体验)
单元测试内容
业务逻辑、算法、数据处理
Prompt 模板渲染、输入验证、输出解析器
集成测试内容
API 调用、数据库操作、消息队列
Prompt 组装→LLM→输出解析、工具调用链、RAG 链路
E2E 测试内容
完整用户操作流程
完整用户旅程 + 质量人工抽检
Mock 策略
Mock 外部服务(数据库、第三方 API)
Mock LLM API(单元测试层),集成层可用真实 LLM
执行频率
单元:每次提交;集成:每次 PR;E2E:每次发布
同左,但集成测试执行更频繁(AI 系统变更多在集成层)

关键结论: 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.mock Mock 掉所有外部依赖,测试单个函数/类的正确性
  • 集成测试层:用 pytest.fixture 创建测试管道,验证模块间协作,可调用真实 LLM
  • E2E 测试层:用真实 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
Python 测试框架
全部三层(单元测试 + 集成测试 + E2E)
unittest.mock
Mock 外部依赖
单元测试(Mock LLM API、数据库)
langchain
LLM 应用框架
集成测试(Prompt 模板、输出解析器)
httpx
HTTP 客户端
E2E 测试(模拟用户 API 调用)
pytest-cov
测试覆盖率统计
单元测试(确保覆盖所有分支)
Locust
性能测试框架
E2E 层(并发用户场景压力测试)

工具选择原则:单元测试用 Mock 隔离、集成测试用真实模块、E2E 测试用真实 API。每层用不同的工具,不要混用。


写在最后

测试分层不是”最佳实践”,是”生存必需”。没有分层,你的测试就会慢到无法接受、失败后无法定位、维护成本高到无法承受。

你现在的 AI测试体系中,单元测试、集成测试、E2E 测试的比例大概是多少?

有没有出现过”只有 E2E 测试导致排查困难”的情况?欢迎在评论区分享你的经历。