f8e02290f4
0006 定「叫什么、什么形状」:五个接缝的 Protocol 名与签名、公共类型的英文名与 字段清单、类型分到 types / ports / tools 三个模块的判据。 0007 定「同一个签名下什么算对」:三个动作状态的触发条件、动作被拒绝时观察由库 合成而不取执行器那段、解释器不许抛异常、read_log 读不存在的运行返回空日志。 两份拆开是因为后者的权威处按 §0 是 tests/contract/,design doc 只记当初为什么这么定。 这两份改动了 0003 四处,全部在文首登记:记录集合是六种东西不是五类; 参数视图是方法不是字段;预算是四项不是两个计数;ports 装「Protocol 与它们的 入参/返回结构体」那半句写不出来——照它写 types 会反向依赖 ports。 四处全是「把字段类型逐个写出来」这个动作本身逼出来的,纯读文档看不见。 四轮评审:两轮硕士生冷读报了约 45 条,两轮 Codex 对抗审查报了 13 条, 逐条核实后基本全部成立并修完。最后一轮是唯一一次契约测试与文档互相抓到对方的错—— 文档改了方法名测试没跟,测试把 dissect 的动作语言写死成输入会误杀 GovDoc 的实现。 结论回写 architecture.md:第七节补类型归属判据,第八节改 ports 那一行, 第九节补五个 Protocol 的英文名,第十四节把「英文名还没定」那条缺口换成指向; 决策索引加两行。字段表刻意不回写——按 §0 那是代码的权威。 CLAUDE.md 与 README.md 开头的「一次 Agent Session」是术语漂移,改成「一次运行」。 CLAUDE.md §7 加两条工作方式:能压成一段结论的活尽量交给 subagent、 委托出去的活交证据不交判断;以及持续往下做,只在人类门和真判断不了的岔路停。 §8 那句「讲完停下来等回应」与后者打架,收窄到只管说话方式。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
201 lines
14 KiB
Markdown
201 lines
14 KiB
Markdown
# 迁移:dissect
|
||
|
||
> **更新触发点:第 2 档(同提交同改)。** 公共类型或接缝语义变更时,在同一个提交里核对
|
||
> 本文的组件映射与需求条目;dissect 侧真正搬走某一块代码时,在同一个提交里勾掉删除清单
|
||
> 对应行。
|
||
>
|
||
> **当前状态:一行代码都还没搬。** 组件映射一栏填的是接缝名而不是类名,因为 `src/` 还不
|
||
> 存在;等第 ③ 阶段架构定了再补具体类型名。
|
||
|
||
dissect 是 PolyLoop 唯一做硬迁移的消费者,也是唯一有跑着的代码、有测试、有真实实验在依赖
|
||
的消费者。它的循环在 `reference/dissect/harness/agent/`,五个模块加起来 1158 行,配套测试
|
||
(`reference/dissect/tests/agent/`)1278 行。
|
||
|
||
**验收口径:把那套循环搬到 PolyLoop 上之后,dissect 现有测试全绿,且真实实验跑出来的轨迹
|
||
与迁移前逐字段可比。** 这是 PolyLoop 唯一的硬验收标准,另外两个消费者走设计级验收
|
||
(见 `govdoc-saas.md`)。
|
||
|
||
## dissect 的项目术语
|
||
|
||
这些词是 dissect 的,不是 PolyLoop 的——库不认识它们,本文和 dissect 侧的代码注释里会用到。
|
||
PolyLoop 自己的词表在 `../explanation/architecture.md` 第二节。
|
||
|
||
**rollout**——被试 agent 在一道题上跑完的一次完整过程。它正好对应 PolyLoop 的「一次运行」,
|
||
所以迁移之后 dissect 的一个 rollout 就是一次 `run`。
|
||
|
||
**三本账**——dissect 把模型调用按用途分成三类分别记账:做题的(生成)、跑验证集的(评估)、
|
||
让模型反思改进的(反思)。分开记是它的实验纪律要求的——比较两个配置时,双方的**总花费必须
|
||
对等**,而混在一本账里就算不清谁在哪一类上多花了。每次调用记哪一本由调用方指定,循环本身
|
||
不选。
|
||
|
||
**best-of-N**——同一道题独立重做 N 次、取最好的那次。它是一个「同预算基线」:如果一个花哨
|
||
的方法效果好,得先证明它比「同样的钱拿去重做 N 次」更好。
|
||
|
||
**因子**——dissect 要测的设计变量,十来个,每个能独立扫几档取值。它整个项目就是在测每个因子
|
||
的效应量,所以任何被库写死、又会影响成绩的取值,都可能污染某个因子的测量。
|
||
|
||
**AppWorld**——一个交互式 benchmark(给 agent 一批模拟的 app 和一个任务,看它能不能用代码
|
||
操作那些 app 完成)。它是 dissect 的第一个任务,dissect 的提示词格式和多代码块处理策略都跟
|
||
它的官方实现对齐,为的是成绩可比。
|
||
|
||
**缓存成本校正的锚点**——供应商对「命中缓存的前缀」按折扣价计费。要事后算清一次运行真实
|
||
花了多少,得知道哪些前缀被缓存了;而一次上下文截断会把前缀打断,之后每一步重新全价,
|
||
原来那个推算的参照就没了。
|
||
|
||
## dissect 是什么,为什么它的约束特别硬
|
||
|
||
dissect 是一个论文项目,研究「agent 的文本自我进化为什么有效」。它把自我进化拆成十来个可以
|
||
独立操纵的设计因子,用受控实验测每个因子的效应量。**被试 agent 的每一次运行都是一个实验
|
||
数据点**,轨迹文件是论文的原始数据。
|
||
|
||
这带来一批别的项目没有的约束。轨迹里少记一个字段,事后补不回来——那次运行已经发生过了。
|
||
一个本该被记录的失败被静默吞掉,会以「效应」的形式进入统计。所以下面「需求条目」那一节里
|
||
的每一条,都是从这个性质推出来的,不是偏好。
|
||
|
||
## 删除清单
|
||
|
||
「继任」指这块代码的职责由 PolyLoop 承担,dissect 侧删掉。「留下」指它是 dissect 的项目
|
||
资产,PolyLoop 只提供它要实现的接缝。
|
||
|
||
| dissect 侧 | 行数 | 去向 |
|
||
|---|---|---|
|
||
| `harness/agent/loop.py` 的 `ReActAgent._loop` 与 `_take_step` | ~130 | 继任 |
|
||
| `harness/agent/loop.py` 的 `_make_step` | ~35 | 继任 |
|
||
| `harness/agent/loop.py` 的 `_observe` 与 `_is_done` | ~40 | 继任(成为动作执行接缝的语义) |
|
||
| `harness/agent/loop.py` 的 `ReActAgent.run` 装配部分 | ~75 | 留下(组装 Rollout 头、传项目 metadata) |
|
||
| `harness/agent/memory.py` 的 `StopReason` | ~25 | 继任 |
|
||
| `harness/agent/memory.py` 的 `Step` | ~65 | 继任(字段是库的下界,见需求条目) |
|
||
| `harness/agent/memory.py` 的 `Rollout` | ~90 | 留下 |
|
||
| `harness/agent/memory.py` 的 `render_messages` | ~40 | 继任 |
|
||
| `harness/agent/context.py` 的 `ArtifactView` | ~20 | 继任(成为 Skill 注入的输入形态) |
|
||
| `harness/agent/context.py` 的 `build_prefix` 段顺序约束 | ~15 | 继任(约束本身,不含渲染格式) |
|
||
| `harness/agent/context.py` 的 `PromptTemplate` 与 `_split_by_role` | ~95 | 留下 |
|
||
| `harness/agent/parser.py` 全部 | 182 | 留下 |
|
||
| `harness/agent/config.py` 的预算字段 | ~10 | 继任 |
|
||
| `harness/agent/config.py` 的 YAML 加载与校验 | ~140 | 留下 |
|
||
| `harness/envs/protocol.py` 的 `Episode` | ~60 | 继任(成为动作执行接缝) |
|
||
| `harness/envs/protocol.py` 的 `ScoredEpisode` / `TaskEnv` / `TaskItem` / `ItemScore` | ~120 | 留下 |
|
||
|
||
标「继任」的十行相加,净删除约 440 行,其中循环本体和轨迹结构占大头。
|
||
|
||
### 三个「留下」值得解释
|
||
|
||
**`Rollout` 留下**,因为它的二十个头字段绝大多数是项目 metadata——运行标识、进化轮次、
|
||
题目标识、场景标识、数据划分、用途、随机种子、尝试序号这些,PolyLoop 一个都不认识。
|
||
库只返回它自己知道的事实,不反向吸收这些字段——吸收了就等于替另外两个下游做了它们不需要
|
||
的假设。
|
||
|
||
**`parser.py` 留下**,因为它是 dissect 的动作语言。它把模型输出里的 Python 代码围栏抽出来,
|
||
处理未闭合围栏、空围栏、多围栏这些情况,并且刻意把第一个围栏之后的文字截掉(模型常在
|
||
代码块后编造「执行结果」)。这套规则是 dissect 和 AppWorld 官方对齐的口径,不是通用的。
|
||
PolyLoop 提供的是「把模型响应解释成动作、最终回答或无效决策」这条接缝,`parser.py` 是它的
|
||
一个实现。
|
||
|
||
**`config.py` 的加载与校验留下**,因为 dissect 有一条硬纪律:所有实验参数从 YAML 读入、
|
||
没有任何默认值、多余的键也要报错。这套「配置双模式」是它自己的规矩。PolyLoop 只接受
|
||
已经组装好的预算值。
|
||
|
||
## 需求条目
|
||
|
||
这些是 dissect 的代码和它的实验有效性对 PolyLoop 施加的硬约束。每一条都能指出一个具体的
|
||
失败后果。
|
||
|
||
**一、停止原因必须是枚举,而且要能区分「恰好在最后一步做完」与「预算耗尽」。**
|
||
从轨迹长度反推不出来,两种情况的长度一模一样。dissect 的崩坏判据要按停机原因分层,这一档
|
||
分不开,整批数据的分层就塌了。
|
||
|
||
**二、上下文超限必须显式终止,不许静默截断。** 截断是一次前缀破坏操作,会让后续每一步
|
||
重新按全价计费,并把缓存成本校正的锚点搞丢。而且被截断的运行会表现成一批低分,看起来像
|
||
模型能力不足。
|
||
|
||
**三、三类没碰环境的步也必须留痕:解析失败、模型调用失败、环境故障。** 前者消耗了一次
|
||
模型调用,不计的话预算对等不成立。后两者的情形是钱已经花了、账已经记了,丢掉那一步会让
|
||
账目与轨迹对不上,而且会丢掉模型在出故障那一步说了什么——排查「是环境坏了还是模型写了
|
||
危险代码」最需要的就是这段原文。
|
||
|
||
**四、模型调用标识是轨迹与账目之间唯一的连接键,它可以是「没有」,但不能是空串。**
|
||
空串是个看起来合法的键,连表时静默匹配不上;显式的「没有」至少能被筛出来。它为空的合法
|
||
含义只有一个:调用在记账之前就失败了。
|
||
|
||
**五、消息段必须按变化频率从低到高装配。** 稳定前缀在前、逐题变化的在后。排错了不会报错,
|
||
只会让供应商的 prompt cache 静默失效,而多付的幅度随注入内容的规模变化——于是缓存伪影会
|
||
精确地伪装成因子效应。
|
||
|
||
**六、历史只能追加,不能改写或重排。** 理由同上:第 n 步的提示词正好是第 n-1 步加一段尾巴,
|
||
任何中途截断、摘要、重排都会让后续每一步重新全价计费。
|
||
|
||
**七、模型原文与真正进入历史的文本必须分开记,环境观察同理。**
|
||
dissect 的解析器会把第一个代码围栏之后的文字丢掉,所以「模型说了什么」和「下一轮模型看见
|
||
什么」是两个不同的量。混用会让统计口径出错。
|
||
|
||
**「分开记」记的是一段文本加一个数字,不是两段文本。** dissect 两侧都这么做:模型那侧存
|
||
`raw_output`(解析器截断之后的,正是回填进历史的那段)加 `content_chars`(模型可见输出的
|
||
全长);观察那侧存 `observation`(进历史的那段)加 `observation_truncated_chars`。照「两段
|
||
文本」读会得出「库少了一个字段」的结论,那个结论是错的,见
|
||
`../design/0005-storage-atomicity-and-record-fields.md` 决策三。
|
||
|
||
**八、可见回复与推理段的长度按字符记,不依赖上游上报。**
|
||
实测中转网关会用本地分词器补算并整体替换用量对象,把明细一起吃掉——某次标定里 24 次调用
|
||
的推理 token 全部没上报。字符数直接数,不受上报与否影响。
|
||
|
||
**九、一个 agent 定义可以被并发驱动跑不同的题,任何实例级可变状态都不允许。**
|
||
dissect 有一档实验要在同一时刻用不同的注入内容跑同一批题,实例级状态会让并发的候选互相污染。
|
||
|
||
**十、循环拿不到分数。** 让被试拿得到真值等于开后门,实验当场作废。评分必须在会话关闭前由
|
||
外层用宽接口完成,PolyLoop 从始至终只见窄接口。
|
||
|
||
## 组件映射
|
||
|
||
接缝的完整清单与各自职责在 `../explanation/architecture.md` 第九节,本表只做映射。
|
||
|
||
| dissect 侧 | PolyLoop 侧 | 备注 |
|
||
|---|---|---|
|
||
| `LedgerClient.chat` | 模型调用接缝 | dissect 的三本账记账继续作为绑定上下文的实现装配进来 |
|
||
| `parse_code_action` | 决策解释接缝 | 三分支:动作 / 最终回答 / 无效决策 |
|
||
| `Episode.execute` | 动作执行接缝 | dissect 侧只有「返回文本」和「抛异常」两种结果,状态恒为「已执行」 |
|
||
| `Episode.is_done` | 动作执行接缝返回值上的完成信号字段 | **不是独立接缝**,见 architecture.md 第九节 |
|
||
| `ArtifactView`(通道、条目标识、内容三字段) | Skill 注入的输入形态 | |
|
||
| `build_prefix` 的段顺序约束 | **不是接缝**,是库内 `_assembly` 的纯函数 | 渲染格式留在 dissect,理由是排除条款(见 `../explanation/scope.md`) |
|
||
| `AgentConfig` 的三个数值上限 | 请求上的预算 | `max_steps`、`max_consecutive_parse_failures`、`max_prompt_chars`;库的预算比它多一个已执行动作数上限 |
|
||
| `StopReason` | 停止原因 | dissect 现有六个取值是库的下界 |
|
||
| `Step` | 逐步轨迹的一步 | dissect 现有十三个字段是库的下界 |
|
||
|
||
具体类型名与签名按 `../../CLAUDE.md` §2 要过人类门,还没定;定了之后本表补上英文名。
|
||
|
||
## 缺口登记
|
||
|
||
**轨迹文件的格式是反思模型的唯一输入界面,迁移后要由 dissect 自己重组。**
|
||
`Rollout.to_jsonl` 现在把项目 metadata 头和逐步轨迹写在同一个文件里,第一行是头、后面每行
|
||
一步。PolyLoop 只返回运行标识和步序列,所以 dissect 要自己把两者拼回那个格式,而且格式
|
||
不能变——现有的分析代码和不变量检查器都在读它。这是一笔真实的迁移成本,不是设计缺陷。
|
||
|
||
**模型是正常收尾还是被输出长度上限砍断,现在拿不到。** dissect 的 `Step` 里没有这个字段,
|
||
原因是 PolyGateway 的响应类型不透出它。这个信息有实际价值——输出被砍断时代码只写了一半,
|
||
表现成解析失败或语法错误,会与「模型不会做」混淆。迁移后仍然拿不到,要补得给 PolyGateway
|
||
提 PR。登记在这里是为了迁移时不要误以为是 PolyLoop 弄丢的。
|
||
|
||
**动作被拒绝这个概念 dissect 侧没有。** 它的环境执行只有两种结果:返回文本(代码报错也算
|
||
正常观察)或者抛异常(环境故障)。PolyLoop 的动作执行接缝要区分「已执行」和「未执行」,
|
||
是因为另一个消费者需要。dissect 侧的实现状态恒为「已执行」,这一点要在迁移时确认不会改变
|
||
它的步数统计口径。
|
||
|
||
### 由 `../design/0003-public-api-shape.md` 新产生的四项成本
|
||
|
||
**每个 episode 要写一个包装类。** dissect 现有的 `Episode` 协议与 PolyLoop 的动作执行接缝
|
||
**不构成结构化子类型**:那边是 `execute(action: str) -> str` 与 `is_done() -> bool`,
|
||
而库这边要的是一个返回结构体(状态、观察、是否合成、完成信号、截断字符数)的方法。
|
||
所以不能直接把 `Episode` 传进去,每个 benchmark 的适配器要多一层薄包装。
|
||
|
||
**续跑路径要改。** dissect 的 runner 现在把「没有结果行的尝试」重新排进待办,用完全相同
|
||
的六维主键第二次调用循环。迁移后这条路要改成调「接着跑」而不是「再跑一次」——按
|
||
`../design/0003` 的前置条件,用同一个运行标识第二次调「跑一次」会直接报错。
|
||
|
||
**要选一个存储实现并给它一个目录。** 存储接缝是必填的,不传就装配不起来。dissect 如果暂时
|
||
不要恢复能力,要显式装配那个明确命名的、不提供恢复的内存实现——「我不要恢复」是一次
|
||
看得见的选择,不是一个可以忘记传的参数。
|
||
|
||
**模型绑定要从关键字参数还原。** dissect 现在给每次调用传五个关键字参数(账本、轮次、
|
||
阶段、题目、尝试序号)。库这边接的是一个字符串映射,所以适配器要做一次还原(把账本那个
|
||
字符串转回枚举、把轮次和尝试序号转回整数)。这是适配器该干的活,收益是这五维能原样进
|
||
参数快照。
|