docs: 修正审查发现的 6 处事实错误,并发布 v1.0.5
reason 词表混淆了 scope 级与 source 级两张表;教程说改错 key 得到 AllSourcesExhausted,实测是 CircuitOpenError(照写会捕不到);TPM 的 EST_TOKENS 必填在 v1.0.3 已废除;limiter/breaker 后端标错必填而真正 必填的 RETRY/BREAKER 键没标;llm_calls 是 22 列(21 是端口参数数,差 created_at);结构化覆盖 response_format 只在原生 schema 策略下成立。
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
实验室统一的大语言模型调度与中转库:LLM / VLM / OCR / Embedding 共用同一套生产级治理栈(多源多账号、限流、错误分类重试、熔断、响应缓存、流式看门狗、遥测与成本)。治理单位是**一次模型调用**;任务编排与业务解析留在业务侧。
|
实验室统一的大语言模型调度与中转库:LLM / VLM / OCR / Embedding 共用同一套生产级治理栈(多源多账号、限流、错误分类重试、熔断、响应缓存、流式看门狗、遥测与成本)。治理单位是**一次模型调用**;任务编排与业务解析留在业务侧。
|
||||||
|
|
||||||
当前版本 **v1.0.4**,已通过 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目的全量迁移验收。
|
当前版本 **v1.0.5**,已通过 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目的全量迁移验收。
|
||||||
|
|
||||||
## 文档地图(按你此刻要干什么选入口)
|
## 文档地图(按你此刻要干什么选入口)
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -8,8 +8,9 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `from_env` | `(scope="LLM", *, limiter=None, breaker=None, cache=None, telemetry=None, registry=None, env=None) -> GatewayClient` | 从 .env/环境变量装配;关键字参数可注入自定义后端(测试/共享状态) |
|
| `from_env` | `(scope="LLM", *, limiter=None, breaker=None, cache=None, telemetry=None, registry=None, env=None) -> GatewayClient` | 从 .env/环境变量装配;关键字参数可注入自定义后端(测试/共享状态) |
|
||||||
| `from_settings` | `(settings: GatewaySettings, ...) -> GatewayClient` | 从已解析配置装配 |
|
| `from_settings` | `(settings: GatewaySettings, ...) -> GatewayClient` | 从已解析配置装配 |
|
||||||
| `chat` | `(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True, overlay=None) -> LLMResponse` | 一次治理调用;messages 为 OpenAI 形态(原生支持多模态 content 数组);`structured` 传 pydantic 模型类或 `"json"`;`overlay` 传采样参数(未发布版新增,见 [[指南-采样参数]]) |
|
| `chat` | `(messages, *, session_id=None, parent_call_id=None, cache_salt=None, cache_namespace=None, structured=None, stream=True, overlay=None) -> LLMResponse` | 一次治理调用;messages 为 OpenAI 形态(原生支持多模态 content 数组);`structured` 传 pydantic 模型类或 `"json"`;`overlay` 传采样参数(v1.0.5新增,见 [[指南-采样参数]]) |
|
||||||
| `aclose` | `() -> None` | 幂等释放连接与后端资源 |
|
| `aclose` | `() -> None` | 幂等释放连接与后端资源 |
|
||||||
|
| `gather_bounded`(模块级函数) | `(coros, limit) -> list` | 有界并发跑一批协程,顶层导出;用它替代裸 `asyncio.gather`,避免一次性把几百个请求压进治理栈 |
|
||||||
|
|
||||||
## LLMResponse(frozen dataclass)
|
## LLMResponse(frozen dataclass)
|
||||||
|
|
||||||
@@ -38,13 +39,13 @@
|
|||||||
|
|
||||||
`EmbeddingResponse`:vectors(等长保序)/ dim / model / provider / prompt_tokens / usage_source / latency_ms / call_id / source_name / cost。
|
`EmbeddingResponse`:vectors(等长保序)/ dim / model / provider / prompt_tokens / usage_source / latency_ms / call_id / source_name / cost。
|
||||||
|
|
||||||
## OcrClient(`from polygateway.ocr import OcrClient`)
|
## OcrClient(`from polygateway import OcrClient`,亦可从 `polygateway.ocr` 导入)
|
||||||
|
|
||||||
| 方法 | 签名 |
|
| 方法 | 签名 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `from_env` | `(scope="OCR", ..., env=None) -> OcrClient` |
|
| `from_env` | `(scope="OCR", ..., env=None) -> OcrClient` |
|
||||||
| `recognize_text` | `(image: bytes) -> OcrTextResult` |
|
| `recognize_text` | `(image: bytes, *, session_id=None, parent_call_id=None) -> OcrTextResult` |
|
||||||
| `parse_layout` | `(image: bytes) -> OcrLayoutResult` |
|
| `parse_layout` | `(image: bytes, *, session_id=None, parent_call_id=None) -> OcrLayoutResult` |
|
||||||
| `check_health` | `() -> dict[str, bool]`(逐源并发预检) |
|
| `check_health` | `() -> dict[str, bool]`(逐源并发预检) |
|
||||||
| `aclose` | `() -> None` |
|
| `aclose` | `() -> None` |
|
||||||
|
|
||||||
@@ -55,8 +56,8 @@
|
|||||||
| 导出 | 用途 |
|
| 导出 | 用途 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `GatewaySettings` / `EmbeddingSettings` / `OcrSettings` | `from_env` 的解析产物;高级场景可自行构造后走 `from_settings` |
|
| `GatewaySettings` / `EmbeddingSettings` / `OcrSettings` | `from_env` 的解析产物;高级场景可自行构造后走 `from_settings` |
|
||||||
| `SourceConfig` | 单源完整配置(构造期校验不变式)。方法 `effective_est_tokens() -> int`:TPM 入场预扣量,显式 `est_tokens > 0` 优先,否则按 `max(1, tpm // 60)` 派生,`tpm=0` 时为 0(v1.0.3 新增)。字段 `extra_body`(未发布版新增):本源恒定的采样参数,构造后是只读视图——**该字段令 `SourceConfig` 不再 hashable**,`asdict()`/`deepcopy()` 亦不再适用(加任何 mapping 字段的固有代价);要可变副本用 `dict(source.extra_body)`,要改字段用 `dataclasses.replace` |
|
| `SourceConfig` | 单源完整配置(构造期校验不变式)。方法 `effective_est_tokens() -> int`:TPM 入场预扣量,显式 `est_tokens > 0` 优先,否则按 `max(1, tpm // 60)` 派生,`tpm=0` 时为 0(v1.0.3 新增)。字段 `extra_body`(v1.0.5新增):本源恒定的采样参数,构造后是只读视图——**该字段令 `SourceConfig` 不再 hashable**,`asdict()`/`deepcopy()` 亦不再适用(加任何 mapping 字段的固有代价);要可变副本用 `dict(source.extra_body)`,要改字段用 `dataclasses.replace` |
|
||||||
| `ProviderProfile` / `DEFAULT_PROFILES` | provider 方言注册表(thinking 注入方式、思考流字段等);自定义 provider 经 `registry=` 传入 |
|
| `ProviderProfile` / `DEFAULT_PROFILES` / `register_provider` | provider 方言注册表(thinking 注入方式、思考流字段等);自定义 provider 经 `register_provider()` 注册或 `registry=` 传入 |
|
||||||
| `PricingTable` / `ModelPrice` | 价格表(成本折算) |
|
| `PricingTable` / `ModelPrice` | 价格表(成本折算) |
|
||||||
|
|
||||||
异常层级见 [[参考-异常]];全部 env 键见 [[参考-配置键]]。
|
异常层级见 [[参考-异常]];全部 env 键见 [[参考-配置键]]。
|
||||||
|
|||||||
+2
-2
@@ -18,9 +18,9 @@
|
|||||||
| 属性 | 含义 |
|
| 属性 | 含义 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `scope` | 哪个 scope(小写) |
|
| `scope` | 哪个 scope(小写) |
|
||||||
| `reason` | 受控词表: network_error / timeout / rate_limited / source_dead / circuit_open / retry_exhausted / stalled |
|
| `reason` | **scope 级**受控词表(越界值构造期报错): `circuit_open` / `retry_exhausted` / `stalled` / `quota_exhausted`(配额满且 `QUOTA_FULL=fail_fast`)/ `no_sources` |
|
||||||
| `retry_after_s` | 最早值得重试的秒数(读熔断后端;0=可立即);任务队列按它延期重投 |
|
| `retry_after_s` | 最早值得重试的秒数(读熔断后端;0=可立即);任务队列按它延期重投 |
|
||||||
| `per_source_reasons` | 逐源失败原因字典,诊断用 |
|
| `per_source_reasons` | 逐源失败原因字典,诊断用。**值域与 `reason` 是两张表**: `network_error` / `timeout` / `rate_limited` / `source_dead` / `circuit_open` / `cooldown` / `adaptive_paced` |
|
||||||
|
|
||||||
## 基建故障
|
## 基建故障
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -41,8 +41,8 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | 跨源合计闸 |
|
| `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | 跨源合计闸 |
|
||||||
| `{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 含首次) |
|
| `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S` | **必填**(或用下方平铺简写);MAX_ATTEMPTS 含首次 |
|
||||||
| `{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S` | 熔断基本参数 |
|
| `{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S` | FAIL_THRESHOLD 与 COOLDOWN_S **必填**(或用平铺简写);PROBE_TTL_S 可省 |
|
||||||
| `{SCOPE}__BREAKER__MIN_CALLS / FAIL_RATE / WINDOW_S / MAX_COOLDOWN_S` | 失败率通道与开路退避封顶 |
|
| `{SCOPE}__BREAKER__MIN_CALLS / FAIL_RATE / WINDOW_S / MAX_COOLDOWN_S` | 失败率通道与开路退避封顶 |
|
||||||
| `{SCOPE}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S` | 配额满等待的判死窗口(须 ≥ 最大源 TTFT) |
|
| `{SCOPE}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S` | 配额满等待的判死窗口(须 ≥ 最大源 TTFT) |
|
||||||
| `{SCOPE}__QUOTA_FULL` | wait(默认)/ fail_fast |
|
| `{SCOPE}__QUOTA_FULL` | wait(默认)/ fail_fast |
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
| 键 | 取值 | 备注 |
|
| 键 | 取值 | 备注 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PGW_LIMITER_BACKEND` / `PGW_BREAKER_BACKEND` | memory / redis | 必填;多进程必须 redis(需 `REDIS_URL`) |
|
| `PGW_LIMITER_BACKEND` / `PGW_BREAKER_BACKEND` | memory / redis | 缺省 `memory`;多进程必须 redis(需 `REDIS_URL`) |
|
||||||
| `PGW_CACHE_BACKEND` | none / memory / redis | 必填;redis 需 NAMESPACE + TTL_S + REDIS_URL |
|
| `PGW_CACHE_BACKEND` | none / memory / redis | 必填;redis 需 NAMESPACE + TTL_S + REDIS_URL |
|
||||||
| `PGW_CACHE_NAMESPACE` / `PGW_CACHE_TTL_S` | | 缓存启用时必填;TTL 必须 > 0 |
|
| `PGW_CACHE_NAMESPACE` / `PGW_CACHE_TTL_S` | | 缓存启用时必填;TTL 必须 > 0 |
|
||||||
| `PGW_TELEMETRY_BACKEND` | none / sqlite / postgres | 必填;sqlite 需 `PGW_TELEMETRY_SQLITE_PATH`,postgres 需 `PGW_TELEMETRY_PG_DSN` |
|
| `PGW_TELEMETRY_BACKEND` | none / sqlite / postgres | 必填;sqlite 需 `PGW_TELEMETRY_SQLITE_PATH`,postgres 需 `PGW_TELEMETRY_PG_DSN` |
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ key = `model + messages 摘要 + namespace + salt + sampling`;多模态 content(
|
|||||||
|---|---|
|
|---|---|
|
||||||
| namespace 必填 | 跨项目/跨租户互相读到对方缓存 |
|
| namespace 必填 | 跨项目/跨租户互相读到对方缓存 |
|
||||||
| salt(per-call) | 需要强制重采样的场景命中旧缓存 |
|
| salt(per-call) | 需要强制重采样的场景命中旧缓存 |
|
||||||
| sampling(未发布版) | 不同解码参数的响应互相污染——尤其是同 messages 跑多个 seed 时全部命中第一次的结果,标准差恒为 0 且不报错 |
|
| sampling(v1.0.5) | 不同解码参数的响应互相污染——尤其是同 messages 跑多个 seed 时全部命中第一次的结果,标准差恒为 0 且不报错 |
|
||||||
| 坏结果不写缓存 | 截断流/解析失败被固化 |
|
| 坏结果不写缓存 | 截断流/解析失败被固化 |
|
||||||
|
|
||||||
`sampling` **仅在非空时参与**,不传采样参数时 key 与旧版逐字相同,升级不会作废存量缓存。反过来,逐次变化的 `seed` 会让这条路径全部 miss——这是正确语义,但要知道缓存对它不再省钱。另注意 key 里的 `model` 是**全 scope 所有源的合集指纹**(含各源的 `EXTRA_BODY`),不是本次实际选中那个源的指纹:同 scope 各源解码参数不同时,仍可能读到另一源的响应。详见 [[指南-采样参数]]。
|
`sampling` **仅在非空时参与**,不传采样参数时 key 与旧版逐字相同,升级不会作废存量缓存。反过来,逐次变化的 `seed` 会让这条路径全部 miss——这是正确语义,但要知道缓存对它不再省钱。另注意 key 里的 `model` 是**全 scope 所有源的合集指纹**(含各源的 `EXTRA_BODY`),不是本次实际选中那个源的指纹:同 scope 各源解码参数不同时,仍可能读到另一源的响应。详见 [[指南-采样参数]]。
|
||||||
|
|||||||
+6
-3
@@ -12,7 +12,7 @@ PGW_TELEMETRY_SQLITE_PATH=logs/telemetry.db # sqlite 时必填
|
|||||||
|
|
||||||
实验室纪律:PG DSN 只许指向专用库 `polygateway`,严禁在用业务库。
|
实验室纪律:PG DSN 只许指向专用库 `polygateway`,严禁在用业务库。
|
||||||
|
|
||||||
## 表结构(`llm_calls`,21 字段冻结)
|
## 表结构(`llm_calls`,22 列)
|
||||||
|
|
||||||
| 字段组 | 字段 |
|
| 字段组 | 字段 |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -23,7 +23,10 @@ PGW_TELEMETRY_SQLITE_PATH=logs/telemetry.db # sqlite 时必填
|
|||||||
| 时延 | latency_ms / ttft_ms / max_inter_token_ms |
|
| 时延 | latency_ms / ttft_ms / max_inter_token_ms |
|
||||||
| 结果 | cache_hit / error(异常类名前缀,如 `TransientError: ...`)/ cost |
|
| 结果 | cache_hit / error(异常类名前缀,如 `TransientError: ...`)/ cost |
|
||||||
| 可观测(v1.0.4) | cached_prompt_tokens / model_reported(见下) |
|
| 可观测(v1.0.4) | cached_prompt_tokens / model_reported(见下) |
|
||||||
| 复现(未发布版) | sampling —— 本次调用的采样参数(见下) |
|
| 复现(v1.0.5) | sampling —— 本次调用的采样参数(见下) |
|
||||||
|
| 落库时刻 | created_at(库自动填,不由调用方传;做时间窗聚合直接用它) |
|
||||||
|
|
||||||
|
表是 22 列,但 `TelemetryRecorder` 端口是 21 个参数——差的正是 `created_at`(由数据库默认值生成)。自定义遥测后端实现该端口时按 21 个关键字参数接。
|
||||||
|
|
||||||
v1.0.4 新增的两列会**自动补到已存在的旧表上**,无需手工迁移;历史行的新列为 NULL。两个后端都是先探测缺列、只在真缺列时才 ALTER——稳态下一条 ALTER 都不发(`ADD COLUMN IF NOT EXISTS` 即使列已存在也会先取排他锁,而遥测是内联写入,锁住共享审计表会拖慢业务调用);补列失败也只是这几行遥测被丢弃,不会让遥测整体停摆。
|
v1.0.4 新增的两列会**自动补到已存在的旧表上**,无需手工迁移;历史行的新列为 NULL。两个后端都是先探测缺列、只在真缺列时才 ALTER——稳态下一条 ALTER 都不发(`ADD COLUMN IF NOT EXISTS` 即使列已存在也会先取排他锁,而遥测是内联写入,锁住共享审计表会拖慢业务调用);补列失败也只是这几行遥测被丢弃,不会让遥测整体停摆。
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ WHERE cache_hit = false AND cached_prompt_tokens IS NOT NULL;
|
|||||||
|
|
||||||
`model_reported` 是 API 响应体里实际返回的 model,和 `.env` 里配的别名可能不是一个东西——供应商把别名指向新权重时,只有它认得出当时真正跑的版本。要做可复现的实验快照,记这一列。
|
`model_reported` 是 API 响应体里实际返回的 model,和 `.env` 里配的别名可能不是一个东西——供应商把别名指向新权重时,只有它认得出当时真正跑的版本。要做可复现的实验快照,记这一列。
|
||||||
|
|
||||||
## 采样参数 `sampling`(未发布版)
|
## 采样参数 `sampling`(v1.0.5)
|
||||||
|
|
||||||
「调用方传的 ⊎ 生效源的 `EXTRA_BODY`」的规范化 JSON,没传则 NULL。有了它,"这批数据跑在什么解码条件下"才在事后可查——这和 `model_reported` 是同一类需求。
|
「调用方传的 ⊎ 生效源的 `EXTRA_BODY`」的规范化 JSON,没传则 NULL。有了它,"这批数据跑在什么解码条件下"才在事后可查——这和 `model_reported` 是同一类需求。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ for seed in range(5):
|
|||||||
# 请求体最终是 {"temperature":0, "top_p":1, "seed":<seed>, ...}
|
# 请求体最终是 {"temperature":0, "top_p":1, "seed":<seed>, ...}
|
||||||
```
|
```
|
||||||
|
|
||||||
优先级 **结构化输出注入 > 调用级 `overlay` > 源级 `EXTRA_BODY`**。结构化输出排最高是因为它关系到响应能否被解析——`structured=` 时传 `overlay={"response_format": ...}` 会被覆盖。
|
优先级 **结构化输出注入 > 调用级 `overlay` > 源级 `EXTRA_BODY`**。结构化输出排最高是因为它关系到响应能否被解析:用**原生 schema 策略**(provider 支持 `response_format` 时自动选用)的话,`structured=` 时你传的 `overlay={"response_format": ...}` 会被它覆盖。缺省的 json_repair 策略不改请求体,此时你传的 `response_format` 会照发——它不在保护键里,库不拦。
|
||||||
|
|
||||||
## 三个坑
|
## 三个坑
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
```bash
|
```bash
|
||||||
LLM__QWEN__1__MAX_CONCURRENCY=8 # 单源并发
|
LLM__QWEN__1__MAX_CONCURRENCY=8 # 单源并发
|
||||||
LLM__QWEN__1__RPM=60 # 单源每分钟请求
|
LLM__QWEN__1__RPM=60 # 单源每分钟请求
|
||||||
LLM__QWEN__1__TPM=100000 # 单源每分钟 token(启用则 EST_TOKENS 必填)
|
LLM__QWEN__1__TPM=100000 # 单源每分钟 token;照配额页填即可,EST_TOKENS 可不填
|
||||||
LLM__QWEN__1__EST_TOKENS=2000 # TPM 预扣依据;调用后按实际用量结算退款
|
LLM__QWEN__1__EST_TOKENS=2000 # TPM 预扣依据;调用后按实际用量结算退款
|
||||||
LLM__GLOBAL__MAX_CONCURRENCY=16 # scope 级跨源合计
|
LLM__GLOBAL__MAX_CONCURRENCY=16 # scope 级跨源合计
|
||||||
LLM__GLOBAL__RPM=120
|
LLM__GLOBAL__RPM=120
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ asyncio.run(main())
|
|||||||
|
|
||||||
## 4. 看见治理在工作
|
## 4. 看见治理在工作
|
||||||
|
|
||||||
把 `.env` 里 API_KEY 改成错的再跑一次——你会得到 `AllSourcesExhausted` 而不是裸的 401:库先按分类判定(401=源失效)、熔断该源、发现无源可换后抛出带 `retry_after_s` 的结构化异常。改回正确 key,再把遥测打开:
|
把 `.env` 里 API_KEY 改成错的再跑一次——你会得到 `CircuitOpenError` 而不是裸的 401:库先按分类判定(401 = 源失效)、立即熔断该源,下一轮发现无源可用即抛出带 `retry_after_s` 的结构化异常。**要一网打尽请捕父类 `GatewayUnavailableError`**:重试次数耗尽走的是 `AllSourcesExhausted`,源被熔断走 `CircuitOpenError`,两者同父不同类,词表见 [[参考-异常]]。改回正确 key,再把遥测打开:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PGW_TELEMETRY_BACKEND=sqlite
|
PGW_TELEMETRY_BACKEND=sqlite
|
||||||
|
|||||||
Reference in New Issue
Block a user