
RAG 这个赛道从来不缺模型和框架缺的是当它答错时你能第一时间指出错在哪一环。很多做 RAG 项目的朋友估计都有过这种经历向量库建了prompt 也调了好几轮用户一问它还是能一本正经地编答案。这时候你缺的往往不是更贵的模型而是一台能贴在链路上的心电监护仪。LangSmith 就是干这个的它面向 LangChain 生态也支持原生 API 接入核心能力是追踪每一次检索和生成的全过程把 RAG 从黑盒变成可复盘的透明链路。这篇文章基于我最近一个知识库问答项目的真实排查经历聊聊全链路观测怎么落地以及那些文档里不太会写的细节。适合正在做 RAG 项目、被检索对不对、引用准不准折磨的开发者和算法工程师参考。1. 先说说 RAG 的心电监护为什么难装1.1 三段式链路里最容易查不出问题的环节RAG 的标准流程大家都熟先召回Retrieve再增强Augment最后生成Generate。理论上链路清晰、边界分明但实际一调就发现问题根本不会乖乖待在某一层里。检索阶段返回了一堆语义相近但其实没用的文档生成阶段大概率会把这些文档里的内容复述出来于是最终答案看起来有理有据实际上全是干扰信息。反过来也一样检索结果明明是对的但 prompt 里文档顺序不对、上下文被截断模型照样会答偏。这种问题之所以难查是因为每个环节都在消化上一个环节的输出。错误信号经过一次文本拼接、一次注意力加权之后早就被稀释得看不出原形了。你盯着最终答案猜了很久才意识到应该往回看到底是这一步错了还是上一步喂进来的东西就有问题没有观测手段的时候排查全靠脑补效率极低。我当时第一个项目就这么翻车的。用户问了一个制度条款里的适用条件系统返回了一段看起来很像的公告内容模型也照着公告答了但真正的约束条件在另外一篇文档里。我花了一下午调 prompt结果一点用都没有。后来把检索结果单独打印出来才发现top1 和 top2 的语义相关度相差很大真正该被召回的段落排名太靠后压根没进上下文。1.2 RAG 瓶颈通常在检索而不是生成我自己的项目里八成的 RAG 翻车都发生在检索侧而不是模型侧。这不是说生成阶段不会出错而是说检索环节出错时生成环节一定会乖乖配合——它会把错误上下文包装成流畅回答反而让问题更难暴露。这也是为什么很多人一上来就怀疑 prompt 或模型却忽略了检索召回本身就已经偏了。检索侧的瓶颈又往往和知识库形态强相关。纯文档切块后做的向量召回适合语义相似这类开放场景比如找相关新闻、相似问答。但一旦涉及结构化的约束关系比如某条规则适用于哪些对象某个流程的前置条件是什么纯向量召回很容易翻车因为它对实体关系和逻辑约束不敏感。这时候就要考虑把非结构化知识库和结构化知识库区分开非结构化走向量检索结构化走图谱查询或规则引擎甚至用 ontology 这类本体建模来做约束推理。全链路观测要做的第一件事就是把这两种不同来源的检索结果都纳入 trace否则你根本不知道答案是基于哪条路径拼出来的。另外chunk 大小、重叠窗口、embedding 模型选择这些偏基建的参数也会直接影响召回质量。它们不体现在单次调用报错里而是藏在相似度分数和召回文档排名的变化中。没有观测工具之前这些参数几乎是盲调有了全链路观测之后你才能把参数改动和检索质量变化对上号。2. LangSmith 到底观测了什么2.1 从 Trace 看一次 RAG 的完整旅程LangSmith 的核心概念是 Trace翻译过来就是追踪记录。每当你发起一次 RAG 请求它会把这次请求拆成一棵调用树从最外层的问题到中间的召回、prompt 拼装再到最内层的模型调用全部串起来。每个节点都记录了开始时间、结束时间、输入、输出、token 消耗以及当时使用的模型名、温度这类参数。我最初接入 LangSmith 时第一反应是这不就是个日志面板吗。但真正在项目里跑了几轮之后才发现Trace 的价值不在于记录而在于对照。你可以把一次失败回答的完整链路展开看到用户问题进入检索器之后返回了哪些文档、每个文档的相似度评分是多少、文档被塞进 prompt 后实际占了多少字符、模型读取到第几段之后开始输出。这个过程就像把一次手术的全程录像翻出来每一步操作都有时间戳而不是只给你一张术后报告。更实用的是Trace 天然支持嵌套。你可以在一次 RAG 请求里同时看到外部检索函数调用和内部大模型调用两条子链路。这意味着你不用为了观测而拆散原有代码结构只要在关键位置埋点就能保持调用关系清晰。对已经有业务系统的团队来说这一点很重要因为大规模重构链路观测往往比想象中成本高。2.2 核心对象Project、Run、Span、FeedbackLangSmith 里几个最重要的概念先理清楚再动手会省很多事。Project 是隔离单位。你可以开一个rag-dev项目做本地调试再开一个rag-prod项目观察线上。两者数据互不干扰对比起来也很方便。我一般还会按版本开项目比如rag-v1-experiment、rag-v2-reranker方便回溯是哪一版改动导致指标跳变。Run 是一次最小执行单元比如一次向量检索、一次大模型调用。Span 则是 Run 之间的关系。放在一起看的话Run 是节点Span 是边整棵调用树就是一次 Trace。LangChain 框架接入 LangSmith 时会自动生成这些 Run不需要你手动去定义 Span 结构。Feedback 是人工或自动给某次运行打的标注。你可以给一条回答标好/坏也可以打一个 1 到 5 分的相关性评分还可以附上文字解释。这些标注会反过来成为评估集的样本用来做离线回归测试。我把 Feedback 理解为医生在病历本上的批注——光有心电波形不行还得有医生写下此刻病人状态如何、为什么这么判断。2.3 可观测性不是监控是可复盘很多人会把监控和可观测性混为一谈。监控解决的是挂了没比如接口超时率是否上升、错误数是否暴增。可观测性解决的是为什么挂需要你在没有预设答案的情况下通过 trace 数据反推出当时的系统状态。RAG 项目里后者远比前者重要。原因很简单RAG 的错误不是二值的。你不会看到接口直接 500你会看到它返回了一个看似合理但实际错误的答案。这种错误没法靠健康检查发现只能靠事后复盘。LangSmith 做的就是把每次运行变成一份可以反复查看的病例档案让你在用户投诉之后还能还原出当时检索到了什么、模型为什么这么答。接入 LangSmith 还有一个隐形好处它默认不改变你的链路逻辑只做埋点和采集。也就是说你可以先加上观测再慢慢优化检索策略而不是一上来就重构整个 RAG 流程。对于已经有线上服务的团队这种渐进式改造的接受度会高很多。3. 实操接入给 RAG 链路装上探针3.1 环境准备与最小接入先装 Python 包我习惯用 pip 装最新版langsmith同时确保langchain版本不要太旧因为旧版本对 tracing 的支持不太完整。pip install -U langsmith langchain langchain-openai langchain-chroma然后在项目入口设置环境变量。这里有个细节旧版 LangChain 用LANGCHAIN_TRACING_V2true新版也开始兼容LANGSMITH_TRACINGtrue。我建议两种都写上避免版本切换时踩坑。import os os.environ[LANGCHAIN_TRACING_V2] true os.environ[LANGSMITH_TRACING] true os.environ[LANGCHAIN_PROJECT] rag-observation os.environ[LANGCHAIN_API_KEY] lsv2_你的密钥如果你的 RAG 代码是基于 LangChain 的create_retrieval_chain这类高层 API 搭建的设置完环境变量之后基本就能自动出 trace不需要额外改调用代码。这一点对新手很友好也是我推荐先从 LangChain 入手的原因。但如果你和我一样链路里混了不少自定义函数比如自己写的重排序逻辑、自建的知识库查询接口就需要手动埋点了。LangSmith 提供了traceable装饰器给函数加一行就能纳入追踪。from langsmith import traceable traceable def my_retrieve(query: str): # 这里写你自己的检索逻辑 return results这样装饰之后my_retrieve的输入输出、耗时、异常信息都会出现在 trace 里。自定义业务代码和框架代码之间不再有观测盲区这是全链路观测真正能落地的关键一步。3.2 分步埋点Retriever 和 Generator 分开看很多人第一次接入 LangSmith 时只满足于有 trace 了但真正排障时发现不够用。问题在于高层 API 自动生成的 trace 虽然完整但信息太粗。比如 retriever 返回的文档列表默认记录的是Document对象你要看每个 chunk 的相似度得分、排名、来源还得自己拆。我的建议是在关键节点做分步埋点把检索侧和生成侧分开。检索侧我一般会包一层自定义函数输出结构化结果from langsmith import traceable traceable def retrieve_with_scores(query: str, top_k: int 5): # 假设 vector_store 是已初始化的向量库客户端 docs vector_store.similarity_search_with_relevance_scores( query, ktop_k ) return [ { doc_id: doc.metadata.get(id), source: doc.metadata.get(source), score: round(score, 4), content_preview: doc.page_content[:100], } for doc, score in docs ]这样设计的好处是trace 里可以直接看到当前 query 召回了哪些文档、每个文档的得分如何、来自哪个源文件而不需要再到原始日志里翻。我踩过一次坑没有给文档打 doc_id结果一次 badcase 里出现了两个同名文档根本分不清是哪一份。后来强制给所有 chunk 加全局唯一 ID排查效率立刻上来了。生成侧也要单独埋点。我的习惯是把生成前的 prompt和生成后的答案都记录下来。因为很多时候你会发现问题不在检索也不在模型而是 prompt 里塞了太多无关上下文把模型注意力带偏了。traceable def generate_answer(prompt: str): response llm.invoke(prompt) return { answer: response.content, usage: response.usage_metadata, # token 数统计 }把检索结果和生成答案分开之后你再回头看 trace 时就能做交叉验证如果检索文档得分很高但答案仍然出错问题大概率在 prompt 组装或模型侧如果检索文档本身相关度就很低那再怎么调 prompt 都是白费功夫。3.3 在 Mac 上本地调试的一点建议热词里有人问怎么在 Mac 上搭建 RAG 知识库这个我确实有发言权因为我大部分开发就是在 Mac 上做的。本地调试 RAG 项目首要原则是别把资源耗在启动模型上。Mac 上跑一个 7B 模型做生成除非是 M 系列高配芯片否则会拖慢整个调试节奏。我的做法是本地用轻量 embedding 模型加一个文件型向量库生成环节直接调用云端模型服务这样既能在本地快速验证检索逻辑又能保证生成效果和线上一致。依赖安装这块Mac 上主要注意 Apple Silicon 的兼容性。Chroma、FAISS、sqlite-vec 这些向量库大部分都已经提供 ARM 的预编译包直接装就好。如果遇到某些老库编译失败先检查是不是 Rosetta 环境问题我通常是尽量用原生 ARM 的 Python 环境而不是在终端里开 x86 模式硬编。另外LangSmith 在本地调试时完全可以继续用免费额度足够个人开发。不过有一点要提醒API Key 不要写死到代码里更不要提交到 Git 仓库。我见过不止一次有人把密钥放在 notebook 里顺手发到共享文档上结果整个项目的调用记录全部暴露。建议用.env文件加python-dotenv加载同时在.gitignore里把.env过滤掉。这属于最基础的安全习惯但在 RAG 项目里尤其容易被忽略。4. 全链路观测的关键指标与阈值4.1 检索侧看召回率和相关性有了 Trace 不等于会看数据你还需要定义指标。检索侧我通常关注两个东西一个是相关性得分一个是召回的覆盖率。相关性得分直接看similarity_score但这个分数跟你用的 embedding 模型强相关不能跨模型硬比。比如我用某开源 embedding 时相似度 0.35 已经属于强相关换成某商业模型的向量空间后0.35 可能只是勉强沾边。所以我会先跑一批样本看正样本和负样本的分数分布再定一个当前项目专属的及格线。没有这个基线你真不知道 0.6 到底是好是坏。召回覆盖率则要结合评估集来看。我会准备一小批已知标准答案的问题比如十到二十条每条问题都预先标注好应该命中哪些文档。跑完 trace 后看这些文档是否出现在检索结果里。如果标准文档排名在 top5 之外基本可以判定 chunk 切分方式或 query 改写有问题。实话说检索这一步是 RAG 优化里性价比最高的环节。调 prompt 可能调十版才有 2% 的提升但把 chunk 切分方式从固定 500 字改成按语义段落切分再加重叠检索命中率往往能明显改善。全链路观测的作用就是让这种改善从感觉变成数据。4.2 生成侧看忠实度和答案相关性生成侧有两个容易混淆的指标一个是忠实度faithfulness一个是答案相关性answer relevance。前者关心答案是否忠于检索到的上下文后者关心答案是否回答了用户的问题。这两者可以互相独立模型可能忠实复述了检索文档但那篇文档压根文不对题模型也可能答非所问但答出来的内容确实来自上下文。LangSmith 的评估功能里就有对应的自动评估器基于大模型做打分。我一般把它们配置成两个独立的评估步骤from langsmith.evaluation import evaluate, LangchainStringEvaluator faithfulness LangchainStringEvaluator(criteria, config{criteria: faithfulness}) answer_relevance LangchainStringEvaluator(criteria, config{criteria: answer_relevance})在配置评估器时我想提醒一句评估用的模型最好不要和生产模型完全相同。否则你可能会遇到模型自己评价自己的回答怎么看怎么顺眼的盲目自信问题。我习惯用一个参数风格差异较大的模型来做打分这样至少能暴露部分自嗨倾向。日常观测中我还会盯生成环节的 token 数和耗时。如果某次回答的 token 数突然比同类问题高出一大截通常意味着 prompt 塞进了过量上下文或者模型进入了自我重复的循环。这时候不需要看答案内容只看数字就能发现异常。4.3 手动打分与自动化评估互补自动化评估处理批量问题很高效但我不建议完全依赖它。因为基于 LLM 的评估器对事实性错误的敏感度并没有想象中高。模型生成的答案只要文从字顺、结构完整自动评估器打分经常偏高。真正能暴露问题的反而是人工点开某条 trace看到检索文档 A 里根本没有这条结论但模型概括得像真的一样。所以我现在的做法是双轨并行线上跑自动化评估做第一轮筛选每天抽几条异常样本到 LangSmith 的 Feedback 面板里人工打分并写上备注。这些人工标注会沉淀成高置信度的回归测试集用来做后续版本对比。这个反馈闭环的价值比单次 trace 大得多因为每次你新增一条人工 badcase都是在给项目加一个再也不会犯同类错误的保险。一开始可能觉得人工标注很费时间但实际操作下来每天十到二十条样本几分钟就完事。这点时间的投入远比上线后用户投诉再返工要划算。5. 常见问题与排查技巧实录5.1 问题速查表我把 RAG 全链路观测中最高频的几个问题整理成了速查表都是自己项目里真实遇到过的场景。现象可能原因排查动作检索文档得分普遍很低embedding 模型和领域不匹配换行业语料微调的 embedding 模型答案引用了不存在的细节上下文截断模型脑补检查 prompt 里实际塞入的文档长度检索结果重复top5 里同一文档出现两次chunk 重叠区域过大调整 overlap 参数或做去重相同问题线上和本地答案不一致向量库版本不同或模型服务版本不同固定 embedding 模型版本记录向量库元信息一次回答 token 数异常高塞入过多无关上下文降低 top_k增加相关度阈值答案答非所问但检索分数很高query 与目标文档语义接近但不含关键约束引入结构化知识库查询或 query 改写Trace 里没有看到自定义函数未加 traceable 装饰器给自定义逻辑补装饰器或加 callbacks自动评估分数很高但人工判断是坏答案评估模型和生产模型同源自评偏乐观切换到风格不同的评估模型这张表你可以直接贴到项目文档里团队里新人排查的时候照着看比我手动带人高效得多。5.2 我踩过的三个坑第一个坑是最开始只追踪了生成环节没有给检索函数埋点。当时系统回答经常偏我在 LangSmith 里只能看到模型用什么 prompt 生成了什么答案但看不到喂进来的文档是哪来的、分数多少等于拿着半张病历在诊断。后来补齐检索侧的埋点后才发现很多坏答案的第二条、第三条上下文本来就是弱相关文档模型是被这些低质量文档带偏了。如果你刚开始接入观测切记先覆盖检索侧。第二个坑是把评估集做得太小。我一开始只挑了三五条 badcase 做成评估集结果每次改动都显得效果完美提升但上线后真实数据依旧打脸。样本量太小评估结果方差极大几率的偶然性会被当成确定性。后来我把评估集扩大到五十条以上并且保证正负样本比例相对均衡结论才慢慢变得可信。第三个坑是 prompt 里塞了太多检索上下文。有段时间我发现 token 消耗暴增但答案质量反而下降。在 trace 里仔细看才发现我的 prompt 模板把 top_k 从 3 调到了 8导致每轮生成都喂入了大量重复和弱相关内容。模型不是搜索引擎给它太多无关信息就像让一个速记员去抄十个黑板抄到后面必然开始扭曲原意。用观测数据把上下文长度和答案质量的曲线拉出来之后我才果断把 top_k 调回 4并加上相似度得分低于 0.4 的文档直接丢弃的规则。5.3 一个真实的排错案例说一次比较典型的排错过程。当时用户问题很具体某制度的适用对象是哪些人。系统给出的回答包含了一段看上去很正式的描述但没有正面列出适用对象。我第一反应是模型没理解问题结构调了两版 prompt 都没用。打开 LangSmith 的 trace我按照query → retriever → prompt → llm → response的顺序逐层看。检索侧显示top1 是一篇制度公告的摘要得分 0.82top2 是一个具体条目的说明得分 0.61。prompt 组装时我的模板是按得分降序排列的所以模型先看到了摘要再看到条目说明。摘要内容偏宏观模型就顺着摘要的措辞去回答了给出的内容自然没有落到适用对象这个具体约束上。这个 case 的关键不是模型不够好而是检索排序把更泛化的文档放在了最前面压制了具体条目。修复策略有两步一是在检索端增加一个query 包含限定词就优先匹配结构化字段的规则二是引入重排序器把包含具体对象类型匹配的文档权重调高。改完之后再跑同一条问题trace 里显示 top2 的条目说明被排到了第一位最终答案也能明确列出适用对象了。这个案例让我彻底明白全链路观测的价值不是帮你省掉排查时间而是帮你把排查时间花在正确的环节上。没有 trace我可能还在那傻傻地调 prompt永远不会意识到问题出在排序策略上。6. 关于这块我的一些个人体会装上 LangSmith 这大半年我最深的感受是全链路观测改变的不只是排障效率还有团队对RAG 瓶颈在哪的讨论方式。以前大家凭感觉争论是检索问题还是生成问题现在直接拉 trace谁是谁非一目了然。我现在每周会把上周的 badcase 全部拉出来过一遍往评估集里补样本再决定下一版优化方向。这个习惯带来的实际效果比单纯换个更大的模型要明显得多。如果后续时间允许我准备写下篇聊聊怎么把这些观测数据沉淀成自动评估流水线以及如何用回归测试约束 RAG 项目的每一次改动。