第四个实战最 hairy:用 Graph 工作流引导 Agent 重构一个多模块(≥30 文件、≥5000 行)的中型项目。这种任务单 ReAct loop 撑不下来——决定要做什么、做哪部分、什么时候跳、什么时候熔断,全靠 Graph。

28.1 任务定义

一个老 Python 项目:

  • 30+ 文件,5000+ LOC
  • 没有类型注解,没有 unit test 覆盖(5%)
  • pyproject.toml 里没 ruff
  • 三个 orchestrator 文件 (main, cli.py, web_server.py) 错综耦合

重构目标(一季):

  1. 引入类型注解 + basedpyright 严格
  2. 单测覆盖率 70%+
  3. 三 orchestrator 拆出公共 lib

28.2 为什么单 Agent Loop 不行

一次 Claude Code session 用 ReAct loop 跑:

Step 1: 想想 - "拆 main 和 cli" 大致步骤
Step 2-30: 边读边改文件 30 个 → context soon 80k+ → 压缩
Step 31-50: 压缩了早期 notes, 起初约束忘
Step 51: refactor 30 文件已混乱,report "DONE but lots of TODO"
Step 80-200: 又迭代一次,实际上模型忘了第 3 步的架构
budget 到,模型默认 "DONE", 实际未完成 80%

Graph + Looper 优势:把项目拆成 30 个 node, 每个 node 限 scope, progressive cap, 不烧枚举上下文。

28.3 Graph 设计

digraph refactor {
  rankdir=LR;
  start [shape=doublecircle];

  // Phase 1: assessment
  start -> scan[codeowners]
  scan -> inventory[import_graph]
  inventory -> typeset[typing_baseline]
  typeset -> plan[plan_remediation]
  plan -> kickoff [label="user approve spec", color=green]

  // Phase 2: per-module (loop)
  kickoff -> next [shape=diamond];
  next -> mod[m: refactor_one_module]
  mod -> verify[m: verify_module_graph]
  verify -> next [label="pass"]
  verify -> remediate [label="fail"]
  remediate -> mod
  next -> done_phase2 [label="all done"]
  verify -> done_phase2

  // Phase 3: orchestrate fusion
  done_phase2 -> fuse[fuse_orchestrator]
  fuse -> end [shape=doublecircle]
}

四个关键节点:

  • scan / inventory / typeset:建立 ground truth
  • plan_remediation:用 oracle (expensive) 设计方案,用户 review approval
  • refactor_one_module + verify_module_graph:每个模块独立 loop
  • fuse_orchestrator:三 orchestrator 拆出 lib

28.4 节点彼此协作的 state

from typing import TypedDict, NotRequired

class RefactorState(TypedDict):
    modules: list[str]                 # 从 inventory 来
    typing_baseline: dict[str,int]     # 模名 -> 错误数
    plan_md: str                       # oracle 产出的 plan
    approved: bool                     # user 是否同意
    cur_module: str | None             # current node 因子
    completed: list[str]
    failures: list[tuple[str,str,str]] # (module,file,why)
    final_report: str

State 用 TypedDict 不用 dict,让 model 写测试时类型稳。

28.5 node_refactor_one_module 最棘手

async def node_refactor_one_module(state: RefactorState) -> RefactorState:
    module_path = state["cur_module"]
    if module_path in state["completed"]:
        return state

    logger.info(f"着手中: {module_path}")

    files_under_module = list(Path(module_path).rglob("*.py"))
    # Splitting 秀: 单 Atmospheric File ≥ 250 lines
    for f in files_under_module:
        if num_loc(f) >= 250:
            split_plan = await oracle_decide_split(f)
            # oracle 帮我们决定 split plan
        # 否则单文件作为 unit of work

    # 用 sub build 同时 refactor 多文件
    parallel_prs = asyncio.gather(*[
        OpenCode_subagent(subagent_type="build",
            prompt=f"""
[CONTEXT]
project root: {project_root}
module: {module_path}
target file: {f.path}

[GOAL]
为该文件加完整 type annot, 拆长函数 (< 80 行 / fn)

[DOWNSTREAM]
verify_node 会跑 basedpyright, 失败要这文件回退到 pass

[REQUIRED TOOLS]
read, edit, lsp

[MUST DO]
1. read 文件
2. read 相关 importers 看是否有 API 依赖
3. 加 type, 不改行为(except for shrink)
4. 运行 ruff (black + isort + unused import)
5. 输出 1-3 行 diff summary

[MUST NOT DO]
- 不跨模块改 file
- 不替代 inline impl
- 不引入新依赖
""",
        )
        for f in files_under_module
    ])

    await parallel_prs
    state["completed"].append(module_path)
    return state

28.6 verify_node:Graph 的最终熔断

async def node_verify_mod_graph(state: RefactorState) -> RefactorState:
    mod = state["cur_module"]
    # 1 类型校验
    type_result = subprocess.run(["basedpyright", mod],
                                  capture_output=True, text=True)
    if type_result.returncode != 0:
        state["failures"].append((mod, type_result.stdout[:200]))
        return replace(state, cur_module=None)  # 路由回 remediate

    # 2 unit test
    test_result = subprocess.run(["pytest", module_to_test_dir(mod), "-x", "-q"],
                                  capture_output=True, text=True)
    if test_result.returncode != 0:
        state["failures"].append((mod, test_result.stdout[:200]))
        return replace(state, cur_module=None)

    return state

def route_after_verify(state: RefactorState) -> str:
    if any(f[0] == state["cur_module"] for f in state["failures"]):
        return "remediate"
    return "next"

28.7 Plan 阶段用 oracle 进行架构决策

async def node_plan(state: RefactorState) -> RefactorState:
    inventory_dump = serialize_dict_diff(state, only_top_keys=True)
    plan_oracle = await OpenCode_subagent(
        subagent_type="oracle",
        prompt=f"""
[CONTEXT]
这是项目 inventory + typing baseline + module import graph:

{inventory_dump}

[GOAL]
设计一份 30 天重构 plan spec.md

[CRITICAL DECISIONS]
1. 哪个模块先重构? (lowest coupling-first)
2. 三个 orchestrator (main, cli, web_server) 拆成新 lib 名什么?
3. 哪个模块跳过(因 legacy 太久 / 未来替换)? (限 ≥ 1 个)
4. test framework 用什么? (pytest / pytest-xdist 建议为什么异?)

[OUTPUT]
markdown spec.md with header:
# Plan
## Rationale
## Phase 1 (Week 1)
## Phase 2 (Week 2-3)
## Phase 3 (Week 4)
## Risks
""",
    )
    state["plan_md"] = plan_oracle
    return state

oracle 帮写架构决策 cost 高但从生产回报大。谁用谁节省三周盲目迭代。

28.8 几个生产心态 / traps

28.8.1 Human-in-the-loop 必须在 plan 阶段卡住

async def wait_for_user_approval(state: RefactorState) -> RefactorState:
    spec = state["plan_md"]
    Path("spec.md").write_text(spec)
    print("Spec written to spec.md.  Reply 'go' to continue.")
    answer = input_presented_to_user()
    if answer.strip().lower() != "go":
        # 让用户修改 plan_md
        ...
    state["approved"] = True
    return state

绝不能让 Agent 关着改完整个项目、无人 review。这是 6 章讲过的"完成 Todo"的底线。

28.8.2 多次熔断(circuit breaker)

def route_after_verify(state: RefactorState) -> str:
    failed_on_mod = [f for f in state["failures"]
                      if f[0] == state["cur_module"]]
    if len(failed_on_mod) >= 3:
        print(f"在 {state['cur_module']} 失败 ≥ 3 次, 报告而退出")
        return "escalate"
    if failed_on_mod:
        return "remediate"
    return "next"

不让模型对同一 mod 无限试错。

28.8.3 写循环不变量

def safety_check(state: RefactorState) -> None:
    # every "completed" mod must pass verify
    for mod in state["completed"]:
        if not module_passes_verify(mod):
            raise RuntimeError(f"completed mod {mod} failed verify")
    # 全测试一次性终末
    if state["final_report"]:
        full_test = subprocess.run(["pytest", "-x", "-q"], ...)
        if full_test.returncode != 0:
            raise RuntimeError("full test suite failed post-refactor")

帮你发现"局部 pass 全局 fail"的坑。

28.9 拆 orchestrator fuse 节点

第 12 章提的三 orchestrator 拆出 lib,由单独 fuse node 处理:

async def node_fuse_orchestrator(state: RefactorState) -> RefactorState:
    # 1. 把三个 orchestrator 的公共逻辑 extract 到 `lib/<project>_common/`
    # 2. orchestrator 只保留 CLI / WS / main 入口 + plugins
    extract_actions = await OpenCode_subagent(
        subagent_type="explore",
        prompt=("[GOAL] 列出 main.py / cli.py / web_server.py 共享的"
                " helper functions, ≤ 30 个, 按 import 关系排序"),)
    # 然后让 build 一一 extract phase
    extracted = await OpenCode_subagent(subagent_type="build",
        prompt=f"Based on:\n{extract_actions}\n\n做 extract + update imports")
    state["final_report"] = f"Extracted: {extracted[:100]}"
    return state

28.10 测 Graph

async def test_graph_runs_on_tiny_project(tmp_path):
    # mock excludes ok state
    state = init_minimal_state(tmp_path)
    runner = compile_graph(graph_spec).compile()
    final = await runner.ainvoke(state)
    assert "orchestrator extraction" in final["final_report"]
    assert 95 > len(final["completed"]) > 0

async def test_remediation_loop():
    state = init_with_one_fail_mod(tmp_path)
    runner = compile_graph().compile()
    final = await runner.ainvoke(state)
    assert final["failures"] == []

让 graph 本身有 unit 测试也是工程化——不要 stop-by-stop 跑真实项目调试。

28.11 实战产出指标

指标 单 loop Graph + Looper
context 用尽 mid-task 40h 内几乎肯定 不到 100k
type 增 error from baseline 600 → 600 (基本没动) 600 → 50 (75% 减)
unit test 覆盖率起 5% → 75% 5% → 25% (出 stack overflow) 5% → 80%+
重构时长 60h+ 30h
user 在场时间 30h+ 5-10h (主 plan 和 final review)

28.12 小结

  • 大型 refactor 用 Graph 不用单 Agent
  • Plan 阶段 oracle 提决策, human approve
  • Per-module node 用 build subagent, verify 做 hard gate
  • 全测试终末不变量 + 多次熔断保护投资
  • 30h 重构 5000 LOC 不是 magic 是 Graph 给出的结构

下一篇: 《29 实战五:AI 测试与 CI 集成》——把前面那些实战端正地接到你公司 CI/CD,_EOF。

Summary: Graph + Looper 把 5000 LOC refactor 从 60h 单 loop 折半至 30h,核心是 oracle 设计 plan、verify 做硬 gate、circuit breaker + human approve。