Skip to content

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实现建议
A1check_out() / commit() 的归属L126-130 在 Agent 上;L172-186 整段按 Agent 叙述在 Snapshot 上(snapshot.py:216、:270);test_agent.py:75 断言 Agent 没有这两个方法文档,理由见 A1 详述
A2edit() 返回值L145-147 与 L158 表格明确「返回 None」返回 str(agent.py:157)文档,理由见 A2 详述
A3set_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_scopeL240 算法自己调 self.target.set_scope({"skill"})算法只声明 evolve_scope/observation_scope,基类代调(algorithm.py:55);六个算法无一自己调用文档(声明式写法正是目标 1)
A5run() 签名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 更彻底)
A7component_rulesL118 类属性 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 撞名,建议文档里换词
A9snapshot 属性L115 普通字段属性;纯 LLM Worker 无版本文件时访问会 raise(agent.py:92)文档(L114 注释已是对的,签名没跟上)

A1 详述:基线放在 Snapshot 上避免了一个文档写法做不到的事 ​

mvp_doc L154 要求 check_out()「在框架内部记住 checkout 时的基线」——这是隐藏状态,且每个 Agent 只能记住一个。但实际用法里同时存在多个基线:

两个 checkout 同时存在时,Agent 记住的「那一个」基线就是错的。放在 Snapshot 上则基线天然显式:

python
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实现建议
B1format_versionL97 = 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实现建议
C1primary_metric未出现在类定义中(只在 L242 算法示例里被读)类属性,子类必须声明(exact_match / reward)文档,另见 H3
C2train/dev/test未出现在类定义中(L211、L243 被读)基类属性,load() 未调用时 raise(benchmark.py:75)文档(含「先 load 再取 split」的时序)
C3构造签名无 __init__基类 __init__(self),但四个子类全是 __init__(self, config)实现,见 H1
C4Benchmark.load_from_config(config)L209不存在;runner 直接 benchmark_type(config.benchmark)(runner.py:56)文档(删;与 C3 一并解决)
C5grade() 的前置条件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)文档(删前一段,它与实现完全相反)
D2evolve() 返回类型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文档(最大一块,见下)
D4Results 类型不存在_evaluate() 的返回值,带 .score/.per_task(trajectory.py:63)文档
D5算法是否读 primary_metric—skillopt:117 和 ace:148 仍在读它判断单条轨迹成败,而 _evaluate 返回的是聚合结果待定,见 H3

D3 涉及算法作者需要了解的基类能力,早期 mvp_doc 没有集中说明,接入者只能从现有算法反推。 当时建议补一张「基类提供的动作」表,以减少接入成本。

E. Trajectory / Task ​

#契约mvp_doc实现建议
E1task 是否必填L192 task: Task 必填task: Task | None = None——ask()/edit() 的轨迹确实没有 Task(llm.py:58、:64)文档(Worker 问答不属于任何任务,这是实质差异不是笔误;与 C5 配套)
E2metrics 默认值L196 = (),但类型标的是 Mappingfield(default_factory=dict)文档(文档处是笔误)
E3可变性L191 普通类frozen=True, slots=True,三个映射字段在 __post_init__ 转 MappingProxyType文档(与 L106「文件映射与文件条目均不可变」是同一类约定,文档只对 Snapshot 说了)
E4TaskL58-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 契约只在子类里体现,同一段样板重复了三份:

python
# _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_scopeobservation_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)。


当时建议的执行顺序 ​

  1. 明确 H3 和 H4 的接口取舍,同步更新相关接口说明。
  2. 回写 mvp_doc:A/B/C/D/E/F 共 32 项,集中更新可避免混用不同版本的接口描述。
  3. H1(一个可独立 review 的小步骤),H2 和 H4(a) 可以顺带。

Released under the Apache License 2.0.