diff --git a/Home.md b/Home.md index 9cff5b0..d153e7f 100644 --- a/Home.md +++ b/Home.md @@ -2,7 +2,7 @@ 实验室统一的大语言模型调度与中转库:LLM / VLM / OCR / Embedding 共用同一套生产级治理栈(多源多账号、限流、错误分类重试、熔断、响应缓存、流式看门狗、遥测与成本)。治理单位是**一次模型调用**;任务编排与业务解析留在业务侧。 -当前版本 **v1.0.4**,已通过 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目的全量迁移验收。 +当前版本 **v1.0.5**,已通过 GovDoc-SaaS 与 CHSAnalyzer 两个真实项目的全量迁移验收。 ## 文档地图(按你此刻要干什么选入口) diff --git a/参考-公共API.md b/参考-公共API.md index 105a2af..7c7d21a 100644 --- a/参考-公共API.md +++ b/参考-公共API.md @@ -8,8 +8,9 @@ |---|---|---| | `from_env` | `(scope="LLM", *, limiter=None, breaker=None, cache=None, telemetry=None, registry=None, env=None) -> GatewayClient` | 从 .env/环境变量装配;关键字参数可注入自定义后端(测试/共享状态) | | `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` | 幂等释放连接与后端资源 | +| `gather_bounded`(模块级函数) | `(coros, limit) -> list` | 有界并发跑一批协程,顶层导出;用它替代裸 `asyncio.gather`,避免一次性把几百个请求压进治理栈 | ## LLMResponse(frozen dataclass) @@ -38,13 +39,13 @@ `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` | -| `recognize_text` | `(image: bytes) -> OcrTextResult` | -| `parse_layout` | `(image: bytes) -> OcrLayoutResult` | +| `recognize_text` | `(image: bytes, *, session_id=None, parent_call_id=None) -> OcrTextResult` | +| `parse_layout` | `(image: bytes, *, session_id=None, parent_call_id=None) -> OcrLayoutResult` | | `check_health` | `() -> dict[str, bool]`(逐源并发预检) | | `aclose` | `() -> None` | @@ -55,8 +56,8 @@ | 导出 | 用途 | |---|---| | `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` | -| `ProviderProfile` / `DEFAULT_PROFILES` | provider 方言注册表(thinking 注入方式、思考流字段等);自定义 provider 经 `registry=` 传入 | +| `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` / `register_provider` | provider 方言注册表(thinking 注入方式、思考流字段等);自定义 provider 经 `register_provider()` 注册或 `registry=` 传入 | | `PricingTable` / `ModelPrice` | 价格表(成本折算) | 异常层级见 [[参考-异常]];全部 env 键见 [[参考-配置键]]。 diff --git a/参考-异常.md b/参考-异常.md index ca8f50c..08dd00a 100644 --- a/参考-异常.md +++ b/参考-异常.md @@ -18,9 +18,9 @@ | 属性 | 含义 | |---|---| | `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=可立即);任务队列按它延期重投 | -| `per_source_reasons` | 逐源失败原因字典,诊断用 | +| `per_source_reasons` | 逐源失败原因字典,诊断用。**值域与 `reason` 是两张表**: `network_error` / `timeout` / `rate_limited` / `source_dead` / `circuit_open` / `cooldown` / `adaptive_paced` | ## 基建故障 diff --git a/参考-配置键.md b/参考-配置键.md index d60c178..7c8a131 100644 --- a/参考-配置键.md +++ b/参考-配置键.md @@ -41,8 +41,8 @@ |---|---| | `{SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM` | 跨源合计闸 | | `{SCOPE}__SELECTOR` | health_aware(默认)/ round_robin / least_inflight | -| `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S` | 重试(MAX_ATTEMPTS 含首次) | -| `{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S` | 熔断基本参数 | +| `{SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S` | **必填**(或用下方平铺简写);MAX_ATTEMPTS 含首次 | +| `{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}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S` | 配额满等待的判死窗口(须 ≥ 最大源 TTFT) | | `{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_NAMESPACE` / `PGW_CACHE_TTL_S` | | 缓存启用时必填;TTL 必须 > 0 | | `PGW_TELEMETRY_BACKEND` | none / sqlite / postgres | 必填;sqlite 需 `PGW_TELEMETRY_SQLITE_PATH`,postgres 需 `PGW_TELEMETRY_PG_DSN` | diff --git a/指南-响应缓存.md b/指南-响应缓存.md index 5541f91..6665e7c 100644 --- a/指南-响应缓存.md +++ b/指南-响应缓存.md @@ -19,7 +19,7 @@ key = `model + messages 摘要 + namespace + salt + sampling`;多模态 content( |---|---| | namespace 必填 | 跨项目/跨租户互相读到对方缓存 | | salt(per-call) | 需要强制重采样的场景命中旧缓存 | -| sampling(未发布版) | 不同解码参数的响应互相污染——尤其是同 messages 跑多个 seed 时全部命中第一次的结果,标准差恒为 0 且不报错 | +| sampling(v1.0.5) | 不同解码参数的响应互相污染——尤其是同 messages 跑多个 seed 时全部命中第一次的结果,标准差恒为 0 且不报错 | | 坏结果不写缓存 | 截断流/解析失败被固化 | `sampling` **仅在非空时参与**,不传采样参数时 key 与旧版逐字相同,升级不会作废存量缓存。反过来,逐次变化的 `seed` 会让这条路径全部 miss——这是正确语义,但要知道缓存对它不再省钱。另注意 key 里的 `model` 是**全 scope 所有源的合集指纹**(含各源的 `EXTRA_BODY`),不是本次实际选中那个源的指纹:同 scope 各源解码参数不同时,仍可能读到另一源的响应。详见 [[指南-采样参数]]。 diff --git a/指南-遥测与成本.md b/指南-遥测与成本.md index 32ec86c..a2ac792 100644 --- a/指南-遥测与成本.md +++ b/指南-遥测与成本.md @@ -12,7 +12,7 @@ PGW_TELEMETRY_SQLITE_PATH=logs/telemetry.db # sqlite 时必填 实验室纪律: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 | | 结果 | cache_hit / error(异常类名前缀,如 `TransientError: ...`)/ cost | | 可观测(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` 即使列已存在也会先取排他锁,而遥测是内联写入,锁住共享审计表会拖慢业务调用);补列失败也只是这几行遥测被丢弃,不会让遥测整体停摆。 @@ -92,7 +95,7 @@ WHERE cache_hit = false AND cached_prompt_tokens IS NOT NULL; `model_reported` 是 API 响应体里实际返回的 model,和 `.env` 里配的别名可能不是一个东西——供应商把别名指向新权重时,只有它认得出当时真正跑的版本。要做可复现的实验快照,记这一列。 -## 采样参数 `sampling`(未发布版) +## 采样参数 `sampling`(v1.0.5) 「调用方传的 ⊎ 生效源的 `EXTRA_BODY`」的规范化 JSON,没传则 NULL。有了它,"这批数据跑在什么解码条件下"才在事后可查——这和 `model_reported` 是同一类需求。 diff --git a/指南-采样参数.md b/指南-采样参数.md index 6fec029..3f36563 100644 --- a/指南-采样参数.md +++ b/指南-采样参数.md @@ -16,7 +16,7 @@ for seed in range(5): # 请求体最终是 {"temperature":0, "top_p":1, "seed":, ...} ``` -优先级 **结构化输出注入 > 调用级 `overlay` > 源级 `EXTRA_BODY`**。结构化输出排最高是因为它关系到响应能否被解析——`structured=` 时传 `overlay={"response_format": ...}` 会被覆盖。 +优先级 **结构化输出注入 > 调用级 `overlay` > 源级 `EXTRA_BODY`**。结构化输出排最高是因为它关系到响应能否被解析:用**原生 schema 策略**(provider 支持 `response_format` 时自动选用)的话,`structured=` 时你传的 `overlay={"response_format": ...}` 会被它覆盖。缺省的 json_repair 策略不改请求体,此时你传的 `response_format` 会照发——它不在保护键里,库不拦。 ## 三个坑 diff --git a/指南-限流与熔断.md b/指南-限流与熔断.md index 81f7fa7..5f67368 100644 --- a/指南-限流与熔断.md +++ b/指南-限流与熔断.md @@ -7,7 +7,7 @@ ```bash LLM__QWEN__1__MAX_CONCURRENCY=8 # 单源并发 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__GLOBAL__MAX_CONCURRENCY=16 # scope 级跨源合计 LLM__GLOBAL__RPM=120 diff --git a/教程-十分钟接入.md b/教程-十分钟接入.md index 1a9d8dc..db52828 100644 --- a/教程-十分钟接入.md +++ b/教程-十分钟接入.md @@ -56,7 +56,7 @@ asyncio.run(main()) ## 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 PGW_TELEMETRY_BACKEND=sqlite