数据规格书:把「网关产 Trace,Trace 产评测集,评测产标签」落到字段级

施工图篇的下一层细化:四段数据链每段一份可直接建表的规格——五块式 Trace schema(贴 OTel GenAI 属性名 + 路由决策扩展 + 三级采样)、评测集生产漏斗(八类挖掘信号、样本封闭化、审核状态机、版本化)、两种标签的生产口径(Wilson 区间 + 最小样本量 + 失效规则)、路由器偏好对的 SQL 导出与冷启动,以及贯穿全链的「字段三问」数据契约定律。

施工图篇的元规律是一条数据依赖链:网关产 trace,trace 产评测集,评测产标签,标签产路由器。那篇回答了模块怎么切,这篇只做一件事:把链上每个「产」字拆成一份可以直接建表的规格书——字段、类型、写入方、消费方。检验一份 schema 的标准不是「字段全不全」,而是每个字段都能回答三问:谁写入、谁消费、缺了会断哪一段闭环。回答不了三问的字段是装饰,回答得了的字段就是数据契约。

写在最前:这篇的所有 schema 是设计方案,不是行业标准(行业目前没有标准)。字段名可以改,但每个字段背后的「三问」答案改不掉——那才是本体。文中出现的具体数值(采样比例、样本量门槛、漏斗衰减)除标明来源者外均为设计值,需要在你自己的流量分布上调。

一、四段链传的是什么:一条请求的四次形态变化

先把全链的数据血缘画出来。四段链传递的其实是同一条请求在四种形态之间的变换,血缘靠主键逐段传递:request_idsource_trace_idsample_id → 偏好对,任何一段断了主键,追溯和归因就到此为止。

flowchart LR
    T["Trace<br>主键 request_id"] -->|"挖掘规则"| S["评测样本<br>sample_id<br>带 source_trace_id"]
    S -->|"nightly 评测"| R["评测结果<br>run_id × sample_id"]
    R -->|"按模型聚合"| L1["能力标签<br>model × task_class"]
    R -->|"按样本配对"| L2["偏好对<br>sample × 模型对"]
    L1 -->|"L2 语义路由查表"| RT["路由器<br>策略版本 vN"]
    L2 -->|"L3 预测器训练"| RT
    RT -->|"线上决策写回"| T
形态变换触发节奏产出物主键对应模块
① 网关产 trace请求 → 结构化证据在线实时(异步落库)trace_id / request_idM1+M5
② trace 产评测集证据 → 可重放的题周度批处理 + 人工审核sample_idM6
③ 评测产标签题的得分 → 两种标签nightlyrun_id + 标签行M7+M8
④ 标签产路由器标签 → 训练数据 → 模型月度或按数据量触发训练集版本 + 模型 hashM3+M4

注意②③④的节奏都比①慢一个数量级以上——这条链天然是「在线快、离线慢」的,schema 设计要允许慢的一侧异步消费快的一侧,而不是反过来拖住在线路径。

二、规格一:Trace——网关产 trace

2.1 设计骨架:五块分治

一条 trace 按「写入方、消费方、存储成本、可变性」四个维度切成五块。这个切法不是美学,是因为五块的生命周期完全不同:身份块和决策块便宜且不可变,全量存;内容块贵,采样存;反馈块在 trace 落库之后才陆续到达,必须设计成可追加。

写入方主要消费方存储成本可变性
A 身份与关联M1 网关全链血缘极低不可变
B 路由决策M3 路由引擎可解释查询、错路由回放不可变
C 执行M1 网关成本账本、延迟监控、能力标签的生产观测不可变
D 内容M1 网关(经脱敏管道)评测集挖掘、反事实回放不可变
E 反馈业务端 SDK / 抽样 judge评测集挖掘、路由器训练信号追加式

2.2 A 身份与关联块

字段类型说明
trace_idstring观测系统主键(Langfuse/OTel 语境下的 trace)
request_idstring全链血缘主键,响应头里也带给业务方
session_idstring?多轮会话分组,对应 OTel 的 gen_ai.conversation.id
tenant / api_key_idstring成本归集与预算管控的分母
app_idstring哪个业务应用发起,评测集按业务分层的依据
tstimestamp请求时间
sampling_tierenummetadata / full,见 2.7

设计要点:身份块存内部 ID,不存终端用户的原始标识——PII 处理放在入库前的管道里做,事后清洗等于没洗(备份、下游副本都已经带毒)。

2.3 B 路由决策块(自定义扩展)

这一块 OTel 没有对应约定,是自研 schema 的核心,即施工图篇 M3 那条决策 JSON 的落库形态:

字段类型说明
policy_versionstring决策时生效的策略版本,回滚与归因的锚点
decision_levelenumL1-rule / L2-semantic / L3-model
task_classstring语义分类结果,评测集分层的主维度
task_class_confidencefloat分类置信度,低置信样本是类目体系的改进素材
candidates[]array各候选的 {model, score, est_cost},错路由回放的对照组来源
chosenstring最终决定
reasonstring人话理由,「可解释」的最后一公里
fallback_chain[]array预先算好的降级链
routing_latency_msint路由自身开销,必须摊在明处
shadow[]array?影子策略的平行决策 {policy_version, chosen},只记录不生效

三问示范(以 candidates[] 为例):写入方是路由引擎;消费方是 M9 的反事实回放——「路由器没选的那个模型」是谁、当时估分多少,全靠它;缺了它,错路由率就没有测量对象。

2.4 C 执行块(贴 OTel GenAI 属性名)

执行块尽量贴 OpenTelemetry GenAI 语义约定的属性名(该约定已迁移到独立仓库维护,截至本文写作整体状态仍是 Development,属性名可能变,所以自己这层留一次字段映射,别把标准名刻进下游表结构——这一条施工图篇提醒过,这里再钉一次):

字段(OTel 属性名)类型说明
gen_ai.provider.namestring提供方(OTel 标 Required)
gen_ai.request.modelstring请求的模型名
gen_ai.response.modelstring实际应答的模型版本,与上一项不同时说明发生了 fallback 或别名解析
gen_ai.usage.input_tokens / output_tokensint用量,成本的分子
gen_ai.usage.cache_read.input_tokensint?缓存命中 token,算真实成本必需
gen_ai.response.finish_reasons[]array终止原因,截断类故障的信号
gen_ai.response.time_to_first_chunkdoubleTTFT
error.typestring?错误分类
server.addressstring命中的副本/端点

外加三个 OTel 没有定义、必须自己算的字段:cost_total(网关按价目表折算,OTel 约定里没有成本属性)、latency_e2e_msfallback_hops(实际走了几跳降级链)。

2.5 D 内容块(采样存储)

字段类型说明
input_uri / output_uriuri完整输入输出存对象存储,trace 表只存引用
gen_ai.system_instructionsprompt_name+prompt_versionstring系统提示词用版本号引用而不是全文重复存(OTel 有 gen_ai.prompt.name / gen_ai.prompt.version 属性)
tool_definitions_hashstring工具定义快照的指纹,重放时按 hash 取当时的定义
tool_calls[]array?工具调用往返(gen_ai.tool.call.arguments / result

内容与元数据分离存储是成本结构决定的:元数据每条 KB 级可以全量永久存,内容每条可达 MB 级只能采样存。分离之后,「先查元数据锁定可疑请求、再取内容细看」也是自然的排障动线。

2.6 E 反馈块(迟到的字段)

反馈的本质是迟到的质量信号:trace 落库后几秒到几天才陆续到达,所以 schema 上是追加式事件列表,不是 trace 的固定列。

事件类型类别信号强度
thumbs_down / thumbs_up / rating显式强,但稀疏(多数用户不点)
regenerate隐式强负信号:用户亲手宣判「这答案不行」
human_takeover隐式强负信号(Agent 场景:人接管了)
edit_distance隐式用户对输出改了多少才用,弱但密
judge_score抽样线上 LLM-as-judge 抽检打分(评分纪律见 judge 笔记

2.7 采样策略:分级 + 强制条款

存什么比例
metadataA+B+C+E 块100%(便宜,全量)
full加上 D 内容块基线随机 1–10%(设计值),外加强制条款

强制升级为 full 的条件清单(命中任何一条就全量存,因为这些正是评测集最想要的样本):触发了 fallback;收到显式负反馈或 regenerate;shadow 决策与生效决策不一致;成本进入当日 top 分位;judge 抽检低分。采样策略的本质是「为第②段链预留原料」——如果最有价值的坏例只存了元数据,评测集挖掘就只能对着骨架叹气。

2.8 一条完整 trace 示例

{
  "trace_id": "tr-01J8Q...", "request_id": "r-8f3a",
  "session_id": "conv-77e1", "tenant": "team-gamedata",
  "app_id": "npc-dialog-bot", "ts": "2026-08-09T10:23:41+08:00",
  "sampling_tier": "full",
  "routing": {
    "policy_version": "routing-policy-v13",
    "decision_level": "L2-semantic",
    "task_class": "code_gen", "task_class_confidence": 0.91,
    "candidates": [
      {"model": "qwen3-32b", "score": 0.71, "est_cost": 0.003},
      {"model": "strong-api-x", "score": 0.88, "est_cost": 0.041}
    ],
    "chosen": "qwen3-32b",
    "reason": "cheapest above class threshold 0.65",
    "fallback_chain": ["strong-api-x"],
    "routing_latency_ms": 14,
    "shadow": [{"policy_version": "v14", "chosen": "strong-api-x"}]
  },
  "execution": {
    "gen_ai.provider.name": "self-hosted-vllm",
    "gen_ai.request.model": "qwen3-32b",
    "gen_ai.response.model": "qwen3-32b-sft-0801",
    "gen_ai.usage.input_tokens": 1834, "gen_ai.usage.output_tokens": 512,
    "gen_ai.response.finish_reasons": ["stop"],
    "gen_ai.response.time_to_first_chunk": 0.41,
    "latency_e2e_ms": 3120, "fallback_hops": 0,
    "error.type": null, "cost_total": 0.0031
  },
  "content_ref": {
    "input_uri": "obj://traces/2026-08-09/r-8f3a/in.json",
    "output_uri": "obj://traces/2026-08-09/r-8f3a/out.json",
    "prompt_name": "npc-dialog", "prompt_version": "3.2.0",
    "tool_definitions_hash": "sha256:9c1f..."
  },
  "feedback": [
    {"ts": "2026-08-09T10:25:02+08:00", "type": "regenerate"}
  ]
}

落库载体直接用 Langfuse(其数据模型 trace / observation / session 与上述结构天然对齐,且本身构建在 OpenTelemetry 之上,SDK 异步批量上报);这篇讲的是往里面填什么

三、规格二:评测集——trace 产评测集

3.1 一个漏斗,不是一次收集

「从 trace 建评测集」不是导出一批数据,而是一个每周转一轮的漏斗——每一层的进入条件都是明文规则(下方数量级为设计值示意):

flowchart TD
    A["全量 Trace<br>十万至百万级/天"] -->|"八类信号触发"| B["候选池<br>千级/周"]
    B -->|"去重 + 脱敏 + 封闭化"| C["审核队列<br>百级/周"]
    C -->|"审核通过"| D["金集增量<br>约 20-50 条/周"]
    C -->|"驳回"| X["丢弃或回炉"]
    D --> E["数据集新版本<br>不可变快照"]

3.2 挖掘规则表:八类入池信号

信号(查 trace 哪个字段)优先级为什么值得成为一道题
fallback 触发(fallback_hops > 0)模型或容量边界的第一现场
显式负反馈(thumbs_down用户亲自标注的失败
regenerate / human_takeover隐式失败,比点踩密集得多
shadow 分歧(shadow[].chosenchosen新旧策略打架处 = 路由决策边界样本,训练价值最高
judge 抽检低分无用户反馈时的兜底质量信号
高成本 top 分位降本潜力最大的请求形态
长尾类目补采(按 task_class 分布)防止金集偏科——失败样本天然偏向难题
业务方提报领域知识的入口,不经漏斗直接进审核队列

前四类全部依赖规格一里的对应字段——这就是「schema 即契约」的具体含义:第②段的挖掘规则,逐条消费第①段的字段。

3.3 封闭化:从「现场记录」到「可重放的题」

这是整段链最容易被低估的工程环节。线上 trace 是开放系统的切片——它依赖当时的工具返回、检索结果、外部状态;评测样本必须是封闭系统,任何时候重放都得到可比的结果。封闭化清单:

  • 快照依赖:工具定义按 tool_definitions_hash 取当时版本;RAG 场景把检索结果冻结进样本(除非你评的就是检索)。
  • 截断会话:多轮对话截到评测点,之前的轮次成为固定上下文。
  • 冻结时钟:涉及「今天/最新」的题写死评测时钟,否则题目会随日历自然腐烂。
  • 可执行类样本带环境:代码题记录环境镜像指纹,重放不靠缘分(SWE-bench 式做法,见拆解笔记)。
  • 脱敏:内容过 PII 管道,业务专名按映射表替换(保语义、去实体)。

3.4 样本 schema

sample_id: smp-000482
source_trace_id: tr-01J8Q...      # 血缘:随时可回溯线上现场
task_class: code_gen
input:
  messages: [...]                 # 封闭化后的输入
  tools: [...]                    # 工具定义快照(非 hash 引用,样本自包含)
env_snapshot: img-sha256:ab31...  # 可执行类样本的环境指纹
oracle_type: exec                 # exec / rule / judge / human
oracle_spec:
  test_cmd: "pytest tests/smp_000482_test.py"
rubric_id: null                   # oracle_type=judge 时指向 rubric 版本
difficulty: hard                  # 审核人初标,之后由多模型通过率反推校正
status: golden                    # 见 3.5 状态机
added_in_version: regset-v0.9.0
review:
  reviewer: lei
  reviewed_at: 2026-08-05
  notes: "原 trace 需求含糊,审核时补写了验收条件"

两个关键字段的三问:

  • oracle_type:写入方是审核人,消费方是评分器分派逻辑。它强制每道题在入集时就回答「怎么判分」——能执行验证就绝不用 judge(Oracle 梯度定律)。缺了它,评分方式就由跑评测的人临场决定,分数从此不可比。
  • source_trace_id:缺了它,样本成为「孤儿题」——出了争议无法回看线上现场,也无法统计「金集覆盖了线上分布的百分之几」。

3.5 审核状态机与版本化

stateDiagram-v2
    [*] --> candidate: 挖掘规则命中
    candidate --> reviewed: 人工审核通过
    candidate --> [*]: 驳回
    reviewed --> golden: 编入数据集版本
    golden --> retired: 泄漏/过时/分布漂移
    retired --> [*]

审核动作有明确定义:改写含糊的输入、补写判分标准(oracle_spec 或 rubric)、初标难度。人工时间只花在这里——这正是施工图篇 M7 说的「人工评审只用于金集建设」。

版本化规则三条:数据集版本是不可变快照(发布即冻结,修题=出新版本);每版带 changelog(加了哪些题、退了哪些题、为什么);评测结果永远记录自己是针对哪个数据集版本跑的。没有这三条,「上周 85% 这周 82%」就永远分不清是模型退了还是题变难了——归因树的「数据问题」分支直接残废。

四、规格三:标签——评测产标签

4.1 一次评测运行,两路标签产物

「评测产标签」里的「标签」其实是两种东西,混淆它们是这条链上最常见的设计事故:

  • 模型级能力标签(聚合级):model × task_class → pass_rate,写回模型注册表,供 L2 语义路由查表
  • 请求级结果标签(样本级):(sample, model) → score,累积成 L3 预测器的训练数据

同一次 nightly run 同时产出两种——第一章血缘图里 R 节点分出的两条边就是它们。上游 schema 相同,下游消费方、聚合粒度、失效规则完全不同。

4.2 评测运行记录:四元组是主键的一部分

run_id: run-2026-08-09-nightly
quad:                              # 四元组:缺一个,归因就少一个可排除项
  model: qwen3-32b-sft-0801
  prompt_pack: npc-dialog@3.2.0
  dataset: regset-v0.9.0
  scorer: scorer-pack@1.4.0        # 内含 judge 模型指纹与 rubric 版本
started_at: 2026-08-09T02:00:00+08:00
results:
  - {sample_id: smp-000482, score: 1.0, oracle_type: exec, cost: 0.004}
  - {sample_id: smp-000483, score: 0.0, oracle_type: judge, cost: 0.011}

4.3 能力标签的计算口径:点估计必须带不确定度

能力标签如果只存一个 0.80,路由器就会把 10 道题上的 0.80 和 300 道题上的 0.80 当同一个数。这不是理论洁癖——用 Wilson 区间算一遍就知道差距有多大(数字已用脚本验算):

通过 / 总数pass rateWilson 95% 区间区间宽度
8 / 100.800[0.490, 0.943]0.453
24 / 300.800[0.627, 0.905]0.278
240 / 3000.800[0.751, 0.841]0.090

同一个 0.8:n=10 时真实水平可能低到 0.49,n=300 时才收窄到 ±0.05。Wilson 区间的公式(选它而不是正态近似,因为小样本、极端通过率下正态近似会失真甚至越界):

center=p^+z22n1+z2n,half=zp^(1p^)n+z24n21+z2n\text{center} = \frac{\hat p + \frac{z^2}{2n}}{1+\frac{z^2}{n}}, \qquad \text{half} = \frac{z\sqrt{\frac{\hat p(1-\hat p)}{n}+\frac{z^2}{4n^2}}}{1+\frac{z^2}{n}}

于是能力标签的 schema 长这样:

model_id: qwen3-32b-sft-0801       # 不可变坐标:模型一变即是新行
task_class: code_gen
pass_rate: 0.80
n: 300
wilson_95: [0.751, 0.841]
eval_run: run-2026-08-09-nightly   # 出处,可解释性的锚
dataset_version: regset-v0.9.0
status: fresh                      # fresh / stale

更新规则n 低于门槛(设计值 30)不更新标签,保留旧值并标 stale;路由引擎对 stale 标签保守处理(提高该类目走强模型的比例)。宁可承认不知道,不可假装知道。

4.4 标签失效规则:三种上游变更,三种处置

上游变更处置为什么
模型权重/版本变model_id 换新值,旧标签自然失效(新行,不是更新)标签是「这个版本的模型」的属性,不是「这个名字」的属性
scorer 版本变历史分数标记不可比,能力标签全量重算换了裁判,旧比分不能和新比分同场——裁判先过校准集再上岗
dataset 大版本变新旧版本分数不混合聚合题变了,分数的分母含义变了

五、规格四:训练数据——标签产路由器

5.1 两种训练格式,对应两种路由器

  • 偏好对格式(query, model_strong, model_weak, winner)RouteLLM 的路线——其开源实现提供五种路由器(mf 矩阵分解、bert 分类器、causal_llmsw_ranking 加权 Elo、random 基线),前几种都训练在偏好数据上(细节我在源码深读拆过)。
  • 难度/胜率标签格式query → P(便宜模型也能过)Hybrid LLM 的路线,BERT 量级分类器。

两种格式都能从 4.1 的请求级标签表机械导出——这就是「标签产路由器」四个字的全部内容:

-- 从请求级标签表导出偏好对(同一样本、同一评分口径下,两模型的分数对比)
SELECT a.sample_id,
       'strong-api-x'   AS model_strong,
       'qwen3-32b'      AS model_weak,
       CASE WHEN a.score > b.score THEN 'strong_win'
            WHEN a.score < b.score THEN 'weak_win'
            ELSE 'tie' END AS label
FROM per_request_labels a
JOIN per_request_labels b USING (sample_id, dataset_version)
WHERE a.model_id = 'strong-api-x'
  AND b.model_id = 'qwen3-32b'
  AND a.scorer_version = b.scorer_version;   -- 口径一致才可比,否则训练数据带毒

注意 WHERE 里的最后一行:它就是 4.4 失效规则在导出时的体现。跨评分器版本的分数硬凑成偏好对,路由器会学到「裁判的偏差」而不是「模型的差距」。

5.2 冷启动:公开偏好数据打底,内部标签逐步替换

内部标签积累到能训练需要几个月,冷启动直接用 RouteLLM 论文的路子:其路由器训练于公开人类偏好数据(Chatbot Arena)加数据增强,论文报告质量不降的情况下成本降低超过 2 倍,开源仓库同时把训好的模型和数据集放在 Hugging Face 上,可直接下载校准使用。之后随内部偏好对积累,逐步替换公开数据——替换的价值在于分布蒸馏定律说路由器是评测器的在线蒸馏,蒸馏数据越贴近你的真实任务分布,路由器越准。公开数据是别人家的分布,内部标签才是你的。

5.3 训练数据与模型都进版本闭环

训练集打版本号(源自哪些 run_id、哪个导出 SQL 版本);训出的路由器以模型 hash 形式进入 M4 的策略版本对象。这样灰度发布时序图里回滚一个策略版本,连带回滚的就包括路由器本身——回滚才是完整的。

六、总表:字段三问 × 全链关键字段

把四份规格书压缩成一张表——每行是一个「缺了会断闭环」的字段。建设时可以当 checklist 用:逐行检查你的系统里这个字段存在吗、写入方对吗、消费方接上了吗。

字段写入方消费方缺了断哪段
request_idM1 网关全链血缘血缘全断,样本无法回溯现场
policy_version + reasonM3 路由引擎可解释查询、回滚「为什么选它」变考古
candidates[]M3 路由引擎错路由反事实回放错路由率失去测量对象
shadow[]M1/M3挖掘规则、灰度评估决策边界样本消失
sampling_tier + 强制条款M5 采集器评测集挖掘最有价值的坏例只剩骨架
feedback[] 事件业务端 SDK挖掘规则、训练信号隐式质量信号消失
source_trace_idM6 挖掘管道争议回溯、覆盖率统计金集变孤儿题集
oracle_type + oracle_spec审核人评分器分派评分方式临场化,分数不可比
dataset_version(不可变)M6四元组、归因树模型退了还是题变了,无解
四元组(model/prompt/dataset/scorer)M8归因树四个分支归因失去排除法
n + wilson_95标签流水线路由阈值比较n=10 的 0.8 冒充 n=300 的 0.8
capability_eval_run标签流水线决策→标签→评测的追溯链标签变黑箱
scorer_version 一致性约束导出 SQL偏好对质量路由器学会裁判偏差
路由器模型 hash训练管道M4 策略版本回滚不含路由器,回滚不完整

本篇元规律(数据契约定律):闭环系统的实现难度不在模块内部,而在段与段之间的数据契约;契约的最小单位是字段,每个字段用三问验收——谁写入、谁消费、缺了断哪段。四张字段表一旦写清,「建设一套路由评测系统」就退化为「建表、填表、按节奏调度」。反过来说:一个回答不了三问的字段,无论多常见,都可以从 schema 里删掉。

接上系列的递进:施工图篇说模块的施工顺序由数据依赖决定,这篇再往下一层——数据依赖的实体就是字段,闭环不是架构图上的箭头,是一条条字段的接力。箭头断没断,看图看不出来;字段有没有人消费,查表查得出来。

诚实的提醒

按本站惯例声明证据边界:

  • 核实过的来源:OTel GenAI 属性名(gen_ai.provider.namegen_ai.usage.input_tokensgen_ai.response.time_to_first_chunk 等)逐个查自其官方仓库文档,整体状态 Development,属性名未来可能变;Langfuse 数据模型(trace/observation/session、基于 OpenTelemetry)查自官方文档;RouteLLM 五种路由器与偏好数据训练方式查自其 GitHub 与论文摘要(「成本降低超过 2 倍」为论文自报,我未复现)。
  • 亲手验算:Wilson 区间三组数字用脚本算过;公式本身是标准统计结果。
  • 设计值(未验证):采样比例 1–10%、样本量门槛 n≥30、漏斗各层数量级、周度/月度节奏——全部需要在你的流量分布上重调。五块 trace 结构、八类挖掘信号、样本状态机、标签失效三规则,均为我的设计方案,行业无统一标准。
  • OTel 成本字段的缺失是我检索属性清单后的观察(清单里确无 cost 属性),如未来版本补充,以官方为准。

成本最低的亲手验证实验(半天,纸上或脚本皆可):取你自己最近的 30 条真实 LLM 使用记录,手工把这条链走一遍——① 每条填一份最小 trace(身份+决策+执行三块,字段照 2.8 的 JSON 删减);② 用 3.2 的规则挑 10 条进「金集」,每条写清 oracle_type 和判分标准;③ 找两个模型各答一遍,得到请求级标签表;④ 算两模型的 pass rate 和 Wilson 区间(n=10 时看看区间有多宽——你会亲手体会 4.3 那张表);⑤ 用 5.1 的 SQL 逻辑(手工也行)导出 10 条偏好对。做完这五步,四份规格书的每个字段你都亲手写过一遍。

参考来源

工程实践 / 官方文档

arXiv 论文

本站相关旧文