本文记录我用 WorkBuddy 作为开发搭子,在 ai-arch-lab 项目里从零搭出一套多后端 LLM 网关的完整过程。包含架构设计、关键代码、以及 5 个真实踩过的坑。全文所有代码均已在本地跑通,可直接复用。
做 AI 应用架构,第一件事不是选模型,而是别把可用性绑死在单一供应商上。
我的需求很具体:
qwen2.5-coder:7b),零成本、数据不出本机,但 7B 模型能力有限这里的「失败」不只是 HTTP 错误,还包括:超时、返回内容不符合 JSON Schema、Pydantic 校验不通过。
目标链路(见下图,建议对照阅读):
用户请求
│
▼
┌──────────────┐ 失败(超时/校验不通过/报错)
│ local │ ─────────────────────┐
│ Ollama │ │
└──────────────┘ ▼
┌──────────────────┐
│ siliconflow │
└──────────────────┘
│ 失败
▼
┌──────────────────┐
│ bailian │
└──────────────────┘
│ 全失败
▼
抛出聚合异常用 uv 管依赖(Java 同学可以类比成 Maven,pyproject.toml 就是 pom.xml,uv.lock 就是 dependency.lock):
uv init ai-arch-lab
cd ai-arch-lab
uv add openai httpx pydantic pyyamlJava 类比:
uv≈ Maven/Gradle,uv.lock≈ 锁定的依赖树,uv run≈mvn exec:java。 注意:不要 activate 虚拟环境,直接用uv run xxx.py就能保证在正确的环境里跑。
目录结构:
ai-arch-lab/
├── config.yaml # 后端配置(解耦)
├── gateway.py # 核心网关
├── schemas.py # Pydantic 输出模型
└── demo.py # 调用示例不要把 API Key 和模型名硬编码进代码。用一个 config.yaml:
backends:
- name: local
base_url: "http://localhost:11434/v1"
api_key: "ollama"
model: "qwen2.5-coder:7b"
timeout: 30
- name: siliconflow
base_url: "https://api.siliconflow.cn/v1"
api_key: "${SILICONFLOW_API_KEY}"
model: "Qwen/Qwen2.5-7B-Instruct"
timeout: 60
- name: bailian
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key: "${DASHSCOPE_API_KEY}"
model: "qwen-plus"
timeout: 60${VAR} 从环境变量读,密钥不进 Git。这三家的接口都是 OpenAI 兼容格式(/v1/chat/completions),所以可以用同一个 SDK 客户端,只换 base_url 和 api_key——这是省掉大量适配代码的关键。
大模型返回的 JSON 是不可信的。三层防御:
response_format={"type": "json_schema", ...} 强制模型按 schema 输出json.loads 失败 → 捕获schemas.py:
from pydantic import BaseModel, Field
class ExtractedTask(BaseModel):
"""从自然语言中抽取出的结构化任务"""
title: str = Field(description="任务标题,不超过 20 字")
priority: int = Field(ge=1, le=5, description="优先级 1-5,5 最高")
tags: list[str] = Field(default_factory=list, description="标签列表")
deadline: str | None = Field(default=None, description="截止时间,ISO 8601")Java 类比:Pydantic 模型 ≈ 带 Bean Validation 注解的 DTO,
Field(ge=1, le=5)就是@Min(1) @Max(5),model_validate_json()相当于「Jackson 反序列化 + JSR-380 校验」一步到位。
import json
import os
import yaml
from openai import OpenAI
from pydantic import ValidationError
class AllBackendsFailed(Exception):
"""所有后端均失败时抛出,携带每个后端的失败原因"""
def __init__(self, errors: dict[str, str]):
self.errors = errors
detail = "; ".join(f"{k}: {v}" for k, v in errors.items())
super().__init__(f"全部后端失败 -> {detail}")
def _resolve_env(value: str) -> str:
"""把 ${VAR} 替换成环境变量实际值"""
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
return os.environ.get(value[2:-1], "")
return value
class Gateway:
def __init__(self, config_path: str = "config.yaml"):
with open(config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f)
self.backends = cfg["backends"]
def chat_structured(self, prompt: str, schema: type) -> object:
"""
按顺序尝试每个后端,返回第一个通过 Pydantic 校验的结果。
全部失败则抛出 AllBackendsFailed。
"""
errors: dict[str, str] = {}
for backend in self.backends:
name = backend["name"]
try:
client = OpenAI(
base_url=backend["base_url"],
api_key=_resolve_env(backend["api_key"]),
timeout=backend["timeout"],
)
resp = client.chat.completions.create(
model=backend["model"],
messages=[{"role": "user", "content": prompt}],
response_format={
"type": "json_schema",
"json_schema": {
"name": schema.__name__,
"schema": schema.model_json_schema(),
},
},
)
raw = resp.choices[0].message.content
# 双重保险:即使模型说它遵守了 schema,也要自己校验一遍
result = schema.model_validate_json(raw)
print(f"[OK] 后端 {name} 命中")
return result
except (ValidationError, json.JSONDecodeError) as e:
# 返回内容不合法 -> 换下一个后端
errors[name] = f"schema 校验失败: {e}"
continue
except Exception as e:
# 网络/超时/鉴权等 -> 换下一个后端
errors[name] = f"{type(e).__name__}: {e}"
continue
raise AllBackendsFailed(errors)from gateway import Gateway, AllBackendsFailed
from schemas import ExtractedTask
gw = Gateway("config.yaml")
prompt = "帮我记一下:下周三之前务必搞定数据库迁移方案,优先级最高,涉及 devops 和 backend。"
try:
task: ExtractedTask = gw.chat_structured(prompt, ExtractedTask)
print(task.model_dump_json(indent=2))
except AllBackendsFailed as e:
print("全部后端挂了:", e.errors)输出:
{
"title": "搞定数据库迁移方案",
"priority": 5,
"tags": ["devops", "backend"],
"deadline": "2026-10-07T00:00:00"
}一开始给三个后端都设了 60 秒超时。结果本地 7B 模型遇到复杂 prompt 会干等到 60 秒才降级,用户体感极差。
改法:本地设 30 秒甚至 20 秒,云端设 60 秒。本地模型的价值是「快」,不是「等」。
response_format 不是所有模型都支持json_schema 类型的 response_format 是较新的能力,一些旧模型或自部署模型会直接报 400。
改法:降级链本身就成了兜底——本地模型不支持就自动换到云端。但更好的做法是在 config 里给每个后端加一个 supports_json_schema: bool 标记,不支持时改用 prompt 里塞 schema 的方式。
即使指定了 json_schema,部分模型仍会返回:
```json
{"title": "..."}
```直接 json.loads 会炸。
改法:写一个容错解析器,先 strip 掉 ``` 围栏再解析:
def strip_code_fence(text: str) -> str:
text = text.strip()
if text.startswith("```"):
lines = text.splitlines()
lines = lines[1:] # 去掉 ```json 那一行
if lines and lines[-1].strip() == "```":
lines = lines[:-1] # 去掉结尾 ```
return "\n".join(lines).strip()
return text${VAR} 没替换成功,而报错信息完全看不出原因如果环境变量没设置,_resolve_env 返回空字符串,OpenAI SDK 会报一个模糊的 401。
改法:在 _resolve_env 里直接抛明确异常:
def _resolve_env(value: str) -> str:
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
var = value[2:-1]
if var not in os.environ:
raise RuntimeError(f"环境变量 {var} 未设置,请先 export/set")
return os.environ[var]
return value这条特别值得强调:配置错误要在启动时炸,不要等到调用时炸。
写完代码最容易犯的错——看起来能跑,但降级逻辑从来没被触发过。
验证方法:故意把第一个后端的 base_url 改成一个不存在的端口,观察日志是否顺次打到第二个、第三个后端:
# 临时把 local 的 base_url 改成 http://localhost:9999/v1 跑一次
# 期望看到:local 报 ConnectionError -> siliconflow 命中这套网关的核心思路其实只有三条:
原则 | 落地方式 |
|---|---|
可用性优先 | 多后端顺序降级,任一环节失败自动切换 |
输出可信 | JSON Schema + 容错解析 + Pydantic 校验,三层防御 |
配置解耦 | YAML 管后端,密钥走环境变量,代码零硬编码 |
后续可以继续做:并行竞速(多后端同时请求取最快)、按任务复杂度动态选后端、失败率统计与自动剔除连续失败的后端。
本文基于 ai-arch-lab 项目真实开发过程整理,代码已在 Python 3.11 + uv 环境下验证通过。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。