Workflow 系列(05):评测体系——三层测试结构与 Trace 追踪

为什么 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 和耗时
  • 人工确认门触发记录为事件,包含触发原因

总结

  1. 三层测试各有分工:Layer 1 用夹具验契约(秒级)、Layer 2 测数据流和路由(秒级)、Layer 3 跑完整链路(分钟级);前两层覆盖大多数问题,Layer 3 只在主链路改动时跑
  2. 指标基线是发布门控:端到端完成率、Phase 4 平均轮次、候选通过率、门触发率------4 个指标任一超阈值,本次修改需要检查
  3. Trace 把黑盒变成可查询的记录:出问题不用猜顺序,不用翻文件,直接在 Langfuse 搜索对应 Trace 看 Span 详情

欢迎访问 PrimeSkills ------ 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。

更多实用知识和有趣产品,欢迎访问我的个人主页

相关推荐
ethantan2 小时前
一篇讲解AI Agent 组成:像人一样思考的智能体
人工智能·后端·程序员
Cosolar4 小时前
vLLM 生产级部署完全指南
人工智能·后端·架构
CodePlayer竟然被占用了4 小时前
被美国政府封杀18天,Claude Fable 5 回来了——但代价是什么?
人工智能
IT_陈寒4 小时前
垃圾回收器选错了,我的Java服务内存炸了
前端·人工智能·后端
smartpi5 小时前
SmartPi GPIO 脉冲与回复语执行时序指南
人工智能
阿里云大数据AI技术5 小时前
PAI支持一键部署GLM-5.2,Coding能力比肩Claude Opus 4.8
人工智能
吾鳴5 小时前
腾讯版贾维斯(Marvis),用过就回不去了
人工智能
黄啊码5 小时前
【黄啊码】都是循环,workflow 和 Loop Engineering 有何不同?
人工智能
网易云信6 小时前
9.9 元领 3 亿 Token,这个夏天实现 AI 自由!
人工智能·aigc·产品