跳到主要内容

可观测与线上评测

离线 JSONL 证明「这天评测集过了」;线上 trace 证明这一次请求发生了什么。离线脚本骨架见 工具调用与评测实践;面试答法见 AI 应用测试与效果评估;单请求成本怎么算见 Token 计算与成本;验收指标要能从本篇字段算出来,见 FDE 交付闭环实践。

一次请求的最小字段​

轨迹与评测共用同一 schema,避免「评测一套、线上一套对不上」。

字段含义
request_id一次用户请求(可含多步、多次 LLM)
feature产品功能:如 order_qa
user_id脱敏后的主体(哈希或内部号,不是明文邮箱)
thread_id会话 / LangGraph thread
step本请求内第几步
model实际调用的模型名
tool工具名;无则空
ok本步是否成功
prompt_tokens / completion_tokens本步 LLM 实计
latency_ms本步耗时
retrieved_idsRAG chunk id 列表
outcome请求级:success / reject / handoff / error / cancelled
{"request_id":"r1","feature":"order_qa","user_id":"u_9f3","thread_id":"t1","step":1,"model":"mock","tool":null,"ok":true,"prompt_tokens":410,"completion_tokens":28,"latency_ms":320,"retrieved_ids":["p-12"],"outcome":null}
{"request_id":"r1","feature":"order_qa","user_id":"u_9f3","thread_id":"t1","step":2,"model":"mock","tool":"get_order","ok":true,"prompt_tokens":480,"completion_tokens":12,"latency_ms":90,"retrieved_ids":[],"outcome":null}
{"request_id":"r1","feature":"order_qa","user_id":"u_9f3","thread_id":"t1","step":3,"model":"mock","tool":null,"ok":true,"prompt_tokens":520,"completion_tokens":64,"latency_ms":410,"retrieved_ids":["p-12"],"outcome":"success"}

请求结束那一行才写 outcome。人审等待中不要提前标 success。取消、流式中断的取值见 流式取消与人审。

平台用 Langfuse、LangSmith 还是自建 APM 不重要;字段对不齐,换产品也救不了排障。

版本四元组​

效果漂移时先问「改了哪一版」,再调参。

维度记什么
prompt模板 id + hash
tools工具 schema 的 hash(裁剪后的那一份)
index检索索引 / 数据快照版本
model模型名 + 日期(同一名字会变)

四元组任一变更:必须跑对照评测集(工具评测篇的 JSONL),再决定发版。CI 分层仍见测试评估篇:Mock 单测 → smoke → nightly。

线上怎么看​

手段口径
Trace默认全量;高压 feature 可按比例采样,但 error / handoff / cancelled 建议全留
效果抽样1%~5% 人工或规则断言;开放题再上判分模型
用户信号转人工率、点踩、同一问题重复问
告警工具失败率升、单请求 token 异常、拒答率骤降(可能闸门被绕过)

离线评「能不能发版」;线上评「有没有漂移」。两者都要,测试评估篇已讲答法,本篇只钉字段。

看板四类(能从字段算)​

看板怎么算给谁
任务成功率outcome=success / 结束请求数(分母不含纯 cancelled 可视产品定)产品 / FDE 验收
平均步数每 request_id 的 max(step)工程(死循环、工具抖)
P95 延迟请求级 Σ latency_ms 或首 token 另记体验
单请求成本Σ token × 单价,算法见 Token 篇FinOps

FDE 验收表里的「20 条集通过率」应对同一 feature 的离线集;上线后看同一 feature 的成功率是否掉出约定带。对不上字段,就无法向客户复盘。

# board.py — 从 JSONL 算四类看板(只看带 outcome 的结束行)
import json
from collections import defaultdict


def board(path: str, feature: str) -> dict:
req_steps, req_tokens, req_lat, outcomes = defaultdict(int), defaultdict(int), defaultdict(int), {}
for line in open(path, encoding="utf-8"):
r = json.loads(line)
if r.get("feature") != feature:
continue
rid = r["request_id"]
req_steps[rid] = max(req_steps[rid], r["step"])
req_tokens[rid] += (r.get("prompt_tokens") or 0) + (r.get("completion_tokens") or 0)
req_lat[rid] += r.get("latency_ms") or 0
if r.get("outcome"):
outcomes[rid] = r["outcome"]
ended = [rid for rid, o in outcomes.items() if o != "cancelled"]
ok = sum(1 for rid in ended if outcomes[rid] == "success")
lats = sorted(req_lat[rid] for rid in ended)
p95 = lats[int(len(lats) * 0.95)] if lats else 0
return {
"success_rate": (ok / len(ended)) if ended else 0,
"avg_steps": (sum(req_steps[rid] for rid in ended) / len(ended)) if ended else 0,
"p95_ms": p95,
"avg_tokens": (sum(req_tokens[rid] for rid in ended) / len(ended)) if ended else 0,
}

单价把 avg_tokens 换成钱,仍走 Token 篇;本函数只负责从字段聚合。

和离线评测怎么接​

同一 schema
├─ 离线:eval_set.jsonl 驱动 Agent,写 trace,算通过率
└─ 线上:真实流量写 trace,抽样打标,回流进评测集

线上 bad case(outcome=error 或人工点踩)要能一键变成离线用例——至少带上 retrieved_ids、tool、脱敏后的输入。不要只存最终字符串。

常见坑​

  1. 只 log 最后一轮 LLM: Agent 成本与步数全部失真(Token 篇已警告)。
  2. 明文 PII 进 trace: 安全篇的泄漏面。
  3. 评测字段和线上字段不一致: 发版通过、线上却对不齐。
  4. 用产品名代替字段设计: 「我们上了 Langfuse」不等于能回答失败在哪一步。
  5. cancelled 算失败: 用户点停止不是模型崩了;分母要单独看。

实践要点​

  1. 先定最小字段表,离线与线上共用。
  2. 请求级 outcome 只在结束时写;人审中、取消另算。
  3. prompt / tools / index / model 四元组变更必跑对照集。
  4. 看板只选四类能从字段算出的:成功率、步数、P95、成本。
  5. 面试挂钩:离线证明发版、线上证明漂移;trace 全量(或错误全留)+ 效果抽样。