biodsbench-adapter / examples /ARCHITECTURE.md
starpacker52's picture
Add files using upload-large-folder tool
4c0ecfa verified
|
Raw
History Blame Contribute Delete
11.5 kB

串行测评 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

内部流程

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

顶层循环

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 件事

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 没被传出去

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.pytask_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. 流程对比图

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 整个文件