From 48f82404eee7d172b55e50cb3c85932668b2807f Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 20 Jul 2026 04:00:07 -0400 Subject: [PATCH] docs: add D14 structured-output ladder decision Define the five-step ladder (native-schema prevention, repair, shape validation, bounded feedback re-ask, ResultInvalidError) as D14; rewrite section 7.9 accordingly, give the structured parameter its three-tier semantics in the chat() signature, and scope the bounded re-ask outside transport retry and breaker counting. --- CLAUDE.md | 4 ++-- research-wiki/ARCHITECTURE.md | 29 +++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4735ff3..33d1813 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ ## 1. 项目元数据 - **核心目标**: PolyGateway = 统一的大语言模型(LLM/VLM/OCR,音频预留)调度与中转库。治理单位是**一次模型调用**:请求封装、多源多账号、限流、错误分类与重试、熔断、Redis 响应缓存、流式看门狗、遥测(含成本)、结构化输出策略。全组件端口化可插拔。 -- **架构权威文档**: `research-wiki/ARCHITECTURE.md`(架构单一事实源,含 D1-D13 决策及讨论过程、子系统设计、三项目迁移验收标准;**不受 400 行设计文档限制**,以无歧义传达既有讨论为准绳)。开发顺序见 `research-wiki/ROADMAP.md`;`research-wiki/designs/` 仅存放每次实现具体功能的设计文档。 +- **架构权威文档**: `research-wiki/ARCHITECTURE.md`(架构单一事实源,含 D1-D14 决策及讨论过程、子系统设计、三项目迁移验收标准;**不受 400 行设计文档限制**,以无歧义传达既有讨论为准绳)。开发顺序见 `research-wiki/ROADMAP.md`;`research-wiki/designs/` 仅存放每次实现具体功能的设计文档。 - **参考项目**: `reference/` 下三个项目是本库的需求来源与代码蓝本(**只读,勿改**);库必须能按 ARCHITECTURE.md §11 被它们迁移接入,否则即边界缺口。 - **技术栈**: Python 3.11+,核心仅依赖 `httpx` + `pydantic`,其余(redis/sqlite/postgres/json_repair/openai)一律 optional extras。conda 环境 `PolyGateway`。 @@ -107,7 +107,7 @@ project_root/ | 需求 | 路径 | |---|---| -| 架构全貌: 决策 D1-D13 及讨论过程、端口清单、错误分类、子系统设计、迁移验收 | `research-wiki/ARCHITECTURE.md`(单一事实源) | +| 架构全貌: 决策 D1-D14 及讨论过程、端口清单、错误分类、子系统设计、迁移验收 | `research-wiki/ARCHITECTURE.md`(单一事实源) | | 开发顺序与里程碑状态 | `research-wiki/ROADMAP.md`(活文档,随进度更新) | | 三项目迁移文档(ARCHITECTURE §11 的展开,库设计的常驻约束) | `research-wiki/migrations/`(govdoc-saas / video-tree-trm5 / chsanalyzer) | | 功能设计文档(每次实现新功能时新增) | `research-wiki/designs/` | diff --git a/research-wiki/ARCHITECTURE.md b/research-wiki/ARCHITECTURE.md index d37c2db..0c1e7c0 100644 --- a/research-wiki/ARCHITECTURE.md +++ b/research-wiki/ARCHITECTURE.md @@ -119,7 +119,7 @@ HTTP API → arq 队列 → worker 协程 脚本 → asyncio.gather 协 --- -## 3. 架构决策记录(D1–D13,含讨论过程与备选方案) +## 3. 架构决策记录(D1–D14,含讨论过程与备选方案) > 每条决策记录格式:**决策 / 背景与讨论 / 被否决的备选 / 影响**。这些决策已与人类逐条确认;推翻任何一条需要人类批准并修订本节。 @@ -238,6 +238,16 @@ HTTP API → arq 队列 → worker 协程 脚本 → asyncio.gather 协 **影响**: §7.2 的单层重试原则不变;RetryMW 循环保持结构性禁止 `except BaseException`;此结论基于 2026-07 的库现状,若 tenacity 未来提供逐次编排能力可重评。 +### D14 结构化输出阶梯:修复 → 校验 → 有界带反馈重问(2026-07-20) + +**决策**: 结构化输出处理为五级阶梯——①预防(provider 声明支持时用原生 schema)→ ②修复(围栏剥离→json_repair→provider 变体归一化,零网络)→ ③形态校验(调用方传 pydantic 模型时库内校验,零网络)→ ④**受约束重问**(有界 `max_structured_retries`,默认 1-2;校验错误作为反馈追加重问;可升级到原生 schema 策略;照过限流但不计熔断;逐次遥测)→ ⑤耗尽抛 `ResultInvalidError`(携原始文本+修复错误+校验错误)。**校验入库但可选**,三档: 不传 `structured` = 原始文本零负担;`structured="json"` = 仅修复;`structured=` = 完整阶梯。库只校验**形态**(schema),语义校验(业务规则,如 bbox 是否在图内)留用户层(D12 零业务假设)。 + +**背景与讨论**: 人类提出"先修复、修不好再重试、重试要有约束",并质疑校验层归属("放用户层是否增加调用方负担")。现状盘点: D7 只定义了两个策略,"修复失败之后"仅 §6.1 一句"策略层可选二次尝试"未设计;三项目对解析失败的处理互相不同——VT/GovDoc 靠业务步级重试无脑重来(不带反馈),CHS 刻意不重试转人工复核——库须同时支持,CHS 策略即 `max_structured_retries=0`。**校验入库的结构性理由**: 校验必须位于重问循环内侧,循环才能由校验失败触发;放用户层则三项目各自重搭循环,恰是要消灭的重复;且 NativeSchemaStrategy 发 response_format 本就需要 schema。带反馈重问与 transport 重试(§7.2)是两种重试: 独立计数、医不同的病;反馈改变 messages,天然不命中原坏答案的缓存 key;不计熔断(服务健康,§6.3);照过限流(真实请求)。CHS `classifiers.py` 跨模块 import 私有 `_extract_json_object` 证明用户对库侧解析有真实需求。 + +**被否决的备选**: 校验全放用户层(破坏循环闭环、重复×3);无界重问(违背"重试有约束");解析失败并入 transport 重试计数(混淆坏结果与坏服务)。 + +**影响**: §5.2 `structured` 参数三档语义、§7.9 重写为阶梯、§6.1 ResultInvalid 行注 D14;缓存写入发生在阶梯通过之后(§7.5 "不固化坏结果"的执行点);反馈模板与策略升级细则留 M1 设计文档。 + --- ## 4. 总体架构 @@ -324,7 +334,7 @@ flowchart TB `ChatRequest`(model/messages/结构化输出参数/per-call 覆盖项)、`Usage`(tokens + elapsed,OCR 无计费填 0)、`OcrTextResult`(text + 溯源三件套 source_name/usage/raw)、`OcrLayoutResult`(elements 含 bbox/type + page_size + 溯源)。全部 frozen dataclass。空结果语义:合法"无内容"用空值/None 表达,调用失败必须走异常——二者严格区分。 -**`chat()` 公共签名定稿(2026-07-20,GovDoc 迁移缺口 G1/G2)**: `chat(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True)`。要点: ① `session_id`/`parent_call_id` 与三项目现有 `LLMProvider.chat` Protocol 逐字兼容——这是"调用点零改动"承诺的前提;② **per-call `cache_namespace`**: GovDoc 是单 client 服务多租户、tenant 每请求变化,装配级 namespace 只是默认值,per-call 传入时覆盖并进入缓存 key(§7.5);③ `cache_salt` per-call 可传(Video-Tree 跨 epoch 重采样)。 +**`chat()` 公共签名定稿(2026-07-20,GovDoc 迁移缺口 G1/G2)**: `chat(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True)`。要点: ① `session_id`/`parent_call_id` 与三项目现有 `LLMProvider.chat` Protocol 逐字兼容——这是"调用点零改动"承诺的前提;② **per-call `cache_namespace`**: GovDoc 是单 client 服务多租户、tenant 每请求变化,装配级 namespace 只是默认值,per-call 传入时覆盖并进入缓存 key(§7.5);③ `cache_salt` per-call 可传(Video-Tree 跨 epoch 重采样);④ `structured` 三档语义(D14): 不传 = 原始文本,`"json"` = 仅修复,pydantic 模型 = 完整阶梯(修复+形态校验+有界带反馈重问)。 --- @@ -337,7 +347,7 @@ flowchart TB | `TransientError` | 超时/5xx/429/网络抖动/SSE 异常(畸形帧、断流无 [DONE])/看门狗超时 | ✅ 退避后 | ✅ | ✅ | | `SourceDeadError` | 401/403/欠费/insufficient_quota(429 body 细分) | ❌ | ✅ 立即 | ✅ force_open | | `RequestRejectedError` | 400/请求格式错/坏输入(如不支持的图像格式) | ❌ | ❌ | ❌ | -| `ResultInvalidError` | 调用成功但内容不可解析(JSON 修不好、ZIP 缺关键文件) | ❌(策略层可选二次尝试) | ❌ | ❌(熔断记**成功**) | +| `ResultInvalidError` | 调用成功但内容不可解析(JSON 修不好、ZIP 缺关键文件) | ❌(仅 D14 结构化阶梯的有界带反馈重问,不入 transport 重试计数) | ❌ | ❌(熔断记**成功**) | | `CircuitOpenError` / `AllSourcesExhausted` | 开路 / 全源耗尽 | 调用方决定: wait / fail-fast 可配 | — | — | **scope 级不可用的结构化语义(2026-07-20,CHS 迁移缺口 G1)**: `AllSourcesExhausted`/`CircuitOpenError` 必须携带结构化字段——`retry_after_s: float | None`(建议恢复等待,取各源冷却与 Retry-After 的最小值)、`reason` 枚举(承接 CHS `ProviderUnavailableError` 的 7 种: circuit_open / retry_exhausted / stalled / quota_exhausted / no_sources / backpressure_timeout / probe_pending)、`per_source_reasons: dict[str, str]`。CHS 的"scope 级不可用 → arq 延期重投、不消耗业务失败预算"(`workers/tracking.py:406-428`)依赖 `retry_after_s` 复现。 @@ -422,14 +432,17 @@ flowchart TB - **单一 helper 铁律**: 遥测调用点收敛为一个内部函数/上下文管理器;Video-Tree 与 GovDoc 各有 4-5 处逐字复制的 `record_llm_call(15 个参数)` 是本条的直接教训。 - 成本: `pricing.py` 维护 model → (input 单价, output 单价) 表,遥测时换算 `cost` 字段;查不到价格记 None 并 warning,**不阻塞调用**。 -### 7.9 结构化输出策略 +### 7.9 结构化输出阶梯(D14) -| 策略 | 机制 | 适用 | +| 级 | 内容 | 成本 | |---|---|---| -| `JsonRepairStrategy` | prompt 约定 + ```json 围栏剥离 + json_repair + provider 变体归一化(如 DeepSeek 参数平铺) | 任意网关;三项目现状的收敛 | -| `NativeSchemaStrategy` | response_format json_schema / function calling | provider 注册表声明支持时 | +| ① 预防 | provider 注册表声明支持时,用 response_format / function calling 直接约束(`NativeSchemaStrategy`) | 无额外 | +| ② 修复 | 围栏剥离 → json_repair → provider 变体归一化(DeepSeek 参数平铺等)(`JsonRepairStrategy`) | 零网络 | +| ③ 校验 | 调用方传 pydantic 模型时库内做**形态**校验;语义校验留业务层 | 零网络 | +| ④ 受约束重问 | `max_structured_retries`(默认 1-2;0 = CHS"不重试转人工"策略): 校验错误作为反馈追加重问(messages 已变,不命中原坏答案缓存);可从 ② 升级到 ① 策略;照过限流闸,**不计熔断**,逐次遥测 | 真实调用 | +| ⑤ 耗尽 | 抛 `ResultInvalidError`,携原始文本 + 修复错误 + 校验错误,业务决定兜底(人工复核/降级) | — | -逐调用可选;解析失败统一抛 `ResultInvalidError`(§6.3)。 +`structured` 参数三档: 不传 = 原始文本(零负担);`"json"` = 仅②;pydantic 模型 = ①-⑤ 完整阶梯。缓存写入发生在阶梯通过之后——这是 §7.5 "ResultInvalid 的原始响应不缓存"的执行点。 ### 7.10 OCR 端口族