LangSmith 实战:让 Agent 链路追踪不再是黑盒
做 Agent 开发最痛苦的是什么不是模型不给力也不是工具报错而是你眼睁睁看着它跑完一轮对话却完全不知道它中间到底干了些啥。我早期做工具调用型 Agent 的时候调试基本靠猜先在代码里塞一堆 print再盯着控制台脑补每一步的输入输出遇到非确定性 bug 就来回重跑效率低到怀疑人生。后来切到 LangSmith 做链路追踪相当于给 Agent 装了一套“行车记录仪”每一步调了哪个工具、传了什么参数、模型花了多长 token、回答基不靠谱全部一目了然。这篇内容不搞概念堆砌直接从我实际接入和排障的经验出发讲清 LangSmith 到底是什么、怎么在最短时间内把 Agent 链路的每一步看在眼里并且会重点强调就算你不用 LangChain只用裸 OpenAI SDK 或者自己拼的函数也一样能拿到完整的 trace。内容主要面向正在做 Agent 开发、被“黑盒推理”折磨过的朋友如果你是刚入门跟着前面的环境准备部分一步步点就行。1. LangSmith是什么Agent可观测性的刚需1.1 从“日志打点”到“链路追踪”的进化传统后端服务出问题大家习惯打日志入口记录一条出口记录一条数据库调用再记录一条。可这套思路放到 LLM 应用上立刻不灵了。模型输出是非确定性的Agent 的执行路径更是分叉多到吓人一个用户问题进来模型可能先决定调搜索工具工具返回一个不太理想的结果模型又换了个关键词再搜一次中途还可能因为上下文超限触发截断最后生成一个看起来漂亮但实际引用了错误信息的答案。你只靠 print根本拼不出这条完整的执行拼图。LangSmith 做的是“链路追踪”基本概念和我们熟悉的 APM 工具很像一条用户请求对应一个 tracetrace 里面嵌套若干 run每个 run 是链路里的一个执行步骤比如一次 LLM 调用、一次工具执行、一次检索、一段自定义逻辑。父子 run 之间形成一棵树根节点是 Agent 入口往下依次是“模型决策→工具调用→模型再总结”这类子节点。我在微服务领域用过 SkyWalking、Jaeger第一次看到 LangSmith 界面时第一反应是LLM 应用终于也有正经的 APM 了。它把一次 Agent 运行的完整时间线、输入输出、token 消耗、异常信息全部落成一个可视化视图想回溯哪一步哪一层有问题直接在树上点开就行。这种“观测性”不是日志那种事后拼拼凑凑而是一份结构化的事件流每条链路都带有可复现的上下文。1.2 不止LangChainLangSmith能接什么先说一句容易引起误解的话LangSmith 最开始确实是跟着 LangChain 一起火起来的很多人以为它是 LangChain 的付费私有功能离开 LangChain 就抓瞎。实际完全不是LangSmith 是独立的可观测性平台LangChain 只是它最亲密的“第一位接入者”。你自己手写的 Agent 编排逻辑、裸调 OpenAI API、用别的编排框架都能接进来。官方提供的接入路线有几条我按实际使用频率排了个表接入方式适用场景接入成本LangChain / LangGraph 自动透传本来就用 LangChain 搭 Agent环境变量一开全部自动记录最低约等于零traceable装饰器用自己的代码编排 Agent或者裸调 OpenAI / Anthropic SDK给关键函数加装饰器手动 Client API需要精确控制 trace 结构与自定义元数据需要主动创建 runOpenTelemetry 集成已有标准 APM 体系希望 LLM trace 跟服务调用链打通需要额外安装 exporter如果你用的是 CrewAI、LlamaIndex 这类框架它们内部很多组件基于 LangChain 的 callback 机制那么在开启 LangChain tracing 的前提下至少能透出大部分关键 run如果透不出来就用traceable在框架入口和出口各包一层链路照样完整。这一点对我来说很关键因为并不是所有项目都值得为了“能看 trace”而重写成 LangChain。2. 环境准备与项目接入三步让链路开始记录2.1 申请密钥与项目规划用 LangSmith 之前要先注册账号并创建一个 API Key这个 Key 的格式通常是lsv2_开头。拿到 Key 后你其实不用在代码里写一行显式调用只需要把几个环境变量挂上比如export LANGCHAIN_TRACING_V2true export LANGCHAIN_API_KEYlsv2_你的密钥 export LANGCHAIN_PROJECTmy-agent-devLANGCHAIN_PROJECT这个变量很值得提前规划好。项目不存在也没关系第一次上报时后台会自动创建但如果你偷懒不设所有 trace 都会涌进一个默认项目里等数据量上来之后找链路就跟在草稿箱里找最终版文档一样痛苦。我现在的习惯是“业务名 环境”的组合比如order-agent-dev、customer-support-prod这样筛选和后续做对比实验都清爽。另外这句话我要放到前面trace 是异步上报的代码执行完后需要十几秒甚至更久才会在页面上刷新出来别点了运行就跑去看页面报“怎么没有”这是传输缓冲不是丢了数据。2.2 自动埋点五秒钟接好LangChain链路如果你这块业务本来就在用 LangChain设置好上面的环境变量后所有Chain、Agent、Tool、Retriever类型的 run 都会被自动捕获不需要给每个组件手动打点。举个例子一段最简单的链式调用from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_template(给我讲一个关于{topic}的冷笑话) llm ChatOpenAI(modelgpt-4o-mini) chain prompt | llm result chain.invoke({topic: 程序员}) print(result.content)只要环境变量还在这行invoke结束之后页面上就会出现一条完整的 trace底层会拆出两个 run一个是 prompt 模板的 chain 类型 run一个是 llm 类型的 run每个 run 都记录了自己的耗时、输入、输出和 token 统计。你可能会问“这也太黑魔法了吧底层怎么做到的” 其实就是 LangChain 内部集成了 LangSmith 的回调处理器每次运行结束时自动把数据推给服务端。所以这块没有额外代码最大的坑反而来自版本如果没装langsmith这个 SDK或者langchain-core版本太老自动透传可能不生效。解决方式很简单先把依赖升级到当前主流版本再跑一条最简 demo 验证。2.3 自定义追踪不依赖LangChain也能用接下来是裸 OpenAI 用户最关心的部分。假设你现在就是直接调OpenAI的 Python SDK没有 LangChain 这一层封装最省事的接法是给函数加一个装饰器from langsmith import traceable from openai import OpenAI client OpenAI() traceable(run_typellm, nameopenai_chat) def chat_with_gpt(prompt: str, system: str ): messages [] if system: messages.append({role: system, content: system}) messages.append({role: user, content: prompt}) return client.chat.completions.create( modelgpt-4o-mini, messagesmessages, ) result chat_with_gpt(用一句话解释为什么天上会下雨) print(result.choices[0].message.content)这里traceable会把函数的入参、返回值、耗时抓到 LangSmith 去返回值如果是 OpenAI 的 response 对象SDK 会自动提取 token 使用量和输出文本省掉手动解析的工夫。它不只是给“单独一次 LLM 调用”用你的 Agent 入口、工具选择逻辑、后处理函数都可以打上同一个装饰器这样链路结构就自然出来了traceable(nameagent_entry) def agent_entry(user_question: str): plan make_plan(user_question) # 内部也可以是 traceable 函数 tool_result run_tool(plan) return compose_answer(tool_result)装饰器只负责圈定“这一段叫什么、属于哪一层”真正决定父子关系的是函数调用栈你只要保证内层函数也被 traceable 覆盖长出来的 trace 就是一棵规整的树。我实际踩过的一个教训是装饰器参数里run_type不要随便填LLM 调用就填llm工具就填tool自定义逻辑填chain这样界面上才能用颜色和图标快速区分类型全填chain虽然也能看但排查效率会低很多。3. 核心功能逐个拆解链路数据面板怎么读3.1 Run视图看树不要看日志LangSmith 页面打开后默认是 run 列表看起来像一张表格有项目、时间、类型、耗时、token 等字段。很多人习惯性地把它当成日志列表来扫这是最大的误区。真正有价值的是点进去之后的 Trace 视图那是整棵依赖树。树顶层是根 run比如你的agent_entry或AgentExecutor。展开之后每一层都是子 run从上到下依次是模型、工具、再下一个模型。想定位“模型为什么选错了工具”重点看两个地方一是 LLM run 的 input 里完整的 prompt二是 tool 调用的 arguments。有一次我的 Agent 反复调错计算工具参数所有函数签名看起来都正常但点开 trace 才发现模型把参数拼接成了字符串而不是数值列表问题出在工具 description 没说清楚参数格式。没有这棵树我可能又要花一下午瞎试。还要多说一句 token 消耗。LLM run 的卡片上会单独列出 prompt tokens、completion tokens 和总耗时这对做成本优化特别有用。我见过很多 Agent 重复调用同一个工具来回失败重试token 直接翻了三倍只看最终答案根本发现不了但在 trace 树上同一层出现三次工具 run耗时和 token 都摆在那里说不过去。3.2 反馈与在线评估让追踪变成度量链路数据如果只是“看得到”价值还只发挥了一半。另一半是把它变成可量化的反馈信号。LangSmith 提供了 Feedback 机制你可以把用户对回答的点赞、点踩、打分写入服务端并关联到具体的 run_id。后端拿到用户反馈后可以用 SDK 写入from langsmith import Client client Client() client.create_feedback( run_idrun_id, keyuser_rating, score1, comment用户明确点赞回答专业 )这个反馈数据会出现在对应链路下面下次你在后台想看“哪些输入容易引发差评”直接按低分过滤就行不用靠用户投诉才知道哪里烂。更进一步LLM-as-a-judge 的在线评估器也可以挂进去让一个强模型对 Agent 输出自动打分评价维度可以自定义为“是否跑题”“是否包含幻觉”“是否遵守格式要求”。做评价最忌讳的是标准太抽象。我建议每个项目先只订两个硬指标一个是“最终答案里是否包含要求的信息”一个是“工具调用是否成功完成”。先让指标可测再慢慢叠更多维度别想着一步到位。3.3 数据集与离线回归复用每一轮真实数据很多团队把 LangSmith 当监控面板用其实它的数据还能反过来喂给测试。页面上看到一条表现很好的链路可以一键把它保存成例子一组例子落成一个 Dataset之后每次改 prompt、换模型、调工具描述都能用这个数据集跑一遍回归。我在代码里也经常用 SDK 创建数据集做批量测试大致是这样的思路client.create_dataset( dataset_nameagent-regression-set, description核心业务回归用例 ) client.create_examples( dataset_id数据集 ID, inputs[{question: 帮我算 (23*1789)/2}], outputs[{answer: 234.5}] )建完数据集后跑回归测试可以用 LangSmith 的 evaluate 接口把预测函数和数据名传进去它会自动跑完所有样例并生成一个 experiment 报告。这样你改一句话术就能立刻知道它在历史数据上是变好还是变坏而不是拍脑袋觉得“好像变强了”。这个过程里最划算的一点是数据全来自真实线上链路天然覆盖了各种边角输入比你手工编测试用例省太多事了。4. 实操全记录一个带工具调用的Agent排障实录4.1 搭一个最小可追踪Agent空谈概念不如亲手跑一遍。下面这个 Agent 用的是 LangChain 的工具调用型 Agent搭配两个小工具一个做数学表达式计算一个取当前时间。我刻意把代码控制得很短方便你完整复现。import os import ast from datetime import datetime from langchain_openai import ChatOpenAI from langchain.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate from langchain_core.tools import Tool os.environ[LANGCHAIN_TRACING_V2] true os.environ[LANGCHAIN_API_KEY] lsv2_你的密钥 os.environ[LANGCHAIN_PROJECT] agent-demo-dev # 用 ast 做一层白名单检查别在生产环境直接 eval def calculate(expression: str) - str: try: tree ast.parse(expression, modeeval) for node in ast.walk(tree): if isinstance(node, (ast.Import, ast.ImportFrom, ast.Call, ast.Attribute)): return 仅支持数值运算请使用 - * / ** ( ) return str(eval(compile(tree, string, eval), {__builtins__: None}, {})) except Exception as e: return f计算失败: {e} def current_time(_: str) - str: return datetime.now().strftime(%Y-%m-%d %H:%M:%S) tools [ Tool(namecalculator, funccalculate, description计算数学表达式例如 23*1789/2), Tool(namecurrent_time, funccurrent_time, description获取当前日期和精确时间), ] prompt ChatPromptTemplate.from_messages([ (system, 你是乐于助人的助手需要工具时先调用工具再用工具结果组织回答。), (human, {input}), (placeholder, {agent_scratchpad}), ]) llm ChatOpenAI(modelgpt-4o-mini, temperature0.2) agent create_tool_calling_agent(llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, verboseTrue, handle_parsing_errorsTrue) if __name__ __main__: question 请计算 (23*1789)/2并告诉我现在几点了。 result executor.invoke({input: question}) print(result[output])代码里我故意用了eval来计算虽然前面加了 AST 白名单过滤但这只是一个教学示例。真实业务里建议用一个专门的安全表达式库或者干脆把计算逻辑做成一个独立服务不要让模型传一段字符串到你进程里直接执行。跑完这段代码后去 LangSmith 项目里刷新你会看到一条 trace。展开后会是这样根节点是AgentExecutor下面嵌套一个 LLM run再下面出现calculator和current_time两个工具 run最后的 LLM run 负责把工具结果整理成自然语言回答。每个工具 run 的 input 里能看到模型生成的参数output 里是工具返回的字符串整条链路瞬间立体起来了。4.2 从Trace里揪出“答非所问”的真凶你以为链路是通的就万事大吉我把问题改一下试试这次让 Agent 只回答“现在几点了”不带任何计算。跑了几遍回答有时准有时居然报出一个完全不存在的“下午三点”而真实时间根本不是这个点。第一次出现时我以为是模型偶然抽风后来连续跑多次发现它经常不调用current_time工具而是“凭记忆”直接编时间。点开 trace 就能看到问题所在在 Agent 内部的 LLM run 输出里模型压根没有生成 tool call而是直接生成了最终回答。也就是说它在 system prompt 已经写了“需要工具时先调用工具”的前提下仍然选择了自作聪明。这个根因从日志里很难看出来但在 trace 树上一眼明了——工具 run 根本没出现。修复方式也很直接把 system prompt 从“需要工具时”改成“凡是涉及时间、日期、计算的任务必须先调用对应工具不调用工具就不允许回答。”重新跑一遍之后trace 里的工具 run 按时出现回答也稳定了。整个过程只花了几分钟但如果是线上出了问题再靠用户反馈反推代价就完全不是一个量级了。这个例子还说明一个细节Agent 的“思考过程”未必都完整暴露在 trace 界面上。有些模型不会把中间推理作为输出文本写进 LLM run你可能只能看到最终输出和 tool call。想真正看到完整 reasoning需要在模型侧单独开启 reasoning 内容输出并把它们写进 trace 的 metadata或者使用有完整reasoning_content返回的模型接口。我第一次排查时一直找不到模型“为什么决定不调工具”的明确原因后来才意识到是把“看不到推理”误当成了“没有推理”。5. 常见问题排查与避坑实录5.1 我的Trace为什么没出现这个问题被问到的频率最高我把实际遇到过的原因整理成了一张速查表症状最可能原因处理办法一个 trace 都没有环境变量没设置或没生效确认LANGCHAIN_TRACING_V2true且 API Key 正确只有一部分 run 出现只给部分函数加了 traceable入口和关键子函数都要覆盖有根节点但没有子节点内部函数是同步调用但外层用的异步 traceable检查异步函数是否通用了同一套 context页面一直不刷新异步上报存在缓冲等 10-30 秒再刷新只要进程没崩通常不会丢报 API Key 无效Key 过期或复制了多余空格在后台生成新 Key复制时注意别带换行某些字段显示未捕获返回值不是可序列化对象保证返回值是 str / dict / list 等基本类型我最常犯的错误是忘了设置LANGCHAIN_TRACING_V2只设置了LANGCHAIN_API_KEY结果 LangChain 组件静默不报错但也没有任何数据上报。另一个容易忽略的点是换电脑或者部署到新环境时.env文件没同步本地跑得很好一到容器里就全断了。建议把这三个环境变量写进项目入口并在启动时打印一次当前值方便排查。5.2 数据安全、资源消耗与成本控制用 LangSmith 这类云端服务有一个绕不开的话题你的 prompt 和数据会发送到第三方平台。默认情况下LLM 的完整输入、输出、工具调用的参数值都会被记录。这对开发调试是好事但如果在生产环境直接开 tracing等于把用户隐私、内部业务逻辑全部交给平台保管这个风险必须提前评估。我的建议分三层第一开发环境随便记录本来就是要看细节第二预发环境只开启部分关键 Agent 链路并且对敏感字段做脱敏比如把用户手机号、身份证号在进入函数前就替换成占位符第三生产环境如果确实需要观测优先考虑企业自托管方案让数据留在内网里。另外trace 记录会消耗一定网络 IO 和存储配额项目如果跑得很频繁建议别把所有内部辅助函数全打点挑主干链路记就够了不然页面会被大量无用 run 淹没。还有一个很容易被忽略的坑SDK 是异步上报应用如果被强制kill -9或者 Pod 被直接回收最后一批还在内存队列里的 trace 会直接丢失。这不是 bug而是异步模型的固有行为。如果你发现线上 trace 覆盖率比预期低先检查是不是经常发生非优雅退出。程序正常结束前调用 SDK 提供的 flush 接口可以降低丢失概率但我不建议为了追求 100% 上报而无脑加同步等待那会反过来拖慢主流程。5.3 评估与回归别落下很多团队把 LangSmith 部署好、能看到链路之后就“步入了可观测性的幸福生活”从此只看不败新兵。可单靠肉眼看 trace 做排查本质上还是手动测试的变体。真正让追踪体系发挥复利价值的是把它和离线回归、在线评估绑定在一起。下面这个示例展示了如何用一个历史数据集批量跑 Agent 并自动评估结果from langsmith.evaluation import evaluate def make_predictor(executor): def predict(inputs): question inputs[question] result executor.invoke({input: question}) return {answer: result[output]} return predict def check_answer_contains_date(outputs, reference_outputs): answer outputs[answer] matched 1 if 202 in answer or (: in str(answer)) else 0 return {key: has_date, score: matched} evaluate( make_predictor(executor), dataagent-regression-set, evaluators[check_answer_contains_date], )这个写法只是示意各版本 SDK 的入参格式会有些微差异核心思想是你准备一套历史高质量样例然后每次改代码后跑一遍用指标说话。我实际做完这一步之后最明显的收益是再也没出现过“改 prompt 觉得效果变好上线后却被用户投诉”的尴尬。因为回归集里早就覆盖了这类问题。6. 写在最后几个让我少走弯路的习惯这几年做 Agent 项目我最深的体会不是模型能力不够而是系统到了一定复杂度之后肉眼根本盯不过来。LangSmith 对我而言不是“一个好看的网页”而是一套强制你把每一次 Agent 行为当作数据来对待的工作方式。如果让我给你三个最实用的建议会是这样第一每个 Agent 单独一个 project用环境后缀区分从一开始就把 trace 分好类第二反馈接口从上线第一天就埋上不要等出了事故再补第三每两周用线上真实数据更新一次回归数据集让测试集跟着业务走。最后还有个小技巧在 LangSmith 里遇到一条特别满意的链路直接在页面保存成典型样例以后做回归测试的数据集就有了。这个动作成本几乎为零却是沉淀质量基线最划算的方式。