Files
PolyGateway/research-wiki/designs/issue11-caller-dimensions.md
iomgaa 9d9e4ee533 docs: point the deferred items at the issues that now hold them
The design said three times that retention and the _BACKFILL question
would be filed separately, and neither had been. That is the failure
mode the release checklist already records: a closing step nobody does
and nobody notices. Filed as #12 and #13, and the design now names them
so a later reader can follow the thread instead of trusting a promise.
2026-08-17 23:02:12 -04:00

6.2 KiB

type, node_id, title, date
type node_id title date
design design:issue11-caller-dimensions 调用方自定义维度设计(issue #11) 2026-08-17

调用方自定义维度设计(issue #11)

正文: 2026-08-17-issue11-caller-dimensions-design.md。状态: 已人类审批(2026-08-17),进入 writing-plans。

审批时三个待定项按设计原值定稿,人类未提出改动: meta key 数量上限 16 / str value 上限 256 / tenant_id 上限 128(量级推断,无本项目实测依据);库不自动建索引、不自动启用 RLS(GovDoc 需 DBA 执行模板 SQL 才拿到数据库层隔离);_BACKFILL 自动 ALTER 降级议题不纳入本次

  • 选定方案: tenant_id 提真实列(RLS 硬需求)+ meta JSON 容器承载任意调用方自定义 KV(默认不建索引)。四个公共方法(chat/embed/recognize_text/parse_layout)各增两个带默认值的 keyword-only 参数,签名冻结承诺不破。端口 22 → 24 字段。
  • 范围(2026-08-17 人类决策): 只做调用方自定义的维度;请求自带信息(模型名/供应商/源名)继续走现有列,库不往 meta 写任何自采信息。issue 第 4 条(保留期与访问控制)另开,已建 issue #12
  • 范围补正(2026-08-17,写计划时发现后经人类追认): 覆盖 chat / embed / OCR 三条遥测链路。issue 与设计初稿都只说了前两条,但 OcrClient 经同一 emitter 写遥测(ocr.py:426)且行落同一张表,漏掉会让同表内一部分行有归属、一部分永远空白,不可逆性论证对其同样成立(同 issue #10 判断)。
  • 为什么必须提列而不能纯 JSON: 两条独立实证。① RLS 挂 meta->>'tenant_id' 语法合法但会静默退化——PG 的 Planner Statistics and Security 规则在 RLS 场景下对非 LEAKPROOF 函数当作没有统计信息规划,而 ->> 未标 leakproof;pgsql-general 实证案例的最终解法就是"索引列改成非 JSONB",Tom Lane 警告手工标 leakproof 是安全问题。② 与 RLS 无关的独立问题: planner 对 JSONB 本就无可用统计,@> 走硬编码 0.1% 选择率,Heap 复现里行数低估 12 万倍、join 从 300ms 变 584 秒。
  • 为什么不做"可配置提升列白名单": dbt/Airbyte/Fivetran 三家一致禁止用户自定义列(Fivetran 的后续 MERGE 直接把用户列置 NULL,官方方案是建视图)。本库场景更糟: 两个下游对同名 key 推断出不同类型时,第二个到达者的 ADD COLUMNIF NOT EXISTS 静默跳过,从此一直静默写错类型——不报错、持续污染。且 _COLUMNS/_INSERT 从常量变运行时拼接,SQL 注入面从零出现,端口"22 字段冻结"与列序断言全部失效。
  • 同类系统佐证: LiteLLM(同为 LLM 网关、同为每调用一行进 PG)的 SpendLogs 正是此形态——team_id/organization_id/end_user/session_id 全部提列并索引,而 metadata/request_tags 无任何索引。Grafana Loki 的三层(labels 索引 / structured metadata 不索引但可筛 / log line)是同一分野。六家 LLM 可观测平台无一例外都是"少数物化列 + 一个 KV blob"。没有任何成熟系统允许任意 key 自动获得列/索引待遇;唯一的自动推断派 ES dynamic mapping 也是唯一有公开事故名的(mapping explosion)。
  • 库止步于列 + policy 模板,绝不自动 ENABLE RLS: 启用 RLS 而无匹配 policy 是 default-deny(零行可写,静默不报错)。三个下游里只有 GovDoc 多租户,库若自动启用,另两家升级后遥测全量写失败,叠加"遥测写失败静默降级"铁律 = 无声全局丢数据——这才是 issue「不可逆」担忧的真正落点。另三条理由: 库无权知道角色拓扑;按最佳实践部署时库的运行时角色恰好不是表属主、无权 CREATE POLICY;SQLite 无 RLS,承诺它会让两后端语义不对等。先例(graphile-worker/Ent+Atlas/django-multitenant)一致把 policy 授权留给使用方。
  • 哨兵值而非 NULL: PG 的 USING 表达式返回 false 或 null 的行都不可见且静默跳过,故 NULL 的 tenant_id 不是"未归属"而是对所有人永久不可见的黑洞。用 NOT NULL DEFAULT '' 则老行可一条 SQL 审计;同时满足 PG 11+ 加非易失默认值列不重写全表、SQLite 要求 NOT NULL 列必须有非 NULL 常量默认值。
  • 超限报错而非静默丢弃: Langfuse 的"value 超 200 字符直接丢弃"不抄,违反 P5。报错点在 chat() 入口而非遥测写入点——遥测层一切失败都被降级成 warning,校验放那里等于没有校验(同 overlay 保护键先例)。
  • 不进缓存 key: cache_namespace 已是必填的租户隔离维度并已进 key(ARCH §7.5),重复;且进 key 会让存量缓存全量冷启动。
  • 被否决备选: 纯 meta JSON 不提列(RLS 静默退化);可配置提升列白名单(多下游共表静默写错类型);复用 cache_namespace 传租户(缓存隔离单位 ≠ 数据归属,会让下游无法表达"同租户多命名空间");tenant_id 混在 meta 里当约定 key(拼错不报错,静默降级成普通维度)。
  • 审查留痕(Codex,2026-08-17): 报 4 项,逐条核实后全部采纳。① embed() 路径覆盖不足——EmbeddingClient 不走 chat 洋葱,_emit()embedding.py:360 现场构造 ChatRequest,只改 chat 会导致 embed 行维度恒空,恰好落空 issue 第 2 条诉求;② 非有限 float 会击穿"序列化不可达"论断——json.dumpsnan 写成 NaN 字面量(非合法 JSON,PG JSONB 拒收),失败会被降级吞成 warning,即调用方输入错误转化为静默丢遥测;实测确认后改为入口 math.isfinite + 序列化 allow_nan=False 双层收口;③ 校验入口表述只写 chat(),与双路径 API 不一致;④ §4.5 承诺"提供 RLS 模板"却只给了索引模板,已补上含 FORCE/USING+WITH CHECK/NULLIF(current_setting(...)) 的完整定稿。第 ② 条的推翻过程已写进正文 §6,因为"入口校验完备 ⇒ 下游不可能失败"这个推理模式容易复发。
  • 另开议题(已建 issue #13): _BACKFILL 自动 ALTER 是否应降级为默认关闭(Hangfire EnableHeavyMigrations 先例、APScheduler 4.x 版本不认识即拒绝启动)——与本 issue 同源但属独立架构变更,按反 gold-plating 不纳入本次。