☰
大模型应用可观测与评测闭环:Langfuse+LangSmith+OTel 实践
2026/10/17 8:17:36 网站建设 项目流程

做 LLM 应用的人,大概都经历过这种时刻:线上用户反馈"最近的回答质量明显变差了",但翻遍后端日志,HTTP 状态码全是 200,错误率没有波动,P99 延迟甚至还在下降。代码没改、数据库没动、模型版本没换——问题到底出在哪?传统软件的可观测性方法论在这里完全失灵,因为大模型应用的输出是概率性的,一次极小的 Prompt 措辞调整、一次上下文顺序变化,都可能让模型在用户看不到的地方悄悄"变心"。

我跟团队花了大概三周,把这个问题从"感觉"变成了"数据"。落地方案是用 Langfuse + LangSmith 结合 OpenTelemetry(后面统一叫 OTel),搭了一套大模型可观测与评测闭环,并在这套闭环上完整跑完了一次 Prompt A/B 评测迭代。内部给这次迭代的编号是 24.8,所以标题沿用了这个代号。如果你正在做 RAG 问答、Agent 编排这类 LLM 应用,又纠结"线上 trace 怎么管""Prompt 改没改对""评测指标怎么建",这篇应该对你有用。下面我会先解释为什么闭环是必须的,再讲工具分工、OTel 接入方式,最后用一次真实的 A/B 迭代复盘收尾。

1. 先说清楚:大模型应用的可观测,到底在观测什么

1.1 传统监控为什么在 LLM 应用上失灵

传统后端的监控体系,核心对象是"确定性系统":同一个请求进来,代码路径是确定的,行为和输出是确定的。所以 QPS、错误率、延迟、CPU、内存这些指标能精确刻画系统健康度。但 LLM 应用不一样,它是概率系统——同一个 Prompt、同一套参数、同一段上下文,多次调用的输出都可能漂移。更要命的是,这种漂移往往不会反映在 HTTP 状态码上,接口照样 200,延迟也没有异常,但返回内容已经错了。

我见过最典型的案例:某个问答应用上线后,用户投诉"答非所问",团队排查了两天,最后发现是检索层的一个排序权重参数被某次配置更新悄悄改掉了,导致召回的文档顺序变化,LLM 生成的答案跟着漂了。这类问题用传统监控根本抓不住,因为你没有把"检索到了什么""模型看到了什么""最终答了什么"这三个关键环节记录下来。

所以 LLM 应用的可观测性,粒度必须下沉到"一次思考过程"级别:输入是什么、检索命中哪些内容、上下文怎么拼的、模型怎么答的、用了多少 token、花了多少钱、用户是否满意。这一整条链路,在可观测性领域就叫 trace。

1.2 一条 LLM trace 里应该装什么

我梳理下来,至少四类 span 是必要的。第一类是入口业务 span,记录用户 ID、问题原文、Prompt 版本号;第二类是检索 span,记录 Query 改写结果、召回文档列表、每篇文档的得分和排序;第三类是 LLM 调用 span,记录模型名、温度、max_tokens、实际输入输出、token 用量、延迟;第四类是输出处理 span,记录格式校验、后处理逻辑、最终回复。

这里我想特别强调两个字段,它们是后面做评测闭环的地基:一个是prompt_version,另一个是trace_id。没有版本号,线上 trace 就没法和实验数据对应,A/B 分组就成了空谈;没有 trace_id,评测分数就没法回写,离线发现的问题也没法反查线上的完整现场。这两个字段从第一天就埋进去,比后面补要省事得多。

大致结构可以参考这张表:

Span 层级关键数据解决的问题
入口业务user_id、question、prompt_version、session_id用户级归因、A/B 分流
检索query、retrieved_docs、scores、doc_ids检索质量归因
LLM 调用model、temperature、input/output、usage、latency模型行为、成本、延迟分析
输出后处理format_check、post_process、final_answer输出合规与链路完整性

1.3 可观测和评测为什么必须长成一个闭环

很多人把追踪(tracing)和评测(evaluation)当成两件事:追踪是运维视角,评测是算法视角。但在 LLM 应用里,这两件事不打通,价值都会大打折扣。

光有追踪没有评测,日志里全是"模型输出了什么",但没人告诉你"这个输出好不好"。几百个 trace 摆在 Langfuse 里,你根本不知道今天的数据质量比上周是好了还是差了。光有评测没有追踪,你只知道评测集上的分数变化,但不知道线上真实流量下表现如何,也不知道低分案例背后的检索、上下文、模型调用到底是哪一步出了问题。

闭环的意思就一句话:线上产生的 trace 是评测集的来源,评测分数要写回 trace,评出来的问题要能反查到完整现场,修复后的新版本再灰度、再追踪、再评测。这个循环转起来之后,Prompt 迭代就不再是"拍脑袋改一版,看感觉行不行",而是每个改动都有数据背书。

2. 工具选型:Langfuse、LangSmith、OTel 各自负责哪一段

2.1 三者定位对照

先给一个直观的对照表,是我实际用下来的感受,不是官方定位照抄:

工具强项弱项在我这套方案里的角色
Langfuse可自托管、生产追踪成熟、支持 OTel 接入、有 Prompt 版本管理离线评测和实验管理偏弱生产环境 trace 底座
LangSmithdataset 管理成熟、experiment/评测工作流完整、LLM-as-judge 生态好数据在平台侧,自托管不方便开发期离线评测跑分
OTel厂商中立标准、一套插桩多处复用、不绑定 UI本身没有应用层能力,需要配合后端统一数据管道和埋点规范

先说 Langfuse。我选它的核心原因是自托管。生产环境的 trace 里有一堆用户问题和业务数据,直接打到外部平台,合规这关就过不去。Langfuse 开源、支持私有化部署,能接 OTLP,意味着我可以把数据管道标准化之后,再落入一个可控的存储和展示层。

再说 LangSmith。它的 dataset 和 experiment 工作流是我用过的工具里最顺手的,尤其是"同一批评测数据、跑两个 Prompt 版本、逐条对比得分"这种场景,LangSmith 把流程封装得很干净。离线评测、指标设计和结果对比,我主要在这边完成。

OTel 则是中间那根管子。我不希望所有埋点代码和 Langfuse 的 SDK 深度绑定,万一以后要换后端、或者要同时往多个平台投递数据,就得重新插桩。用 OTel 做一层标准化采集,上层应用只对 OTel 负责,底层接什么平台随时可换。

2.2 为什么不是二选一,而是双轨并行

我知道很多人会问:Langfuse 和 LangSmith 功能明明重叠,为什么两个都用?这里的关键是"开发期"和"生产期"的需求不一样。

开发期,我高频在跑评测实验,需要快速建数据集、跑 experiment、对比多个 Prompt 版本的分差,LangSmith 这套交互是最顺的。生产期,我面对的是大量真实用户请求,需要长时间保留 trace、稳定接收 OTLP 数据、支持按版本号和评测分数反查,Langfuse 自托管更合适。两边通过同一套 Prompt 版本号和同一批评测集打通,数据语义是一致的。

换句话说,LangSmith 是"试验场",负责回答"这个 Prompt 版本到底行不行";Langfuse 是"生产线",负责回答"这个版本在生产环境表现稳不稳"。两者服务同一个目标,但生命周期不同。

2.3 整个数据流向

我用文字描述一下整体数据流,避免画图,但逻辑很清楚:

  • 开发、评测环境:LangChain 或自定义链路直接接 LangSmith,建立 dataset,跑 evaluate,得到各 Prompt 版本的离线分数。
  • 生产环境:应用内部用 OTel API 埋点,生成统一的 trace 和 span,通过 OTLP 协议导出到自托管的 Langfuse。
  • 评测结果回流:离线评测产出的分数、在线规则校验产出的分数,都以 span attribute 的形式写回 Langfuse,让 trace 自带质量标签。
  • 决策流:从 Langfuse 里筛出低分 trace,定位问题环节,修改 Prompt 或检索策略,进入下一轮 LangSmith 评测。

这套架构跑通之后,最大的感受是:工具之间的边界不是按"谁的功能更强"来划分的,而是按"哪个阶段更需要谁"来划分的。

3. 打通 OTel 管道:让 Langfuse 收到标准化的 LLM Trace

3.1 LLM 场景下的 OTel 语义约定

OTel 本身是一套通用标准,传统场景里 span 上打的是 HTTP method、status code、URL 这类属性。到了 LLM 场景,需要一套新的属性约定,业界目前在推进 gen_ai 相关的语义约定(semantic conventions)。我当时落地时,约定还没有完全冻结,不同 SDK 的实现也有出入,所以我做了一件比较笨但很稳的事:不依赖框架的自动插桩,而是在应用层自己包了一层统一的埋点封装。

这套封装只做三件事:统一读取模型名、温度、token 用量;统一生成 LLM 调用的 span 属性;统一处理输入输出的截断和脱敏。核心字段大致长这样:gen_ai.request.model、gen_ai.request.temperature、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens。再加上业务自定义字段,比如app.prompt_version、app.user_id、app.session_id。

这么做的原因是,自动插桩省事,但不同框架版本、不同模型服务商,span 属性命名可能不一致,到了 Langfuse 里会出现同一个概念在不同 trace 里字段对不上的情况。自己做一层,虽然多点工作量,但数据口径统一,后面做统计和筛选才靠谱。

3.2 最小可用的接入代码

下面这一段是接入的核心配置,基于当时的 OTel Python SDK,换成其他语言思路类似:

import os from opentelemetry import trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor provider = TracerProvider() exporter = OTLPSpanExporter( endpoint=os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"], headers={"Authorization": f"Bearer {os.environ['LANGFUSE_PUBLIC_KEY']}"}, ) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider)

生产环境一定要用 BatchSpanProcessor,不要用 Simple 模式,否则每条 span 都同步发送,延迟和性能都会出问题。endpoint 指向你部署的 Langfuse 实例提供的 OTLP 接收地址,认证信息按实例要求配置即可。

业务里埋点长这样,我简化了关键逻辑:

tracer = trace.get_tracer("app.qa_agent") def answer_question(user_id: str, question: str, prompt_version: str) -> str: with tracer.start_as_current_span("qa_agent.answer") as root: root.set_attribute("app.user_id", user_id) root.set_attribute("app.prompt_version", prompt_version) root.set_attribute("app.question", truncate(question, 512)) with tracer.start_as_current_span("llm.call") as llm_span: llm_span.set_attribute("gen_ai.request.model", "gpt-4o-mini") llm_span.set_attribute("gen_ai.request.temperature", 0.3) response = call_model(prompt, question) llm_span.set_attribute("gen_ai.usage.input_tokens", usage.prompt_tokens) llm_span.set_attribute("gen_ai.usage.output_tokens", usage.completion_tokens) root.set_attribute("eval.format_check", format_score) return response

注意两个细节:问题原文只截断前 512 字符,避免 PII 大范围落库;评测分数通过在线规则校验实时写入根 span,比如格式合规率这类硬指标,不需要等离线评测。

3.3 采样、脱敏与成本控制

可观测性不是采样越多越好。我把采样策略分成三层:入口业务 span 全量记录,因为它是排查的入口;LLM 调用 span 按用户会话采样,比如同一用户每小时最多记录 20 次完整 prompt 和输出;检索文档详情只记录 doc_id 和得分,不把检索文档全文塞进 span。

脱敏是另一个容易翻车的地方。我观察到很多人直接把用户输入原样打进 span,后面做数据合规审查时完全没法交代。我的做法是增加一个sanitize函数,在写入 span 前对输入输出做一遍检测,手机号、地址、证件号等敏感信息一律替换成占位符。Tracing 系统里的数据,默认都应该当作"会被别人看到的日志"来对待,这个心态很重要。成本控制上,还可以给 trace 设置保存周期,比如生产 trace 保留 30 天,评测集和标注数据永久保留,避免存储成本无上限增长。

4. Prompt A/B 评测:从线上数据到离线评估的完整回路

4.1 评测集怎么构建

这是整套闭环里我踩过最多坑的环节。最初的评测集是团队手工攒的几十条"经典难题",跑分挺好看,但完全反映不了线上真实情况。后来我把构建评测集的逻辑改成:从生产 trace 里抽样。

抽样要分层,不能随机抽完就完。我按问题类型、用户来源、时段三个维度做分层,比如商品咨询、订单问题、售后投诉各抽一部分,保证评测集能覆盖线上主要流量分布。抽完之后,安排人工标注,给每条测试数据写标准答案或者关键事实点。第一批我们标了 200 条,后来又扩到 500 条。这活儿确实费人工,但评测集的底子决定了上面所有评测指标的可信度,这步偷懒,后面全是镜花水月。

还要注意评测集的"保鲜期"。线上用户的问题分布会随时间变化,一个新活动上线,问题类型可能就变了。我现在的习惯是每个月从最新的生产 trace 里补抽一批数据,重新标注,替换掉评测集里已经过时的样本,保持评测集和线上分布对齐。

4.2 评测指标:硬规则和 LLM-as-judge 组合

指标我分成两类。一类是硬指标,用代码和规则就能判定的:格式合规率、是否包含禁止词、答案里引用的 doc_id 是否来自检索结果、拒绝回答的情况是否合理。这类指标稳定性高,不会漂移,适合做门槛检查。

另一类是语义指标,比如"回答是否忠实于检索内容""事实一致性怎么样""和标准答案是否等价",这些指标靠规则写不出来,只能用 LLM-as-judge。具体做法是写一个评测 Prompt,让模型当裁判,对回答逐项打分。这里有几个经验:

  • Judge 的输入必须包括:用户问题、检索到的关键事实、被评答案、评分标准。
  • 评分标准要尽量具体,比如"如果答案中出现知识库中不存在的信息,扣 30% 分数",而不是笼统的"是否相关"。
  • 可以跑 3 次 judge,取平均分或中位数,降低概率性波动。
  • Judge 模型和被测模型最好不同源,减少偏好干扰。

我常用的几组指标:

指标评价方适用场景
格式合规率规则代码结构约束严格的输出
事实一致性LLM-as-judgeRAG 问答、客服
忠实度LLM-as-judge + 规则判断答案是否基于检索内容
拒绝合理性规则 + 人工抽检知识库覆盖不全的场景
用户点踩率线上埋点生产环境长期监控

4.3 A/B 分流和实验设计

离线评测通过后,我会上线做小流量 A/B 验证。分流按用户 ID 做一致性哈希,保证同一个用户始终看到同一个 Prompt 版本,避免用户体验的割裂感。初期灰度比例通常是 10% 用户走新版本,90% 走旧版本。

实验周期要看样本量。按我的经验,如果两个版本在一个核心指标上预期差 5 个百分点,一组要积累到至少几百条有效数据,才能让结论稳定。所以灰度期一般拉满一周,如果业务流量本身不大,我会把比例提到 20% 到 30%,但绝不会直接 50% 对半分,因为一旦新版本有明显问题,影响面太大。

灰度期除了看离线评测设定的指标,还要同步盯线上业务指标,比如用户点踩率、转人工率、平均回答长度、token 成本。这些指标才是业务真正关心的,评测分数只是中间观测值。

4.4 评测结果写回 trace,形成闭环

A/B 跑完之后,我会把离线评测产生的结果以 attribute 的形式写回对应 trace。举个例子,某条线上 trace 在离线评测里拿到了事实一致性 0.7 分,我会给这条 trace 打上eval.factual_consistency=0.7的标签。这样在 Langfuse 里就能直接搜索"所有事实一致性低于 0.6 的生产 trace",并逐条展开看现场。

这个动作看起来不起眼,但它是闭环真正的关键。一旦评测分数成为 trace 的一个可筛选属性,你就能做很多以前做不到的事:按版本、按分桶、按时段统计质量分布;低分样本一键跳转到完整链路;定位到是检索召回为空、还是模型编造、还是 Prompt 指令被忽略。后面第 5 节的一次真实迭代复盘,就是靠这个能力把问题归因到了检索层。

5. 一次真实迭代复盘:Prompt 版本 24.8 的 A/B 全过程

5.1 问题背景

这次迭代对应一个某零售业务的客服问答助手(模拟项目场景)。线上版本是 Prompt A,已经在跑,但用户投诉里出现了一个高频问题:"AI 会一本正经地编造知识库里不存在的政策,导致用户按错误的售后流程操作。"

我们设计了 Prompt B,改动点有三个:第一,增加"如果检索内容不包含答案,必须明确告知用户暂无相关信息,禁止猜测";第二,要求回复先给结论再给依据,引用检索到的原文;第三,当内容属于售后政策时,必须在回答末尾附上"具体以官方最新说明为准"的提示。

项目代号按内部迭代编号记为 24.8。

5.2 评测对比数据

在 LangSmith 里,我把线上抽样得到的 500 条评测集同时跑了一遍 Prompt A 和 Prompt B,关键数据如下:

指标Prompt APrompt B变化
事实一致性0.760.86+0.10
格式合规率90%97%+7%
拒绝明确告知"不知道"比例11%19%+8%
平均输出 token320290-30
平均延迟1.8s1.7s-0.1s

第一眼看上去,B 版本全面占优。但团队内部开评审会时,有同学提了一个问题:拒绝率从 11% 涨到 19%,用户会不会觉得体验变差了?这个问题的答案是:要看比例。客服场景里,用户最怕的不是被告知"不知道",而是被一个错误答案带偏。合理的拒绝不仅能省 token,还能减少后续投诉和舆情风险。所以 19% 的拒绝率在业务侧是能接受的,关键在于拒绝要发生在"确实没有足够信息"的情况下,而不是误杀。

5.3 从 trace 里找到真正的归因

离线评测通过后,我切了 10% 流量做线上灰度。跑了两天之后,我去 Langfuse 里翻 trace,重点看那些被评测模型打了低分的事实一致性样本。翻下来发现了一个很有价值的模式:B 版本低分 trace 里,大部分是检索召回不足的 case,也就是检索层压根没找到相关文档,而不是 Prompt 指令没执行到位。

这个发现直接改变了下一步优化方向。本来我们已经准备继续调 Prompt 措辞,但 trace 数据显示问题根因在检索层:当时用的 Embedding 模型对某些商品口语化描述召回能力偏弱。所以下一轮迭代的主线从"调 Prompt"变成了"换检索方案",同时把评测集里这些低分 case 单独拆出来,作为检索层专项评测的种子数据。

这就是闭环带来的直接价值:你不仅能知道新版 Prompt 好不好,还能知道分数变差的现场长什么样、责任到底在哪个环节。

5.4 灰度上线后的持续监控

B 版本在 10% 流量跑了 5 天,线上用户点踩率从 4.2% 降到 3.1%,延迟和 token 成本也有小幅下降,结论稳定。随后我按 20%、50%、100% 分批切流,每一步都盯两份数据:Langfuse 里的 trace 质量指标,和 BI 侧的业务指标。全量上线之后,B 版本被固化到 Langfuse 的 Prompt 管理里,后续任何改动都以 B 版本为基线再开新分支。

现在再看这次迭代的意义,不只是把 Prompt 改好了,更是确立了一个标准动作:每次 Prompt 改动都要走"离线评测 → 小流量 A/B → 分批灰度 → 固化基线"这条线。后续团队再改 Prompt,没有评测数据就默认不予上线。

6. 这套闭环真正跑起来之后,我踩过的几个深坑

6.1 评测集污染:线上数据会带病

第一次做评测集抽样时,我直接拿最近一周的 trace 扔进去让人标注,结果评测集里混进了不少当时线上 Prompt 版本导致的坏案例。最后评测分数看起来很高,但其实是在"迁就"旧版本的错误行为模式。这个坑的教训是:评测集可以从线上来,但抽取后要经过清洗和校验,标注前先筛掉明显错误样本,并且评测集一旦确定,就不能频繁跟着线上行为跑偏。

还要注意评测集不能被 A/B 实验污染。如果评测集里的样本本身就是在实验版本下产出的 trace,评估新版本时会出现样本偏差。后来我维护了两套数据:一套是标注好的离线评测集,只增删不修改;另一套是每周从生产抽的新鲜样本,用来做趋势监控,专门暴露"评测集和线上分布漂移"的问题。

6.2 OTel SDK 和框架版本不一致导致 span 丢失

有一阵子 LangChain 升了个小版本,突然发现 Langfuse 里新 trace 少了将近一半。查了半天,发现是框架自动插桩的 instrumentation 和 OTel SDK 版本不兼容,span 在导出阶段被静默丢弃。传统应用里这类问题最多体现在"日志少了几条",但 LLM 可观测里 span 一丢,整个调用链就断了,后端的评测回写也没法做。

从那次之后我定了两条规矩:一,升级任何会影响 trace 的依赖,必须先在测试环境跑一遍 trace 完整性校验,比如连续发 100 个请求,确认 span 生成率不低于 99%;二,应用层手写的那套统一埋点作为兜底方案,不依赖框架自动插桩,这样即使上层框架出问题,核心 span 也不会丢。

6.3 LLM-as-judge 的自我偏好问题

我最初以为 LLM-as-judge 是万能的,直到发现评测模型对"结构化输出"有明显偏好。同一个事实内容,分点列举的回答比自然段落的回答更容易得高分,即便自然段落里的事实完全正确。这导致一批"金玉其外"的答案在评测里虚高。

解决方式分三层:第一,Judge Prompt 里明确"只评估事实正确性,不考虑排版和措辞风格,如果两种答案包含相同事实必须给相同分数";第二,对关键评测集做人工抽检,每一百条至少要人工复评 20 条,用来校准 judge 的漂移;第三,对争议样本,让 judge 跑三遍取中位数。这不能完全消除偏好,但能把噪声控制到可以接受的范围内。

6.4 不要什么都记:trace 体量与成本

有一段时间,检索后处理逻辑里把 20 篇召回文档的全文都塞进了 span 属性,结果 Langfuse 的存储量一周翻了几倍,查询性能也肉眼可见地变慢。后来我们统一了 span 属性的容量规范:长文本一律截断到 500 字符以内;文档内容只记 doc_id 和得分,需要看全文时通过链接跳转外部存储;单条 trace 的 span 数量设上限,超过阈值只保留父子链路摘要。

这里我的体会是:可观测性的目标不是"记下一切",而是"在需要的时候能还原足够现场"。设计 span 的时候,要先问三个问题:这个数据会被谁查?查来解决什么问题?保留多久?回答不上来的字段,就不应该进 trace。

最后再分享一点个人看法。这套闭环跑顺之后,团队对 Prompt 的态度发生了很微妙的变化:以前大家改 Prompt 像改文案,凭语感;现在改 Prompt 像改代码,先跑测试、再看 diff、再灰度。大模型业务最大的不确定来源就是模型本身和 Prompt 的交互,而可观测性和评测闭环做得越扎实,这种不确定就越可控。如果正打算搭这套体系的同学,建议不要试图一步到位,先把"线上 trace 完整可查"和"离线评测集可复跑"这两件事做扎实,闭环的齿轮自然会转起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询