From c0d9d7b66e33360b02088880e6e800e63852e8a9 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Sat, 29 Aug 2026 11:53:20 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=9B=9E=E5=86=99=20CHANGELOG=E3=80=81?= =?UTF-8?q?GovDoc=20=E8=BF=81=E7=A7=BB=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=8E=8B?= =?UTF-8?q?=E6=B5=8B=E9=82=A3=E6=9D=A1=E8=BF=87=E6=9C=9F=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG 落在「未发布」段,按那一段自己的规矩不提前写版本号。写清了四条会抛 ValueError 的 情形与迁移写法,因为这是一次破坏性变更。 migrations/govdoc-saas.md 加一节讲租户命名空间怎么传,含迁完算不算数的四条判据,最终判据是 「同一段文本由两个租户各提交一次,各自拿到自己的那份输出」。参数一律指向 0017 不复述。这份 文档原来说 GovDoc 侧「还没有可迁移的东西」,现在有了第一条能逐条验的接入动作,文件头那句 状态说明跟着补了一句例外。 tools/soak/run_soak.py 那份空绑定上方的注释在复述旧转发规则,顺手改对。它给的理由本来就不准 ——让绑定留空的真正原因是绑定的全部键值都进参数快照,每批都不同的值会让故障注入那一步续跑时 报一次假的参数漂移,和转发哪些键无关。三份压测绑定常量里都没有裸键,所以这次变更打不到压测。 Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 33 +++++++++++++++ research-wiki/migrations/govdoc-saas.md | 56 ++++++++++++++++++++++++- tools/soak/run_soak.py | 6 +-- 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27144b0..2508939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,39 @@ 长期停在 1.0.5,下游 `pip install` 拿不到任何修复且无人发现。提前把号写进这一段就是在重演 那个形态——读到号的人会以为那一版已经在 registry 上,而它不在。 +### 网关适配器改按保留前缀转发绑定,而不是按键名撞 + +**这是一次破坏性的行为变更。** 绑定里不带前缀的 `session_id` 与 `parent_call_id` 从「静默转发 +给网关」变成「抛 `ValueError` 并给出改法」,改法是把键名写成 `gateway.session_id`。 + +```python +model_binding={ + "book": "b7", # 项目自己的坐标,不传 + "gateway.cache_namespace": "acme:v1:tenant:x7", # 传成 chat(cache_namespace=...) + "gateway.tenant_id": "x7", +} +``` + +绑定里键名以 `gateway.` 开头的,前缀之后那一段当作网关 `chat()` 的关键字参数名,值原样传下去; +其余的键照旧不传、也不报错。**本库不再持有一份网关参数名单**——上游哪天再加一个字符串维度, +下游当天就能用,本库不发版,声明的依赖下界也不用跟着抬。 + +原来那份写死的白名单只认两个键,把 `cache_namespace` 挡在外面,而那是 PolyGateway 指定的租户 +隔离手段:挡掉之后,两个租户提交内容相同的一段文字,第二个会读到第一个那次的模型输出。给出 +那份白名单的理由是「网关只有两个槽位放得下这类东西」,而这句话在写下的那天就已经不成立—— +当时装得到的最低版本上已经有四个。 + +四条会抛 `ValueError` 的情形:带前缀的键名指向 `messages`、`stream`、`structured`、`overlay` +之一(这些改变请求本身,另有权威);带前缀的键取值是空串或纯空白(在网关那边和「没传」分不 +开,或者不带任何信息,而一个看着配了、实际没配的隔离比没配更糟);键恰好是 `gateway.`、前缀 +后面没跟参数名;以及不带前缀的那两个历史裸键。 +判断发生在把请求交给网关之前,所以出错的那次调用不会真的发出去。 + +转发的键照旧全部进参数快照,键名不变(`request.binding.gateway.cache_namespace`),所以换一个 +命名空间续跑会照常撞参数漂移。 + +方案与被否掉的三条路见 `research-wiki/design/0017-gateway-forwarding.md`。 + ## 1.0.2(2026-08-27) 回应下游项目在 PolyLoop 仓库上提的五个 issue。 diff --git a/research-wiki/migrations/govdoc-saas.md b/research-wiki/migrations/govdoc-saas.md index a1b62df..ec3da7b 100644 --- a/research-wiki/migrations/govdoc-saas.md +++ b/research-wiki/migrations/govdoc-saas.md @@ -5,7 +5,8 @@ > 来源与缺口清单。 > > **当前状态:这不是一份迁移清单,是一份需求清单。** GovDoc-SaaS 的 agent 部分还没有可迁移 -> 的东西,本文的作用是防止 PolyLoop 只按 dissect 一家的形状长。 +> 的东西,本文的作用是防止 PolyLoop 只按 dissect 一家的形状长。唯一已经能逐条验的接入动作是 +> 租户隔离参数那一节。 PolyLoop 只有 dissect 一个硬消费者(见 `dissect.md`)。只对着一个消费者做,做出来的库会长成 那个消费者的形状,而这件事在完成之前看不出来。GovDoc 这边不做迁移验收,改做**设计级验收**: @@ -141,6 +142,59 @@ Codex 对抗审查抓出来了,修法见 `../design/0004-stopping-and-step-rec 显式注入,重试对它们就静默失效」——那是 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 现在答不上来的问题。答不上来不等于设计错了,但每一条都得有明确结论—— diff --git a/tools/soak/run_soak.py b/tools/soak/run_soak.py index b1f7b45..551c582 100644 --- a/tools/soak/run_soak.py +++ b/tools/soak/run_soak.py @@ -51,9 +51,9 @@ if TYPE_CHECKING: APPWORLD = "appworld" GOVDOC = "govdoc" -#: 传给每次模型调用的项目侧标识。**空的**:网关适配器只往下转发 `session_id` 与 -#: `parent_call_id`,而这两样每批都不同;进了参数快照,故障注入那一步续跑时的逐字段比对 -#: 就会报一次假的参数漂移。 +#: 传给每次模型调用的项目侧标识。**空的**:绑定的全部键值都进参数快照,而每批都不同的值会让 +#: 故障注入那一步续跑时的逐字段比对报一次假的参数漂移。这里跟转发无关——网关适配器只转发带 +#: `gateway.` 前缀的键,而这份一个都没有。 MODEL_BINDING: Mapping[str, str] = {} #: GovDoc 的工作区落在 runs 目录下的这个子目录里。记分板枚举 run 用的是