如何针对一个高度不确定的复杂 LangGraph Agent 编写单元测试或集成测试,以保证其核心工作流的稳定性?
针对高度不确定且复杂的 LangGraph Agent,传统的“输入 A 必须等于输出 B”的测试方法注定会失败。LLM 的非确定性、复杂的路由逻辑(Conditional Edges)以及外部工具的调用,构成了测试的三大难题。
为了保证其核心工作流的稳定性,我们需要采用分层测试策略(Testing Pyramid for Agents):解耦确定性逻辑与非确定性逻辑。
以下是针对 LangGraph Agent 编写单元测试和集成测试的系统性指南。
第一层:单元测试 (Unit Testing) —— 关注“确定性”
在这一层,我们绝对不调用真实的 LLM。目标是验证图(Graph)中的各个独立组件是否按预期工作。
1. 测试工具 (Tools)
工具通常是确定性的 Python 函数。你可以像测试普通后端代码一样测试它们。
- 方法:使用
pytest,遇到网络请求或数据库调用时使用unittest.mock.patch。
2. 测试路由逻辑 (Conditional Edges / Routers)
LangGraph 的核心复杂性在于路由。路由函数通常接收 State 并返回下一个节点的名称。建议将路由函数写成纯函数。
- 方法:构造不同的 Mock
State,断言路由函数是否返回了正确的节点名称。
# router.py
def route_after_agent(state: dict) -> str:
messages = state.get("messages", [])
last_message = messages[-1]
if last_message.tool_calls:
return "tools"
return "END"
# test_router.py
from router import route_after_agent
from langchain_core.messages import AIMessage
def test_route_to_tools():
# 模拟 LLM 决定调用工具的状态
mock_state = {"messages": [AIMessage(content="", tool_calls=[{"name": "search", "args": {"query": "LangGraph"}, "id": "1"}])]}
assert route_after_agent(mock_state) == "tools"
def test_route_to_end():
# 模拟 LLM 直接回答的状态
mock_state = {"messages": [AIMessage(content="这是一个最终答案")]}
assert route_after_agent(mock_state) == "END"
3. 测试状态更新 (State Reducers/Modifiers)
测试各个 Node 处理完毕后,是否正确地将数据合并/追加到了全局 State 中。
第二层:集成测试 (Integration Testing) —— 关注“拓扑与流转”
在这一层,我们要测试 LangGraph 的节点连接是否正确,以及状态在图中的流转是否正常。
为了消除 LLM 的不确定性,我们需要Mock LLM 的响应。
1. 使用 Fake LLM 模拟工作流
Langchain 提供了 FakeListChatModel 或 GenericFakeChatModel,可以预设 LLM 的输出,从而强制 Graph 走特定的路径。
import pytest
from langchain_core.messages import AIMessage, HumanMessage
from langchain_community.chat_models import FakeListChatModel
from my_agent.graph import create_graph # 假设这是你的建图函数
def test_full_workflow_with_tool_call():
# 1. 预设 LLM 的两轮输出:第一轮调用工具,第二轮给出最终答案
fake_responses = [
AIMessage(content="", tool_calls=[{"name": "weather_tool", "args": {"city": "Beijing"}, "id": "call_1"}]),
AIMessage(content="北京今天天气晴朗。")
]
fake_llm = FakeListChatModel(responses=fake_responses)
# 2. 将 Fake LLM 注入到你的 Graph 中 (建议图的构建函数支持依赖注入)
graph = create_graph(llm=fake_llm)
# 3. 执行 Graph
initial_state = {"messages": [HumanMessage(content="北京天气怎样?")]}
result_state = graph.invoke(initial_state)
# 4. 断言工作流的状态轨迹
messages = result_state["messages"]
assert len(messages) == 4 # Human -> AI (ToolCall) -> ToolResult -> AI (Final)
assert messages[-1].content == "北京今天天气晴朗。"
assert messages[2].name == "weather_tool" # 确认工具节点被正确执行
2. 测试 LangGraph 的断点与恢复 (Time Travel)
如果你的 Agent 包含 Human-in-the-loop(如审批环节),你需要测试中断 (interrupt_before) 和状态更新。
- 方法:配置
MemorySaver,运行图直到挂起,更新状态,然后继续运行。
第三层:端到端与评估测试 (E2E & Evaluative Testing) —— 应对“不确定性”
在这一层,我们接入真实的 LLM。由于输出是不确定的,传统的断言(assert result == "预期值")不再适用,需要采用模糊断言或 LLM-as-a-Judge(让 LLM 当裁判)。
1. 结构化与确定性特征断言
即使文本内容不确定,Agent 执行的结构通常是确定的。
- 断言最终状态中包含了必需的 key。
- 断言
ToolMessage存在(即它确实调用了工具)。 - 正则表达式匹配(例如,确保输出包含某个数字或特定格式的 JSON)。
2. LLM-as-a-Judge (使用评估模型)
编写一个测试脚本,将你 Agent 的输出丢给另一个强大的模型(如 GPT-4o 或 Claude 3.5 Sonnet),让它根据你的标准进行评分。
import pytest
from my_agent.graph import app # 真实的 Graph
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
# 评估裁判模型
judge_llm = ChatOpenAI(model="gpt-4o", temperature=0)
EVAL_PROMPT = PromptTemplate.from_template("""
你是一个严格的测试评估员。请评估 Agent 的回答是否满足用户需求。
用户提问: {query}
Agent 回答: {response}
规则:
1. 回答必须包含具体的数据。
2. 态度必须礼貌。
请仅输出 "PASS" 或 "FAIL",并附带简短理由。例如 "PASS: 包含数据且礼貌"。
""")
@pytest.mark.e2e
def test_agent_accuracy_with_llm_judge():
query = "请查询2023年公司营收并写一段总结。"
# 执行真实 Agent
result_state = app.invoke({"messages": [("user", query)]})
agent_response = result_state["messages"][-1].content
# 裁判模型评估
eval_chain = EVAL_PROMPT | judge_llm
judge_result = eval_chain.invoke({"query": query, "response": agent_response}).content
# 断言裁判的结果
assert judge_result.startswith("PASS"), f"Agent 测试失败,裁判意见: {judge_result}"
3. 语义相似度测试 (Semantic Similarity)
如果你有一个标准的参考答案,可以使用 Embedding 模型计算 Agent 输出与参考答案的余弦相似度。如果相似度 > 0.85,则认为测试通过。
最佳实践总结 (LangGraph 专属)
- 依赖注入 (Dependency Injection):将 LLM 实例作为参数传递给生成 Graph 的函数。这样在测试时,可以极其方便地替换为
FakeListChatModel。 - 获取执行轨迹 (Tracing):在集成测试中,利用
graph.stream(..., stream_mode="values")来捕获每一个步骤的State快照,从而可以精准断言 Agent 是按什么顺序流转的。 - 使用 LangSmith 进行回归测试:
- 高度复杂的 Agent 很难在本地完全覆盖。最佳方案是使用 LangSmith。
- 在 LangSmith 中建立一个包含 50-100 个典型 User Query 的 Dataset。
- 编写自定义的 Evaluators(如准确性、是否有幻觉)。
- 每次修改 LangGraph 逻辑(比如改了 Prompt 或增删了 Edge)后,在 CI/CD 中通过脚本触发对该 Dataset 的全量测试,对比通过率。
- 隔离不可逆的工具操作:如果 Agent 有发邮件、删库等 Tool,在 E2E 测试环境务必配置沙盒 API 或进行深度 Mock,防止自动化测试造成破坏。