LangSmith 实战
一、自己搭的追踪 vs LangSmith
上篇我自己从零搭了一套追踪——TraceContext、traced_span、SQLite 存查。写完之后一个做 LangChain 的朋友说"你在 LangGraph 上跑,直接用 LangSmith 不就完了?"
我当时的反应是——确实。LangGraph 和 LangSmith 都是 LangChain 公司的产品,LangGraph 跑图的过程 LangSmith 自动拦截:每个节点、每个 LLM 调用、每次工具执行、输入输出——全自动抓到,不需要在代码里加任何 traced_span。
但 LangSmith 不能替代全部场景。两者分工大概是:
- LangGraph 里的调用:LangSmith 自动抓到,零代码。我上一篇的埋点白写了(在 LangGraph 项目里)
- 非 LangChain 的自定义逻辑:比如自己写的数据清洗函数、评分函数——LangSmith 需要手动
@traceable标记。上一篇的traced_span方案同样可用 - 测试数据集 + 评估实验:LangSmith 内置。上一篇完全没有这个能力,要自己搭
- UI 查询和对比:LangSmith 自带 Web UI,搜索、排序、左右对比。上一篇只有 SQL
- 成本:上一篇 SQLite 免费。LangSmith Free 计划每月 3000 条 Trace,个人项目够用;超了要付费
简单说:如果是 LangChain/LangGraph 项目,用 LangSmith。如果不是,参考上一篇自己搭。
下面我把从接入、看 Trace、自定义埋点、建数据集、跑评估到实战排查整个过程写下来。
二、接入——设三个环境变量就可以了
什么包都不用装。langchain 和 langgraph 自带了 LangSmith 的埋点代码。只需要设三个环境变量:
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__your-api-key-here"
os.environ["LANGCHAIN_PROJECT"] = "hubei-cs-assistant"
三个变量的作用:
LANGCHAIN_TRACING_V2: 开关。设成"true"后 LangChain/LangGraph 自动开始上报 TraceLANGCHAIN_API_KEY: 认证用的。去 smith.langchain.com 注册,在 Settings → API Keys 里创建。免费就够LANGCHAIN_PROJECT: Trace 在 LangSmith UI 里按项目分组。同一个项目的 Trace 归在一起,方便查
设完之后什么都不用改。直接跑你的 LangGraph 图:
# 这一行 invoke,背后所有的 LLM 调用、工具调用、节点执行
# 全被 LangSmith 自动记录了
result = agent_graph.invoke({"user_input": "王教授带哪些课"})
跑完打开 smith.langchain.com,点进你设的 project,一条完整的 Trace 已经在上面了。
它是怎么做到的? LangChain 在 ChatOpenAI.invoke()、tool.invoke()、Runnable.run() 这些关键方法里内置了回调钩子。当你设了 LANGCHAIN_TRACING_V2=true,这些回调钩子就被激活——每次 LLM 调用前后、工具执行前后、chain 节点前后,自动记录输入输出和耗时,组装成 Span 上报到 LangSmith 的服务器。
三、自动抓到了什么——看一条真实 Trace
以湖北理工官网助手为例,学生问"王教授带哪些课",LangSmith 自动抓到的 Trace 树:
🟢 hubei-cs-assistant (root run, 6.1s)
├── 🟢 LangGraph ── supervisor_node (1.2s)
│ ├── 🟢 ChatOpenAI ── qwen-max (1.0s)
│ │ ├── input_tokens: 2140
│ │ ├── output_tokens: 156
│ │ └── 完整 messages 列表(含 system prompt)可展开查看
│ └── 🟢 parse_plan (0.1s)
│
├── 🟢 LangGraph ── route_agent_node (2.3s)
│ ├── 🟢 ChatOpenAI ── qwen-max (1.8s)
│ └── 🟢 tool ── vector_search (0.4s)
│ ├── input: {"query": "王教授 课程"}
│ └── output: [3 chunks, 1560 chars] ← 完整 chunk 内容可展开
│
├── 🟢 LangGraph ── knowledge_graph_agent (1.5s)
│ ├── 🟢 tool ── neo4j_query (0.6s)
│ │ ├── input: {"cypher": "MATCH (t:Teacher {name:'王建国'})-[:teaches]->(c:Course) RETURN c.name"}
│ │ └── output: [2 rows]
│ └── 🟢 ChatOpenAI ── qwen-max (0.8s)
│
└── 🟢 LangGraph ── aggregator_node (1.1s)
└── 🟢 ChatOpenAI ── qwen-max (1.0s)
展开任何节点可以看到完整的输入输出。这是 LangSmith 最实用的地方——不用去日志文件里翻 JSON,点开就有。
抓到的内容覆盖面:
| 套的是什么 | 自动抓到的内容 |
|---|---|
ChatOpenAI.invoke() |
模型名、input/output token 数、延迟、完整的 messages 列表(system prompt 也在里面)、LLM 输出全文 |
tool.invoke() |
工具名、入参 JSON 全文、返回值全文 |
| LangGraph 节点 | 节点名、输入 state、输出 state、该节点下所有子调用的 Span 树 |
| 并行分支 | 自动识别 LangGraph 的 fan-out 结构,正确构建父子 Span 关系 |
| 异常 | 完整异常堆栈 + 失败节点的输入状态(可以直接拿输入在 UI 里点 Rerun 复现) |
这个覆盖度对调试来说基本够用了。除了自己写的非 LangChain 逻辑(比如一段原生 Python 文本清洗),其他全部自动有。
四、自定义逻辑用 @traceable 标记
Agent 系统里总有一些代码不是 LangChain 原生的——比如数据预处理函数、课程编号规范化、答案后处理。这些 LangSmith 抓不到,需要手动标记:
from langsmith import traceable
@traceable(run_type="chain", name="normalize_course_code")
def normalize_course_code(raw_code: str) -> str:
"""把 CS201 这种不规范编号转成 CS-201"""
import re
match = re.match(r'([A-Z]+)(\d+)', raw_code.upper().strip())
if not match:
return raw_code
return f"{match.group(1)}-{match.group(2)}"
加完这个装饰器之后,每次有人调 normalize_course_code("CS201"),LangSmith 会在当前 Trace 下自动创建一个子 Span,入参("CS201")和返回值("CS-201")都自动记录。
@traceable 的参数:
@traceable(
run_type="chain", # 决定 UI 里用什么图标: "llm" / "chain" / "tool" / "retriever"
name="human_readable", # Trace 树里显示的名字,不设就用函数名
metadata={"version": "2"}, # 静态标签,可以在 UI 里按这个过滤
tags=["production", "critical"], # 动态标签,也可以过滤
)
run_type 设什么其实不影响功能,只影响 UI 显示。选一个语义最接近的就行——数据清洗设 "chain",外部 API 调用设 "tool"。
五、给 Trace 加业务上下文
自动抓的 Trace 里有 LLM 的输入输出、工具的调用参数——这些是技术层面的。但业务层面的信息呢?这次请求是谁发的、属于哪个 session、用户之前聊过什么?这些 LangSmith 没法自动知道,需要自己加。
有两种方式。第一种是在函数级别加静态 metadata:
@traceable(
run_type="chain",
name="handle_user_request",
metadata={"project": "hubei-cs-assistant", "version": "v2.3"}
)
def handle_user_request(session_id: str, user_id: str, user_input: str) -> dict:
return agent_graph.invoke({
"user_input": user_input,
"session_id": session_id,
})
metadata 在每次调用时都带上,适合存"版本号""部署环境"这种不变的标签。
第二种是动态注入——在代码运行过程中拿到当前的 Trace,往上面塞东西:
from langsmith.run_helpers import get_current_run_tree
def supervisor_node(state: AgentState) -> dict:
# 拿当前 Run——这是 LangSmith 底层 API,能拿到当前 Trace 的句柄
run = get_current_run_tree()
if run:
# tags 适合存你想在 UI 里快速过滤的维度
run.tags = run.tags or []
run.tags.extend([
f"session:{state['session_id']}",
f"user:{state.get('user_id', 'anonymous')}",
])
# metadata 适合存想记录的数值和文本
run.metadata.update({
"input_length": len(state["user_input"]),
"agents_available": ["route", "knowledge_graph", "vector_search", "info"],
})
response = llm.invoke(messages)
return {"reply": response.content}
加完这些之后,在 LangSmith UI 里可以按 session:xxx 过滤出一个用户的所有对话记录,也可以按 user:xxx 看某个用户的全部请求历史。
还有一个很实用的功能——记录用户反馈。如果前端有👍👎按钮,用户点了之后把结果挂到 Trace 上:
from langsmith import Client
client = Client()
def collect_user_feedback(trace_id: str, score: int, comment: str = ""):
client.create_feedback(
run_id=trace_id,
key="user_score",
score=score, # 0(踩)或 1(赞)
comment=comment, # 用户写的文字评价(如果有)
)
有了反馈数据之后,可以在 LangSmith 里按 score 过滤——只看被用户踩的 Trace,逐个分析为什么回复质量差。
六、调试实战——UI 比 grep 好用太多
LangSmith 的 Web UI 是它相比自建追踪最大的优势。几个我常用的操作:
找最慢的请求:在 Runs 列表页按 Duration 降序排。点进最慢那条,看 Latency 饼图——哪个 Span 占了最多时间一眼能看出来。
找报错的请求:Filter 设 Status = Error。点进一条报错的 Run,展开到失败的节点,输入状态完整保留——可以直接点"Rerun"用同样的输入再跑一次,不需要自己写复现脚本。
看 LLM 到底收到了什么:点进一个 ChatOpenAI Span,Input 区域展开能看到完整的 messages 列表——system prompt 全文都在里面。Output 区域是 LLM 的原始输出。调试 prompt 的时候这个功能是刚需。
对比两次调用的差异:Ctrl/Command 选中两条 Run,点 Compare——左右分屏,输入、输出、延迟、token 数一一对比。排查"为什么这次回复质量差"的时候可以并排看两次的 prompt 和检索结果有什么不同。
七、建数据集——把测试用例管理起来
除了追踪,LangSmith 还能管理数据集。给湖北理工官网助手建测试集:
from langsmith import Client
client = Client()
# 创建数据集
dataset = client.create_dataset(
dataset_name="hubei-cs-qa-v1",
description="湖北理工计算机学院官网助手评测集",
)
# 攒一批测试用例
test_cases = [
{
"question": "王建国教授负责哪些课程?",
"expected_answer": "王建国负责数据结构与算法(CS201)、算法设计与分析(CS301)",
"tags": ["teacher", "entity-relation"],
},
{
"question": "数据结构与算法的先修课是什么?",
"expected_answer": "程序设计基础(CS101)",
"tags": ["course", "prerequisite"],
},
{
"question": "CS201考试考什么?",
"expected_answer": "包含期末笔试70%+平时作业20%+实验10%",
"tags": ["exam", "factual"],
},
{
"question": "计算机学院有哪些NLP方向的老师?",
"expected_answer": "张伟教授、李明副教授",
"tags": ["teacher", "entity-search"],
},
{
"question": "实验课旷课几次取消考试资格?",
"expected_answer": "2次,且需提前报备(2025版规定)",
"tags": ["policy", "version-sensitive"],
},
]
# 批量写入。inputs 是给 Agent 的输入,outputs 是期望答案
inputs = [{"question": tc["question"]} for tc in test_cases]
outputs = [
{"expected_answer": tc["expected_answer"], "tags": tc["tags"]}
for tc in test_cases
]
client.create_examples(
dataset_id=dataset.id,
inputs=inputs,
outputs=outputs,
)
建好之后在 UI 里能看到每条用例。生产环境发现新的 bad case,随时加进去。数据集是活的——持续维护。
八、跑评估实验——改完 prompt 之后验证效果
数据集有了之后,每次改 system prompt、换模型、调 RAG 参数,跑一次实验看效果变好还是变坏。
先定义评估函数。我用 LLM 做评判器——把 Agent 的实际输出和期望答案一起喂给 LLM 打分:
def evaluate_answer(run_output: dict, example: dict) -> dict:
"""对比 Agent 输出和期望答案,用 LLM 打分"""
actual = run_output.get("reply", "")
expected = example.outputs["expected_answer"]
eval_prompt = f"""你是评估裁判。对比实际回答和期望回答,给出 0-1 的分数。
期望回答: {expected}
实际回答: {actual}
评分标准:
1.0 - 完全正确,关键信息全部覆盖
0.7 - 基本正确,但遗漏了部分信息
0.3 - 部分相关,但核心信息错误
0.0 - 完全错误或答非所问
只输出数字分数。"""
score = float(llm.invoke(eval_prompt).content.strip())
return {
"score": score,
"key": "correctness",
"comment": f"期望: {expected[:80]}...",
}
然后跑实验。run_on_dataset 会自动遍历数据集里的每条用例,用你的 Agent 跑一遍,然后用评估函数打分:
from langsmith import run_on_dataset
experiment_results = run_on_dataset(
client=client,
dataset_name="hubei-cs-qa-v1",
llm_or_chain_factory=lambda: agent_graph, # 你的 LangGraph 图
evaluation=evaluate_answer,
concurrency_level=3, # 并发数。别超过 LLM API 的并发限制
project_name="eval-prompt-v3",
)
concurrency_level=3 的意思是同时跑 3 条用例。如果你的 LLM API 免费版限制每分钟 30 次调用,设太高会触发限流,设太低跑得慢。3 是一个比较安全的默认值。
跑完后 LangSmith 出实验报告:
实验: eval-prompt-v3
数据集: hubei-cs-qa-v1 (5条)
─────────────────────────────
平均 correctness: 0.82
1.0 分: 3 条
0.7 分: 1 条
0.3 分: 1 条 ← 这条需要人工看一下
平均延迟: 4.2s
平均 token: 3420
对比上一次实验 (eval-prompt-v2):
平均 score: 0.82 vs 0.71 → +15%
评分 0.3 的那条点进去,看 Agent 的实际输出和期望答案差在哪——是检索没召回相关内容,还是 LLM 理解错了问题。
这个评估不是完美的——LLM 打分有时候也不准。但比人工逐条看快多了。改一行 prompt,跑 5 分钟实验,看分数涨了还是跌了,心里有底。
九、一个真实排查过程
教务处反馈:同一个问题"考试作弊怎么处理",隔几天问答案不一样。用 LangSmith 排查过程:
在 Runs 列表搜索 考试作弊怎么处理,过滤最近 7 天。找到两条 Run。
Ctrl 选中两条,点 Compare,左右分屏对比:
Run #1 (4月25日): Run #2 (4月27日):
───────────────── ─────────────────
召回 chunk: 召回 chunk:
chunk-v2-08 "作弊处理办法" chunk-v2-08 "作弊处理办法"
chunk-v2-15 "考试纪律补充" chunk-v1-03 "考试管理办法2024版" ← 旧版!
LLM 输出: "记过处分" LLM 输出: "严重警告"
Run #2 的向量检索捞到了 2024 旧版 chunk——版本去重逻辑里 active 过滤在某些条件下失效了。具体原因是当时版本切换时旧版 chunk 的 status 字段没及时更新为 archived。
不用 LangSmith 的话,排查路径大概是:怀疑版本策略有 bug → 去服务器上翻日志 → 手动重现请求 → 对比两次检索结果。至少半天。有了 LangSmith 之后:搜索 → Compare → 看到差异 → 10 分钟。
十、免费额度
LangSmith Free 计划:
- 每月 3000 条 Trace(日均 100 次请求)
- 数据保留 1 个月
- 基本功能都有(追踪、数据集、手动评估)
Developer 计划 $39/月:
- 每月 10000 条 Trace
- 数据保留 6 个月
- 在线评估 + 自动评估
官网助手这个量级(日均几十次请求),Free 完全够。等量大了再说。
LangSmith 对 LangChain/LangGraph 项目来说,最大的价值是零代码接入——设三个环境变量,所有调用自动追踪。UI 搜索对比比 grep 日志快一个数量级。再加上数据集和评估实验,改 prompt 之后能快速验证效果。不是替代自建追踪,是省掉了 LangGraph 项目里 80% 的埋点工作量。