From edc73607e93aa4e825c2e6c1f8f2f8660a125f88 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Fri, 31 Jul 2026 22:37:34 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=87=E6=A0=B7=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E9=80=8F=E4=BC=A0(issue=20#4)+=20=E8=A1=A5=20v1.0.1/v1.0.2=20?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E7=9A=84=E8=A3=85=E9=85=8D=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issue #4: 新增指南-采样参数,并同步公共API/配置键/响应缓存/遥测与成本/侧边栏。 v1.0.1 与 v1.0.2 当初只 bump 了 Home 的版本号,两版的实质内容(装配守卫 在所有构造路径生效、scope 规范化为小写会切 Redis key 命名空间)一行未进 wiki,本次一并补进参考-配置键。 --- _Sidebar.md | 1 + 参考-公共API.md | 4 ++-- 参考-配置键.md | 20 ++++++++++++++++ 指南-响应缓存.md | 5 +++- 指南-遥测与成本.md | 18 +++++++++++++- 指南-采样参数.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 指南-采样参数.md diff --git a/_Sidebar.md b/_Sidebar.md index 26f87c9..6f2a74e 100644 --- a/_Sidebar.md +++ b/_Sidebar.md @@ -9,6 +9,7 @@ - [[指南-响应缓存]] - [[指南-遥测与成本]] - [[指南-结构化输出]] +- [[指南-采样参数]] - [[指南-OCR]] - [[指南-Embedding]] - [[指南-迁移既有项目]] diff --git a/参考-公共API.md b/参考-公共API.md index 992bfd5..105a2af 100644 --- a/参考-公共API.md +++ b/参考-公共API.md @@ -8,7 +8,7 @@ |---|---|---| | `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) -> LLMResponse` | 一次治理调用;messages 为 OpenAI 形态(原生支持多模态 content 数组);`structured` 传 pydantic 模型类或 `"json"` | +| `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` 传采样参数(未发布版新增,见 [[指南-采样参数]]) | | `aclose` | `() -> None` | 幂等释放连接与后端资源 | ## LLMResponse(frozen dataclass) @@ -55,7 +55,7 @@ | 导出 | 用途 | |---|---| | `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 新增) | +| `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=` 传入 | | `PricingTable` / `ModelPrice` | 价格表(成本折算) | diff --git a/参考-配置键.md b/参考-配置键.md index 274f31c..d60c178 100644 --- a/参考-配置键.md +++ b/参考-配置键.md @@ -2,6 +2,25 @@ 装配只有两条路:`from_env()`(读 .env + 环境变量,环境变量优先)或构造函数全量注入。缺关键键装配即报错。主仓库 `.env.example` 是带注释的全量模板,本页为速查表。 +## 两条装配路的校验完全一致(v1.0.1 / v1.0.2) + +**经 `from_env()` 装配的调用方不受这两版影响**——那条路本就跑全部校验。手工构造 `GatewaySettings`(或对它 `dataclasses.replace`)的调用方请读本节:这些校验此前只有 `from_env` 拦得住,`from_settings()` 与直接构造一律放行,故障留到运行时才表现出来。现已全部收进 `__post_init__`。 + +| 现在构造期就报错的 | 此前的运行时表现 | +|---|---| +| 源 `timeout_s` > `PGW_LEASE_TTL_S` | 租约先于请求过期,并发悄悄超出配额 | +| `stall_window_s` < 最大源 TTFT | 正常的慢首包被误判卡死掐断 | +| `probe_ttl_s` < 最慢源 `timeout_s` + 5 | 半开探针在途即被接管 | +| `sources` 为空 | 装出必然选源失败的 client | +| 六个后端/策略字段取值越界(`limiter_backend`/`breaker_backend`/`cache_backend`/`telemetry_backend`/`selector`/`quota_full`) | 静默不建后端,或裸 `AssertionError`(`python -O` 下退化为第三方库的天书报错) | +| 取 `redis` 的后端缺 `redis_url`;启用缓存缺 namespace 或 TTL ≤ 0;`sqlite`/`postgres` 缺对应路径/DSN | 同上 | +| `structured_max_retries` 为负、`scope` 为空 | 同上 | +| `EmbeddingSettings` 的 `batch_size` / `expected_dim` 越界 | 推迟到 `EmbeddingClient` 构造时才报 | + +**一处静默改值(v1.0.2)**:手工构造传 `scope="LLM"` 这类非全小写值时,scope 会被**规范化为小写**。这直接改变 Redis key——`pgw:limit:LLM:…` 切到 `pgw:limit:llm:…`。旧行为下这批 key 与 `from_env` 装配的进程根本不在同一命名空间,同一逻辑 scope 的限流与熔断状态分裂成两套、各记各的配额,分布式治理静默失效;修复即为此。但**切换发生的那一刻,旧键上的在途租约会被遗弃**,靠 TTL 自愈,滚动升级期间限流配额会短暂偏松。 + +同批规范化:`redis_url` / `pricing_path` 的空串归 `None`(留着空串会骗过 `is None` 判断,把错误推迟成连接串解析异常或 `Is a directory: '.'`);Postgres DSN 剥掉 SQLAlchemy 驱动后缀(`postgresql+asyncpg://` 的 `+asyncpg` asyncpg 不认)。剥离时会发一条 warning——库动了调用方给的值不该静默;日志只出现 scheme 段,DSN 带密码故整串不入日志。 + ## 源键 `{SCOPE}__{PROVIDER}__{N}__{FIELD}` | FIELD | 必填 | 说明 | @@ -14,6 +33,7 @@ | ENABLE_THINKING | | 三态: 缺省不注入 / true 注入开 / false 注入关 | | MISSING_DONE | | SSE 缺 `[DONE]`: `retry`(默认)/ `salvage`(打捞已收内容) | | TRUST_ENV | | `false` = 绕过本地代理(LAN 直连) | +| EXTRA_BODY | | 本源恒定的采样参数,JSON **对象**串(数组/标量报错),如 `{"temperature":0}`。并入请求体,优先级低于 `chat(overlay=...)`。禁用键 `model`/`messages`/`stream`/`stream_options`(会击穿治理),配了直接报错。**OCR/EMBED scope 不消费此键**——配了会被剥离并 warning。见 [[指南-采样参数]] | ## scope 级键 diff --git a/指南-响应缓存.md b/指南-响应缓存.md index 4135d54..5541f91 100644 --- a/指南-响应缓存.md +++ b/指南-响应缓存.md @@ -13,14 +13,17 @@ REDIS_URL=redis://:pass@host:6379/3 ## key 公式与防毒化 -key = `model + messages 摘要 + namespace + salt`;多模态 content(base64 图)**先摘要再 hash**,原图不进 key 也不进存储。设计约束: +key = `model + messages 摘要 + namespace + salt + sampling`;多模态 content(base64 图)**先摘要再 hash**,原图不进 key 也不进存储。设计约束: | 要素 | 防什么 | |---|---| | namespace 必填 | 跨项目/跨租户互相读到对方缓存 | | salt(per-call) | 需要强制重采样的场景命中旧缓存 | +| sampling(未发布版) | 不同解码参数的响应互相污染——尤其是同 messages 跑多个 seed 时全部命中第一次的结果,标准差恒为 0 且不报错 | | 坏结果不写缓存 | 截断流/解析失败被固化 | +`sampling` **仅在非空时参与**,不传采样参数时 key 与旧版逐字相同,升级不会作废存量缓存。反过来,逐次变化的 `seed` 会让这条路径全部 miss——这是正确语义,但要知道缓存对它不再省钱。另注意 key 里的 `model` 是**全 scope 所有源的合集指纹**(含各源的 `EXTRA_BODY`),不是本次实际选中那个源的指纹:同 scope 各源解码参数不同时,仍可能读到另一源的响应。详见 [[指南-采样参数]]。 + ## per-call 控制 ```python diff --git a/指南-遥测与成本.md b/指南-遥测与成本.md index 276a5e0..32ec86c 100644 --- a/指南-遥测与成本.md +++ b/指南-遥测与成本.md @@ -12,7 +12,7 @@ PGW_TELEMETRY_SQLITE_PATH=logs/telemetry.db # sqlite 时必填 实验室纪律:PG DSN 只许指向专用库 `polygateway`,严禁在用业务库。 -## 表结构(`llm_calls`,20 字段冻结) +## 表结构(`llm_calls`,21 字段冻结) | 字段组 | 字段 | |---|---| @@ -23,6 +23,7 @@ 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.4 新增的两列会**自动补到已存在的旧表上**,无需手工迁移;历史行的新列为 NULL。两个后端都是先探测缺列、只在真缺列时才 ALTER——稳态下一条 ALTER 都不发(`ADD COLUMN IF NOT EXISTS` 即使列已存在也会先取排他锁,而遥测是内联写入,锁住共享审计表会拖慢业务调用);补列失败也只是这几行遥测被丢弃,不会让遥测整体停摆。 @@ -90,3 +91,18 @@ WHERE cache_hit = false AND cached_prompt_tokens IS NOT NULL; 原因和上面 cost 缺口的口径一样:缓存命中行里这两个字段是**原样回放**的历史值(与 `model`、`prompt_tokens` 同一规则——命中时只有时延类字段被清零),计进去就是重复计数。 `model_reported` 是 API 响应体里实际返回的 model,和 `.env` 里配的别名可能不是一个东西——供应商把别名指向新权重时,只有它认得出当时真正跑的版本。要做可复现的实验快照,记这一列。 + +## 采样参数 `sampling`(未发布版) + +「调用方传的 ⊎ 生效源的 `EXTRA_BODY`」的规范化 JSON,没传则 NULL。有了它,"这批数据跑在什么解码条件下"才在事后可查——这和 `model_reported` 是同一类需求。 + +```sql +SELECT DISTINCT sampling FROM llm_calls +WHERE session_id = 'exp-42' AND cache_hit = false AND error IS NULL; +``` + +两点口径:① 这一列**不含**结构化输出注入的 `response_format`(列名是采样参数,schema 不是,且数 KB 的 schema 逐行落库只会让审计表膨胀);② 缓存命中行与最终失败行只记调用级参数,不含源的 `EXTRA_BODY`——那两条路径没有"生效源"可言,与 `model`/`source_name` 在失败行置空是同一回事;命中行也不损失信息,因为采样参数已进缓存 key,能命中就意味着调用级参数与历史那次逐字相同。 + +OCR / Embedding 路径的这一列**恒为 NULL**:那两条路径不发采样参数,配了 `EXTRA_BODY` 也会在装配时被剥离。详见 [[指南-采样参数]]。 + +补列规则同 v1.0.4 那两列:自动补到旧表、先探测再 ALTER、失败只丢这几行。 diff --git a/指南-采样参数.md b/指南-采样参数.md new file mode 100644 index 0000000..6fec029 --- /dev/null +++ b/指南-采样参数.md @@ -0,0 +1,58 @@ +# 指南:固定解码参数(temperature / seed / max_tokens) + +受控实验要求解码行为可复现:温度必须钉死,每个 rollout 的 seed 必须显式受控并记进快照。本页讲怎么配、以及三个不配就会静默出错的地方。 + +## 两个层次,按参数是否随调用变化来选 + +| 层次 | 怎么配 | 适合 | +|---|---|---| +| 配置级 | `.env` 里 `{SCOPE}__{PROVIDER}__{N}__EXTRA_BODY={"temperature":0}` | 全局恒定的参数。**推荐**:不必让每个调用点都记得传,而漏传一次不会报错,只会让数字悄悄不可比 | +| 调用级 | `await client.chat(msgs, overlay={"seed": 42})` | 逐次变化的参数 | + +```python +# .env: LLM__QWEN__1__EXTRA_BODY={"temperature":0,"top_p":1} +for seed in range(5): + resp = await client.chat(messages, overlay={"seed": seed}) + # 请求体最终是 {"temperature":0, "top_p":1, "seed":, ...} +``` + +优先级 **结构化输出注入 > 调用级 `overlay` > 源级 `EXTRA_BODY`**。结构化输出排最高是因为它关系到响应能否被解析——`structured=` 时传 `overlay={"response_format": ...}` 会被覆盖。 + +## 三个坑 + +**① 逐次变化的 `seed` 会让缓存全部 miss。** 采样参数进缓存 key,这是有意的:不进的话,同样的 messages 跑 5 个 seed 会全部命中第一次的响应,**报出的标准差恒为 0 且不报错**,整批实验静默作废。代价是这条路径不再省钱。不传采样参数时 key 与旧版逐字相同,存量缓存不受影响。 + +**② 缓存身份是 scope 级的,不是源级的。** 源的 `EXTRA_BODY` 会并进缓存指纹,但那是**全 scope 所有源的合集指纹**。同一个 scope 下若各源的 `EXTRA_BODY` 不同,缓存仍可能把 A 源(temperature=0)的响应返回给本该走 B 源(temperature=1)的调用。要求逐源可复现的实验,请让每个源独享 scope,或用 `chat(cache_namespace=...)` 区分。 + +**③ OCR / Embedding scope 配了 `EXTRA_BODY` 不生效。** 这两条路径的请求根本不带它(embedding 请求体只有 `model` 和 `input`,OCR 走 multipart 表单)。配了会被剥离并发一条 warning,装配照常成功。剥离不只是打扫:不剥的话遥测的 `sampling` 列会记下一个从未发出去的参数,那比"参数没生效"更糟——审计表会说这次调用跑在 temperature=0 下,而事实并非如此。需要 `dimensions` 之类的 embedding 参数,请提 issue。 + +## 哪些键不能传 + +`model` / `messages` / `stream` / `stream_options` 由治理层拥有,传了直接 `ValueError`: + +| 键 | 覆盖后果 | +|---|---| +| `model` | 遥测记录的 model 与实际请求分叉,成本按错单价换算 | +| `messages` | 缓存 key 与遥测口径同时失真 | +| `stream` | 绕过流式活性看门狗,TTFT / inter-token 超时全部失效 | +| `stream_options` | 丢 usage 帧,成本遥测归零、TPM 闸结算失准 | + +值也必须能 JSON 序列化——`numpy` 标量请先 `float()`。这条同样在调用入口就报错,不会等到运行中途:否则它会在缓存层的降级保护之外抛出裸 `TypeError`,连一行遥测都留不下。 + +## 事后复现:参数进了遥测 + +每行 `llm_calls` 有 `sampling` 列,内容是「调用方传的 ⊎ 生效源的 `EXTRA_BODY`」的规范化 JSON,没传则为 NULL。它**不含**结构化输出的 `response_format`(列名是采样参数,schema 不是,且逐行落库会让审计表膨胀)。 + +```sql +-- 某批实验实际跑在什么解码条件下 +SELECT DISTINCT sampling FROM llm_calls +WHERE session_id = 'exp-42' AND cache_hit = false AND error IS NULL; +``` + +缓存命中行与最终失败行的这一列只记调用级参数,不含源的 `EXTRA_BODY`——那两条路径没有"生效源"可言(与 `model`/`source_name` 在失败行置空是同一回事)。命中行不损失信息:采样参数已进缓存 key,能命中就意味着调用级参数与历史那次逐字相同。 + +## 顺带一提:`ENABLE_THINKING` 对某些 provider 无效 + +`openai` 与 `minimax` 两个 provider 的 thinking 注入片段是空的——它们没有已知的推理开关参数,所以 `ENABLE_THINKING=false` 对这两类源**不产生任何效果**,而不是静默生效。真需要下发关闭推理的参数,用 `EXTRA_BODY`。 + +全部 env 键见 [[参考-配置键]];`chat()` 签名见 [[参考-公共API]];遥测列口径见 [[指南-遥测与成本]]。