c0d9d7b66e
CHANGELOG 落在「未发布」段,按那一段自己的规矩不提前写版本号。写清了四条会抛 ValueError 的 情形与迁移写法,因为这是一次破坏性变更。 migrations/govdoc-saas.md 加一节讲租户命名空间怎么传,含迁完算不算数的四条判据,最终判据是 「同一段文本由两个租户各提交一次,各自拿到自己的那份输出」。参数一律指向 0017 不复述。这份 文档原来说 GovDoc 侧「还没有可迁移的东西」,现在有了第一条能逐条验的接入动作,文件头那句 状态说明跟着补了一句例外。 tools/soak/run_soak.py 那份空绑定上方的注释在复述旧转发规则,顺手改对。它给的理由本来就不准 ——让绑定留空的真正原因是绑定的全部键值都进参数快照,每批都不同的值会让故障注入那一步续跑时 报一次假的参数漂移,和转发哪些键无关。三份压测绑定常量里都没有裸键,所以这次变更打不到压测。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 lines
16 KiB
Markdown
229 lines
16 KiB
Markdown
# 设计对齐:GovDoc-SaaS
|
||
|
||
> **更新触发点:第 2 档(同提交同改)。** 公共类型或接缝语义变更时,在同一个提交里核对
|
||
> 本文的需求条目还能不能被承载;GovDoc 侧的 agent 方案定下来时,在同一个提交里更新需求
|
||
> 来源与缺口清单。
|
||
>
|
||
> **当前状态:这不是一份迁移清单,是一份需求清单。** GovDoc-SaaS 的 agent 部分还没有可迁移
|
||
> 的东西,本文的作用是防止 PolyLoop 只按 dissect 一家的形状长。唯一已经能逐条验的接入动作是
|
||
> 租户隔离参数那一节。
|
||
|
||
PolyLoop 只有 dissect 一个硬消费者(见 `dissect.md`)。只对着一个消费者做,做出来的库会长成
|
||
那个消费者的形状,而这件事在完成之前看不出来。GovDoc 这边不做迁移验收,改做**设计级验收**:
|
||
把它真实需要的东西列成条目,逐条问 PolyLoop 能不能承载,答不上来的登记成缺口。
|
||
|
||
## GovDoc 的项目术语
|
||
|
||
这些词是 GovDoc 的,不是 PolyLoop 的。PolyLoop 自己的词表在
|
||
`../explanation/architecture.md` 第二节。
|
||
|
||
**PES**——Plan-Execute-Summarize,「计划—执行—总结」三阶段。GovDoc 的一个任务被切成这三段,
|
||
每段跑一次 agent,段与段之间靠工作区里的文件交换状态:计划阶段产出一份计划文件,执行阶段
|
||
读它并产出一批发现,总结阶段读那批发现产出最终结果。
|
||
|
||
**工作区**——一次 agent 运行独占的沙箱目录。里面有这次运行能看的数据、能写的输出、以及
|
||
运行结束后要留下的产物。它由 GovDoc 侧建立、快照和清理,PolyLoop 不认识它。
|
||
|
||
**必须产出的文件**——每个阶段声明几个文件路径,跑完之后这些文件必须存在,否则这个阶段算
|
||
失败。这是 GovDoc 判断「阶段目标达成没有」的方式,按 `../explanation/scope.md` 它在界外。
|
||
|
||
**提交型完成**——agent 靠调用一个特定的工具来宣布自己做完了,环境状态不发生任何变化。它跟
|
||
另一种完成方式(环境自己报告「这道题结束了」)是两回事,两者都要能表达。
|
||
|
||
## 为什么这里没有迁移清单
|
||
|
||
GovDoc 有两套 agent,形态完全不同,而两套都不能直接当迁移对象。
|
||
|
||
**GovDoc-Editor 是今天跑在生产上的系统。** 它的 agent 循环归 Claude Agent SDK,业务层明令
|
||
禁止 import 那个 SDK,中间隔着一层叫 Scrivai 的编排框架。Scrivai 做的是「计划—执行—总结」
|
||
三阶段编排:每个阶段跑一次 agent,阶段之间通过工作区里的文件交换状态,每个阶段有自己的
|
||
步数上限、可用工具集和必须产出的文件清单。
|
||
|
||
按 `../explanation/scope.md` 的判据,**阶段编排在 PolyLoop 界外**。所以 GovDoc-Editor 不是
|
||
迁移对象,它是需求来源——它告诉我们「阶段编排要建在执行内核之上」时,对执行内核提了哪些
|
||
要求。
|
||
|
||
**GovDoc-SaaS 是 GovDoc-Editor 的重构版**,它的 `packages/docagent-core/` 正在把上面那套东西
|
||
换成自建:`workflow/phase.py` 的注释写着「三阶段泛化为 N 阶段」,`PhasedWorkflow` 自己接
|
||
`AgentLoop`,Claude Agent SDK 不见了。
|
||
|
||
但那份 `AgentLoop` 不能当迁移基准。文件头第一句写着它「完整保留」某个项目的循环逻辑,而
|
||
那个项目已经废弃;更要紧的是,`grep "from docagent_core" src/` 在业务层零匹配——它有测试,
|
||
但从来没被任何业务代码调用过。**拿一份没有调用方、且血统来自废弃项目的代码当验收标准,
|
||
验的是那个废弃项目的行为。**
|
||
|
||
所以需求来源是两处:GovDoc-Editor 的生产实践,以及 `docagent-core` 作为一次已有的抽象尝试
|
||
所暴露的形状。
|
||
|
||
## 需求条目
|
||
|
||
**一、一份 agent 定义要能按次运行配不同的预算。**
|
||
生产上那份审核 agent 的三个阶段分别是 50、50、16 步。如果预算只能挂在定义上,就得为三个
|
||
阶段建三份定义,而它们除了预算之外完全一样。
|
||
|
||
**二、要能按次运行收窄工具集。**
|
||
三个阶段可见的工具不同:计划阶段有读、写、搜索、执行;执行阶段多一个技能调用;总结阶段
|
||
只有读、写、通配查找。这条和「模型可见的 schema、存在性校验、分发三者同源」不冲突——
|
||
每次运行构造一个窄的注册表就行。
|
||
|
||
**三、工具调用协议是 JSON,而且要容错。**
|
||
模型输出的是一段 JSON,含反思、计划、动作三部分,动作里有工具名和参数。实际遇到的两种
|
||
偏差都得处理:整段 JSON 被包在代码围栏里,以及参数被平铺在动作层而没有嵌套。后者的收拢
|
||
要保守——只有当除工具名之外确实存在平铺参数时才收拢,否则会把「缺参数」这个错误静默升级
|
||
成「参数为空但合法」。
|
||
|
||
**四、无效的工具调用不计有效步,但要计入总迭代上限。**
|
||
不计有效步是因为它没真的做事;计入总迭代是因为模型可能一直调用不存在的工具,没有这个上界
|
||
循环不会停。这两个计数必须分开,混成一个就防不住无限循环。
|
||
|
||
**五、提交型完成:某个特定工具被调用即视为完成。**
|
||
生产上那份 agent 靠调用一个提交工具来结束,环境状态不发生变化。这和另一种完成方式——环境
|
||
自己报告「这道题结束了」——是两回事,两者都要能表达。
|
||
|
||
**六、审计事件必须覆盖模型步与工具调用,且事件发送失败不能中断循环。**
|
||
业务侧有一条硬纪律:agent 的原始输出、修复后的输出、恢复来源全程留痕,禁止静默修复。所以
|
||
「模型原文」和「解析/修复之后的结果」要分别可见。
|
||
|
||
**七、取消要能穿透。** 长任务的所有权以数据库租约为准,取消信号从外部进来之后必须能打断
|
||
正在进行的模型调用与工具执行,并释放在途资源。
|
||
|
||
**八、工作区是一个有边界的端口。**
|
||
生产实现把它抽象成六个操作:按行读、原子写、grep、列文件、判断存在、算校验和;所有路径都
|
||
是工作区内的相对路径,越界要抛异常。按 `../explanation/scope.md`,工作区的建立、快照与清理
|
||
在界外,但**提示词的构造要能读工作区**(阶段的提示词是从上一阶段的产物生成的),这一点见
|
||
下面的缺口。
|
||
|
||
**九、阶段级续跑要能判断上一次运行是不是真的跑完了。**
|
||
阶段级续跑本身在界外,但它依赖一个界内的事实:运行结果必须是可持久化、可读回、可判定的
|
||
结构,而且「这次运行结束了」要由库自己写下来。这条已经落进 `../design/0002-step-level-resume.md`。
|
||
|
||
## 已经有答案的(原缺口登记,`0003` 与 `0013` 回答)
|
||
|
||
这几条曾经登记为「PolyLoop 答不上来」,现在定了。留在这里是因为它们是 GovDoc 侧真实的接入
|
||
约束,迁移时要照着改代码。
|
||
|
||
**审计纪律由意图日志承担,不由事件流承担**(`../design/0013-event-set-and-callbacks.md`
|
||
决策一与决策二)。这条原来登记成「事件流能不能覆盖审计需求,取决于事件里带不带原始响应」,
|
||
答案是不带——事件流可丢,一件只存在于可丢通道里的事实撑不起「禁止静默修复」。纪律要留痕的
|
||
三样在日志里各有位置:模型原文在模型调用结果记录的回复里,修复后的文本是步记录的
|
||
`raw_output`,恢复来源是意图日志本身(哪一步有意图没结果、按哪条重放策略处置过)。
|
||
|
||
对 GovDoc 的实质影响是**审计出口要包在存储接缝上,不是接在事件出口上**。存储的写是运行的
|
||
一部分,写失败运行就停;事件的投递不是。今天那个发一条带类型和载荷的接口迁过来之后,位置
|
||
变了。事件流仍然可以接——一步走完发一条,带整条步记录——但它是给进度回写用的,不能当审计。
|
||
|
||
**它的三类事件里有一类在界外。** `phase_recovery` 发自阶段执行器,而阶段编排不归本库;那
|
||
一层要发什么事件由它自己定。本库这边它需要的只是「这次运行结束了没有、结果是什么」,那是
|
||
存储的结束记录在回答。
|
||
|
||
**预算挂在请求上,不是定义上。** 一份定义可以被三个阶段共用,各传一份预算,不会长出三份
|
||
除预算外完全相同的定义。**不设「定义给默认值、请求可覆盖」**——两处取值意味着「这次到底
|
||
跑的什么设置」要对照两个地方才答得出来。
|
||
|
||
**提示词的内容由项目读好了传进来,库不接触工作区。** 上下文装配不是接缝,所以「让库接受
|
||
一个项目对象再转交」这条路不存在。每个阶段的提示词构造器仍然在 GovDoc 侧,它照常读工作区,
|
||
只是把读出来的结果作为上下文的一段交给库。库只管段的顺序和注入槽的位置。
|
||
|
||
**两层重试都不归本库,预算口径按库能观测的量算。** 一次模型调用之内的重试与换源归
|
||
PolyGateway,整次运行的重试归 GovDoc 自己的编排层;中间那一层(库在模型调用失败后自己再
|
||
调一次)举不出两个消费者,不做。**PolyGateway 内部换源重试不消耗任何预算**,因为库数的是
|
||
一次模型调用接缝的调用——口径必须是库自己能观测的量,否则换个后端口径就变了。
|
||
|
||
**提交型完成靠工具注册表上的完成标记。** 注册表知道哪个工具一旦被成功执行就代表目标达成,
|
||
库在动作结算之后查它。GovDoc 的动作执行接缝完成信号恒为「未完成」——它的环境状态不因为
|
||
提交而改变,所以环境这条通路对它永远不成立,收尾走的是完成标记这条。
|
||
|
||
这一条曾经差点出大事:`0003`/`0004` 的初稿把完成信号定成「布尔或空、空表示取不到」,并且
|
||
规定「取不到即环境故障」。照那个写法,GovDoc 的**每一次运行都会在第一步撞环境故障终止**。
|
||
Codex 对抗审查抓出来了,修法见 `../design/0004-stopping-and-step-record.md` 决策三 G 档。
|
||
|
||
这一条对 GovDoc 是实质变化:`docagent-core` 现有的那层步级重试(超时与网络异常,退避
|
||
20 秒和 40 秒)迁移后没有对应位置。它当初存在的理由是「治理层的某些异常类型如果没在装配点
|
||
显式注入,重试对它们就静默失效」——那是 PolyGateway 装配的问题,要在装配点解决,不是在
|
||
Agent 层补一层。
|
||
|
||
## 租户隔离参数怎么传
|
||
|
||
**绑定**是挂在一次运行请求上的一组字符串到字符串的映射,装的是下游项目自己的坐标。PolyLoop
|
||
不解释它的内容,原样交给模型调用接缝;自带的网关适配器再从里面挑出该往 PolyGateway 那次调用
|
||
上传的键。
|
||
|
||
GovDoc 在 PolyLoop 仓库提的 issue #6 说的是:适配器挑键的老办法把租户命名空间挡在外面,而那
|
||
是 PolyGateway 指定的租户隔离手段,挡掉之后两个租户提交相同的一段文本时,第二个会读到第一个
|
||
那次的模型输出。`../design/0017-gateway-forwarding.md` 决策一换掉了挑键的机制——**绑定里键名
|
||
以 `gateway.` 开头的才往下传,前缀之后那一段当作网关 `chat()` 的关键字参数名**。
|
||
|
||
### 要改的地方
|
||
|
||
租户命名空间与租户标识都写成带前缀的绑定键,每次运行按这次运行属于哪个租户填值:
|
||
|
||
```python
|
||
model_binding = {
|
||
"case": "...", # GovDoc 自己的坐标,不带前缀,不往下传
|
||
"gateway.cache_namespace": ..., # 这次运行所属租户的命名空间
|
||
"gateway.tenant_id": ..., # 同一个租户的标识
|
||
}
|
||
```
|
||
|
||
两个值的字符串怎么编码由 GovDoc 自己定(它的设计记录里已经定死),PolyLoop 不解释这两个字符串
|
||
的内容,也不替它们拼任何前后缀。这两个网关参数各自是什么语义、以及租户坐标为什么走绑定而不是
|
||
走适配器的构造参数,见 `0017` 的术语节与决策三。
|
||
|
||
**issue 里提的第三个维度 `meta` 本库不做**,理由在 `0017` 决策五。它原本要带的那个自己的 trace
|
||
标识改走 `gateway.session_id`:网关那边这个槽位本来就是一个用来分组的字符串,而 PolyLoop 不往
|
||
里面填任何东西,它一直留给下游自己写。
|
||
|
||
**绑定里现有的不带前缀的 `session_id` 与 `parent_call_id` 要改成带前缀的写法。** 这两个键在旧
|
||
机制下会被转发,`0017` 之后不再转发,而且会被适配器显式拒绝——用一次报错换掉一次静默的行为
|
||
变更,理由在 `0017` 决策四的第四条防御。拒绝发生在第一次模型调用时,形态是一次「第 0 步就以
|
||
模型调用失败收尾」的运行,失败说明里带着出问题的那个键名,没有预算被花掉。落地在哪个版本、
|
||
以及网关依赖下界要不要跟着动,见 `0017`。
|
||
|
||
### 迁完算不算数
|
||
|
||
**绑定里再没有不带前缀的 `session_id` 或 `parent_call_id`。** 这条装上新版就自己验了:有残留
|
||
的话,用到那份绑定的第一次运行会在第一次模型调用上失败。
|
||
|
||
**每一次运行的绑定里都带着这次运行所属租户的命名空间键,值不是空串。** 空串会被适配器拒绝
|
||
(`0017` 决策四的第二条防御)。查法是拿一次真实运行的运行开始记录看参数快照:带前缀的键照旧
|
||
逐字段进快照,所以「这次运行用的是哪个命名空间」事后查得到。
|
||
|
||
**同一段文本由两个租户各提交一次,各自拿到自己的那份模型输出。** issue #6 的失败场景不再复现
|
||
——这是最终判据,前面两条都是为它服务的。
|
||
|
||
**续跑一次运行时命名空间不变。** 命名空间随绑定进参数快照,而续跑开工前会把重算的快照与日志
|
||
里那份逐字段比对,换过命名空间就会被当成参数漂移中止。撞上这条说明续跑读到的缓存可能属于另一
|
||
个租户,中止是对的。
|
||
|
||
## 缺口登记
|
||
|
||
这些是 PolyLoop 现在答不上来的问题。答不上来不等于设计错了,但每一条都得有明确结论——
|
||
哪怕结论是「不支持,理由是什么」。
|
||
|
||
**模型原生的工具调用没有路。** PolyLoop 的模型调用入参上只有消息序列,没有 tools 字段,
|
||
所以工具要让模型看见,唯一的路是变成项目自己拼进上下文的文字
|
||
(`../design/0010-context-assembly.md` 决策一与文末)。生产实现今天的动作协议是写在文本里的
|
||
JSON,不用原生工具调用,所以现在不缺。哪天要换成原生工具调用,PolyLoop 的模型调用入参要加一
|
||
个 tools 字段——那是兼容变更,但还要看下面那一层的共用库暴不暴露这个参数,以及原生工具调用的
|
||
返回怎么进决策解释接缝那三个分支。**这条要在换协议之前定,不能边换边定。**
|
||
|
||
**动作被拒绝那一档,工具单独的耗时拿不到。** 生产实现的 `tool_call` 审计事件记的是工具执行
|
||
那一段的毫秒数,而 PolyLoop 只记整步墙钟——里面混着模型调用、解释和动作执行三段。迁过去这个
|
||
数的口径会变,迁移前后两批数据不可比。要拆开就得往步记录里加分段计时,那是一次持久化结构的
|
||
改动,得先说清楚这个数拿去做什么(是给人看慢在哪,还是要进统计)。这条在
|
||
`../design/0013-event-set-and-callbacks.md` 的代价一节登记过,那份文档没解决它。
|
||
|
||
**动作被拒绝之后的重复行为。** 生产实现对无效工具调用不设单独的重试上限,靠总迭代上界收敛。
|
||
PolyLoop 目前的想法一致,但要确认这在长阶段(50 步)上够不够——模型反复调用同一个不存在的
|
||
工具,会把整个步数预算烧光而不产出任何东西,而这在轨迹上表现成「预算耗尽」,与真的做不完
|
||
混在一起。
|
||
|
||
## CHSAnalyzer 为什么没有对应的文档
|
||
|
||
CHSAnalyzer 是第三个已知消费者,定位是远期兼容。它的架构文档第十四节明确写了评估层和诊断层
|
||
现在是空的,而且「不为它们预留任何结构」,理由是「留了位置整个系统会立刻复杂一个量级,
|
||
而现在还不知道它们真正需要什么形状——猜出来的接缝比没有接缝更难拆」。
|
||
|
||
**没有需求可以登记,所以不建那份文档。** 等它的方案定下来再建。这一段写在这里,是为了让
|
||
读者知道这是一个决定而不是遗漏。
|