From f9995ef61b3ce04b888fc38ec99ce2dd306ff142 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Thu, 23 Jul 2026 03:37:20 -0400 Subject: [PATCH] docs: add project README --- README.md | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b04ba99 --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +# 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(公开包,匿名可装): + +```bash +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` + +```bash +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. 发起治理调用 + +```python +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` 关键字参数用于链路追踪与缓存控制。 + +### 3. OCR 与 Embedding + +```python +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. 业务侧异常处理 + +```python +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](.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,各自独立配置与治理状态。 + +## 架构 + +端口适配器 + 中间件洋葱:决策逻辑一份,状态存储可插拔。 + +```mermaid +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/ARCHITECTURE.md)。 + +## 可靠性证据 + +行为不是宣称出来的,是压测出来的(数字见 `research-wiki/findings/`): + +| 场景 | 结果 | +|---|---| +| 故障混编 soak(坏 key/黑洞/慢源/限流源混合,8000 调用) | 成功率 98.96%,坏源吸流被压制,真实源零误熔 | +| OCR 故障池 soak(1500 调用,redis 双后端跨进程) | 成功率 99.73%,13 项不变量全过(租约归零/探针不悬挂/零取消泄漏等) | +| 两项目全量迁移回归 | 原测试全绿 + 真实链路冒烟 + 50 样本批跑 100% 解析 | + +时间语义测试(租约过期、窗口滚动、半开探针)全部真实等待不缩放;Redis/Postgres 测试打真实实验室后端,不 mock Lua。 + +## 开发 + +```bash +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](CLAUDE.md)。 + +## 文档导航 + +| 想了解 | 看 | +|---|---| +| 全部架构决策及理由(单一事实源) | `research-wiki/ARCHITECTURE.md` | +| 里程碑与状态 | `research-wiki/ROADMAP.md` | +| 项目迁移指南(删除清单/组件映射/行为审计) | `research-wiki/migrations/` | +| 每个功能的设计与验收记录 | `research-wiki/designs/`、`research-wiki/findings/` | +| 版本变更 | [CHANGELOG.md](CHANGELOG.md) | + +## 兼容性承诺 + +`LLMResponse` 等被下游消费的公共类型,字段**只增不删不改名**且新增字段必带默认值;`{SCOPE}__{PROVIDER}__{N}__{FIELD}` 与平铺韧性键名(`LLM_TIMEOUT` 等)沿用三项目既有习惯,不做破坏性改名。实验室内部库,随实验室项目需求演进。