不依赖第三方库,构建一个标准库-only 的 LLM 提供商路由层,支持任务分类调度和降级策略,深入理解模型路由核心逻辑。
每隔几周就会有一个新模型发布,我的学习小组群里就会充斥着截图:"这个更便宜","这个代码能力更强","赶紧换"。我从来没法快速验证这些说法,因为我的测试脚本都是硬编码某一家提供商的客户端。重写测试工具的时间比热点周期还长。
所以这里有个学习问题:一个只有约 60 行、标准库-only 的 Python 路由板,能否让我在一个接口背后切换提供商、将玩具任务路由到不同后端、并用一个失败的 fixture 来证明路由在哪里出了问题?
运行最终脚本,你会看到:
[route=summarize] backend=local-echo cost=0.0000 out='SUMMARY: ...'
[route=code] backend=mock-strong cost=0.0020 out='def add(a, b): ...'
[route=summarize] backend=mock-strong cost=0.0020 out='SUMMARY: ...' (fallback fired)
第三行才是有趣的部分——继续往下看。
Python 3.11+(在 3.12.4、macOS 和 WSL2 Ubuntu 上测试过)
不依赖第三方包。核心实验不需要 API key。
封装器隐藏的是某一家提供商的quirks。路由板做的是另一件不同的事:它定义你的任务类型(summarize、code、chat)并将每个类型映射到一个后端加一个 fallback。当新模型出现时——不管这个月它叫什么——你只需要加一条记录,重新跑同样的 fixture。你的评估问题保持不变,而后端在它们下面轮转。
这才是真正的技能:把"问什么"和"谁来答"分开。
保存为 switchboard.py:
"""Tiny provider-agnostic LLM switchboard (stdlib only)."""
from dataclasses import dataclass, field
from typing import Callable
# --- Backends: same signature (str) -> str, so they are interchangeable ---
def local_echo(prompt: str) -> str:
"""Free 'backend': deterministic, offline, good enough for smoke tests."""
return f"SUMMARY: {prompt[:40]}"
def mock_strong(prompt: str) -> str:
"""Pretend paid model. Fails on empty input, like a real API rejects it."""
if not prompt.strip():
raise ValueError("backend rejected empty prompt")
if prompt.startswith("write code"):
return "def add(a, b):\n return a + b"
return f"SUMMARY: {prompt[:40]}"
@dataclass
class Backend:
name: str
fn: Callable[[str], str]
cost_per_call: float # USD, your own pricing notes go here
@dataclass
class Switchboard:
routes: dict[str, list[Backend]] = field(default_factory=dict)
spent: float = 0.0
def register(self, task: str, backends: list[Backend]) -> None:
self.routes[task] = backends
def run(self, task: str, prompt: str) -> str:
if task not in self.routes:
raise KeyError(f"no route registered for task '{task}'")
last_err = None
for backend in self.routes[task]:
try:
out = backend.fn(prompt)
self.spent += backend.cost_per_call
print(f"[route={task}] backend={backend.name:<11} "
f"cost={backend.cost_per_call:.4f} out={out.splitlines()[0]!r}")
return out
except Exception as e: # try the fallback backend
last_err = e
raise RuntimeError(f"all backends failed for '{task}'") from last_err
if __name__ == "__main__":
free = Backend("local-echo", local_echo, 0.0)
paid = Backend("mock-strong", mock_strong, 0.002)
sb = Switchboard()
sb.register("summarize", [free, paid]) # cheap first, paid as fallback
sb.register("code", [paid]) # only the 'strong' backend
sb.run("summarize", "explain gradient descent to a first-year student")
sb.run("code", "write code to add two numbers")
sb.run("summarize", " ") # free backend accepts junk... or does it?
print(f"total spent: ${sb.spent:.4f}")
[route=summarize] backend=local-echo cost=0.0000 out='SUMMARY: explain gradient descent to a f'
[route=code] backend=mock-strong cost=0.0020 out='def add(a, b):'
[route=summarize] backend=local-echo cost=0.0000 out='SUMMARY: '
total spent: $0.0020
等等——第三行并没有触发 fallback,它返回了对空格的垃圾摘要。我的 local_echo 后端愉快地接受了空 prompt。我在前言中承诺的 failure 只会在主后端抛出异常时才会触发。继续阅读之前:哪种 fixture 输入会强制显示前言中的第三行 fallback 日志?(答案在底部。)
调换注册顺序,让严格的后端优先:
sb.register("summarize", [paid, free]) # strict first, lenient as fallback
sb.run("summarize", " ")
现在你得到了前言中的第三行:mock-strong 对空 prompt 抛出异常,switchboard 捕获了它,然后 local-echo 作为替代回答。真正重要的概念是:fallback 顺序是一个策略决策,"免费优先"和"严格优先"会在相反的方向上失败。免费优先静默返回垃圾;严格优先静默花钱,当你本以为走的是便宜路径。
捕获 nothing 或 everything blindly。捕获 Exception 在教学 harness 中没问题,但在真实代码中你应该区分"提供商宕机"(retry/fallback)和"我的 prompt 被拒绝"(fallback 只是昂贵地重新失败)。
在不同 prompt 上比较模型。如果 backend A 看到了清洗过的 prompt 而 backend B 看到了原始的,你的成本/质量记录就毫无意义。switchboard 向每个后端传递完全相同的字符串——保持这种方式。
信任发布周的定价声明。当新模型被宣传为"便宜且强大"时,在将其接线为默认路由之前,先在提供商的官方页面上核实定价。发布周的数据会变化,而且 context-window 限制会悄悄改变每个任务的真实成本。
我大多数实验都在 MonkeyCode 里做,因为它提供的免费模型访问让我可以针对真实的 LLM 端点做原型,而不是只有 mock,并且免费 server 选项意味着 harness 运行在某台不是我的笔记本的机器上(在课间)。披露:本文是 MonkeyCode 产品推广的一部分。不过上面的 switchboard 模式故意做到了 provider-agnostic——同样的文件可以对接任何端点,包括完全没有。
这是一个学习产物,不是生产级基础设施:没有带 backoff 的重试、没有 rate-limit 处理、没有 async、没有 token 级别的成本核算(这里的 cost 是按次计算的数字,你自己填)。
如果你已经在用一个成熟的 router/gateway 库,这篇文章在操作层面教不了你什么新东西——虽然亲手实现一次仍然有助于理解那个库隐藏了什么。
不要用 fallback 链来掩盖评估问题。"它返回了点什么"不是质量信号。
添加第三条路由"chat"和一个包含五个 prompt 的 fixture 文件,书面预测每个 prompt 应该由哪个后端处理。然后让 mock_strong 在 30% 的调用中随机抛出异常(用种子使结果可复现),检查你的成本预测是否仍然成立。如果你的成本估算假设零失败,这告诉你关于发布周定价对比的什么信息?
当 summarize 注册为 [free, paid] 时,只有让 local_echo 本身抛出的输入才会触发 fallback——而 local_echo 从不抛出。前言中的第三行只有在 strict-first 注册方式下才会出现。如果你预测到了这一点,你就理解了策略顺序这个要点;如果没有,跑两个版本并 diff 输出。
如果你找到了一个 fixture 输入使得 fallback 让情况变得更糟(例如,宽松的后端返回一个危险地合理的答案),发出来——最小反例是这类讨论最有价值的部分。