数据截至 (上游 commit e01fa5c38e66)
规划阶段:把论文压成一份冻结的复现计划
这章讲 Phase 5:怎么把一篇长论文(或一段需求)变成一份结构化的
initial_plan.txt——它列出要建哪些文件、每个文件干什么、优先级如何。这份计划之后会被冻结,整个实现阶段都照着它走。
1. 它要解决的小问题
直接把「整篇论文 + 写代码吧」丢给模型,结果往往是:文件结构混乱、漏掉关键算法、或者上下文太长被截断。DeepCode 的思路是先想清楚再动手:产出一份明确的「文件清单 + 每文件职责 + 优先级」的计划,把「设计」和「实现」彻底分开。
2. 思路/直觉:为什么计划要「冻结」
计划一旦生成并通过校验,就写进 initial_plan.txt,实现阶段只读不改。好处:
- 实现 agent 永远有一个稳定的「全集」——它随时能算出「哪些文件还没实现」(见第 03 章的退出条件)。
- 计划是单一事实来源,实现阶段每次清空对话后都把它重新塞回上下文(第 04 章)。
这呼应整个项目的设计哲学:靠落盘的稳定产物传递状态。
3. 主线:run_code_analyzer 的三段式
规划核心在 run_code_analyzer(workflows/agent_orchestration_engine.py:614)。它分三段:
第一段:决定要不要分段。 文档超过阈值(5 万字符)就先做文档分段,产出一份紧凑的、确定性的上下文喂给 planner,避免一次塞整篇:
# 示意,非源码:分段阈值判定(对应 utils/llm_utils.py:89 should_use_document_segmentation)
threshold = 50_000 # size_threshold_chars
if len(doc_text) > threshold:
use_segmentation = True # 太长 → 先分段成紧凑上下文
else:
use_segmentation = False # 够短 → 传统全文规划
但注意它的注释强调的一条原则(:594-:599):无论是否分段,最终计划永远由「单一权威 planner」生成,不做多 planner 并行——避免 fan-out 死锁、保持规划架构一致。分段只是用来准备上下文,不是用来并行出多份计划。
第二段:带重试的生成循环。 用一个 planner agent(server_names=[],纯 LLM 不带工具),最多重试 3 次(max_retries)。每次生成后做两道检查(:740-:748):
_assess_output_completeness(result)—— 完整性打分(0~1)。validate_plan_text(result)—— 结构校验(必需小节是否齐全)。
只有 完整性 ≥ 0.8 且校验通过,才认定成功、写盘、返回(:750-:772):
# 示意,非源码:成功判定(对应 :750)
if completeness_score >= 0.8 and plan_validation["valid"]:
write_planning_meta(paper_dir, {"status": "success", ...})
return result
不达标就调参重试(_adjust_params_for_retry 加 max_tokens、改温度,:800-:805),并记下「目前最好的那次不合格结果」(best_invalid_result)备用。
第三段:多级降级,而不是直接崩。 三次都没出合格计划时,它不是抛异常了事,而是降级(:839-:869):
三次重试都不合格?
│
▼
拿「最好的那次不合格结果」做 fallback_source
│ 太短(<500字符)?
▼
退而用 分段上下文 / 论文前 6000 字符
│
▼
coerce_text_to_minimal_plan() ← 把自由文本套进「最小计划 schema」
│ 套完能通过 validate_plan_text?
├─ 能 → 当成 success 返回(source="coerced_from_freeform")
└─ 不能 → 写 error meta,raise
这一串「重试 → 取最优不合格 → schema 包装 → 实在不行才 raise」是规划阶段最体现工程韧性的地方。
4. 深入:几个关键细节
① 分段模式更「卷」。 分段时给 planner 的 max_iterations=5、温度 0.2;传统全文时 max_iterations=2、温度 0.3(:640-:642)。分段意味着 planner 要多轮拼合,所以给更多迭代、更低温度求稳。
② 「延迟到工具」也算降级信号。 如果 planner 输出表现为「我需要某个不可用的工具才能继续」(_is_deferred_planning_output,:269、:774),直接判定本轮无望,改用分段上下文兜底。
③ 复用已有计划(续跑)。 Phase 5 的外层 orchestrate_code_planning_agent(:1208)先调 is_existing_plan_usable:如果任务目录里已有可用的 initial_plan.txt,直接复用、跳过整个规划——这是断点续跑的一环(:1226-:1243)。
④ 自适应 prompt。 分段 vs 传统用不同的 prompt 模板(get_adaptive_prompts),论文分析/规划各有 _TRADITIONAL 后缀的版本(见 prompts/code_prompts.py:1234 起)。
5. 巧妙之处
- 「单一权威 planner」而非多 planner 投票。 作者明确选择不并行(
:596-:599),理由是一致性和避免死锁——一个反直觉但务实的取舍。 - 完整性打分 + 结构校验双闸门。 不只看「有没有输出」,而是量化「够不够完整、结构对不对」,这才敢据此重试/降级。
- 降级到 schema 包装。 与其让流水线崩在规划,不如把「最好的那坨自由文本」硬塞进最小计划骨架——保证下游有计划可读。
6. 边界与局限
- 计划质量决定一切。 计划冻结后实现阶段不回头改设计;如果计划本身漏了某个核心算法,实现阶段不会补。
- 完整性打分是启发式的。
_assess_output_completeness是基于结构/长度的近似,不是语义正确性保证。 - 5 万字符阈值是硬编码默认值(
utils/llm_utils.py:80),可配置但默认固定。
7. 横向对比
很多编码 agent 把「规划」和「执行」混在同一个对话里(模型边想边写)。DeepCode 把规划独立成阶段并冻结产物,更接近「先写设计文档、评审通过、再开工」的工程流程——甚至内置了 run_plan_review_gate 审计闸门(第 01 章)。这是「计划-执行分离」流派的代表实现。
8. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 规划核心(重试+降级) | workflows/agent_orchestration_engine.py | run_code_analyzer |
| 规划阶段外层(复用/校验) | 同上 | orchestrate_code_planning_agent |
| 单 planner 生成 | 同上 | _generate_plan_with_single_agent |
| 完整性打分 | 同上 | _assess_output_completeness |
| 分段阈值判定 | utils/llm_utils.py | should_use_document_segmentation |
| 自适应 prompt | utils/llm_utils.py | get_adaptive_prompts |
| 计划校验/降级/元信息 | workflows/planning_runtime.py | validate_plan_text / coerce_text_to_minimal_plan / is_existing_plan_usable / write_planning_meta |
| 规划 prompt 模板 | prompts/code_prompts.py | CODE_PLANNING_PROMPT / CODE_PLANNING_PROMPT_TRADITIONAL |