土豆(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_reason 是 null。
我最开始的错误处理只捕获异常,完全没有检查响应内容,导致空字符串被作为正常结果写入数据库。
修复:加一个响应内容校验层:
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 同理
- 月均成本从 28,同等任务量
- 降级成功率:DeepSeek 超时后自动切 GPT-4o,测试中零漏单
GPT-6 发布后我会跑一轮真实任务对比测评,到时候更新。感觉 200 万 Token 上下文对我的长文档场景是真实利好,但成本怎么算还得看定价。