GPT-6发布前夜:我把多模型路由改造了一遍,踩了两个坑

0 阅读4分钟

土豆(GPT-6)三天后发布,DeepSeek V4据说这个月底也出来。上周我把自己项目里的 LLM 调用层大改了一遍,目标是:新模型上线后不改业务代码,直接配置切换。 记录一下过程和踩坑。


背景

我做了个 SaaS 工具,核心功能是帮用户做长文档的结构化提取。之前全部走 GPT-4o,每月账单大概在 $180-220 之间。

DeepSeek V3 出来之后,我测了一下,大部分提取任务效果差不多,成本直接降到 $22。但问题来了——DeepSeek 不稳定,高峰期超时概率约 15%,纯靠 DeepSeek 跑生产太慌了。

所以我需要一个主力 + 降级 + 灾备的三层路由方案,还要能在 GPT-6 和 V4 出来后无缝接入。


技术方案

整体架构很简单:统一的 LLMClient 类,屏蔽底层模型差异,业务代码只感知 client.complete()

# llm_client.py
import os
import asyncio
import httpx
from typing import Optional
​
MODELS = {
    "deepseek-v4": {
        "base_url": "https://api.deepseek.com/v1",
        "api_key_env": "DEEPSEEK_API_KEY",
        "model": "deepseek-v4",
        "cost_input_per_1k": 0.0003,
        "cost_output_per_1k": 0.0008,
    },
    "gpt-6": {
        "base_url": "https://api.openai.com/v1",
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-6",
        "cost_input_per_1k": 0.018,
        "cost_output_per_1k": 0.054,
    },
    "gpt-4o": {
        "base_url": "https://api.openai.com/v1",
        "api_key_env": "OPENAI_API_KEY",
        "model": "gpt-4o",
        "cost_input_per_1k": 0.005,
        "cost_output_per_1k": 0.015,
    },
}
​
# 路由优先级:成本优先,超时自动降级
ROUTING_ORDER = ["deepseek-v4", "gpt-4o", "gpt-6"]
​
class LLMClient:
    def __init__(self, timeout: float = 30.0, max_retries: int = 2):
        self.timeout = timeout
        self.max_retries = max_retries
​
    async def complete(
        self,
        messages: list[dict],
        preferred_model: Optional[str] = None,
        **kwargs
    ) -> dict:
        order = [preferred_model] + [m for m in ROUTING_ORDER if m != preferred_model] \
                if preferred_model else ROUTING_ORDER
​
        last_err = None
        for model_key in order:
            cfg = MODELS[model_key]
            for attempt in range(self.max_retries):
                try:
                    return await self._call(cfg, messages, **kwargs)
                except (httpx.TimeoutException, httpx.HTTPStatusError) as e:
                    last_err = e
                    await asyncio.sleep(0.5 * (attempt + 1))
                    continue
            # 当前模型全部重试失败,下一个
        raise RuntimeError(f"All models failed. Last error: {last_err}")
​
    async def _call(self, cfg: dict, messages: list[dict], **kwargs) -> dict:
        api_key = os.environ[cfg["api_key_env"]]
        async with httpx.AsyncClient(timeout=self.timeout) as client:
            resp = await client.post(
                f"{cfg['base_url']}/chat/completions",
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "application/json",
                },
                json={"model": cfg["model"], "messages": messages, **kwargs},
            )
            resp.raise_for_status()
            return resp.json()

业务代码调用示例:

# 业务层完全不关心底层模型
client = LLMClient()
result = await client.complete(
    messages=[
        {"role": "system", "content": "你是一个文档结构提取助手"},
        {"role": "user", "content": f"请提取以下文档的关键字段:\n\n{doc_text}"}
    ],
    preferred_model="deepseek-v4",  # 优先低成本,超时自动降级
    temperature=0.1,
    max_tokens=2048,
)

踩坑记录

坑 1:DeepSeek 的超时不抛 TimeoutException,而是返回 200 + 空 content

这个坑让我 debug 了半天。DeepSeek 在服务压力大的时候,偶尔会返回 HTTP 200,但 choices[0].message.content 是空字符串,或者 finish_reasonnull

我最开始的错误处理只捕获异常,完全没有检查响应内容,导致空字符串被作为正常结果写入数据库。

修复:加一个响应内容校验层:

def validate_response(resp: dict) -> bool:
    """检查模型响应是否有效"""
    try:
        content = resp["choices"][0]["message"]["content"]
        finish = resp["choices"][0].get("finish_reason")
        # 空内容或异常终止原因,视为失败
        if not content or content.strip() == "":
            return False
        if finish not in ("stop", "end_turn", None):
            return False
        return True
    except (KeyError, IndexError):
        return False

然后在 _call 方法里调用这个校验,不通过直接抛异常触发降级。


坑 2:不同模型的系统提示词敏感度差异很大

这个坑更隐蔽。

我把同一套 System Prompt 同时跑在 GPT-4o 和 DeepSeek V3 上,发现 DeepSeek 对"必须严格返回 JSON"这类指令的遵循率明显低于 GPT-4o,某些复杂文档大概有 8% 的概率输出的不是合法 JSON。

原因我猜测是两家的 RLHF 对指令跟随的强化方向有差异——GPT 系更偏向格式约束,DeepSeek 更倾向于内容完整性。

解决方案:不依赖 System Prompt 做格式约束,改用 JSON Mode(DeepSeek 和 OpenAI 都支持):

# 使用 JSON Mode 替代 prompt 格式约束
result = await client.complete(
    messages=[...],
    response_format={"type": "json_object"},  # 强制 JSON 输出
)

DeepSeek API 的 JSON Mode 文档藏得比较深,在 /v1/chat/completions 接口文档的最底部,很容易被跳过。


环境准备

本地开发环境:

pip install httpx pydantic python-dotenv
​
# .env
OPENAI_API_KEY=sk-...
DEEPSEEK_API_KEY=sk-...

GPT-6 和 DeepSeek V4 出来之前,如果测试账号申请不到,可以先通过 Ztopcloud 接入,支持 OpenAI / DeepSeek / 阿里云等多家模型的统一 API,我自己也在用,账单合并管理比较方便。


小结

这次改造加上测试,花了大概两天。现在的效果:

  • DeepSeek V4 正式发布后,改一行配置的 model 字段就能接入
  • GPT-6 同理
  • 月均成本从 200降到约200 降到约 28,同等任务量
  • 降级成功率:DeepSeek 超时后自动切 GPT-4o,测试中零漏单

GPT-6 发布后我会跑一轮真实任务对比测评,到时候更新。感觉 200 万 Token 上下文对我的长文档场景是真实利好,但成本怎么算还得看定价。