LLM 可观测性与评测平台全景指南:Langfuse、LangSmith、Braintrust 怎么选

AI教程22小时前更新 程序员阿超
362 0 0

一、背景:为什么传统监控管不住大模型应用

做过 RAG 问答、Agent 助手的团队都遇到过这类灵异事件:同样的提示词,今天答得好好的,明天换了个模型版本就开始胡说八道;HTTP 状态码全是 200,延迟也正常,但用户投诉回答驴唇不对马嘴;Agent 调用了十几个工具、烧了几千 token,最后交出一个自信满满的错误答案——日志里却看不出任何异常。

传统 APM(应用性能监控)盯的是 CPU、内存、接口延迟、错误率这些”机器指标”。但 LLM 应用的故障大量发生在语义层:检索召回了错误的文档、提示词被截断、工具参数传错了、模型出现了幻觉。这些在 APM 眼里全部”正常”。

LLM 可观测性与评测平台要补的正是这块空白:把 LLM 管线里的每一个 span(提示词、补全、检索、工具调用、token 数、延迟、花费)全部记录下来,再用自动化评估器给输出质量打分。到 2026 年,这类工具已经从”可选项”变成了生产级 AI 的核心基建:LLM 可观测性平台市场规模 2026 年约 26.9 亿美元,预计 2030 年达到 92.6 亿美元;LangChain 对 1300 多名从业者的调查显示,57% 的人已经在生产环境运行 Agent,其中 89% 接入了可观测性,但只有约一半做了离线评测——质量仍是上线最大的拦路虎(32% 受访者这么认为)。

教程带你从零建立完整认知:市场四类玩家怎么选、OpenTelemetry 语义约定怎么落地、CI 回归评测怎么做、线上失败案例如何回流成测试集,最后用 Langfuse 自建方案做一次完整实战。

二、原理:先理解三层能力模型

选型之前必须先理解:所谓”LLM 可观测性平台”,其实是三层能力的组合,不同厂商的强项各不相同。

2.1 第一层:追踪(Tracing)

追踪回答的是”发生了什么”。一次用户提问进来,平台把它拆成一棵 span 树:

trace(一次问答请求)
├── span: 路由/意图识别(输入、输出、延迟)
├── span: 向量检索(query、top-k、召回文档 id、分数)
│   └── span: embedding 调用(模型、token 数)
├── span: 提示词组装(模板版本、最终 prompt 全文)
├── span: LLM 生成(模型、温度、输入输出全文、token、费用)
└── span: 工具调用 × N(函数名、参数、返回值)

关键设计点:

  • 全量记录 prompt 与 completion:没有这两样,线上问题根本无法复现。这是 LLM 追踪和传统链路追踪最大的区别。
  • ** token / 费用归因**:每个 span 挂 token 数,trace 级别汇总成单次请求成本,才能做成本优化。
  • 采样与脱敏:全量记录意味着巨大的存储开销和隐私风险,生产环境一般按比例采样,并对 PII 字段做脱敏或屏蔽。

2.2 第二层:评测(Evaluation)

评测回答的是”答得好不好”。分两种:

  • 离线评测(Offline Eval):准备一个带标准答案的测试集,每次改提示词、换模型、调检索参数后跑一遍,对比指标变化。这是 CI 回归的基础。
  • 在线评测(Online Eval):对线上真实流量抽样打分(LLM-as-a-judge、人工标注、用户点赞/点踩),发现生产环境的新问题。

常用评估器(evaluator)类型:

类型 原理 适用场景
规则/启发式 正则、长度、格式校验、JSON 合法性 格式约束类任务
Embedding 相似度 生成答案与参考答案的向量相似度 开放问答粗筛
LLM-as-a-judge 用强模型按 rubric 打分 语气、相关性、幻觉检测
Agent 轨迹评分 检查工具调用序列是否合理 Agent 任务
人工标注 人审 高风险场景终裁

LLM-as-a-judge 的核心坑是”自利偏见”(judge 偏爱自己家族模型的输出)和分数不稳定,后面”常见坑”一节细讲。

2.3 第三层:生产监控与实验(Monitoring & Experimentation)

  • 质量看板:评分分布、幻觉率、拒答率、token 成本随时间的曲线,模型版本一上线就能看出来有没有变差。
  • 告警:错误率、延迟 p99、费用突增、评分下滑触发通知。
  • 提示词/数据集版本管理:把 prompt 模板、测试集、评估配置都版本化,才能做可复现的对比实验(A/B 两个 prompt 跑同一测试集)。

2.4 市场四类玩家

  1. AI 原生开源派(Langfuse 为代表):从 LLM 场景长出来,trace 数据模型贴合 prompt/completion/retrieval,自建友好,被 ClickHouse 收购后在分析性能上有加持。适合想自主可控、数据不出内网的团队。
  2. 生态绑定派(LangSmith 为代表):与 LangChain/LangGraph 深度集成,在自家生态里埋点几乎零成本,评测与部署流水线顺滑。重度 LangChain 用户首选,但中立性弱一些。
  3. 评测优先派(Braintrust 为代表):从评测切入,数据集管理、批量打分、回归对比体验最好,”先写测试集再上线”理念的团队会很喜欢。
  4. 传统可观测延伸派(Arize 等):从 ML/APM 监控延伸过来,强项是生产指标、漂移检测、告警体系,适合已经有成熟 SRE 体系、要把 LLM 纳入统一监控的大厂。

选型口诀:生态深就用生态绑定的,要自建就用开源的,评测驱动就用评测优先的,已有监控大盘就用传统延伸派。

2.5 OpenTelemetry 语义约定:为什么重要

各家 SDK 字段各异会导致”换平台 = 重埋点”。OpenTelemetry 的 GenAI 语义约定(gen_ai.* 系列属性:gen_ai.systemgen_ai.request.modelgen_ai.usage.input_tokens 等)正在统一 LLM span 的字段命名。遵循它的好处:埋点一次,多后端可接;将来从自建切到商业平台迁移成本低。本教程实战代码会显式使用 OTel 兼容的 span 命名。

三、环境准备

3.1 方案与依赖

实战采用 Langfuse 自建(Docker)+ Python SDK 方案,另配一个最小的 RAG 应用作为被观测对象。需要:

  • Docker 与 Docker Compose(跑 Langfuse + Postgres + ClickHouse)
  • Python 3.10+
  • 一个 OpenAI 兼容的模型 API(OpenAI、DeepSeek、通义千问均可,代码里用环境变量切换)
# 1. 拉起 Langfuse 自建栈
mkdir llm-obs && cd llm-obs
curl -O https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml
docker compose up -d

# 2. 打开 http://localhost:3000,注册账号,创建一个项目,
#    拿到 Public Key / Secret Key

# 3. Python 环境
pip install langfuse openai faiss-cpu tiktoken pandas datasets

3.2 环境变量

export LANGFUSE_PUBLIC_KEY="pk-lf-xxx"
export LANGFUSE_SECRET_KEY="sk-lf-xxx"
export LANGFUSE_HOST="http://localhost:3000"
export LLM_API_KEY="你的模型key"
export LLM_BASE_URL="https://api.openai.com/v1"   # 兼容渠道自行替换
export LLM_MODEL="gpt-4o-mini"

四、分步实战

我们要观测的对象是一个最小 RAG 问答服务:读本地文档 → 切分 → 向量检索 → 组装 prompt → LLM 生成。先让它”裸奔”,再一层层加上追踪、评测、回归、回流。

4.1 步骤一:写一个最小 RAG 应用(被观测对象)

# app.py —— 最小 RAG(后续所有观测都围着它转)
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["LLM_API_KEY"],
                base_url=os.environ.get("LLM_BASE_URL"))

DOCS = [
    ("退货政策", "本店支持7天无理由退货,15天内质量问题免费换新,退款在3个工作日内原路返回。"),
    ("运费规则", "满99元免运费,未满收取6元运费,偏远地区加收8元,新疆西藏不包邮。"),
    ("会员权益", "会员享95折,生日月88折,积分100抵1元,每年赠送2张免邮券。"),
    ("发票说明", "支持电子普通发票与增值税专用发票,开票内容为明细,需下单时备注。"),
]

# 极简检索:关键词命中计分(真实项目换成向量检索,不影响观测方法)
def retrieve(query: str, top_k: int = 2):
    scored = sorted(DOCS, key=lambda d: sum(1 for ch in query if ch in d[1]), reverse=True)
    return [{"title": t, "content": c} for t, c in scored[:top_k]]

def answer(query: str) -> str:
    hits = retrieve(query)
    context = "\n".join(f"【{h['title']}】{h['content']}" for h in hits)
    prompt = f"你是客服助手,只能依据以下资料回答,资料不足时说不知道。\n资料:\n{context}\n\n用户问题:{query}"
    resp = client.chat.completions.create(
        model=os.environ.get("LLM_MODEL", "gpt-4o-mini"),
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return resp.choices[0].message.content

if __name__ == "__main__":
    print(answer("满100块包邮吗?"))

4.2 步骤二:接入 Langfuse 追踪(核心:span 树 + token 费用)

# trace_app.py —— 给 RAG 加上完整追踪
import os, time
from langfuse import Langfuse
from openai import OpenAI
from app import retrieve

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host=os.environ["LANGFUSE_HOST"],
)
client = OpenAI(api_key=os.environ["LLM_API_KEY"],
                base_url=os.environ.get("LLM_BASE_URL"))
MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")

def answer_traced(query: str, session_id: str = "demo") -> str:
    trace = langfuse.trace(name="rag-answer", session_id=session_id,
                           input=query, metadata={"model": MODEL})
    # --- retrieval span(OTel 风格命名) ---
    t0 = time.time()
    hits = retrieve(query)
    trace.span(name="retrieval", input=query,
               output={"hits": hits},
               metadata={"top_k": len(hits),
                         "latency_ms": round((time.time() - t0) * 1000, 1)})
    # --- prompt 组装 span(含模板版本,可复现) ---
    context = "\n".join(f"【{h['title']}】{h['content']}" for h in hits)
    template_v = "prompt/v3"
    prompt = f"你是客服助手,只能依据以下资料回答,资料不足时说不知道。\n资料:\n{context}\n\n用户问题:{query}"
    trace.span(name="prompt.build", input={"template": template_v},
               output={"prompt": prompt})
    # --- LLM 生成 span(记录全文 + token + 费用归因) ---
    gen = trace.generation(name="llm.generate", model=MODEL,
                           model_parameters={"temperature": 0},
                           input=[{"role": "user", "content": prompt}])
    resp = client.chat.completions.create(
        model=MODEL, messages=[{"role": "user", "content": prompt}], temperature=0)
    text = resp.choices[0].message.content
    usage = resp.usage
    gen.end(output=text,
            usage_details={"input": usage.prompt_tokens,
                           "output": usage.completion_tokens,
                           "total": usage.total_tokens})
    trace.update(output=text)
    langfuse.flush()
    return text

if __name__ == "__main__":
    for q in ["满100块包邮吗?", "会员生日有什么优惠?", "能开专票吗?"]:
        print("Q:", q)
        print("A:", answer_traced(q), "\n")

跑完后去 Langfuse 界面验证三件事:每条 trace 能点开看到检索命中了哪两篇文档;generation 里有 prompt 全文、回答全文、token 数;按 session 或时间能汇总出单次问答成本。

4.3 步骤三:定义评估器(规则 + LLM-as-a-judge 双轨)

# evaluators.py —— 可复用的评估器
import os, json
from openai import OpenAI

client = OpenAI(api_key=os.environ["LLM_API_KEY"],
                base_url=os.environ.get("LLM_BASE_URL"))
JUDGE_MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")

def eval_grounded(answer: str, contexts: list[str]) -> dict:
    """规则评估:答案关键断言是否能在检索上下文中找到(极简版接地性检查)。"""
    import re
    nums = re.findall(r"\d+", answer)
    ctx = "".join(contexts)
    unsupported = [n for n in nums if n not in ctx]
    score = 1.0 if not unsupported else max(0.0, 1 - 0.25 * len(unsupported))
    return {"score": score, "unsupported_numbers": unsupported}

JUDGE_PROMPT = """你是问答质量评审。只依据【资料】判断【回答】。
从三个维度打分(0~1)并给出理由,以JSON返回:
{{"faithfulness": x, "relevance": x, "no_hallucination": x, "reason": "..."}}
【资料】:{context}
【用户问题】:{question}
【回答】:{answer}"""

def eval_llm_judge(question: str, answer: str, context: str) -> dict:
    resp = client.chat.completions.create(
        model=JUDGE_MODEL,
        messages=[{"role": "user",
                   "content": JUDGE_PROMPT.format(context=context,
                                                 question=question, answer=answer)}],
        temperature=0, response_format={"type": "json_object"},
    )
    return json.loads(resp.choices[0].message.content)

4.4 步骤四:建测试集 + 跑离线评测(CI 回归的基础)

# offline_eval.py —— 离线评测集与批量打分
from trace_app import answer_traced, langfuse
from app import retrieve
from evaluators import eval_grounded, eval_llm_judge

# 测试集:每个用例含参考答案与判定要点(落地时至少 50~200 条)
DATASET = [
    {"q": "满100块包邮吗?", "ref": "满99元免运费,所以100元包邮。",
     "must_contain": ["99"]},
    {"q": "会员生日有什么优惠?", "ref": "生日月88折。",
     "must_contain": ["88"]},
    {"q": "能开专票吗?", "ref": "支持增值税专用发票。",
     "must_contain": ["专用发票"]},
    {"q": "新疆包邮吗?", "ref": "新疆不包邮,偏远地区加收。",
     "must_contain": ["不包邮", "加收"]},
    {"q": "退款几天到账?", "ref": "3个工作日内原路返回。",
     "must_contain": ["3个工作日"]},
]

def run_eval(dataset_name: str = "rag-baseline-v1"):
    ds = langfuse.create_dataset(name=dataset_name)
    for item in DATASET:
        ds.create_item(input={"q": item["q"]},
                       expected_output={"ref": item["ref"],
                                        "must": item["must_contain"]})
    results = []
    for item in DATASET:
        q = item["q"]
        ans = answer_traced(q, session_id=f"eval-{dataset_name}")
        hits = retrieve(q)
        ctx = "\n".join(h["content"] for h in hits)
        rule = eval_grounded(ans, [h["content"] for h in hits])
        judge = eval_llm_judge(q, ans, ctx)
        passed = all(m in ans for m in item["must_contain"])
        results.append({"q": q, "answer": ans, "rule": rule["score"],
                        "faith": judge["faithfulness"], "passed": passed})
    ok = sum(1 for r in results if r["passed"])
    print(f"通过 {ok}/{len(results)}")
    for r in results:
        print(f"[{'PASS' if r['passed'] else 'FAIL'}] {r['q']} "
              f"rule={r['rule']} faith={r['faith']}")
    return results

if __name__ == "__main__":
    run_eval()

offline_eval.py 接进 CI(GitHub Actions 示例):每次改 prompt 模板或升级模型,流水线自动跑测试集,通过率 < 阈值(比如 0.9) 则阻断合并。这就是”CI 回归”。

# .github/workflows/rag-eval.yml
name: rag-eval
on: [pull_request]
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install langfuse openai
      - run: python offline_eval.py
        env:
          LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
          LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
          LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST }}
          LLM_API_KEY: ${{ secrets.LLM_API_KEY }}

4.5 步骤五:线上失败回流测试集(闭环的核心)

线上用户点”踩”、judge 打低分、人工抽查发现的坏案例,要一键回流成测试集的新用例:

# reflux.py —— 失败案例回流
from langfuse import Langfuse
import os

langfuse = Langfuse(public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
                    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
                    host=os.environ["LANGFUSE_HOST"])

def reflux_bad_cases(score_threshold: float = 0.5,
                     dataset_name: str = "rag-baseline-v1"):
    """把线上低分 trace 回流为测试集用例(示意:按 score 过滤)。"""
    ds = langfuse.get_dataset(dataset_name)
    # 实际做法:查询线上 trace 中 judge 分数 < 阈值或用户点踩的样本,
    # 经人工确认参考答案后追加为新 item
    ds.create_item(
        input={"q": "偏远地区运费到底怎么收?"},   # 线上真实失败问题
        expected_output={"ref": "偏远地区加收8元,新疆西藏不包邮。",
                         "must": ["加收", "不包邮"]},
        metadata={"source": "prod-feedback", "date": "2026-08-10"})
    print("已回流 1 条失败案例,下次 CI 会覆盖它。")

if __name__ == "__main__":
    reflux_bad_cases()

完整闭环就是:线上追踪 → 抽样评分 → 失败人工确认 → 回流测试集 → CI 回归守住下限。每一次线上事故都沉淀为永久的回归用例,测试集越滚越大,模型越换越稳。

4.6 步骤六:成本与质量看板(生产监控)

在 Langfuse 里建两个 dashboard:

  • 质量:faithfulness 均值/分布、拒答率、用户点踩率,按模型版本分组对比;
  • 成本:单请求 token p50/p99、日总费用、检索命中率 vs 生成长度(检索越准,生成越短越便宜)。

告警建议:faithfulness 日均值跌超 0.1 发告警;单日费用超预算 120% 发告警;p99 延迟超阈值发告警。

五、常见坑

  1. 只记元数据不记全文:很多团队为了省存储只记 token 数和延迟,出问题时无法复现。正确做法是全文记录 + 采样 + PII 脱敏,而不是不记。
  2. Judge 用自家小模型:拿被测模型自己当裁判,自利偏见严重。Judge 至少要和被测模型同级或更强,且定期用人审校准 judge 与人的一致性(agreement rate)。
  3. 测试集太小且全是 happy path:5 条用例的回归等于没有。测试集要覆盖:边界数字(99 元门槛)、否定问法(”新疆包邮吗”期望否定回答)、资料缺失(期望说不知道)、多跳组合(”会员在偏远地区买99元包邮吗”)。
  4. 改 prompt 不跑回归:prompt 是代码,改 prompt 就要跑 CI。把 prompt 模板版本化(prompt/v3),每次变更走评测流水线。
  5. 采样率拍脑袋:流量大时全量存 trace 费用爆炸。建议生产按 5%~10% 采样,但错误和低分样本 100% 保留(tail-based sampling)。
  6. 忽略检索质量:RAG 答得差,七成是检索的锅。给 retrieval span 单独打分(召回文档是否包含答案),检索坏了先修检索,别调提示词。
  7. 费用归因缺失:Agent 场景一个请求可能调十几次模型,没有 span 级 token 归因,月底账单一脸懵。每个 generation 必须挂 token 用量。

六、总结

  • LLM 应用的故障在语义层,传统 APM 看不见,必须用”追踪 + 评测 + 监控”三层能力覆盖。
  • 四类平台按需选:生态深用 LangSmith 类,自建用 Langfuse 类,评测驱动用 Braintrust 类,已有 SRE 大盘用 Arize 类。
  • 埋点遵循 OpenTelemetry GenAI 语义,换平台不重埋。
  • 工程闭环四件套:全量追踪 → 双轨评估(规则+judge)→ CI 回归门禁 → 线上失败回流测试集。
  • 从今天起给你的 RAG/ Agent 接上追踪,攒下第一批 trace 和测试集——这是后续一切优化的地基。

参考资料:Langfuse / LangSmith / Braintrust / Arize 官方文档与 LangChain State of Agent Engineering 调查。 点击阅读原文

© 版权声明

相关文章

暂无评论

暂无评论...