docs: 修正第四轮审查发现的 6 处不一致
3 条仍是我前两轮修正的次生偏差(全局 TPM 闸"完全失效"实为滞后闸、 PROVIDER 段对 EMBED 并无注册表约束、timeout 桶不止 httpx 超时)。 另 3 条为建站即错: EMBED 的健康降权通道从不喂数(health_aware 静默退化 为 least_inflight)、structured=json 档解析失败的溯源字段也全为 None、 __version__ 在自称全集的页面里缺席。 P1 与 P5 按报告建议改为描述性判据,不再给闭合可数清单。
+1
-1
@@ -61,4 +61,4 @@
|
|||||||
| `ProviderProfile` / `DEFAULT_PROFILES` / `register_provider` | provider 方言注册表。`register_provider(profile, *, base=None)` 是**纯函数**——返回 `base`(缺省 `DEFAULT_PROFILES`)+ 新条目的**新表**(同名覆盖),不改全局状态(`DEFAULT_PROFILES` 是 MappingProxyType,改不动)。`base` 是 **keyword-only**,位置传参会 `TypeError`。构造 profile 用 `ProviderProfile(name, thinking_on, thinking_off, strip_think_tags, supports_native_schema=False)`——**前四个必填无默认**;`thinking_on`/`thinking_off` 是 `ENABLE_THINKING` 为 True/False 时并入请求体的片段(两档皆填 `{}` 表示该 provider 无推理开关,见 [[指南-采样参数]]),`strip_think_tags` 声明是否剥离 `<think>` 标签。新表必须经 `from_env(registry=...)` / `from_settings(registry=...)` 传入才生效;**只调 `register_provider()` 不传 `registry=`,装配期必抛 `ValueError: 未注册的 provider`**。内置四个 profile 的 `supports_native_schema` 均为 `False` |
|
| `ProviderProfile` / `DEFAULT_PROFILES` / `register_provider` | provider 方言注册表。`register_provider(profile, *, base=None)` 是**纯函数**——返回 `base`(缺省 `DEFAULT_PROFILES`)+ 新条目的**新表**(同名覆盖),不改全局状态(`DEFAULT_PROFILES` 是 MappingProxyType,改不动)。`base` 是 **keyword-only**,位置传参会 `TypeError`。构造 profile 用 `ProviderProfile(name, thinking_on, thinking_off, strip_think_tags, supports_native_schema=False)`——**前四个必填无默认**;`thinking_on`/`thinking_off` 是 `ENABLE_THINKING` 为 True/False 时并入请求体的片段(两档皆填 `{}` 表示该 provider 无推理开关,见 [[指南-采样参数]]),`strip_think_tags` 声明是否剥离 `<think>` 标签。新表必须经 `from_env(registry=...)` / `from_settings(registry=...)` 传入才生效;**只调 `register_provider()` 不传 `registry=`,装配期必抛 `ValueError: 未注册的 provider`**。内置四个 profile 的 `supports_native_schema` 均为 `False` |
|
||||||
| `PricingTable` / `ModelPrice` | 价格表(成本折算) |
|
| `PricingTable` / `ModelPrice` | 价格表(成本折算) |
|
||||||
|
|
||||||
异常层级见 [[参考-异常]];全部 env 键见 [[参考-配置键]]。
|
版本号也走顶层导出:`polygateway.__version__`(当前 `1.0.5`),日志与 bug 报告可直接带上。异常层级见 [[参考-异常]];全部 env 键见 [[参考-配置键]]。
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@
|
|||||||
| `repair_error` | JSON 修复失败的原因 |
|
| `repair_error` | JSON 修复失败的原因 |
|
||||||
| `validation_errors` | tuple,pydantic 校验错误 |
|
| `validation_errors` | tuple,pydantic 校验错误 |
|
||||||
|
|
||||||
`source_name` / `status_code` 为 `None` 的路径有**两条**:① 结构化阶梯耗尽(失败不归因于某个源);② **OCR `parse_layout` 的结果包解析失败**(坏 ZIP / 缺 `_middle.json` / 退化 bbox / 非有限数值)——三个溯源字段全为 `None`,所以别在 except 里读 `exc.source_name` 定位坏源(会拿到 `None`,`.lower()` 直接 AttributeError),OCR 多实例场景请用遥测行定位。
|
**判据(不是可数清单)**:凡由**结构化解析层**(`structured="json"` 档解析失败、pydantic 档阶梯耗尽)与 **OCR 结果包解析**(坏 ZIP / 缺 `_middle.json` / 退化 bbox / 非有限数值)抛出的 `ResultInvalidError`,三个溯源字段全为 `None`;只有 transport 与 embedding 层抛的才带 `source_name`。所以**不要**在 `except ResultInvalidError` 里直接写 `exc.source_name.lower()`(会 `AttributeError`)——定位坏源请用遥测行。注意 `structured="json"` 是常规用法且**单次即抛不走阶梯**,它的解析失败正命中这条。
|
||||||
|
|
||||||
## scope 级不可用(重试预算走完后)
|
## scope 级不可用(重试预算走完后)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
| `scope` | 哪个 scope(小写) |
|
| `scope` | 哪个 scope(小写) |
|
||||||
| `reason` | **scope 级**受控词表(越界值构造期报错): `circuit_open` / `retry_exhausted` / `stalled` / `quota_exhausted`(配额满且 `QUOTA_FULL=fail_fast`)/ `no_sources` |
|
| `reason` | **scope 级**受控词表(越界值构造期报错): `circuit_open` / `retry_exhausted` / `stalled` / `quota_exhausted`(配额满且 `QUOTA_FULL=fail_fast`)/ `no_sources` |
|
||||||
| `retry_after_s` | 最早值得重试的秒数,**取值来源随 reason 而异**:`circuit_open` 与「无源可跑的 stalled」读熔断后端;`retry_exhausted` = `BACKOFF_BASE_S`;**429 持续 pushback 触发的调用级 `stalled` 也 = `BACKOFF_BASE_S`**(不读后端,故拿到的是 1-2s 量级而非源冷却期);`quota_exhausted` = `POLL_INTERVAL_S`(缺省 0.05s);`no_sources` = 0.0。这是**最短建议间隔而非源恢复时间**,队列重投请自加下限 |
|
| `retry_after_s` | 最早值得重试的秒数,**取值来源随 reason 而异**:`circuit_open` 与「无源可跑的 stalled」读熔断后端;`retry_exhausted` = `BACKOFF_BASE_S`;**429 持续 pushback 触发的调用级 `stalled` 也 = `BACKOFF_BASE_S`**(不读后端,故拿到的是 1-2s 量级而非源冷却期);`quota_exhausted` = `POLL_INTERVAL_S`(缺省 0.05s);`no_sources` = 0.0。这是**最短建议间隔而非源恢复时间**,队列重投请自加下限 |
|
||||||
| `per_source_reasons` | 逐源失败原因字典,诊断用。**值域与 `reason` 是两张表**: `network_error` / `timeout` / `rate_limited` / `source_dead` / `circuit_open` / `cooldown` / `adaptive_paced`。注意**上游 429 与本地限流闸拒绝都记 `rate_limited`,二者不可区分**;`adaptive_paced` 只由 AIMD pacer 拦截产生。另注意 **`network_error` 是兜底桶而非字面网络错误**——归类只识别 source_dead / 429 / httpx 超时三类,其余 Transient(HTTP 5xx、空补全、SSE 截断、响应体非法 JSON、缺 choices)全落进它;要区分真实成因请看遥测行的 error 文本 |
|
| `per_source_reasons` | 逐源失败原因字典,诊断用。**值域与 `reason` 是两张表**: `network_error` / `timeout` / `rate_limited` / `source_dead` / `circuit_open` / `cooldown` / `adaptive_paced`。注意**上游 429 与本地限流闸拒绝都记 `rate_limited`,二者不可区分**;`adaptive_paced` 只由 AIMD pacer 拦截产生。另注意 **`network_error` 是兜底桶而非字面网络错误**——归类只识别 source_dead / 429 / 超时三类,其余 Transient(HTTP 5xx、空补全、SSE 截断、响应体非法 JSON、缺 choices)全落进它。而 `timeout` 桶也不止 httpx 超时:**库自身的 `StreamLivenessTimeout`(kind ∈ ttft / inter_token / total)同样记 `timeout`**,且 total 层不配也按 `TIMEOUT_S` 恒生效——「长生成超总预算」这条最常见路径根本不经 httpx 却仍落这个桶,两者在 `per_source_reasons` 上不可区分。**排障别急着加大 `LLM_TIMEOUT`**(它同时是 total 层看门狗的预算),先看遥测 error 文本区分「流活性超时(kind)」与「超时: …」,该调的通常是 `TTFT_TIMEOUT_S` / `INTER_TOKEN_TIMEOUT_S` |
|
||||||
|
|
||||||
## 基建故障
|
## 基建故障
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@
|
|||||||
| FIELD | 必填 | 说明 |
|
| FIELD | 必填 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| BASE_URL / API_KEY / MODEL | ✔ | 无鉴权服务 API_KEY 填占位 `none` |
|
| BASE_URL / API_KEY / MODEL | ✔ | 无鉴权服务 API_KEY 填占位 `none` |
|
||||||
| (PROVIDER 段) | ✔ | chat/embed scope 的 PROVIDER 必须是注册表里的键;**OCR scope 的 PROVIDER 段必须字面写 `MONKEY`**(大小写不敏感),否则装配期抛 `ValueError: OCR 装配仅支持 provider=monkey`——D9 的其余 OCR 后端尚未实现 |
|
| (PROVIDER 段) | ✔ | **三个 scope 的约束各不相同**。chat(`GatewayClient`): 必须是注册表里的键,装配期逐源 `get_provider`,未注册即 `ValueError: 未注册的 provider`。**EMBED: 不查注册表**——`EmbeddingClient` 与 `/embeddings` 调用全程不读 `ProviderProfile`,PROVIDER 段可填任意名(bge / jina / vllm 等自建端点照实填即可),它只是遥测 `provider` 列与 `EmbeddingResponse.provider` 的标签,**别为迁就注册表谎报成 qwen/openai**,否则成本无法按真实供应商归因。OCR: 必须字面写 `MONKEY`(大小写不敏感),否则装配期抛 `ValueError: OCR 装配仅支持 provider=monkey`(D9 其余后端未实现) |
|
||||||
| TIMEOUT_S | ✔(或平铺 `LLM_TIMEOUT` 兜底) | 单次调用墙钟上限;须 ≤ `PGW_LEASE_TTL_S` |
|
| TIMEOUT_S | ✔(或平铺 `LLM_TIMEOUT` 兜底) | 单次调用墙钟上限;须 ≤ `PGW_LEASE_TTL_S` |
|
||||||
| MAX_CONCURRENCY / RPM / TPM | | 0/缺省=不启用;照供应商配额页填即可,无需搭配 EST_TOKENS |
|
| MAX_CONCURRENCY / RPM / TPM | | 0/缺省=不启用;照供应商配额页填即可,无需搭配 EST_TOKENS |
|
||||||
| EST_TOKENS | | **可选调优覆盖**(v1.0.3 起由必填降为可选)。TPM 入场预扣量,按实际用量结算退款;不填时库按 `max(1, TPM // 60)` 派生——即"一次调用约占一秒钟的配额份额",任何配额规模都收敛到约 60 个在途 |
|
| EST_TOKENS | | **可选调优覆盖**(v1.0.3 起由必填降为可选)。TPM 入场预扣量,按实际用量结算退款;不填时库按 `max(1, TPM // 60)` 派生——即"一次调用约占一秒钟的配额份额",任何配额规模都收敛到约 60 个在途 |
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
| 键 | 说明 |
|
| 键 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | 跨源合计闸。**全局 TPM 要成为准入闸有前置条件**:入场预扣量取自**源级**(`EST_TOKENS` 或 `max(1, 源TPM//60)` 派生)——只配 `{SCOPE}__GLOBAL__TPM` 而每个源上既无 TPM 也无 EST_TOKENS 时,预扣恒为 0、入场永远放行,该闸退化成事后计数 |
|
| `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | 跨源合计闸。入场预扣量取自**源级**(`EST_TOKENS` 或 `max(1, 源TPM//60)` 派生)。只配 `{SCOPE}__GLOBAL__TPM` 而源上既无 TPM 也无 EST_TOKENS 时预扣恒为 0——闸失去的是**预留**能力(一批并发会被同时放行、可远超上限),但**它仍是准入闸**:结算把每次调用的真实 token 补记进全局分钟窗口,累计一旦超上限后续入场即被拒(判据是 `已用量 + 预扣 ≤ 上限`),直到窗口翻转才恢复。即**超额一次才刹车,不是完全失效**。`quota_full=wait`(默认)表现为轮询等待到整分钟翻页(现象像"请求集体挂住、日志无错"),`fail_fast` 抛 `AllSourcesExhausted(reason='quota_exhausted')`。要让它按预期节流,请给源配 `TPM` 或 `EST_TOKENS` |
|
||||||
| `{SCOPE}__SELECTOR` | health_aware(默认)/ round_robin / least_inflight |
|
| `{SCOPE}__SELECTOR` | health_aware(默认)/ round_robin / least_inflight |
|
||||||
| `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S` | **必填**(或用下方平铺简写);MAX_ATTEMPTS 含首次。退避公式 `min(BASE × 2^(n-1), MAX) × jitter`,**jitter ∈ [0.5, 1.5)**——`BACKOFF_MAX_S` 封顶的是抖动**前**的基数,单次实际等待最大是它的 1.5 倍,按它估算任务软超时请乘 1.5 |
|
| `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S` | **必填**(或用下方平铺简写);MAX_ATTEMPTS 含首次。退避公式 `min(BASE × 2^(n-1), MAX) × jitter`,**jitter ∈ [0.5, 1.5)**——`BACKOFF_MAX_S` 封顶的是抖动**前**的基数,单次实际等待最大是它的 1.5 倍,按它估算任务软超时请乘 1.5 |
|
||||||
| `{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S` | FAIL_THRESHOLD 与 COOLDOWN_S **必填**(或用平铺简写);PROBE_TTL_S 可省,缺省派生 `max(2 × 最慢源 timeout_s, COOLDOWN_S, 最慢源 timeout_s + 5)`——如 timeout 120 / cooldown 30 得 **240**(不是 125)。**显式配置值**另须 ≥ 最慢源 `timeout_s + 5`,否则装配期报错(派生公式与校验下限是两回事) |
|
| `{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S` | FAIL_THRESHOLD 与 COOLDOWN_S **必填**(或用平铺简写);PROBE_TTL_S 可省,缺省派生 `max(2 × 最慢源 timeout_s, COOLDOWN_S, 最慢源 timeout_s + 5)`——如 timeout 120 / cooldown 30 得 **240**(不是 125)。**显式配置值**另须 ≥ 最慢源 `timeout_s + 5`,否则装配期报错(派生公式与校验下限是两回事) |
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
# 指南:Embedding
|
# 指南:Embedding
|
||||||
|
|
||||||
与 chat 同一治理栈:多源、重试、熔断、遥测;外加分批与维度校验。
|
与 chat 共用治理件:多源、重试、熔断、遥测;外加分批与维度校验。
|
||||||
|
|
||||||
|
> **两处与 chat 不同,都是静默的**:① **健康降权通道对 EMBED 不生效**——喂数只发生在 chat 与 OCR 路径,EMBED 的健康分恒为初值,`health_aware`(默认)实际退化成 `least_inflight`,隔离坏源**只能靠熔断**;② 429 照常计入 `MAX_ATTEMPTS`(chat 路径才有 pushback 豁免)。两者都不报错、遥测也看不出来。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ LLM__QWEN__2__TIMEOUT_S=120
|
|||||||
| `round_robin` | 轮询 | 想要严格均摊 |
|
| `round_robin` | 轮询 | 想要严格均摊 |
|
||||||
| `least_inflight` | 选在途最少 | 源性能差异大 |
|
| `least_inflight` | 选在途最少 | 源性能差异大 |
|
||||||
|
|
||||||
健康视图由每次尝试的结果喂数(真实成功=好;瞬时失败/429/源死=坏;**坏结果不算坏服务**,不降权)。
|
健康视图由每次尝试的结果喂数(真实成功=好;瞬时失败/429/源死=坏;**坏结果不算坏服务**,不降权)。**喂数只发生在 chat 与 OCR 路径**——`EmbeddingClient` 不喂,所以 EMBED scope 的健康分恒为初值、`health_aware` 退化为 `least_inflight`(见 [[指南-Embedding]])。
|
||||||
|
|
||||||
## 换源与冷却行为
|
## 换源与冷却行为
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
> **RPM/TPM 是分钟固定窗口**(按 `int(now/60)` 分桶,整分钟到点归零),不是滑动窗口。照抄供应商的滑动窗口配额会在**整分钟交界处出现约 2 倍瞬时速率**(上一分钟末尾打满 + 新分钟开头再打满),建议配成配额的一半左右。
|
> **RPM/TPM 是分钟固定窗口**(按 `int(now/60)` 分桶,整分钟到点归零),不是滑动窗口。照抄供应商的滑动窗口配额会在**整分钟交界处出现约 2 倍瞬时速率**(上一分钟末尾打满 + 新分钟开头再打满),建议配成配额的一半左右。
|
||||||
>
|
>
|
||||||
> **全局 TPM 的预扣量取自源级**:入场预扣是每个源的 `EST_TOKENS`(或由源 `TPM // 60` 派生)。只配 `LLM__GLOBAL__TPM` 而源上既无 `TPM` 也无 `EST_TOKENS` 时,预扣恒为 0、入场永远放行,全局 TPM 闸退化成事后计数。
|
> **全局 TPM 的预扣量取自源级**:入场预扣是每个源的 `EST_TOKENS`(或由源 `TPM // 60` 派生)。只配 `LLM__GLOBAL__TPM` 而源上既无 `TPM` 也无 `EST_TOKENS` 时预扣恒为 0——闸失去的是**预留**能力(一批并发会被同时放行、可远超上限),但**它仍是准入闸**:结算把真实 token 补记进全局分钟窗口,累计超上限后入场即被拒,直到窗口翻转才恢复。即**超额一次才刹车,不是完全失效**;`quota_full=wait` 下表现为轮询等待到整分钟翻页(像"请求集体挂住、日志无错")。要按预期节流请给源配 `TPM` 或 `EST_TOKENS`。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
LLM__QWEN__1__MAX_CONCURRENCY=8 # 单源并发
|
LLM__QWEN__1__MAX_CONCURRENCY=8 # 单源并发
|
||||||
|
|||||||
Reference in New Issue
Block a user