第 24 章 Agent 测试策略
24.1 测试金字塔与 pytest 集成
LLM 应用的非确定性使传统精确断言不再充分,需采用分层测试金字塔(自底向上):单元测试 覆盖工具函数、reducer、路由函数、prompt 构造等确定性逻辑,精确断言;节点测试 对单个节点注入 Mock LLM 输出与 Mock 工具返回,验证节点 state 更新;图集成测试 跑完整图但 Mock 外部依赖(LLM、HTTP、DB),验证路由条件、状态流转、checkpoint 写入;评估测试 在数据集上运行评估框架,使用弱断言(阈值断言,如准确率 ≥80%);在线监控与回归 在生产环境持续监控指标并触发告警。
测试栈采用 pytest + pytest-asyncio。图构建建议封装为 fixture,避免每个用例重复创建:
# tests/conftest.py
import pytest
from my_agent import build_graph
@pytest.fixture
def graph():
return build_graph()24.2 Mock 与确定性控制
Mock LLM 使用 langchain_core.language_models.fake_chat_models.FakeListChatModel 返回预设消息序列,避免消耗 API 并保证可复现;对需要 tool_calls 的场景,可直接构造 AIMessage(tool_calls=[...]) 作为 fake 响应。
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.messages import AIMessage
fake_llm = FakeListChatModel(responses=[
AIMessage(content="", tool_calls=[{"name":"get_joke","args":{"topic":"cat"},"id":"c1"}]),
AIMessage(content="为什么猫咪喜欢趴在键盘上?因为它想抢占你的关注。"),
])Mock 工具 使用 unittest.mock.patch 替换真实工具函数或 HTTP 客户端,返回预设数据或抛出异常以测试错误分支。
确定性控制 包括:测试时将 LLM temperature 设为 0、传入固定 seed;使用 VCR 模式 记录并回放 LLM 与外部 HTTP 交互(vcrpy 或 langchain 实验性 RecordAndPlay),第一次运行录制、后续运行回放,既不消耗 API 又能复现历史响应。
核心测试场景包括:
- 路由测试:给定输入 state,调用条件边函数断言返回的目标节点名;对
Command(goto=...)验证其 goto 字段。 - 状态更新测试:
graph.invoke(initial)后断言 final state 包含关键字段、值符合预期;checkpointer 场景下断言 snapshot 正确写入。 - 中断与恢复测试:使用
MemorySaver,第一次invoke命中interrupt后捕获Interrupt事件,再以Command(resume=payload)二次 invoke,断言恢复后 state 正确推进。 - 时间旅行测试:通过
graph.get_state_history(config)验证快照链完整性,graph.update_state(config, values, as_node=...)验证状态修正逻辑。 - 并发测试:
Send扇出场景断言 reducer 正确合并并行返回;多 thread_id 并发调用验证 thread 隔离,无状态串扰。 - 回归测试:保留一套 golden dataset,每次代码改动后运行,输出 diff 报告(输出变化字段、分数变化、token 变化)。
持续集成(CI)在 GitHub Actions/GitLab CI 中运行单元、节点、集成与小规模评估;评估耗时长可放到定时(nightly)任务,避免阻塞 PR。CI 环境必须提供 LLM API Key 或启用 VCR 回放,否则测试跳过会形成假绿灯。
24.3 完整示例与典型问题
下面给出一个笑话生成器图的 pytest 示例,覆盖路由、循环终止、interrupt 恢复:
# tests/test_joke_graph.py
import pytest
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command, interrupt
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class JokeState(TypedDict):
topic: str
joke: str
approved: bool
def write_joke(state: JokeState):
return {"joke": f"{state['topic']} - 笑话文本"}
def human_review(state: JokeState):
decision = interrupt({"joke": state["joke"]})
return {"approved": decision == "yes"}
def route_after_review(state: JokeState):
return "write_joke" if not state["approved"] else END
@pytest.fixture
def joke_graph():
g = StateGraph(JokeState)
g.add_node("write_joke", write_joke)
g.add_node("human_review", human_review)
g.add_edge(START, "write_joke"); g.add_edge("write_joke", "human_review")
g.add_conditional_edges("human_review", route_after_review, {"write_joke":"write_joke", END:END})
return g.compile(checkpointer=MemorySaver())
def test_interrupt_resume(joke_graph):
cfg = {"configurable": {"thread_id": "t1"}}
joke_graph.invoke({"topic": "猫"}, cfg)
state = joke_graph.get_state(cfg)
assert state.next == ("human_review",) # 中断在 review 节点后
out = joke_graph.invoke(Command(resume="yes"), cfg)
assert out["approved"] is True
assert joke_graph.get_state(cfg).next == ()本章小结:Agent 测试需采用分层金字塔,确定性层用精确断言、非确定性层用阈值与数据回归;Mock LLM/Mock 工具与 VCR 回放保证测试稳定可复现;LangGraph 特有的 interrupt、time-travel、Send 扇出必须纳入测试范围。
常见错误:单元/集成测试直接调用真实 LLM API,导致成本高、响应不稳定、CI 偶发失败;只覆盖 happy path,未对工具异常、LLM 返回截断、超时等错误分支做断言;assert 检查完整 state 导致脆弱(字段顺序或无关字段变化即失败),应仅检查关键字段;CI 未配置 API Key 时直接 skip 测试,形成假绿灯;HITL/interrupt、checkpoint、Send 等 LangGraph 特有能力未覆盖,恰恰是线上故障高发区。
生产建议:测试金字塔中单元+节点+集成必须在每次 PR 必跑,评估测试作为发布前门禁,在线监控兜底长周期回归;维护一套 VCR 录制的固定交互库,回归时优先使用回放;针对 interrupt 场景建立独立用例集覆盖 resume 不同 payload、超时、取消等分支;CI 中显式拒绝 skip 关键路径,通过 pytest --strict-markers 强制标记管理。
