上个月帮朋友排查他们内部客服机器人的 Bug,问题特别典型:用户提交退款申请,机器人明明查询到订单状态是“已退款”,最后却回复“退款正在处理中,请耐心等待”。从结果看这就是一次标准幻觉,但最让人头疼的从来不是幻觉本身,而是你完全不知道它从哪一步开始跑偏——是查询工具没被调用?是工具返回内容被截断?还是模型在最后归纳时犯了傻?在没有追踪系统的情况下,这类问题只能靠猜,往 prompt 里多加几句“注意不要瞎说”,然后祈祷。
后来我把这套系统接入了 LangSmith。LangSmith 是 LangChain 团队推出的 LLM 应用可观测平台,核心就是解决 Agent 链路“过程黑盒”的问题。你只需要做最小化的配置,之后每一个 Agent 任务从开始到结束的完整路径,包括每次 LLM 调用、每个工具请求与响应、每一步决策,都会被记录成一条可浏览、可检索、可回放的 Trace。这篇文章我就以一个正在把 Agent 项目往生产推的工程师视角,聊聊怎么用 LangSmith 打通 Agent 链路追踪,以及我在真实项目里踩过的一些坑。
1. 先搞明白一件事:Agent 为什么必须要“可追踪”
1.1 Agent 应用和传统接口调用差在哪
传统后端接口是确定性流水线:请求进来,经过预设逻辑,返回结果。出错时你有异常堆栈、有断点调试、有日志上下文,哪怕链路再长,也能靠调用链工具一层层翻到底。
Agent 应用不一样。它是由模型驱动的决策循环:LLM 先理解用户目标,决定下一步动作,调用工具后拿到结果,再重新判断,循环往复,直到模型认为任务完成。这导致同一个问题的执行路径每次都可能不同,而且绝大多数“过程”发生在模型的隐层思考里。
我习惯用一个类比:传统系统像工厂里固定好的传送带,哪里卡住一眼就能看到;Agent 则像一个有自主裁量权的临时员工,你必须全程看着他办事,才知道他把活干成了什么样。大家现在都在聊 agent 开发、agent 架构、agent 框架与编排,但很多团队只关心“怎么把 Agent 接进来”,却忽略了真正上线后会遇到的问题:你不知道它每次到底干了什么。
1.2 Agent 链路里最容易翻车的几个环节
做 agent 应用时间长了你会发现,Agent 的幺蛾子往往就出在这几个地方:
| 环节 | 常见问题 | 在链路里怎么看 |
|---|---|---|
| 意图理解 | 用户真实诉求和任务目标偏移 | 看 LLM 节点输入里是否包含完整上下文 |
| 工具选择 | 多个工具时选错、或者干脆不调用工具 | 看是否出现了 ToolCall 记录 |
| 工具入参 | JSON 参数拼错、日期格式传错、字段缺位 | 看工具节点的 arguments 字段 |
| 工具结果处理 | 返回过长被截断、格式异常、模型忽略结果 | 看工具节点的 output 和后续 LLM 输入 |
| 循环与终止 | Agent 反复调用工具停不下来,或提前误判完成 | 看 Trace 总步数和每个节点的耗时 |
| 副作用安全 | 工具带写入操作时,危险调用难以被发现 | 看全部工具调用记录,做事后审计 |
很多时候最终回答是错的,但根因在中间某一步。没有哑链路记录,你只能对着最终输出猜,猜来猜去几乎不可能命中。
1.3 LangSmith 在设计上把钱花在哪里
LangSmith 不只是一个日志系统,它把 LLM 应用的可观测性分成了几层:链路追踪(tracing)、调试(debugging)、离线评估(evaluation)、线上监控(monitoring)。链路追踪是所有上层操作的地基。
它的核心价值在于“全自动”。只要用了 LangChain 或 LangGraph,代码里设置好环境变量,框架会自动把整条 Agent 执行链上报,不需要你为每一次 LLM 调用、每一个工具函数手工写埋点。这一点太重要了,因为手工埋点的最大问题不是麻烦,而是你永远会遗漏几个关键位置,而真正出问题的地方恰恰是你没埋的那一步。
当然,LangSmith 现在也提供了手动接入能力,不依赖 LangChain 也能用。对于已经用 LangChain 跑通的团队,接入成本几乎为零,这也是我推荐大家先上手的理由。
2. 开始接入前:先认清 LangSmith 的核心概念
2.1 Project、Run、Trace、Span,它们到底什么关系
我见过不少开发者第一次打开 LangSmith 后台直接懵掉,满屏英文词分不清谁是谁。先把概念理顺,后面效率会高很多。
一个 Project 相当于一个应用的日志“文件夹”,通常我们按项目名或环境来建,比如 agent-demo、prod-support-bot。一个 Project 里包含大量 Run。Run 是某个执行单元的记录,可以嵌套;最顶层的那个 Run 就是 Trace,代表一次完整任务。Tracean 下面每一个具体步骤,比如一次 LLM 调用、一次工具调用,都叫 Span。
这样理解会更顺:Project 是巡检记录本,Trace 是某一次巡检的完整记录,Span 是巡检过程中一个个具体动作。你在 LangSmith 界面看到的树形结构,本质就是 Trace 和 Span 的嵌套关系。
2.2 最小化接入:环境变量与初始化代码
接入 LangSmith 的第一步不是写代码,而是先拿到 API Key。登录 LangSmith 后台,在 Settings 里创建 API Key,格式一般是lsv2_开头。
然后配置环境变量,我推荐在应用启动脚本或环境配置文件里统一设置:
export LANGCHAIN_TRACING_V2=true export LANGCHAIN_API_KEY=lsv2_你的key export LANGCHAIN_PROJECT=agent-tutorial如果你用的是较新的 SDK,有的版本也支持LANGSMITH_API_KEY,为了兼容起见,两个变量都设上没坏处。
依赖方面,最少需要这些:
pip install langchain langchain-openai langsmith如果要用社区工具,再补一个langchain-community。配置完环境变量,LangChain 的链式调用、AgentExecutor、LangGraph 状态图会自动产生 Trace,不需要额外传参。第一次跑通时,你会看到后台出现第一条不完整或完整的链路,那一刻的成就感非常直观。
2.3 先想清楚:采样策略与成本
很多人接入 LangSmith 之后立刻开启全量上报,跑了几天发现账单不太舒服。LangSmith 的定价和数据量挂钩,虽然单条 Trace 费用不高,但生产环境请求量大,积少成多就明显了。
我建议从一开始就把采样率定好。开发环境全量追踪没问题,因为量小、调试需要;生产环境使用LANGCHAIN_TRACING_SAMPLING_RATE控制采样比例,比如只追踪 10% 的请求:
export LANGCHAIN_TRACING_SAMPLING_RATE=0.1另外,线上问题和开发问题要分 Project。同一个 Project 里混合开发流量和生产流量,排查时很容易互相干扰。我见过朋友在生产集群上忘记切 Project,结果一个 Bad Case 翻半天,因为其他环境的 Trace 太多。按环境建 Project,按需求调整采样比例,是我刚开始接入时最后悔没做的事。
3. 第一个可追踪的 Agent:从代码到 Trace
3.1 用 LangChain 跑通一个带工具的 Agent
我们直接写一个最小可运行的 Agent:它带一个天气查询工具,用户问天气时,LLM 判断需要工具,调用它拿数据后组织回答。
import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = "lsv2_你的key" os.environ["LANGCHAIN_PROJECT"] = "agent-tutorial" from langchain_openai import ChatOpenAI from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """返回指定城市的实时天气,只接受精确城市名。 例如:北京市、上海市。""" # 生产环境这里换成真实天气 API return f"{city}今天晴,气温18-24℃,体感舒适。" tools = [get_weather] llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) agent = create_tool_calling_agent(llm, tools) executor = AgentExecutor(agent=agent, tools=tools) resp = executor.invoke({"input": "北京今天需要穿羽绒服吗?"}) print(resp["output"])跑完这段代码,打开 LangSmith 后台的 agent-tutorial 项目,你会看到一条新 Trace。这套流程里,模型先规划是否使用工具,然后生成一个 ToolCall,框架调用 get_weather 并把结果送回给模型,最后模型根据真实天气数据生成回答。整个过程被完整记录。
3.2 打开 LangSmith,逐层读懂节点信息
进入某条 Trace 后,你看到的结构大概长这样:
AgentExecutor 根节点(整次任务) └── ChatOpenAI 调用(模型决策:生成 ToolCall) └── get_weather 工具调用(入参 / 返回结果) └── ChatOpenAI 调用(读取工具结果,生成最终回答)每个节点都可以点开,看到输入输出、开始结束时间、耗时、Token 用量、模型名等原始信息。排查问题的时候,我最常看的就是工具的原始返回:它是脏数据、是超时、是被截断,一眼就能判断。
比如刚才那个客服机器人案例,我用 LangSmith 一查,发现它压根没有调用查询工具,模型直接凭印象生成了一句话。根因不在 prompt 最后那句“你要诚实”,而在模型决策阶段没有把“先查单再回答”作为强制动作。这种定位,没有链路追踪之前靠猜很难得出。
3.3 换成 LangGraph 后 Trace 和代码一一对应
LangChain 的 AgentExecutor 封装度高,Debug 起来有时候还是隔了一层。最近 agent 框架的主流方向是更显式的编排,LangGraph 逐渐成为标配。节点和边都由你定义,链路追踪的逻辑和代码结构完全对应。
用 LangGraph 重写上面的 Agent:
from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode from langgraph.graph.message import add_messages from langchain_core.messages import AnyMessage, HumanMessage class AgentState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] llm_with_tools = llm.bind_tools(tools) def call_agent(state: AgentState) -> dict: response = llm_with_tools.invoke(state["messages"]) return {"messages": [response]} builder = StateGraph(AgentState) builder.add_node("agent", call_agent) builder.add_node("tools", ToolNode(tools)) builder.add_edge(START, "agent") builder.add_conditional_edges( "agent", lambda state: "tools" if state["messages"][-1].tool_calls else END, ) builder.add_edge("tools", "agent") graph = builder.compile() graph.invoke({"messages": [HumanMessage(content="上海适合穿什么?")]})这次 Trace 会清晰地出现两条路径:
agent 节点(第一轮决策) └── tools 节点(执行天气查询) └── agent 节点(第二轮生成回答)你写的是什么架构,在 LangSmith 里看到的就是什么架构。节点粒度、边逻辑、条件判断全部可见,排查 Agent 行为的体验会好很多。这也是我建议新项目直接上 LangGraph 的原因之一。
3.4 非 LangChain 项目:手动埋点与 wrap_openai
如果你没有用 LangChain,而是直接调用 OpenAI SDK,或者自己封装了一层 HTTP 调用,也不是不能用 LangSmith。官方 SDK 提供了@traceable装饰器和wrap_openai包装器。
以 OpenAI 为例,接入方式非常轻量:
from langsmith.wrappers import wrap_openai from openai import OpenAI client = wrap_openai(OpenAI()) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "今天北京天气怎么样?"}], )调用中的模型名、回调、Token 用量都会被 LangSmith 自动记录。@traceable则适合包裹你自己的业务函数,让每个手动步骤也变成一行 Span。区别就是你需要自己决定哪里埋点,不像 LangChain 那样全自动,但接入成本依然可控。
4. 不只是记录:把 Trace 变成真正有用的证据链
4.1 从“哪里报错”到“为什么出错”
链路追踪最容易踩的误区是把 Trace 当成“新式日志”,只看“有没有报错”。真正要做的,是把 Trace 当作证据链,沿着它找到因果。
我的排查套路固定是三步:
- 第一,看有没有调用预期中的工具。这一步能区分“模型没规划对”和“工具执行出问题”。
- 第二,看工具参数和原始返回。参数拼错属于模型生成问题;返回脏数据属于工具侧问题。
- 第三,看最终回答是否忠实于工具结果。如果工具明确返回“已退款”,模型却说“正在处理中”,那就是生成阶段忠实度不足。
这个套路在 LangSmith 上操作起来很顺手,因为每一步都有独立 Span,你能快速跳转、对比。出问题后,不要直接在结论上争论,把证据链贴出来,比一万句解释都有效。
4.2 用 Playground 快速现场复现
LangSmith 的 Playground 是我很依赖的功能。它可以在网页端直接加载一条 Trace 的配置,重新跑一遍相同或修改后的输入,不需要把代码仓库拉下来。
我经常用它做两类事:一类是复现 Bad Case,把用户原始输入原封不动重发,看看是不是稳定复现;另一类是现场调参,比如把 temperature 从 0 调到 0.3,换一个 prompt 前缀,立刻能看到对比结果。
这里有个很实用的经验:把“怀疑点”写成多条不同的 prompt 变体,在 Playground 里来回切,几分钟就能确定哪个改动真正解决问题。比在本地一遍遍改代码跑测试高效得多。
4.3 人工反馈与迭代闭环
好的链路追踪系统一定支持人工反馈。LangSmith 里可以在 UI 上给 Trace 点赞、点踩,也可以通过 API 记录结构化反馈,便于后续做数据分析。
from langsmith import Client client = Client() client.create_feedback( run_id="带 id 的那条 trace", key="user_score", score=1, comment="回答正确但有点啰嗦", )线上用户反馈、内部人工标注,都会和对应 Trace 绑定。久而久之,它就变成你评估集的一部分,为后面的回归测试提供素材。
5. 把追踪变成护栏:回归测试与评估
5.1 从历史 Trace 一键生成测试数据集
Agent 项目迭代得越多,越需要一个“考卷”。最缺的反而不是测试用例,而是那些从线上捞回来的真实 Bad Case。LangSmith 支持直接从 Trace 生成数据集,你看到一条代表性的历史记录,点一下 Add to dataset,它就会被固化进指定的 Dataset。
我通常把数据集分成两类:一类是正确行为,防止回归;一类是历史 Bad Case,验证修复有没有生效。这样每次改 prompt、换模型、调整工具逻辑,都有了一个可复现的基准。
5.2 离线评估跑起来
有了 Dataset,下一步就是在 LangSmith 里跑离线评估。用代码触发或者直接在后台创建测试任务,框架会自动把数据集里的每条输入跑一遍,然后用你定义的评估器打分,输出一个可比较的实验结果。
简单示意如下:
from langsmith.evaluation import evaluate def run_agent(inputs: dict) -> dict: answer = executor.invoke({"input": inputs["input"]}) return {"output": answer["output"]} evaluate( run_agent, data="agent-tutorial-dataset", )你可以加自带评估器,也可以写自定义规则。LangSmith 会把两次实验的得分放在一起对比,让你快速看到这次改动的平均表现和分项表现。这一步做下来,prompt 迭代就不再是拍脑袋,而是有“考试成绩”支撑的工程行为。
5.3 发布前先问自己:新 prompt 会让老问题反弹吗
我吃过最典型的亏是:修好了 A 类 Bad Case,结果 B 类历史问题复发。原因很简单,改 prompt 通常是在“压”模型某一类行为,而模型行为是整体联动的,很容易顾此失彼。
现在我的习惯非常固定:每次要动 prompt 或换模型,先跑一遍整个历史数据集,看分数有没有下降。如果之前的重点场景出现回归,宁可不改或者继续调。没有回归测试这套护栏,Agent 的迭代几乎必然走向“按下葫芦浮起瓢”。
6. 实战排雷:LangSmith 使用中的高频坑与心得
6.1 Trace 不出来?先按这个顺序排查
接入 LangSmith 最常遇到的尴尬是:代码跑完了,后台一条 Trace 都没有。我见过的情况基本集中在这几类:
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 完全无数据 | 环境变量没设置或 Key 填错 | 检查LANGCHAIN_TRACING_V2和 API Key |
| 数据跑到旧项目 | LANGCHAIN_PROJECT没更新 | 确认当前进程的环境变量 |
| 偶发缺失 | 采样率设置过低 | 把LANGCHAIN_TRACING_SAMPLING_RATE调高或临时去掉 |
| 程序结束无输出 | 异步上报没刷新 | 进程退出前调用flush() |
另外我要提醒一下:如果你所在网络环境访问 LangSmith API 不稳定,链路会静默丢数据。遇到“偶尔有 Trace、偶尔没有”的情况,先检查网络连通性和超时日志,不要一上来怀疑 SDK。
6.2 成本与性能怎么控
采集本身是异步的,对主流程延迟影响很小,但极端情况下大量 Trace 同时上报,还是可能出现堆积。我这里有几个实用的控制手段:
- 生产环境默认采样,遇到重大发布或线上事故再临时开全量。
- 按环境拆分 Project,方便按项目配额管理。
- 定期清理旧项目或用完的 Dataset,避免后台数据越来越乱。
- 核心链路全量追踪,非核心链路低频采样,留存最有价值的证据。
成本控制的核心思想是:把追踪作为一种“可调节的观测手段”,而不是无脑全开。
6.3 数据安全、权限与团队协作
Agent 的 Trace 里往往包含用户输入、工具调用参数、检索结果,甚至可能是拼凑出来的隐私信息。如果你的业务对数据合规有要求,上云端的默认区域前必须先确认数据策略。LangSmith 支持私有化部署,适合要求数据不出内网的团队。
团队协作方面,LangSmith 支持成员角色管理和项目权限隔离。建议按团队或模块分 Project,只给需要的人开权限。另外,尽量别在生产环境把 API Key 硬编码在代码里,用密钥管理服务,权限最小化。
6.4 一点个人体会
用 LangSmith 时间越久,我越觉得它的真正价值不是“看链路”,而是逼着我把 Agent 的调用结构想清楚。Agent 开发看上去很自由,但如果没有可观测性兜底,它就是一团看不清的黑箱。链路追踪做起来之后,prompt 迭代、模型选型、工具设计,都会从“玄学”变成“工程”。
最后分享一个小技巧:给自己业务日志里加上 request_id 或 session_id,然后通过元数据写进 LangSmith。线上用户报问题时,你拿着业务侧 ID 一搜就能定位到那条 Trace,时间从半小时缩短到几秒钟。生产排查体验的提升,是立竿见影的。