首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 WorkBuddy 搭多后端 LLM 网关:5 个坑、一套降级链,从本地 Ollama 到云端全打通

用 WorkBuddy 搭多后端 LLM 网关:5 个坑、一套降级链,从本地 Ollama 到云端全打通

原创
作者头像
用户11212855
修改于 2026-09-29 13:31:34
修改于 2026-09-29 13:31:34
330
举报

本文记录我用 WorkBuddy 作为开发搭子,在 ai-arch-lab 项目里从零搭出一套多后端 LLM 网关的完整过程。包含架构设计、关键代码、以及 5 个真实踩过的坑。全文所有代码均已在本地跑通,可直接复用。

一、为什么需要「多后端 + 降级链」

做 AI 应用架构,第一件事不是选模型,而是别把可用性绑死在单一供应商上。

我的需求很具体:

  • 本地有 Ollama(qwen2.5-coder:7b),零成本、数据不出本机,但 7B 模型能力有限
  • 云端有 SiliconFlow 和百炼(Bailian),能力强但要花钱、且可能超时或限流
  • 需要一套逻辑:先试本地 → 失败降级到云端 → 再失败换另一家云

这里的「失败」不只是 HTTP 错误,还包括:超时、返回内容不符合 JSON Schema、Pydantic 校验不通过。

目标链路(见下图,建议对照阅读):

代码语言:javascript
复制
用户请求
   │
   ▼
┌──────────────┐   失败(超时/校验不通过/报错)
│  local       │ ─────────────────────┐
│  Ollama      │                      │
└──────────────┘                      ▼
                            ┌──────────────────┐
                            │  siliconflow     │
                            └──────────────────┘
                                      │ 失败
                                      ▼
                            ┌──────────────────┐
                            │  bailian         │
                            └──────────────────┘
                                      │ 全失败
                                      ▼
                              抛出聚合异常

二、技术栈与项目结构

用 uv 管依赖(Java 同学可以类比成 Maven,pyproject.toml 就是 pom.xml,uv.lock 就是 dependency.lock):

代码语言:javascript
复制
uv init ai-arch-lab
cd ai-arch-lab
uv add openai httpx pydantic pyyaml

Java 类比:uv ≈ Maven/Gradle,uv.lock ≈ 锁定的依赖树,uv run ≈ mvn exec:java。 注意:不要 activate 虚拟环境,直接用 uv run xxx.py 就能保证在正确的环境里跑。

目录结构:

代码语言:javascript
复制
ai-arch-lab/
├── config.yaml          # 后端配置(解耦)
├── gateway.py           # 核心网关
├── schemas.py           # Pydantic 输出模型
└── demo.py              # 调用示例

三、配置层:用 YAML 解耦后端

不要把 API Key 和模型名硬编码进代码。用一个 config.yaml:

代码语言:javascript
复制
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 Schema + Pydantic 双保险

大模型返回的 JSON 是不可信的。三层防御:

  1. 请求层:用 response_format={"type": "json_schema", ...} 强制模型按 schema 输出
  2. 解析层:json.loads 失败 → 捕获
  3. 校验层:Pydantic 模型校验,失败即判定该次调用失败,触发降级

schemas.py:

代码语言:javascript
复制
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 校验」一步到位。

五、核心网关实现

代码语言:javascript
复制
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)

六、调用示例(demo.py)

代码语言:javascript
复制
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)

输出:

代码语言:javascript
复制
{
  "title": "搞定数据库迁移方案",
  "priority": 5,
  "tags": ["devops", "backend"],
  "deadline": "2026-10-07T00:00:00"
}

七、5 个真实踩过的坑

坑 1:本地 Ollama 的 timeout 必须比云端短

一开始给三个后端都设了 60 秒超时。结果本地 7B 模型遇到复杂 prompt 会干等到 60 秒才降级,用户体感极差。

改法:本地设 30 秒甚至 20 秒,云端设 60 秒。本地模型的价值是「快」,不是「等」。

坑 2:response_format 不是所有模型都支持

json_schema 类型的 response_format 是较新的能力,一些旧模型或自部署模型会直接报 400。

改法:降级链本身就成了兜底——本地模型不支持就自动换到云端。但更好的做法是在 config 里给每个后端加一个 supports_json_schema: bool 标记,不支持时改用 prompt 里塞 schema 的方式。

坑 3:模型返回的 JSON 被 markdown 代码块包住了

即使指定了 json_schema,部分模型仍会返回:

代码语言:javascript
复制
```json
{"title": "..."}
```

直接 json.loads 会炸。

改法:写一个容错解析器,先 strip 掉 ``` 围栏再解析:

代码语言:javascript
复制
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

坑 4:${VAR} 没替换成功,而报错信息完全看不出原因

如果环境变量没设置,_resolve_env 返回空字符串,OpenAI SDK 会报一个模糊的 401。

改法:在 _resolve_env 里直接抛明确异常:

代码语言:javascript
复制
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

这条特别值得强调:配置错误要在启动时炸,不要等到调用时炸。

坑 5:如何验证降级链路真的生效

写完代码最容易犯的错——看起来能跑,但降级逻辑从来没被触发过。

验证方法:故意把第一个后端的 base_url 改成一个不存在的端口,观察日志是否顺次打到第二个、第三个后端:

代码语言:javascript
复制
# 临时把 local 的 base_url 改成 http://localhost:9999/v1 跑一次
# 期望看到:local 报 ConnectionError -> siliconflow 命中

八、小结

这套网关的核心思路其实只有三条:

原则

落地方式

可用性优先

多后端顺序降级,任一环节失败自动切换

输出可信

JSON Schema + 容错解析 + Pydantic 校验,三层防御

配置解耦

YAML 管后端,密钥走环境变量,代码零硬编码

后续可以继续做:并行竞速(多后端同时请求取最快)、按任务复杂度动态选后端、失败率统计与自动剔除连续失败的后端。


本文基于 ai-arch-lab 项目真实开发过程整理,代码已在 Python 3.11 + uv 环境下验证通过。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、为什么需要「多后端 + 降级链」
  • 二、技术栈与项目结构
  • 三、配置层:用 YAML 解耦后端
  • 四、结构化输出:JSON Schema + Pydantic 双保险
  • 五、核心网关实现
  • 六、调用示例(demo.py)
  • 七、5 个真实踩过的坑
    • 坑 1:本地 Ollama 的 timeout 必须比云端短
    • 坑 2:response_format 不是所有模型都支持
    • 坑 3:模型返回的 JSON 被 markdown 代码块包住了
    • 坑 4:${VAR} 没替换成功,而报错信息完全看不出原因
    • 坑 5:如何验证降级链路真的生效
  • 八、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档