跳转至

log-diagnosis

第5章 · Coding Agent 与代码生成 · 配套项目 chapter5/log-diagnosis

项目说明

实验 5-8:生产日志的智能诊断系统

配套《深入理解 AI Agent》第 5 章「代码作为生成式能力 —— Agent 执行日志自动分析和问题诊断」。

目的

生产环境的 Agent 会产生大量轨迹日志(trajectory)。从中识别问题、定位根因、构建回归测试成本很高。 本实验让一个诊断 Agent 自动完成这条流水线:

读轨迹集合 + 架构文档 + PRD → 定位问题、生成结构化报告 → 生成回归测试用例 → 重放框架真正执行验证 → (mock) 通过 MCP 对接 GitHub 创建 Issue。

诊断流水线

data/trajectories.jsonl  (含已知问题的生产轨迹)
data/architecture.md     (系统架构)          ┐
data/PRD.md              (产品需求)          ├─► [LLM] diagnose()      结构化问题报告(优先级/模块/描述/建议)
                                             ┘        │
                                            [LLM] gen_test_cases()   回归测试用例(引用轨迹ID+交互轮次)
                                            replay.py 重放框架  ── 对同一输入重放被测系统并断言
                                              (A) 未修复系统 → FAIL(复现bug)
                                              (B) 修复后系统 → PASS(验证修复)
                                            github_mcp.py  (mock) 渲染并打印/落盘 GitHub Issue
  • diagnoser.py:诊断 Agent,两次真实调用 OpenAI(默认 gpt-5.6-luna,JSON 模式)。
  • sut.py:被测系统的确定性仿真器fixed=False 复现线上 bug,fixed=True 模拟修复后行为。
  • replay.py:回归测试重放框架。取轨迹输入 → 重放 sut → 在新轨迹上求值断言(内置 4 种断言 DSL)。
  • github_mcp.py:GitHub Issue 创建,默认 mock(打印 + 写 output/github_issues.json)。

预置的已知问题(Agent 应能定位)

轨迹 问题 违反 PRD 定位模块
T-1001 / T-1002 退款前跳过了强制的 verify_refund_eligibility 校验 R1 (P0) order_service
T-1002 process_refund 反复失败、无退避、且最终误报成功 R2 (P0) payment_service
T-1003 check_stock 延迟 8300ms 超时未降级 R3 (P1) inventory_service
T-1004 正常轨迹(对照组,无问题)

运行

pip install -r requirements.txt
cp env.example .env      # 填入 OPENAI_API_KEY(模型默认 gpt-5.6-luna);未配置时设 OPENROUTER_API_KEY 自动改走 OpenRouter
python demo.py           # 完整流程(两次真实 LLM 调用)

demo.py 一次跑完:读轨迹 → 诊断报告 → 回归测试用例 → 重放执行(通过/失败) → (mock) GitHub Issue。

常用参数(python demo.py -h 查看全部):

  • --smoke免 API 快速自检,跳过 LLM,用内置诊断结果仅跑重放框架 + GitHub mock,验证管道是否端到端连通(全绿退出码 0)。适合无 Key 环境或 CI。
  • --model gpt-5.6:临时覆盖模型(等价于设置 OPENAI_MODEL)。
  • --data-dir DIR:换用自己的输入目录(需含 trajectories.jsonl + architecture.md + PRD.md,默认 data/)。
  • --output FILE:mock GitHub Issue 的落盘路径(默认 output/github_issues.json)。
  • --create-issue经 MCP 在真实仓库创建 Issue(需 GITHUB_TOKEN + GITHUB_REPO,见下节;缺失时自动回退 mock)。
  • --no-github:跳过步骤 4,不生成 GitHub Issue。

真实运行输出(节选)

诊断阶段,Agent 定位到全部 3 个预置问题:

[问题 1] 未进行退款资格校验
  优先级 : P0    模块: order_service    PRD: R1
  轨迹   : ['T-1001', 'T-1002']  关键轮次: [3]
[问题 2] 支付重试机制未正确实现
  优先级 : P0    模块: payment_service    PRD: R2
[问题 3] 库存查询延迟未降级处理
  优先级 : P1    模块: inventory_service    PRD: R3

回归测试用例被重放框架真正执行(先复现 bug、再验证修复):

(A) 对『线上未修复』系统重放 —— 期望复现 bug(FAIL)
    [FAIL] RT-001 (T-1001)  工具 verify_refund_eligibility 缺失
    [FAIL] RT-002 (T-1002)  process_refund 调用 3 次, 失败 3 次, 末次失败
    [FAIL] RT-003 (T-1003)  check_stock 最大延迟 8300ms, 阈值 5000ms
(B) 对『修复后』系统重放 —— 期望修复被验证(PASS)
    [PASS] RT-001 (T-1001)  工具 verify_refund_eligibility 出现
    [PASS] RT-002 (T-1002)  process_refund 调用 2 次, 失败 1 次, 末次成功
    [PASS] RT-003 (T-1003)  check_stock 最大延迟 400ms, 阈值 5000ms
  小结:复现 bug 3/3 条;修复后通过 3/3 条。

mock GitHub Issue 打印并写入 output/github_issues.json,示例:

title  : [P0][order_service] 未进行退款资格校验
labels : ['module:order_service', 'priority:critical', 'auto-diagnosis']
body   : ## 问题描述 ... ## 关联回归测试用例 - RT-001 (轨迹 T-1001 第 3 轮) ...

回归测试断言 DSL(replay 框架内置)

Agent 生成的测试用例须使用以下断言之一,框架可自动求值:

  • step_present {tool}:某工具必须出现(如强制前置校验)。
  • tool_succeeds {tool}:某工具最终成功、且不存在"多次失败后误报成功"。
  • latency_under {tool, threshold_ms}:某工具单次延迟低于阈值。
  • final_status_is {value}:任务最终状态等于给定值。

如何适配/扩展

  • 换模型:设置 OPENAI_MODEL(或 python demo.py --model <名称>)。diagnoser.py 默认 gpt-5.6-luna,均走 JSON 模式;更强模型对复杂/隐性问题更稳。
  • 换供应商:本项目用官方 openai SDK,只需再设 OPENAI_BASE_URL 指向兼容 OpenAI 接口的服务(如 Moonshot / 火山方舟 / 本地 vLLM),配合该供应商的 OPENAI_API_KEYOPENAI_MODEL 即可,无需改代码。例如:
    export OPENAI_BASE_URL=https://api.moonshot.cn/v1
    export OPENAI_API_KEY=sk-...          # 该供应商的 Key
    export OPENAI_MODEL=kimi-k3
    python demo.py
    
  • 换日志:把你自己的生产轨迹按 data/trajectories.jsonl 的结构(trajectory_id / task / task_input / turns[] / final_statusturns 内含 module/tool/input/output/status/latency_ms)落盘替换即可;同时更新 data/architecture.mddata/PRD.md 作为诊断依据。若轨迹字段不同,sut.py(重放桩)与 replay.py(断言求值)按新字段小幅调整。
  • 接入真实 GitHub MCP:见下一节,通过 GITHUB_TOKEN + GITHUB_REPO + --create-issuemock=False 接通。

接入真实 GitHub MCP(需 token,默认 mock)

本实验默认 mock,--create-issue 才会真正联网。真实创建的实现已内置在 github_mcp._create_issues_via_mcp():通过 MCP 客户端(mcp SDK,stdio)连接官方 GitHub MCP Server,逐个调用其 create_issue 工具,传入 build_issue() 生成的 title / body / labels / assignees。启用步骤:

  1. 准备一个 GitHub Personal Access Token(repo 权限),写入 .envGITHUB_TOKEN; 并设置目标仓库 GITHUB_REPO=owner/repo
  2. 确保本机可启动官方 GitHub MCP Server。默认启动命令用官方 Docker 镜像 ghcr.io/github/github-mcp-server;可用 GITHUB_MCP_COMMAND 覆盖为任意暴露 create_issue 工具的 MCP Server(token 经 GITHUB_PERSONAL_ACCESS_TOKEN 注入其环境)。
  3. pip install mcp(仅此路径需要;mock/自检不需要)。
  4. 运行 python demo.py --create-issue。缺少 GITHUB_TOKEN / GITHUB_REPO 时会打印提示并 自动回退 mock,避免误联网。

局限

  • 被测系统 sut.py确定性仿真,用于让回归测试可真正重放、给出稳定的通过/失败;真实场景下重放需对接实际系统或录制/回放的依赖桩。
  • 诊断质量取决于 LLM;gpt-5.6-luna 在本数据集能稳定定位全部 3 类预置问题(R1 校验缺失 / R2 重试误报 / R3 延迟未降级),步骤 1 诊断稳定;但它倾向把 R1「校验缺失」按 T-1001、T-1002 各报一条,故常输出 4 条问题(而非上文示例合并成的 3 条)。步骤 2 生成断言时,它稳定地为「支付重试」问题选用 final_status_is:failed(两次实跑均如此)而非上文示例的 tool_succeeds——因修复后的被测系统会重试成功(final_status=success),该断言在修复后重放中判 FAIL,使修复后通过数降为 3/4 而非满绿。此外它偶尔给 step_present/latency_under 的工具名加上模块前缀(如 order_service.verify_refund_eligibility),与重放框架按裸工具名匹配不符,会进一步压低修复后通过数(两次实跑分别得到 3/4 与 0/4)。--smoke 内置用例则确定性给出 3/3。
  • GitHub 创建默认 mock,不联网;--create-issue 才经真实 MCP Server 联网创建(需 token + repo + 可用的 GitHub MCP Server)。
  • 轨迹格式为简化示意,生产环境轨迹字段更丰富(token 用量、子 Agent 调用树等)。

源代码

demo.py

"""
demo.py —— 实验 5-8:生产日志的智能诊断系统(全流程演示)

流水线:
  读轨迹集合 + 架构 + PRD
    -> [LLM] 诊断:定位问题、结构化报告(优先级/模块/描述/建议)
    -> [LLM] 生成回归测试用例(引用轨迹ID+交互轮次)
    -> 重放框架真正执行:先复现 bug(FAIL),再验证修复(PASS)
    -> (mock) 通过 MCP 对接 GitHub 创建 Issue

运行:
  cp env.example .env && 填入 OPENAI_API_KEY
  python demo.py                 # 完整流程(两次真实 LLM 调用,GitHub 步骤默认 mock)
  python demo.py --smoke         # 快速自检:跳过 LLM,用内置用例仅跑重放+GitHub mock
  python demo.py --model gpt-5.6  # 临时切换模型
  python demo.py --create-issue  # 真正经 MCP 创建 GitHub Issue(需 GITHUB_TOKEN+GITHUB_REPO)
  python demo.py -h              # 查看全部参数

换供应商/模型:设置 OPENAI_BASE_URL + OPENAI_MODEL(见 README『如何适配/扩展』)。
"""

import argparse
import json
import os
import sys

try:
    from dotenv import load_dotenv
    load_dotenv()
except Exception:
    pass

from diagnoser import Diagnoser
import replay
import github_mcp

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data")
DEFAULT_OUTPUT = os.path.join(HERE, "output", "github_issues.json")

# --smoke 自检用的内置样例:与 LLM 在本数据集上的稳定产物一致,
# 使得无需联网/无需 API Key 即可验证 重放框架 + GitHub mock 的端到端管道。
_CANNED_PROBLEMS = [
    {"title": "未进行退款资格校验", "priority": "P0", "module": "order_service",
     "description": "退款前缺失强制的 verify_refund_eligibility 校验。", "prd_ref": "R1",
     "trajectory_ids": ["T-1001", "T-1002"], "focus_turns": [3]},
    {"title": "支付重试机制未正确实现", "priority": "P0", "module": "payment_service",
     "description": "process_refund 反复失败、无退避、且最终误报成功。", "prd_ref": "R2",
     "trajectory_ids": ["T-1002"], "focus_turns": [7]},
    {"title": "库存查询延迟未降级处理", "priority": "P1", "module": "inventory_service",
     "description": "check_stock 延迟 8300ms 超时未降级。", "prd_ref": "R3",
     "trajectory_ids": ["T-1003"], "focus_turns": [3]},
]
_CANNED_TEST_CASES = [
    {"test_id": "RT-001", "trajectory_id": "T-1001", "focus_turn": 3,
     "description": "退款前必须先做资格校验",
     "assertion": {"type": "step_present", "params": {"tool": "verify_refund_eligibility"}}},
    {"test_id": "RT-002", "trajectory_id": "T-1002", "focus_turn": 7,
     "description": "process_refund 应最终成功且无『多次失败后误报成功』",
     "assertion": {"type": "tool_succeeds", "params": {"tool": "process_refund"}}},
    {"test_id": "RT-003", "trajectory_id": "T-1003", "focus_turn": 3,
     "description": "check_stock 延迟应低于 5000ms",
     "assertion": {"type": "latency_under", "params": {"tool": "check_stock", "threshold_ms": 5000}}},
]


def _read(data_dir, name):
    with open(os.path.join(data_dir, name), "r", encoding="utf-8") as f:
        return f.read()


def _traj_path(data_dir):
    return os.path.join(data_dir, "trajectories.jsonl")


def _hr(title):
    print("\n" + "=" * 70)
    print(title)
    print("=" * 70)


def _replay_and_issues(problems, test_cases, do_github=True,
                       traj_path=None, out_path=DEFAULT_OUTPUT, create_issue=False):
    """步骤 3/4:对同一输入重放被测系统并断言,再生成 GitHub Issue(默认 mock)。

    对 fixed=False / fixed=True 各重放一次,演示同一条回归用例的
    『失败(复现bug)』与『通过(验证修复)』。返回 (复现数, 验证数)。
    """
    traj_path = traj_path or replay._DATA
    _hr("步骤 3|重放框架真正执行测试用例")
    print("(A) 对『线上未修复』系统重放 —— 期望复现 bug(FAIL)")
    buggy = replay.run_suite(test_cases, fixed=False, path=traj_path)
    for r in buggy:
        flag = "PASS" if r["passed"] else "FAIL"
        print(f"    [{flag}] {r['test_id']}  ({r.get('trajectory_id')})  {r['detail']}")

    print("\n(B) 对『修复后』系统重放 —— 期望修复被验证(PASS)")
    fixed = replay.run_suite(test_cases, fixed=True, path=traj_path)
    for r in fixed:
        flag = "PASS" if r["passed"] else "FAIL"
        print(f"    [{flag}] {r['test_id']}  ({r.get('trajectory_id')})  {r['detail']}")

    reproduced = sum(1 for r in buggy if not r["passed"])
    verified = sum(1 for r in fixed if r["passed"])
    print(f"\n  小结:复现 bug {reproduced}/{len(buggy)} 条;修复后通过 {verified}/{len(fixed)} 条。")

    if do_github:
        token, repo = os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_REPO")
        if create_issue and token and repo:
            _hr(f"步骤 4|通过 MCP 对接 GitHub 在 {repo} 真实创建 Issue")
            github_mcp.create_issues(problems, test_cases, mock=False,
                                     out_path=out_path, repo=repo, token=token)
        else:
            if create_issue:
                print("\n[提示] --create-issue 需要 GITHUB_TOKEN 与 GITHUB_REPO(owner/repo),"
                      "当前缺失,已回退到 mock。")
            _hr("步骤 4|通过 MCP 对接 GitHub 创建 Issue(mock,不联网)")
            github_mcp.create_issues(problems, test_cases, mock=True, out_path=out_path)
    return reproduced, verified


def run_smoke(data_dir=DATA, out_path=DEFAULT_OUTPUT):
    """快速自检:不调用 LLM,用内置样例仅跑 重放框架 + GitHub mock 的端到端管道。

    退出码:管道全绿(复现全部 + 验证全部)返回 0,否则返回 3。
    """
    _hr("自检模式(--smoke):跳过 LLM,用内置诊断结果验证重放+GitHub mock 管道")
    reproduced, verified = _replay_and_issues(
        _CANNED_PROBLEMS, _CANNED_TEST_CASES,
        traj_path=_traj_path(data_dir), out_path=out_path)
    n = len(_CANNED_TEST_CASES)
    ok = reproduced == n and verified == n
    print(f"\n自检结果:{'OK' if ok else 'FAILED'}(复现 {reproduced}/{n},验证 {verified}/{n})")
    return 0 if ok else 3


def run_full(model=None, do_github=True, data_dir=DATA,
             out_path=DEFAULT_OUTPUT, create_issue=False):
    """完整流程:真实调用 OpenAI 诊断并生成回归用例,再重放执行。"""
    if not (os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY")):
        print("错误:未设置 OPENAI_API_KEY(或 OPENROUTER_API_KEY 兜底),请 cp env.example .env 后填入"
              "(或用 python demo.py --smoke 免 API 自检)。")
        sys.exit(1)

    # ---------- 0. 读取输入 ----------
    architecture = _read(data_dir, "architecture.md")
    prd = _read(data_dir, "PRD.md")
    trajectories = list(replay.load_trajectories(_traj_path(data_dir)).values())
    _hr(f"步骤 0|读取输入:{len(trajectories)} 条生产轨迹 + 架构文档 + PRD")
    for t in trajectories:
        print(f"  - {t['trajectory_id']}: {t['task']}{len(t['turns'])} 轮)")

    agent = Diagnoser(model=model) if model else Diagnoser()
    print(f"  使用模型:{agent.model}")

    # ---------- 1. 诊断:定位问题 ----------
    _hr("步骤 1|Agent 诊断(真实调用 OpenAI):定位问题并生成结构化报告")
    problems = agent.diagnose(architecture, prd, trajectories)
    if not problems:
        print("未诊断出问题(异常)。")
        sys.exit(2)
    for i, p in enumerate(problems, 1):
        print(f"\n[问题 {i}] {p.get('title', '')}")
        print(f"  优先级 : {p.get('priority')}    模块: {p.get('module')}    PRD: {p.get('prd_ref')}")
        print(f"  轨迹   : {p.get('trajectory_ids')}  关键轮次: {p.get('focus_turns')}")
        print(f"  描述   : {p.get('description')}")
        print(f"  建议   : {p.get('suggestion')}")

    # ---------- 2. 生成回归测试用例 ----------
    _hr("步骤 2|Agent 生成回归测试用例(真实调用 OpenAI):引用轨迹ID + 交互轮次")
    test_cases = agent.gen_test_cases(problems)
    for tc in test_cases:
        print(f"  {tc.get('test_id')}  轨迹={tc.get('trajectory_id')} "
              f"轮次={tc.get('focus_turn')}  断言={json.dumps(tc.get('assertion'), ensure_ascii=False)}")
        print(f"      说明: {tc.get('description')}")

    # ---------- 3/4. 重放执行 + GitHub Issue(默认 mock) ----------
    _replay_and_issues(problems, test_cases, do_github=do_github,
                       traj_path=_traj_path(data_dir), out_path=out_path,
                       create_issue=create_issue)

    _hr("完成|读轨迹 -> 诊断报告 -> 回归测试用例 -> (mock) GitHub Issue 全流程跑通")


def main():
    parser = argparse.ArgumentParser(
        description="实验 5-8:生产日志的智能诊断系统(读轨迹->诊断->回归测试->GitHub Issue)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="示例:\n"
               "  python demo.py                     完整流程(需 OPENAI_API_KEY)\n"
               "  python demo.py --smoke             免 API 快速自检(仅重放+GitHub mock)\n"
               "  python demo.py --model gpt-5.6      临时切换模型\n"
               "  python demo.py --data-dir ./mine   换用自己的轨迹/架构/PRD 目录\n"
               "  python demo.py --create-issue      经 MCP 真实创建 Issue(需 GITHUB_TOKEN+GITHUB_REPO)\n"
               "换供应商:设置 OPENAI_BASE_URL + OPENAI_MODEL 环境变量。")
    parser.add_argument("--smoke", action="store_true",
                        help="快速自检:跳过 LLM,用内置样例仅跑重放框架+GitHub mock(无需 API Key)")
    parser.add_argument("--model", default=None,
                        help="临时覆盖模型(等价于设置 OPENAI_MODEL;默认 gpt-5.6-luna)")
    parser.add_argument("--data-dir", default=DATA, metavar="DIR",
                        help="输入目录:轨迹日志 trajectories.jsonl + architecture.md + PRD.md(默认 data/)")
    parser.add_argument("--output", default=DEFAULT_OUTPUT, metavar="FILE",
                        help="GitHub Issue(mock)落盘路径(默认 output/github_issues.json)")
    parser.add_argument("--create-issue", action="store_true",
                        help="经 MCP 在真实仓库创建 Issue(需 GITHUB_TOKEN 与 GITHUB_REPO;默认 mock 不联网)")
    parser.add_argument("--no-github", action="store_true",
                        help="跳过步骤 4(既不 mock 也不创建 GitHub Issue)")
    args = parser.parse_args()

    if args.smoke:
        sys.exit(run_smoke(data_dir=args.data_dir, out_path=args.output))
    run_full(model=args.model, do_github=not args.no_github,
             data_dir=args.data_dir, out_path=args.output,
             create_issue=args.create_issue)


if __name__ == "__main__":
    main()

diagnoser.py

"""
diagnoser.py —— 诊断 Agent(真实调用 OpenAI)

两个阶段,均为真实 LLM 调用:
  1) diagnose()        读轨迹集合 + 架构 + PRD -> 结构化问题报告(优先级/模块/描述/建议)
  2) gen_test_cases()  基于问题报告 -> 生成可被 replay.py 自动执行的回归测试用例

默认模型 gpt-5.6-luna,输出走 JSON 模式,尽量稳定可解析。
"""

import json
import os
from typing import Dict, Any, List

from openai import OpenAI

MODEL = os.getenv("OPENAI_MODEL", "gpt-5.6-luna")

# --- 通用 OpenRouter 兜底 ---
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"


def _map_to_openrouter_model(model: str) -> str:
    """把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
    if not model or "/" in model:
        return model or "openai/gpt-5.6-luna"
    m = model.lower()
    if m.startswith(("gpt-", "o1", "o3", "o4")):
        return "openai/" + model
    if m.startswith("claude"):
        if "haiku" in m:
            return "anthropic/claude-haiku-4.5"
        if "sonnet" in m:
            return "anthropic/claude-sonnet-4.6"
        return "anthropic/claude-opus-4.8"
    if m.startswith("gemini"):
        return "google/" + model
    return "openai/gpt-5.6-luna"

# 供 LLM 生成测试用例时使用的断言 DSL 说明(须与 replay.py 保持一致)
_ASSERTION_SPEC = """可用断言类型(assertion.type 只能取以下之一):
- "step_present"    params: {"tool": <工具名>}                 该工具必须在轨迹中出现
- "tool_succeeds"   params: {"tool": <工具名>}                 该工具最终成功且无"多次失败后误报成功"
- "latency_under"   params: {"tool": <工具名>, "threshold_ms": <整数>}  该工具单次延迟须低于阈值
- "final_status_is" params: {"value": "success"|"failed"}      任务最终状态必须等于给定值"""


class Diagnoser:
    def __init__(self, model: str = MODEL):
        # 通用 OpenRouter 兜底:无直连 key,或默认 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。
        api_key = os.getenv("OPENAI_API_KEY")
        base_url = os.getenv("OPENAI_BASE_URL")
        orkey = os.getenv("OPENROUTER_API_KEY")
        prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5")
        if prefer_or or (not api_key and orkey):
            api_key, base_url, model = orkey, OPENROUTER_BASE_URL, _map_to_openrouter_model(model)
        # timeout / max_retries:让偶发的网络/SSL 抖动自动重试,不至于整轮崩溃
        kw = {"timeout": 60.0, "max_retries": 3}
        if api_key:
            kw["api_key"] = api_key
        if base_url:
            kw["base_url"] = base_url
        self.client = OpenAI(**kw)
        self.model = model
        # 推理模型(gpt-5 / o 系列等)不接受 temperature=0。
        self._temp = (1 if any(k in (model or "").lower()
                               for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
                      else 0)

    # ---------- 阶段一:诊断 ----------
    def diagnose(self, architecture: str, prd: str,
                 trajectories: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        traj_text = json.dumps(trajectories, ensure_ascii=False, indent=2)
        system = (
            "你是资深的 Agent 系统诊断专家。给定系统架构文档、PRD 与一组生产轨迹,"
            "你要判断每条轨迹的执行流程是否符合架构与 PRD 的要求,识别问题模式、定位到具体模块,"
            "输出结构化问题报告。只报告确有证据的问题,不要臆造。"
        )
        user = f"""# 系统架构
{architecture}

# PRD
{prd}

# 生产轨迹集合(JSON)
{traj_text}

# 任务
逐条核对轨迹与 PRD/架构,找出偏离项。以 JSON 输出,结构:
{{
  "problems": [
    {{
      "title": "一句话问题标题",
      "priority": "P0|P1|P2|P3",
      "module": "涉及的模块名(取架构中的模块)",
      "description": "问题描述,引用具体轨迹与轮次作为证据",
      "suggestion": "可操作的改进建议",
      "trajectory_ids": ["涉及的轨迹ID"],
      "focus_turns": [关键交互轮次的index],
      "prd_ref": "对应的PRD条目(如 R1/R2/R3)",
      "suggested_assignee": "建议负责人(可留空)"
    }}
  ]
}}
只输出 JSON。"""
        resp = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "system", "content": system},
                      {"role": "user", "content": user}],
            response_format={"type": "json_object"},
            temperature=self._temp,
        )
        data = json.loads(resp.choices[0].message.content)
        return data.get("problems", [])

    # ---------- 阶段二:生成回归测试用例 ----------
    def gen_test_cases(self, problems: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        prob_text = json.dumps(problems, ensure_ascii=False, indent=2)
        system = (
            "你是测试工程师。基于诊断出的问题,为每个问题生成一条回归测试用例,"
            "用例引用问题轨迹 ID 与关键交互轮次,并给出一个可被自动重放框架求值的断言。"
        )
        user = f"""# 已诊断的问题
{prob_text}

# 断言 DSL
{_ASSERTION_SPEC}

# 任务
为每个问题生成 1 条回归测试用例。断言应表达"修复后系统应满足的正确行为"
(例如:退款前应出现 verify_refund_eligibility;process_refund 应最终成功;check_stock 延迟应 < 5000ms)。
以 JSON 输出:
{{
  "test_cases": [
    {{
      "test_id": "RT-001",
      "trajectory_id": "引用的问题轨迹ID",
      "focus_turn": 关键轮次index,
      "description": "该用例验证什么",
      "assertion": {{"type": "...", "params": {{...}}}}
    }}
  ]
}}
只输出 JSON。"""
        resp = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "system", "content": system},
                      {"role": "user", "content": user}],
            response_format={"type": "json_object"},
            temperature=self._temp,
        )
        data = json.loads(resp.choices[0].message.content)
        return data.get("test_cases", [])

github_mcp.py

"""
github_mcp.py —— GitHub Issue 创建(默认 mock,可选真实 MCP)

- mock(默认):把"创建 Issue"渲染成将要提交的 Issue 结构,打印并写入本地文件,
  不联网、不需要 token。
- 真实(mock=False,需 GITHUB_TOKEN + GITHUB_REPO):通过 MCP 协议连接官方
  GitHub MCP Server(stdio),调用其 `create_issue` 工具在真实仓库创建 Issue。
  出于安全,真实创建须由 demo.py 的 --create-issue 显式开启。
"""

import json
import os
import shlex
from datetime import datetime
from typing import Dict, Any, List

_OUT = os.path.join(os.path.dirname(__file__), "output", "github_issues.json")

# 官方 GitHub MCP Server 的默认启动命令(可用 GITHUB_MCP_COMMAND 覆盖)。
# 默认用 Docker 运行官方镜像;也可换成任何暴露 create_issue 工具的 MCP Server。
_DEFAULT_MCP_COMMAND = (
    "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server")

# 优先级 -> GitHub label 的映射
_PRIORITY_LABEL = {"P0": "priority:critical", "P1": "priority:high",
                   "P2": "priority:medium", "P3": "priority:low"}


def build_issue(problem: Dict[str, Any], test_cases: List[Dict[str, Any]]) -> Dict[str, Any]:
    """把一条诊断问题 + 关联回归测试用例,渲染成 GitHub Issue 结构。"""
    prio = problem.get("priority", "P2")
    module = problem.get("module", "unknown")
    related = [tc for tc in test_cases
               if tc.get("trajectory_id") in problem.get("trajectory_ids", [])]

    body_lines = [
        f"## 问题描述\n{problem.get('description', '')}",
        f"\n## 涉及模块\n`{module}`",
        f"\n## 优先级\n{prio}",
        f"\n## 改进建议\n{problem.get('suggestion', '')}",
        f"\n## 相关生产轨迹\n" + ", ".join(problem.get("trajectory_ids", []) or ["(无)"]),
    ]
    if related:
        body_lines.append("\n## 关联回归测试用例")
        for tc in related:
            body_lines.append(
                f"- `{tc.get('test_id')}` (轨迹 {tc.get('trajectory_id')} "
                f"第 {tc.get('focus_turn')} 轮): {tc.get('description', '')}")

    return {
        "title": f"[{prio}][{module}] {problem.get('title', problem.get('description', ''))[:60]}",
        "body": "\n".join(body_lines),
        "labels": [f"module:{module}", _PRIORITY_LABEL.get(prio, "priority:medium"),
                   "auto-diagnosis"],
        "assignees": [problem.get("suggested_assignee", "")] if problem.get("suggested_assignee") else [],
    }


def create_issues(problems: List[Dict[str, Any]], test_cases: List[Dict[str, Any]],
                  mock: bool = True, out_path: str = _OUT,
                  repo: str = None, token: str = None) -> List[Dict[str, Any]]:
    """为每条问题创建 Issue。

    mock=True(默认):打印 + 落盘到 out_path,不联网。
    mock=False:通过 GitHub MCP Server 在 repo(owner/repo)真实创建 Issue,需 token。
    """
    issues = [build_issue(p, test_cases) for p in problems]

    if mock:
        os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
        payload = {"created_at": datetime.now().isoformat(),
                   "mode": "mock", "issues": issues}
        with open(out_path, "w", encoding="utf-8") as f:
            json.dump(payload, f, ensure_ascii=False, indent=2)
        print(f"\n[github_mcp:mock] 已将 {len(issues)} 个 Issue 写入 {out_path}")
        for i, iss in enumerate(issues, 1):
            print(f"\n----- Mock GitHub Issue #{i} -----")
            print(f"title  : {iss['title']}")
            print(f"labels : {iss['labels']}")
            print("body   :")
            for ln in iss["body"].splitlines():
                print("  " + ln)
    else:
        if not token or not repo:
            raise RuntimeError(
                "真实创建需 GITHUB_TOKEN 与 GITHUB_REPO(owner/repo),见 README。")
        created = _create_issues_via_mcp(issues, repo=repo, token=token)
        print(f"\n[github_mcp] 通过 MCP 在 {repo} 创建了 {len(created)} 个 Issue:")
        for url in created:
            print(f"    {url}")

    return issues


def _create_issues_via_mcp(issues: List[Dict[str, Any]], repo: str, token: str) -> List[str]:
    """通过 stdio 连接官方 GitHub MCP Server,逐个调用 create_issue 工具。

    返回创建成功的 Issue URL 列表。需要已安装 `mcp` Python SDK 与可用的
    GitHub MCP Server(默认 Docker 镜像,可用 GITHUB_MCP_COMMAND 覆盖启动命令)。
    """
    import asyncio

    try:
        from mcp import ClientSession, StdioServerParameters
        from mcp.client.stdio import stdio_client
    except ImportError as e:  # pragma: no cover - 依赖缺失时给出清晰指引
        raise RuntimeError(
            "缺少 MCP 客户端:pip install mcp(并确保 GitHub MCP Server 可启动)") from e

    owner, _, name = repo.partition("/")
    if not owner or not name:
        raise RuntimeError(f"GITHUB_REPO 需形如 owner/repo,收到:{repo!r}")

    cmd = shlex.split(os.getenv("GITHUB_MCP_COMMAND", _DEFAULT_MCP_COMMAND))
    params = StdioServerParameters(
        command=cmd[0], args=cmd[1:],
        env={**os.environ, "GITHUB_PERSONAL_ACCESS_TOKEN": token})

    async def _run() -> List[str]:
        urls: List[str] = []
        async with stdio_client(params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()
                for iss in issues:
                    result = await session.call_tool("create_issue", {
                        "owner": owner, "repo": name,
                        "title": iss["title"], "body": iss["body"],
                        "labels": iss["labels"],
                        "assignees": iss["assignees"],
                    })
                    # MCP 工具返回文本内容;尽力提取 Issue URL,否则回退为原始文本。
                    text = "".join(getattr(c, "text", "") for c in result.content)
                    url = text
                    try:
                        url = json.loads(text).get("html_url", text)
                    except Exception:
                        pass
                    urls.append(url)
        return urls

    return asyncio.run(_run())

replay.py

"""
replay.py —— 回归测试重放框架

输入:Agent 生成的回归测试用例(结构化 JSON),引用轨迹 ID 与交互轮次。
过程:从原始轨迹取出该任务的输入,喂给被测系统(sut.run_task)重放,
      在重放产生的新轨迹上求值断言,给出 通过/失败。

一个测试用例的结构(Agent 需按此 DSL 生成):
{
  "test_id": "RT-001",
  "trajectory_id": "T-1001",       # 引用的问题轨迹
  "focus_turn": 3,                 # 关键交互轮次(问题所在)
  "description": "退款前必须先做资格校验",
  "assertion": {"type": "step_present", "params": {"tool": "verify_refund_eligibility"}}
}

支持的断言类型(replay 框架内置,可被自动求值):
- step_present   params.tool            某工具在轨迹中必须出现(如强制前置校验)
- tool_succeeds  params.tool            某工具最终必须成功、且不得出现连续失败后误报成功
- latency_under  params.tool, threshold_ms   某工具单次延迟必须低于阈值
- final_status_is params.value          任务最终状态必须等于给定值
"""

import json
import os
from typing import Dict, Any, List, Tuple

import sut

_DATA = os.path.join(os.path.dirname(__file__), "data", "trajectories.jsonl")


def load_trajectories(path: str = _DATA) -> Dict[str, Dict[str, Any]]:
    """读取生产轨迹集合,按 trajectory_id 索引。"""
    out = {}
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            t = json.loads(line)
            out[t["trajectory_id"]] = t
    return out


# ---------------- 断言求值器 ----------------

def _tool_turns(traj: Dict[str, Any], tool: str) -> List[Dict[str, Any]]:
    return [t for t in traj.get("turns", []) if t.get("tool") == tool]


def _eval_assertion(assertion: Dict[str, Any], traj: Dict[str, Any]) -> Tuple[bool, str]:
    a_type = assertion.get("type")
    params = assertion.get("params", {})

    if a_type == "step_present":
        tool = params.get("tool") or params.get("step")
        ok = len(_tool_turns(traj, tool)) > 0
        return ok, f"工具 {tool} {'出现' if ok else '缺失'}"

    if a_type == "tool_succeeds":
        tool = params.get("tool")
        calls = _tool_turns(traj, tool)
        if not calls:
            return False, f"{tool} 未被调用"
        n_err = sum(1 for c in calls if c.get("status") == "error")
        last_ok = calls[-1].get("status") == "success"
        # 修复标准:最终成功,且不存在"多次失败后仍误报成功"(>=2 次失败视为未正确处理)
        ok = last_ok and n_err < 2
        return ok, f"{tool} 调用 {len(calls)} 次, 失败 {n_err} 次, 末次{'成功' if last_ok else '失败'}"

    if a_type == "latency_under":
        tool = params.get("tool")
        thr = params.get("threshold_ms") or params.get("threshold")
        if thr is None:
            return False, "latency_under 断言缺失阈值设置"
        try:
            thr = float(thr)
        except (TypeError, ValueError):
            return False, f"latency_under 阈值非法: {thr!r}"
        calls = _tool_turns(traj, tool)
        if not calls:
            return False, f"{tool} 未被调用"
        latencies = []
        for c in calls:
            lat = c.get("latency_ms")
            if lat is None:
                lat = 0
            try:
                latencies.append(float(lat))
            except (TypeError, ValueError):
                latencies.append(0.0)
        worst = max(latencies) if latencies else 0.0
        ok = worst < thr
        return ok, f"{tool} 最大延迟 {worst}ms, 阈值 {thr}ms"

    if a_type == "final_status_is":
        want = params.get("value")
        ok = traj.get("final_status") == want
        return ok, f"final_status={traj.get('final_status')}, 期望={want}"

    return False, f"未知断言类型: {a_type}"


def run_test_case(tc: Dict[str, Any], trajectories: Dict[str, Any],
                  fixed: bool) -> Dict[str, Any]:
    """对单条测试用例:取原始轨迹输入 -> 重放被测系统 -> 求值断言。"""
    tid = tc.get("trajectory_id")
    src = trajectories.get(tid)
    if src is None:
        return {"test_id": tc.get("test_id"), "passed": False,
                "detail": f"引用的轨迹 {tid} 不存在"}

    replayed = sut.run_task(src["task_input"], fixed=fixed)
    passed, detail = _eval_assertion(tc.get("assertion", {}), replayed)
    return {
        "test_id": tc.get("test_id"),
        "trajectory_id": tid,
        "focus_turn": tc.get("focus_turn"),
        "passed": passed,
        "detail": detail,
        "replay_mode": "fixed" if fixed else "buggy",
    }


def run_suite(test_cases: List[Dict[str, Any]], fixed: bool,
              path: str = _DATA) -> List[Dict[str, Any]]:
    """跑完整套测试用例,返回结果列表。"""
    trajectories = load_trajectories(path)
    results = []
    for tc in test_cases:
        try:
            results.append(run_test_case(tc, trajectories, fixed))
        except Exception as e:  # 单条用例出错不影响整套
            results.append({"test_id": tc.get("test_id"), "passed": False,
                            "detail": f"用例执行异常: {e}",
                            "replay_mode": "fixed" if fixed else "buggy"})
    return results

sut.py

"""
sut.py —— System Under Test(被测系统的确定性仿真)

回归测试的核心是"用相同输入重放,断言修复后系统能产生正确行为"。
这里用一个**确定性**的仿真器来扮演线上 Agent 系统:

- run_task(task_input, fixed=False):复现线上(有 bug)的行为,
  产出的轨迹会带上和生产轨迹一致的三类已知问题。
- run_task(task_input, fixed=True):模拟"修复后"的系统,
  正确执行前置校验 / 重试退避 / 库存降级。

replay.py 会分别对 fixed=False / fixed=True 重放同一输入,
从而演示同一条回归测试用例的"失败(复现bug)"与"通过(验证修复)"。

轨迹结构与 data/trajectories.jsonl 完全一致,便于对比。
"""

from typing import Dict, Any


def run_task(task_input: Dict[str, Any], fixed: bool = False) -> Dict[str, Any]:
    """给定任务输入,确定性地跑一遍被测系统,返回一条轨迹。"""
    intent = task_input.get("intent")
    order_id = task_input.get("order_id", "UNKNOWN")
    turns = []
    idx = 0

    def add(**kw):
        nonlocal idx
        kw["index"] = idx
        idx += 1
        turns.append(kw)

    # 0. 用户输入 & 意图识别
    add(role="user", content=f"task={intent}, order={order_id}")
    add(role="assistant", module="intent_parser", content=f"意图={intent}")

    final_status = "success"

    if intent == "refund":
        # 查询订单
        add(role="tool", module="order_service", tool="query_order",
            input={"order_id": order_id},
            output={"status": task_input.get("order_status", "paid")},
            status="success", latency_ms=210)

        # R1:退款前置资格校验(仅修复版本执行)
        if fixed:
            add(role="tool", module="order_service", tool="verify_refund_eligibility",
                input={"order_id": order_id},
                output={"eligible": True}, status="success", latency_ms=120)

        # R2:支付重试 + 退避
        if task_input.get("payment_flaky") and not fixed:
            # 线上 bug:无退避,连续失败后误报成功
            for _ in range(3):
                add(role="tool", module="payment_service", tool="process_refund",
                    input={"order_id": order_id},
                    output={"error": "gateway_timeout"},
                    status="error", latency_ms=3000)
            add(role="assistant", module="payment_service",
                content="多次失败,仍按成功结束(bug)")
            final_status = "success"  # 误报成功
        elif task_input.get("payment_flaky") and fixed:
            # 修复:一次失败后带退避重试成功
            add(role="tool", module="payment_service", tool="process_refund",
                input={"order_id": order_id},
                output={"error": "gateway_timeout"}, status="error", latency_ms=1500)
            add(role="assistant", module="payment_service", content="退避 800ms 后重试")
            add(role="tool", module="payment_service", tool="process_refund",
                input={"order_id": order_id, "retry": 1},
                output={"refund_id": "R-OK"}, status="success", latency_ms=600)
        else:
            add(role="tool", module="payment_service", tool="process_refund",
                input={"order_id": order_id},
                output={"refund_id": "R-OK"}, status="success", latency_ms=540)

    elif intent == "order_status":
        add(role="tool", module="order_service", tool="query_order",
            input={"order_id": order_id},
            output={"status": "paid", "sku": task_input.get("sku")},
            status="success", latency_ms=220)

        # R3:库存查询延迟
        if task_input.get("slow_inventory") and not fixed:
            # 线上 bug:超时仍阻塞等待,不降级
            add(role="tool", module="inventory_service", tool="check_stock",
                input={"sku": task_input.get("sku")},
                output={"stock": 12}, status="success", latency_ms=8300)
        elif task_input.get("slow_inventory") and fixed:
            # 修复:超过阈值走降级路径,快速返回
            add(role="tool", module="inventory_service", tool="check_stock",
                input={"sku": task_input.get("sku"), "degraded": True},
                output={"stock": "cached:12", "degraded": True},
                status="success", latency_ms=400)
        else:
            add(role="tool", module="inventory_service", tool="check_stock",
                input={"sku": task_input.get("sku")},
                output={"stock": 5}, status="success", latency_ms=300)

    # R4:通知用户
    add(role="tool", module="notification_service", tool="notify_user",
        input={"final_status": final_status},
        output={"sent": True}, status="success", latency_ms=60)

    return {
        "trajectory_id": f"REPLAY::{order_id}::{'fixed' if fixed else 'buggy'}",
        "task_input": task_input,
        "final_status": final_status,
        "turns": turns,
    }

test_github_mcp_bare_output.py

import json
import os

import github_mcp


def test_create_issues_bare_filename(tmp_path, monkeypatch):
    """out_path 为不带目录的裸文件名时,落盘不应崩溃(dirname 为空串)。"""
    monkeypatch.chdir(tmp_path)
    problems = [{"priority": "P1", "module": "refund",
                 "title": "退款未校验", "description": "描述"}]
    issues = github_mcp.create_issues(problems, [], mock=True,
                                      out_path="issues.json")
    assert os.path.exists("issues.json")
    with open("issues.json", encoding="utf-8") as f:
        payload = json.load(f)
    assert payload["issues"] == issues


def test_create_issues_with_directory(tmp_path):
    """带目录的 out_path 保持原行为:自动创建父目录。"""
    out = tmp_path / "sub" / "issues.json"
    github_mcp.create_issues([], [], mock=True, out_path=str(out))
    assert out.exists()

test_replay_latency.py

from replay import _eval_assertion


def test_latency_under_null_latency_ms():
    assertion = {
        "type": "latency_under",
        "params": {"tool": "test_tool", "threshold": 150},
    }
    traj = {
        "turns": [
            {"tool": "test_tool", "latency_ms": None},
            {"tool": "test_tool", "latency_ms": 120},
        ]
    }
    ok, msg = _eval_assertion(assertion, traj)
    assert ok
    assert "最大延迟 120.0ms" in msg


def test_latency_under_missing_threshold():
    assertion = {"type": "latency_under", "params": {"tool": "test_tool"}}
    traj = {"turns": [{"tool": "test_tool", "latency_ms": 100}]}
    ok, msg = _eval_assertion(assertion, traj)
    assert not ok
    assert "断言缺失阈值设置" in msg

data/trajectories.jsonl

{"trajectory_id": "T-1001", "task": "用户要求对订单 A1001 退款", "task_input": {"intent": "refund", "order_id": "A1001", "order_status": "paid", "payment_flaky": false, "slow_inventory": false}, "final_status": "success", "turns": [{"index": 0, "role": "user", "content": "我要退款,订单号 A1001"}, {"index": 1, "role": "assistant", "module": "intent_parser", "content": "意图=refund, order_id=A1001"}, {"index": 2, "role": "tool", "module": "order_service", "tool": "query_order", "input": {"order_id": "A1001"}, "output": {"status": "paid", "amount": 199}, "status": "success", "latency_ms": 210}, {"index": 3, "role": "assistant", "module": "order_service", "content": "订单已支付,直接发起退款", "note": "未调用 verify_refund_eligibility"}, {"index": 4, "role": "tool", "module": "payment_service", "tool": "process_refund", "input": {"order_id": "A1001", "amount": 199}, "output": {"refund_id": "R-9001"}, "status": "success", "latency_ms": 540}, {"index": 5, "role": "tool", "module": "notification_service", "tool": "notify_user", "input": {"msg": "退款成功"}, "output": {"sent": true}, "status": "success", "latency_ms": 60}]}
{"trajectory_id": "T-1002", "task": "用户要求对订单 A1002 退款", "task_input": {"intent": "refund", "order_id": "A1002", "order_status": "paid", "payment_flaky": true, "slow_inventory": false}, "final_status": "success", "turns": [{"index": 0, "role": "user", "content": "订单 A1002 退款"}, {"index": 1, "role": "assistant", "module": "intent_parser", "content": "意图=refund, order_id=A1002"}, {"index": 2, "role": "tool", "module": "order_service", "tool": "query_order", "input": {"order_id": "A1002"}, "output": {"status": "paid", "amount": 88}, "status": "success", "latency_ms": 230}, {"index": 3, "role": "assistant", "module": "order_service", "content": "订单已支付,直接发起退款", "note": "未调用 verify_refund_eligibility"}, {"index": 4, "role": "tool", "module": "payment_service", "tool": "process_refund", "input": {"order_id": "A1002", "amount": 88}, "output": {"error": "gateway_timeout"}, "status": "error", "latency_ms": 3010}, {"index": 5, "role": "tool", "module": "payment_service", "tool": "process_refund", "input": {"order_id": "A1002", "amount": 88}, "output": {"error": "gateway_timeout"}, "status": "error", "latency_ms": 3005}, {"index": 6, "role": "tool", "module": "payment_service", "tool": "process_refund", "input": {"order_id": "A1002", "amount": 88}, "output": {"error": "gateway_timeout"}, "status": "error", "latency_ms": 3020}, {"index": 7, "role": "assistant", "module": "payment_service", "content": "重试多次失败,按成功结束", "note": "无退避;最终误报成功"}, {"index": 8, "role": "tool", "module": "notification_service", "tool": "notify_user", "input": {"msg": "退款成功"}, "output": {"sent": true}, "status": "success", "latency_ms": 55}]}
{"trajectory_id": "T-1003", "task": "用户查询订单 A1003 的库存/发货状态", "task_input": {"intent": "order_status", "order_id": "A1003", "sku": "SKU-77", "payment_flaky": false, "slow_inventory": true}, "final_status": "success", "turns": [{"index": 0, "role": "user", "content": "A1003 还有货吗,什么时候发"}, {"index": 1, "role": "assistant", "module": "intent_parser", "content": "意图=order_status, order_id=A1003"}, {"index": 2, "role": "tool", "module": "order_service", "tool": "query_order", "input": {"order_id": "A1003"}, "output": {"status": "paid", "sku": "SKU-77"}, "status": "success", "latency_ms": 240}, {"index": 3, "role": "tool", "module": "inventory_service", "tool": "check_stock", "input": {"sku": "SKU-77"}, "output": {"stock": 12}, "status": "success", "latency_ms": 8300, "note": "延迟超过 5000ms 阈值,未降级"}, {"index": 4, "role": "tool", "module": "notification_service", "tool": "notify_user", "input": {"msg": "有货,1天内发货"}, "output": {"sent": true}, "status": "success", "latency_ms": 70}]}
{"trajectory_id": "T-1004", "task": "用户查询订单 A1004 状态(正常轨迹,对照组)", "task_input": {"intent": "order_status", "order_id": "A1004", "sku": "SKU-12", "payment_flaky": false, "slow_inventory": false}, "final_status": "success", "turns": [{"index": 0, "role": "user", "content": "A1004 到哪了"}, {"index": 1, "role": "assistant", "module": "intent_parser", "content": "意图=order_status, order_id=A1004"}, {"index": 2, "role": "tool", "module": "order_service", "tool": "query_order", "input": {"order_id": "A1004"}, "output": {"status": "shipped", "sku": "SKU-12"}, "status": "success", "latency_ms": 200}, {"index": 3, "role": "tool", "module": "inventory_service", "tool": "check_stock", "input": {"sku": "SKU-12"}, "output": {"stock": 5}, "status": "success", "latency_ms": 300}, {"index": 4, "role": "tool", "module": "notification_service", "tool": "notify_user", "input": {"msg": "已发货"}, "output": {"sent": true}, "status": "success", "latency_ms": 65}]}