大模型可观测与评测闭环:Langfuse+LangSmith+OTel实战 大模型上线以后最让人头疼的不是“报错了”而是“看着都正常但效果就是不对”。报错可以打日志、查异常有明确的技术栈去处理效果不对你根本说不清到底是大模型本身的问题、Prompt写得不对、还是下游解析出了问题甚至连“现在线上到底是哪一版Prompt在生效”都很难回答。这篇文章从我们实际落地的项目出发讲讲怎么把Langfuse、LangSmith和OTel结合起来在一个从24.8版本迭代开始接入的客服对话系统里搭出一条完整的大模型可观测与评测闭环把Prompt A/B评测从“线下跑一次对比”变成“线上持续运行的机制”。我尽量不堆概念直接讲我们在真实配置和运行中踩过的坑、趟出来的路。文章里涉及的具体方案不一定适用于所有团队但链路设计和判断取舍的思路应该对正在做LLM应用工程化的同学有参考价值。1. 为什么大模型应用需要“可观测与评测闭环”很多人觉得给大模型应用加了日志、接了监控面板就算可观测了。真跑过一段时间就会发现传统可观测手段在LLM场景里存在几个明显断层这也是我们决定做这套闭环的根本原因。1.1 LLM应用传统观测手段的三个盲区第一个盲区是链路断层。一次完整的LLM调用往往不是一个单一接口而是“用户请求进来 → 经过意图识别 → 检索知识库 → 拼接Prompt → 调用模型 → 解析结果 → 触发下游动作”这样一串调用。传统日志通常只记录一个应用节点的进出中间Prompt拼成什么样、检索命中了哪些切片、模型返回了多长内容全都没记下来。第二个盲区是语义层面完全不可见。传统监控能告诉你接口P95延迟是2秒、错误率是5%但回答是否准确、格式是否合规、有没有幻觉、某类用户群体的体验是否特别差这些信息在数值指标里是看不到的。我们必须把“输入、输出、中间变量”以结构化的方式记录到trace里才可能做语义层面的分析。第三个盲区是版本与效果无法挂钩。Prompt是文本不是代码很多人维护Prompt靠注释和文件名区分改了一版就重新上线。过两周有人反馈效果变差了你根本不知道线上当前是用哪一版Prompt也不知道这一版Prompt是哪个时间段被切换上去的。1.2 评测不闭环等于白测评测如果不和生产环境打通就只能算“线下模拟”价值会大打折扣。当模型决定的输出有不确定性评测集和线上分布有差异离线指标和真实体验就会明显对不上。我们之前吃过亏离线跑分看着新版Prompt准确率提升了几个点上了线用户投诉反而增多。后来发现是评测集里的样本偏简单刻意构造的句式在线上占比很低线上一堆口语化、带错别字、指代不明的问法根本没覆盖到。所以评测必须形成闭环线上真实请求持续抽样回流到评测集 → 周期性运行离线评测 → 用评测结果辅助Prompt迭代 → 迭代后的Prompt再通过A/B实验放到线上验证。这个链路少了任何一环评测都只是自嗨。1.3 闭环设计要解决的四个核心问题我们当时列了一组必须回答的问题算是整个项目的验收标准每一次请求的完整调用链能不能追溯回看每次调用的Prompt模板版本、模型参数、Token消耗能不能对得上一个Prompt改动上线后能不能用数据判断它比前一版好还是坏评测结果能不能反哺Prompt迭代形成持续改进的节奏带着这四个问题我们才确定了Langfuse、LangSmith和OTel这套组合。2. 工具选型Langfuse、LangSmith和OTel的分工刚接触这三样东西的人很容易把它们搞混因为它们都涉及“追踪”。实际上这三个工具的定位差异非常大组合使用刚好覆盖了从开发、测试到生产监控的完整生命周期。2.1 Langfuse负责生产环境的链路记录Langfuse给我的直观感觉就是给LLM应用装了一台“行车记录仪”。它会记录完整的trace包括每次调用的输入、输出、模型名、Token数量、延迟、以及你自己定义的评分。它把数据组织成一个树状结构父span是一次业务请求下面的子span是每次模型调用或工具调用。这样某次回答出了问题你可以像查调用链一样把整条链路展开回看。Langfuse对生产环境的另外一个价值是成本中心。它支持按项目、按用户、按标签维度聚合Token消耗。我们在没接入之前大模型账单就是个黑盒只知道每个月花了多少钱不知道钱花在哪些场景、哪些用户、哪些Prompt版本上。接入Langfuse之后账单可以精确到一行业务记录成本治理就变成了一个可以讨论的问题。2.2 LangSmith负责评测实验的编排LangSmith更偏“开发与评测”场景它的核心能力是数据集、实验和评估回调。你可以把一个Prompt的多个候选版本挂到同一个数据集上离线批量跑也可以定义“程序化评估函数”来处理结果比如字符串规则、语义相似度或者让另一个模型去打分。它其实是在解决“怎么科学地比较两个Prompt”的问题。但LangSmith在线上数据回流这块需要自己处理对接。它在生产环节的嵌入式监控能力偏重在线反馈收集真正从业务系统里自动抽取线上请求并导入评测集通常还依赖我们自己写数据管道。这也是我们后来把OTel纳入进来的原因之一。2.3 OTel统一数据出口与标准语义OTel在大模型可观测里的角色不是“另一个追踪系统”而是一个统一的遥测数据结构标准。它定义了一套文本规范、语义约定和传输协议让应用把trace、metrics、logs统一发出来再交给任意一个后端去处理。在我们这套方案里OTel发挥了一个很关键的作用解耦。业务应用不需要直接调用Langfuse或LangSmith的私有API只需要按OTel规范生成span由一个OpenTelemetry Collector统一接收后再按需求转发给不同的后端。后续如果要把某部分数据同时送到自有监控平台也不需要在业务代码里改任何埋点逻辑只改Collector配置就够了。2.4 三个工具的协作边界如何划一句话概括我们的分工OTel负责“数据怎么统一出来”Langfuse负责“生产数据怎么存和看”LangSmith负责“评测实验怎么跑和比”。Langfuse可以有OTel的数据接入LangSmith也可以直接采集应用运行时的日志但我们为了避免数据口径混乱强制规定了一条数据流向业务应用 → OTel SDK埋点 → OpenTelemetry Collector → Langfuse生产链路与 自有监控后端LangSmith则作为离线评测平台使用评测集数据来源有两个一是团队手工构建的标注样本二是从Langfuse导出并经过脱敏筛选的真实请求。生产链路和评测链路分开存储但通过“trace_id”和“评测集样本ID”建立关联这样一份数据既能做监控复盘也能做实验评估。3. 落地实操从埋点到链路打通确定方案后真正动手阶段遇到的问题比预想多。下面这部分是我们实际操作时比较关键的技术点以Python服务和OTel Collector举例其他语言SDK类似。3.1 依赖引入与基础配置我们服务的框架是Python FastAPI需要用到的核心包有opentelemetry-api、opentelemetry-sdk、opentelemetry-exporter-otlp以及用于把FastAPI请求自动生成span的opentelemetry-instrumentation-fastapi。基础配置在进程启动阶段完成代码大致是这样from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter resource Resource.create(service_namecustomer-service-assistant) trace_provider TracerProvider(resourceresource) # 这里指向本机或集群内的otel-collector服务 exporter OTLPSpanExporter(endpointhttp://otel-collector:4318/v1/traces) trace_provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(trace_provider)有个细节值得注意BatchSpanProcessor默认的导出间隔是几秒如果你的服务突然重启最后几秒的span可能会因为来不及导出而丢失。我们在测试环境出现过这种问题解决方案是增加shutdown()时机处理并发完请求后主动调用一次processor的shutdown强制刷空缓冲。3.2 在业务代码里定义业务Span自动埋点能捕获HTTP请求但捕获不到“Prompt拼接”、“模型调用”这些业务语义。真正有用的是自己手动创建业务span。我们在服务里封装了一个统一的LLM调用入口所有走大模型的操作都经过这个入口然后用tracer创建spanfrom opentelemetry import trace tracer trace.get_tracer(llm-call) def call_llm_with_trace(user_query: str, prompt_body: str, model_name: str): with tracer.start_as_current_span(llm.inference) as span: span.set_attribute(llm.prompt_template, prompt_body[:2000]) span.set_attribute(llm.model_name, model_name) span.set_attribute(llm.user_query, user_query[:500]) # 实际调用模型... response actual_model_call(prompt_body) span.set_attribute(llm.response, str(response)[:3000]) span.set_attribute(llm.usage.total_tokens, response.usage.total_tokens) return response这里我截断了文本字段的长度原因有两个一是LLM输入输出可能很大全量打出来会占用大量存储把成本顶上去二是可能包含敏感信息记录前要先脱敏。我们实际是把user_query和llm.response在写入前都过了脱敏服务。如果你比较在意隐私合规这一步建议尽早设计而不是等数据存进去了再补。3.3 用OpenTelemetry Collector做数据路由业务服务不直接连接Langfuse的后端而是把span发到本地Collector。Collector配置里同时定义多个exporter一份数据可以复制到多个目的地。receivers: otlp: protocols: grpc: http: processors: batch: timeout: 5s send_batch_size: 1024 exporters: otlphttp/langfuse: endpoint: http://langfuse-server:3000 headers: Authorization: Bearer sk-lf-xxxx otlphttp/own-monitor: endpoint: http://monitor-backend:4318 service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlphttp/langfuse, otlphttp/own-monitor]这一步把业务应用和存储后端彻底解耦了。我们后来想加一个自建的trace备份系统完全没改业务代码只是在Collector配置里挂了一个exporter就完成了。需要提醒的是Langfuse对OTel接入有自己的格式约定。我们一开始直接把Langfuse的鉴权头放在Collector配置里看起来能通但到Langfuse面板里发现span缺失了一些字段。后来查了官方文档发现需要Langfuse的OTel端点配合特定的Header格式。如果你在接入时发现trace到了但没法按会话id关联多检查一下Collector的headers配置方式。3.4 通过语义约定建立统一标签体系为了后续做数据分析和A/B评测我们在所有span上统一打了一套业务标签稍微介绍下这里的设计思路app.version当前业务版本号对应我们内部发布的24.8等迭代版本prompt.versionPrompt模板的版本号而不是文本内容prompt.groupPrompt的分组名比如“客服主流程”“售后处理”“商品推荐”user.segment用户分群标签用于后续分析不同人群的表现差异evaluation.status是否进入评测回流采样可选值sampled或none标签设计看着简单但它是所有后续分析的基础。如果一开始没打上prompt.version后面想定位“某个Prompt版本到底让哪些请求变差了”根本无从下手。我们是在方案设计阶段就统一了标签命名跑了一个月的数据之后发现这个决策非常关键。4. Prompt A/B评测从拍脑袋到数据说话整个闭环里最核心、也最容易被做砸的部分就是Prompt评测。我们先说方法论再讲技术落地。4.1 评测指标怎么定评测指标不能只盯着“准不准”对于客服场景我们拆成了五个维度答案准确率模型回答与标准答案的语义相似度采用人工标注和LLM打分结合格式合规率回答能否被下游解析器正确解析这是程序化规则可以精确计算幻觉率回答内容是否包含知识库中不存在的信息需要人工或交叉模型判定端到端时延从用户发起请求到返回答案的P95耗时直接影响体验单次对话成本Token消耗乘以模型单价影响可持续性有个很容易犯的错是只评估“生成内容”本身不评估“业务结果”。比如客服回答写得很好但用户根本没理解或者解析器无法提取回传表单那这个Prompt就算不合格。所以我们把“格式合规率”和“端到端时延”同时纳入A/B对比指标这是很多纯离线评估容易漏掉的。4.2 评测集建设从真实流量中来回真实流量中去评测集的构建决定评测结果的可信度。我们当时做评测集时不只是找业务方要了100条历史对话而是从Langfuse中把线上一个月的真实请求按分层抽样方式导出来抽样维度包括常见问法、长尾问法、带错别字问法、多轮上下文问法、情绪激烈问法。人工标注之后把这些样本作为LangSmith的数据集导入。这里最核心的一条原则是评测集必须持续更新。我们设置了每周一次的回流任务从生产环境抽新请求评估之后把有代表性的样本加到评测集里同时淘汰掉已经被模型完全解决的过时样本。没有这个更新机制评测集很快就会和生产环境脱节评测结果变成一种“自我实现的预言”。4.3 用LangSmith跑离线对照实验Prompt迭代的流程一般是这样先从线上数据里发现某个类型的回答质量差分析原因然后修改Prompt模板再到LangSmith里建一个实验把新版Prompt和当前线上版Prompt在同一个评测集上跑。LangSmith会把两个版本的结果并列展示配合评估函数自动打分。我们的评估函数分两类。一类是完全程序化的检查JSON格式、检查是否有敏感词这类可在LangSmith的Python评估回调里直接定义。另一类是语义评估我们调用一个大模型评测器让它按给定维度给答案打分LangSmith支持把它也定义成评估函数。关于语义评估有个经验值得分享评测模型要用比较稳定的版本不要频繁切换否则你很难判断效果变化是Prompt改动的功劳还是评测模型升级导致的。4.4 线上A/B实验的流量切分与效果判定离线评测通过之后真正的考验是线上A/B。我们的大致做法是在业务系统里维护一个Prompt版本映射表里面包含prompt.version和对应的线上流量百分比默认情况下线上版为online-v1新版本online-v2先分配10%流量通过请求属性prompt.version: online-v2Langfuse会自动把两版流量打上不同标签实现流量切分时我们做了一个业务决策同一个用户可以稳定命中同一个版本而不是按每次请求随机分桶。这是为了避免同一个用户在不同版本间来回横跳导致体验不一致、反馈数据无法归因。切分逻辑是通过用户ID的哈希取模实现的代码很轻量。实验跑一段时间后从Langfuse导出两拨数据做对比分析。我们当时用了一个很简单的显著性判断方法对核心指标计算95%置信区间如果两版的区间没有重叠或者重叠很小才认为差异是可信的。如果只看点估计很容易被小样本噪音带偏。比如A版准确率91%、B版93%看似B更好但样本量只有200置信区间宽度达到10个点这个结论并不可靠。4.5 效果分析双维度的决策矩阵落地24.8版本的时候我们实际评测过两个Prompt候选效果差异正好能说明问题。旧版Prompt回答内容其实很丰富但是格式不稳定偶尔会出现“1、先登录2、……”这种带编号的回答下游解析器没法稳定提取。新版Prompt约束了输出为严格的JSON格式格式合规率从88%提升到99.5%但答案准确率略微下降因为结构化约束在一定程度上限制了表达丰富度。如果只比准确率新版不一定赢如果只看合规率旧版必输。综合来看我们把“格式合规率”作为硬性门槛只要准召率下降在一定范围内就能接受。最终新版以“高收益、可接受的成本增长”上线。Langfuse的标签面板帮了大忙两版Prompt的成本、时延、Token消耗都直接分开展示不用自建聚合脚本。5. 跑了一个季度才遇见的坑问题排查实录文章前面都是方案叙述下面这部分真的是我们踩过的硬坑单列出来供大家避雷。5.1 Span丢了数据但日志里没有报错有段时间Langfuse面板里的trace数量明显比业务请求量少排查下来发现是OTel Collector的batch处理器积压数据超过内存阈值会丢弃数据而默认日志级别没有输出丢弃的统计。这个问题的检测方式是在Collector配置里增加一个memory_limiter并把日志级别调到debug观察器里就能看到具体的错误。另一个原因是业务进程在容器滚动重启时未导出的span没有来得及flush就被杀掉了。我们后来在Pod的生命周期钩子里加入了flush逻辑问题明显减少。5.2 LLM评测器自己也不稳定用另一个大模型做答案质量评分时同一个Prompt同一个回答两次评分可能差出一到两分。这个问题起初让我们很困惑后来把评测器温度降到0并且加了“必须输出JSON打分格式”的强约束之后重复评分方差才降下来。即便如此我们仍然没有让LLM打分作为唯一信号而是把它作为人工抽检的前置筛选器凡是打分处于“边缘分数”的样本必须进入人工复核队列。这里的原理其实和代码审查——自动化检测抓明显问题人工只处理边界情况——是一样的。5.3 A/B实验样本分布不均导致误判有一期实验结果看起来B版本在“高情绪用户”场景下大幅领先后来一查B版本流量里面情绪激烈用户占比只有2%样本量总共就八十多条统计上完全没有意义。原因是我们的流量切分是按用户ID哈希但高情绪用户的ID分布存在偏置运气不好就全部落到A版本。后来我们在流量切分的时候加入了用户分群维度做分层抽样保证每个用户分群在不同Prompt版本中的占比一致。这是做A/B实验最容易忽略、也最致命的问题。5.4 成本归因时漏掉了Embedding调用我们刚开始只统计主模型调用的Token消耗后来发现知识库检索场景的Embedding调用开销并不低而且它也是Prompt方案的一部分——如果改了Prompt导致召回方式变化Embedding调用次数和输入长度都会变。Langfuse本身支持自定义Span记录Embedding我们后来把检索链路里的“向量化”“检索”“重排”也各自打成了span成本归因才真正完整。细分程度越高排查问题的能力越强。5.5 评测环境和生产环境的数据口径不一致初期我们的离线评测直接读取生产库但没统一“是否包含多轮上下文”“是否包含限流后的重试结果”导致离线评测结果和生产指标对不上。后来我们明确了一条规则离线评测只使用控制变量的方式运行即固定上下文、固定模型版本、固定参数生产分析的指标只来自线上trace。两套数据严格分开。交叉对比时要确保使用的样本定义完全一致比如都限定为首轮对话不然指标根本没法对齐。6. 一套可复用的落地参考我们的Checklist文章最后放一张我们内部整理出来的Checklist方便有需要的团队按图索骥。这不是标准答案但照着走一遍至少不会踩我们踩过的大坑。确定目标指标准确率、合规率、时延、成本缺一不可定义统一标签prompt.version、app.version、user.segment必须提前定好打通OTel链路业务SDK → Collector → Langfuse数据备份出口提前预留构建动态评测集真实流量分层抽样标注后导入LangSmith每周回流更新定义多维评估函数程序化规则LLM评测人工抽检三级分层设计线上A/B机制流量切分含用户ID哈希按用户分群分层抽样稳定分桶设置显著性与置信度门槛先看置信区间再下结论建立主动观测机制每天巡检evaluation.statussampled的样本判断回流数据是不是评得靠谱我个人感受比较深的一点是这套闭环的工程难度不算特别高真正的难点在于坚持。Prompt评测是一个持续性工作不是上线一个版本就结束了。模型会升级数据分布会漂移业务目标会调整评测集和指标定义都必须随之迭代。如果团队没有把它变成例行流程而是想起来才跑一次那前面所有的技术投入都会白费。另外建议先找一个业务价值最高、但是效果最难证明的场景作为试点比如客服应答、报表生成这类“内容质量直接影响业务结果”的方向。把这个场景的闭环跑通并形成团队共识之后再往其他场景复制效率会高得多。大模型应用正处在快速演进期可观测和评测不应该是事后补救的辅助工具而应该成为Prompt工程和模型迭代的基础设施早一点建好后面所有关于效果好坏的争论都会少很多。