5 minutes
实战四:用 Graph 重构大型项目
第四个实战最 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) 错综耦合
重构目标(一季):
- 引入类型注解 +
basedpyright严格 - 单测覆盖率 70%+
- 三 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 truthplan_remediation:用 oracle (expensive) 设计方案,用户 review approvalrefactor_one_module+verify_module_graph:每个模块独立 loopfuse_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。