==一次请求 = 一棵 Run 树==。最外层那个 Run 同时也是这棵 Trace 的根(root run),它的
trace_id等于自己的id;子 Run 通过parent_run_id指回父节点、共享同一个trace_id。决定一个 Run 填什么run_type的唯一标准:这一步在做什么?调大模型→llm;查知识库/向量库→retriever;调外部工具/函数→tool;其它组合编排步骤→chain。
三层骨架:Project / Trace / Run
| 概念 | 是什么 | 关键字段 | 类比 |
|---|---|---|---|
| Project | Run 的归集容器,按项目 / 环境隔离数据 | 项目名(env: LANGSMITH_PROJECT) | 一个看板 / 一个命名空间 |
| Trace | 一次端到端请求展开成的 Run 树,整棵树共享一个 trace_id | trace_id(= 根 Run 的 id) | 一次分布式追踪 / 一棵调用树 |
| Run | 树里的单个执行单元,是真正落库的记录 | id、name、run_type、inputs、outputs、parent_run_id、trace_id、tags、metadata、start_time、end_time、error | 树上的一个节点 / 一段 span |
| run_type | Run 的类型标签,决定 UI 渲染方式 | llm / chain / tool / retriever / prompt / parser | 节点的「种类」 |
接入方式一:LangChain / LangGraph 零代码自动接入
如果你的应用是用 LangChain 或 LangGraph 写的,接入 LangSmith 不需要改任何业务代码。LangChain 内部的每一个 Runnable(模型、prompt、chain、retriever、tool)都内建了回调(callback),只要进程里设了对应环境变量,这些回调就会把每一步执行上报成对应 run_type 的 Run,并自动按调用关系拼成 Trace 树。你要做的只是设三个环境变量。
# 安装:把 langsmith 和 langchain 一起装上(langchain 内建上报回调)
# pip install -U langsmith langchain langchain-openai
import os
# === 仅靠这几个环境变量即可开启自动追踪,下面是零代码接入的全部「配置」 ===
os.environ["LANGSMITH_TRACING"] = "true" # 总开关:true 才上报
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_xxx" # 你的 API key(见上一章)
os.environ["LANGSMITH_PROJECT"] = "my-rag-app" # Run 归集到哪个 Project(不设则进 default)
# os.environ["LANGSMITH_ENDPOINT"] = "https://api.smith.langchain.com" # 默认值,自托管才改
os.environ["OPENAI_API_KEY"] = "sk-xxx"
# === 下面是「纯 LangChain 业务代码」,没有一行是为追踪写的 ===
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_messages([
("system", "你是简洁的技术助手,一句话回答。"),
("human", "{question}"),
])
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 这条链 = prompt | model | parser,是一个组合 Runnable
chain = prompt | model | StrOutputParser()
# 只要执行它,LangSmith 就自动收到一棵 Trace:
# chain(run_type=chain)
# ├─ ChatPromptTemplate (run_type=prompt)
# ├─ ChatOpenAI (run_type=llm) ← 自动带 token / 成本
# └─ StrOutputParser (run_type=parser)
answer = chain.invoke({"question": "LangSmith 的 Trace 是什么?"})
print(answer)
# 打开 https://smith.langchain.com 的 my-rag-app 项目,就能看到这棵树
接入方式二:@traceable 手动追踪普通 Python 函数
如果你的代码不是 LangChain(自己写的 RAG、自己拼的 Agent、调裸 OpenAI SDK),就用 langsmith 的 @traceable 装饰器:给函数加一行装饰器,每次调用就上报成一个 Run,run_type 由你指定,函数的入参和返回值自动成为 inputs 和 outputs。关键点:嵌套调用会自动串成树——被装饰的函数 A 里调用了被装饰的函数 B,B 的 Run 自动认 A 为父,靠的是 SDK 用 contextvars 维护的「当前父 Run」上下文,你不用手传任何 id。
# 仅需 langsmith,无需 langchain
# pip install -U langsmith openai
import os
from langsmith import traceable
from openai import OpenAI
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_xxx"
os.environ["LANGSMITH_PROJECT"] = "manual-rag"
oai = OpenAI() # 需要 OPENAI_API_KEY
# 1) 检索步骤 —— 标成 retriever,UI 会把返回值当文档列表渲染
@traceable(run_type="retriever", name="retrieve_docs")
def retrieve(query: str) -> list[dict]:
# 这里假装查了向量库;真实场景换成你的检索逻辑
return [
{"page_content": "LangSmith 把一次请求记成一棵 Run 树。", "metadata": {"id": "d1"}},
{"page_content": "run_type 决定节点在 UI 里的渲染方式。", "metadata": {"id": "d2"}},
]
# 2) 生成步骤 —— 标成 llm,UI 会统计 token / 成本
@traceable(run_type="llm", name="generate_answer")
def generate(question: str, context: str) -> str:
resp = oai.chat.completions.create(
model="gpt-4o-mini",
temperature=0,
messages=[
{"role": "system", "content": f"只依据以下资料回答:\n{context}"},
{"role": "user", "content": question},
],
)
return resp.choices[0].message.content
# 3) 顶层编排 —— 标成 chain,它是这棵 Trace 的根 Run
@traceable(run_type="chain", name="rag_pipeline")
def rag_pipeline(question: str) -> str:
docs = retrieve(question) # ← 子 Run,自动认 rag_pipeline 为父
context = "\n".join(d["page_content"] for d in docs)
return generate(question, context) # ← 另一个子 Run,同样自动挂上
answer = rag_pipeline("LangSmith 怎么组织追踪数据?")
print(answer)
# 上报到 manual-rag 项目,结构是一棵自动串好的树:
# rag_pipeline (chain) ← 根 Run,trace_id == 自己的 id
# ├─ retrieve_docs (retriever) parent_run_id = rag_pipeline
# └─ generate_answer (llm) parent_run_id = rag_pipeline
用 tags 与 metadata 标注 Run
光有树还不够,生产里你要能按维度筛选和回溯。tags 是一个字符串列表,用来给 Run 打分类标签(环境、实验组、功能模块),LangSmith UI 里可以按 tag 过滤;metadata 是结构化键值,用来挂任意业务上下文(用户 id、版本号、检索条数)。两者都可以在 @traceable 里静态声明,也可以在运行时动态注入。
# tags / metadata 的两种写法:装饰器静态声明 + 运行时动态注入
import os
from langsmith import traceable, get_current_run_tree
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "lsv2_pt_xxx"
os.environ["LANGSMITH_PROJECT"] = "tagging-demo"
# 写法 A:在装饰器里静态声明(每次调用都带上)
@traceable(
run_type="chain",
name="answer_question",
tags=["prod", "rag"], # ← 字符串列表,用于筛选
metadata={"pipeline_version": "v2.3"}, # ← 结构化键值,挂业务上下文
)
def answer_question(question: str, user_id: str) -> str:
# 写法 B:运行时动态注入(值依赖入参时用)
rt = get_current_run_tree() # 拿到当前 Run 句柄
if rt is not None:
rt.tags = (rt.tags or []) + [f"user:{user_id}"]
rt.metadata["region"] = "cn" # 动态补一个 metadata 字段
return f"已回答 {user_id} 的问题:{question}"
# 写法 C:调用点临时覆盖(不改函数定义,单次调用追加标注)
answer_question(
"什么是 Trace?",
user_id="u-42",
langsmith_extra={ # ← traceable 注入的特殊参数,不进 inputs
"tags": ["experiment-A"],
"metadata": {"ab_group": "A"},
},
)
# 该 Run 最终带的 tags = prod, rag, user:u-42, experiment-A
# metadata = pipeline_version=v2.3, region=cn, ab_group=A
# 之后在 UI / SDK 里可按 tag "prod" 或 metadata.ab_group="A" 过滤检索
| 维度 | 类型 | 用途 | 怎么筛 |
|---|---|---|---|
| tags | 字符串列表 | 粗粒度分类:环境 / 实验组 / 功能开关 | UI 按 tag 点选过滤;SDK list_runs(filter='has(tags, "prod")') |
| metadata | 结构化键值(dict) | 细粒度业务上下文:user_id、version、ab_group | UI / SDK 按 metadata 字段精确匹配过滤 |
| inputs | Run 的输入 | 函数入参,UI 里展示与回放 | 按内容全文搜索 |
| outputs | Run 的输出 | 函数返回值,做正确性回溯的基础 | 按内容全文搜索 |
✓推荐做法
- LangChain / LangGraph 代码:只设 LANGSMITH_TRACING=true + API key + PROJECT,零改业务代码
- 自研普通函数:用 @traceable 并显式指定 run_type,让 UI 渲染和统计对得上
- 顶层编排函数标 chain(它是根 Run),检索标 retriever,模型调用标 llm,工具标 tool
- 环境 / 实验组用 tags 标,user_id / version 等结构化上下文用 metadata 标
- 两种接入可共存:@traceable 的根函数里调 LangChain 链,会自然挂进同一棵 Trace
✗不推荐
- 不要把 LLM 调用的 run_type 标成 chain——会丢掉 token 与成本统计
- 不要手写 parent_run_id 去拼同步代码的树,contextvars 已经替你做了
- 不要混设 LANGSMITH_ 与 LANGCHAIN_ 两套同义环境变量,容易互相覆盖
- 不要把大段文本塞进 metadata——结构化小字段才进 metadata,长文本应是 inputs/outputs
⚠常见误区
- 设了 API key 但忘了 LANGSMITH_TRACING=true:什么都不会上报,且不报错
- 异步任务 / 新线程里直接调被装饰函数:上下文断裂,子 Run 会断成另一棵孤树
- 短脚本跑完进程立刻退出:langsmith 异步上报可能还没发完,CI 里建议结束前确保 flush
在 LangSmith UI 打开一条 Trace,能看到完整嵌套树、llm 节点有 token 数、retriever 节点显示文档列表、Run 上带着你设的 tags 与 metadata,即说明数据模型与接入都用对了。
先想清楚每个 Run 该是什么 run_type、该挂在树的哪一层,再写埋点——类型标错,UI 里的 token 曲线和文档视图就全是空的。
— 本章小结