The design claimed merging would immediately break dissect. It would not: dissect keeps running whatever version it already has, and this release does not touch it. What is true is narrower -- once dissect moves to 1.0.6, the M2.7 scope will refuse to assemble. Worth recording because the distinction is not academic here. dissect/requirements.txt declares polygateway>=1.0.1,<1.1, a range rather than a pin, so 1.0.6 satisfies it and any routine reinstall picks it up without anyone deciding to upgrade. So it is not "breaks on merge", it is "breaks on the next dependency install". The paragraph now carries both corrections it went through, since a claim about downstream impact that was wrong twice is worth leaving visible rather than quietly rewriting.
PolyGateway
实验室统一的大语言模型调度与中转库:LLM / VLM / OCR / Embedding 四类调用共用同一套生产级治理栈——多源多账号、限流、错误分类重试、熔断、响应缓存、流式看门狗、遥测与成本。治理单位是一次模型调用;任务编排、业务解析、图像预处理都留在业务侧。
由三个真实项目(GovDoc-SaaS / CHSAnalyzer / Video-Tree-TRM5)各自手写的治理栈提炼而来,并以"能否全量迁移回这三个项目"作为验收标准。v1.0.0 已通过 GovDoc 与 CHSAnalyzer 两项目的全量迁移验收(约 −6800 行项目侧治理代码由本库继任)。
为什么需要它
每个接入大模型的项目都会重写同一批东西:重试循环、429 处理、熔断器、SSE 解析、遥测埋点——写三遍就有三份 bug。本库把这些收敛为一份经过压测验证的实现:
| 能力 | 说明 |
|---|---|
| 多源多账号 | {SCOPE}__{PROVIDER}__{N}__* 配置任意多源;健康感知选源(EWMA×在途 P2C)自动避开坏源 |
| 限流 | 并发/RPM/TPM × 全局/单源六道闸;TPM 预扣入场、按实际用量结算退款;Redis 后端跨进程原子(Lua) |
| 错误分类重试 | 一切失败落入四分类(见下),由分类决定重试/换源/熔断;429 属 pushback 不消耗重试预算;退避含 jitter 且尊重 Retry-After |
| 熔断 | 双通道(连续失败 + 失败率窗口,健康证据抑制误熔);半开单探针带租约(持有者死亡自动回收);epoch fencing 拒绝迟到写回;开路时长指数递增 |
| 自适应并发 | AIMD:429 削减、成功缓升,防止打爆上游 |
| 响应缓存 | Redis/内存;key 含 model + messages 摘要 + namespace/租户 + salt,多模态 content 先摘要再 hash(防毒化);可 per-call 绕过(科研重采样) |
| 流式看门狗 | TTFT / inter-token / 总超时三层活性;thinking token 刷活性不计结果;截断流(缺 [DONE])判瞬时不入缓存 |
| 遥测与成本 | 每次调用(含缓存命中与失败)必录 18 字段;SQLite / Postgres 后端;按价格表折算成本;多模态内容摘要落库不存原图 |
| 结构化输出 | json_repair 修复 / 原生 schema 双策略 + 校验失败有界带反馈重问 |
| OCR | MonkeyOCR 双端点(文本转录 + 版面解析),bbox 数值防御下沉,逐源健康预检 check_health() |
| Embedding | 分批、维度校验、与 chat 同一治理栈 |
降级方向是铁律:缓存/遥测后端掉线 → 静默降级(warning);限流/熔断后端掉线 → 报错而非放行(防击穿上游)。asyncio.CancelledError 全链路穿透,in-flight 资源在 finally 释放。
安装
发布在实验室 Gitea PyPI(公开包,匿名可装):
pip install --extra-index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ \
"polygateway[redis,postgres,structured]==1.0.*"
核心仅依赖 httpx + pydantic;按需选 extras:
| extra | 内容 | 何时需要 |
|---|---|---|
redis |
redis-py | Redis 限流/熔断/缓存后端 |
postgres |
asyncpg | Postgres 遥测后端 |
structured |
json-repair | 结构化输出的修复策略 |
sdk |
openai | 可选的 SDK transport(默认手写 httpx,不需要) |
要求 Python ≥ 3.11。
快速开始
1. 配置 .env
LLM__MINIMAX__1__BASE_URL=https://your-gateway/v1
LLM__MINIMAX__1__API_KEY=sk-xxx
LLM__MINIMAX__1__MODEL=MiniMax-M3
LLM__MINIMAX__1__TIMEOUT_S=120
LLM_MAX_RETRIES=3
LLM_RETRY_BASE_DELAY=2.0
LLM_RETRY_MAX_DELAY=30.0
LLM_CIRCUIT_BREAKER_THRESHOLD=5
LLM_CIRCUIT_BREAKER_COOLDOWN=60
PGW_LIMITER_BACKEND=memory
PGW_BREAKER_BACKEND=memory
PGW_CACHE_BACKEND=none
PGW_TELEMETRY_BACKEND=none
缺任何关键键都会在装配时报错——本库禁止默认值兜底掩盖配置缺失。
2. 发起治理调用
from polygateway import GatewayClient
async def main() -> None:
client = GatewayClient.from_env("LLM") # 读 .env 装配整套治理栈
try:
resp = await client.chat([{"role": "user", "content": "你好"}])
print(resp.content, resp.source_name, resp.latency_ms)
finally:
await client.aclose() # 归还连接与治理后端资源
chat() 原生接受 OpenAI 多模态 content 数组(image_url data URL),VLM 调用无需专门客户端;session_id / parent_call_id / cache_salt 关键字参数用于链路追踪与缓存控制;overlay 传采样参数(temperature / seed / max_tokens 等,恒定值宜配在源的 EXTRA_BODY 上)——它会进缓存 key,故逐次变化的 seed 天然不命中缓存。
3. OCR 与 Embedding
from polygateway import EmbeddingClient
from polygateway.ocr import OcrClient
ocr = OcrClient.from_env("OCR") # OCR__MONKEY__1__* 多源
text = await ocr.recognize_text(image_bytes) # 文本转录
layout = await ocr.parse_layout(image_bytes) # 版面解析(带 bbox 的元素列表)
health = await ocr.check_health() # 逐源预检 {"monkey_1": True, ...}
embed = EmbeddingClient.from_env("EMBED") # EMBED__*__* + EMBED__BATCH_SIZE
vectors = (await embed.embed(["文本 a", "文本 b"])).vectors
4. 业务侧异常处理
from polygateway import GatewayUnavailableError, RequestRejectedError
try:
resp = await client.chat(messages)
except GatewayUnavailableError as exc:
# 整个 scope 暂时无源可用: 延期重投,不消耗业务失败预算
schedule_retry(after_s=exc.retry_after_s) # exc.reason / exc.per_source_reasons 供诊断
except RequestRejectedError:
... # 请求本身有问题(400/格式拒绝): 不重试,直接失败
错误模型(四分类)
一切失败在 transport 层翻译为四类之一,治理行为由分类决定,业务侧不需要判断状态码:
| 分类 | 含义 | 库内行为 |
|---|---|---|
TransientError |
超时/5xx/网络抖动/截断流 | 换源重试 + 退避 |
SourceDeadError |
401/403/欠费(429+insufficient_quota) | 立即熔断该源 + 换源 |
RequestRejectedError |
400/内容拒绝/本地格式拒绝 | 不重试不换源,快速失败 |
ResultInvalidError |
调用成功但结果不合格(坏 JSON/维度不符/坏 bbox) | 不熔断("坏结果 ≠ 坏服务"),按策略有界重问或上抛 |
预算耗尽/全源熔断时抛 GatewayUnavailableError 族(CircuitOpenError / AllSourcesExhausted),携带 scope / reason / retry_after_s / per_source_reasons,供任务队列做延期重投。
配置参考
配置只有两条装配路径:from_env()(读 .env/环境变量)或构造函数全量注入(测试/高级);库内部任何组件不自读环境变量。键名全集见 .env.example,约定速览:
| 键形态 | 作用 |
|---|---|
{SCOPE}__{PROVIDER}__{N}__{FIELD} |
第 N 个源;FIELD ∈ BASE_URL/API_KEY/MODEL/TIMEOUT_S/MAX_CONCURRENCY/RPM/TPM/EST_TOKENS/TTFT_TIMEOUT_S/INTER_TOKEN_TIMEOUT_S/ENABLE_THINKING/TRUST_ENV |
{SCOPE}__GLOBAL__* |
scope 级全局限额(跨源并发/RPM/TPM) |
{SCOPE}__RETRY__* / BREAKER__* / BACKPRESSURE__* / SELECTOR |
per-scope 韧性参数;缺省回落平铺键(LLM_MAX_RETRIES 等,兼容旧项目习惯) |
PGW_LIMITER_BACKEND / PGW_BREAKER_BACKEND |
memory(单进程)或 redis(跨进程共享,需 REDIS_URL) |
PGW_CACHE_BACKEND |
none / redis(需 PGW_CACHE_NAMESPACE + PGW_CACHE_TTL_S) |
PGW_TELEMETRY_BACKEND |
none / sqlite(需 PGW_TELEMETRY_SQLITE_PATH)/ postgres(需 PGW_TELEMETRY_PG_DSN) |
SCOPE 是逻辑角色(LLM/VLM/OCR/EMBED/JUDGE/SEARCH…任意大写名),同一进程可按角色装配多个 client,各自独立配置与治理状态。
架构
端口适配器 + 中间件洋葱:决策逻辑一份,状态存储可插拔。
graph LR
A[业务代码] --> B[GatewayClient]
B --> C[缓存 MW] --> D[遥测 MW] --> E[重试/选源/限流/熔断 MW]
E --> F[Transport httpx]
F --> G[(上游网关)]
E -.端口.-> H[(内存 / Redis 后端)]
D -.端口.-> I[(SQLite / Postgres)]
| 模块 | 职责 |
|---|---|
types.py / errors.py / ports.py |
内核:冻结类型、四分类异常、全部 Protocol(最内层,不依赖任何实现) |
middleware/ |
治理算法(重试/限流/熔断/缓存/遥测),只面向端口 |
transports/ |
协议细节:OpenAI 兼容 SSE、MonkeyOCR 双端点;错误翻译在此层 |
backends/ |
限流/熔断/缓存的内存与 Redis 实现(同一契约测试套件双后端共用) |
telemetry/ |
SQLite / Postgres 遥测后端 |
structured/ |
结构化输出策略 |
依赖纪律由 import-linter 机械化执法(make lint)。完整架构决策(D1-D14 含论证过程)见 research-wiki/ARCHITECTURE.md。
可靠性证据
行为不是宣称出来的,是压测出来的(数字见 research-wiki/findings/):
| 场景 | 结果 |
|---|---|
| 故障混编 soak(坏 key/黑洞/慢源/限流源混合,8000 调用) | 成功率 98.96%,坏源吸流被压制,真实源零误熔 |
| OCR 故障池 soak(1500 调用,redis 双后端跨进程) | 成功率 99.73%,13 项不变量全过(租约归零/探针不悬挂/零取消泄漏等) |
| 两项目全量迁移回归 | 原测试全绿 + 真实链路冒烟 + 50 样本批跑 100% 解析 |
时间语义测试(租约过期、窗口滚动、半开探针)全部真实等待不缩放;Redis/Postgres 测试打真实实验室后端,不 mock Lua。
开发
conda create -n PolyGateway python=3.11 && conda activate PolyGateway
make install # editable 安装(dev + 全部 extras)
make test # pytest + 覆盖率(目标 ≥80%)
make lint # ruff + import-linter
make ci # 只读全量验证
测试组织:tests/{unit,integration,e2e} + 双后端契约测试;并发/取消/降级方向是一等测试对象。压测 harness 在 tools/soak/。贡献流程与项目纪律见 CLAUDE.md。
文档导航
| 想了解 | 看 |
|---|---|
| 全部架构决策及理由(单一事实源) | research-wiki/ARCHITECTURE.md |
| 里程碑与状态 | research-wiki/ROADMAP.md |
| 项目迁移指南(删除清单/组件映射/行为审计) | research-wiki/migrations/ |
| 每个功能的设计与验收记录 | research-wiki/designs/、research-wiki/findings/ |
| 版本变更 | CHANGELOG.md |
兼容性承诺
LLMResponse 等被下游消费的公共类型,字段只增不删不改名且新增字段必带默认值;{SCOPE}__{PROVIDER}__{N}__{FIELD} 与平铺韧性键名(LLM_TIMEOUT 等)沿用三项目既有习惯,不做破坏性改名。实验室内部库,随实验室项目需求演进。