跳到主要内容

实现主循环:逐文件写代码的 800 轮 while

这章讲 Phase 9 的心脏:_pure_code_implementation_loop。它怎么一轮一轮地让模型写文件、怎么判断「全部写完了」、怎么防止模型卡死在循环里。

1. 它要解决的小问题

拿到冻结的计划后,要把计划里列出的每个文件真的写出来。难点不是「让模型写一个文件」,而是:① 一个项目几十个文件,对话会越来越长直到爆上下文;② 模型可能卡在「反复读、反复想、不动手」的死循环;③ 怎么客观判断「做完了」。

2. 思路/直觉:用循环 + 工具,而非一次生成

模型不会一次性吐出整个项目。DeepCode 让它一步一个工具调用(系统 prompt 明确要求「SINGLE FUNCTION CALL PER MESSAGE」,prompts/code_prompts.py:1120),通过几百轮 while 累积出整个代码库:

┌────────────────────────────────────────────────────┐
│ while iteration < 800 且 elapsed < 7200s: │
│ 1. 检查墙钟/循环检测器 → 该停就 break │
│ 2. 调 LLM(带工具)→ 拿到 tool_calls │
│ 3. 执行工具(write_file / read_code_mem / ...) │
│ 4. 记进度;若是 write_file → 标记「下轮清空记忆」 │
│ 5. 若触发优化 → 把对话压成「计划+摘要」干净起点 │
│ 6. unimplemented_files 空了? → completed,break │
└────────────────────────────────────────────────────┘

3. 主线:一轮里发生什么

循环在 _pure_code_implementation_loop(workflows/code_implementation_workflow.py:363)。开局先建三个角色(:388-:400):

  • CodeImplementationAgent —— 执行工具、跟踪文件实现。
  • ConciseMemoryAgent —— 管记忆,初始化时从计划里抽出全部文件清单 all_files_list(这就是「全集」)。
  • progress_tracker / loop_detector —— 进度与死循环防护。

每轮(:426 起)做这些事:

① 前置闸门。 超 2 小时墙钟(max_time=7200,:430)、或循环检测器喊停(should_abort,:439),立刻 break 并记下原因。

② 调 LLM 并计时。 _call_llm_with_tools,并把这次 LLM 等待时间从「停滞预算」里扣掉(note_llm_wait,:498)——否则一次慢响应会被误判成「卡住了」。

③ 执行工具调用。 调工具前再过一遍循环检测(同一个工具反复调?:510-:521),然后 code_agent.execute_tool_calls。对每个结果(:530-:567):

# 示意,非源码:工具结果处理(对应 :535-:567)
if not tool_result["isError"]:
loop_detector.record_success()
if tool_call["name"] == "write_file": # 写文件成功 → 记一个文件完成
progress_tracker.complete_file(normalize(filename))
else:
loop_detector.record_error(...) # 失败 → 计入错误,可能触发 abort
memory_agent.record_tool_result(...) # 喂给记忆 agent(write_file 会触发清零标记)

④ 给模型回话 + 触发记忆优化。 根据工具有没有报错拼一段 guidance 回给模型(:572-:581);然后问记忆 agent「该清空了吗」,该清就把对话压成干净起点(:584-:594,机制见第 04 章)。

⑤ 判完成。 关键退出条件不是模型说「我写完了」,而是(:621-:631):

# 示意,非源码:真正的完成判定(对应 :622)
unimplemented = memory_agent.get_unimplemented_files() # 计划全集 - 已实现
if not unimplemented: # 空了才算完
run_state = {"status": "completed", ...}
break

这就是为什么计划要冻结成「全集」:实现是否完成,由「全集减已实现是否为空」客观裁定,而不是信模型自述。

4. 深入:状态如实回传

循环退出后,把真实情况快照进 _last_run_state(:646-:655):status(completed / max_iterations / max_time / aborted / incomplete)、迭代数、已完成/总文件数、未实现文件列表。这份快照一路传回编排引擎(第 01 章 §4),让最终汇报能区分「全部完成」和「写了一半就停了」。

紧急截断。 即便没触发 write_file 优化,只要 len(messages) > 50 也会强制压一次记忆(:634-:643),这是上下文长度的最后一道保险。

MCP agent 初始化。 实现 agent 连三台 MCP 服务器:code-implementation(写文件/执行)、code-reference-indexer(查参考)、document-segmentation(:668-:672),并在启动时 set_workspace 把工作目录定到 generate_code/(:682-:684)。

5. 巧妙之处

  • 完成 = 全集减差集为空。 不信模型「我做完了」,用计划全集做客观裁判,杜绝模型提前宣称完成。
  • 把 LLM 等待从停滞预算里扣除。 一个很细但很重要的工程修正——否则慢响应会误触发死循环 abort(:473-:498)。
  • 多层防卡死。 墙钟超时 + 循环检测器(pre-LLM、工具调用前两道)+ 错误计数 + 50 条消息硬上限,层层兜底。
  • 进度有稳定分母。 progress_tracker 用计划的总文件数当分母,进度百分比不会因对话清空而跳变。

6. 边界与局限

  • 2 小时 / 800 轮硬上限。 超大项目可能在写完前撞墙,返回 incomplete(部分产物保留)。
  • 完成判定只看「文件是否被写过」,不验语义正确。 write_file 成功 ≠ 代码能跑对;循环本身不强制跑测试(implement_code_pure 注释里写明「focus on code writing without testing」,:288)。
  • 强依赖计划里的文件清单解析。 文件全集是从 initial_plan.txt 抽的(第 02、04 章),计划格式坏了会导致全集错、完成判定错。

7. 横向对比

和标准 ReAct 循环(模型自由决定何时停)相比,DeepCode 给这个循环套了一个外部客观终止条件(计划全集)和多重防卡死护栏。它更像「按清单逐项打勾的施工队」,而非「自我判断何时收工的工匠」。这种「外部 checklist 驱动」是它能在 PaperBench 上跑完整个大项目的关键。

8. 代码地图

主题文件符号
实现主循环workflows/code_implementation_workflow.py_pure_code_implementation_loop
纯实现入口同上implement_code_pure
MCP agent 初始化同上_initialize_mcp_agent
带工具的 LLM 调用同上_call_llm_with_tools
工具执行 + read_file 拦截workflows/agents/code_implementation_agent.pyCodeImplementationAgent.execute_tool_calls
未实现文件计算workflows/agents/memory_agent_concise.pyget_unimplemented_files / all_files_list
实现系统 promptprompts/code_prompts.pyGENERAL_CODE_IMPLEMENTATION_SYSTEM_PROMPT