Files
PolyGateway/research-wiki/schemas/llm-calls.md
T

12 KiB
Raw Blame History

type, node_id, title, date
type node_id title date
schema schema:llm-calls 表结构: llm_calls(遥测 26 字段) 2026-07-20

表结构: llm_calls(遥测 26 字段)

列定义(冻结,M1 设计 §4.4 / ARCH §7.8)

类型 说明
call_id TEXT PRIMARY KEY 每次尝试独立 UUID;INSERT OR IGNORE 幂等
parent_call_id / session_id TEXT 调用链路(agent step → LLM call)
model / provider / source_name TEXT NOT NULL 溯源;model 由旧 Protocol 的 model_name 更名(VT 迁移 §8)
messages / response / thinking TEXT NOT NULL messages 落库前多模态 part 摘要(与缓存 key 共用 digest_messages)
prompt_tokens / completion_tokens INTEGER NOT NULL usage 帧;帧缺失记 0/0(不编造估值,由 usage_source 标注)
usage_source TEXT NOT NULL measured / estimated / unavailable(2026-07-30 起三态,见下)
latency_ms INTEGER NOT NULL 尝试耗时;缓存命中 0
ttft_ms / max_inter_token_ms REAL 流式活性测量
cache_hit INTEGER NOT NULL DEFAULT 0 命中标记
error TEXT 异常信息;取消记 "cancelled"
cost REAL M2 起 pricing 换算;usage_source='unavailable' 的真实调用行为 NULL(缓存命中行例外,仍为 0.0)
created_at TEXT NOT NULL DEFAULT (datetime('now')) 落库时刻
cached_prompt_tokens INTEGER 供应商 prompt cache 命中的输入 token(2026-07-31,issue #3);NULL = 该源未上报,0 = 上报了真实零命中,两者不可混同
model_reported TEXT API 响应体实际返回的 model;NULL = 未上报。与 model(配置别名)可能分叉
sampling TEXT 本次调用的采样参数 canonical JSON(2026-07-31,issue #4);NULL = 未传。见下方口径
reasoning_tokens INTEGER 推理消耗的输出 token(2026-08-02,issue #6);含在 completion_tokens 内,不影响成本总额,只补归因。NULL = 本次调用未上报
tenant_id TEXT NOT NULL DEFAULT '' 调用方租户(2026-08-17,issue #11);缺省落哨兵空串而非 NULL——PG 的 RLS USING 对返回 NULL 的行一律隐藏且不报错,NULL 的租户不是「未归属」而是对所有人永久不可见
meta TEXT / JSONB NOT NULL DEFAULT '' / '{}' 调用方自定义维度(同批,≤16 个 KV);SQLite 存 canonical JSON 串,PG 存 JSONB
thinking_observation TEXT 本次推理是否真的发生的三态裁定(2026-08-25,issue #16/#17);observed / absent / unknown。见下方口径
reasoning_effort TEXT 按四种行来源记录的推理意图/实际编码档位(2026-09-09 澄清,issue #20/#26);八档 Effort 字面量之一,NULL = 调用方未表态(与 none「明确要求不推理」不可混同)。见下方口径

usage/成本口径(2026-07-30,est_tokens 解耦)

usage_source 含义 生产者 cost
measured usage 帧完整可信 正常路径;OCR 成功行(0 token 是事实) 按 token 换算
estimated 有实测数字但可信度降级 打捞路径(收到 usage 帧但流被截断) 按 token 换算
unavailable 用量信息不可得 usage 帧缺失、失败尝试、终态失败 NULL

SUM(cost) 天然跳过 NULL,故账单汇总不再被虚构的估值污染;账目缺口的度量口径固定为 WHERE usage_source = 'unavailable' AND cache_hit = falsecache_hit 限定不可省:缓存命中行未产生新调用,cost 是事实上的 0.0 而非未知,本无账目缺口,漏掉该条件会让缺口度量偏高。

供应商 prompt cache 口径(2026-07-31,issue #3)

新增两列排在 created_at 之后——旧表只能经 ALTER TABLE ADD COLUMN 追加到末尾,DDL 里若插在前面,新建库与升级库的物理列序会分叉(列序断言无合规修法)。两个后端在初始化期幂等补列:CREATE TABLE IF NOT EXISTS 不会给旧表加列,不补则每行写入被逐行 warning 丢弃、遥测静默全失。两侧都先探测缺列再 ALTER(ADD COLUMN IF NOT EXISTS 即使列已存在也先取 ACCESS EXCLUSIVE 锁,遥测是内联 await,锁共享审计表会拖垮业务调用),且补列失败只降级为逐行丢弃,绝不让 recorder 整体失能——两侧纪律必须对称。

cache_hitPolyGateway 自身响应缓存,与供应商 prompt cache 是两回事。缓存命中行的这两列是原样回放的历史值(与 model/prompt_tokens 同一口径),故命中率度量口径固定为:

SELECT SUM(cached_prompt_tokens)::float / NULLIF(SUM(prompt_tokens), 0)
FROM llm_calls WHERE cache_hit = false AND cached_prompt_tokens IS NOT NULL;

WHERE cache_hit = false 不可省,理由与上面 cost 缺口口径同源:回放行计入即重复计数。

采样参数口径(2026-07-31,issue #4)

reasoning_tokens 的 NULL 语义与 cached_prompt_tokens 不同: 后者的 NULL 是"该源不报这个数",前者只能读作"本次调用未上报"——中转在上游不返回 usage 时会用本地 tokenizer 补算并整体替换 usage 对象,把 completion_tokens_details 一并吃掉(实测同一请求 10 轮呈 6:4 双峰)。故当时的统计口径是 IS NULL OR = 0 才算"未推理",写 = 0 的条件永远不成立——实测三家供应商在未推理时都是整个 details 缺失,无人上报字面 0不可用 completion_tokens 反推是否推理: 两档的输出长度分布重叠(关闭档实测最高 46,开启档最低 13)。

该口径 2026-08-25 作废(issue #16/#17): 供应商可能整体停报 completion_tokens_details(MiniMax 这一路实测已停),此时 NULL 只意味着「没上报」而非「没推理」——同一次调用里库拿得到 185 字符推理正文。统计一律改按新列 thinking_observation 分组,见下方「推理观测口径」。

sampling 列 = 「调用方采样意图 ⊎ 生效源 extra_body」的 canonical JSON,空则 NULL。不含结构化输出注入的 response_format——列名是采样参数,schema 不是,且数 KB schema 逐行落库会让审计表无谓膨胀。补列纪律与 issue #3 两列逐字相同(排在末尾、先探测再 ALTER、失败只逐行降级)。

三个 emit 入口的取值必须各自定死,否则同一列在不同行含义不同:

入口 调用者 有生效源? 记什么
emit_attempt RetryMW(最内) merge(source.extra_body, request.sampling)
emit_cache_hit TelemetryMW(最外) request.sampling
emit_terminal_failure TelemetryMW request.sampling

后两行缺 extra_body 是客观事实而非口径瑕疵——它们没有"生效源"可言,与 model/source_name 在终态行置空是同一先例;缓存命中行亦无损:sampling 已进缓存 key,能命中即意味调用级参数与历史那次逐字相同。三者统一读 request.sampling 而非 request.overlay(后者在 RetryMW 处已被结构化注入污染、在 TelemetryMW 处未被污染,直接用必然三行分叉)。

OCR / embedding 路径的该列恒为 NULL:两条路径的 transport 不发 extra_body(embed payload 硬编码 {model, input}、MonkeyOCR 只发 multipart),故其源在构造期就被剥离——不剥离则该列会记录一个从未发出的参数,那是数据造假而非参数失效。

复现某批实验的解码条件:

SELECT DISTINCT sampling FROM llm_calls
WHERE session_id = $1 AND cache_hit = false AND error IS NULL;

推理观测口径(2026-08-25,issue #16/#17)

thinking_observation响应侧的裁定结果,不是请求侧的声明: 推理正文(thinking)非空即 observed(正文是事实本身,压倒 usage 明细这一转述);正文空而 reasoning_tokens > 0observed;reasoning_tokens == 0absent(上游明确上报未推理);两个信号双缺为 unknown

unknown 不得并进「未推理」。它是本列存在的全部理由: MiniMax 这一路上游 2026-08-25 起不再返回 completion_tokens_details,reasoning_tokens 因此恒 NULL,而同一次调用里库拿得到 185 字符推理正文——旧口径 reasoning_tokens IS NULL OR = 0 会把这类调用统计成「没推理」。该旧口径自本版起作废,统计一律按本列分组。M3 非流式档更极端: 推理已计费(completion 53 vs 关闭档 3)却不回传正文,该档只能是 unknown,任何把它读成「没推理」的报表都在撒谎。

按模型看各观测态占比,用于发现某模型从哪天起观测不到推理:

SELECT model,
       thinking_observation,
       count(*)                                                          AS calls,
       round(100.0 * count(*) / sum(count(*)) OVER (PARTITION BY model), 1) AS pct
FROM llm_calls
WHERE cache_hit = false AND error IS NULL
  AND created_at >= now() - interval '7 days'
GROUP BY model, thinking_observation
ORDER BY model, calls DESC;

三条限定各有理由: cache_hit = falsecost/cached_prompt_tokens 同源——缓存命中行原样回放历史观测值,计入即重复计数;error IS NULL 排除失败尝试与终态失败行,那些行的本列恒为 unknown(无响应可裁定,默认值本身不撒谎),混进来会把「观测不到」的占比整体抬高;时间窗是为了让变化可见——某模型的 unknown 占比从 0 跳到 100%,正是它停报推理信号的那一天。补列之前写入的历史行本列为 NULL,与 unknown 是两回事(前者是那时还没有这一列),跨版本对比须显式区分。

推理档位口径(2026-09-04,issue #20)

reasoning_effort 的来源取决于行类型,不能总括为「实际发出」——补列之前,25 列里没有任何一列答得出,于是「不同档位是不是真有用」在数据侧无从分组。NULL 有两个来源(调用方未表态 / 档位名读不懂),两者都不可折叠进 none:none 是一次「要求不推理」的表态。

三个 emit 入口的取值同样各自定死,与 sampling 同构:

入口 有生效源? 记什么
emit_attempt(成功) response.applied_effort——transport 裁定的实发档
emit_attempt(失败) effective_effort(请求级 > 源级 > enable_thinking)请求档
emit_cache_hit 本次 request.reasoning_effort,不取历史 applied、不推源级
emit_terminal_failure 本次 request.reasoning_effort,可能尚未选源

真实成功尝试必须读实际编码档而非重算(分析须同时排除 cache_hit 和 error;未知 AUTO 仅尽力,不证明上游推理,raw-only=NULL: 源上开了 EFFORT_FALLBACK=nearest 时请求 medium 而模型只有 low/high/max,实发的是 low,重算会把整行挂在一个从未发出过的档下。失败尝试没有响应,实发档无从得知,故退回请求档——于是开了映射的源上成功行与失败行不是同一把尺子,跨 error IS NULL 混合统计前必须显式分开。仍然记而不留空,是因为档位错误(resolve_thinking 的 Phase 2/4/5)根本没发 HTTP 就被拒,这类行记的正是被拒绝的那一档,而「哪一档配错了」正是排障要的信号。

OCR / embedding 路径的该列恒为 NULL(emit_attempt(reasoning_applies=False)),理由与 sampling 逐字相同: 两条路径的 payload 不带推理参数,源上即便误配了 ENABLE_THINKING,记一个档也是记录一个从未发出的参数。

按档位看推理产出,即压测「高档是不是真的多想」的基本查询:

SELECT model, reasoning_effort,
       count(*) AS calls,
       round(avg(reasoning_tokens)) AS avg_reasoning_tokens
FROM llm_calls
WHERE cache_hit = false AND error IS NULL AND reasoning_effort IS NOT NULL
GROUP BY model, reasoning_effort
ORDER BY model, calls DESC;

埋点位置(单一 helper 铁律)

  • middleware/telemetry.py::TelemetryEmitter 是全库唯一 record_llm_call 调用点;
  • TelemetryMW(最外层)记缓存命中与最终失败;RetryMW._emit 经同一 Emitter 逐次记尝试;
  • 写失败降级 warning 不冒泡;取消路径 finally 尽力记录。

评估基线

首版无历史基线,标"待首次运行后建立";验收断言: 单测覆盖成功/失败/缓存命中/取消四路径各产生恰一行;并发 50 协程写全落库。

1.3.4 测试侧证据(不新增 schema)

tests/live_evidence.pye2e conftest 只在内存保存完整错误体与独立非流式身份,逐轮 Markdown 白名单输出到 tests/outputs/134/live/;凭据、Authorization、提示词、原始异常/响应均不落报告。生产数据仍经 TelemetryEmitter。评估复用 metric:call-telemetry-coverage,实际 live 覆盖基线待首次执行。