Files
PolyLoop/research-wiki/design/0001-scope-boundary.md
T
iomgaa 4f8812fa82 docs(design): 落成边界、续跑、公共 API 形状与停止语义四份决策
第 ② 阶段需求对齐与第 ③ 阶段架构的产出,代码尚未开始。

design/0001 定边界判据:三道测试(时机 / 信息 / 性质)全过才在界内,
外加「只认接缝、不认接缝后面是什么」与不夺走下游实验因子的排除条款。
design/0002 定步级续跑:不承诺原子性,承诺绝不静默丢失与不替工具猜幂等性;
先写意图再执行、结果 ID 预分配、重放策略由工具声明且默认绝不重放。
design/0003 定公共 API 形状:单一入口两个动词、五个接缝、三个伪接缝的排除理由、
分层与九条依赖规则。design/0004 定停止判定顺序、十个停止原因取值与步记录字段表。
0003 与 0004 需过 CLAUDE.md §2 人类门,已由项目负责人确认,状态转为已接受。

explanation/scope.md 与 explanation/architecture.md 是这四份决策的常青回写,
分层与模块边界的权威在 architecture.md,将来由 import-linter 契约机器断言。
migrations/ 下 dissect 是唯一的硬迁移验收,govdoc-saas 只做设计级对齐。

三道闸都过了:14 agent 对抗辩论定骨架,两轮硕士生阅读报的 30 余条已修完,
Codex 对抗审查抓出的两条致命问题(提交型完成被误判成环境故障、
崩溃恢复漏一个状态)已修,修完的形状还没送 Codex 复审。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:48:33 -04:00

10 KiB
Raw Blame History

Design 0001 · PolyLoop 的边界

日期 2026-08-07 · 状态 已接受

背景

需要的三个前提

PolyLoop 是一个库,它的治理单位是一次 Agent Session。 所谓一次 Agent Session,指围绕 一个目标的有界多轮循环:模型做一次决策,执行一个动作,把观察写回历史,再决策,直到完成 或者撞上某个停止条件。一次模型调用本身不归它管,那是 PolyGateway 的治理单位。

「执行内核」与「阶段编排」是两种不同的东西,日常都被叫作「agent 框架」。

执行内核持有上面那个循环,管预算、停止判定、逐步轨迹、取消。阶段编排是另一层:它把一个 大任务切成几个固定阶段,每个阶段跑一次执行内核,阶段之间交换状态,并检查每个阶段的产物 合不合格。

四个已知消费者里,两种都有真实实现。dissect 的 harness/agent/loop.py 是执行内核。 GovDoc-SaaS 的 packages/docagent-core/ 两种都有——agent/loop.py 是执行内核, workflow/ 是阶段编排(它的 phase.py 第一行注释写着「PES 三阶段泛化为 N 阶段」)。 GovDoc-Editor 今天在生产上跑的是阶段编排,而它把执行内核外包给了 Claude Agent SDK。

四个消费者的成熟度差得很远。 dissect 的循环是活的,有测试,跑在真实实验里。 GovDoc-Editor 是活的生产系统,但形态是阶段编排。GovDoc-SaaS 正在重构,它的执行内核代码 移植自一个已废弃的项目,且业务层一次都没调用过。CHSAnalyzer 的评估层和诊断层还是空的, 它自己的架构文档明确写了不为它们预留结构。除这四个之外,实验室还有若干将来可能接入的 项目,现在连需求形状都不知道。

问题

边界画错有两个方向,代价不对称。

画大了:为一个还不存在的需求写代码。这类代码删不掉——它会长出配套的字段、分支和测试, 而且一旦下游装上了,公共类型的字段就受「只增不删不改名」的约束,真要删得发新 major。

画小了:每个下游各写一遍同样的东西。这正是抽库要消灭的事,而且写三遍就有三份 bug。

真正麻烦的不是这两个方向本身,是没有判据。当前只有 dissect 一个硬消费者,如果不给判据, 库会不知不觉长成 dissect 的形状;而每来一个新项目,「这个该不该进库」就要重新吵一遍, 每次吵的结论还未必一致。

决策

一、只做执行内核,阶段编排留在项目侧

三条理由。

执行内核有两份独立的一手证据——dissect 和 docagent-core 各写了一遍。阶段编排只有 GovDoc 一家,dissect 连「阶段」这个概念都没有。

依赖方向是单向的。docagent-core 自己的 import-linter 契约就写着子包互不依赖,唯一豁免是 workflow 依赖 agent。所以先做执行内核不会走进死胡同:阶段编排将来无论留在项目里还是抽成 另一个库,都是在执行内核之上加东西,不需要回头改它。

反过来做会把 dissect 挡在门外。

二、判据:一个目标之内的事在界内,多个目标之间的事在界外

这句话本身判不了案,配三条判据,三条全中才在界内

  1. 时机 —— 它发生在一次运行开始之后、返回之前。
  2. 信息 —— 它不需要知道这次运行之外的任何事:上一次运行的结果、下一次运行是什么、 此刻还有几次运行在跑、这个目标是谁派下来的。
  3. 性质 —— 它回答「怎么跑」,不回答「跑什么」「为什么跑」「跑得好不好」。

再配一条补充规则,否则第一条会被滥用:只认接缝,不认接缝后面是什么。 检索、沙箱、 容器、工作区目录,库一概不知道——它只知道有一个环境接口,交给它一个动作、拿回一段观察。 没有这条,「检索发生在一次运行之内,所以检索该进库」这种推理就成立了。

补充规则有个好用的等价说法:能表达成「环境提供的一个动作」的,就在界外。 检索能, 沙箱执行能,写文件能,调外部 API 能。而「记这一步花了多少预算」不能——没有任何环境会 提供这么个动作,那是库自己的算法。

当前的完整裁决清单在 ../explanation/scope.md,那份是常青文档,会随新消费者接入而更新。 这里只记几条有代表性的推理,因为它们是判据怎么用的示范。

评分和业务正确性判定在界外,栽在「性质」上:它们回答「跑得好不好」。这条对 dissect 是 硬要求——让循环拿得到分数等于给被试开真值后门,实验当场作废。

阶段产物校验在界外,同样栽在「性质」上:required_outputs 判的是「这个阶段的目标达成 没有」,和评分同类。但要注意「校验」这个词底下压着三件不同的事,另外两件在界内:模型输出 能不能解析成一个动作(那是解析接缝),以及动作参数合不合法(那是环境裁决,库定语义)。

工具的注册与分发机制在界内,而且必须由库提供。 三条判据全过,但更硬的理由是:模型看见 的工具 schema、「这个工具存不存在」的校验、以及最后真正分发的那张表,三者必须同源。 不同源就会漂移——模型看见一个已经删掉的工具,或者校验放行了一个分发时找不到的名字。库不 提供的话,每个项目自己写一个注册表,就等于每个项目自己漂移一次。

具体某个工具的实现要分两类。 通用工具(读写文件、grep、跑 shell)不含业务概念,可以 作为可选装配放进库。业务工具(查法条、读超声图像、向 benchmark 提交答案)留在项目。检索 属于后者:三个下游里只有 GovDoc 需要,而它已经有自己的检索引擎;把检索放进来还会拖进 embedding、向量库、文本切分一整套依赖,而这些东西的形态差异极大,抽出来那个八成谁都不合用。

「接着跑」在界内,「知道要接着跑、以及从哪儿读」在界外。 崩溃后恢复这件事被当成一个词 用,实际要拆开:拿着一段已有历史从中间继续,三条判据全过;而判断该不该续、历史存在哪、 怎么读回来,要知道这次运行之外的事。展开见 0002-step-level-resume.md

三、准入规矩:界外的东西要进来,先有两个真实消费者

「真实消费者」指有跑着的代码或已冻结的设计,不包括「将来可能需要」。在满足这个条件之前, 界外的需求只登记不实现,也不为它预留结构

不预留是刻意的。预留一个接缝的成本不是零:它会出现在公共类型里,受兼容性约束,还要有人 维护它的测试;而它守护的那个形状是猜出来的,等真需求到了多半不合用,那时候拆比重写贵。 CHSAnalyzer 在自己的架构文档里对同一件事的判断是「猜出来的接缝比没有接缝更难拆」,这里 采用同一个判断。

否决的方案

执行内核和阶段编排都做。 GovDoc-SaaS 已经写了一版 PhasedWorkflow,看起来不用白不用。 否决理由是阶段编排只有一个消费者验证过它的语义,而 dissect 用不上那半个库——一个下游只用 一半的库,另一半就没人替它挑错。另外阶段编排的形状分歧很大:GovDoc-Editor 是固定三阶段 靠文件交换状态,docagent-core 泛化成了 N 阶段带断点续跑,两者已经不是一个东西了。

列一份「界内功能清单」,不给判据。 这是最省事的做法,也是最先失效的。清单没法回答 它没列到的东西,而实验室将来要接入的项目现在连需求形状都不知道;每来一个新项目,清单就要 补一次,而补的人手上没有判据,只能凭当时的感觉。判据的价值正在于它对没见过的情况也能出结论。

reference/agent-core.md 那份提案画边界。 那是别人为本项目写的架构提案,不是本项目 的决策。它对执行内核和阶段编排的切法与这里基本一致,这一点是它的贡献。但它的证据基础是 两个消费者(其中一个的形态与今天的实际情况已经对不上),而且它把崩溃恢复整个推到了库外, 与本文的裁决不同。按 ../../CLAUDE.md §0,那份文档不是任何事实的权威,其中任何一条在被 本项目的 design doc 明确采纳之前都不作数。

为不确定的未来消费者预留接缝。 见上面「准入规矩」那一节的理由。

代价

当前只有一个硬消费者,库有长成 dissect 形状的风险。 这是最实在的代价,没法消除。对策是 GovDoc 那边不做迁移但做设计级验收:把 GovDoc-Editor 今天在跑的阶段编排、以及 docagent-core 的 PhasePhasedWorkflow 当成一份需求清单,逐条问「PolyLoop 能不能承载它」,答不上来的 就是边界缺口,登记进 ../migrations/

「性质」那条判据有主观空间。 「怎么跑」和「跑得好不好」在多数情况下分得清,但边缘情况会 有争议。缓解办法是按 ../../CLAUDE.md §3 那条:结论有分歧时,以能否指出具体失败场景为准。

「两个真实消费者」这条规矩会拖慢真需求。 一个只有 dissect 需要、但确实属于执行内核的 机制,按规矩要等第二个消费者。接受这个代价,因为反方向的错误更贵:一个提前进库的错抽象会 同时污染所有下游,而且受兼容性约束删不掉。

结论落到哪里

本文的裁决清单落在 ../explanation/scope.md(常青层,会随新消费者更新),判据本身也在那里。 ../../CLAUDE.md §0 的权威表指向它。代码还不存在,所以现阶段没有机器兜底——将来 pyproject.toml 的 import-linter 契约能守住其中一部分(不反向 import 下游、业务概念不进内核), 但「这个机制该不该进库」这类判断守不住,只能靠评审。