本文所涉及的代码:https://github.com/LiuSandy/agent-playground/tree/tutorial-02
LLM 是黑盒,Agent 是黑盒中的黑盒
在上一篇文章里,我们已经把 Reactive、Deliberative、Hybrid 三种 Agent 模式跑起来了。但 Agent 一旦真的动起来,新的问题马上出现:它每一步到底做了什么?为什么这次选了工具,下次又不选?为什么最后答案看起来不对,却很难判断是哪一步开始跑偏?。
先承认一个事实:你写过的每一个有 bug 的 Agent,你都不太确定 bug 出在哪。
裸 LLM 的输出本身就是不可解释的——你不知道它为什么在某个时刻选择了这个词而不是那个词。到了 Agent 层面,不可解释性被放大了好几倍:LLM 选了一个工具 → 工具返回了奇怪的结果 → LLM 基于这个奇怪的结果继续推理 → 第三步时方向已经全歪了。
问题是谁的锅?是工具返回的格式不对?是 LLM 选错了工具?还是一开始的 prompt 就给了错误的引导?没有完整的执行痕迹,你只能靠猜。而猜的命中率,和抛硬币差不多。
一个更具体的例子。假设你的 Agent 在处理"调研 Django 和 Flask 的性能差异"这个任务时,最终输出了一份支离破碎的报告。你能看到的是输入和输出,看不到的是:
- LLM 一共调了几次工具?
- 每次工具调用花了多少时间、返回了什么?
- 在哪一步 LLM 开始"跑偏"了?
- 如果没有 Replanner 兜底,结果会有多离谱?
这些问题不是靠 print() 能回答的。print() 只能告诉你某个节点的输出是什么,但它回答不了"这条链路从哪一步开始出错的"——因为一旦图结构复杂起来(像上一篇文章的 Deliberative 模式,七个节点加四条条件边),日志的输出顺序和节点之间的因果关系之间没有对等关系。
可观测性要解决的就是这件事:让 Agent 的执行过程不再是黑盒,每一步都能被回溯、审查和对比。这时候就需要 LangSmith 了。
LangSmith 让调试有迹可循
LangSmith 是 LangChain 团队做的一个 LLM 应用可观测性平台。一句话概括:给你的 Agent 装上行车记录仪。
它和 LangChain / LangGraph 的关系是原生绑定——不需要你手动在代码里插桩,一行环境变量配置就能自动捕获所有 LLM 调用、工具执行、节点流转。录下来的数据包括:输入、输出、延迟、Token 用量、错误信息,以及节点之间的父子关系。
LangSmith 里和 Agent 开发最相关的几个常用能力包括:
- Tracing:自动记录每次调用的完整链路,这是最基础也最常用的功能
- Evaluation:创建数据集、定义评估器,对不同版本的 prompt 或模型跑批量实验
- Datasets:沉淀测试样本,让评估不只依赖临时手测
- Prompt Hub / Context Hub:管理 Prompt、Agent 指令和工具上下文的版本、协作与分享
- Studio:在交互式环境里调试和迭代 Agent
这篇文章先聚焦 Tracing——让 Agent 的执行过程真正"看得见"。后续还会继续介绍数据集、自动化测试和 Agent 评估方法。
让 Trace 自动出现在 Dashboard
注册 LangSmith 账号在 smith.langchain.com,用 GitHub 或 Google 账号即可登录。登录后在 Settings 页面生成一个 API Key。
然后在项目的 .env 里加三行:
# agent-playground/.env(追加)
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=ls__your-api-key
LANGSMITH_PROJECT=agent-playground
LANGSMITH_TRACING=true 是当前推荐的追踪总开关——设成 true 后,LangChain / LangGraph 内的 Runnable、LLM 调用、工具执行都会自动被追踪。LANGSMITH_PROJECT 用来指定 trace 写入的项目;如果不设置,LangSmith 会写到默认项目。
如果你的 LangSmith 账号不在默认的 US 区域,还需要额外设置 LANGSMITH_ENDPOINT,例如 EU 区域是:
LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com
新增一个依赖:
uv add langsmith
然后跑一下之前写过的 Agent:
# agent-playground/main.py
from app.core.agent import create_reactive_agent, run_agent
agent = create_reactive_agent()
answer = run_agent(agent, "调研 Django 和 Flask 的性能差异")
print(answer)
跑完打开 LangSmith Dashboard,进入 Tracing 页面并选择 agent-playground 项目,你应该能看到一条新的 Trace 已经自动出现了。没有改任何业务代码,追踪就生效了。
背后的原理:LangChain 内部所有的 LLM 调用和工具调用都经过一个 Callback 系统。LANGSMITH_TRACING=true 激活后,LangSmith 的 Callback Handler 被自动挂载进去,每次 invoke() 调用都会被捕获并上报。
先看懂 Project、Trace 和 Run
实际使用 LangSmith 的路径很短:先在本地配置 LANGSMITH_TRACING、LANGSMITH_API_KEY 和 LANGSMITH_PROJECT,然后正常运行你的 LangGraph Agent。只要调用链路里用了 LangChain / LangGraph 的 Runnable、LLM 或 Tool,LangSmith 就会自动把这次执行上报到对应 Project。接下来你要做的不是翻日志,而是打开 Dashboard,点进这条 Trace,看每个节点到底输入了什么、输出了什么、耗时多久、有没有调用工具。
打开 LangSmith 的 Tracing 页面,你会看到项目、Trace 和 Run 这几层结构。这几个名词的意思要分清楚:
Project 是一组 Trace 的容器。一般一个应用、一个服务或一个实验环境对应一个 Project,比如这里的 agent-playground。
Trace 代表一次完整操作。一次 agent.invoke() 通常就是一个 Trace,包含这次请求从输入到最终输出的全部步骤。
Run 是 Trace 里的每一个工作单元,也就是可观测性语境里的 span。一次 LLM 调用是一个 Run,一次工具执行是一个 Run,一个 prompt 格式化或状态更新也可以是一个 Run。Run 之间通过父子关系组成树。
用上一篇文章的 Reactive Agent 举例。用户输入"北京今天天气怎么样?适合跑步吗?",Trace 的树大概长这样:
Trace (agent-playground)
├── Run: call_model # LLM 推理,判断需要调用 search_tool
├── Run: call_tool # ToolNode 执行 search_tool
│ └── Run: search_tool # 工具内部的执行记录
├── Run: call_model # LLM 拿到天气结果,判断可回答,不再调工具
这几层 Run 对应了两次 LLM 调用和一次工具调用。每一步谁在什么时候做了什么、花了多少 Token、用了多长时间,全部可见。
每个 Run 点开后能看到的信息:
- 输入(Input):这个节点收到了什么数据
- 输出(Output):这个节点产出了什么
- 延迟(Latency):执行耗时,精确到毫秒
- Token 用量:如果这个 Run 是 LLM 调用,会显示 prompt_tokens / completion_tokens / total_tokens
- 错误信息(Error):如果这个 Run 失败了,完整的 error message 和 stack trace 都在这
这些信息在 print() 时代你想都不敢想。特别是延迟和 Token 用量——你可以在一次 LLM 调用里精确定位到"这个 prompt 花了 1500 tokens,输出花了 200 tokens",这对于成本控制和 prompt 优化太关键了。
Dashboard 里最该看的三块区域
刚进 Dashboard 可能会有点不知所措。几个最重要的入口:
Tracing 项目页:所有 Trace 按时间倒序排列。每条 Trace 一行,显示时间、延迟、Token 用量、是否有错误。顶部的 filter bar 可以按属性、全文、输入输出 key-value、metadata、tags 等条件筛选,右侧的 Filter Shortcuts 会给出常用快捷筛选。
上图就是一次 LangGraph Agent 调用在 LangSmith 里的样子,可以分成三块看:
- 左侧项目与 Trace 列表:当前在
agent-playground项目的 Tracing 页面,列表里选中的LangGraph就是一条完整 Trace。上方的Last 1 day、Threads / Traces / Runs、输入搜索框和Add filter用来缩小排查范围。 - 中间 Run 树:选中 Trace 后,LangSmith 会按父子关系展开整次执行。最外层的
LangGraph是根 Run,下面的router、planner、executor、call_tool、search_tool等就是 Agent 图里的节点和工具调用。每个节点旁边的时间表示耗时,旁边的数字摘要可以帮助你快速判断这一步的调用规模和消耗。 - 右侧 Run 详情面板:图中红框标出的区域就是 Run 详情面板。选中某个 Run 后,这里会展示
Feedback、Input、Output、Attributes等信息。截图里可以看到用户输入是"调研 Django 和 Flask 的性能差异",模型随后多次调用search_tool,每次工具查询参数和工具返回结果都被完整记录下来。
单个 Trace 页面是你的主要工作区。点进任意一条 Trace 后:
- Messages 视图:用对话形式查看这次请求实际发送给模型和模型返回的内容
- Details 视图:树状展示 Trace 里的所有 Run。可以折叠/展开,也可以在当前 Trace 内继续筛选 Run
- Run 详情面板:选中某个 Run 后查看输入、输出、延迟、Token 用量、metadata、tags 和错误信息
Feedback 功能:每条 Trace 支持手动打分(👍 / 👎 或自定义评分)。你可以给质量好的 Trace 点 👍,给跑偏的点 👎,后续筛选时按 Feedback 过滤,快速定位有问题的那批调用。更进阶的用法是在代码里通过 LangSmith SDK 自动添加 Feedback,下一篇会讲。
筛选是日常调试最常用的操作。几个实用的筛选条件:
- Error 筛选:按错误状态只看失败的 Trace,一眼确认问题不是个例
- Latency 筛选:用数值过滤器找出延迟超过某个阈值的调用
- Token 筛选:用数值过滤器找出 Token 消耗异常高的调用
- 按时间筛选:只看最近 1 小时 / 24 小时的数据,缩小排查范围
用 metadata 和 tags 区分实验版本
自动追踪解决的是"录下来了"的问题,但"录下来了一堆东西怎么管理"还需要手动标记。
设想一个场景:你换了 prompt 的措辞,或者从 DeepSeek 换到了 GPT-4o,跑了 20 次调用,Dashboard 里 20 条 Trace 混在一起。你没法区分哪条是哪条。
metadata 和 tags 就是干这件事的。在调用 agent.invoke() 时传入 RunnableConfig:
# agent-playground/main.py
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
from app.core.agent import create_deliberative_agent
agent = create_deliberative_agent()
config = RunnableConfig(
metadata={
"version": "v2",
"model": "deepseek-chat",
"prompt_version": "v3",
},
tags=["experiment", "deliberative"],
)
result = agent.invoke(
{"messages": [HumanMessage(content="调研 Django 和 Flask 的性能差异")]},
config,
)
print(result["messages"][-1].content)
metadata 是键值对,适合存"这个调用是什么版本、用了什么模型、什么 prompt"这类结构化信息。tags 是字符串列表,适合存"这是实验、这是生产、这是测试"这类分类标签。
在 Dashboard 里,filter bar 可以直接按 tags 筛选,也可以用 Metadata Key / Value 过滤 metadata 的键值对,例如筛出 version 为 v2 的调用。
实践建议:每次换模型、改 prompt、调整参数,都换一个 metadata 标记。过一周回看 Dashboard,你能精准对比"v1 和 v2 的 prompt 哪个效果好"——前提是你标记了。
用 traceable 追踪自己的函数
LangChain 的 LLM 调用和工具调用会自动被追踪,但 Agent 里总有一些自定义的 Python 函数——比如你在 Deliberative Agent 写的 parse_plan() 解析 JSON 的那个函数——它不会被自动追踪,因为它不是 LangChain 组件。
@traceable 装饰器解决这个问题:
# agent-playground/app/core/graph.py(在已有 import 后追加)
from langsmith import traceable
@traceable(run_type="chain", name="parse_plan")
def planner(state) -> dict:
···
加了 @traceable 后,parse_plan() 的每次调用都会作为一个独立的 Run 出现在 Trace 树上。你可以看到它接收了什么输入(原始的 LLM 输出字符串),产出了什么输出(解析后的步骤列表),花了多少时间。
run_type 参数告诉 LangSmith 这个 Run 的类型——"tool" 表示这是一个工具类函数,Dashboard 上会按工具类的样式展示。其他可选值:"llm"、"chain"、"retriever"、"embedding"。选对类型对后续筛选和统计有帮助。
@traceable 可以嵌套——如果 parse_plan 内部又调了另一个 @traceable 函数,Trace 树上会有父子关系。这就相当于给任何 Python 代码加了一层零侵入的追踪能力。
四个最常见的 Agent 调试现场
这四个场景覆盖了 Agent 开发中最常遇到的四类问题。
场景 1:Agent 没调工具
用户问"北京今天天气怎么样?",Agent 直接回答"抱歉,我无法获取实时天气信息"。
在 Dashboard 里找到这条 Trace,点开 call_model 的 Run,看 Output 部分。如果 content 是文本回答而不是 tool_calls,说明 LLM 在这一步没有识别到应该调用工具。通常原因:工具描述不够清晰,或者 prompt 里没有强调"遇到天气类问题请调用工具"。
场景 2:工具调用失败
Agent 调了工具但拿回来的结果不对,后续推理全歪。
在 Trace 树上找到 call_tool Run,先看 Error 字段——如果有 error message,直接定位到具体原因(参数格式错误、工具超时、返回数据异常等)。如果没有 error,看 Output 字段——检查工具返回的内容是什么,是不是和预期格式一致。
场景 3:Token 消耗异常高
Agent 一次调用烧了 15000 tokens。
点开 Trace,顶部统计栏会显示总 Token。注意看每个 LLM 调用的 Token 用量——如果有某一次调用 prompt_tokens 特别高,大概率是消息历史过长导致的。检查那个 Run 的 Input,看传进去的消息列表有多长,就能定位是哪些历史消息在占空间。
场景 4:回答质量差,但不知道哪里出的问题
这时候 LangSmith 最大的价值就体现了——你可以回溯完整推理链路。从 Trace 树的顶部开始,从上往下看每一步的输入输出,找到"转折点"——那一步之前的推理还是对的,那一步之后开始偏。定位到这个 Run 后,检查它的输入和输出,通常能发现原因:工具返回的信息不对、LLM 误解了上一步的结果、某个步骤的 prompt 引导有偏。
先看见问题,再谈优化
LangSmith 在 Agent 开发流程中扮演的角色可以分三个阶段看:
- 开发期:实时追踪,快速定位问题。加一个新节点后跑一次,Dashboard 上直接看新节点的运行情况——不用翻日志
- 实验期:用 metadata 标记不同版本(v1 / v2 的 prompt、不同模型),积累一段时间后回头看,哪个版本效果好一目了然
- 上线后:监控异常 Trace。如果某天突然出现大量超时或错误,Dashboard 上立刻能看到
今天讲了最核心的部分——自动追踪、层级结构、metadata 标记、@traceable、Dashboard 调试。下一篇文章会讲 LangSmith 的 Dataset 和批量评估能力,在有了追踪数据的基础上,怎么系统地衡量 Agent 的改进效果。