跳到主内容

一文搞懂 LLM 可观测性:Tracing、评测集与线上质量回归体系搭建 ​

LLM 应用上线三个月后,老板在例会上问:「回答质量怎么样?」我们手里只有 token 消耗报表和调用成功率——两者都是 99.9%。但用户反馈里明明有「答非所问」「引用了过期的制度」。传统 APM 只能看到「这次调用没报错、耗时 2.3 秒」,看不到「这次回答是幻觉」。LLM 应用的「错误」形态变了:检索没召回、提示词被改坏、模型升级后格式跑偏,这些都是返回 200 的失败。我们需要一套专门的可观测体系,这篇文章记录我们从零搭建的完整过程。

三件套:Tracing、评测集、回归门禁 ​

三类方案的能力边界差异很大:

方案能回答的问题局限
只看调用日志与 token 报表调了多少次、花了多少钱无法定位质量问题的根因环节
结构化 Tracing问题出在检索、提示组装还是模型本身只能事后排查,无法防回归
Tracing + 评测集 + CI 回归每次 prompt/模型变更质量是升是降需要持续维护评测集

三者是递进关系:Tracing 让问题可定位,评测集让质量可量化,回归门禁让变更有底线。缺了任何一环,「越改越差但不自知」只是时间问题。

先说 Tracing 的字段设计,这是整个体系的骨架。每个 span 除了输入输出摘要与耗时,我们还固定记录四个字段:环节标识(retrieve / prompt_build / llm_call / postprocess)、模型名称与版本、token 消耗、业务标签(会话类型、用户部门)。这四个字段决定了事后能不能按「哪个模型版本、哪类会话」切片分析。多数团队不是没有数据,而是切片维度没设计好——trace 存了一堆,复盘时却回答不了任何具体问题。

实操:从埋点到回归门禁 ​

第一步,Tracing 埋点:把一次请求拆成 span 树。 一次 RAG 问答至少包含四个环节:检索、提示组装、模型调用、后处理,每个环节记录输入摘要、输出摘要、耗时与 token 数。用装饰器做轻量埋点,不侵入业务逻辑:

python
import time, json, functools, uuid
from contextlib import contextmanager

# trace 上下文:贯穿一次请求的全部 span
_trace_ctx = {}

@contextmanager
def trace_request(user_id: str):
    """请求级 trace:生成全局 trace_id,会话内所有 span 共享"""
    _trace_ctx.clear()
    _trace_ctx["trace_id"] = uuid.uuid4().hex[:12]
    _trace_ctx["user_id"] = user_id
    yield

def span(name: str):
    """环节级 span 装饰器:记录输入摘要、输出摘要、耗时"""
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            start = time.time()
            result = fn(*args, **kwargs)
            # 只记录摘要而非全文,控制存储成本与敏感信息风险
            record = {
                "trace_id": _trace_ctx.get("trace_id"), "span": name,
                "latency_ms": round((time.time() - start) * 1000),
                "input_digest": str(args)[:200], "output_digest": str(result)[:200],
            }
            print(f"[trace] {json.dumps(record, ensure_ascii=False)}")
            return result
        return wrapper
    return deco

@span("retrieve")
def retrieve(query): ...

@span("llm_call")
def llm_call(prompt): ...

真实项目里可以直接采用 Langfuse、OpenLLMetry 这类现成方案,但埋点的粒度设计(哪四个环节、记录什么字段)无论用什么工具都一样。线上按 1%~10% 采样常规流量、错误请求 100% 记录,是成本与排障能力的平衡点。流式接口还要单独埋一个「首 token 延迟」的 span,它是最影响体感的指标,聚合报表里必须单列。

第二步,评测集:把质量变成一个数字。 从线上 badcase 和高频场景中冻结一批评测样本,格式用 JSONL:

json
{"id": "qa-001", "question": "年假未休完能否折现?", "must_contain": ["折现", "离职清算"], "must_not": ["不知道"], "source_doc": "hr-leave-v3"}
python
def evaluate(eval_file: str, answer_fn) -> dict:
    """离线评测:逐条跑答案,统计命中关键词的比例与延迟"""
    passed, latencies = 0, []
    for line in open(eval_file, encoding="utf-8"):
        case = json.loads(line)
        t0 = time.time()
        ans = answer_fn(case["question"])
        latencies.append(time.time() - t0)
        # 判定规则:必须包含的关键点全部命中,且不触发禁止词
        ok = all(k in ans for k in case["must_contain"]) and not any(k in ans for k in case["must_not"])
        passed += ok
    total = sum(1 for _ in open(eval_file, encoding="utf-8"))
    p95 = sorted(latencies)[int(len(latencies) * 0.95) - 1]
    return {"score": passed / total, "p95_latency": round(p95, 2)}

主观质量(语气、条理)用 LLM-as-Judge 打分,但 judge 模型要与被评模型不同源,并定期用人工标注校准一致性。

评测跑分之后还有关键一步:建立基线与告警阈值。我们把当前版本的评测分数写进受版本管理的基线文件,CI 门禁的规则是「分数不低于基线的 0.85」,每日定时评测发现分数较基线下滑超过 3% 就在群里告警——前者守住变更底线,后者捕捉环境漂移,比如上游模型悄悄升级、语料过期导致的静默劣化。这类劣化没有任何报错,只有评测分数能第一时间抓住。

第三步,接入 CI 做回归门禁。 每次 prompt、模型版本、检索参数的变更都跑一遍评测集,分数低于基线阈值就拦截合并:

yaml
# .github/workflows/llm-eval.yml(节选)
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - name: 评测集回归,分数低于基线则失败
        run: python scripts/evaluate.py --eval eval/golden.jsonl --min-score 0.85

工具与流程之外,可观测性最终要落成人的习惯。我们每周开一次 30 分钟的质量例会:过一遍本周 badcase 的 trace 归因分布(检索问题几条、格式问题几条、幻觉几条)、评测分数走势,以及下周要修的前三名问题,会议产出直接对应评测集的更新与提示词调整。会上还会同步告警处理的时效统计,确保没有人对告警麻木。这个例会让「质量」从一次性的上线验收,变成持续的运营过程——这也是 LLM 应用与传统应用在运维上最本质的区别。

避坑总结 ​

  1. trace 里存了完整明文 prompt:提示词里常带用户数据与内部文档原文,全量落库等于自建泄露源。记录摘要 + 哈希,敏感字段脱敏后再进日志。
  2. 用同一个模型给自己当裁判:同源 judge 会系统性偏好自己的输出风格,分数虚高。judge 换更强或不同系的模型,并每月抽样人工标注做一致性校准。
  3. 评测集一劳永逸:业务在变,固定的评测集会慢慢失去代表性。建立 badcase 回灌机制——线上用户点踩与人工修正的案例每周进入评测集,评测集本身版本化管理。
  4. 全量记录 trace 不做采样:高频场景下存储成本反噬。常规流量采样 1%~10%,错误与慢请求全量保留,既保住排障能力又控制成本。
  5. 只盯平均延迟:LLM 应用的延迟分布是长尾,平均值掩盖了流式中断与首 token 卡顿。要盯 P95/P99 延迟、首 token 时间和流式中断率这三个用户体感指标。

最后补充两个容易被当作「锦上添花」而漏掉的指标:引用命中率与用户修正率。前者统计回答里给出的引用是否真的支持结论,直接对应幻觉问题;后者统计用户对答案点踩或手动改写的比例,是用户用脚投票的真实质量信号。这两个指标都不需要复杂的技术投入,却是向管理层汇报时最有说服力的两个数字。

可观测性的本质是把「模型行为」翻译成「工程语言」:Tracing 负责定位,评测集负责量化,回归门禁负责守住底线。这套体系搭起来之后,「回答质量怎么样」就从一个感受问题变成了一个可以拉出报表、可以设阈值、可以在变更前拦截的问题。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。