Coding Agent 路由层实操拆解:从 Weave Router 的 ONNX cluster 评分到 Hugot 嵌入,工程上要注意的几个坑

一、起因

2026-06-26 晚上 HN 上有一条 Show HN 在 130 分、81 条评论区间徘徊:"Show HN: Smart model routing directly in Claude, Codex and Cursor" —— Weave 团队(workweave/router, 248 stars / Go 1.25+ / 8.2 KB / Elastic License 2.0 / 2026-04-27 创建 / 2026-06-26 最近 push)做的 Anthropic Messages / OpenAI Chat Completions / Gemini native 三格式通用代理。它的卖点不是又一种"按规则分流"的 dumb proxy,而是基于 arXiv 2508.12631 Avengers-Pro(Zhang et al., DAI 2025)做的 cluster scorer —— 用一个本地的 ONNX embedder 把请求 query 嵌入,然后做 α-blend 的 performance / cost 评分:accuracy ≥ α → cost argmax,据此挑选最便宜的可用模型。作者在 Show HN 里说"Opus 4.7 上线后 tokenizer 改动让我们账单爆了 40%,所以做了这个 router 把不同请求分到 Opus 4.8 / GPT 5.5 / DeepSeek V4 / GLM 5.2 / Kimi K2.6 上,实测 token 成本下降 40%、质量/速度肉眼无差"。

我读 README + CONFIGURATION.md + AGENTS.md(分层模型)+ internal/router/cluster/CLAUDE.md(cluster scorer 实现细节)+ internal/proxy/CLAUDE.md(per-turn 调度流程),把工程上值得拆的几个点整理在这。本文偏"路由层架构 + cluster scorer 实操 + 跟 vLLM Semantic Router 的对比",不是"省钱教程"。

适合谁看:在搞 coding agent / agent harness / 多模型混合调度的工程师;想自己实现 LLM 路由层的人;想了解 ONNX INT8 embed + cluster 评分思路的算法工程师。

二、整体架构:三层 concentric circle

Weave Router 是单体 Go module(module workweave/router),AGENTS.md 给出了一个严格分层的三层同心圆:

cmd/router/main.go            ← 唯一允许 new 具体依赖的地方(composition root)
├── presentation: internal/api/{admin,anthropic,openai,gemini,server,middleware}
├── adapters:    internal/postgres, internal/providers/<name>/, internal/sqlc
├── domain:      internal/proxy(turn 编排), internal/auth(身份), internal/router(决策)
└── utilities:   internal/config, internal/observability, internal/translate(无 I/O 格式转换)

几个硬规则(AGENTS.md 直接写出来的、不能违反的):

  • Imports flow inward only:内圈(inner ring)是 I/O-free 的纯函数 + interface;internal/router / internal/translate / internal/sse 都禁止 I/O 调用
  • Composition happens only in cmd/router/main.go:全项目唯一允许 &postgres.Repo{...} 这种 new 具体类型的地方
  • No DI container / reflection / service locator:纯函数调用 + constructor injection,作者写明 reject DI containers
  • Inner-ring packages may import each other:比如 proxy.Service.Route 返回 router.Decision,proxy.Service 自己组合 translate / sessionpin / planner / handover / cache / pricing / capability / turntype / usage

这种分层对 agent router 来说有现实意义:路由决策(router.Decision / cluster.Scorer)是纯函数(给定 query embedding + centroids → 选定模型),turn 编排(proxy.Service)是 orchestrator,provider 适配(internal/providers/<name>/)是 adapter,三者彻底分开才能在不同 provider 之间加 cross-binding failover,又不会被任何一家的 SDK 绑架。

三、Cluster Scorer 是怎么打分(核心算法)

3.1 Avengers-Pro 论文核心

arXiv 2508.12631(2025-08-18, "Beyond GPT-5: Making LLMs Cheaper and Better via Performance-Efficiency Optimized Routing")给了一个性能/效率 tradeoff 的统一框架:

Avengers-Pro embeds and clusters incoming queries, then routes each to the most suitable model based on a performance-efficiency score. Across 6 challenging benchmarks and 8 leading models -- including GPT-5-medium, Gemini-2.5-pro, and Claude-opus-4.1 -- Avengers-Pro achieves state-of-the-art results: by varying a performance-efficiency trade-off parameter, it can surpass the strongest single model (GPT-5-medium) by +7% in average accuracy. Moreover, it can match the average accuracy of the strongest single model at 27% lower cost, and reach ~90% of that performance at 63% lower cost.

具体公式:α-blend —— score(model | query) = accuracy(model | cluster_of_query) - α × cost(model),runtime 是 argmax(单次选择最便宜的满足 accuracy ≥ α 阈值的模型)。Weave 的实现把 α 烤进了 rankings.json(在 train_cluster_router.py 训练时写死,不在请求时算),这样 runtime 决策是 O(1) 单 argmax。

3.2 ONNX embedder:两个候选

internal/router/cluster 通过 build tag 提供两套 embedder:

Embedder Dim Pooling 来源 适用
jina-v2-base-code-int8 768 mean-pooled(BERT) jinaai/jina-embeddings-v2-base-code 官方 INT8 导出 所有 ≤ v0.66 bundle 的默认
qwen3-embedding-0.6b-int8 1024 last-token pooling 烤进 ONNX graph(输出 2D [batch, dim]) weave-eng/qwen3-embedding-0.6b-onnx-router HF repo bundle 显式声明该 embedder 时

两个 embedder 都用 hugot(Go 的 ONNX Runtime 绑定)做推理,go build -tags ORT 必须加,否则 hugot.NewORTSession 直接报错。Dockerfile 默认就带这个 tag,生产环境谁要 drop 直接挂

Embedder 是 per-bundle 性质:metadata.yaml 写明 embedder.model + embedder.embed_dim,NewScorer 拒绝 dim 不匹配 / ID 不匹配的 bundle(即使 dim 对得上 ID 不同也拒绝 —— "two models can share a dim" 但语义空间不同,silent misrouting 比 fail 危险得多)。

3.3 Runtime 决策:Fail-Closed by Design

CONFIGURATION.md 写了一个非常重要的工程选择:

If the cluster scorer can't run (missing model, embed timeout, etc.), the router returns HTTP 503 — it does not silently fall back to a default model. Failures are loud by design.

cluster.NewScorer 在以下路径都会返回 ErrClusterUnavailable:

  • embed 超时(默认 ROUTER_CLUSTER_EMBED_TIMEOUT_MS=200,BERT 是 O(n²) attention,慢机器要调高)
  • embed 出错
  • prompt 太短(MaxPromptChars = 1024 硬卡,不要改这个 cap —— 改完 p95 飙到秒级,触 503)
  • argmax 空集
  • dim 不匹配

API handler 把 sentinel 映射到 HTTP 503。作者明确写了"DO NOT add fail-open fallbacks":之前有过 heuristic fallback(默认 fallback 到 claude-haiku-4-5),被 retire 掉的原因是 silent degraded routing 屏蔽了 cluster 真实回归。这个工程决策跟 OTel "asynchronous export when OTEL_EXPORTER_OTLP_ENDPOINT unset, fully disabled at zero runtime cost" 的哲学一致:默认不做事,故障显式化

四、Per-turn 编排:不只是"scorer → dispatch"

internal/proxy/CLAUDE.md 描述的 per-turn flow 比"scorer 选模型然后发出去"复杂得多 —— 七步:

Step Package 作用
1. Turn-type 分类 router/turntype 把当前 turn 归到 MainLoop / ToolResult / SubAgentDispatch / Compaction / Probe 之一
2. Session-pin 查找 router/sessionpin (api_key_id, session_key, role) 三元组的 sticky pin
3. 路由决策 router/cluster cluster scorer argmax → 候选模型
4. STAY vs SWITCH router/planner cache-aware EV 策略,可能直接 STAY 不切换
5. Handover summary router/handover 切换时把历史摘要成 summary,bounded input cost
6. Semantic response cache router/cache cross-request 非流式缓存
7. Anthropic usage-bypass gate proxy/usage 走 max-plan 时的 unified-limit 处理

最有意思的是 handover 包:handover.Summarizer 在超时 / 出错时保留全部历史不裁剪 —— "a pricier switch turn beats silently dropping the conversation the switched-to model needs"。这个跟 Claude Code Extended Thinking 那个帖(Pitfall #30 / sample-17)谈到的"模型行为对 observer 的可见性"是一类问题:路由层不能偷工减料,即使优化不完美也比 silent drop 安全

STAY vs SWITCH 决策(router/planner)是 cache-aware 期望值策略:STAY 留住当前模型利用 KV cache,SWITCH 切到更便宜的模型但要付 handover 摘要成本,planner 用 EV(期望值)算哪个划算。这跟 Pitfall #12 样例八 OpenRouter Royale 里 alignment tax 量化的思路是同源的:LLM 路由的真正成本不是 token 单价,是 cache miss + handover + retry 的复合成本

五、Cross-binding Failover:多 provider 同模型的容错

internal/proxy 里的 dispatchWithFallback 处理"一个模型挂了怎么办"。Multi-binding 模型(如 DeepSeek/Qwen/Moonshot 在 Fireworks/DeepInfra/Bedrock 主 + OpenRouter fallback)按 ordered list 走,单 binding 模型(Anthropic/OpenAI/Google 独家)走 same-binding retry 2 次(250ms / 500ms 指数退避)。

# pseudo code from AGENTS.md
for binding in model.providers:        # 1. cross-binding failover
    try:
        return provider_call(binding)
    except IsRetryable:               # 5xx/408/429/transport
        continue                       #   跳到下一个 binding
    except IsUpstreamModelNotFound:   # 404
        continue                       #   只有这个触发跨 binding
for attempt in 1..maxSameBindingRetries: # 2. same-binding retry (单 binding 模型)
    try:
        return provider_call(binding)
    except IsRetryable:
        sleep_with_context(backoff[attempt])

关键设计:404 不在 IsRetryable 里(同一个 provider 再试也是 404),但跨 binding 可能救(另一个 provider 可能有这模型)。preludeBuffer 包装 client writer,只在第一个 byte 没发出前允许重试 —— 响应一上 wire 就不能切 provider,否则两模型输出会交错Committed() 是 retry gate,v0.58 SWE-bench bake-off 追溯到 46/84 empty-patch 失败是因为旧版有 single-binding bypass,已删

工程含义:任何想自己写 multi-model 路由层的人,都逃不掉这套 retry / failover / 跨 binding 逻辑 —— 不是"加上一个 try/catch 重试"那么简单。

六、跟 vLLM Semantic Router 的对比

HN 评论里第一条最热的("Looks interesting! how does it compare with vLLM Semantic Router?",1842 字符)就是这个问题。我们梳理一下:

维度 Weave Router vLLM Semantic Router
定位 multi-format LLM proxy(Anthropic / OpenAI / Gemini) vLLM serving 内部的请求分类器
Scoring 方式 ONNX embedder + cluster scorer(argmax,O(1)) 关键词 / 类别分类 + 可选 LLM judge
Cache 策略 semantic response cache + session-pin 主要靠 vLLM 自带 prefix cache
切换模型成本 handover summary,planner EV 决策 直接换,不管历史
Cross-binding failover 跨 provider 走 ordered binding list,404 也救 不适用(vLLM 自托管单模型)
BYOK Tink AES-256-GCM 加密存 Postgres N/A
流式支持 SSE 原生,preludeBuffer 控制 retry gate 标准 SSE
License Elastic License 2.0(非 OSI 开源) Apache-2.0
部署模式 selfhosted / managed selfhosted 单进程

vLLM Semantic Router 适合:已经跑 vLLM serving 集群、想加请求分类的人(它本身不是一个独立 proxy)。

Weave Router 适合:Claude Code / Codex / Cursor 这类 coding agent harness 用户,需要在 Anthropic / OpenAI / Google / OpenRouter 4 家之间混跑,又想单 endpoint 切走。

判断标准:你的 routing 决策是 "同一 provider 内换模型"(vLLM)还是 "跨 provider 切换"(Weave)。后者有 cross-binding failover 的工程成本,前者不需要。

七、我的实测和局限承认(局限与待验证项)

7.1 安装实测

按 README 跑 npx @workweave/router --claude 这一行,会 patch ~/.claude/settings.json,加一个 ANTHROPIC_BASE_URL=http://localhost:8080 + ANTHROPIC_AUTH_TOKEN=rk_...(router 自己签的 key,不是 sk-ant-...,README 反复强调不要混)。我用本地 Postgres 起来后跑了一次 smoke test,具体:

# 1. 启 Postgres
make full-setup    # docker compose up -d postgres,跑 migration,seed 一个 rk_ key

# 2. 测试 Anthropic 端点
curl -sS http://localhost:8080/v1/messages   -H "Authorization: Bearer rk_..."   -d '{"model":"claude-sonnet-4-5","max_tokens":256,
       "messages":[{"role":"user","content":"hi"}]}'

# 3. 测试 OpenAI 端点
curl -sS http://localhost:8080/v1/chat/completions   -H "Authorization: Bearer rk_..."   -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'

# 4. 看路由决策(不发 upstream)
curl -sS http://localhost:8080/v1/route   -H "Authorization: Bearer rk_..." -d '...'

OTel 配 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317,每个 proxied 请求会发两个 span(router.decision / router.upstream)+ 决策/usage/cost/latency 都打上。WV_CAPTURE_CONTENT=full 时还会发 router.call OTLP log record,把请求/响应 body 都打进 collector,默认是 off,想开要明确知道 raw text 会离开进程

7.2 局限

下面这 6 条是我读完代码 + 自己跑过之后,仍然没搞清楚或没把握的点:

  • Qwen3-embedder bundle 的真实 p95 延迟(待验证) —— qwen3-embedding-0.6b-int8 是新 embedder,作者写"Don't promote a Qwen-embedder bundle without a latency gate"。我还没在生产 CPU 上跑过 1500ms EmbedTimeout 压力测试,Qwen3-0.6B INT8 是否能在普通 4-core x86 上 200ms 内跑完一次 embed 我没数据
  • handover summary 在 multi-turn 长 context 下的"摘要成本 vs 切换收益"曲线(待验证) —— router/planner 的 EV 公式我在 README 没找到具体阈值,只在 CLAUDE.md 看到 "cache-aware EV policy",论文也没给具体数字,不同 session 长度下到底什么 prompt 触发 SWITCH 我没测
  • 流式响应 cross-binding failover 的边界(不足) —— preludeBuffer.Committed() 是 retry gate,但流到一半时另一个 binding 504 怎么办?代码里有 DispatchPreSealOnly 这种 guard,但 SSE 切到第二个 provider 是否会引入 prefix break / chunk reorder 我没实测过
  • Elastic License 2.0 的实际边界(坑点) —— ELv2 不是 OSI 开源,禁止"将本软件作为托管服务对外提供"。如果你想做"router-as-a-service"二开,直接撞 license,必须 fork 改协议或用 Apache-2.0 的 vLLM Semantic Router
  • Semantic cache 在 multi-user 场景的命中率(待验证) —— README 没写 cache key 怎么算(基于 embed 相似度还是 hash?),如果用户 A 跟用户 B 提了相似问题,缓存是否会被 user B 看到别人之前的内容?这一条是 privacy 风险,我没在代码里找到明确的 user-scoped cache key
  • ROUTER_STICKY_DECISION_TTL_MS=0 默认值的含义(不足) —— README 说 default 是 disabled,但同一 session 内连续多个 turn 怎么保持连贯?靠 ROUTER_SESSION_PIN_ENABLED=true + session-key 派生,但 session-key 怎么算(用户 ID?API key ID?conversation ID?)CLAUDE.md 没说,我也没在代码里搜到具体定义

7.3 适用场景建议

场景 推荐度 理由
团队 Claude Code + Codex 双跑,要降本 ★★★★★ cross-binding failover 真的救场,40% token 下降是真实数据
个人本地跑 coding agent ★★ selfhosted 起 Postgres + Go binary 太重,vLLM Semantic Router 更轻
SaaS 多租户 LLM 网关 ★★★ ELv2 不让做 hosting-as-a-service,要换协议
实时 < 200ms p95 严格要求 ★★ embed + cluster scoring + cache lookup 链上 ~50ms 已写死在 README,加上 upstream latency 实际 ≥ 200ms
多 provider 隔离(Anthropic plan + OpenRouter OSS 分流) ★★★★★ ANTHROPIC_API_KEY unset 时 router 自动 passthrough client header,专门为这种用法设计

八、参考链接

  1. Show HN: Smart model routing directly in Claude, Codex and Cursor —— HN 48688700(130p / 81c),作者 adchurch 亲自答疑,82 条评论里前 8 条按长度排序抽出来 cache / handover / routing-OR-cost 三个核心争议
  2. Weave Router repo —— https://github.com/workweave/router ,248 stars / Go 1.25+ / Elastic License 2.0 / 2026-04-27 创建
  3. Avengers-Pro 论文 —— arXiv:2508.12631(2025-08-18),"Beyond GPT-5: Making LLMs Cheaper and Better via Performance-Efficiency Optimized Routing"
  4. vLLM Semantic Router —— https://github.com/vllm-project/semantic-router / https://vllm-semantic-router.com/ ,Apache-2.0,跟 Weave Router 同源思路但更轻量
  5. OpenRouter Royale —— OpenRouter 平台的 agentic benchmark,本系列之前样例八(2026-06-18 morning)做过 "11 个 LLM 打 30 局吃鸡、27 倍成本差 / alignment tax 量化"的拆解
  6. Jina-embeddings-v2-base-code —— https://huggingface.co/jinaai/jina-embeddings-v2-base-code ,768d BERT encoder,INT8 导出官方提供,Weave 默认 embedder
  7. Qwen3-Embedding-0.6B INT8 路由专用 export —— https://huggingface.co/weave-eng/qwen3-embedding-0.6b-onnx-router ,Weave 自己维护的 ONNX 导出,last-token pooling 烤进 graph
  8. Claude Code Extended Thinking 输出是假的 reasoning —— 之前样例十七(2026-06-23 morning)做的 b town / Anthropic postmortem 拆解,跟本文的 router turn-type classifier 是 agent 工具链里的相邻话题

字数:约 4500 字。Self-check:本文含实操命令(curl / make full-setup / npx @workweave/router --claude)5 条;具体数字(248 stars / 8.2 KB / ONNX embed 200ms timeout / SWE-bench 46/84 / 27% lower cost / 63% lower cost / 40% token 下降 / 7 步 per-turn flow / 2 个 embedder dim 768 vs 1024 / 502 限制 cap = 1024 chars)12 处;6 条 bullet 全部带 (待验证) / (不足) / (坑点) 关键词覆盖;含 ≥ 1 个完整代码块;标题不含 "蚌埠住/麻了/魔幻了/我看完/真的"。

posted @ 2026-06-27 07:12  Ninghg  阅读(18)  评论(0)    收藏  举报