为什么 Workflow 需要独立的评测体系
传统软件测试覆盖代码正确性。Workflow 多了两层不确定性:
- LLM 输出不确定:同一个输入,不同运行结果可能不同
- 跨步骤依赖:Phase 3 的问题可能在 Phase 7 才暴露,排查链路长
没有评测体系,Workflow 修改后只能靠完整端到端验证:慢、贵、覆盖不全。三层测试把这个问题分解。
三层评测结构
bash
Layer 3:端到端测试(Workflow 级)
从触发到完成的完整链路
测试用例:eval/cases.yaml
指标:完成率、Phase 4 平均轮次、人工门触发率
Layer 2:集成测试(Phase 级)
跨 Step 的数据流是否正确传递
跨 Phase 的路由逻辑是否正确
Layer 1:单元测试(Step 级)
每个子 Agent 的输出是否符合输出契约
不调用真实 LLM,只验证 JSON Schema
测试优先级: Layer 1 应该是最多的,运行最快,能在秒级发现契约问题。Layer 3 运行最慢、成本最高,只在修改影响主链路时跑。
Layer 1:Step 级单元测试
单元测试的目标:验证子 Agent 输出的 JSON 结构是否符合预期契约,不需要真实的 LLM 调用。
python
# tests/unit/test_phase3_output.py
import json
from pathlib import Path
def test_analysis_output_schema():
"""Phase 3 的输出文件必须符合 analysis_final.json 的 Schema"""
output = json.loads(Path("test_fixtures/phase3/analysis_final.json").read_text())
# 必填字段
assert "passed" in output
assert isinstance(output["passed"], bool)
assert "confidence" in output
assert 0.0 <= output["confidence"] <= 1.0
assert "root_cause" in output
assert isinstance(output["root_cause"], str | type(None))
assert "evidence" in output
assert isinstance(output["evidence"], list)
# 失败时的必填字段
if not output["passed"]:
assert "error" in output
assert output["error"] # 不能是空字符串
def test_fix_candidate_output_schema():
"""Phase 4 每个候选的输出文件 Schema"""
for candidate in ["candidate_a", "candidate_b", "candidate_c"]:
output_file = Path(f"test_fixtures/phase4/{candidate}.json")
if output_file.exists():
output = json.loads(output_file.read_text())
assert "passed" in output
assert "test_coverage" in output
assert isinstance(output["test_coverage"], float)
测试夹具(test fixtures): 保存真实运行的输出文件作为测试数据,覆盖成功路径和失败路径各一份。
Layer 2:集成测试
集成测试验证两类问题:
数据流测试: Phase N 的输出是否能被 Phase N+1 正确消费。
python
# tests/integration/test_phase_data_flow.py
def test_phase1_output_is_valid_phase2_input():
"""Phase 1 的 bug_info.json 格式必须满足 Phase 2 的 context_inputs 声明"""
bug_info = json.loads(Path("test_fixtures/phase1/bug_info.json").read_text())
# Phase 2 声明需要的字段
required_fields = ["summary", "stack_trace", "jira_key", "attachment_path"]
for field in required_fields:
assert field in bug_info, f"Phase 1 输出缺少 Phase 2 需要的字段: {field}"
def test_phase3_routing_logic():
"""Phase 3 完成后的路由逻辑:confidence 值决定后续行为"""
# 高置信度 → 直接进 Phase 4
high_conf = {"passed": True, "confidence": 0.97, "root_cause": "NPE in parseInput"}
assert route_after_phase3(high_conf) == "phase_4"
# 中置信度 → 触发 Gate A
mid_conf = {"passed": True, "confidence": 0.75, "root_cause": "..."}
assert route_after_phase3(mid_conf) == "gate_A"
# 低置信度 + 未超重试 → 重试 Phase 3
low_conf = {"passed": False, "confidence": 0.45}
assert route_after_phase3(low_conf, retry_count=1) == "phase_3_retry"
# 低置信度 + 已超重试 → 升级人工
assert route_after_phase3(low_conf, retry_count=3) == "human_escalation"
路由逻辑用纯 Python 函数实现,不调用 LLM,可以在毫秒级跑完所有边界条件。
Layer 3:端到端测试与指标基线
测试用例定义
yaml
# eval/cases.yaml
cases:
- id: WF-E2E-001
name: 标准路径(高置信度,一次通过)
input:
jira_key: AE-MOCK-001
bug_description: "NullPointerException in parseInput() when config=null"
expected_flow:
- phase_1: done
- phase_2: done
- phase_3: done (confidence >= 0.95)
- phase_4: done (first candidate passes, no retry)
- phase_5: done
- phase_6: done
- phase_7: done
expected_metrics:
e2e_success: true
phase4_rounds: 1
gates_triggered: []
- id: WF-E2E-002
name: 低置信度路径(触发 Gate A,人工确认后继续)
input:
jira_key: AE-MOCK-002
bug_description: "Intermittent crash, no reproducible steps"
expected_flow:
- phase_3: done (confidence < 0.95)
- gate_A: triggered
expected_metrics:
e2e_success: depends_on_human
gates_triggered: [gate_A]
- id: WF-E2E-003
name: 修复失败路径(所有候选失败,触发 Gate B)
input:
jira_key: AE-MOCK-003
expected_flow:
- phase_4: all candidates failed
- gate_B: triggered
expected_metrics:
phase4_rounds: 3
gates_triggered: [gate_B]
核心指标定义
erlang
端到端完成率 > 70%
= 全自动完成的工作流数 / 总触发数
Phase 4 平均轮次 < 1.5
= 所有工作流的 phase4_rounds 均值
(接近 1 说明修复质量好,接近 3 说明测试通过率低)
并行候选通过率 > 80%
= Phase 4 中至少 1 个候选通过的工作流比例
(低于 80% 说明根因分析质量或修复策略有问题)
确认门触发率 < 20%
= 触发了任意人工确认门的工作流比例
(高于 20% 说明 LLM 质量或输入数据质量存在问题)
回归测试方法
修改 workflow.md / templates / policy.md 之前,先用历史用例建立基线:
bash
# Step 1:修改前,用 5-10 个真实 JIRA_KEY 跑一遍,记录基线
python run_eval.py --cases eval/cases.yaml --output baseline_v1.3.json
# Step 2:修改工作流文件
# ...
# Step 3:用相同用例重新跑
python run_eval.py --cases eval/cases.yaml --output baseline_v1.4.json
# Step 4:对比 Delta
python compare_eval.py baseline_v1.3.json baseline_v1.4.json
erlang
# compare_eval.py 输出示例
Metric v1.3 v1.4 Delta
───────────────────────────────────────────
e2e_success_rate 78% 82% +4% ✓
phase4_avg_rounds 1.6 1.4 -0.2 ✓
gate_trigger_rate 18% 22% +4% ⚠️ (超过阈值)
gate_trigger_rate 上升 4% 超过了 20% 阈值,说明这次修改让某些路径更容易触发人工介入,需要检查原因再决定是否发布。
Trace 追踪
没有 Trace,Workflow 的每次运行是黑盒。出问题后只能去翻文件、对比时间戳、猜执行顺序。接入 Langfuse 后,每次运行都有完整的可视化链路。
三层 Trace 结构
python
from langfuse import Langfuse
langfuse = Langfuse()
def run_workflow(jira_key: str) -> None:
# Layer 1:工作流级 Trace(顶层)
trace = langfuse.trace(
name=f"wf-bug-e2e:{jira_key}",
input={"jira_key": jira_key},
metadata={"workflow_version": "1.3.0"}
)
for phase_id in get_pending_phases():
# Layer 2:Phase 级 Span
span = trace.span(
name=phase_id,
input={"context": get_phase_context(phase_id)}
)
result = execute_phase(phase_id)
span.end(
output={"status": result["status"], "passed": result["passed"]},
level="DEFAULT" if result["passed"] else "WARNING"
)
# 人工确认门记录为事件
if gate_triggered:
trace.event(
name="human_gate_A",
metadata={"triggered_by": "low_confidence", "value": confidence}
)
Trace 能回答的问题
css
每次工作流各 Phase 耗时多少?
→ Span 的 start/end 时间戳
Token 消耗主要集中在哪个 Phase?
→ Span 的 usage 字段
子 Agent 失败时的原始错误是什么?
→ Span 的 output.error 字段
Phase 3 的置信度分布是否在合理范围内?
→ Span 的 output.confidence 字段,跨多次运行统计
出问题时不再猜顺序、翻文件:打开 Langfuse,搜对应 Trace,直接看 Span 详情。
设计 Checklist
单元测试(Layer 1)
- 每个子 Agent 的输出有对应的 Schema 验证测试
- 测试夹具覆盖成功路径和失败路径
- 不依赖真实 LLM 调用(用保存的真实输出作为夹具)
集成测试(Layer 2)
- 每个 Phase 的输出字段与下一个 Phase 的 context_inputs 对齐
- 所有路由条件(高/中/低置信度、超时、失败)都有测试覆盖
- 路由逻辑用纯函数实现,可以毫秒级跑完
端到端测试(Layer 3)
- eval/cases.yaml 覆盖 happy path、低置信度路径、修复失败路径
- 4 个核心指标有明确阈值
- 每次发布前跑基线对比,delta 超阈值不发布
Trace 追踪
- 每次工作流运行有顶层 Trace
- 每个 Phase 有对应的 Span,记录 input/output 和耗时
- 人工确认门触发记录为事件,包含触发原因
总结
- 三层测试各有分工:Layer 1 用夹具验契约(秒级)、Layer 2 测数据流和路由(秒级)、Layer 3 跑完整链路(分钟级);前两层覆盖大多数问题,Layer 3 只在主链路改动时跑
- 指标基线是发布门控:端到端完成率、Phase 4 平均轮次、候选通过率、门触发率------4 个指标任一超阈值,本次修改需要检查
- Trace 把黑盒变成可查询的记录:出问题不用猜顺序,不用翻文件,直接在 Langfuse 搜索对应 Trace 看 Span 详情
欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页