mvp_doc 与实现的历史接口审阅记录
本页记录
158074e与早期设计稿的接口比较及当时的建议,不代表当前接口或待办清单。 当前使用方式见使用指南,公共接口以 core 源码及接入说明为准。
对应 migration-plan.md 步骤 0 中「列出设计文档和实现的接口差异,由开发者 决定规范更新」。基准:mvp_doc.md(320 行)对 158074e。本文只列差异和建议,不修改 mvp_doc。
当时的审阅将 36 项差异分为 32 项文档更新建议和 4 项接口取舍(§H), 重点是减少算法、Benchmark 和 harness 开发者需要了解的抽象与接入步骤。
标记:文档 = 当时建议回写 mvp_doc | 实现 = 当时建议改代码 | 待定 = 当时待讨论的取舍
A. Agent 接口
当时建议按实现更新以下接口说明;A3 中的权限区分另见 H4 的讨论。
| # | 契约 | mvp_doc | 实现 | 建议 |
|---|---|---|---|---|
| A1 | check_out() / commit() 的归属 | L126-130 在 Agent 上;L172-186 整段按 Agent 叙述 | 在 Snapshot 上(snapshot.py:216、:270);test_agent.py:75 断言 Agent 没有这两个方法 | 文档,理由见 A1 详述 |
| A2 | edit() 返回值 | L145-147 与 L158 表格明确「返回 None」 | 返回 str(agent.py:157) | 文档,理由见 A2 详述 |
| A3 | set_scope 签名与 r 级别 | L124 set_scope(components: set[str]);L153 表格「选择允许修改的 component」 | set_scope(scope: Mapping[str, str]),rw/r/none 三级(agent.py:116) | 签名 文档;r 级别 待定,见 H4 |
| A4 | 谁调 set_scope | L240 算法自己调 self.target.set_scope({"skill"}) | 算法只声明 evolve_scope/observation_scope,基类代调(algorithm.py:55);六个算法无一自己调用 | 文档(声明式写法正是目标 1) |
| A5 | run() 签名 | L139-141 run(task: Task, snapshot: Snapshot) | run(prompt: str, working_dir: Path | None)(agent.py:132);版本由 rollout 固定在执行上下文里 | 文档,附带说明见 A5 详述 |
| A6 | 后端实现哪些方法 | L138「后端按支持的能力实现」下列 run/ask/edit 三个 | 后端只实现 _run 一个钩子;run/ask/edit/rollout 全是基类的 | 文档(比文档自己的目标 3 更彻底) |
| A7 | component_rules | L118 类属性 Mapping[str, tuple[str, ...]] | 从 components.yaml 读、由 Snapshot 持有、Agent 转发的只读属性(agent.py:100) | 文档(结构上保证 worker 改不了规则,对应 L160「分类规则不能由 worker 修改」) |
| A8 | 初始 Snapshot 从哪来 | L152 表格「Agent 根据 seed 和自身分类规则建立」 | 从适配器所在目录 + components.yaml 自动 capture(agent.py:270);无 seed 概念,test_agent.py:75 断言没有 | 文档:接入方零代码;且 "seed" 与 benchmark.seed、algorithm.seed 撞名,建议文档里换词 |
| A9 | snapshot 属性 | L115 普通字段 | 属性;纯 LLM Worker 无版本文件时访问会 raise(agent.py:92) | 文档(L114 注释已是对的,签名没跟上) |
A1 详述:基线放在 Snapshot 上避免了一个文档写法做不到的事
mvp_doc L154 要求 check_out()「在框架内部记住 checkout 时的基线」——这是隐藏状态,且每个 Agent 只能记住一个。但实际用法里同时存在多个基线:
- test_snapshot_files.py:64 覆盖同一 snapshot 并发 checkout 两次;
- GEPA 和 EvoPrompt 是种群算法,手上同时握着多个候选版本(gepa/init.py)。
两个 checkout 同时存在时,Agent 记住的「那一个」基线就是错的。放在 Snapshot 上则基线天然显式:
with current.check_out(working_dir=working) as directory: # 基线就是 current
reply = await self.worker.edit(directory, prompt)
candidate = current.commit(directory) # 从哪个版本分叉,读一眼就知道比 L178 的 self.target.check_out() → self.target.commit(workdir) 更能回答「这个候选是谁的 后代」。附带好处:check_out(working_dir=) 让临时目录落在实验目录的 working/ 下,便于按实验管理。
A2 详述:返回 str 保住了文档想要的性质,还多给了一个机制
文档要 None 的意图是「不提交 target 的 Snapshot」(L158)——这一点实现完全守着,edit() 只 改目录,提交仍是调用方的事。而返回值让编辑变得可核对:ahe:125-143 用返回的文本解析 <MANIFEST>,再用 changed_paths() 把 Agent 的自述和真实改动对账。「让 Agent 对自己的编辑负责」这个机制,返回 None 就不存在了。
A5 详述:一个附带的不对称,建议在文档里写明
run 只收到 task.question,Task 本身由 rollout 在 agent.py:231 事后 replace 贴回去。于是:env 任务的后端能看到完整 Task(走 env_runner),非 env 任务的 后端只看到一个字符串,读不到 task.metadata。
今天不影响任何组件(QA benchmark 把 context 都烘进 prompt 了),但这是真实的能力边界,建议 写进文档,免得后来的接入者以为 _run 里能读 metadata。
B. Snapshot
| # | 契约 | mvp_doc | 实现 | 建议 |
|---|---|---|---|---|
| B1 | format_version | L97 = 1 | = 2;v1 明确拒绝加载并说明原因「no component rules; recapture the source」(snapshot.py:389) | 文档(v1 从未发布过数据,无需兼容) |
| B2 | 字段组成 | L89-97 七个字段 | 多 component_rules(进哈希,snapshot.py:93)和 _scope(私有、compare=False、不参与身份) | 文档,理由见 B2 详述 |
| B3 | 公共方法 | L99-102 只有 save/load | 另有 check_out/commit(见 A1)、from_directory、changed_paths(ahe:143),模块函数 discard_runtime_files(ahe:134) | 文档;from_directory 见 H2 |
B2 详述:component_rules 进哈希是对的,值得在文档里说明
规则进哈希意味着「同样的文件 + 不同的分类规则」是两个不同版本。这正是 mvp_doc L107「加载时检查 Agent 类型、格式和版本来源」想要的性质:改了 components.yaml 之后,旧候选无法再被 update 进来(snapshot.py:338 检查 rules 相等)。而 _scope 反过来 不能进哈希——scope 是算法的临时视图,同一个版本在不同 scope 下必须仍是同一个版本,所以它 标了 compare=False。这一对取舍是 Snapshot 设计的核心,文档里现在一句都没有。
C. Benchmark
| # | 契约 | mvp_doc | 实现 | 建议 |
|---|---|---|---|---|
| C1 | primary_metric | 未出现在类定义中(只在 L242 算法示例里被读) | 类属性,子类必须声明(exact_match / reward) | 文档,另见 H3 |
| C2 | train/dev/test | 未出现在类定义中(L211、L243 被读) | 基类属性,load() 未调用时 raise(benchmark.py:75) | 文档(含「先 load 再取 split」的时序) |
| C3 | 构造签名 | 无 __init__ | 基类 __init__(self),但四个子类全是 __init__(self, config) | 实现,见 H1 |
| C4 | Benchmark.load_from_config(config) | L209 | 不存在;runner 直接 benchmark_type(config.benchmark)(runner.py:56) | 文档(删;与 C3 一并解决) |
| C5 | grade() 的前置条件 | L75-76 只说填 metrics | 要求 trajectory.task is not None,否则报错要求改用 rollout(_qa.py:56) | 文档(与 E1 相关:ask() 的轨迹不能送去 grade) |
D. Algorithm
| # | 契约 | mvp_doc | 实现 | 建议 |
|---|---|---|---|---|
| D1 | 两份互斥草稿并存 | L203-228 class Algorithms(类名多了 s、self.worker = Agent(config) 自己造协作者)与 L229-279 并存 | 按后者:runner 注入三个协作者(algorithm.py:39-41) | 文档(删前一段,它与实现完全相反) |
| D2 | evolve() 返回类型 | L210、L278 -> Agent;L239 -> Snapshot(文档自相矛盾) | 统一 -> Snapshot,六个算法一致;runner 直接拿它评 test(runner.py:76-77) | 文档(统一为 Snapshot) |
| D3 | 算法作者的实际契约 | 完全缺失 | 声明:evolve_scope/observation_scope/_defaults/_ranges;钩子:_check()/_normalize();基类动作:_evaluate/_candidate/_read_text/_record/_promote/_instruction_path/_evidence/_trajectory_evidence | 文档(最大一块,见下) |
| D4 | Results 类型 | 不存在 | _evaluate() 的返回值,带 .score/.per_task(trajectory.py:63) | 文档 |
| D5 | 算法是否读 primary_metric | — | skillopt:117 和 ace:148 仍在读它判断单条轨迹成败,而 _evaluate 返回的是聚合结果 | 待定,见 H3 |
D3 涉及算法作者需要了解的基类能力,早期 mvp_doc 没有集中说明,接入者只能从现有算法反推。 当时建议补一张「基类提供的动作」表,以减少接入成本。
E. Trajectory / Task
| # | 契约 | mvp_doc | 实现 | 建议 |
|---|---|---|---|---|
| E1 | task 是否必填 | L192 task: Task 必填 | task: Task | None = None——ask()/edit() 的轨迹确实没有 Task(llm.py:58、:64) | 文档(Worker 问答不属于任何任务,这是实质差异不是笔误;与 C5 配套) |
| E2 | metrics 默认值 | L196 = (),但类型标的是 Mapping | field(default_factory=dict) | 文档(文档处是笔误) |
| E3 | 可变性 | L191 普通类 | frozen=True, slots=True,三个映射字段在 __post_init__ 转 MappingProxyType | 文档(与 L106「文件映射与文件条目均不可变」是同一类约定,文档只对 Snapshot 说了) |
| E4 | Task | L58-63 | 一致 ✓(字段、默认值、语义都对得上) | — |
F. 文件清单(§4)与 MVP 计划(§6)
| # | 差异 | 建议 |
|---|---|---|
| F1 | 实际多出:_records.py、_run_summary.py、algorithms/_replies.py、benchmarks/{_data,_qa,_archive,metrics}.py、integrations/{tau2,_container}.py、agents/llm.py | 文档 |
| F2 | 算法多出 5 个(OPRO/GEPA/EvoPrompt/ACE/AHE),benchmark 多出 3 个(HotpotQA/Tau3Banking/TerminalBench) | 文档 |
| F3 | §6 的 0-7 步实现都在,只有第 7 步的真实容器验收未完成(本机无 Docker/Podman) | 文档(标注状态,不标成未实现) |
H. 需要决策的四项
H1. Benchmark 基类补 __init__(self, config)
当时基类是 __init__(self),而 config 契约只在子类里体现,同一段样板重复了三份:
# _qa.py:30 / terminal_bench:47 / tau3_banking:66 —— 三份逐字相同
super().__init__()
self.config = config["benchmark"] if "benchmark" in config else config
if self.config.get("name", self.name) != self.name:
raise ValueError(...)
self._tasks_by_id: dict[str, Task] = {}load() 里「算 split + 建 _tasks_by_id 索引」的四行同样重复了三份,存在提取公共逻辑的空间。
建议:基类接 config、做 name 校验、持有 _tasks_by_id;子类只留 _iter_tasks() 和自己的 额外参数(如 tau3 的 variant)。这同时把 C3 的契约缺口补上——benchmark 接入者不必靠读现有 子类来猜构造签名,对应 §1 目标 3。改动范围:core/benchmark.py + 三个子类 + test_benchmark.py。
H2. Snapshot.from_directory 没有生产调用者
当时只有 tests/test_snapshot_files.py 在用(10 处),flashrsi/、examples/、scripts/ 零调用。Agent 走 _capture,算法走 commit。
建议:删掉,或在 docstring 里写明它的定位(「curated 目录入口,供外部工具建初始版本」)并在 mvp_doc 中说明用途。
H3. primary_metric 与聚合结果的职责
_evaluate 返回的结果自带 score 和 per_task,但两个算法仍然要读 primary_metric, 因为它们问的是 Results 回答不了的问题:这一条轨迹算不算成功 (trajectory.metrics[metric] > success_threshold)。Results 只给聚合分和逐题分,给不了 「给我这条轨迹的主指标」。
两条路:
- (a) 将
primary_metric作为公共接口说明:写进 mvp_doc 的 Benchmark 类定义,说明 逐条轨迹的成功判定读取primary_metric。零代码改动。 - (b) 让数据自己回答:
Results补一个逐条取主指标的办法,两个算法改为不碰 metric 名。 这样会给基类增加一个算法作者需要了解的接口。
当时建议采用 (a):primary_metric 已是 Benchmark 的公开属性,可直接用于单条轨迹的成功判定。
H4. observation_scope 和 r 权限级别的使用情况
核对六个算法的实际声明:
| 算法 | evolve_scope | observation_scope | 仅可读部分 |
|---|---|---|---|
| ace | {prompt} | {prompt} | 空 |
| ahe | {*} | {} | 空 |
| gepa | {prompt} | {prompt} | 空 |
| opro | {prompt} | {prompt} | 空 |
| evoprompt | {prompt} | {prompt} | 空 |
| skillopt | {skill} | {skill} | 空 |
observation_scope 一次都没有超出 evolve_scope。于是 algorithm.py:210 的 self._observe | self._evolve 恒等于 _evolve,snapshot.py:255 里 == "r" 那行权限降级从未生效。
在当时核对的算法中,该权限区分没有实际用例,却增加了接口说明成本。
两条路:
- (a) 删:算法作者少记一条规则,
_read_text少一次无意义的并集,set_scope收成rw/none两级。代价是将来真出现「让 Worker 读它不能改的代码」的算法时要加回来。 - (b) 留:那至少要有一个算法真的用上它(例如让 SkillOpt 观察
prompt但只改skill), 否则它就是没有读者的字段。
当时建议采用 (a),待出现实际只读用例后再评估。set_scope(Mapping) 的签名本身建议保留 (见 A3):它不是算法作者的接口,算法写的是两个 frozenset,Mapping 是基类现造的;none 与 rw 的区分是真在用的(隐藏组件不进 checkout)。
当时建议的执行顺序
- 明确 H3 和 H4 的接口取舍,同步更新相关接口说明。
- 回写 mvp_doc:A/B/C/D/E/F 共 32 项,集中更新可避免混用不同版本的接口描述。
- H1(一个可独立 review 的小步骤),H2 和 H4(a) 可以顺带。