可观测性驱动的大模型应用Prompt A/B评测闭环实践
最近一个多月我一直在帮团队把一套 RAG 客服系统从“能跑”调到“能改”。那种感觉就像在给一个不断漂移的靶子画靶心Prompt 改一句线上指标可能跳 5 个点但没人能说清楚是哪部分出了问题。后来我把 Langfuse、LangSmith 和 OpenTelemetryOTel串成了一条完整的链路才真正把“效果靠感觉”变成了“评测有数据、回归有基线、上线有把握”。这篇文章就是把这一版实践沉淀下来版本号我记成了 24.8主要围绕一个核心问题如何用可观测能力驱动 Prompt A/B 评测闭环。这套东西不是某一个工具的功劳而是三个工具的接力Langfuse 负责线上的链路追踪和打分明细LangSmith 负责离线数据集和 Prompt 回归实验OTel 则是中间的“通用语”让不同系统的 trace 结构能对得上。文章会把我踩过的坑、验证过的配置、以及一个可以直接抄的 A/B 评测流程都写出来。适合正在做大模型应用工程化、被 Prompt 调优搞到头大的同学也适合刚接触可观测体系的团队做选型参考。1. 为什么大模型应用需要一个评测闭环从“能跑”到“能改”1.1 大模型应用与传统软件的最大差别行为不可预期传统软件只要输入不变输出就是确定的。大模型应用不是这样同一个 Prompt、同一份参数丢给同一个模型返回结果都可能不一样更别提模型版本升级、上下文窗口占满、RAG 召回质量波动这些变量。做了一段时间之后我发现团队里最大的内耗不是模型能力不行而是“出了问题不知道怪谁”。有一次我们线上准确率突然掉了 4 个点排查了半天最后发现是某个 Prompt 模板里多了一个换行符触发了解析路径变化。这种问题如果只靠人工抽查聊天记录基本不可能发现。可观测性要解决的就是这件事把每一次模型调用的输入、输出、Token 消耗、耗时、链路上下文全部记录下来让“效果变化”变成可追溯的“数据变更”。这背后其实是一个观念转变大模型应用的核心代码不再是 if-else而是 Prompt 和数据。Prompt 就是我们的“代码”评测就是我们的“单测”。没有评测闭环的应用就像没有测试用例的代码库改起来全靠胆量。1.2 评测闭环的核心环节采集、标注、评估、回归、上线评测闭环不是一个工具而是一个持续运转的流程。我用五个环节来拆解采集、标注、评估、回归、上线。采集把线上真实请求完整记录下来这是最宝贵的数据来源。包括用户问题、检索结果、模型输出、评分、用户行为反馈。标注针对采集到的样本打上 ground truth。无论是人工标注还是模型自评都要先解决“什么是对的答案”这个问题。评估用离线数据集跑不同 Prompt 版本得出可量化指标比如准确率、完整率、幻觉率、拒绝率。回归每次改 Prompt、换模型、调参数之后在固定数据集上重跑一遍确保没有反向退化。上线通过回归的新版本以小流量放出再做在线 A/B 对比同时继续采集新数据循环迭代。这个闭环里最关键的一点是数据集必须是稳定的。没有稳定数据集所有评测结果都是噪声。LangSmith 的 Dataset 功能其实就是帮我们管理这个“测试集”而 Langfuse 则管着“线上运行日志”两者一旦打通闭环就成立了。1.3 可观测与评测的先后顺序先有 trace再有 eval我见过很多团队一上来就急着搭评测平台结果发现没有数据可测因为线上调用根本没有埋点。这是常见的误判。评测的前提是可观测可观测的前提是每一条请求都能被追踪。所以我的建议是先花一周时间把链路追踪建起来把线上的真实请求全部录进 Langfuse等积累到几百条真实样本之后再拿这些样本做标注、构造数据集。先有 trace再有 eval顺序不能反。这也是我这次把 OTel 放在架构最底层的原因没有一套统一的数据标准后面接什么评测工具都会别扭。2. 可观测体系选型Langfuse、LangSmith、OTel 的分工与连接2.1 三个工具各管什么很多初学者会把 Langfuse 和 LangSmith 搞混觉得都是“给 LLM 用的监控工具”。它们确实有重叠但定位差别很大。我用一句话概括Langfuse 管线上运行时的 trace 和评分LangSmith 管离线数据集的实验与回归。具体来说Langfuse一个开源的可观测平台核心概念是 Trace、Span、Generation、Score。它的重点是把线上每一次 LLM 调用的完整链路、输入输出、成本、延迟记录下来并且支持在任意环节打“分数”比如人工评分、模型评分都可以回传到对应 trace。LangSmith偏重实验管理和 Prompt 回归。你可以把一组评测样本做成 Dataset然后对不同的 Prompt 模板或模型配置跑一轮 Experiment自动出来对比结果。OpenTelemetryOTel一个厂商中立的标准。它定义了 trace 的格式、span 的语义以及怎么上报数据。简单说如果 Langfuse 和 LangSmith 是两座城市OTel 就是连接它们的高速公路。这三者的关系不是互相替代而是互补。我用 Langfuse 看线上真实情况用 LangSmith 做离线回归中间靠 OTel 把数据模型统一起来避免每一套系统各搭各的管道。2.2 OTel 在闭环里的位置不只是协议是数据模型OTel 早期做的是链路追踪标准比如我们熟悉的 trace_id、span_id、parent_span_id。但在大模型场景下光有链路还不够还得有语义。比如“这次调用用了哪个模型”“输入了多少 token”“输出是什么”这些都需要一个统一字段规范。OTel 社区早就给生成式 AI 场景设计了语义约定常见的包括gen_ai.request.model、gen_ai.response.model、gen_ai.usage.input_tokens、gen_ai.usage.output_tokens。这些字段的存在意味着只要你的埋点遵循这套约定无论你用的是哪个模型厂商 SDK、哪个追踪平台数据都能被识别和解析。我理解 OTel 最大价值不是“上报数据”而是“统一描述数据”。它让 Langfuse 能接收 OTel 格式的 trace也让 LangSmith 的实验结果可以被纳入同一个语义框架。尤其当一个团队同时使用多个模型厂商时没有统一模型描述后面的成本和效果对比就会乱成粥。2.3 Langfuse 与 LangSmith 的边界在线 trace 与离线 experimentLangfuse 和 LangSmith 看起来都是“追踪 评测”但使用场景有一个明显区分线上和线下。线上场景重在时效和细节。一条线上请求进来我们关心的是它调了哪个 Prompt、用了哪些上下文、返回了什么、花了多少钱、用户最后有没有点“有用”。这些都是 Langfuse 的强项它能把一次请求的整个链路串起来还支持在 trace 上挂打分。离线场景重在批量对比。我们要评估“Prompt V1 vs Prompt V2”哪个更好需要固定样本集、批量跑完、逐条对比输出。LangSmith 的 Dataset 和 Experiment 就是为了这个场景设计的。同一个数据集可以反复跑每次跑完自动生成对比报告。这块我补充一个细节LangSmith 和 Langfuse 都能打分但它们的打分用途不一样。Langfuse 的 Score 更多反映线上反馈比如用户点赞、客服标记、自动检查规则LangSmith 的跑分则是实验结论比如这条样本答对了没有、相似度多少。我在实践中会两边都保留线上评分进 Langfuse离线评测结论进 LangSmith互相印证。3. 落地步骤一打通 OTel 到 Langfuse 的观测链路3.1 Langfuse 接入的三种方式Langfuse 的接入并不只有官方 SDK 这一条路我根据团队实际情况整理了三种方式。第一种是官方 Python/JS SDK。上手最快直接在代码里初始化 Langfuse 客户端用observe()装饰器或手动封装来记录调用。适合从零开始、代码可控性高的项目。第二种是 OTel 原生端点。Langfuse 本身暴露了一个兼容 OTel 的上报端口你只需要把 OTel 的 Trace Exporter 指向这个端口它就能接收标准的 OTLP 数据并把 trace 自动映射到 Langfuse 的模型里。这个方式适合已经有 OTel 埋点的服务代码改动最小。第三种是通过 OTel Collector 中转。在服务与 Langfuse 之间架一个 Collector服务端把 OTLP 数据发给 CollectorCollector 经过批量、过滤、字段加工之后再转发给 Langfuse。这种方式最灵活适合多服务、多后端、需要数据脱敏的场景。我当时选择的是第三种因为团队里已经有服务用标准 OTLP 上报我不想再为 Langfuse 单独写一套埋点。拦截层统一做字段修正后续换平台也方便。3.2 用 OTel GenAI 语义约定规范埋点直接上报裸 trace 给 Langfuse 会有一个问题平台无法识别“这是 LLM 调用”。所以必须在 span 上打上 GenAI 语义字段。下面这段是我在代码里实际使用的 OTel 埋点风格用的是 Python 的opentelemetry-sdkfrom 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.name: rag-customer-service, deployment.environment: prod, }) provider TracerProvider(resourceresource) provider.add_span_processor(BatchSpanProcessor( OTLPSpanExporter(endpointhttp://otel-collector:4318/v1/traces) )) trace.set_tracer_provider(provider) tracer trace.get_tracer(rag.tracer) def trace_llm_call(query, prompt, response, model, input_tokens, output_tokens, duration_ms): span tracer.start_span(chat/completions, kindtrace.SpanKind.CLIENT) span.set_attribute(gen_ai.request.model, model) span.set_attribute(gen_ai.request.prompt, prompt) span.set_attribute(gen_ai.response.text, response) span.set_attribute(gen_ai.usage.input_tokens, input_tokens) span.set_attribute(gen_ai.usage.output_tokens, output_tokens) span.set_attribute(llm.latency_ms, duration_ms) span.end()这段代码的关键点在于字段名。Langfuse 收到 OTel trace 后会按 GenAI 语义约定识别模型、token、响应文本字段。你如果自创字段名虽然数据也能上去但平台没法把 token 数、模型名这些关键维度解析出来后续看板和分析就全废了。3.3 采集哪些核心指标token、成本、延迟、质量分数管线通了之后要确定“采集什么”。我提炼了四类必须采集的数据。基础标识trace_id、session_id、user_id、request_id。这是关联一切数据的主键。性能指标模型名、模型版本、首字延迟、总耗时、输入 token、输出 token、总成本。内容数据系统 Prompt、用户消息、检索到的上下文、模型完整回复。这部分涉及数据脱敏和存储成本需要做裁剪但绝不能完全丢弃否则无法排查质量问题。反馈数据用户是否点赞/点踩、客服标记是否正确、业务侧完成的率。我们当时用 Langfuse 自身的 Score API 把反馈数据挂到 trace 上。下面是打分示例from langfuse import Langfuse langfuse_client Langfuse() langfuse_client.score( trace_idtrace_id_xxx, nameuser_feedback, value1, comment回答准确, )Score 的作用是给 trace 打标签。线上每一条请求最终会被贴上多个标签比如“用户满意”“检索漏召回”“拒绝错误”。有了这些标签后面做 A/B 时就不只盯 token 成本还能看质量维度。3.4 通过 OTel Collector 做数据脱敏大模型场景最大的数据合规隐患就是用户问题可能带着手机号、账号、地址等信息直接进了日志系统。我强烈建议在 Collector 层做一次字段过滤或者哈希处理不要等到存进库里之后再清理。可以在 Collector 的 transform processor 里写规则对gen_ai.request.prompt和gen_ai.response.text做正则脱敏。比如把 11 位手机号替换成掩码。这样 Langfuse 里存的数据已经是脱敏后的数据既能排查问题也降低后续数据合规审查的风险。这一段是纯经验之谈当时我们为了省事跳过脱敏直接全量记录结果安全团队查日志时发现真实手机号整改了一周。越早加脱敏代价越小。4. 落地步骤二用 LangSmith 做 Prompt A/B 的离线评测4.1 构造评测数据集不要拿生产日志直接当 ground truth很多团队拿到生产日志直接就当成评测数据集开始跑分。这个做法有隐患生产日志只是“模型的输出”不是“标准答案”。模型输出丢了个关键信息日志不会告诉你这是对的还是错的。正确做法是先做样本标注。从 Langfuse 导出一批线上请求人工或半自动标注出标准答案再入进 LangSmith 的 Dataset。标注时我会尽量覆盖以下几类场景典型问题比如“怎么退款”“怎么查物流”每个类目 10 到 20 条。边界问题相似意图但实际业务不同比如“补差价”和“退差价”需要模型能区分。对抗样本故意加口语化表达、错别字、长文本模拟线上真实噪声。拒绝样本哪些问题模型应该明确拒绝而不是强行编造。数据集不需要特别大我的经验是每个评测子类 50 到 100 条样本已经能跑出一个相对可信的基线。数据集稳定之后尽量不要往里面随意加样本每次加样本都当成一次版本更新。4.2 跑一次 A/B Experiment发布 Prompt 之前必须看的回归LangSmith 的 Experiment 逻辑很好理解给定一个 Dataset给定一个 Prompt 模板或 Agent 配置跑一遍得到所有样本的输出再跟 ground truth 对比。我一般会维护两套 Prompt一套是线上基础版本 baseline另一套是待上线的候选版本 candidate。跑 Experiment 时两套都跑然后对比结果。示例代码大致像这样from langsmith import Client client Client() dataset client.create_dataset( dataset_namecustomer_service_24_8, description客服 RAG 评测基准集 ) client.create_examples( dataset_iddataset.id, inputs[{question: 订单怎么取消}], outputs[{answer: 在订单详情页点击取消...}], ) # 使用 Runner 跑一次实验简化示意 runner client.create_prompt_experiment( dataset_iddataset.id, experiment_prefixprompt_v3_candidate, prompt_identifierprompt_template_v3, )跑完之后LangSmith 会输出一份对比表包含每条样本的输出、正确与否、token 消耗等。我一般重点看三类指标整体正确率、错误样本分布、成本变化。一个候选 Prompt 的正确率提升哪怕只有 2 个点只要错误样本分布没有明显恶化我就会考虑放到线上小流量测试。这里有个经验Experiment 不是跑一次就完。只要改了代码、换了检索或改了模型都必须把同一个数据集重新跑一遍。否则你根本不知道线上指标波动是改 Prompt 造成的还是改检索造成的。4.3 评测指标与 LLM-as-judge 的做法相比于传统分类任务大模型输出的评测更让人头大。同样一个问题标准答案 30 个字模型输出 80 个字意思对了但表达完全不同简单字符串比对根本不可靠。我的做法是分三层评测。第一层是规则检测适合可以明确校验的场景比如 JSON 格式是否正确、是否包含指定关键词、是否包含链接。第二层是相似度打分用 ROUGE、BLEU 或者向量余弦相似度适合对表达顺序有要求的任务。第三层是 LLM-as-judge用一个更强大的模型来当裁判对输出进行多维度评分比如正确性、完整性、简洁性、是否幻觉。用 LLM-as-judge 时我给裁判模型写的评测 Prompt 有固定模板里面必须包含四个要素原始问题、标准答案、待测模型输出、评分维度说明。这样跑出来的分数才可重复。另外裁判模型本身也有一定随机性我建议每条样本至少评两次取均值用温度设为 0进一步降低波动。5. Prompt A/B 评测的真实运营流程数据回流与决策机制5.1 A/B 分组与流量划分策略离线评测通过之后下一步就是线上小流量验证。线上 A/B 的关键在于分组逻辑。最简单的做法是基于用户 ID 或会话 ID 做哈希分流这样同一个用户始终落在同一组避免用户体验跳变。这里有个权衡如果你只关心“最终答案质量”按请求粒度做随机分组就能保证统计样本的独立性如果你关心“用户长期体验”就要按用户粒度分组。我当时做的是按会话 ID 哈希分流因为客服场景中同一个用户的多轮交互属于一个会话按会话分组既容易实现也不会让用户一会儿看到新版答案、一会儿看到旧版答案。分组代码很简单哈希取模即可import hashlib def get_ab_group(session_id: str) - str: ab_hash int(hashlib.md5(session_id.encode()).hexdigest(), 16) return candidate if ab_hash % 100 20 else baseline这里 20 表示候选版本只拿 20% 流量稳妥起见不要 50/50 直接开。先小流量跑一天看看有没有明显异常再逐步放量。5.2 在线评测与离线评测如何闭环互证很多人以为 A/B 评测就是“线上看准确率哪个高”实际上在线场景很难直接拿到 ground truth。你只知道用户有没有点有用、有没有发起转人工但这些是间接信号。所以我的闭环思路是双向印证。离线评测的时候提前在数据集上定义好“什么算优秀”跑出一个预期值。线上 A/B 跑完我再看两组之间的用户反馈率、转人工率、平均修复轮次等指标。如果离线评测说 V3 更好线上数据却不支持问题大概率出在数据集与线上分布不一致这时就去检查线上日志把新出现的典型问题补进数据集。这个“线上发现问题 - 补数据集 - 离线回归 - 再上线上”的循环就是评测闭环真正的运转方式。它不会让效果一次到位但会让每一次改动越来越有把握。5.3 样本量与显著性判断A/B 评测最容易犯的错是样本量不够就下结论。我见过有人只跑了几十条数据就说新版准确率提升 10%这种结论在统计上没有任何意义。关于样本量用个简单直觉判断如果我们关注的是二分类指标比如“回答是否让用户满意”两组之间如果差异只有 1 到 2 个点那至少需要几千到上万条样本才能达到统计显著如果差异有 5 个点以上可能几百条就能看出趋势。我一般会在实验配置里提前算好最小样本量别等数据出来了再问“够不够”。也可以用现成的显著性校验工具比如对两组比例做 Z 检验或卡方检验p 值低于 0.05 就认为有显著差异。没有显著差异时我的原则是“不要上线”。既然数据不能证明新版本更好就没有理由承担上线风险。5.4 上线前的 checklist跑完数据还不够上线之前我习惯过一遍 checklist避免低级失误。确认候选版本已经在 LangSmith 数据集上完成回归正确率不低于基线。确认线上 trace 可以正常区分 baseline 和 candidate 两个版本。确认 Langfuse 已经按 prompt_version 维度打好标签方便后续数据回查。确认成本增幅在可接受范围特别是 candidate 版本如果增加了 prompt 长度token 成本可能翻倍。确认脱敏规则对 candidate 版本同样生效新的 Prompt 模板里如果有新的可变字段也要做合规检查。这些事项看起来琐碎但每一条都对应着我踩过的坑。尤其“成本增幅”这条我们曾经有一个 Prompt 版本在 A/B 中效果略好但输入 token 增了 60%算下来月度成本多了一大截最终还是被拦下了。6. 常见问题与排坑实录6.1 Langfuse 里看不到完整 trace原因大概率埋点创建了独立 span但没有串到同一个 trace。检查时看两点一是所有 span 是否共享同一个 trace_id二是 Langfuse 是否把请求正确映射到了 trace。常见问题出在observe()和手动创建 span 混用导致父子关系错乱。我的经验是统一走一套入口封装不要这边装饰器、那边手动埋点。6.2 OTel 字段上报了但 token 数据解析不出来多半是字段名不对。OTel GenAI 语义约定里token 相关字段应该挂在 span attributes 上字段名建议写成gen_ai.usage.input_tokens和gen_ai.usage.output_tokens。如果上报成别的自定义名字Langfuse 无法识别虽然能查到原始 attributes但无法参与平台自身的指标计算和成本估算。6.3 LangSmith 数据集越跑越脏这是离线评测的经典问题。团队里每个人都往里加样本加着加着数据集就看不懂了。我的对策是给数据集立规矩新增样本必须附带类别、来源、标注置信度。并且每次更新数据集都当成一次发布记录变更说明。没有记录的样本宁可不用。6.4 评测分数很高线上效果却很差恭喜你遇到了数据集与线上分布不一致的问题。常见原因有两个数据集过窄只覆盖了少量高频场景或者标注答案本身带入了主观偏好比如标注者喜欢较长回答模型就往长了写。这时候要回到 Langfuse 里拉线上真实请求日志重新做一轮数据分布分析而不是继续在旧数据集上调 Prompt。6.5 LLM-as-judge 打分手感不稳定裁判模型也会飘。我遇到过同一批样本上午跑和下午跑结果差异明显最后发现是裁判模型的温度参数没有固定以及不同批次间混入了少量长尾样本影响判断。把温度固定为 0、每条样本重复评分取均值之后稳定性好多了。更严格的做法是对裁判模型也做一次一致性评估确认它对同一输入重复输出的方差可接受。6.6 可观测数据成本失控记录详细的输入输出必然带来存储成本上涨尤其大模型响应文本是几倍于用户问题的体量。我们会做三层裁剪第一层全量采样保留一周第二层固定比例采样保留一个月第三层系统关键错误全量永久保留。样本量如果还是太大就对响应文本做摘要再存储但注意摘要字段和原始字段要分清别混在一起。实操中的两点心得第一点工具链搭建不是越复杂越好。我见过团队把 Langfuse、LangSmith、OTel、Prometheus、Grafana 全接上结果没人看得完数据最后全部停用。我这次只保留了真正产生决策依据的三件套简单到团队成员看一眼就能定位问题。评测闭环的价值不在于数据量大而在于每次改动都能拿出证据说话。第二点Prompt 版本管理一定要前置。我当时是先让 Langfuse 里每条 trace 都带上了prompt_version标签后来做 LangSmith 回归实验时才能轻松把历史版本捞出来对比。如果一开始没有版本概念后面所有分析都只能靠“我记得当时用的模板好像是这个”来还原效率极低。24.8 这版实践走到今天最直观的改变是团队里再也没有人敢基于感觉改 Prompt 了。每次改完LangSmith 数据集跑一遍Langfuse 线上数据验证一遍两个闭环对上才允许动线上流量。这套流程虽然朴素但正是大模型应用从“玄学调参”走向“工程化迭代”最务实的一步。