ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RAG系统全链路观测实战:LangSmith接入与排查指南

RAG系统全链路观测实战:LangSmith接入与排查指南

1. 为什么 RAG 系统需要一台“心电监护仪”

做过 RAG 应用的人大概都有过这种体验:用户反馈“答得不对”,你打开日志一看,检索回来的文档片段看起来没问题,生成模型也没报错,但最终答案就是跑偏了。更让人头疼的是,你根本不知道问题出在检索环节、重排序环节,还是生成环节。整个链路就像一个黑盒,输入进去,输出出来,中间发生了什么全靠猜。

这就是 RAG 系统最典型的困境——链路太长,环节太多,排查全靠玄学。一个完整的 RAG 流程至少包含查询改写、向量检索、关键词检索、结果融合、重排序、上下文组装、Prompt 拼接、大模型生成这么七八个步骤,任何一个环节出问题都会导致最终结果异常。传统做法是在每个环节手动打日志,但日志之间没有关联,你看到的是散落的点,拼不出完整的链路图。

LangSmith 解决的就是这个问题。它本质上是一个面向大模型应用的全链路观测平台,可以理解成给 RAG 系统装了一台“心电监护仪”——每一次用户请求进来,从入口到出口,每个环节的输入输出、耗时、Token 消耗、中间状态全部被自动记录下来,并且以树状结构呈现。你不再需要手动埋点,只要接入 SDK,整条链路就自动被追踪。

这篇文章适合正在做 RAG 应用、已经被排查问题折磨过一轮、想建立系统化观测能力的开发者。不管你是用 LangChain 还是自己手写的链路,只要涉及多步骤的大模型调用,这套观测思路都能直接复用。我会从整体设计思路讲起,然后拆解核心概念和实操要点,再走一遍完整的接入流程,最后把踩过的坑和排查技巧整理出来。

2. 全链路观测的整体设计思路拆解

2.1 为什么传统日志方案不够用

先说清楚为什么不能靠print和logging解决问题。RAG 链路的调用关系是嵌套的,一次用户请求会触发多个子调用,子调用之间还有依赖关系。用传统日志,你看到的是时间线上平铺的一堆记录,但看不出谁调用了谁、哪个检索结果被传给了哪个生成步骤。

举个例子,用户问“某个产品的保修政策是什么”,链路可能是这样的:查询改写把问题拆成两个子查询,分别走向量检索和关键词检索,两路结果合并后经过重排序,取 Top-3 片段拼进 Prompt,最后生成答案。如果最终答案错了,你需要知道:是改写后的查询偏了?还是向量检索没召回相关文档?还是重排序把正确文档排到了后面?还是 Prompt 拼接时截断了关键信息?

传统日志只能告诉你“检索返回了 5 条结果”,但没法告诉你这 5 条结果在后续环节中的流转路径。LangSmith 的树状追踪结构天然解决了这个问题——每个环节是一个节点,节点之间有父子关系,你可以沿着树从上往下逐层展开,精确定位问题出在哪一层。

2.2 观测系统的三个核心能力

一套好用的 RAG 观测系统需要具备三个核心能力,这也是我在选型时重点考察的维度。

第一是自动追踪能力。理想情况下,接入 SDK 后不需要改业务代码,链路自动被记录。LangSmith 通过回调机制实现这一点,LangChain 的callbacks参数或者环境变量配置就能开启全局追踪。对于手写链路,用@traceable装饰器也能达到类似效果。

第二是结构化呈现能力。追踪数据不能是一堆散乱的 JSON,需要以可视化的树状图展示调用层级,每个节点能展开看输入输出。LangSmith 的 Trace 视图就是这种设计,左侧是调用树,右侧是选中节点的详情,排查时非常直观。

第三是评估与对比能力。光看到链路还不够,你还需要知道“这次改动后效果是变好了还是变差了”。LangSmith 提供了数据集管理和自动评估功能,可以把典型问题做成测试集,每次改动后跑一遍,对比不同版本的检索命中率、答案准确率等指标。

2.3 接入方式的选择逻辑

LangSmith 的接入方式主要有三种,选择哪种取决于你的技术栈和改造意愿。

第一种是环境变量全局开启,设置LANGCHAIN_TRACING_V2=true和 API Key 后,所有 LangChain 调用自动被追踪。这种方式改动最小,适合已经用 LangChain 构建链路的项目,基本零成本接入。

第二种是代码级显式配置,在创建 LLM 或 Chain 时传入callbacks参数。这种方式更灵活,可以控制哪些调用被追踪、哪些不被追踪,适合需要精细化管理的场景。

第三种是手动埋点,用@traceable装饰器标记自定义函数。适合不用 LangChain、完全手写链路的项目,或者需要追踪 LangChain 之外的业务逻辑。

我个人的建议是:如果项目已经深度使用 LangChain,直接走第一种,五分钟就能看到效果;如果是混合架构,用第二种加第三种组合,核心链路自动追踪,自定义逻辑手动埋点。

3. 核心概念与实操要点解析

3.1 Trace、Run、Span 的关系

LangSmith 的数据模型有三个核心概念,理解它们是用好这个工具的前提。

Run是最小追踪单元,代表一次具体的操作,比如一次 LLM 调用、一次检索、一次工具调用。每个 Run 有独立的 ID、输入、输出、开始时间、结束时间、Token 消耗等属性。

Trace是一次完整请求的追踪记录,包含从入口到出口的所有 Run。一个 Trace 就是一棵调用树,根节点是入口函数,子节点是各个子调用。

Span是 Run 在树状结构中的表现形式,强调层级关系。父 Span 和子 Span 之间通过parent_run_id关联。

用生活化的类比:一次用户请求是一次“就诊”,Trace 就是这次就诊的完整病历,Run 是病历里的每一项检查记录(验血、拍片、心电图),Span 是这些检查之间的层级关系(先挂号,挂号下面有分诊,分诊下面有各项检查)。

实操中你需要注意:Run 的嵌套层级不要太深。我见过有的项目把每个函数都包一层,结果一棵树有十几层,排查时展开都费劲。建议只对关键环节打点,比如检索、重排序、生成这三个核心步骤,辅助性的工具函数不用单独追踪。

3.2 关键参数配置与含义

接入 LangSmith 需要配置几个关键参数,每个参数的含义和取值逻辑需要说清楚。

参数名作用推荐值注意事项
LANGCHAIN_TRACING_V2开启追踪开关true生产环境建议按需开启,避免全量追踪
LANGCHAIN_API_KEY认证密钥从平台获取不要硬编码在代码里,用环境变量注入
LANGCHAIN_PROJECT项目分组按环境命名建议区分 dev/staging/prod
LANGCHAIN_ENDPOINT数据上报地址默认即可一般不需要改

这里重点说LANGCHAIN_PROJECT的命名策略。我踩过的坑是:开发阶段所有追踪都往一个项目里塞,结果测试数据和线上数据混在一起,排查时根本分不清。后来改成按环境加日期命名,比如rag-dev-0715、rag-prod,清晰很多。

另一个容易忽略的是采样率。LangSmith 默认全量上报,高并发场景下会产生大量数据,既增加成本也影响性能。虽然社区版没有直接的采样率配置,但可以通过代码级控制实现——只对特定用户或特定类型的请求开启追踪。

3.3 追踪数据的隐私与安全边界

这一点必须单独拿出来说。RAG 系统的输入输出往往包含业务敏感信息,追踪数据上报到第三方平台前需要做脱敏处理。

LangSmith 提供了hide_inputs和hide_outputs参数,可以在特定 Run 上隐藏输入输出。我的做法是:对涉及用户隐私的字段(如手机号、身份证号)在进入链路前就做脱敏,追踪记录里只保留脱敏后的数据。另外,对于核心业务逻辑的 Prompt,如果不想暴露,可以用hide_inputs=True隐藏具体内容,只保留调用结构和耗时信息。

注意:脱敏要在数据进入追踪系统之前完成,不要指望平台帮你处理。我见过有团队直接把原始用户数据上报,后来做安全审计时被要求整改,返工成本很高。

4. 完整接入流程与核心环节实现

4.1 环境准备与依赖安装

先把手头的环境理清楚。假设你用的是 Python 技术栈,LangChain 版本在 0.1 以上,那么接入 LangSmith 只需要装一个包。

pip install langsmith

如果你用的是 LangChain,langsmith会作为依赖自动安装,不需要单独装。但如果你要用手动埋点的方式,就需要显式安装。

环境变量配置有三种方式,按优先级从高到低排列:

第一种是直接在 shell 里 export,适合临时调试:

export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=your_api_key_here export LANGCHAIN_PROJECT=rag-observability-demo

第二种是写在.env文件里,用python-dotenv加载,适合本地开发:

from dotenv import load_dotenv load_dotenv()

第三种是在代码里用os.environ设置,适合容器化部署时从配置中心注入:

import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = os.getenv("LANGSMITH_KEY") os.environ["LANGCHAIN_PROJECT"] = "rag-prod"

我个人的习惯是:本地开发用.env文件,CI/CD 环境用环境变量注入,生产环境从密钥管理服务读取。绝对不要把 API Key 硬编码在代码里提交到仓库,这是安全红线。

4.2 LangChain 链路的自动追踪接入

如果你的 RAG 链路是用 LangChain 的 LCEL 或者 Chain 构建的,接入追踪几乎零成本。设置好环境变量后,所有调用自动被记录。

from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o-mini") prompt = ChatPromptTemplate.from_template("根据以下上下文回答问题:\n{context}\n\n问题:{question}") chain = prompt | llm | StrOutputParser() result = chain.invoke({ "context": "某产品的保修期为两年,自购买之日起计算。", "question": "保修期是多久?" })

这段代码执行后,LangSmith 里会自动出现一条 Trace,包含 Prompt 渲染、LLM 调用、输出解析三个 Span。你可以在平台上看到每个环节的输入输出和耗时。

但这里有个细节需要注意:LCEL 的追踪粒度。默认情况下,LCEL 会把整个链作为一个 Trace,内部每个步骤作为子 Span。如果你用了RunnableParallel做并行检索,并行分支会作为兄弟节点出现在树里,排查时能清楚看到哪一路先返回、哪一路拖了后腿。

4.3 自定义 RAG 链路的手动埋点

不用 LangChain 的项目,或者需要在链路中插入自定义逻辑的,用@traceable装饰器手动埋点。

from langsmith import traceable @traceable(name="vector_retrieval", run_type="retriever") def vector_search(query: str, top_k: int = 5): # 模拟向量检索 results = vector_db.search(query, top_k=top_k) return results @traceable(name="rerank", run_type="tool") def rerank_documents(query: str, documents: list): # 模拟重排序 scored = [(doc, compute_score(query, doc)) for doc in documents] scored.sort(key=lambda x: x[1], reverse=True) return [doc for doc, _ in scored[:3]] @traceable(name="rag_pipeline", run_type="chain") def rag_pipeline(question: str): docs = vector_search(question) reranked = rerank_documents(question, docs) context = "\n".join([d.content for d in reranked]) answer = llm.generate(context, question) return answer

这里的关键是run_type参数,它决定了 Span 在平台上的图标和分类。常用的类型有llm、chain、retriever、tool、embedding等。设置正确的类型能让 Trace 视图更清晰,排查时一眼就能区分检索和生成。

name参数也很重要,默认会用函数名,但函数名往往不够直观。建议用业务语义命名,比如vector_retrieval比search更清楚,rerank比process更明确。

4.4 关键指标的采集与呈现

接入追踪只是第一步,真正有价值的是从追踪数据中提取关键指标。RAG 系统需要重点关注的指标有这么几类。

检索质量指标:召回文档数量、重排序后的 Top-1 得分、检索耗时。这些指标能帮你判断检索环节是否健康。如果召回数量总是很少,可能是向量库索引有问题;如果重排序得分普遍偏低,可能是 Embedding 模型不适合当前领域。

生成质量指标:输入 Token 数、输出 Token 数、生成耗时、首 Token 延迟。Token 数异常增长往往意味着 Prompt 拼接出了问题,比如上下文片段重复拼接或者截断逻辑有 Bug。

链路整体指标:端到端耗时、各环节耗时占比、错误率。我习惯在 LangSmith 的 Dashboard 里看耗时分布,如果检索环节占了总耗时的 80%,那优化重点就很明确了。

LangSmith 的 Trace 详情页会自动展示这些指标,但你也可以自定义反馈(Feedback)来记录业务指标。比如在答案生成后,用client.create_feedback记录用户是否满意,后续做数据分析时就能把技术指标和业务指标关联起来。

5. 常见问题与排查技巧实录

5.1 追踪数据不出现的排查思路

这是接入阶段最高频的问题,按以下顺序排查基本能覆盖 90% 的情况。

第一步,确认环境变量是否生效。在代码里打印os.getenv("LANGCHAIN_TRACING_V2"),看是不是true。我遇到过.env文件没被加载的情况,原因是load_dotenv()调用时机太晚,在 import LangChain 之后才执行。

第二步,确认 API Key 是否有效。可以在终端用 curl 测试连通性,或者看 LangSmith 平台上的项目列表是否有新项目创建。如果 Key 无效,SDK 通常会静默失败,不会抛异常,这点很坑。

第三步,确认网络是否可达。追踪数据需要上报到平台,如果部署环境有网络限制,数据会积压在本地缓冲区。LangSmith SDK 有重试机制,但网络不通时最终会丢弃数据。

第四步,检查是否有异常被吞掉。LangChain 的回调机制在某些异常场景下会跳过追踪,比如 Chain 执行中途抛异常。建议在关键环节加 try-catch,把异常信息也记录到追踪里。

5.2 追踪数据量过大导致性能下降

全量追踪在高并发场景下确实会影响性能。我实测过一个中等规模的 RAG 服务,开启全量追踪后 P99 延迟增加了约 15%。优化手段有这么几个。

按需开启:只对特定用户或特定类型的请求开启追踪。比如只追踪 VIP 用户的请求,或者只追踪走了兜底逻辑的请求。

异步上报:LangSmith SDK 默认是异步上报的,但如果你的服务本身就在高负载状态,可以调整缓冲区大小和上报间隔。

定期清理:LangSmith 免费版有数据保留期限,超期的数据会自动清理。如果用的是自部署版本,需要自己配置清理策略。

采样策略:虽然社区版没有内置采样,但可以在代码里用随机数控制,比如 10% 的请求开启追踪。

5.3 常见问题速查表

问题现象可能原因排查方法解决方案
平台上看不到 Trace环境变量未生效打印环境变量值检查加载顺序,确保在 import 前设置
Trace 只有根节点子调用未正确嵌套检查parent_run_id确保子函数在父函数内调用
输入输出显示为空数据被隐藏检查hide_inputs配置调整隐藏策略或脱敏后上报
耗时数据异常时钟不同步对比本地日志时间确保服务器时间同步
Token 数不准模型不匹配检查模型名称配置使用平台支持的模型标识
并行分支顺序混乱异步执行时序问题查看各分支开始时间用asyncio.gather明确并发控制

5.4 几个容易踩的坑

坑一:在循环里调用被追踪的函数。如果在一个 for 循环里调用@traceable装饰的函数,每次调用都会生成一个独立的 Trace,而不是嵌套在同一个 Trace 里。解决方案是把循环逻辑包在一个外层函数里,外层函数加装饰器,循环内的调用自然成为子 Span。

坑二:异常处理吞掉了追踪信息。用 try-except 捕获异常后,如果直接pass或者只打印日志,追踪记录里看不到异常信息。正确做法是在 except 块里用langsmith.Client手动记录错误,或者让异常继续抛出,由框架层捕获。

坑三:Prompt 模板变量未正确传递。LCEL 的追踪会记录 Prompt 渲染前后的内容,但如果变量名拼写错误,渲染结果会是空值,追踪里看到的就是一个空 Prompt。建议在开发阶段加断言检查关键变量不为空。

坑四:多线程环境下的上下文丢失。LangSmith 用上下文变量传递 Trace ID,多线程环境下如果没正确处理,子线程的调用会丢失父 Trace 关联。解决方案是用contextvars显式传递,或者改用异步编程模型。

6. 从观测到优化的闭环思路

追踪数据本身不产生价值,基于数据做优化才产生价值。我通常按这个闭环来迭代:先看 Trace 定位问题环节,然后针对该环节做改动,再用数据集评估改动效果,最后把有效的改动固化下来。

举个实际例子。之前有个 RAG 服务,用户反馈“答非所问”的比例偏高。打开 LangSmith 看 Trace,发现检索环节召回了不少文档,但重排序后的 Top-1 得分普遍在 0.6 左右,而正常情况应该在 0.8 以上。进一步看重排序的输入,发现查询改写后的子查询和原始问题语义偏差较大。问题定位到查询改写环节,调整了改写 Prompt 的约束条件后,重排序得分提升到 0.75 以上,用户反馈明显改善。

这个过程中,LangSmith 的价值在于把“答非所问”这个模糊的用户反馈,快速定位到了具体的代码环节。没有追踪系统,你可能要花几天时间逐个环节排查;有了追踪系统,半小时就能锁定问题。

后续还可以把典型问题做成评估数据集,每次改动后自动跑一遍,对比检索命中率、答案准确率等指标。LangSmith 的 Dataset 和 Evaluation 功能就是干这个的,把观测能力升级成持续优化的基础设施。

我个人在实际操作中的体会是:观测系统的价值不在于数据多全,而在于能不能快速回答“问题出在哪”这个问题。LangSmith 的树状追踪和自动埋点,把 RAG 链路的黑盒变成了白盒,这是它最核心的价值。至于评估、对比这些进阶功能,等基础观测跑顺了再逐步接入,不要一上来就追求大而全。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进