基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何针对一个高度不确定的复杂 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,断言路由函数是否返回了正确的节点名称。
python
# 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 提供了 FakeListChatModelGenericFakeChatModel,可以预设 LLM 的输出,从而强制 Graph 走特定的路径。

python
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),让它根据你的标准进行评分。

python
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 专属)

  1. 依赖注入 (Dependency Injection):将 LLM 实例作为参数传递给生成 Graph 的函数。这样在测试时,可以极其方便地替换为 FakeListChatModel
  2. 获取执行轨迹 (Tracing):在集成测试中,利用 graph.stream(..., stream_mode="values") 来捕获每一个步骤的 State 快照,从而可以精准断言 Agent 是按什么顺序流转的。
  3. 使用 LangSmith 进行回归测试
    • 高度复杂的 Agent 很难在本地完全覆盖。最佳方案是使用 LangSmith
    • 在 LangSmith 中建立一个包含 50-100 个典型 User Query 的 Dataset。
    • 编写自定义的 Evaluators(如准确性、是否有幻觉)。
    • 每次修改 LangGraph 逻辑(比如改了 Prompt 或增删了 Edge)后,在 CI/CD 中通过脚本触发对该 Dataset 的全量测试,对比通过率。
  4. 隔离不可逆的工具操作:如果 Agent 有发邮件、删库等 Tool,在 E2E 测试环境务必配置沙盒 API 或进行深度 Mock,防止自动化测试造成破坏。
00:00
00:00