从链路追踪到自动化评估:LangSmith 构建 RAG 质量保障体系
1. 从一次 RAG 效果翻车说起为什么链路追踪救不了你的检索质量去年下半年我接手了一个内部知识库问答系统的调优工作。上线初期团队里所有人都觉得效果还行——毕竟 demo 阶段拿十几个标准问题测回答都挺像样。但真实用户一进来问题就藏不住了有人问报销流程里住宿标准那一条具体怎么规定的系统返回的答案引用了三个月前已经作废的旧版制度有人问一个跨部门协作的流程问题检索出来的三段内容分别来自三个不同部门的文档拼在一起逻辑互相打架。最离谱的一次用户问年假结转的截止日期系统信誓旦旦地给了一个日期结果那个日期是从一份培训通知里断章取义出来的。那段时间我每天的工作就是翻日志。传统的做法是看应用日志里打印的检索结果和最终回答但这种方式有个致命问题你只能看到输入和输出中间发生了什么完全是个黑盒。检索器到底召回了哪些文档重排序之后顺序变成了什么样大模型是基于哪几段上下文生成的答案这些信息散落在不同的日志文件里靠时间戳去拼效率极低而且经常对不上。这就是链路追踪Tracing要解决的核心痛点。LangSmith 最初打动我的地方就是它能把一次完整的 RAG 调用拆解成一棵可视化的调用树从用户输入开始经过查询改写、向量检索、重排序、上下文组装、大模型生成每一个环节的输入输出、耗时、token 消耗都清清楚楚地挂在树上。你点开任意一个节点就能看到那个环节到底吃了什么、吐了什么。但用了一段时间之后我发现光有链路追踪还不够。链路追踪解决的是看得见的问题它让你知道系统里发生了什么但它不告诉你好不好。你能看到检索器召回了 5 篇文档但你不知道这 5 篇里有几篇是真正相关的你能看到大模型生成了答案但你不知道这个答案有没有胡编。换句话说链路追踪是显微镜评估才是尺子。没有尺子你只能靠肉眼判断而肉眼在几十上百条测试用例面前很快就会失效。所以这篇文章我想聊的是我怎么从单纯用 LangSmith 做链路追踪一步步走到用它搭建 RAG 自动化评估体系的完整过程。这里面有工具选型的思考有数据集构建的踩坑有评估器设计的取舍也有把评估跑进 CI 之后带来的工作方式变化。如果你也在做 RAG 系统或者任何需要多步骤编排的大模型应用这套思路应该能直接拿去用。2. LangSmith 的追踪模型Run、Trace、Project 到底怎么组织在讲评估之前得先把 LangSmith 的数据组织方式说清楚否则后面聊评估数据集和实验的时候容易懵。LangSmith 的核心概念其实就三个Run、Trace、Project但它们的层级关系和适用场景官方文档讲得比较散我按自己的理解重新梳理一遍。2.1 Run 是最小执行单元但它的类型决定了你能看到什么一个 Run 代表一次具体的执行动作。比如你调用了一次检索函数这是一个 Run你调用了一次大模型这也是一个 Run。每个 Run 有自己的类型run_type常见的有llm、chain、tool、retriever、embedding等。这个类型不是随便标的它直接影响 LangSmith 在界面上怎么渲染这个节点——llm类型的 Run 会显示 token 数和成本retriever类型的 Run 会显示召回的文档列表。我踩过的一个坑是早期我用 LangChain 的 LCEL 写链路很多自定义函数没有显式标注 run_type结果在 LangSmith 里全部显示成默认的chain点进去只能看到输入输出看不到我想要的文档结构化展示。后来我养成了一个习惯凡是涉及检索、嵌入、工具调用的函数都用traceable装饰器显式指定 run_type这样在追踪树里一眼就能定位到关键环节。from langsmith import traceable traceable(run_typeretriever, namehybrid_search) def hybrid_search(query: str, top_k: int 5): # 向量检索 关键词检索融合 ... return documents这里name参数也很重要。默认情况下 LangSmith 会用函数名作为节点名但如果你有多个检索器比如一个查制度库、一个查产品文档函数名可能都是search在追踪树里就分不清了。显式命名能省掉大量排查时间。2.2 Trace 是一棵调用树父子关系靠上下文自动串联一次完整的用户请求会产生一个 Trace它是一棵有向树。根节点通常是你的入口函数子节点是它调用的各个环节。LangSmith 的巧妙之处在于父子关系的建立基本是自动的——只要你用 LangChain 的组件或者traceable装饰器运行时的上下文会自动把嵌套调用串起来。但自动串联有个前提调用必须发生在同一个执行上下文里。我遇到过一种情况为了做并行检索我用asyncio.gather同时发起多个检索请求结果这些检索的 Run 在追踪树里变成了平级的兄弟节点而不是挂在同一个父节点下。这本身不算错但如果你想让它们归属清晰需要手动用trace上下文管理器包一层。from langsmith import trace def parallel_retrieve(queries): with trace(namemulti_query_retrieve, run_typechain) as run: # 在这里发起的子调用都会挂到这个节点下 results asyncio.gather(*[search(q) for q in queries]) return results这个细节在单路检索时无所谓但一旦你做了多路召回或者查询改写追踪树的结构清晰度就很重要了否则评估的时候你根本分不清哪路召回贡献了有效文档。2.3 Project 是隔离边界环境、版本、实验都靠它区分Project 是 Run 的集合你可以把它理解成一个命名空间。我一般会按环境 用途来划分 Project比如rag-prod、rag-staging、rag-eval。这样做的好处是生产环境的追踪数据不会和评估实验混在一起查问题的时候不会被噪音干扰。这里有个容易被忽略的点LangSmith 的评估实验Experiment本质上也是往某个 Project 里写 Run只不过这些 Run 会带上实验标记。所以如果你把评估实验和生产追踪放在同一个 Project界面会变得很乱。我的做法是专门开一个rag-evalProject 跑评估生产环境用rag-prod两边互不干扰。概念层级核心作用常见误区Run最小单元记录单次执行不标 run_type 导致展示不全Trace调用树串联完整链路并行调用时父子关系断裂Project命名空间隔离环境与实验评估和生产混用导致界面混乱把这三个概念理清楚之后链路追踪的部分基本就够用了。你能看到每次请求的完整路径能定位到具体哪个环节出了问题。但正如开头说的看到问题不等于能衡量问题接下来才是重头戏——怎么把感觉效果不好变成检索命中率 62%需要优化。3. 评估数据集RAG 评估的地基也是最容易偷懒的地方我见过不少团队做 RAG 评估数据集就是随手从生产日志里捞几十条用户问题然后人工写个参考答案就开始跑评估了。这种做法不是不行但很快会遇到两个问题一是覆盖面太窄生产日志里的问题往往集中在少数几个高频场景长尾问题根本覆盖不到二是参考答案质量参差不齐不同人写的标准不一样导致评估结果波动很大。3.1 数据集要分层黄金集、回归集、对抗集各司其职我的做法是把评估数据集分成三层每层的目的和构建方式都不一样。黄金集Golden Set是核心中的核心通常 50 到 100 条每条都经过仔细标注。它覆盖的是系统最核心的能力场景比如制度查询、流程问答、数据检索。黄金集的每一条都要有明确的标准答案而且答案要能追溯到具体的源文档片段。这个集子构建起来最费时间但它是衡量系统整体水平的基准线不能省。回归集Regression Set是从历史 bug 里沉淀出来的。每次线上发现一个 bad case修复之后就把这个 case 加进回归集。它的作用是防止老问题复发。回归集不需要像黄金集那样精雕细琢但每一条都要记录清楚当初为什么错和修复后应该是什么样。对抗集Adversarial Set是我后来才加进去的专门用来测系统的边界。比如故意问一个知识库里根本没有的问题看系统会不会硬编一个答案或者问一个措辞模糊、有多种理解方式的问题看系统会不会只抓住其中一种理解。对抗集的答案往往不是标准答案而是期望行为比如应该回答不知道或者应该追问澄清。数据集类型规模建议构建来源核心目的黄金集50-100 条人工精心标注衡量整体能力基准回归集持续增长历史 bad case防止问题复发对抗集20-30 条主动设计边界场景测试鲁棒性3.2 用 LangSmith 管理数据集别把 CSV 当数据库早期我用 CSV 管理评估数据每次跑评估都要手动读文件、拼 prompt、跑完再写回结果流程极其繁琐。后来转到 LangSmith 的数据集功能最大的好处是数据集和实验结果是关联的——你跑完一次实验能直接在界面上看到每条用例的输入、期望输出、实际输出还能对比不同实验之间的差异。在 LangSmith 里创建数据集很简单可以手动加也可以从已有的追踪记录里批量导入。我常用的方式是从生产追踪里筛选出有代表性的 Trace直接转成数据集条目。这样构建的数据集天然带有真实的输入分布比凭空想要靠谱得多。from langsmith import Client client Client() # 从生产追踪中筛选并创建数据集 dataset client.create_dataset( dataset_namerag-golden-set-v1, descriptionRAG 系统黄金评估集覆盖制度查询与流程问答 ) # 批量添加用例 client.create_examples( inputs[{question: 住宿报销标准是多少}, ...], outputs[{answer: 根据XX制度第X条...}, ...], dataset_iddataset.id )这里有个实操心得数据集的版本要管起来。我一般用rag-golden-set-v1、v2这样的命名每次对数据集做实质性修改比如修正了某条标准答案、新增了一批用例就升一个版本。这样当你发现评估分数突然变化时能快速判断是系统改了还是数据集改了。我吃过这个亏有一次分数掉了 8 个点排查了半天以为是检索出了问题最后发现是有人往数据集里加了一批特别难的用例。3.3 标准答案的标注写应该包含什么比写完整答案更实用标注标准答案这件事我走过一段弯路。一开始我要求标注人员写出完整的标准回答结果发现两个问题一是不同人的表达风格差异很大评估器很难判断实际输出和标准输出是不是一个意思二是写完整答案太耗时标注 100 条要花好几天。后来我改成要点标注法不写完整答案而是列出这条问题的答案必须包含哪几个关键要点。比如住宿报销标准这条要点是一线城市 500 元二线城市 400 元需提供发票。评估的时候看实际输出有没有覆盖这些要点而不是逐字比对。这种方式的另一个好处是它天然适配基于大模型的评估器。你可以让评估模型判断实际输出是否包含了以下要点这比让它判断两个答案是否语义一致要稳定得多。语义一致性判断很容易受表达风格影响而要点覆盖判断的标准更客观。4. 评估器设计从字符串匹配到 LLM-as-Judge 的取舍数据集准备好之后下一个问题就是怎么判断一次回答是好是坏。这个问题看起来简单实际上坑很多。我经历过三个阶段纯字符串匹配、基于嵌入的相似度、基于大模型的评判。每个阶段都有它的适用场景和局限。4.1 字符串匹配和嵌入相似度快但不够聪明最开始我用的是最朴素的方法看实际输出里有没有包含标准答案的关键词。这种方法跑得飞快成本几乎为零但误判率很高。比如标准答案是需要提供发票实际输出是报销时必须附上票据意思完全对但关键词匹配不上被判为错误。后来我换成基于嵌入的相似度把实际输出和标准答案都转成向量算余弦相似度超过某个阈值就算通过。这比关键词匹配好一些但依然有问题嵌入相似度高不代表语义正确。我遇到过实际输出和标准答案都在讲报销但一个讲的是住宿标准一个讲的是交通标准嵌入相似度却不低因为它们的用词和句式很像。这两个阶段的教训是对于 RAG 这种需要精确事实的场景浅层的文本相似度不够用。RAG 的核心价值在于准确引用知识库内容而准确与否往往体现在细节上细节恰恰是嵌入相似度最容易忽略的地方。4.2 LLM-as-Judge强大但需要精心设计评判标准转到 LLM-as-Judge 之后评估的准确度上了一个台阶。核心思路是让一个能力较强的大模型来当裁判给它看问题、标准答案或要点、实际输出让它判断实际输出是否合格。但直接让模型判断好不好是不行的它会给出非常随意的结果。关键在于把评判标准拆解成具体的维度每个维度给出明确的判断依据。我目前用的评判维度主要有四个忠实度Faithfulness实际输出中的事实性陈述是否都能在检索到的上下文中找到依据。这个维度专门抓胡编问题。评判时把检索到的文档片段和实际输出一起给模型让它逐句检查有没有无中生有的内容。答案相关性Answer Relevance实际输出是否直接回答了用户的问题有没有答非所问或者绕圈子。这个维度抓的是检索对了但生成跑偏的情况。上下文精度Context Precision检索到的文档里有多少是真正相关的。这个维度评估的是检索器的质量和生成质量分开看。上下文召回Context Recall标准答案需要的信息有多少能在检索到的文档里找到。这个维度抓的是检索漏了关键文档的问题。from langsmith.evaluation import RunEvaluator from langchain_openai import ChatOpenAI class FaithfulnessEvaluator(RunEvaluator): def __init__(self): self.judge ChatOpenAI(modelgpt-4, temperature0) def evaluate_run(self, run, example): context run.outputs.get(context, ) answer run.outputs.get(answer, ) prompt f请判断以下回答中的事实性陈述是否都能在给定上下文中找到依据。 上下文 {context} 回答 {answer} 请输出 JSON{{faithful: true/false, reason: 判断理由}} 只输出 JSON不要有其他内容。 result self.judge.invoke(prompt) # 解析结果并返回评分 ...这里有个关键细节temperature 一定要设成 0。评估器需要的是稳定、可复现的判断任何随机性都会让评估结果失去参考价值。我试过 temperature 0.7 的评估器同一条数据跑两次结果不一样这种评估还不如不做。4.3 评估器的校准用人工标注验证机器判断LLM-as-Judge 最大的风险是评估器本身不准。你用它来评判系统但它自己可能就在胡说。所以评估器上线前必须做校准拿一批人工已经判断过好坏的用例让评估器跑一遍看它和人工判断的一致率有多高。我的经验是一致率低于 80% 的评估器不能直接用需要调整 prompt 或者换模型。校准过程中最常见的分歧点是边界情况——比如实际输出包含了标准答案的要点但表述方式很别扭人工可能判合格评估器可能判不合格。这种分歧需要具体分析如果是评估器太严格就放宽标准如果是人工太宽松就统一标注规范。还有一个实用技巧让评估器输出判断理由。当评估结果和你的直觉不符时看理由能快速定位是评估器理解错了还是系统真的有问题。我遇到过评估器把根据制度规定这种套话当成事实性陈述判为不忠实看了理由才发现是 prompt 里没排除这类引导性表述。5. 把评估跑进 CI让每次改动都有据可依评估体系搭好之后如果只是手动跑价值会大打折扣。因为人是有惰性的改完代码之后往往觉得这次改动不大应该没问题就跳过评估直接上线。等线上出问题再回头补评估成本高得多。所以我的做法是把评估集成到 CI 流程里让每次代码提交都自动跑一遍核心评估集。5.1 评估触发的时机不是每次提交都要跑全量全量评估黄金集 回归集 对抗集跑一次成本不低如果每次提交都跑既费钱又拖慢开发节奏。我的策略是分层触发每次提交只跑回归集因为回归集规模小、针对性强能快速发现明显的问题。合并到主分支前跑黄金集 回归集确保核心能力没有退化。发版前跑全量包括对抗集做一次全面体检。这种分层策略在 LangSmith 里可以通过不同的数据集和实验配置来实现。CI 脚本里根据触发条件选择对应的数据集。5.2 评估结果的判定阈值怎么定才合理评估跑完会得到一堆分数但多少分算通过这个问题没有标准答案需要根据业务场景来定。我的做法是核心指标设硬阈值。比如忠实度必须达到 95% 以上低于这个值直接判定失败阻止合并。因为忠实度低意味着系统在胡编这是不可接受的。辅助指标设软阈值。比如上下文精度达到 70% 就算通过但会在报告里标黄提醒关注。因为检索精度受知识库质量影响有时候不是代码的问题。和基线对比。除了绝对阈值还要看和上一次评估的对比。如果某个指标掉了超过 3 个百分点即使还在阈值之上也要标记出来人工确认。因为突然的下降往往意味着有隐藏的问题。指标阈值类型建议阈值失败处理忠实度硬阈值≥ 95%阻止合并答案相关性硬阈值≥ 90%阻止合并上下文精度软阈值≥ 70%标记关注上下文召回软阈值≥ 80%标记关注5.3 评估报告怎么读别只看总分评估报告最容易犯的错误是只看总分。总分 85 分看起来不错但可能意味着核心场景 95 分、长尾场景 60 分。这种不均衡在实际使用中会带来很差的体验因为用户遇到长尾问题的概率并不低。我读报告的习惯是先看总分有没有跌破阈值然后按场景分组看细分分数最后看具体失败的用例。LangSmith 的实验对比功能在这里很好用能直接看到两次实验之间哪些用例从通过变成了失败哪些从失败变成了通过。那些翻转的用例是最值得关注的它们往往揭示了改动的真实影响。还有一个细节评估结果要存档。LangSmith 会自动保存每次实验的结果但我会额外把关键指标导出到自己的监控系统里画成趋势图。这样能看出系统的长期变化趋势而不是只关注单次结果。有一次我发现上下文召回率在过去一个月里缓慢下降了 5 个百分点排查后发现是知识库在持续新增文档但检索器的索引没有及时更新。这种缓慢退化只看单次评估是发现不了的。6. 踩过的坑评估体系搭建中的真实教训聊了这么多方法论最后分享几个我在实操中踩过的坑。这些坑在官方文档里基本不会提但每一个都让我付出了实实在在的时间成本。6.1 评估器的不稳定性同一个输入两次结果不一样前面提过 temperature 要设 0但即使设了 0某些模型在长上下文下依然会有轻微的不确定性。我遇到过最诡异的一次同一条用例评估器第一次判合格第二次判不合格理由是回答中有一处表述不够精确。回去看那条回答两次评估之间系统代码根本没变。后来我的解决办法是对关键指标做多次评估取多数。比如忠实度这个指标跑三次评估两次以上判合格才算合格。这增加了成本但对于核心指标来说是值得的。另一个办法是换更稳定的模型做评估器实测下来参数量更大的模型在判断一致性上确实更好。6.2 数据集泄露评估集和训练集的重叠这个问题比较隐蔽。有一次我们为了提升效果拿一批历史问答数据做了微调。微调之后评估分数大涨大家都很开心。但上线之后发现实际效果并没有提升甚至有些场景还变差了。排查后发现微调用的数据里有一部分和评估集高度重叠相当于考试前把答案泄露给了学生分数自然好看。从那以后我养成了一个习惯维护评估集的时候记录每条数据的来源。如果某条数据来自生产日志而生产日志又被用于微调那这条数据就要从评估集里剔除或者至少标记出来。评估集必须保持系统没见过的状态否则评估就失去了意义。6.3 评估成本失控一次全量评估烧掉的钱LLM-as-Judge 虽然好用但成本不低。我算过一笔账黄金集 100 条每条要跑 4 个评估维度每个维度一次大模型调用再加上系统本身的生成调用一次全量评估大概要几百次大模型调用。如果用能力较强的模型做评估器一次全量评估的成本相当可观。控制成本的办法有几个一是分层触发前面说过了二是评估器分级核心指标用强模型辅助指标用轻量模型三是缓存对于没有变化的用例复用上次的评估结果。LangSmith 本身有缓存机制但需要配置好。我现在的做法是只有系统输出发生变化的用例才重新评估输出没变的直接复用历史结果。6.4 评估和追踪脱节发现问题却定位不到原因评估告诉你这条用例失败了但不会告诉你为什么失败。这时候就需要链路追踪来补位。我的工作流是评估发现失败用例点进对应的 Trace看检索召回了什么、重排序怎么排的、生成时用了哪些上下文。评估和追踪结合才能形成发现问题—定位原因—修复验证的闭环。这里有个小技巧在评估时把 Trace ID 记录下来。LangSmith 的实验结果里每条用例都关联着对应的 Run点进去就能看到完整的追踪树。我还会在评估报告里直接附上失败用例的 Trace 链接这样排查的时候不用再去翻记录。7. 一些让评估体系更耐用的个人习惯这套评估体系跑了大半年中间经历过几次大的调整也沉淀了一些让维护成本更低的习惯。评估集要定期体检。知识库在变、业务在变评估集如果一直不更新会逐渐和实际场景脱节。我每个季度会花半天时间过一遍评估集把已经不适用的用例删掉补充新的场景。那些长期稳定通过的用例如果业务上已经不重要了也可以考虑移除避免评估集越来越臃肿。评估器的 prompt 要版本化。评估器的判断标准会随着理解加深而调整每次调整 prompt 都要记录改了什么、为什么改。我见过因为评估器 prompt 被悄悄改了导致评估分数波动排查了半天才发现是评估器本身变了。现在我的评估器 prompt 都放在代码仓库里和系统代码一起做版本管理。别追求 100% 的自动化。评估体系能自动化大部分判断但总有一些边界情况需要人工介入。我的做法是设置一个人工复核队列评估器判断置信度低的用例自动进入队列由人工确认。这样既保证了效率又避免了评估器误判带来的错误决策。评估结果要能追溯到人。每次评估跑完结果会自动发到团队频道失败用例会 对应的负责人。这不是为了追责而是为了让问题能被及时看到和处理。评估体系最大的价值不是打分而是让质量问题变得可见、可追踪、可改进。这套从链路追踪延伸到自动化评估的实践核心逻辑其实很简单先看得见再量得准最后管得住。LangSmith 提供的追踪能力让你看得见系统内部发生了什么评估体系让你能量准系统到底好不好CI 集成让你能管住每次改动不退化。三步走下来RAG 系统的迭代就从凭感觉变成了看数据效率和质量都会有质的提升。