| # 串行测评 vs 单独运行 —— 架构详解 |
|
|
| > 这份文档解释 `run_imaging101_25303977_serial.py` 到底如何"串行"运行 13 个子任务,以及它与"独立运行一个子任务"有什么本质区别。 |
| |
| --- |
| |
| ## 1. 一句话结论 (TL;DR) |
| |
| > **"串行"在当前实现中,并不是真正意义上的"上下文承接"。它只是按顺序循环 13 次调用单任务 CLI,每次调用都是完全独立的 LLM 会话。所谓的"context"只被保存到磁盘 (`context.json`),从未传给模型。** |
| |
| 所以从模型视角看: |
| - **串行模式** 与 **单独运行** 唯一真正的差别是 → **共享了一个 outputs/ 目录**(被 judge 用,不是被模型用) |
| - 模型既看不到前面任务的代码,也看不到前面的结果 |
| |
| 这就解释了为什么单独跑 96/118,但串行只有 89→95/118:**串行模式没有任何理论优势**,反而完全暴露在"判题环境差异"的风险中。 |
| |
| --- |
| |
| ## 2. 单独运行一个子任务 (Single-task baseline) |
| |
| ``` |
| bun src/harness/evaluation/cli.ts \ |
| --task 25303977_3 \ |
| --tasks-dir /home/yjh/BioDSBench-imaging101-format/tasks \ |
| --runs-dir /tmp/single_run \ |
| --max-rounds 2 \ |
| --timeout-seconds 1500 \ |
| --temperature 1 \ |
| --thinking disabled \ |
| --agent-runtime source |
| ``` |
| |
| ### 内部流程 |
| ```mermaid |
| flowchart LR |
| A[CLI 启动] --> B[读取 task_25303977_3<br/>tasks/25303977_3/README.md<br/>output_schema.json<br/>cases.json] |
| B --> C[buildInitialSourcePrompt<br/>构造 system prompt] |
| C --> D[新建 Claude Session<br/>全新对话历史] |
| D --> E[Round 1: 模型读 public/、写 workspace/、生成 outputs/] |
| E --> F[调用 finalize_submission] |
| F --> G[judge.py 评分] |
| G -->|pass| H[退出 0] |
| G -->|fail| I[Round 2: 把 judge 反馈喂回模型] |
| I --> J[模型修改后重新提交] |
| J --> G |
| ``` |
| |
| ### 单任务模式下模型看到的 prompt(简化版) |
| ``` |
| <run_context> |
| task_id: 25303977_3 |
| cwd: run root |
| python: /home/.../runtime/.venv/bin/python |
| max_judge_rounds: 2 |
| submission_dir: outputs/ |
| </run_context> |
| |
| <public_files> |
| - README.md |
| - data/data.csv |
| - visible_data/cases.json |
| - output_schema.json |
| </public_files> |
| |
| <task_statement> |
| (README.md 的全文 —— 描述这个子任务要做什么) |
| </task_statement> |
| |
| <output_contract> |
| Required file pattern: outputs/{case_id}.npz |
| Required arrays: |
| - answer: shape [1], dtype float |
| </output_contract> |
| |
| <workflow> |
| 1. 读 README.md ... |
| 8. Call finalize_submission ... |
| </workflow> |
| ``` |
|
|
| **关键点**: |
| - 这就是模型看到的**全部信息** |
| - 没有任何"你之前做过的 25303977_0、_1、_2 是什么"的信息 |
| - Round 1/2 是同一个子任务的不同评测轮次,**不是不同的子任务** |
| |
| --- |
| |
| ## 3. 串行运行 13 个子任务 (Serial mode) |
| |
| 来源:`/home/yjh/my_claude/run_imaging101_25303977_serial.py` |
| |
| ### 顶层循环 |
| ```python |
| for task_idx in range(0, 13): |
| task_id = f"25303977_{task_idx}" |
| result = self._execute_task(task_id, task_idx) # 内部还会 retry 3 轮 |
| self.state["tasks"].append(result) |
| ``` |
| |
| 就是一个朴素的 `for` 循环,调用 13 次 `_execute_task`。 |
|
|
| ### `_execute_task` 内部做了 4 件事 |
| ```python |
| for round_num in range(1, 4): # 每个子任务最多重试 3 次 |
| context = self._build_context(...) # ① 构建上下文 dict |
| json.dump(context, context_file) # ② 保存到 round_N/context.json |
| cli_result = self._run_cli(task_id) # ③ 调用 bun cli.ts —— 不传 context! |
| if cli_result.success: |
| self._save_generated_code(...) # ④ 把生成的代码保存到磁盘 |
| return # 通过则进入下一个子任务 |
| ``` |
|
|
| ### **`_run_cli` 的真相**:context 没被传出去 |
| ```python |
| def _run_cli(self, task_id, round_dir): |
| env = { |
| **os.environ, |
| "BIODSBENCH_OUTPUTS_DIR": str(self.outputs_dir), # ← 共享 outputs 目录 |
| "ANTHROPIC_API_KEY": "...", |
| "ANTHROPIC_BASE_URL": "...", |
| "ANTHROPIC_MODEL": "Vendor2/Claude-4.7-opus", |
| ... |
| } |
| cmd = [ |
| "bun", "src/harness/evaluation/cli.ts", |
| "--task", task_id, |
| "--tasks-dir", str(self.tasks_dir), |
| "--runs-dir", str(round_dir), |
| "--max-rounds", "1", |
| "--timeout-seconds", "1800", |
| "--temperature", "1", |
| "--thinking", "disabled", |
| "--agent-runtime", "source" |
| ] |
| subprocess.run(cmd, env=env, ...) |
| # ⚠️ 注意:没有任何参数把 previous_tasks / generated_code 传给 cli.ts |
| ``` |
|
|
| **对比单任务命令**: |
| | 参数 | 单任务 | 串行(每次循环) | |
| |------|--------|------------------| |
| | `--task` | ✅ | ✅ | |
| | `--tasks-dir` | ✅ | ✅ | |
| | `--runs-dir` | ✅ | ✅(每个 round 一个独立子目录)| |
| | `--max-rounds` | 2 | **1** ← 串行把 max-rounds 设为 1,外层自己 retry | |
| | 上下文/前置代码 | 无 | **无** ← 关键! | |
| | env: `BIODSBENCH_OUTPUTS_DIR` | 无 | **有** ← 唯一差别 | |
|
|
| ### 我已经在代码中验证过 |
| 1. `context["previous_tasks"]` 只被 `json.dump` 写盘,没有任何代码读它 |
| 2. `self._save_generated_code()` 写了 `generated_code.py` 和 `task_N_..._code.py` 到磁盘,但**没有传给 cli.ts** |
| 3. `grep -rn "context.json" /home/yjh/my_claude/src/` → **零结果**(CLI 不读这个文件) |
| 4. `grep -rn "BIODSBENCH_OUTPUTS_DIR" /home/yjh/my_claude/src/` → **零结果**(CLI 也不用这个 env) |
|
|
| --- |
|
|
| ## 4. 模型实际看到的 prompt:单任务 vs 串行 |
|
|
| ### 单任务(独立跑 `25303977_3`) |
| ``` |
| <task_statement> |
| README of 25303977_3 |
| </task_statement> |
| ``` |
| |
| ### 串行模式下跑到第 4 个任务(`25303977_3`) |
| ``` |
| <task_statement> |
| README of 25303977_3 ← 完全相同! |
| </task_statement> |
| ``` |
| |
| > ✅ **两者的 prompt 完全一致**。模型不知道自己是单独跑的还是串行第 4 个。 |
| |
| --- |
| |
| ## 5. 那串行和单任务到底差在哪? |
| |
| ### 5.1 唯一真正的差别:共享 `outputs/` 目录 |
| |
| ``` |
| 串行运行目录 (run_dir): |
| └── outputs/ ← 共享!所有 13 个子任务都把结果写到这里 |
| ├── task_0_25303977_0_code.py ← 串行脚本自己复制进来的(模型不会看) |
| ├── case_000.npz ← 25303977_0 的输出 |
| ├── case_000.npz ← 25303977_1 的输出会覆盖 / 增加文件 |
| └── ... |
| |
| 单任务运行目录: |
| └── outputs/ ← 只属于这一个子任务 |
| └── case_000.npz |
| ``` |
| |
| 但要注意:**模型仍然是把结果写到自己 run 的 `outputs/`**(每个 CLI 调用都有独立的 run_dir)。`BIODSBENCH_OUTPUTS_DIR` 这个环境变量是给 **judge.py** 用的(让 judge 能找到提交在哪),**不是给模型用的**。 |
| |
| 实际上 `src/` 下没有任何代码读取 `BIODSBENCH_OUTPUTS_DIR`,连 judge.py 也只在 `_run_test_cases_with_patch` 里通过 `os.environ` 间接使用。 |
|
|
| ### 5.2 其它差别(次要) |
|
|
| | 维度 | 单任务 | 串行 | |
| |------|--------|------| |
| | `--max-rounds` | 2~3(CLI 内 judge 反馈循环)| **1**(外层脚本自己 retry)| |
| | Retry 实现 | CLI 内部:模型在同一 session 内看到 judge feedback | 外层:每次 retry 都是**全新 session**,模型看不到上次 feedback | |
| | 失败后续影响 | 无(独立)| 仍无(虽然循环不停,但下一个子任务也是全新 session)| |
| | 总耗时 | 单任务时间 | ~13× 单任务时间 | |
|
|
| > 🔴 **第二行很重要**:串行脚本里把 `--max-rounds` 设为 **1**,让外层 Python 自己重试 3 次。 |
| > 但 **外层重试 ≠ CLI 内部重试**:外层每次重试都启动**新的 LLM 会话**,模型不会看到上一次失败的 judge feedback! |
| > 这是导致串行 89/118 < 单独 96/118 的**可能根本原因之一**。 |
|
|
| --- |
|
|
| ## 6. 流程对比图 |
|
|
| ```mermaid |
| flowchart TB |
| subgraph Single["单独运行单任务(baseline,96/118)"] |
| direction TB |
| S1[bun cli.ts --task 25303977_3 --max-rounds 2] --> S2[一次 CLI 调用] |
| S2 --> S3[Session 内 Round 1] |
| S3 --> S4{judge pass?} |
| S4 -->|no| S5[Round 2: 同一 session,<br/>模型看到 judge feedback] |
| S5 --> S4 |
| S4 -->|yes| S6[✅] |
| end |
| |
| subgraph Serial["串行运行 13 任务(89→95/118)"] |
| direction TB |
| T0[for task_idx in 0..12] --> T1[for round in 1..3] |
| T1 --> T2[bun cli.ts --task 25303977_X --max-rounds 1] |
| T2 --> T3[全新 LLM session] |
| T3 --> T4[Round 1 only] |
| T4 --> T5{success?} |
| T5 -->|no| T6[Python 外层 retry:<br/>启动全新 session<br/>不带 feedback] |
| T6 --> T2 |
| T5 -->|yes| T7[task_idx++] |
| T7 --> T1 |
| end |
| ``` |
|
|
| --- |
|
|
| ## 7. 为什么会出现 96/118(单跑)vs 89/118 → 95/118(串行)的差异 |
|
|
| | 因素 | 影响 | |
| |------|------| |
| | **`--max-rounds` 不同** | 串行只给 1 轮 judge 反馈,单跑给 2-3 轮,但通过外层 retry 部分弥补 | |
| | **外层 retry 丢失 feedback** | 单跑 Round 2 的模型能看到 Round 1 的 judge 错误信息;串行 retry 的模型每次都从零开始,看不到上一次错在哪 | |
| | **judge.py monkey-patch bug** | 串行的 `BIODSBENCH_OUTPUTS_DIR` 把 judge 指向共享目录,触发了 judge.py 中那些**没打到的 patch**(`_run_test_cases` 内部缺补丁、`os.listdir`/`glob.glob` 没拦截)→ 这就是修了 `judge.py` 后能从 89 涨到 95 的原因 | |
| | **共享 outputs 副作用** | 多个子任务的 .npz 输出可能命名冲突或互相干扰;不过这次没看到此类失败 | |
| | **运行时差异(不应该影响)** | 单跑用 source runtime,串行也用 source runtime,理论上模型行为应一致 | |
|
|
| --- |
|
|
| ## 8. 这意味着什么 |
|
|
| 1. **如果你想真正"串行+上下文承接"**(让 25303977_3 看到 _0/_1/_2 的代码),需要修改 `_run_cli`:把 `previous_tasks` 里的 `generated_code` 拼到一个新的 `--system-prompt` 文件中,或者通过 `cli.ts` 新增 `--prior-context` 参数。**当前实现没做这件事。** |
|
|
| 2. **目前所谓的"串行",本质是"批量跑"**:和 `for i in 0..12; bun cli.ts --task 25303977_$i; done` 几乎等价。差别仅在: |
| - `--max-rounds 1` + 外层 retry(更弱,无 feedback 承接) |
| - 共享 `BIODSBENCH_OUTPUTS_DIR`(只影响 judge,不影响模型) |
|
|
| 3. **能修正到 95/118** 主要是因为修复了 judge.py 的两个 monkey-patch 缺陷(Round 1 测试用例缺补丁 + Round 2 缺 filesystem 拦截);这些其实是**评测环境**的问题,不是模型本身能力的差距。 |
|
|
| 4. **要冲 97+** 必须解决剩下 11 个真实的模型代码错误(列名拼写、数值方法偏差等),而不是再调整串行框架 —— 因为框架本身没有给模型额外信息,只能靠它一次性写对。 |
|
|
| --- |
|
|
| ## 9. 相关源码位置速查 |
|
|
| | 概念 | 文件 | 关键行 | |
| |------|------|--------| |
| | 串行循环 | `/home/yjh/my_claude/run_imaging101_25303977_serial.py` | `run()` line 95-130 | |
| | 单子任务 + 外层 retry | 同上 | `_execute_task()` line 150 | |
| | context 构建(**未使用**)| 同上 | `_build_context()` line 235 | |
| | 调用 CLI 的命令 | 同上 | `_run_cli()` line 297-340 | |
| | 单任务 CLI 入口 | `/home/yjh/my_claude/src/harness/evaluation/cli.ts` | line 78+ | |
| | Prompt 构造 | `/home/yjh/my_claude/src/harness/evaluation/sourceContextBuilder.ts` | `buildInitialSourcePrompt()` line 187 | |
| | 单任务内 judge 反馈承接 | `/home/yjh/my_claude/src/harness/evaluation/sourceTaskLoop.ts` | 整个文件 | |
|
|