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

17 KiB
Raw Permalink Blame History

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

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

列定义(冻结,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「明确要求不推理」不可混同)。见下方口径
scope TEXT 池名(2026-09-09,issue #19/#23);Emitter 构造期注入,三类行都带,不拿 source_name 顶替
operation TEXT chat / embed / recognize_text / parse_layout / parse_document(2026-10-22,#34),由调用点给定;与 PolyGatewayError.operation 是两个语义,链路上不得读后者填本列
logical_call_id TEXT 一次逻辑调用一个 ID(重试/换源/重问/分批共享);上下文缺席落 NULL,不造 ID
event_kind TEXT 三态 attempt / cache_hit / terminal_failure——三类行的唯一机械判据。见下方口径
http_status_code INTEGER 失败 attempt 行的实收状态码(中转改写过就记改写后的,不猜回原值);成功行与终态行恒 NULL,且 200 也可能是失败行(MonkeyOCR success != true)
error_type TEXT 该行自身错误的类名;取消路径传字符串故为 NULL
cause_type TEXT __cause__ 的类名(如 ReadTimeout)——httpx 超时类的 str() 为空,只靠 error 列分不出是哪种超时;仅失败 attempt 行非空
error_body TEXT 网关响应正文摘要(summarize_body 上限,不在 PGW_TELEMETRY_TEXT_CAP 覆盖面内);仅失败 attempt 行非空
attempts INTEGER 该逻辑调用真实打出去的尝试次数(免预算 429 也计);只属终态行
total_latency_ms INTEGER 该逻辑调用的总墙钟(含缓存 IO、退避、准入等待、重问);只属终态行,与该行 latency_ms 同取一份冻结快照

三类行与失败归因口径(2026-09-09,issue #19/#23)

遥测行不再只有"一次尝试"一种。event_kind 三态同时决定其余列的取值面:

event_kind 什么时候写 列取值
attempt 每次真实尝试(含失败与取消) 失败时诊断四列可非空;attempts/total_latency_ms NULL
cache_hit 缓存命中 诊断四列与快照两列全 NULL
terminal_failure 每次领域失败的整池终态,每逻辑调用至多一条 attempts/total_latency_ms 非空;http_status_code/cause_type/error_body 恒 NULL;cost NULL、usage_source='unavailable'、token 0

两条不变量: 每次领域失败至多一条终态行(I3,claim_terminal() 去重);非领域异常(编程错)零条终态行、原样传播(I4)。

终态行三列恒 NULL 是红线: 把最后一次 attempt 的状态码与正文搬上来,等于拿最后一个源冒充整池归因。逐源现场由同一 logical_call_id 的 attempt 行给出:

SELECT event_kind, source_name, http_status_code, error_type, cause_type, error, error_body
FROM llm_calls WHERE logical_call_id = :lcid ORDER BY created_at;

下游口径迁移四条: ① 计失败调用改 WHERE event_kind = 'terminal_failure';② error IS NOT NULL 不再是失败调用判据(跨两类行);③ AVG(latency_ms) 须按 event_kind 分组(终态行是整个逻辑调用的总耗时);④ 费用口径不变(终态行 cost 恒 NULL)。

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 = false。cache_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_hit 指 PolyGateway 自身响应缓存,与供应商 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 > 0 亦 observed;reasoning_tokens == 0 为 absent(上游明确上报未推理);两个信号双缺为 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 = false 与 cost/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.py/e2e conftest 只在内存保存完整错误体与独立非流式身份,逐轮 Markdown 白名单输出到 tests/outputs/134/live/;凭据、Authorization、提示词、原始异常/响应均不落报告。生产数据仍经 TelemetryEmitter。评估复用 metric:call-telemetry-coverage,实际 live 覆盖基线待首次执行。

#29 复用声明(2026-10-01)

OpenAI 保真没有新增遥测表/列;仍经 RetryMW._emit → TelemetryEmitter。 error 帧是失败 attempt,正文与思考仍为兼容投影,不新增工具全文列。 本页原标题/表格停留 36 字段;实际 telemetry/schema.py 已含 stall_ms/stall_count 共 38 个 INSERT 字段(§7.8 已登记)。本次不改 DDL,真实定义以源码为准。

1.3.10 stream_end 列(#31,2026-10-14)

llm_calls 末尾追加可空 stream_end TEXT:INSERT 字段 38 → 39,物理列(含 created_at)39 → 40; 上文"36 字段"标题为历史口径,以 telemetry/schema.py 的 COLUMNS 为准。

值 含义 行
done 收到 [DONE] 流式成功 attempt
eof_after_finish 无 DONE,finish + 其后实测 usage + 干净 EOF 流式成功 attempt
salvaged 文本 missing_done=salvage 打捞 流式成功 attempt
NULL 非流式 / 缓存命中 / 失败 / 取消 / 终态行,或补列前旧行 —

来源: emitter 经 _AttemptUsage.of(response) 读 LLMResponse.stream_end(不走 emit_attempt kw); 命中与终态入口显式 None。评估复用 metric:call-telemetry-coverage;新增观察口径 SELECT stream_end, count(*) FROM llm_calls WHERE event_kind='attempt' GROUP BY 1 统计中转不发 DONE 的比例。