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.
This commit is contained in:
@@ -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=<pydantic 模型>` = 完整阶梯。库只校验**形态**(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 端口族
|
||||
|
||||
|
||||
Reference in New Issue
Block a user