1. 为什么链路追踪和自动化评估值得放在一起聊
做RAG应用的人大概都有过这种体验:本地跑一个问答Demo,效果看着还行,一旦上线,用户问法千奇百怪,回答质量就开始飘。更麻烦的是,你根本不知道是哪一步出了问题——是检索没召回相关文档,还是召回了一堆噪声,又或者是模型拿着正确的上下文却答非所问。这时候如果没有一套完整的链路追踪和评估机制,调优基本靠猜。
LangSmith这个工具,核心解决的就是"让LLM应用的每一步都可见、可量化、可回归"这件事。它提供两大块能力:一是链路追踪(Tracing),把一次请求从入口到出口的每个环节——检索、提示词组装、模型调用、后处理——全部记录下来,形成一棵可展开的调用树;二是自动化评估(Evaluation),让你能批量跑测试集,用规则或模型打分,量化每次改动的效果。
这篇文章适合两类人看:一类是已经在做RAG应用、被"改了提示词反而更差"折磨过的开发者;另一类是刚接触LLM工程化、想知道生产级应用该怎么监控和迭代的同学。我会从整体设计思路讲到具体实操,包括追踪怎么接、评估数据集怎么建、评估器怎么写、常见坑怎么排。内容基于我在实际项目中的使用经验,涉及具体参数和步骤的地方会给出可直接参考的方案。
需要提前说明的是,LangSmith是一个托管服务,也有自托管方案,本文聚焦在它的核心使用逻辑上,不涉及具体部署环境的敏感配置。所有代码示例都是通用写法,你可以根据自己的技术栈调整。
2. 整体设计思路:追踪和评估为什么要打通
2.1 先搞清楚链路追踪到底在追什么
很多人第一次接触链路追踪,会以为它就是个日志系统。其实不是。普通日志是扁平的文本流,你看到的是"某时刻发生了某事";而链路追踪记录的是有层级关系的调用树。一次RAG请求,顶层是一个chain,下面挂着retriever节点、prompt模板节点、llm调用节点、output parser节点,每个节点有自己的输入、输出、耗时、token消耗、甚至中间状态。
为什么这个层级关系重要?因为RAG的失败往往不是单点失败,而是链路传导。举个例子:用户问"某产品的保修政策是什么",检索器召回了三段文档,其中两段是关于退货的,一段是关于保修的。模型拿到这三段,可能被退货信息干扰,回答里混进了退货条款。如果你只看最终输出,会觉得"模型答错了";但看链路追踪,你会发现根因在检索召回不精准。这就是追踪的价值——定位问题发生在哪一环,而不是只看到最终结果。
LangSmith的追踪模型里,有几个核心概念需要先建立认知:
- Run:一次最小执行单元,可以是一次LLM调用、一次检索、一次工具调用,也可以是一个组合链。
- Trace:一棵完整的调用树,对应一次端到端的用户请求。
- Run Tree:Run之间的父子关系,决定了你在UI上看到的层级结构。
- Feedback:附加在Run上的评价信号,可以是人工标注,也可以是自动评估结果。
理解了这四个概念,后面接SDK的时候就不会迷糊。
2.2 自动化评估解决的又是另一个问题
追踪解决的是"看得见",评估解决的是"说得清"。你改了提示词、换了embedding模型、调整了检索top_k,怎么知道效果是变好还是变差?靠人工看几十条case?不现实,也不可复现。
自动化评估的核心逻辑是:准备一批有标准答案或评分标准的测试样本,每次改动后批量跑一遍,用评估器自动打分,对比分数变化。这里的"评估器"可以是简单的字符串匹配、关键词命中,也可以是用另一个LLM来打分(LLM-as-Judge)。
LangSmith在这块的设计比较务实,它把评估拆成了几个可组合的部分:
| 组件 | 作用 | 典型形态 |
|---|---|---|
| Dataset | 测试样本集合 | 输入+期望输出(或参考上下文) |
| Target | 被评估的目标函数 | 你的RAG链、Agent、单次LLM调用 |
| Evaluator | 打分逻辑 | 规则函数、LLM评分器、人工标注 |
| Experiment | 一次批量评估运行 | 某版本代码在某个数据集上的完整结果 |
这个拆法的好处是,你可以固定数据集,换不同的target来对比;也可以固定target,换不同的evaluator来看不同维度的表现。
2.3 追踪和评估为什么要打通
单独用追踪,你只能看单次请求;单独用评估,你只能看聚合分数。打通之后,价值在于:评估中发现某条case分数低,可以直接跳到对应的trace,看这次调用到底发生了什么。反过来,追踪中发现的异常case,可以一键加入数据集,成为下次评估的样本。这个闭环是LangSmith相比"自己写脚本+自己搭看板"最大的优势。
我在实际项目里的做法是:线上追踪持续跑,每周从追踪里挑出bad case和good case各若干条,补充进评估数据集,然后跑一轮全量评估,看当前版本在扩充后的数据集上表现如何。这样数据集会随着线上真实分布不断进化,评估结果也越来越有参考价值。
3. 核心细节解析:追踪接入的关键要点
3.1 环境变量与SDK初始化
LangSmith的接入方式很轻,核心就是配几个环境变量,然后正常调用你的LLM框架,SDK会自动埋点。以Python为例,基础配置如下:
export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY="你的API密钥" export LANGCHAIN_PROJECT="rag-production"这三个变量的含义分别是:开启追踪、认证身份、指定项目名。项目名这个设计很实用,你可以按环境分(dev/staging/prod),也可以按应用分(客服问答/文档助手),在UI上分开查看。
如果你用的是LangChain,配置完环境变量后,代码基本不用改,链式调用会自动上报。如果你用的是其他框架或者裸调API,可以用@traceable装饰器手动埋点:
from langsmith import traceable @traceable(run_type="retriever") def retrieve_docs(query: str): # 你的检索逻辑 return docs @traceable(run_type="llm") def call_llm(prompt: str): # 你的模型调用逻辑 return response这里有个细节值得说:run_type参数决定了UI上这个节点显示成什么图标、用什么默认渲染方式。常用的类型有llm、retriever、tool、chain、prompt。类型选对了,UI上的可读性会好很多。我见过有人把所有节点都标成chain,结果一棵树展开全是同样的图标,排查问题时非常费眼。
注意:环境变量里的API密钥不要硬编码在代码里,也不要在示例代码里写真实值。用密钥管理服务或者本地
.env文件加载,.env记得加进.gitignore。
3.2 嵌套结构的设计原则
追踪树的结构不是随便嵌套的,它应该反映你应用的真实逻辑分层。一个典型的RAG链,我建议的嵌套方式是:
RAGChain (chain) ├── QueryRewrite (chain) │ └── LLMCall (llm) ├── Retrieve (retriever) │ └── VectorSearch (tool) ├── Rerank (chain) ├── PromptBuild (prompt) └── Generate (llm)这样嵌套的好处是,每一层的耗时和输入输出都能单独看。比如你想知道"查询改写"这一步花了多少时间、改写后的query是什么,直接展开对应节点就行。
设计嵌套结构时有个常见误区:嵌套太深。有人把每个函数调用都包一层,结果一棵树展开有十几层,找关键节点要翻半天。我的经验是,只对"你可能会单独关注其输入输出或耗时"的环节做埋点,纯工具函数、数据格式转换这类不需要单独追踪。
另一个误区是嵌套太浅。把所有逻辑塞在一个大函数里,只上报一个顶层run,那追踪就退化成日志了,失去了定位价值。判断标准很简单:如果某个环节出问题,你需要单独看它的输入输出才能定位,那就值得单独埋点。
3.3 元数据与标签的用法
每个run都可以附加metadata和tags,这两个东西在后期筛选和对比时非常有用。
metadata适合放结构化信息,比如:
@traceable(metadata={"model": "gpt-4", "temperature": 0.2, "top_k": 5}) def generate(query, docs): ...tags适合放分类标签,比如["production", "v2-prompt", "customer-service"]。
为什么这两个要分开?因为metadata是键值对,适合精确查询(比如"找出所有temperature=0.2的run");tags是字符串集合,适合模糊筛选(比如"找出所有带v2-prompt标签的run")。在UI上,你可以用这些字段过滤trace列表,快速定位某一批请求。
我在做A/B测试时的做法是:给不同版本的链打不同的tag,比如prompt-v1和prompt-v2,跑一段时间后,在UI上按tag筛选,对比两组的平均耗时、token消耗和用户反馈。这比自己在代码里埋点统计要省事得多。
3.4 敏感数据的处理
追踪会把输入输出完整记录下来,这在调试时是优点,但在生产环境可能带来数据合规问题。LangSmith提供了一些脱敏机制,但更稳妥的做法是在上报前自己处理。
常见的处理方式有两种:一是在埋点函数里对敏感字段做掩码,比如把用户手机号替换成***;二是配置数据保留策略,定期清理历史trace。具体怎么选取决于你的业务场景和数据敏感程度。
提示:如果你的应用涉及用户隐私数据,建议在开发环境用完整追踪,生产环境只追踪必要的元数据(如耗时、token数、状态码),不记录原始输入输出。这个开关可以通过环境变量或代码配置控制。
4. 自动化评估的实操:从建数据集到写评估器
4.1 数据集怎么建才有价值
评估数据集的质量直接决定了评估结果的可信度。我见过不少团队的数据集是随手凑的几十条,跑出来的分数忽高忽低,根本没法指导决策。
一个好的RAG评估数据集,应该覆盖以下几类样本:
- 常规问答:文档中有明确答案,检索和生成都应该没问题。这类样本用来验证基础能力没有退化。
- 多跳推理:答案需要综合多段文档才能得出。这类样本考验检索召回和模型整合能力。
- 边界情况:文档中没有答案,正确行为是明确说"不知道"。这类样本用来检测模型是否在胡编。
- 干扰样本:检索会召回相关但不完全匹配的文档,考验模型的辨别能力。
- 格式要求:需要按特定格式输出(如JSON、表格),考验指令遵循能力。
每类样本的数量不必均等,但都要有。我一般建议起步阶段总共100-200条,覆盖上述类别,后续根据线上bad case持续补充。
数据集里的每条样本,至少要有input(用户问题)和output(期望答案)。对于RAG,还建议加上reference_contexts(参考答案所依据的文档片段),这样评估检索质量时可以直接对比。
在LangSmith里创建数据集有两种方式:一种是在UI上手动添加,适合少量样本;另一种是用SDK批量上传,适合从现有日志或文件导入:
from langsmith import Client client = Client() dataset = client.create_dataset("rag-eval-v1") client.create_examples( inputs=[{"question": "某产品的保修期是多久?"}], outputs=[{"answer": "根据文档,保修期为两年。"}], dataset_id=dataset.id )4.2 评估器的几种写法与适用场景
评估器是打分的核心。LangSmith支持多种评估器,从简单到复杂排列如下:
字符串精确匹配:最简单,适合答案格式固定的场景。缺点是太严格,同义表述会被判错。
关键词/正则匹配:检查答案中是否包含关键信息。比精确匹配宽松,但可能漏判或误判。
语义相似度:用embedding计算答案与参考答案的相似度。适合答案表述多样但语义一致的场景。
LLM-as-Judge:用另一个LLM来打分,可以给出更细致的评价(如正确性、完整性、相关性分维度打分)。这是目前RAG评估最常用的方式。
自定义函数:完全按自己的业务逻辑写打分规则。
我实际项目里的组合是:用LLM-as-Judge做主评估,分"正确性"和"完整性"两个维度各打1-5分;同时用关键词匹配做辅助校验,检查答案是否包含必须出现的关键实体。两个评估器结果不一致的case,人工重点看。
写LLM评估器时,提示词的设计很关键。一个常见的错误是让评估器直接打一个总分,这样信息量太低。更好的做法是分维度打分,并且要求评估器给出打分理由。理由这部分在排查时特别有用——你能看到模型为什么给这条case打了低分。
from langsmith.evaluation import LangChainStringEvaluator correctness_evaluator = LangChainStringEvaluator( "qa", config={"llm": judge_llm} )上面是LangSmith内置的QA评估器,它会对比模型答案和参考答案,给出CORRECT/INCORRECT的判断。如果你需要更细粒度的打分,可以自定义评估函数:
def completeness_evaluator(run, example): answer = run.outputs.get("answer", "") reference = example.outputs.get("answer", "") # 自定义打分逻辑 score = compute_score(answer, reference) return {"key": "completeness", "score": score}4.3 批量评估的执行与结果解读
数据集和评估器准备好后,就可以跑批量评估了。LangSmith的evaluate函数会把数据集里的每条样本喂给你的target函数,收集输出,然后用评估器打分:
from langsmith import evaluate results = evaluate( target=my_rag_chain, data="rag-eval-v1", evaluators=[correctness_evaluator, completeness_evaluator], experiment_prefix="v2-prompt-test" )跑完之后,UI上会展示这次experiment的聚合分数,以及每条样本的详细结果。重点看几个东西:
- 整体分数:和上一次experiment对比,看是涨是跌。
- 分数分布:如果平均分没变但方差变大了,说明改动让表现更不稳定。
- 低分case:逐条看低分样本,点进对应的trace,定位问题环节。
- 评估器一致性:如果两个评估器结果差异很大,说明评估标准本身需要调整。
这里有个经验:不要只看平均分。我踩过的坑是,某次改动让平均分涨了0.1,但仔细看分布,发现是几条简单case从4分涨到5分,而几条难case从3分掉到2分。这种"平均数掩盖问题"的情况很常见,一定要看分布和具体case。
4.4 评估成本的控制
LLM-as-Judge虽然好用,但成本不低。如果数据集有200条,每条要调一次评估LLM,再加上target本身的LLM调用,一轮评估的token消耗可能相当可观。
控制成本的做法有几种:一是分层评估,先用便宜的规则评估器筛一遍,只对规则评估器拿不准的case调用LLM评估器;二是采样评估,数据集大时每次只随机抽一部分跑;三是缓存,相同输入输出的评估结果可以复用。
我一般会在快速迭代阶段用采样评估(比如每次抽50条),在版本发布前跑全量评估。这样既保证了迭代速度,又保证了发布质量。
5. 常见问题与排查技巧实录
5.1 追踪数据不显示或显示不全
这是接入阶段最常见的问题。排查顺序如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全没有trace | 环境变量未生效 | 检查LANGCHAIN_TRACING_V2是否为true |
| 只有顶层run | 子函数未埋点 | 检查子函数是否加了@traceable |
| trace延迟出现 | 网络或队列积压 | 等待片刻,检查网络连通性 |
| 部分run缺失 | 异常中断未上报 | 检查是否有未捕获的异常 |
有个隐蔽的坑:如果你在异步代码里用@traceable,要确保装饰的是async函数,否则可能出现上下文丢失,导致子run挂不到父run上。这个问题的表现是trace树结构错乱,某些节点变成了根节点。
5.2 评估分数波动大
评估分数不稳定,通常不是模型的问题,而是评估本身的问题。常见原因:
- 评估器提示词不够明确:LLM评估器对提示词很敏感,同一个case换个措辞可能就从4分变3分。解决办法是把评分标准写具体,每个分数段给出明确定义和示例。
- 参考答案本身有歧义:如果参考答案写得模糊,评估器就无所适从。参考答案应该是明确、无歧义的。
- 数据集样本太少:几十条样本的评估结果随机性很大。样本量上去之后,分数才会稳定。
- temperature设置:评估LLM的temperature建议设为0,减少随机性。
我的做法是,在正式用某个评估器之前,先人工标注20条作为"金标准",然后看评估器的打分和人工打分的一致率。一致率低于80%的话,评估器需要调整。
5.3 追踪和评估的闭环怎么落地
工具搭好了,但如果没有形成工作流,很快就会荒废。我总结的闭环流程是:
- 线上追踪持续运行,每周导出一次trace列表。
- 筛选bad case:按用户反馈(如果有)、按低分feedback、按异常耗时筛选。
- 补充数据集:把筛选出的case整理成标准格式,加入评估数据集。
- 跑回归评估:每次代码改动后,跑一轮评估,对比分数。
- 定位问题:对分数下降的case,从评估结果跳转到trace,定位具体环节。
- 修复验证:修复后重跑评估,确认分数恢复。
这个流程跑顺之后,迭代效率会有明显提升。以前改一版提示词要人工看半天,现在跑一轮评估十几分钟就有结论。
5.4 几个容易忽略的细节
时间戳对齐:追踪记录的时间是服务端时间,如果你在本地调试时发现时间对不上,检查一下时区配置。
token计数差异:LangSmith统计的token数和你在模型服务商后台看到的可能有出入,这是因为统计口径不同(是否包含系统提示词、是否包含缓存部分)。对比成本时以模型服务商账单为准。
长文本截断:追踪默认会记录完整的输入输出,但如果文本特别长(比如几万字的文档),UI上可能显示不全。这种情况可以在metadata里记录文档ID,需要时去原始存储里查。
并发写入:高并发场景下,追踪上报可能成为瓶颈。LangSmith的SDK有批量上报和异步上报机制,生产环境建议开启,避免阻塞主流程。
6. 我个人的一些实操体会
用LangSmith这套东西大概半年多,最大的感受是:它把LLM应用开发从"玄学调参"往"工程化迭代"推了一步。以前改提示词全靠感觉,现在至少有数据支撑。但工具终究是工具,有几点体会值得分享。
第一,不要追求一步到位。我一开始想搭一个完美的评估体系,结果数据集建了三百条,评估器写了五六个,跑一轮要半小时,反而拖慢了迭代。后来改成先用最小可用版本跑起来——50条数据、2个评估器——快速迭代,再逐步扩充。工具的价值在于用起来,不在于搭得多完整。
第二,追踪的粒度要跟着问题走。刚开始我埋点埋得很细,每个函数都追踪,结果UI上信息过载。后来改成只追踪关键环节,需要细查时再临时加埋点。追踪的目的是定位问题,不是记录一切。
第三,评估器要定期校准。业务在变,评估标准也要跟着变。我每个月会抽时间人工看一批评估结果,检查评估器的打分是否还符合当前的质量标准。评估器本身也是需要维护的"代码"。
第四,数据集是资产。好的评估数据集是团队共享的资产,应该像代码一样管理——有版本、有review、有变更记录。我见过团队把数据集存在某个人本地,人一走数据集就丢了,非常可惜。
最后说一个具体的小技巧:在追踪的metadata里加上git_commit字段,记录当前运行的代码版本。这样当你在评估结果里发现某个版本表现异常时,可以直接定位到对应的代码提交,排查起来快很多。这个字段用CI/CD流水线自动注入就行,成本很低,收益很高。
这套东西后续还可以往几个方向扩展:比如把评估结果接入CI,每次PR自动跑评估,分数下降就阻断合并;比如把线上追踪的异常检测自动化,出现异常模式时自动告警。这些都是在基础追踪和评估跑顺之后,自然可以叠加的能力。