Files
PolyLoop/CLAUDE.md
T
iomgaa 8f5caa0924 docs: CLAUDE.md 补上发布的判据与代理这条环境事实,清掉三处过期记载
§1.10 那条指向的发布指南现在有了,同时补两件调研 PolyGateway 时核实到的:它当年那次
补救只写了文档没有回补上传,所以 1.0.6 与 1.1.0 到今天仍然不在 registry 上,而 dissect
的依赖恰好钉在那个空区间里装不上——记下教训不等于修好问题;以及发布完成的判据是外部可见
结果不是本地步骤跑通,1.1.2 三步全绿而包页面是空白的。

§4 新增一条:这台机器的代理到不了外面,访问实验室 Gitea 的命令都要绕开,否则失败看起来
像服务器挂了。过期记载三处:conda 环境早就建了、契约测试早就写了、十个模块早就不是空骨架。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 00:12:46 -04:00

25 KiB
Raw Blame History

PolyLoop

实验室共用的 Agent 执行内核。治理单位是一次运行:围绕一个目标的有界多轮「模型决策 → 动作 → 观察」循环,含预算、停止语义、取消、逐步轨迹与 Skill 注入。一次模型调用本身不归它管,那是 PolyGateway 的治理单位;PolyLoop 用 PolyGateway 的顶层公共 API,不重建一套模型治理。

首批消费者是 dissect 与 GovDoc-SaaSCHSAnalyzer 是远期消费者。

回复用简体中文;代码与标识符用英文。commit message 是「英文前缀 + 中文正文」,形如 feat(session): 停止判定顺序落成代码(前缀是 类型(范围) 那套约定,范围可省)。

这是库,不是应用。 它的 bug 会同时击穿所有下游项目,所以稳定性、并发正确性、防御校验、可观测与测试不为「简单」让步——YAGNI 仍然适用,但不削减健壮性。 能交给机器的就别靠自觉——能写成 CI、ruff 规则、import-linter 契约或测试的就去写,写不出来的至少要能在 §3 那轮评审里被指出来。本文件本身只是给协作者(人和 AI)的上下文,不是强制层,真要拦住某个动作得靠 CI 或 hook。 本文件不放临时内容。 会过期的东西(当前阶段、进行中的迁移、临时约定)放到它自己的权威处,这里只留一条指向那里的常青规则——否则过期条文会留在这里没人记得删。


0. 事实的解释权(冲突时按此裁决,不要自行调和

这类事实 权威处
哪些事归本库管、哪些不归,以及判据 research-wiki/explanation/scope.md
分层、依赖方向、模块边界 research-wiki/explanation/architecture.md,由 pyproject.toml 的 import-linter 契约机器断言。九条依赖规则里有两条落不进契约(「不许 import 任何第三方」不是可枚举清单,「import 之后 sys.modules 里没有谁」是运行时事实),它们是 tests/unit/ 里的测试
公共 API 的行为契约:一次 run 到底保证什么、边界条件怎么结算 tests/contract/ 的公共契约套件。它同时是任何新适配器的准入标准
公共类型的字段、不变量、枚举取值 src/polyloop/ 的代码与其测试。不另写一份参考文档复述它们——那份文档不重复代码的内容太少,而它腐烂的速度和代码一样快
每个下游项目要迁走什么、迁完算不算数 research-wiki/migrations/ 下对应那份
已定的决策及其理由 research-wiki/design/ 下相关编号最大的那份
某个机制、约束、坑为什么是这样 research-wiki/explanation/
其余查得到的事实:日志字段契约、遥测口径 research-wiki/reference/
当前进度:处在哪个阶段、哪些已完成 README.md 的阶段清单
已发布的版本与每版改了什么 CHANGELOG.md
文档体系怎么组织、新文档该放哪 research-wiki/README.md
协作规则 本文件

reference/ 不在上表里,因为它不是任何东西的权威。 那里的六个仓库和 agent-core.md 地位相同,都是参考资料。agent-core.md 是别人为本项目写的一份架构提案,它不是我们的设计,也不是常青文档——其中任何一条在被我们自己的 design doc 明确采纳之前都不作数。引用它时必须写成「agent-core.md 的说法是……」,不能写成「我们决定……」。代价是每次多写一句话,收益是不会长出「大家都以为这个决定已经做过了」的状态——那种状态在上表的裁决规则下最难修,因为它没有一个错的地方可以指。

reference/ 只读、不入库,也不改。

复述规则:论证可以复述,参数不许复述。

同一个道理在几处各讲一遍是好事——人类读者希望在一份文档里把事情读懂,而不是在几份之间反复跳转,而论证不会漂移,最多某一处写得不如另一处好。但同一个参数(数字、路径、文件名、类型名、枚举取值、命令行的具体形状)只在上表的权威处出现一次,别处引用它:那种东西迟早会有一处被改、另一处没改。

代价是会出现另一种漂移:两处的道理打架(一处说「为了可复现所以冻结」,另一处说「为了省内存所以只留引用」)。机器查不出来,靠 §3 那轮独立评审兜。展开见 research-wiki/README.md

为什么冲突时禁止自行调和。 把两边捏合成一个折中说法,看起来是负责任,实际上会生出第三个没人认过的版本,而且把「有一处已经漂移了」这个真正需要修的信号盖掉了。按表裁决则相反:它逼你去改错的那一处,漂移当场被消灭。

本文件只管协作约定,不管项目事实。 与上表任一文件冲突时以那边为准,并顺手把本文件改对。改本文件本身不需要请示——但如果改的是 §1 的硬约束或 §2 的人类门,先说一声再动。

1. 硬约束

  1. 零业务假设。 库内禁止出现下游的业务词汇(公文、审核点、超声、CHS、benchmark、实验轮次、得分)与业务 fixtures;扩展点一律用 Protocol。三个下游的领域互不相交,一个业务词进来就等于替其中一个项目做了另外两个不需要的假设。这类假设很难删——它会长出配套的字段、分支和测试,删的时候要一起动。
  2. 不反向 import 任何下游项目。 由 import-linter 契约断言。
  3. 公共类型的字段只增不删不改名,新增字段必带默认值。 三个下游各自 pip install 本库,改名会让已经在跑的代码直接 ImportError 或静默拿到默认值。要删要改就发新 major 并写迁移指引。
  4. 持久化结构的 schema 变更走显式版本,不靠默认值补齐。 会被下游存进数据库或实验数据集的结构(运行结果、逐步轨迹)必须带独立的 schema 版本,读到未知 major 直接失败。dissect 的轨迹是论文实验数据,一次静默的默认值填充会把「这件事没发生过」改写成「发生了但值为空」,而这种损坏要到统计阶段才暴露,那时已经分不清哪些行是真的。
  5. 模型调用一律走 PolyGateway(实验室共用库)。不在本项目里另写重试 / 限流 / 熔断 / 缓存 / 遥测。缺能力就给 PolyGateway 提 PR。
  6. asyncio.CancelledError 永不捕获吞没。 取消要能穿过模型调用与环境执行,in-flight 资源在 finally 释放。吞掉它的后果不是「取消失败」这么直白——是容器租约、连接和临时目录持续泄漏,而且一声不吭。
  7. 禁止吞掉错误except Exception: pass 及其跨行形态)。由 ruff S110 / E722 断言。
  8. 测试绑行为,不绑实现。 不写「断言某个内部类有哪些方法」这类测试——它只会让重构连坐。 公共 Protocol 的签名是例外:它本身就是对下游的承诺,不是实现细节,所以 tests/contract/ 断言它是应该的。判据是这个名字有没有对外承诺过——承诺过的改名是破坏性变更(§1.3),断言它就是在守那条承诺;没承诺过的改名只是重构,断言它就是在拖后腿。 断言某个名字「不存在」也是允许的,用来守住一次删除决策。一个已经被删掉的字段没法被重命名,拖不动测试。代价是它守的只是名字不是概念——换个名字把同一个概念加回来,测试照样绿,所以理由必须同时写在被删字段所在类型的 docstring 里。
  9. 测试分层按「依赖什么」定,不按「叫什么」定。 用测试替身的是 unit,连真 PolyGateway 的是 integration,打真实模型网关的是 e2e,验证公共 Protocol 行为一致性的是 contract。按名字分层的话,改个函数名就要挪测试文件;按依赖分,只要这个测试还是不连外部服务,它就一直待在原地。四层之间更细的界线在搭测试框架那个阶段定,现在不必较真。
  10. 发布 = 合并 + push + tag + 构建 + 上传 registry + 验证已发布。只 bump 版本号不叫发布。 教训来自 PolyGateway1.0.6 与 1.1.0 都完成了版本号 bump 与 CHANGELOG,却从未上传,registry 长期停在 1.0.5——下游 pip install 拿不到任何修复,且无人发现。那次的补救只写了文档、没有回补上传,所以那两个版本到今天仍然不在 registry 上,而 dissect 的依赖恰好钉在那个空区间里、装不上。这说明记下教训不等于修好问题。完整步骤与全部已知的坑见 research-wiki/guides/releasing.md判据是外部可见结果,不是本地步骤跑通:收尾要以下游视角逐一打开产物——registry 包页面的正文与仓库链接、仓库的 Releases 页、装完之后包里的文件。PolyGateway 的 1.1.2 三步全绿,包页面却是空白的。
  11. 动手前先看 README 的阶段清单。 不要为了还没到的阶段提前写大量代码,也不要为假设中的工作量预先埋好一堆结构——这就是 §6 YAGNI 的意思,只是在阶段这个尺度上再说一次。

2. 人类门(仅以下需要用户批准,其余自行判断)

场景 为什么
改公共 API:公共类型的字段、Protocol 签名、停止原因的取值、停止判定顺序、持久化 schema 版本 三个下游按它写代码,错了会静默扩散,且改动成本随时间指数上升。先写 design doc,等确认再动手
任何外部可见动作:push、开 / 关 PR、动别人的分支 涉及协作者
发布(含打 tag 与上传 registry 下游一旦装上就收不回来
删除或覆盖既有数据 不可回滚

「改公共 API」指的是改变已有承诺的形状——新增一个可选组件并同时补上它的契约测试,属于「写代码 + 补测试」,自行判断即可。区别在于前者会让已经在用的调用方静默失败。

其余(写代码、跑测试、写文档、重构、补测试、写 design doc 草稿)无需请示;何时先讨论设计由你判断

外部 PR 一律不直接 merge。 逐段审阅:符合本仓库规范的代码直接复用,不符合的按规范改写;无论哪种,落地结果必须与原 PR 功能等价,并在提交信息里标明来源 PR 与作者。理由是本仓库靠一套机器可检查的约定维持一致性,而外部分支不在这套约定下产生。

3. 审查规则

四类高风险产物必须 Codex 对抗审查/codex:rescue --fresh --wait):

  1. 公共类型与 Protocol 签名的任何变更;
  2. 主循环的停止判定与预算结算;
  3. 取消传播,以及并发 Session 之间的状态隔离;
  4. 持久化结构的 schema 演进与反序列化。

这四类的共同点是错了不会当场炸。签名改错要等下游升级那天才发现;停止判定顺序错了,「恰好在最后一步做完」会被记成「预算耗尽」,而两者的轨迹长度一模一样;取消漏掉一处只表现为资源占用慢慢往上涨;schema 靠默认值补齐要到统计阶段才看得出来。这类问题人眼复核的命中率很低,因为它们没有失败现场。

Codex 是 OpenAI 的编码模型,本仓库通过 codex 插件调用它。之所以必须换它、而不是再开一轮自家 subagent:同一个模型的盲区是一致的,它审自己写的东西,会以同样的理由漏掉同样的问题。换一个不同来源的模型才可能戳破这层偏见,也能挡住单个模型偶尔的抽风。「对抗」指的就是让它专门去挑毛病,而不是让它确认我们做得对。

其余代码完成后至少一轮独立 subagent 新鲜上下文审prompt 只给 diff、验收标准和相关文档,不给实现时的推理过程。给了推理过程,它会顺着我们的思路复核一遍,只能验证「按这个思路做得对不对」,验证不了「这个思路本身是不是错的」。

文档大改必须过一轮独立 subagent 的「硕士生阅读」。 触发条件:新写一份文档、重写既有文档的整节、或单份文档改动超过约 100 行。开一个新鲜上下文的 subagent,只给它改后的文档,不给我们的讨论过程、不给相关代码——它必须纯靠文档读懂。先问它读的时候发生了什么,再问它查到了什么,两问的顺序不能反。

第一问是阅读行为:哪几段你跳过去了、读到哪儿开始走神、合上文档能不能把这套东西复述一遍。跳读和走神是行为,不是意见,所以它们不受下面那条「不采纳风格建议」的约束——一个读者跳过了某一段,那就是一个关于这段文字的事实。这一问是 CHSAnalyzer 踩出来的:那边的文档里有整段只在讲文档自己(「这一节把整份文档串成一个故事」这类),前面跑过两轮的审查一条都没报,因为当时的 prompt 明令它不许报这类,于是这一整类问题对这道闸天然不可见。

第二问才是三类具体问题:哪句话读不懂、缺了什么前置知识哪个决策只写了结论没写理由;以及同一个参数(数字、路径、类型名、命令形状)在两处取值不同。前两类主观,但那是它们的性质;第三类是确定性判据,报了就是真的。格式、措辞、结构建议一律不采纳——每次都能挑出十条建议,等于没有建议,这轮评审很快就会被跳过。参数一致性不另开一轮检查,也不写成脚本:按 §0 的复述规则参数本来就只在权威处出现一次,撞车机会很少,专设一道检查会长期空转,而空转的检查很快就会被跳过。

审查反馈只采纳影响正确性或明确需求的项;风格类建议自行取舍,防过度工程。结论有分歧时,以「能否指出具体失败场景」为准

4. 环境与运行

  • 这台机器设了 http_proxy / https_proxy,指向一个到不了外面的本地代理。 凡是访问实验室 Gitea 的命令(上传发布产物、验证已发布、从私有源装包)都得绕开它,否则失败的形态是网关错误而不是「代理有问题」,很容易被当成服务器挂了。具体命令在 research-wiki/guides/releasing.mdREADME.md 的安装一节。
  • Conda 环境 PolyLoopPython 3.11。3.11 不是选出来的,是被下游钉死的:dissect 和 GovDoc-SaaS 都跑在 3.11,一个库不能要求比它的消费者更高的版本。
  • Python 命令一律用这个形状PYTHONUNBUFFERED=1 conda run --live-stream -n PolyLoop <cmd>。conda 和 Python 各缓冲一层,两层都得拆:只加 --live-stream 或只加 -u / PYTHONUNBUFFERED 都仍然全程无输出,直到进程结束才一次性吐出。六种组合的实测与原理见 reference/CHSAnalyzer/research-wiki/explanation/conda-run-output-buffering.md(同一台机器、同一套 conda,结论直接适用)。
  • 超过约一分钟的命令(测试套件、压测、真实网关回归)必须放进 tmux 跑,不要阻塞在前台,也不要只丢进后台。tmux 会话人和 AI 都能 attach,可以一起看同一份实时输出、随时中断。会话按用途命名(如 polyloop-e2e),跑完不要急着 kill,留着给人复查。
  • 长跑命令末尾不得接管道。 pytest ... | tail 的退出码来自管道最后一节,于是失败的测试跑会报成 exit 0。要判断完成用 wait 或轮询 PID不要用 pgrep -f "<完整命令串>"——它会匹配到自己,形成永不结束的等待。这两条是 PolyGateway 实测撞出来的,两种失败都以「看起来还在跑」的形态呈现,从外部区分不了。
  • 本库不部署,也不跑模型推理。 所有模型调用经 PolyGateway 出去(§1.5)。这台机器是和别人共用的,本仓库现在没有任何用得着 GPU 的代码;但只要哪天有了,那条命令就必须显式加 CUDA_VISIBLE_DEVICES=<idx>,省略会自动选卡,占掉别人正在用的显卡。

5. 目录说明(★ = 已存在,其余为规划)

只列需要解释的。src/tests/.github/workflows/ 这类看名字就知道装什么的不列。

★ reference/                   参考资料:六个仓库 + agent-core.md
                               只读、不改、不入库,且不是任何东西的权威(§0)
★ research-wiki/README.md      文档体系怎么组织、新文档该放哪
★ research-wiki/design/        动工前的方案与权衡,只增不改;决策变更 = 新写一份标 supersedes
★ research-wiki/explanation/   为什么这样设计(常青,须写明更新触发点)
★ research-wiki/migrations/    每个下游项目迁走什么、迁完算不算数(常青)
★ research-wiki/guides/        怎么做某件事:发布、本地环境、排障
★ research-wiki/reference/     查得到的事实:日志字段契约、遥测口径
                               (公共类型和枚举取值不在这里,权威见 §0 表格)
★ research-wiki/scratch/       一次性草稿。进 git,但由人在每轮工作会话结束前清理(AI 不要自动删)
★ tests/contract/              公共 Protocol 的行为一致性套件,是那份契约的权威(§0),
                               也是任何新适配器的准入标准
★ tests/e2e/                   打真实模型网关,会产生真实费用。默认不跑,两道闸见 .env.example
★ src/polyloop/                库本体,十个模块

常青层与记录层的分界、各类的更新触发点、scratch/ 那条人工清理规则的已知风险,都在 research-wiki/README.md

硬性规则:根目录不得出现 .py;禁止 helpers/common/shared/misc/utils/ 这类目录名——它们的职责是「剩下的东西」,一句话说不清职责就没有边界,最后什么都往里塞。

6. 代码与文档规范

  • YAGNI:不写当前用不到的代码。但健壮性(并发控制、防御校验、可观测、错误隔离、测试)是当前需要,不在削减之列。抽象只在真正易变 / 需替换 / 需造测试替身的接缝处引入。
  • 显式优于隐式:公共函数完整类型注解;依赖注入,不从全局偷取;不用默认参数掩盖关键逻辑。这条在库里比在应用里重一档——下游看不到实现,只能靠签名和类型判断该传什么。
  • 一切外部输入校验后使用:模型返回、适配器返回、配置都算外部输入。校验用显式异常,assert 只用于内部不变量,不承担生产校验——Python 的 -O 会把 assert 整条移除,下游用 -O 跑的那天校验就静默消失了。
  • 文档的目标读者是「没参与过我们讨论的相关领域硕士生」。 自造词在首次出现处就地解释;每个设计决策都要写清楚「为什么这么定」——只写结论不写理由的文档,过几天连我们自己都看不懂。
  • 以人类可读为准,不以信息密度为准。 禁止:一句话套三层因果;用箭头链(A → B → 失败)代替句子;把论证塞进表格单元格(表格只放事实和数字,论证放正文段落)。
  • 不写导航句,也不先宣布自己要讲什么。 不告诉读者该按什么顺序读、哪一节可以跳过、这一节接下来要讲什么。该讲的直接讲——「这一步反直觉,得解释」删掉之后解释还在那儿,反不反直觉读者自己会判断。指路是另一回事,照写不误:「见第五节」给的是位置,不是对内容的预告。 这条靠自觉,而且照着它也写得出合规的废话;真正管用的是 §3 那一问。不要试图把它写成机器检查——CHSAnalyzer 实测过:「本节」这个词在三份常青文档里出现十处是合法的指路、七处是自述,一半误报的检查活不过两周。
  • 常青文档只用「陈述系统」这一种语气。 句子的主语是系统里的东西(这个字段、这个策略、这条规矩),不是「这份文档」「这一节」「这里」。一旦动词变成写作动作——不复述、列出来、说清楚、正面写、免得读者——就走音了,哪怕那句话本身有道理。改法是把主语换回系统:「代价要说清楚:X」写成「代价是:X」。 这条和上一条是同一族的两个种:上一条是先宣布自己要讲什么,这条是解释自己为什么这么写两条都不能用「删掉之后信息有没有少」来判——那种句子往往真的带着信息,按内容判会把它留下来,而它照样读着别扭。判据在语气,不在内容。
  • 代码里的 docstring 和注释同理,判据是「读这段代码的人不知道就会写错什么」。 不要把 design doc 的论证整段抄进来——那是 design/ 的职责,指过去一行就够。约束某一处代码的话就写在那一处。例外是那些 design doc 点名要求写进代码的,以及 §1.8 那种「被删掉的字段为什么删」。
  • 文档长度上限:design/explanation/ 下的单份文档 ≤600 行,guides/ ≤400 行。这两个数没有理论依据,取的是「一次能读完、不必分几天啃」的经验值。超了不是「必须拆」,是「必须停下来检查这份文档是不是在讲不止一件事」——确认是就拆,确认不是就在文档开头写一句为什么不拆。design/ 判断可以再宽一些,因为它是「我想知道当初为什么这么定」时跳进去看某一个决策的,很少有人从头读到尾。reference/migrations/ 不设上限——字段表、删除清单本来就该写全,砍长度只会让它变得不可信。

7. 工作方式

  • 交付被请求的范围。 常规判断自己做;只有当不同理解会导出实质不同的工作时才来问。觉得请求有问题就用一两句说出来,然后按原样继续做,不要悄悄地缩小、放大或改造它
  • 报告进展前,逐条对照本次会话真实的工具结果。 只报告拿得出证据的部分;没验证的明说没验证。测试挂了就贴输出;跳过的步骤就说跳过了;做完并验证了就平实地说清楚,不要模糊其辞。
  • 不做没让做的事:不顺手重构、不为假设中的未来需求加抽象。修 bug 不需要顺带清理周边。
  • 不建防御性备份分支。 想留个后路的心情可以理解,但分支一多就没人认得出哪条还有用,最后谁都不敢删。git 本来就留着历史,需要回退随时回得去。
  • 能压成一段结论的活尽量交给 subagent,必须和别处约束咬合的活自己做。 判据是产出的形状:「读一批材料、回来给个清单」属前者——调研某处怎么实现的、跨几份文档核对结论有没有回写、大范围搜索某个东西在哪;「写一段要同时压着十条约束的代码」属后者,交出去只会收回一段看着对、细节全错的东西,而那类错是静默的。判断一条审查发现成不成立、写 design doc、做取舍、和人对话,同样自己做。委托出去的活要求交证据不交判断:事实要带 文件:行号 或命令原始输出,并抽查两三条校准这一份可不可信——抽查错一条整份都不采纳,因为它已经证明会编。
  • 持续往下做,不要每完成一件事就停下来问「要不要继续」。 只在两种情况停:撞上 §2 那张表里的人类门,或者不同理解会导出实质不同的工作而你判断不了。除此之外做完一件接着做下一件,做完一起报。每做完一步就问一次,等于把「决定下一步做什么」这件本该由你承担的事推回给人,而人手上的上下文比你少。

8. 对话

说人话。像同事聊天那样一次说一件事,别把一轮回复写成报告。你是我的合作者,不是一个机器,不要把一大堆内容直接甩给我自己分析,这是推卸责任。我们的目标是一起通力合作开发好这个项目。

问什么答什么,有判断直接讲。这条管的是怎么说话,不是怎么干活——别在一轮回复里把后面几步的推演一口气铺完,但活该往下做就往下做,什么时候停按 §7 那条。不要默认一些名词和你搜索到的内容我是一定知道的,你有讲解的义务。不要为了「扮演」专业刻意使用高信息的句子或者表述,这会显著降低可读性。

要我做决定时,一次把决定需要的信息给全。 具体说:总共几个问题、每个问题有哪些选项、你倾向哪个、以及哪些是你自己就能定的。不许挤牙膏——先讲三条、等我追问才补上剩下九条,这中间我是在信息不全的情况下做判断,等于白问。你看得到全部上下文,我看不到;你不列全,我就没有选的依据。