DeepSeek (OpenAI-compatible) as primary with a 智谱 GLM slot that is a real routing path but raises until the 算法备案 model list is confirmed. Adds scene routing, failover, a per-provider circuit breaker, structured output with retry-on-validation-failure, prompts loaded by id+version from prompts/, and cost attribution written to ob_ai_call for every attempt -- successes, retries and failures alike. Two controls exist because they are cheap now and expensive later: a cache-prefix validator that rejects timestamps, uuids and nicknames in the cached system prefix (a volatile prefix silently multiplies input cost), and a hard platform-wide daily budget that rejects non-critical scenes instead of merely alerting, since a retry storm outruns any human on-call.
55 lines
2.9 KiB
Markdown
Executable File
55 lines
2.9 KiB
Markdown
Executable File
# `packages/llm/` — LLM 网关
|
||
|
||
架构 §4.3、§8.1。V1 是进程内包,V2 拆成独立服务。
|
||
**因此接口必须按远程调用来设计**:无共享内存假设、明确超时、可序列化的入参出参。
|
||
|
||
## 唯一入口
|
||
|
||
所有 LLM 调用都走 `LLMGateway.complete()`。业务代码不许自己 `httpx.post` 到任何
|
||
模型供应商 —— 那样会绕过成本归因、预算闸门、故障转移和 Prompt 版本记录,而这四件事
|
||
一旦有缺口就补不回来(历史数据是补不出来的)。
|
||
|
||
## 已实现
|
||
|
||
| 能力 | 位置 |
|
||
| --- | --- |
|
||
| 场景化路由(`scene → provider/model/温度/超时/预算`,全配置化) | `routing.py` |
|
||
| 故障转移 + 熔断(超时/5xx/限流切备用;连续失败熔断,冷却后半开探测) | `gateway.py` `circuit.py` |
|
||
| 结构化输出(Pydantic Schema 约束 + 校验失败重试) | `gateway.py` |
|
||
| Prompt 版本(文件化,按 id+version 加载,版本随每次调用落库) | `prompts.py` `/prompts` |
|
||
| **缓存前缀纪律**(固定前缀含时间戳/随机 ID/昵称 → 拒绝或告警) | `prompts.py` |
|
||
| 成本归因(每次 provider 调用写一行 `ob_ai_call`,含缓存命中 token) | `cost.py` `recording.py` |
|
||
| **全局日成本硬熔断**(超预算即拒绝非关键场景,不是只告警) | `budget.py` |
|
||
|
||
## 未实现(按 §8.1 还差)
|
||
|
||
- **流式输出 SSE**(FR-AGT-01 要求首字 p95 < 2s,非流式做不到)
|
||
- **审核联动**:输入输出审核应在网关内统一完成,业务方无需关心(CR-06)
|
||
- **用户级频控**:目前只有平台级日预算与业务侧配额
|
||
- **A/B 分流**:按用户哈希选择 prompt 版本
|
||
- **智谱 GLM provider**:仅结构占位,见 `providers/zhipu.py`
|
||
|
||
## 加一个新场景
|
||
|
||
1. 在 `types.Scene` 加枚举值。
|
||
2. 在 `routing._DEFAULTS` 加默认配置,想清楚 `critical` 该不该为真
|
||
—— `critical=True` 意味着日预算耗尽时它仍然会花钱。
|
||
3. 在 `prompts/<scene>/v1.txt` 写模板。**`[system]` 段不许有任何变量**。
|
||
4. 跑 `pytest tests/unit/test_prompt_cache_prefix.py`,它会扫描仓库里全部
|
||
prompt 的固定前缀。
|
||
|
||
## 加一个新 provider
|
||
|
||
1. 实现 `providers/base.LLMProvider` 协议(`name` / `default_model` /
|
||
`complete` / `aclose`)。
|
||
2. HTTP 错误必须映射到 `errors.py` 的异常族,`retryable` 决定要不要切备用。
|
||
映射错了会导致「明明该切备用却没切」或者「明明是我们请求写错了却把备用也打一遍」。
|
||
3. usage 字段必须映射出 **缓存命中 token 数**,否则 NFR-COST-03 的缓存命中率
|
||
看板是空的。
|
||
4. 在 `apps/api/deps.build_llm_gateway()` 注册。
|
||
|
||
## 费率
|
||
|
||
`RateTable` 来自配置(`LLM_COST_RATES_JSON`),不是常量。未知模型走 default 费率
|
||
而不是 0 —— 成本上的盲区比不精确更危险。**上线前必须用供应商实际价目表校准。**
|