
在做大模型应用的可观测与评测闭环之前我最常被问的一个问题是Prompt 到底怎么改才靠谱我没有标准答案但我有一套能在半小时内给出对比结论的流程线上链路用 Langfuse 观测和留存 trace离线用 LangSmith 跑 Prompt A/B 评测中间通过 OTel 统一事件模型和上下文传播。这套组合在我负责的内部客服问答系统上已经跑了一年多最近一次迭代就是 24.8 这一版。为什么要折腾这么一套东西因为大模型应用和传统后端不一样接口 200 不代表没出事。上个月我翻线上数据时发现健康检查全绿、平均耗时没涨但业务那边攒了一堆投诉集中在回答越来越像在编。顺着 trace 找到根因某次知识库改版后检索文档结构变了旧 Prompt 里请严格基于资料回答的指令不再生效模型开始自由发挥。这种问题业务指标看不到只有链路级可观测数据能暴露出来。这篇就把我的落地过程完整摊开讲工具怎么分工、埋点怎么接、A/B 评测方案怎么设计、结果怎么判读以及我踩过的几个让评测结果不可信的坑。1. 能跑与能交付之间隔着一条可观测的鸿沟1.1 大模型应用失败的三种典型姿势传统后端出故障看错误率、看堆栈基本能定位。大模型应用不一样输出空间太大失败方式非常隐蔽。我总结下来最常见的其实不是服务挂掉而是下面三件事。第一类是静默质量劣化。接口照常返回但回答开始跑偏。触发原因往往不在模型本身而在上游数据变更、Prompt 与新数据格式不匹配、上下文被意外截断。开头说的知识库改版就是典型例子。这类问题在业务指标上完全不可见只有翻 trace 才能发现。第二类是成本失控。我们一度把模型换成更便宜的型号粗看单价降了实际支出反而涨了 30%。原因很气人新模型的指令遵循能力弱回答经常没踩中要求客户端反复重试加上上下文里不断拼接历史消息token 消耗全线膨胀。没有按 trace 归因的 token 计量成本就是一笔糊涂账。第三类是评测不可复现。手工挑二十条问题觉得新 Prompt 更好上线一周后被业务打脸怎么变笨了。问题出在手工样本太少、太偏打分标准不统一换个人测可能结论就反过来了。这三类问题互为因果观测不到位就抓不到劣化样本劣化样本抓不到评测集就缺了真实场景评测集不真实Prompt 迭代就是盲改。所以要打通闭环。1.2 三个工具各自解决什么问题在这个闭环里OTel、Langfuse、LangSmith 的角色完全不同很多人第一次接触会搞混。OTel 是统一语言负责 trace 的采集协议、跨服务上下文传播、以及模型调用的标准属性。只要链路里各组件都按这个标准输出后续换任何观测后端都不用重新埋点。Langfuse 是线上台账集中记录每次调用的输入输出、模型、token 数、耗时、成本支持给 trace 打标签、写评分、按会话归集适合生产规模的持续观测。LangSmith 是离线裁判台管理评测数据集批量跑多个 Prompt 变体用可配置的评测器打分横向对比实验结果。为什么两者都要保留因为我实际用下来线上观测和离线评测的诉求非常不同。Langfuse 擅长处理生产 trace 量大、留存策略、告警这些工程问题LangSmith 擅长把一组问题反复喂给不同 Prompt、出对比报告。用 trace_id 把两边数据串起来一套闭环就成了。2. 选型复盘为什么是 Langfuse LangSmith 而不是二选一2.1 只上 Langfuse 会缺什么Langfuse 的观测能力很强也自带打分和数据集功能看起来可以一套搞定。但我真正跑多版本 Prompt 对比时发现它在这个场景上不够顺手。你需要自己写循环把两个 Prompt 分别跑完同一批数据再把两组分数合并统计要从线上 trace 堆里找一个低分样本去构造评测用例也得写不少胶水代码。它能做但不像 LangSmith 那样把数据集 实验 对比报告做成一等公民。只说结论Langfuse 适合回答线上发生了什么不太适合回答哪个 Prompt 更好。2.2 只上 LangSmith 会缺什么LangSmith 的评测体验确实顺滑数据集、实验、判官一应俱全。但它的观测定位更偏向开发-实验阶段。你在生产环境要长期保留全量 trace、按 session 回溯十几天的会话上下文、按业务线做成本聚合在 LangSmith 里做这些事就不太顺手。另外如果团队已经有自建监控体系要对接的往往是 OTel 标准而不是某个封闭的私有协议。LangSmith 的 trace 更偏向离线回放的附属产物而不是生产台账。结论是LangSmith 适合回答哪个 Prompt 更好不太适合回答线上今天发生了什么。2.3 OTel 是连接两者的翻译层这里要特别提一下 OTel。Langfuse 可以直接接收 OTel 协议的数据也就是说我不用在业务代码里同时维护两套埋点 SDK而是把模型调用的 span 按照 OTel 的 GenAI 语义约定输出再由采集器转发到 Langfuse。GenAI 语义约定里有几个关键属性gen_ai.system记录模型厂商gen_ai.request.model记录具体模型gen_ai.usage.input_tokens和gen_ai.usage.output_tokens记录 token还有请求和完成内容。Langfuse 识别这些属性后会自动映射成自己的 trace 结构和成本统计。另一个容易被忽略的点OTel 的 W3C traceparent 头能跨服务传递 trace 上下文。入口服务生成 trace_id 后无论后续调用发生在哪个微服务、哪次模型 SDK都归到同一条链路上。这是把业务会话和模型调用对应起来的基础也是后面从评测样本反查线上会话的关键。3. 三线打通从 SDK 埋点到 OTel 采集的落地配置3.1 用 Langfuse 原生 SDK 吃掉线上 trace线上核心链路我一开始直接用 Langfuse 原生 SDK。好处是接入成本极低一个装饰器就能把函数变成被观测的 spanfrom langfuse import Langfuse langfuse Langfuse( public_keypk-xxxx, secret_keysk-xxxx, hosthttps://lf.internal.example, # 自托管地址或云服务地址 ) langfuse.observe(namecrm_answer, capture_inputTrue, capture_outputTrue) def crm_answer(question: str) - str: resp model_client.chat.completions.create( modelassistant-model, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: question}, ], ) return resp.choices[0].message.content装饰器会自动记录函数入参、返回值、内部模型请求、token 用量和耗时。这里有个经验如果函数内部还调了其他服务或检索组件最好把它们也拆成独立函数再套一层 observe这样 trace 里能看清每一步的耗时而不是一个大黑盒。3.2 用 OTel Collector 统一采集再转发到 Langfuse后来接入的服务变多我希望所有上报统一走 OTel于是改成各服务先把 span 发给 Collector再由 Collector 转发到 Langfuse 的 OTel 接入端点。Collector 配置大致长这样receivers: otlp: protocols: http: endpoint: 0.0.0.0:4318 exporters: otlphttp: endpoint: http://langfuse-service:8080/api/public/otel headers: Authorization: Basic base64(pk:sk) service: pipelines: traces: receivers: [otlp] exporters: [otlphttp]服务端这边初始化 OTel SDK 并启用模型 SDK 的自动埋点from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace.export import BatchSpanProcessor provider TracerProvider(resourceResource.create({service.name: crm-answer})) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpointhttp://otel-collector:4318/v1/traces)) ) trace.set_tracer_provider(provider) # 启用模型 SDK 的自动埋点 model_instrumentor().instrument()启用后模型调用会自动生成带 gen_ai.* 属性的 span。这里有一个我踩过的坑自动埋点往往把 prompt 全文塞进 span 属性出于数据隐私和存储成本考虑建议在 Collector 里对 prompt 属性做截断或脱敏只保留前几百字符。这样 Langfuse 收到的数据量也会小很多页面加载明显更快。3.3 LangSmith 接入与实验项目隔离LangSmith 这边我只在做评测实验时才开启 tracing避免开发环境的噪音污染生产项目。核心是给每次实验配独立的 projectimport os from langsmith import Client os.environ[LANGCHAIN_TRACING_V2] true os.environ[LANGCHAIN_PROJECT] prompt-ab-24.8-v2 client Client()每次实验跑完立刻把实验结果导出存档并给 Prompt 版本打上 tag。这样哪个实验结果对应哪个 Prompt 版本是可追溯的。别小看这一步后面做回归对比时最怕的就是实验标签混乱。3.4 trace_id 贯穿把观测数据和业务会话绑死三条链路各自跑通还不够关键是串起来。我的做法是入口网关生成 request_id 和 session_id调用模型时把 session_id 传给 Langfuse 的 session 维度LangSmith 的每次评测输入则带上原始 trace_id 作为元数据。这样当某个评测样本表现异常时我能从实验记录反查到线上那条真实 trace看到当时的上下文、检索结果和完整会话。这一步的价值在排查问题时特别明显。有一次评测显示某个样本事实分骤降我顺着 trace_id 找到线上原会话发现是会话历史里混入了上一条用户消息的重复片段Prompt 把重复内容当成了最新指令。没有 trace_id 的贯通这种根因可能要翻半天日志。4. Prompt A/B 评测方案先定义好再谈对比4.1 评测维度质量、成本、延迟要一起看Prompt A/B 评测最忌讳只比答得对不对。Prompt 一变输出的长度、语气、引用方式、token 消耗都会跟着变。我常用的评测维度表是这样的维度评测方式说明事实一致性LLM 判官 引用编号核查答案是否严格基于给定资料指令遵循规则 判官是否按格式、长度要求输出语气与安全LLM 判官是否礼貌、无有害内容无效回答率规则是否答非所问或直接拒答token 成本Langfuse 聚合单次问答平均 token 数响应延迟Langfuse 聚合p50 / p95 耗时每个维度都要有明确的打分锚点而不是让判官随意打分。比如事实一致性我用 1-5 分并告诉判官1 分等于有杜撰事实3 分等于基本正确但缺少关键引用5 分等于完全正确且引用明确。锚点越具体判官越稳定。4.2 评测集怎么来从线上 trace 回捞真实样本评测集不能靠手工拍脑袋最有效的来源是线上 trace。我的流程是先在 Langfuse 里按分数筛选低分样本导出最近两周内被用户点过不采纳的会话再抽样一部分正常会话合并成评测集。比例上我建议真实分布占主导但刻意多放一些边界 case比如超长问题、空检索结果、多轮指代。构造样本时每条记录至少包含用户问题、必要的上下文、理想回答如果有真值、原始 trace_id。真值不必每条都有可以由判官对照上下文判断。没有真值的样本要在评测时由两个变体分别作答后比较而不是用一个固定的参考答案去套。4.3 离线回放还是线上分流Prompt A/B 有两条路径线上真分流和离线回放。我的建议是两步走先用离线回放做初筛用同一批历史数据分别喂给两个 Prompt结果可重复、成本可控、不打扰真实用户初筛通过后再切小流量线上验证比如按用户 id 尾号分流到新旧两个 Prompt观察真实反馈。离线回放最大的优势是公平两个变体面对的是完全相同的问题集对比的是纯能力差异。线上分流虽然更真实但流量不够时几天都攒不出统计结论而且一旦旧 Prompt 表现太差还得承担用户体验风险。5. 实录一次 Prompt A/B 评测24.8 版本迭代全流程5.1 实验设定基线 Prompt 与变体 Prompt这次迭代的内部代号是 24.8对应 24 年 8 月的版本计划。背景是客服问答系统被投诉回答啰嗦且偶尔编造政策。基线 Prompt V1 是一段很朴素的话你是一名客服助手请结合资料回答用户问题。变体 Prompt V2 做了三处修改要求必须引用资料编号不确定时必须明说根据现有资料无法确认回答控制在 200 字以内并分点。数据集用了 50 条线上样本40 条来自最近两周真实会话10 条是处理空检索、超长问题等边界的人工补充。每条样本带原始会话的 trace_id其中 12 条有业务侧维护的标准答案其余由判官对照上下文评分。5.2 用 LangSmith 批量跑分在 LangSmith 里注册数据集后写两个预测函数分别调用 V1 和 V2各跑一轮实验from langsmith import Client, evaluate client Client() DATASET crm_prompt_ab_248 def predict_v1(inputs: dict) - str: return call_llm(SYSTEM_PROMPT_V1, inputs[question], inputs[context]) def predict_v2(inputs: dict) - str: return call_llm(SYSTEM_PROMPT_V2, inputs[question], inputs[context]) results_v1 evaluate( predict_v1, dataDATASET, evaluators[factual_judge, style_judge, command_follow], experiment_prefixab248_v1, ) results_v2 evaluate( predict_v2, dataDATASET, evaluators[factual_judge, style_judge, command_follow], experiment_prefixab248_v2, )每个 evaluator 都是一个函数接收模型的预测输出和数据集里的参考信息返回一个分数。判官型 evaluator 本质上是另一次 LLM 调用所以评测本身也会产生 token 成本这个后面我会讲怎么控制。跑完后LangSmith 会给出两个实验在相同数据集上的平均分对比。V2 在事实一致性上以 4.3 分对 3.9 分领先指令遵循 4.6 分对 3.6 分领先语气分基本持平。5.3 回到 Langfuse 算成本与延迟账分数领先还不够还要看代价。评测跑分时我让两个变体的每次 LLM 调用都通过 OTel 上报到 Langfuse结束后直接看聚合结果V1 平均 780 token、p95 延迟 1.1 秒V2 平均 520 token、p95 延迟 850 毫秒。V2 因为限制了输出长度、减少了无效兜底反而同时压低了成本和延迟。这一步很多人会省掉但我强烈建议保留。质量分高但 token 贵一倍的新 Prompt在真实流量下未必合适反过来质量和成本同时占优的改动上线时才有底气。5.4 结果判读不要只盯着平均分平均分之外我还会看逐样本的胜负分布。两个变体在 50 条样本上两两对比V2 胜 35 条、平 10 条、负 5 条胜率 70%。负的那 5 条我逐条翻过有两种情况两条是 V2 过度引用编号回答显得机械三条是上下文里本身信息不足两个变体都只能说不确定V2 的表述更生硬但更安全。最终结论V2 整体优于 V1可以上线。但对于过度引用的问题我在 Prompt 里加了一句引用编号仅在明确对应时使用做了一个 V2.1 小版本再复跑这 5 条负样本确认修复后才全量切。6. 让评测结果可信的四个细节以及我踩过的坑6.1 评测集污染别让过去污染未来最常见的问题是评测集里的样本可能已经出现在线上 trace 里而你评测的新 Prompt 正是被这些 trace 调优出来的。如果调优过程依赖了评测集数据评测结果就会虚高。我建议把数据集分成调优时可见的样例和调优后锁定的评估集两块评估集在最终决策前不参与任何 Prompt 迭代。另一点容易被忽略不要用旧 Prompt 的输出作为新 Prompt 的参考标准除非你能确认旧输出确实正确。否则评测等于让新版本去贴近旧版本而不是逼近目标质量。换句话说参考标准应该来自业务侧的人工真值或权威答案而不是上一版模型说了什么。6.2 LLM 判官的排序偏置与冗长偏置用 LLM 当裁判省人力但它有自己的毛病。最典型的是位置偏置同样的两个回答放在后面位置时更容易被裁判判为更好。我验证过一次把顺序颠倒后胜负结果翻过来了将近两成。解决办法是每个样本跑两遍顺序对调两轮都胜才计为最终胜者平局概率会上升但结论更稳。另一个是冗长偏置裁判往往给更长的回答更高分。处理方式是给裁判设定严格长度约束并在评分标准里写明不要因回答更长而加分。我的判官 prompt 里固定有一段话你不是在挑选更详细的回答而是在挑选更准确的回答。每条回答独立评分不要假设排在后面的答案更好。如果两个回答质量相当请给相同分数。判官模型本身也要固定版本temperature 设为 0。评测期间不要换判官模型否则分数漂移会让你分不清是 Prompt 变了还是裁判变了。6.3 Prompt 版本管理比代码版本管理更容易被忽视Prompt 是纯文本改起来太容易导致很多人不把它当资产管理。我吃过亏有一次我直接在线上配置里改了系统提示词没有存档三天后效果回退想回滚却没有旧版本可用。现在的强制规范是所有 Prompt 改动必须走版本化通道Langfuse 的 Prompt 管理或 LangSmith 的版本快照都行但必须做到线上在跑哪个版本、对应哪次评测实验、改动人是谁三件事可查。每次 A/B 评测跑完我把两个 Prompt 的完整文本、评测数据集版本号、实验报告 URL 一起归档命名格式统一为crm_prompt_ab_248_v1_vs_v2。这个习惯后来帮了大忙半年后有人问当时为什么选 V2拉出归档就能完整还原决策过程。6.4 给闭环接上告警观测数据要用起来闭环的最后一公里是告警。评测集和 Prompt 都稳定后我写了一个每日任务从 Langfuse 拉取前一日线上 trace抽取样本交给同一套判官逻辑实时打分把当日的平均事实分、无效回答率和近 7 天基线对比。分数跌破阈值就告警到内部群附带上低分样本的 trace 链接。这个机制的价值在于Prompt 上线不是终点。模型服务方可能静默调整行为上游数据可能再次变更只有持续观测才能第一时间发现漂移。我们连续两周都被这个告警救过其中一次是新版知识库有 3% 的文档格式异常导致引用失效告警在业务投诉爆发前两天就响了。每周再看一眼 Langfuse 的成本聚合token 消耗异常上涨时也能及时定位到是哪个业务线的 Prompt 出了问题。7. 落地一年后回头看这套闭环真正改变了什么如果只能用一个词概括我会说从猜变成了测。以前团队改 Prompt讨论的是我觉得这样更好我试了一条还行。现在改 Prompt先写评测方案再拉数据集跑两轮对比拿分数说话。流程刚建立时大家嫌麻烦嫌50 条样本能说明什么但一个月后没人愿意回到盲改模式——因为被数据打脸的次数实在太多了每次盲改上线总能在评测集上找出几个之前没注意到的失败 case。我最深的体会是这套闭环的价值不是某一次评测跑得多漂亮而是它能被反复执行。50 条样本的评测集跑一次可能看不出威力但当评测集增长到几百条、覆盖各种边界场景时任何一次 Prompt 改动都能在半小时内得到可比较的结论。这种确定性才是团队最大的收益。最后给想入手的读者一个建议不要一开始就追求全自动化先手工跑通一遍线上捞样本、构造数据集、离线 A/B、上线、再观测的流程哪怕样本只有 20 条。流程跑通后再逐步加判官、加告警、加版本管理。我最初用了差不多两周才把链路完整打通但之后每一次 Prompt 迭代的决策时间从几天缩短到了几小时。如果你也在被Prompt 到底怎么改折磨这套组合值得试一次。