前面的系列已经介绍了模型调用、提示词、上下文、成本、错误处理、评估和工作流。实际应用中,所有请求都交给同一个模型并不一定合理:简单改写可能不需要高成本模型,复杂分析则可能需要更强的模型。本篇做一个综合小项目:先用确定性的规则判断任务类型,再根据配置选择模型,最后记录路由理由和调用结果。重点不是“自动猜得多聪明”,而是让选择过程可解释、可测试、可替换。

先定义路由边界

路由器接收一份任务,任务包含 kind 和 text 两个字段。kind 只允许 simple、analysis 和 code 三种值,分别代表简单文本处理、分析任务和代码问题。程序根据类型读取环境变量中的模型名:

  • MODEL_SIMPLE:处理短文本改写等低复杂度任务。
  • MODEL_ANALYSIS:处理需要较长上下文的分析任务。
  • MODEL_CODE:处理代码解释或调试问题。

这不是让模型自己决定调用哪个模型,而是把路由规则放在 Python 中。规则越明确,越容易写测试、估算成本,也越容易在出现异常时解释“为什么用了这个模型”。生产系统还可以把规则改成配置文件,加入长度、租户、预算或人工指定的覆盖项。

准备环境变量

在独立虚拟环境中安装官方 Python SDK:

1
2
3
python -m venv .venv
source .venv/bin/activate
python -m pip install openai

然后配置密钥和模型名。下面只是占位符,真实密钥只能通过环境变量提供:

1
2
3
4
export OPENAI_API_KEY="替换为你的真实密钥"
export MODEL_SIMPLE="替换为可用的轻量模型"
export MODEL_ANALYSIS="替换为可用的分析模型"
export MODEL_CODE="替换为可用的代码模型"

模型名称必须以你实际使用的服务商文档为准,不要因为模型名看起来相似就假定它一定可用。

编写最小路由器

新建 router.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import json
import os
import sys
from dataclasses import dataclass

from openai import OpenAI


@dataclass(frozen=True)
class Route:
model: str
reason: str


def choose_route(kind: str) -> Route:
routes = {
"simple": Route(os.environ["MODEL_SIMPLE"], "简单任务使用轻量模型"),
"analysis": Route(os.environ["MODEL_ANALYSIS"], "分析任务使用分析模型"),
"code": Route(os.environ["MODEL_CODE"], "代码任务使用代码模型"),
}
try:
return routes[kind]
except KeyError as exc:
allowed = ", ".join(routes)
raise ValueError(f"kind 必须是:{allowed}") from exc


def load_task(path: str) -> dict:
with open(path, encoding="utf-8") as file:
task = json.load(file)
if not isinstance(task.get("kind"), str):
raise ValueError("kind 必须是字符串")
if not isinstance(task.get("text"), str) or not task["text"].strip():
raise ValueError("text 必须是非空字符串")
return task


def run(task: dict) -> dict:
route = choose_route(task["kind"])
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.responses.create(
model=route.model,
instructions="用中文回答,直接处理用户任务,不要解释路由过程。",
input=task["text"],
)
return {
"model": route.model,
"reason": route.reason,
"output": response.output_text.strip(),
}


def main() -> None:
if len(sys.argv) != 2:
raise SystemExit("用法:python router.py task.json")
result = run(load_task(sys.argv[1]))
print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
main()

创建输入文件 task.json:

1
2
3
4
{
"kind": "code",
"text": "解释 Python 中列表推导式的基本写法,并给出一个简短例子。"
}

运行:

1
python router.py task.json

程序会实际调用所选模型,并输出模型名、路由理由和本次返回的文本。这里不能预先写死模型回答,因为结果取决于你的服务、模型版本和请求状态;可验证的事实是路由函数选择了配置中的对应模型,网络调用成功后才会有 output。

为什么把路由和调用分开

choose_route 是纯函数式的确定性逻辑,不需要网络,也不需要密钥。它可以单独测试:

1
2
assert choose_route("simple").reason == "简单任务使用轻量模型"
assert choose_route("code").model == os.environ["MODEL_CODE"]

网络请求只集中在 run 中。这样做有两个好处:第一,路由规则错误不会被 API 异常掩盖;第二,测试时可以替换客户端,避免每次测试都消耗额度。更进一步,可以让 run 接收一个客户端参数,生产环境传入真实客户端,测试环境传入返回固定文本的假客户端。

加上长度与预算保护

类型路由只是第一层规则。用户把一篇很长的文档标成 simple,仍可能造成上下文超限或费用异常。因此,调用前应做输入长度检查,并为每类任务设置预算。例如,先限制字符数:

1
2
3
4
5
6
7
MAX_CHARS = {"simple": 4000, "analysis": 20000, "code": 12000}


def validate_size(task: dict) -> None:
limit = MAX_CHARS[task["kind"]]
if len(task["text"]) > limit:
raise ValueError(f"输入超过 {limit} 个字符")

这不是精确的 token 计算,中文、代码和不同 tokenizer 的比例都可能不同;它只是一个便宜的第一道保护。涉及严格上下文窗口时,应按实际模型文档和 tokenizer 估算,并在日志中记录输入长度、模型、耗时和错误类型,不要记录密钥或未经脱敏的敏感正文。

常见问题

为什么不直接让模型选择模型? 模型选择本身也要消耗一次调用,而且结果不稳定、难审计。可以把模型建议作为候选信息,但最终选择应受代码规则、预算和权限约束。

模型名称配置错误怎么办? SDK 通常会返回认证、请求或模型不存在等错误。不要无限重试这类配置错误;启动时可以检查必需环境变量是否存在,并把错误写到标准错误。实际项目还应设置请求超时和有限重试。

多个模型的提示词可以完全相同吗? 不一定。不同模型支持的输入格式、参数和能力可能不同。示例使用 Responses API 的共同字段,但接入其他服务时必须查阅其官方文档确认兼容性。

路由后如何证明成本下降? 不能凭一次调用下结论。应准备固定任务集,记录每次的模型、输入输出 token、延迟、错误和质量评分,再与统一使用单一模型的基线比较。模型更便宜不代表总成本更低,如果返工和失败更多,整体成本可能上升。

小结

本项目把模型路由拆成了三个清晰环节:用输入类型确定规则,用环境变量提供模型配置,用独立函数记录选择理由,再由统一调用层取得结果。它没有把模型能力差异包装成绝对结论,也没有伪造 API 输出。继续工程化时,可以加入基于评测集的路由规则、预算熔断、人工覆盖和指标看板,但每一步都应保留可解释的决策记录,并为失败设计明确的降级路径。