2026年7月23日 2 分钟阅读

本地模型、云端 API 与多 Agent 并行时,怎样用 Millwright 把路由、缓存亲和与成本账本收进一个自托管网关?

tinyash 0 条评论
海军蓝低多边形观测塔与分叉道路的三维插画

AI 编程工作流一旦不止一个模型,真正难管的往往不是“能不能调用”,而是每一次调用到底走了谁、为什么走它、会不会把刚热起来的提示词缓存换掉。Claude Code、Codex 或自建脚本各自保存 Provider Key,也会让权限、成本归属和故障切换散落在不同配置里。

Millwright 是一个 Apache-2.0 开源、自托管的 Rust LLM 路由器。它位于客户端与模型提供商之间:入口可接受 OpenAI Chat Completions 与 Anthropic Messages;出口可接 OpenAI 兼容 API、原生 Anthropic 或 Amazon Bedrock。它不是 Agent 编排器:不创建子 Agent、不读取提示内容来分类,也不改写上下文。这个边界很重要——它解决的是可审计的数据平面路由,而不是替你决定任务本身。

不按“模型名”硬编码,而是先声明三个角色

Millwright 将可用模型放进 cheapmidfrontier 三个角色。客户端请求 millwright-fastmillwright-codemillwright-plan 时,分别是在选择低成本、中档、前沿角色;网关再从该角色允许的健康路由里选择估算成本最低者。

这样做的价值不只是省钱。策略文件还能把任务类型和风险变成可见规则:高风险请求与 planning 被固定提升到 frontier。普通请求默认按 routine_code 处理,若有自己的 harness,也可用 X-Millwright-Task-TypeX-Millwright-Risk 显式说明任务。文档明确指出:这些信息优先级高于模型别名,且网关不会从消息正文“猜”风险。

下面是基于官方示例压缩后的策略骨架;模型名称、价格和密钥哈希都必须替换为自己的真实值:

{
  "version": "v0.1",
  "auth": {
    "api_keys": [
      {
        "name": "dev-workload",
        "team": "platform",
        "key_hash": "",
        "scopes": ["inference"]
      }
    ]
  },
  "model_roles": {
    "cheap": {"models": ["cheap-general"], "allowed_for": ["summarization"]},
    "mid": {"models": ["mid-coder"], "allowed_for": ["routine_code", "verification"]},
    "frontier": {"models": ["frontier-planner"], "allowed_for": ["planning", "high_risk_code"]}
  }
}

密钥的设计也值得借鉴:入站 workload key 与 operator key 分开,策略中只保存 64 位十六进制 SHA-256 哈希。推理客户端只能拿 inference 权限;读取全局账本、指标和 trace 的 operator key 不应发给日常编程客户端。上游 Provider 凭据则只在网关所在机器的环境变量或 _API_KEY_FILE 中出现。

缓存亲和:不是把所有请求串行化

许多“自动选最便宜模型”的做法有一个隐藏副作用:同一会话的请求不断切换 Provider 或模型,提供商侧可复用的长提示前缀就可能失温。Millwright 允许客户端传入不透明的 X-Millwright-Session-ID,并为同一 session 的 cheap、mid、frontier 分别维护一条亲和 lane。

这意味着一个 Agent 可在计划阶段走 frontier,转入编码时走 mid,处理标题或摘要时走 cheap;回到此前角色时,若对应路由仍健康且 TTL 未过,网关会优先复用该 lane。它不会因此把并发请求排队或合并,真实缓存命中仍取决于 Provider 与提示前缀是否兼容。对多 Agent 场景,这比“一个 session 永远固定一个模型”更实用,也比在客户端各自实现粘性逻辑更容易复盘。

从本地 demo 开始,再替换为私有配置

从源码构建需要 Rust 1.97 或更高版本。首次尝试可运行项目自带的 mock Provider;它适合验证协议和路由,不是实际模型服务:

git clone https://github.com/Northwood-Systems/millwright.git
cd millwright
cargo install --path . --locked
millwright init
millwright serve

init 会交互式生成策略、模型目录、Provider 环境变量引用以及两类入站 key。它保存的是 ${VAR} 形式的引用,不把 Provider 密钥写入生成文件。实际部署时,先在网关进程环境中注入这些变量;本地账本默认用 SQLite,生产可设置 MILLWRIGHT_LEDGER_BACKEND=postgres 与 Postgres DSN。

对于容器部署,官方 Compose 默认仅把 8080 发布到宿主机 127.0.0.1,并用 demo mode 与 mock Provider。接入真实 Provider 前,应生成私有 policy/catalog、关闭 demo mode,并让 TLS 在受信任的反向代理、Ingress 或负载均衡器终止。官方特别提醒:不要把管理端点直接暴露到公网;管理 CLI 也拒绝把 operator key 发往非 loopback 的明文 HTTP 地址。

接入 Claude Code 时,把“模型选择”变成角色选择

Claude Code 应指向网关根地址而非 /v1。以下是官方文档给出的核心映射,环境变量值仅应保存在本机 shell、密钥管理器或受控服务环境中:

export ANTHROPIC_BASE_URL="http://localhost:8080"
export ANTHROPIC_AUTH_TOKEN="$MILLWRIGHT_WORKLOAD_KEY"
export ANTHROPIC_MODEL="millwright-code"
export ANTHROPIC_SMALL_FAST_MODEL="millwright-fast"
export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1

这让主工作默认进入 mid,而 Claude Code 的小型后台工作可落到 cheap;需要计划或高风险变更时,再选择 millwright-plan。不过要确认客户端版本与请求形态:Millwright 当前暴露 Chat Completions,而官方文档明确说,若某个 Codex 版本强制使用 Responses API,就不能先把它指向该网关。跨协议路由也只翻译文本与工具调用的受支持子集;图片、文档块、签名 thinking 等无法忠实表达的形态会显式返回错误,而不会悄悄丢字段。

不要把“最低价”当成唯一策略:先校准目录与失败边界

模型目录是成本判断的依据,而不是装饰配置。每个模型条目需要指向已注册的 provider、实际上游模型名,以及输入、缓存读取、输出等价格;缺失价格会让网关拒绝把未知成本伪装成免费。这里的“最低成本”是目录价格与路由健康状态下的选择结果,并不等于质量最优、延迟最低,也不代表跨模型的输出可直接互换。

建议先只放入每个角色一到两个模型,并用一小组固定任务验证:短摘要、常规修改、架构规划各自是否真的命中预期角色;再查看 trace 中的 route reason、被拒绝候选和实际用量。确认语义、工具调用、上下文窗口和速率限制都满足后,才扩大目录。特别是跨协议时,官方选择“无法忠实翻译就报错”的策略,这比静默降级安全,却要求团队事先把图像、文档和复杂流式工具调用留给原生协议的路由。

当上游短暂失败,Millwright 对可重试传输错误、超时、HTTP 408、429 和 5xx 最多进行一次受限 failover。因此应用仍应保留自己的超时、重试和幂等设计;不要把网关误当成“所有故障都会被自动治愈”的高可用系统。路由器的 circuit breaker 也只是避免持续把请求送往不健康的路由,不能替代容量规划和 Provider 侧配额治理。

上线前的最小检查表

第一,给 workload key 与 operator key 分配不同的保存位置和轮换责任人,确认普通 Agent 无法调用管理读取接口。第二,检查 MILLWRIGHT_GATEWAY_ADDR 是否仍为默认的 127.0.0.1:8080;若需要跨机器访问,应通过私有网络和 TLS 反向代理暴露,而不是直接公开服务端口。第三,确认数据库策略:本地试验可以用 SQLite,多个副本或需要长期集中账本时再迁到 Postgres。

第四,观察而不是猜测缓存收益。X-Millwright-Session-ID 只是亲和范围,不会作为 Provider 的缓存 key 转发;只有同一角色、路由健康、TTL 尚暖且提示前缀兼容时,Provider 才可能产生实际缓存命中。第五,给账本导出设置边界:官方文档说明管理 API 会在内存中物化匹配行,大量历史分析应从 Postgres 分页送入数据仓库,而不是把 ledger/export 当无限流式接口。

把这些边界写进运行手册后,团队获得的不是一个神秘的“模型自动驾驶”,而是一条可检查的决策链:客户端请求了什么角色、策略为什么升级或降级、最终选了哪个 Provider、失败时是否切换、成本数据从哪里来。对多模型开发环境来说,这种可解释性往往比又多接入一个模型更有长期价值。

成本记录不是“估算数字”的装饰

每个完成请求可写入用量证据、路由证据、Provider 尝试和成本来源。SQLite 是本地默认值,PostgreSQL 适合生产;millwright spendmodelstrace top 分别用于查看总支出/缓存读取率、模型与 Provider 构成、一次决策的候选淘汰过程,以及终端实时活动。

排查时先从响应头拿 X-Millwright-Trace-ID,再查看 trace,而不是只看最终模型名。网关对可重试的网络失败、408、429 与 5xx 最多做一次受限 failover;“最多两次 Provider 尝试”不等于会遍历所有路由。对高流量团队还要注意,管理 API 当前会在内存中物化匹配账本行,导出大规模历史数据应直接从 Postgres 分页同步到数据仓库。

Millwright 适合已经有多家模型、希望让“质量档位—成本—风险—证据”统一落在私有基础设施的团队。若你只需要单一 Provider 的简单代理,或希望网关替你自动规划任务、检查代码,它反而会显得过重。先用少量明确角色和真实价格目录验证路由,再逐步接入更多客户端,通常比一次性把所有模型都塞进“自动最低价”更稳妥。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。