Veral:把一次模型调用变成可回滚的路由策略

一张图读懂公司级 LLM 评测与智能路由平台的理想全貌:在线路由与离线评测如何隔离,Trace 如何变成冻结数据、能力标签和策略版本,以及为什么真正的产品单位是一次可追溯、可灰度、可回滚的策略变化。

大多数团队把「模型调用成功」当作终点。Veral 的设计把它当作起点:一次请求要留下证据,证据要变成可重复评测,评测要形成带版本的能力判断,判断再经过 shadow、canary 和门禁,才有资格改变下一次请求的去向。

这篇只讲一个核心观点:评测与路由平台真正的产品单位,不是一次模型调用,而是一次有证据、可追溯、可渐进发布、可回滚的策略变化。

前面的系统施工图讲了十个模块怎么切,数据规格书把 Trace、样本、标签和训练数据拆到字段级,Inspect AI 实操跑通了最小评测管道,评测平台产品化划清了平台与业务用户的责任。这篇站得更高一点:把这些零件收回同一张生产架构图,解释它们为什么必须长成一个闭环。

先声明证据档位:文中关于 Veral 边界和目标状态的陈述来自项目已批准的设计规格与 ADR;关于 LiteLLM、Langfuse、Inspect AI 和 OpenTelemetry 的职责来自文末官方文档;「真正的产品单位是策略变化」是我的设计判断,不是行业标准。

先装四个杠杆:证据、版本、隔离、发布

如果只记住四个词,记这四个:

杠杆它解决的问题拧错之后会发生什么
证据为什么这次选了模型 A?它到底答得怎样?路由变成黑箱,失败只能靠猜
版本这次分数对应哪版模型、Prompt、数据集和评分器?新旧结果混算,趋势看似连续、实际不可比
隔离离线评测或观测故障时,在线请求能否继续?队列、数据库或 Langfuse 抖动直接拖垮业务
发布一份更高的离线分数,凭什么直接接管生产流量?评测偏差被放大成线上事故

这四个杠杆共同构成一个判断公式:

证据回答「凭什么改」,版本回答「改的是哪一版」,隔离回答「哪里坏了不连坐」,发布回答「怎么安全地让变化发生」。

全貌:两条不同速度的链,闭合成一个回路

Veral 有两条链。

在线链追求稳定和低延迟,只做一件事:读取已经发布的不可变策略快照,给出完整 RoutingDecision,再让 LiteLLM 执行模型调用和 fallback。

离线链追求证据质量:从 Langfuse Trace 里挖候选样本,审核并冻结数据集,交给 Inspect AI 评测,形成能力标签、Gate 和策略候选,再逐级发布。

flowchart TB
    subgraph ONLINE["在线:只消费已发布策略"]
        direction LR
        APP["业务应用<br>经 Caddy 进入"] --> ROUTE["routing-runtime<br>进程内策略快照"]
        ROUTE --> LITE["LiteLLM Proxy"]
        LITE --> MODEL["外部 API 或 vLLM"]
    end

    subgraph OFFLINE["离线:制造下一版证据"]
        direction LR
        TRACE["Langfuse Trace"] --> DATA["审核与冻结数据集"]
        DATA --> EVAL["eval-runtime<br>Inspect AI"]
        EVAL --> LABEL["能力标签与 Gate"]
        LABEL --> RELEASE["不可变策略版本<br>shadow → canary → active"]
    end

    LITE -. "异步证据" .-> TRACE
    EVAL -. "统一模型调用" .-> LITE
    RELEASE -. "发布快照" .-> ROUTE

图里最重要的不是箭头多,而是箭头的方向:在线只消费离线已经验收的结果,离线不能反向卡住在线请求。

如果 control-api、RabbitMQ、eval-runtime 或 Langfuse 暂时不可用,已经加载策略的 routing-runtime 仍应继续服务;策略刷新失败则继续使用内存与磁盘中的 last-known-good。这里的隔离不是性能优化,而是故障边界。

一次请求如何走完两圈

把同一条请求跟到底,它会完成两次循环。

第一圈:在线执行,留下可解释证据

  1. 业务应用从 Caddy 进入 OpenAI-compatible 接口。
  2. routing-runtime 用请求摘要与策略快照运行纯决策函数,输出选中模型、候选列表、理由、策略版本和 fallback 链。
  3. LiteLLM 负责真正的 Provider 调用、统一协议和执行级 fallback;Veral 不重写网关。
  4. 路由字段和模型执行字段异步进入 Langfuse。内容只按脱敏与采样策略保存,fallback、负反馈、shadow 分歧等高价值请求可以强制完整采样。

第一圈结束时,用户已经拿到回答;离线链路是否健康,不影响这次调用完成。

第二圈:离线评测,把证据变成策略

  1. Trace 经过规则挖掘进入候选池,补齐 Oracle 或 Rubric,审核后冻结成不可变数据集版本。
  2. 用户提交的评测不是一句「测一下模型」,而是模型版本、Prompt Pack、数据集版本、评分器版本组成的版本四元组。
  3. control-api 在业务事务中写入运行状态与 Outbox 事件,RabbitMQ 至少一次投递;eval-runtime 用租约、heartbeat、fencing token 和幂等结果抵抗重复投递与 Worker 崩溃。
  4. Inspect AI 执行评测,但模型访问仍经过 LiteLLM;样本级结果写回 MySQL,.eval 日志与大对象进入 MinIO。
  5. 只有成功运行的结果能派生能力标签。标签带样本量、区间、数据集/评分器/运行来源和 fresh/stale 状态,用户不能手工把模型改成「擅长代码」。
  6. 策略候选先离线回放,再 shadow 观察新旧决策分歧,接着 canary 小流量验证质量、成本、延迟与 fallback,达标才转 active。

第二圈结束时,新的策略快照回到 routing-runtime。下一次请求会消费这次请求留下的集体经验。

数据放在哪里,决定系统能不能解释自己

「都存一份」不是可追溯,「每类数据只有一个明确真源」才是。

系统唯一职责明确不承担
MySQL veral_*业务元数据、状态机、版本引用、审计Trace 大对象、临时缓存
LangfuseTrace、Observation、Session、在线 Score路由策略和业务配置真源
MinIO样本内容、环境快照、.eval 日志、导出产物可变业务状态
RabbitMQ任务与领域事件投递最终业务状态
Redis策略缓存、锁、短期进度不可恢复的唯一数据
LiteLLM模型调用执行与网关统计模型能力标签真源

这张表解决了两个常见误区。

第一,消息成功不等于业务成功。运行状态必须先进入 MySQL,再通过 Outbox 投递;消费者重复收到事件,也只能幂等地得到同一个结果。

第二,缓存命中不等于策略存在。routing-runtime 的热路径可以只读内存,但策略的发布历史、来源证据和回滚关系仍由控制面持有;磁盘 last-known-good 是恢复手段,不是治理真源。

为什么不能直接把三个开源项目粘起来

LiteLLM、Langfuse 和 Inspect AI 已经覆盖了大量通用能力。Veral 的价值不是再写一遍,而是补上三者之间没有共同所有者的业务闭环。

开源底座它已经做好什么Veral 必须持有什么
LiteLLM Proxy统一模型协议、Provider 执行、网关能力业务路由决策、模型治理、策略发布
LangfuseLLM Trace 与 Observation 可观测性路由字段、采样规则、Trace 挖掘与项目权限投影
Inspect AITask、Solver、Scorer、评测执行与日志场景产品化、版本引用、运行编排、结果入库

边界规则很简单:第三方 SDK 只存在于 Adapter;领域模型、跨服务契约和前端 API 只看 Veral 自己的 DTO。这样底座升级时,策略状态机、数据血缘和业务权限不需要一起改写。

生产化不是多起几个副本,而是每条变化都有退路

Veral 首期生产基线仍是 Docker Compose,故障域只承诺单台宿主机内的进程与容器,不伪装成跨机房高可用。在这个边界内,Caddy 是唯一入口,无状态服务与 LiteLLM 可以运行双副本,eval-runtime 以两个竞争消费 Worker 执行任务;状态组件依靠持久卷、健康检查、自动重启、逻辑备份和恢复演练获得可恢复性。

策略发布则用状态机把「谨慎」写进系统:

flowchart LR
    DRAFT["draft"] --> OFFLINE["offline verified"]
    OFFLINE --> SHADOW["shadow"]
    SHADOW --> CANARY["canary"]
    CANARY --> ACTIVE["active"]
    ACTIVE --> ARCHIVED["archived"]

    SHADOW -->|越界| ROLLED["rolled back"]
    CANARY -->|越界| ROLLED
    ACTIVE -->|kill switch| ROLLED
    ROLLED --> LKG["last-known-good"]
    LKG --> ROUTE["routing-runtime"]

这里有一个容易混淆的点:副本解决的是进程退出,回滚解决的是错误仍在正常运行。容器全部 healthy,不代表新策略没有把代码任务路由到便宜但不会写代码的模型。前者靠编排恢复,后者必须靠评测、指标门禁和旧策略快照恢复。

理想全貌不等于当前完成状态

本文画的是 Veral 已批准的目标架构,不是把路线图全部涂成绿色。

当前已经有工程治理、四个部署单元、评测目录与运行、Inspect AI、MinIO 产物、LiteLLM/Langfuse 集成及工作台等经过不同层级验证的基础。接下来的关键产品切片仍是把「策略候选 → 发布/激活 → routing-runtime 决策 → 可回滚证据」跑成真实纵向闭环。

shadow、canary、自动回滚、双副本摘除、租约恢复、DLQ、备份恢复演练和完整生产观测属于理想生产形态的验收项。它们写进架构,是为了约束后续实现方向;在验收器给出证据前,不应被描述为已经上线。

这也是架构图应有的诚实:它既要告诉你终点长什么样,也要让你看见哪段桥还没有验收。

带得走的检查表

判断一套「评测 + 路由」系统是不是闭环,不妨只问八个问题:

  1. 每次路由决定能否说出策略版本、候选、理由和 fallback?
  2. Trace 能否经过脱敏、审核和冻结,变成可重复的评测样本?
  3. 一次分数能否追溯到模型、Prompt、数据集和评分器版本?
  4. 能力标签是否只能由评测流水线写入,而不是人工编辑?
  5. control-api、队列、Langfuse 故障时,已加载策略的在线请求能否继续?
  6. 新策略是否必须经过离线验证、shadow 和 canary 才能拿到全部流量?
  7. 越界时能否自动或人工回到 last-known-good,并留下部署证据?
  8. 每类数据是否有唯一真源,消息与缓存是否都可以重建?

八问都能用运行证据回答,平台才不只是画出了一个圆,而是真的闭合了它。

诚实的提醒

本文没有亲手复现任何性能、可用性或模型质量数字,也没有把项目目标状态冒充生产现状。官方资料只用于核对开源底座的职责边界;Veral 特有的服务划分、状态机与数据所有权来自项目批准设计。

最低成本的验证实验是:使用 mock 模型发布一个最小策略,然后发起一次 model: veral/auto 的 OpenAI-compatible 请求。沿同一条链核对 routing decision、LiteLLM 响应、Langfuse Trace、评测 Run、能力标签和 deployment 记录,确认 request、run 与 policy 标识能串起来;随后让新策略触发门禁越界,观察是否回到 last-known-good。这个实验不证明模型聪明,但能证明闭环不是 PPT。

参考来源

工程实践与官方文档

arXiv 论文

本篇讨论工程边界与发布闭环,没有用论文替代项目规格或运行证据,因此不列与主线无直接关系的论文。