diff --git a/research-wiki/ARCHITECTURE.md b/research-wiki/ARCHITECTURE.md index 23bc24e..2b186f7 100644 --- a/research-wiki/ARCHITECTURE.md +++ b/research-wiki/ARCHITECTURE.md @@ -262,7 +262,8 @@ flowchart TB end subgraph PolyGateway["PolyGateway: GatewayClient"] M1[TelemetryMW 遥测+成本] --> M2[CacheMW 响应缓存] - M2 --> M5["RetryMW 重试循环
每次尝试: 选源 → 熔断门(源) → 限流 permit(全局+源)"] + M2 --> M4["StructuredMW 结构化阶梯(D14)"] + M4 --> M5["RetryMW 重试循环
每次尝试: 选源 → 熔断门(源) → 限流 permit(全局+源)"] M5 --> T["Transport 端口
OpenAICompat(httpx,默认) / OpenAISDK(可选) / MonkeyOCR"] end subgraph 状态后端["可插拔状态后端"] @@ -283,7 +284,7 @@ flowchart TB ### 4.3 默认中间件层序及理由(外→内) -**遥测 → 缓存 → 重试循环(每次尝试: 选源 → 熔断门(源) → 限流 permit(全局+源) → transport)** +**遥测 → 缓存 → 结构化(StructuredMW,D14)→ 重试循环(每次尝试: 选源 → 熔断门(源) → 限流 permit(全局+源) → transport)** > 2026-07-20 修订(CHS 迁移文档缺口 G3): 初版把熔断/限流画在重试循环外,与"每次重试重新过限流闸"的理由自相矛盾,且熔断/限流是 **per-source** 的——源在循环内才被选出,准入只能发生在循环内。修订后与 CHSAnalyzer 实践(`governance.py:120-167` 逐次尝试执行选源→熔断→permit)一致。 @@ -291,6 +292,7 @@ flowchart TB |---|---| | 遥测最外 | 观测一切,包括缓存命中与各类失败;任何路径都留痕 | | 缓存在重试循环外 | 缓存命中不打网关:不消耗限流配额、不受熔断状态影响 | +| 结构化在缓存内、重试外(2026-07-20 M1 设计) | 带反馈重问 = 再次调用内层,天然照过限流/熔断门、逐次遥测;缓存只固化阶梯通过的最终结果 | | 重试循环拥有"尝试"的全部编排 | 每次尝试 = 选源(跳过冷却源)→ 该源熔断门(开路视为该源不可用,换源)→ 全局+该源限流 permit → transport;换源、逐次遥测、熔断计数都在循环内(与 D13 自研理由同构) | | 熔断门先于限流 | 开路源直接跳过,不占限流租约、不排队等配额 | | 每次尝试独立过限流闸 | 重试是真实网络请求,必须重新准入,否则重试风暴击穿配额;限流后端看到的是"含重试的真实请求数" | @@ -326,7 +328,7 @@ flowchart TB | `cache_hit` | bool | 是否缓存命中 | | `call_id` | str | UUID,每次**尝试**独立 | -新增字段(库扩展): `source_name`(多源溯源)、`cost`(pricing 换算,可为 None)、`usage_source`(measured/estimated)。 +新增字段(库扩展,全部带默认值): `source_name`(多源溯源)、`cost`(pricing 换算,可为 None)、`usage_source`(measured/estimated)、`structured_data`(D14 阶梯通过后的解析产物;不参与缓存序列化,命中时由 CacheMW 复用 strategy 零网络重建)。 **API 稳定性约定(2026-07-20,迁移文档反向约束)**: ① 公共类型新增字段必须带默认值——三项目测试中逐字段传参的 fake 构造才能零改动;② 错误四分类从 `polygateway` 顶层命名空间导出——业务侧步级重试要引用它们(GovDoc/Video-Tree 现有 `(TimeoutError, OSError)` 异常元组迁移后会**静默失效**,必须显式替换为库异常);③ `GatewayClient` 提供显式 `aclose()` 与 async context manager 生命周期 API;④ 被取消的调用尽力而为记遥测(error="cancelled",finally 中记录,绝不因遥测延迟取消传播,写失败静默)。 @@ -334,7 +336,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 重采样);④ `structured` 三档语义(D14): 不传 = 原始文本,`"json"` = 仅修复,pydantic 模型 = 完整阶梯(修复+形态校验+有界带反馈重问)。 +**`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),类型定稿 `type[BaseModel] | Literal["json"] | None`(M1 设计): 不传 = 原始文本,`"json"` = 仅修复,pydantic 模型 = 完整阶梯(修复+形态校验+有界带反馈重问)。 --- @@ -350,7 +352,7 @@ flowchart TB | `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` 复现。 +**scope 级不可用的结构化语义(2026-07-20,CHS 迁移缺口 G1;2026-07-20 M1 设计勘误修订)**: `AllSourcesExhausted`/`CircuitOpenError` 必须携带结构化字段——`retry_after_s: float`(**非可选**,承 CHS `ProviderUnavailableError` 同款,0 表示可立即重试;取各源冷却与 Retry-After 的最小值)、`reason` 枚举、`per_source_reasons: dict[str, str]`。reason 两层值域(M1 设计 §3 勘误: 本节初版所列 7 值与 CHS `errors.py:143-153` 实际值域不符,重组如下)——scope 级 `reason`: circuit_open / retry_exhausted / stalled / quota_exhausted / no_sources;`per_source_reasons` 值: network_error / timeout / rate_limited / source_dead / circuit_open / cooldown。CHS 的"scope 级不可用 → arq 延期重投、不消耗业务失败预算"(`workers/tracking.py:406-428`)依赖 `retry_after_s` 复现。 ### 6.2 翻译规则(transport 层职责) @@ -378,7 +380,7 @@ flowchart TB **职责**: 一次原始调用的全部协议细节——请求体组装(含 provider 注册表注入的 thinking 参数)、发送、流式 SSE 解析(增量 content/reasoning_content、usage 帧、[DONE] 检测)、HTTP/线路错误按 §6.2 翻译。**不含**重试/限流/缓存(那是中间件的事)。 -- `OpenAICompatTransport`(默认): httpx.AsyncClient(每源一个,预配 Authorization 与分段超时),SSE 解析移植三项目的模块级纯函数;强制 `stream_options.include_usage`。**非流式快路径**: 短请求可配 `stream=False`(三项目都写死 stream=True 强迫短请求走 SSE+看门狗,库放开)。 +- `OpenAICompatTransport`(默认): httpx.AsyncClient(每源一个,预配 Authorization 与分段超时),SSE 解析移植三项目的模块级纯函数;强制 `stream_options.include_usage`。**SSE 缺 [DONE] 语义(2026-07-20 M1 设计)**: per-source `missing_done: "retry" | "salvage"`,默认 retry(防截断响应进缓存被固化);零内容提前断流(early_eof)恒 retry 不可配;打捞路径强制 `usage_source="estimated"`。CHS 迁移配 salvage 保留其现状行为。看门狗活性口径: 任何增量(content 或 reasoning_content)都算 token——ttft = 首个任意 token,思考流刷新 inter_token 计时(CHS 迁移约束 R1)。**非流式快路径**: 短请求可配 `stream=False`(三项目都写死 stream=True 强迫短请求走 SSE+看门狗,库放开)。 - `OpenAISDKTransport`(可选 extra): 薄封装,`max_retries=0` 关掉 SDK 自带重试(治理归中间件),`extra_body`/`model_extra` 通道非标字段。 - `MonkeyOcrTransport`: 见 §7.10。 @@ -439,10 +441,10 @@ flowchart TB | ① 预防 | provider 注册表声明支持时,用 response_format / function calling 直接约束(`NativeSchemaStrategy`) | 无额外 | | ② 修复 | 围栏剥离 → json_repair → provider 变体归一化(DeepSeek 参数平铺等)(`JsonRepairStrategy`) | 零网络 | | ③ 校验 | 调用方传 pydantic 模型时库内做**形态**校验;语义校验留业务层 | 零网络 | -| ④ 受约束重问 | `max_structured_retries`(默认 1-2;0 = CHS"不重试转人工"策略): 校验错误作为反馈追加重问(messages 已变,不命中原坏答案缓存);可从 ② 升级到 ① 策略;照过限流闸,**不计熔断**,逐次遥测 | 真实调用 | +| ④ 受约束重问 | `max_structured_retries`(默认 1;0 = CHS"不重试转人工"策略): 校验错误作为反馈追加重问(messages 已变,不命中原坏答案缓存);重问一律叠加 ① 策略(若 provider 支持)——修复失败说明 prompt 约定不够,升级到协议约束;照过限流闸,**不计熔断**,逐次遥测 | 真实调用 | | ⑤ 耗尽 | 抛 `ResultInvalidError`,携原始文本 + 修复错误 + 校验错误,业务决定兜底(人工复核/降级) | — | -`structured` 参数三档: 不传 = 原始文本(零负担);`"json"` = 仅②;pydantic 模型 = ①-⑤ 完整阶梯。缓存写入发生在阶梯通过之后——这是 §7.5 "ResultInvalid 的原始响应不缓存"的执行点。 +`structured` 参数三档: 不传 = 原始文本(零负担);`"json"` = 仅②;pydantic 模型 = ①-⑤ 完整阶梯。缓存写入发生在阶梯通过之后——这是 §7.5 "ResultInvalid 的原始响应不缓存"的执行点。**细则(2026-07-20 M1 设计)**: 反馈模板为库内英文常量(原 messages + assistant 坏输出 + user 纠错指令,错误取前 3 条、每条截断 200 字符);provider 变体归一化(如 DeepSeek 参数平铺收拢)面向特定业务 schema,**不入库**——`JsonRepairStrategy(normalize: Callable | None)` 提供注入钩子,业务侧自带(零业务假设铁律)。 ### 7.10 OCR 端口族 @@ -467,7 +469,7 @@ src/polygateway/ ├── errors.py # §6 错误四分类 ├── ports.py # 全部 Protocol(§4 各端口) ├── client.py # GatewayClient + from_env()/from_settings() 装配工厂 + gather_bounded -├── middleware/ # retry.py / ratelimit.py / breaker.py / cache.py / telemetry.py +├── middleware/ # retry.py / ratelimit.py / breaker.py / cache.py / telemetry.py / structured.py ├── transports/ # openai_compat.py / openai_sdk.py / monkey_ocr.py ├── providers.py # D11 provider 注册表 ├── sources.py # SourceConfig + 选源策略