9175 字
46 分钟
LangSmith 源码深潜:Trace、Evaluation 与 Dataset 闭环机制解剖

LangSmith源码学习路线:第 20 篇 Trace、Evaluation 与 Dataset 闭环源码解剖#

核心问题: LangSmith 如何把线上 Agent trace、用户反馈、数据集和 evaluator 串成“发现问题 → 生成样本 → 回归评测 → 证明变好”的工程闭环?

源码主线: traceable / callbacksRun / TraceFeedbackDataset ExampleEvaluatorExperimentRegression Gate

前置文章: 第 13 篇 Streaming / Observability、第 19 篇 Time Travel / Debug Replay

依赖基线: langsmith 当前正式版、langgraph==1.2.7langchain==1.3.11langchain-core==1.4.8

源码基线: https://github.com/langchain-ai/langsmith-sdk/tree/main/python、https://github.com/langchain-ai/langgraph/tree/1.2.7、https://github.com/langchain-ai/langchain/tree/langchain-core==1.4.8

阅读边界: 本文覆盖 LangSmith trace、feedback、dataset、evaluator、experiment 和回归评测闭环;不展开 LangSmith 后端服务实现、计费系统、企业权限模型和完整 UI 内部源码。


0. 本篇在源码学习主线中的位置#

前面的文章已经解释了 LangChain / LangGraph 的框架运行机制。本篇开始把源码阅读推进到工程闭环:能不能验证、能不能部署、能不能在线上持续迭代。

Streaming / Trace / Debug Replay
LangSmith Trace / Evaluation / Dataset
Deployment / Production Runtime

本篇只解决:

  • 解释 trace 与 feedback 如何成为评测数据来源。
  • 解释 dataset、example、evaluator、experiment 的对象边界。
  • 解释如何把线上坏例子转成 regression eval。

本篇不展开:

  • 不实现 LangSmith 服务端数据库。
  • 不把 LLM-as-judge 当成唯一可信标准。

1. 本篇问题、学习目标与能力边界#

核心问题#

LangSmith 如何把线上 Agent trace、用户反馈、数据集和 evaluator 串成“发现问题 → 生成样本 → 回归评测 → 证明变好”的工程闭环?

学习目标#

完成本篇后,读者必须能够:

  1. 画出LangSmith Trace / Evaluation / Dataset的构建期对象关系。
  2. 解释一次运行时调用如何进入主链。
  3. 说明关键分支、异常和停止条件。
  4. 区分公共 API、SDK 契约、平台能力和内部实现。
  5. 根据旅行规划助手场景设计验证或上线闭环。

能力边界#

能力本篇是否覆盖说明
构建期对象组装解释公开参数如何变成可运行或可评测对象
运行时主链解释 trace/eval/run/deployment 的主路径
分支与异常解释失败、降级、重试、终止边界
扩展协作解释与 LangChain / LangGraph / LangSmith 如何组合
平台内部实现不展开 LangSmith 服务端和云基础设施内部

2. 核心概念与最小心智模型#

一句话定义#

LangSmith Evaluation 闭环是把 Agent 运行轨迹转化为可复用评测样本,并用自动或人工 evaluator 持续验证新版本是否变好的工程机制。

最小心智模型#

Streaming / Trace / Debug Replay
LangSmith Trace / Evaluation / Dataset
Deployment / Production Runtime
工程反馈闭环

核心术语#

术语源码对象语义不要误解为
Trace / RunRun一次调用链及其子步骤记录普通日志行
FeedbackFeedback针对 run 或 span 的评分、标签和备注最终答案本身
DatasetDataset评测样本集合线上 trace 全量备份
Evaluatorcode / LLM-as-judge / human对输出或轨迹打分业务目标自动达成
ExperimentExperiment一次模型/提示词/代码版本在 dataset 上的评测运行单次手工测试

与相邻抽象的边界#

对象负责什么不负责什么与本篇对象的关系
RunnableConfig传递 metadata、tags、callbacks不保存业务状态让 trace 和部署观测可关联
Pregel / Graph Runtime执行 graph、stream、checkpoint不自动证明业务变好提供可观测运行底座
LangSmithtrace、feedback、evaluation、deployment不替业务定义成功标准支撑验证和上线闭环

3. 完整执行链路#

本篇的主线不是“调用 LangSmith API 得到一个结果”,而是观察一次 Agent 调用如何被记录、沉淀、评分,并反过来约束下一次发布。最小闭环可以拆成七个对象:RunnableConfig.callbacks、LangSmith Run、trace span、Example、evaluator、Feedback、regression experiment。

从一次 Runnable 调用观察对象变化#

from langchain_core.runnables import RunnableLambda
from langsmith import Client, traceable
client = Client()
@traceable(name="rewrite_answer")
def rewrite_answer(inputs: dict) -> dict:
return {
"answer": inputs["question"].replace("?", "。"),
"source": "demo_chain",
}
result = rewrite_answer({"question": "LangSmith 记录什么?"})
print(result)
# {"answer": "LangSmith 记录什么。", "source": "demo_chain"}

这段代码的业务返回值只是一个 dict,但装饰器在旁路创建了一个 LangSmith run。源码阅读时要把两条链分开:函数调用链继续返回业务对象,观测链把 inputs、outputs、异常、tags、metadata 写成 run tree。LangSmith 的核心设计就是不让 trace 对象污染业务返回值。

如果同一段逻辑被放进 LangChain Runnable,callbacks 会从 RunnableConfig 进入每个子 Runnable。父链、prompt、model、tool 都会形成不同 run type 的节点,最终在 LangSmith 里呈现为一棵树,而不是一条扁平日志。

Runnable callbacks 到 run tree#

chain = RunnableLambda(lambda x: {"answer": x["question"].upper()})
output = chain.invoke(
{"question": "how to evaluate an agent?"},
config={
"run_name": "qa_chain",
"tags": ["article-20", "candidate-v2"],
"metadata": {"revision": "abc123", "dataset": "qa-regression"},
},
)

真实流转可以概括为:

input dict
Runnable.invoke(input, config)
CallbackManager.on_chain_start(serialized, inputs, run_id, parent_run_id)
LangSmith callback handler creates Run(run_type="chain", inputs=...)
子 Runnable / model / tool 继承 parent_run_id
on_chain_end(outputs) 或 on_chain_error(error)
LangSmith 后台把多个 Run 组装成 run tree
业务代码仍然拿到 output dict

这里最关键的对象变化是:输入 dict 没有被改造成 LangSmith 对象;LangSmith 只在 callbacks 旁路复制一份观察数据。run_idparent_run_id 才是 run tree 能复原父子关系的依据。

Trace、Dataset、Evaluation 的闭环#

线上调用 Run
↓ 低分、报错或人工标注
Feedback(run_id, key="correctness", score=0)
从 Run.inputs / Run.outputs 抽取 Example
Dataset(name="qa-regression") 保存 Example(inputs, reference_outputs)
evaluate(target, data=dataset, evaluators=[...])
Experiment 生成新一批 Runs 与 Feedback
和 baseline 对比,决定是否发布

这个闭环解释了 LangSmith 为什么同时有 trace、dataset、evaluation 和 feedback:trace 负责复盘一次真实运行,dataset 把值得回归的输入固化下来,evaluator 把“好不好”转成可比较的分数,feedback 把分数挂回具体 run 或 experiment result。它们不是四套孤立功能,而是同一条质量改进链路上的不同持久化对象。

一张完整时序图#

sequenceDiagram
participant App as Agent App
participant CB as Callback Manager
participant LS as LangSmith Client
participant DS as Dataset
participant EV as Evaluator
participant Gate as Release Gate
App->>CB: invoke(input, config)
CB->>LS: create root Run(inputs, tags, metadata)
App->>CB: child prompt/model/tool events
CB->>LS: create child Runs(parent_run_id)
App-->>CB: output or error
CB->>LS: patch Run(outputs/error)
LS->>LS: user/evaluator writes Feedback(run_id)
LS->>DS: promote bad or important Run to Example
App->>EV: evaluate(target, data=dataset)
EV->>LS: create experiment Runs + Feedback
EV->>Gate: aggregate scores vs baseline
Gate-->>App: allow, warn, or block release

一次正常闭环完成时,应同时看见三类结果:业务调用返回了原本的输出;LangSmith 中出现可展开的 run tree;评测或人工反馈可以定位到具体 run、example 和实验版本。缺少任何一类,都说明链路只跑了一半。


4. 源码地图、关键文件与阅读顺序#

核心目录#

langsmith/
├── client.py
├── run_helpers.py
└── evaluation/
langgraph/
├── graph/
├── pregel/
├── types.py
└── sdk-py/
application/
└── langgraph.json

关键文件#

优先级文件核心对象阅读目的
1langsmith/client.pyClient理解 dataset、feedback、experiment SDK 入口
2langsmith/run_helpers.pytraceable理解 tracing 装饰器入口
3langsmith/evaluation/evaluator helpers理解评测运行组织方式
4langchain_core/runnables/config.pyRunnableConfig理解 metadata/tags 如何进入 trace
5LangSmith Evaluation docsEvaluation workflow理解稳定产品契约

推荐阅读顺序#

1. 先读官方概念文档,确认产品与 SDK 契约。
2. 再读公开 API、SDK Client 或配置文件入口。
3. 顺着构建期对象进入 runtime、trace 或 deployment。
4. 追踪一次 evaluate / run / stream / deployment call。
5. 单独检查失败、重试、权限、持久化和观测。
6. 最后回到旅行规划助手做工程闭环设计。

不建议的阅读顺序#

不建议直接从平台服务端实现或云基础设施开始读。源码学习的目标是掌握业务代码能依赖的公开契约,而不是猜测 LangSmith 内部如何存储所有对象。


5. 对象模型、继承关系与协议边界#

核心对象关系#

Public SDK / Config
Runtime wrapper
Trace / Dataset / Thread / Run
Evaluation / Deployment / Observability

对象职责#

对象生命周期输入输出核心职责
公开入口构建期或调用期用户参数runtime object提供稳定 API
runtime context单次调用config/state/env下游上下文传递配置、状态和观测信息
工程对象单次或长期trace/dataset/thread/run可复盘结果维持工程闭环边界
扩展点构建期注册、运行时触发evaluator/policy/backend修改或观察后的结果插入业务控制逻辑

协议边界#

公共 API
负责:给业务代码稳定入口。
不负责:暴露所有平台内部实现。
运行时协议
负责:让状态、配置、工具、模型和持久化协作。
不负责:替业务判断所有成功标准。
工程闭环协议
负责:把 trace、feedback、dataset、run、deployment 串起来。
不负责:保证错误指标一定代表真实用户价值。

稳定接口与内部实现#

类型对象文章中的使用原则
公共 API官方文档列出的函数、类、SDK 和配置文件可以用于工程示例
扩展接口evaluator、feedback、dataset、thread、run、deployment env说明契约和约束
内部实现LangSmith 服务端、控制平面、私有 runner只用于解释边界,不建议业务依赖

6. 源码阅读策略与证据标准#

本篇阅读策略#

先找公开入口
确认输入输出类型
沿调用方追到核心 SDK / runtime
记录工程对象变化
检查分支、异常和结束条件
回到验证或上线目标

证据等级#

标记含义写作要求
源码事实当前正式版源码可以证明附源码链接
官方契约官方文档或 API Reference 明确说明附官方链接
简化伪代码压缩真实控制流标注不是源码逐字复制
作者推断根据调用链得出的理解明确使用“从调用关系可以推断”
工程建议面向项目实践的建议说明适用条件

本篇证据清单#

结论证据类型文件或文档定位
公共入口存在稳定契约官方契约官方 docs / reference第 14 章链接
核心协议对象存在源码定义源码事实官方源码仓库第 4 章关键文件
运行时分支需要按协议处理作者推断调用链和源码结构第 9 章
工程选型依赖场景边界工程建议旅行规划助手场景第 11 章

7. 构建期源码解剖#

本章回答:在真正调用 Agent 之前,LangSmith 相关对象如何被准备好?重点不是“怎么点 UI”,而是看 SDK 如何把 project、callback、dataset、evaluator 这些配置变成运行时可消费的对象。

构建期职责#

输入归一化动作构建结果
环境变量 / 显式参数解析 endpoint、api key、projectClient 与默认 tracing 上下文
RunnableConfig合并 callbacks、tags、metadata、run_name可向子 Runnable 传播的 callback manager
样本 dict拆成 inputs、outputs、metadataExampleDataset
评分函数规范化为接收 run / example 的 evaluatorexperiment 可调用的评分器

构建期的产物本身通常不执行模型调用。它们的价值是把运行时需要的“观察通道”和“评测契约”提前整理好,避免真正执行时才临时猜测字段含义。

Trace 配置构建#

真实源码签名#

from langsmith import Client, traceable
from langchain_core.runnables import RunnableConfig
client = Client()
@traceable(name="answer_question", run_type="chain")
def answer_question(inputs: dict) -> dict:
...

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def prepare_langsmith_tracing(user_config: dict | None):
env = {
"api_url": read_env("LANGSMITH_ENDPOINT"),
"api_key": read_env("LANGSMITH_API_KEY"),
"project": read_env("LANGSMITH_PROJECT"),
"tracing_enabled": read_env("LANGSMITH_TRACING"),
}
client = Client(
api_url=user_config.get("api_url", env["api_url"]),
api_key=user_config.get("api_key", env["api_key"]),
)
callback_handler = LangSmithCallbackHandler(
client=client,
project_name=user_config.get("project_name", env["project"]),
)
runnable_config = RunnableConfig(
callbacks=[callback_handler, *user_config.get("callbacks", [])],
tags=user_config.get("tags", []),
metadata=user_config.get("metadata", {}),
run_name=user_config.get("run_name"),
)
return client, runnable_config

这段伪代码的输入是环境变量和用户传入的 config,输出是两个对象:一个负责和 LangSmith 服务通信的 Client,一个负责在 LangChain 运行时传播 callbacks 的 RunnableConfig。二者职责不同:Client 是网络与资源入口,RunnableConfig 是运行时事件入口。

对象形态的变化也很明确:字符串环境变量不会直接散落在业务链里,而是被折叠进 client 与 callback handler。运行时每个 on_chain_starton_llm_starton_tool_end 事件都不需要重新读取环境,只要沿着 config 找到 handler 即可。

源码观察点应放在三处:Client 如何保存 endpoint 与认证信息,callback handler 如何在 start/end/error 时调用 client,RunnableConfig 如何在组合链中向子 Runnable 传播。这样才能解释为什么在根链上设置 tags 和 metadata,子 span 通常也能在 LangSmith 中继承到上下文。

框架这样设计,是为了把观测从业务逻辑中剥离出来。业务函数仍然只写输入输出;trace 能力通过 callback 协议插入。这样既能给普通函数加 @traceable,也能给复杂 Runnable 图加统一追踪。

关键分支与异常路径#

条件构建期行为运行时影响
未配置 api keyclient 可构造失败或 tracing 被禁用业务链可选择继续,但不会上传 trace
project 未显式指定使用默认 project 或环境变量run 会落到默认项目,影响后续检索
用户已有 callbacks追加 LangSmith handler 而不是覆盖保留日志、指标、UI 流式输出等其他能力
metadata 不可序列化构建或上传时清洗/失败避免把无法 JSON 化的对象写入 run

源码证据#

  • langsmith/client.py::Client
  • langsmith/run_helpers.py::traceable
  • langchain_core/runnables/config.py::RunnableConfig

Dataset 构建#

真实源码签名#

client.create_dataset(dataset_name="qa-regression")
client.create_examples(
inputs=[{"question": "..."}],
outputs=[{"answer": "..."}],
dataset_id=dataset.id,
)

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def build_regression_dataset(client: Client, dataset_name: str, rows: list[dict]):
dataset = client.create_dataset(
dataset_name=dataset_name,
description="Questions promoted from production traces",
)
example_inputs = []
reference_outputs = []
example_metadata = []
for row in rows:
example_inputs.append({"question": row["question"]})
reference_outputs.append({"answer": row["expected_answer"]})
example_metadata.append({
"source_run_id": row.get("run_id"),
"failure_type": row.get("failure_type"),
"added_by": row.get("reviewer", "system"),
})
examples = client.create_examples(
dataset_id=dataset.id,
inputs=example_inputs,
outputs=reference_outputs,
metadata=example_metadata,
)
return dataset, examples

这段伪代码的输入不是“任意日志”,而是一组已经被挑选出的回归样本。输出也不是模型回答,而是 Dataset 和多条 ExampleExample.inputs 是之后喂给目标应用的请求,Example.outputs 是参考答案或期望结构,metadata 则保留样本来源。

对象形态变化发生在两处:第一,生产 run 中的 inputs/outputs 被复制成可长期保存的 example;第二,线上上下文被压缩为评测需要的最小字段。不是所有 trace 都应该进入 dataset,否则回归集会变成不可维护的日志仓库。

源码观察时要注意批量接口和字段命名。LangSmith evaluation 依赖 example.inputs 调用 target,再把 target 输出和 example.outputs 交给 evaluator。若构建期把 reference output 放错位置,运行时 evaluator 就只能拿到空参考答案。

框架把 dataset 和 trace 分开,是为了让“历史事实”和“未来测试用例”有不同生命周期。trace 记录发生过什么,dataset 记录以后必须反复验证什么。

关键分支与异常路径#

条件构建期行为运行时影响
dataset 已存在复用或创建新版本命名避免重复样本污染统计
样本缺少 expected output只能用于无参考评测或人工评审不能使用 exact match 等参考型 evaluator
metadata 缺少 source_run_id仍可评测失败时难以追溯原始线上 run
输入 schema 混乱构建期应清洗或拒绝target 调用会批量失败

源码证据#

  • langsmith/client.py::Client.create_dataset
  • langsmith/client.py::Client.create_examples

Evaluator 构建#

真实源码签名#

def correctness_evaluator(run, example) -> dict:
return {"key": "correctness", "score": 1, "comment": "..."}

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def build_correctness_evaluator(model):
def correctness(run, example):
prediction = run.outputs.get("answer")
reference = example.outputs.get("answer")
if prediction is None:
return {
"key": "correctness",
"score": 0,
"comment": "target did not return answer",
}
judge_result = model.invoke({
"prediction": prediction,
"reference": reference,
"question": example.inputs.get("question"),
})
return {
"key": "correctness",
"score": judge_result["score"],
"comment": judge_result.get("reasoning"),
"metadata": {"judge_model": model.name},
}
return correctness

这里的输入是一个 judge model 或规则集合,输出是一个可被 evaluate() 调用的函数。运行时 evaluator 接收的不是原始 dict,而是 target 执行后形成的 Run 与当前 Example。这也是为什么 evaluator 代码里要从 run.outputsexample.outputs 分别取预测值与参考值。

对象形态变化是从“评分规则”变成“反馈生成器”。evaluator 的返回 dict 会被 LangSmith 解释为 feedback:key 是指标名,score 是可聚合的数值,commentmetadata 是排障证据。

源码观察点包括 evaluator 返回值的规范化、异常捕获、以及 experiment 如何把评分结果挂到对应 run 上。阅读时不要只看 judge prompt,要追踪评分结果如何成为 LangSmith 的 feedback 记录。

框架这样设计,是因为评测标准经常变化。把 evaluator 做成普通函数,用户可以混合 exact match、规则、embedding 相似度、LLM-as-judge 和人工标注,而 experiment runner 只需要消费统一 feedback 协议。

关键分支与异常路径#

条件构建期行为运行时影响
evaluator 返回 bool规范化为 score可聚合但解释信息少
evaluator 返回多个指标生成多条 feedback可以分别分析正确性、格式、安全性
judge model 未配置构建期失败或跳过该 evaluatorexperiment 仍可运行其他 evaluator
指标 key 不稳定构建期应固定命名否则跨实验无法比较

源码证据#

  • LangSmith Evaluation API
  • LangSmith evaluator 返回协议

构建期产物#

产物保存的信息运行时用途
Clientendpoint、api key、workspace/project上传 run、example、feedback、experiment
RunnableConfigcallbacks、tags、metadata、run_name把 trace 上下文传入每次 Runnable 调用
Dataset / Exampleinputs、reference outputs、metadata驱动回归评测
evaluator function指标 key、score 规则、comment 生成方式把 run + example 转成 feedback

8. 运行时主链源码解剖#

本章回答:构建完成后,一次真实调用如何进入 LangSmith 运行时主链,并产生 run、feedback 与 experiment result?这里要盯住事件生命周期,而不是只看最终分数。

运行时入口#

调用方式公开入口运行时核心对象返回类型
普通调用runnable.invoke(input, config=...)callback manager + root run业务输出
装饰函数@traceable 函数调用tracing context + run tree函数返回值
人工反馈client.create_feedback(run_id=...)feedback recordfeedback 对象或响应
批量评测client.evaluate(target, data=..., evaluators=...)experiment + example runsexperiment results

Trace 采集主链#

真实源码签名#

with tracing_context(project_name="qa-prod"):
result = app.invoke(inputs, config={"tags": ["prod"]})

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def run_traced_runnable(runnable, inputs, config):
callback_manager = configure_callbacks(config.callbacks)
root_run_id = new_uuid()
callback_manager.on_chain_start(
serialized={"name": config.run_name or runnable.get_name()},
inputs=inputs,
run_id=root_run_id,
parent_run_id=config.get("parent_run_id"),
tags=config.tags,
metadata=config.metadata,
)
try:
outputs = runnable._invoke_with_config(
inputs,
config={**config, "parent_run_id": root_run_id},
)
except Exception as exc:
callback_manager.on_chain_error(exc, run_id=root_run_id)
raise
callback_manager.on_chain_end(outputs, run_id=root_run_id)
return outputs

这段伪代码的输入是业务 inputs 和已经准备好的 config,输出仍然是业务 outputs。中间新增的对象是 root_run_id 与 callback event。LangSmith handler 根据 start/end/error 事件写 run,而不是要求业务函数显式返回 run。

对象形态变化有两条线:业务线从 inputs 变成 outputs;观测线从 callback event 变成 Run(inputs, outputs, error, start_time, end_time)。子链调用时,parent_run_id 会继续传入下游,于是 prompt、chat model、tool 都能成为 root run 下的 span。

源码观察点是 on_*_starton_*_end 成对出现的位置,以及异常分支是否仍然调用 on_*_error。如果 error 没有被记录,LangSmith UI 里会只看到缺失输出的 run,而看不到真实失败原因。

框架这样设计,是为了让 trace 成为“运行时事件投影”。同一套 Runnable 可以在不开启 tracing 时正常运行,也可以在加上 LangSmith callback 后获得完整 run tree,业务代码不用分叉。

Feedback 写入主链#

真实源码签名#

client.create_feedback(
run_id=run_id,
key="correctness",
score=0,
comment="回答引用了不存在的政策",
)

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def attach_feedback_to_run(client, run_id, user_vote, review_note):
run = client.read_run(run_id)
if user_vote == "thumbs_up":
score = 1
key = "user_helpfulness"
elif user_vote == "thumbs_down":
score = 0
key = "user_helpfulness"
else:
score = None
key = "needs_review"
feedback = client.create_feedback(
run_id=run.id,
key=key,
score=score,
comment=review_note,
metadata={
"input_hash": stable_hash(run.inputs),
"model_version": run.extra.get("metadata", {}).get("revision"),
},
)
if score == 0:
enqueue_for_dataset_curation(run.id, feedback.id)
return feedback

输入是 run_id 和人工或系统产生的评价,输出是一条 feedback 记录。它不会改写原始 run 的 inputs/outputs,而是以独立对象挂到 run 上。这样同一次运行可以有多个评价维度,例如 helpfulness、correctness、safety。

对象形态变化是从“用户点击或 evaluator 结论”变成可检索、可聚合的 feedback。key 决定指标维度,score 决定统计值,comment 与 metadata 决定后续排障信息。

源码观察点是 feedback 与 run 的关联键。没有 run_id,反馈只能成为孤立事件;有了 run_id,就能回到原始 prompt、模型输出、工具调用与版本 metadata。

框架把 feedback 做成独立对象,是为了支持多人、多指标、多时间点评价。同一个 run 今天可以收到用户差评,明天可以收到人工复核,后天又可以被 evaluator 重新评分。

Experiment 执行主链#

真实源码签名#

client.evaluate(
target=answer_question,
data="qa-regression",
evaluators=[correctness_evaluator],
experiment_prefix="candidate-v2",
)

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def run_experiment(client, target, dataset, evaluators, experiment_prefix):
experiment = client.create_project(
project_name=f"{experiment_prefix}-{timestamp()}",
reference_dataset_id=dataset.id,
)
results = []
for example in client.list_examples(dataset_id=dataset.id):
run = trace_target_call(
project_name=experiment.name,
target=target,
inputs=example.inputs,
reference_example_id=example.id,
)
feedback_items = []
for evaluator in evaluators:
try:
feedback_payload = evaluator(run, example)
except Exception as exc:
feedback_payload = {
"key": "evaluator_error",
"score": 0,
"comment": repr(exc),
}
feedback_items.append(
client.create_feedback(run_id=run.id, **feedback_payload)
)
results.append({
"example_id": example.id,
"run_id": run.id,
"feedback": feedback_items,
})
return experiment, results

输入是 target、dataset 与 evaluators,输出是 experiment 和逐样本结果。每个 example 都会触发一次 target 调用,因此 experiment 不是“离线算分表”,而是一批带 trace 的真实运行。

对象形态变化按顺序是:Example.inputs 进入 target,target 输出形成 experiment run,run 与 example 共同进入 evaluator,evaluator 返回值写成 feedback。这个顺序决定了排查失败时应从 result 找 run,再从 run 找 example,而不是只看 aggregate score。

源码观察点包括并发执行、异常隔离和 evaluator 错误处理。一个样本失败不应让整批实验失去所有结果;但失败样本必须被记录,否则 aggregate score 会被虚假抬高。

框架这样设计,是为了让评测结果可以解释。只给一个总分无法指导修复;保留每个 example 的 run tree、feedback 和 metadata,才能把回归失败定位到具体输入、节点或模型版本。

Callback、事件与可观测性#

时机事件携带数据失败行为
开始on_chain_start / on_llm_startinputs、tags、metadata、run_id观测失败应降级,不应破坏业务主链
子调用on_tool_start / on_chat_model_startparent_run_id、serialized、messages形成 trace span
成功on_chain_end / on_llm_endoutputs、token usage、latencypatch run 并可触发 evaluator
失败on_chain_error / on_tool_errorexception、节点名、run_id写 error 并向上抛出或按业务策略恢复

运行时主链总结#

Runnable input
Callback start event
Run tree / child spans
Business output or error
Feedback attached to run
Dataset example promoted from important runs
Experiment evaluates candidate version
Regression gate decides release

9. 关键分支、异常与边界#

本章回答:trace、feedback、dataset、evaluation 偏离正常主链时,应该如何分流?重点是区分业务失败、观测失败、评分失败和发布门禁失败。它们的处理策略不一样。

分支矩阵#

分支类型触发条件核心对象结果
Trace 分支callback 上传失败、metadata 不可序列化run / callback event业务继续或降级记录
Feedback 分支run_id 缺失、用户差评、重复评价feedback写入失败、待审或进入样本池
Evaluation 分支target 抛错、evaluator 抛错、judge 不确定experiment run / feedback失败样本、人工复核或跳过指标
Regression 分支candidate 分数低于 baselinedataset / experiment summary阻断发布或触发回滚

线上坏例子分支#

真实源码签名#

if feedback.score == 0:
promote_run_to_dataset(run_id, dataset_name="qa-regression")

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def promote_bad_run_to_dataset(client, run_id, feedback_id, dataset_name):
run = client.read_run(run_id)
feedback = client.read_feedback(feedback_id)
if feedback.score is None or feedback.score > 0:
return {"action": "skip", "reason": "not a confirmed failure"}
if contains_private_data(run.inputs, run.outputs):
return {"action": "send_to_redaction", "run_id": run.id}
dataset = get_or_create_dataset(client, dataset_name)
example = client.create_example(
dataset_id=dataset.id,
inputs=extract_replayable_inputs(run.inputs),
outputs=derive_reference_output(run, feedback),
metadata={
"source_run_id": run.id,
"source_feedback_id": feedback.id,
"failure_comment": feedback.comment,
"trace_url": run.url,
},
)
return {"action": "created_example", "example_id": example.id}

输入是已确认低分的 feedback_id 与原始 run_id,输出是一个可回归的 Example 或一个跳过/脱敏动作。这里不能机械地把所有差评 run 加进 dataset,因为线上输入可能包含隐私、临时上下文或不可重放工具结果。

对象形态变化是从“线上失败事实”变成“未来测试样本”。run.inputs 需要被裁剪为 target 能重新接收的输入,run.outputs 不一定能直接当 reference,很多时候要由人工或规则补出期望答案。

源码观察点是 run、feedback、example 之间的关联字段。metadata 中保留 source_run_idsource_feedback_id,后续回归失败时才能追溯到最初的线上问题。

框架不会自动判断“坏例子是否适合回归”,这是业务质量流程的一部分。LangSmith 提供对象和 API,团队仍然需要定义脱敏、去重、标注和样本晋升策略。

Evaluator 不确定分支#

真实源码签名#

if judge_confidence_low:
client.create_feedback(run_id=run.id, key="needs_human_review", score=None)

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def evaluate_with_uncertainty(run, example, judge_model):
judge = judge_model.invoke({
"question": example.inputs["question"],
"prediction": run.outputs.get("answer"),
"reference": example.outputs.get("answer"),
})
if judge.get("confidence", 1.0) < 0.7:
return {
"key": "needs_human_review",
"score": None,
"comment": judge.get("reasoning"),
"metadata": {
"raw_judge": judge,
"route": "human_queue",
},
}
return {
"key": "correctness",
"score": judge["score"],
"comment": judge.get("reasoning"),
}

输入是 experiment run、example 和 judge model,输出是普通 correctness feedback 或 needs_human_review feedback。score=None 不是失败,而是明确告诉报表层:这个样本不能被当成确定数值计入自动门禁。

对象形态变化是从 judge 的非结构化推理变成可路由的反馈记录。低置信度时保留 raw judge 信息,是为了让人工审核者知道模型为什么犹豫,而不是只看到一个空分数。

源码观察点是 aggregate score 如何处理 None、NaN 或缺失指标。若报表层把不确定样本当 0 分,会过度保守;若直接忽略,又可能掩盖高风险输入。需要在 release gate 中单独设置“不确定样本比例”阈值。

框架允许 evaluator 返回灵活结构,是为了容纳真实质量流程。生产评测不是每道题都能自动判定,尤其是开放式生成、安全合规和业务策略类问题。

回归失败分支#

真实源码签名#

if candidate_score < baseline_score - tolerance:
block_release(experiment_id)

细粒度伪代码#

# 简化伪代码,不是源码逐字复制
def compare_experiment_to_baseline(client, candidate_project, baseline_project):
candidate = load_experiment_scores(client, candidate_project)
baseline = load_experiment_scores(client, baseline_project)
failing_examples = []
for example_id, candidate_row in candidate.by_example.items():
baseline_row = baseline.by_example.get(example_id)
if baseline_row is None:
continue
delta = candidate_row.score("correctness") - baseline_row.score("correctness")
if delta < -0.05:
failing_examples.append({
"example_id": example_id,
"candidate_run_id": candidate_row.run_id,
"baseline_run_id": baseline_row.run_id,
"delta": delta,
})
if failing_examples:
return {
"decision": "block_release",
"reason": "regression_detected",
"failures": failing_examples,
}
return {"decision": "allow_release"}

输入是候选实验与基线实验,输出是发布决策和失败样本列表。这里比较的不是单个平均分,而是按 example 对齐后的逐样本差异。平均分可能掩盖关键场景退化。

对象形态变化是从多条 feedback 聚合成 release decision。这个 decision 不属于 LangSmith run 本身,而是工程发布系统消费的门禁结果;它需要保留 candidate_run_id 和 baseline_run_id,便于开发者逐条 diff。

源码观察点是实验项目、数据集版本和指标 key 是否稳定。只要 dataset 或 evaluator key 变了,baseline 就不再可直接比较。评测门禁的可信度来自可重复的样本、固定指标和可追溯版本。

框架这样分层,是为了让 LangSmith 负责证据和评分,CI/CD 或部署平台负责发布动作。不要把“有评测分数”误解为“框架会自动安全上线”。

Retry、Fallback 与恢复边界#

机制适用条件不适用条件幂等要求
Trace retryLangSmith API 临时失败业务调用已经结束且本地无缓存run patch 必须可按 run_id 重试
Feedback retry用户反馈提交网络失败run_id 不存在或权限不足feedback key + source event 应可去重
Evaluator fallbackjudge model 不可用fallback 会降低安全标准fallback 结果必须标注 evaluator 版本
Dataset repair样本字段轻微缺失原始 run 包含敏感信息且未脱敏修复后保留 source_run_id

停止条件与保护上限#

正常结束:run tree 完整、feedback 可追溯、experiment 有逐样本结果。
提前结束:tracing disabled、样本脱敏失败、人工复核未完成。
门禁阻断:candidate 指标低于 baseline、关键样本回归、不确定比例超限。
异常失败:target 或 evaluator 抛错,错误必须进入 run 或 feedback,而不是静默丢弃。

能力边界#

容易误判的能力实际提供者LangSmith 真实职责
自动知道答案是否正确evaluator、人工标注、业务规则保存评分结果并关联 run/example
自动生成高质量数据集样本治理流程提供从 run 晋升 example 的载体
自动阻断所有坏版本CI/CD、发布平台、人工审批提供 experiment 与 baseline 证据
自动修复线上问题开发与产品流程暴露失败 trace、反馈和回归样本

10. 扩展机制与框架协作#

本章补充三个最常见扩展点:自定义 evaluator、从 trace 批量转换 dataset、以及把 experiment 结果接入部署流水线。它们都应该沿用 run、example、feedback 的对象协议,而不是另起一套日志格式。

扩展点总览#

扩展点输入对象输出对象推荐边界
自定义 evaluatorRun + Examplefeedback payload只负责评分,不负责发布
Dataset 转换production run + review labelcurated Example先脱敏、去重、补 reference
部署流水线协作experiment summaryrelease decisionCI/CD 消费决策,不把部署逻辑写进 evaluator

自定义 evaluator#

# 简化伪代码,不是源码逐字复制
def policy_groundedness_evaluator(run, example):
answer = run.outputs.get("answer", "")
citations = run.outputs.get("citations", [])
required_policy_ids = example.metadata.get("required_policy_ids", [])
missing = [policy_id for policy_id in required_policy_ids if policy_id not in citations]
hallucinated = [item for item in citations if not policy_exists(item)]
if missing or hallucinated:
return {
"key": "policy_groundedness",
"score": 0,
"comment": f"missing={missing}, hallucinated={hallucinated}",
"metadata": {"missing": missing, "hallucinated": hallucinated},
}
return {
"key": "policy_groundedness",
"score": 1,
"comment": "answer cites required policy documents",
}

这段 evaluator 的输入是 experiment run 和 dataset example。它不重新调用目标 Agent,也不修改 dataset,只把一次输出是否符合业务证据要求转换成 feedback payload。

对象形态变化很窄:run.outputsexample.metadata 被读取,返回值被 LangSmith 记录成 feedback。这样的边界让 evaluator 可以被不同 experiment 复用,也避免评分逻辑悄悄改变业务结果。

源码观察点是返回字段是否稳定。key 必须长期一致,否则跨实验指标无法比较;metadata 应保留可排障细节,而不是把所有解释都塞进 comment。

Dataset 转换#

# 简化伪代码,不是源码逐字复制
def curate_examples_from_failed_runs(client, run_ids, dataset_id):
created = []
for run_id in run_ids:
run = client.read_run(run_id)
if not is_replayable(run.inputs):
continue
sanitized_inputs = redact_private_fields(run.inputs)
reference = ask_reviewer_for_reference(run, sanitized_inputs)
fingerprint = stable_hash(sanitized_inputs)
if example_exists(dataset_id, fingerprint):
continue
created.append(client.create_example(
dataset_id=dataset_id,
inputs=sanitized_inputs,
outputs=reference,
metadata={
"source_run_id": run.id,
"fingerprint": fingerprint,
"failure_node": run.error.get("node") if run.error else None,
},
))
return created

输入是一组线上失败 run,输出是去重后的 examples。这里最重要的不是“自动加样本”,而是把不可重放、含隐私或没有参考答案的 run 拦下来。

对象形态变化从 trace 事实变成回归契约。trace 可以保留完整上下文,dataset example 必须足够小、可重放、可长期维护。二者不能混用。

源码观察点是去重和 source metadata。没有 fingerprint,回归集会不断膨胀;没有 source_run_id,失败样本又会失去生产来源。

与部署流水线协作#

# 简化伪代码,不是源码逐字复制
def langsmith_release_gate(client, candidate_project, baseline_project):
candidate = summarize_feedback(client, candidate_project)
baseline = summarize_feedback(client, baseline_project)
decision = {
"allow": True,
"reasons": [],
"failed_examples": [],
}
for metric in ["correctness", "policy_groundedness", "safety"]:
if candidate.avg(metric) < baseline.avg(metric) - 0.03:
decision["allow"] = False
decision["reasons"].append(f"{metric} regressed")
decision["failed_examples"].extend(candidate.failures(metric))
if candidate.error_rate > baseline.error_rate + 0.01:
decision["allow"] = False
decision["reasons"].append("target error rate regressed")
return decision

输入是候选实验和基线实验,输出是 CI/CD 可以消费的发布决策。这个函数不部署服务,只回答“证据是否足够支持发布”。

对象形态变化是从多条 feedback 聚合成 release gate。gate 必须保留失败 examples,方便开发者回到 run tree 对比,而不是只给一个红绿灯。

源码观察点是 metric key、dataset 版本和 baseline 项目是否固定。只要其中一个变化,分数就不再可比,部署流水线应要求显式确认。

选择扩展还是重写流程#

场景推荐
只是新增评分维度写 evaluator
需要把线上失败固化为样本写 dataset curation 流程
需要阻断发布在 CI/CD 中消费 experiment summary
需要改变 trace 存储协议优先不要做,除非已有平台级治理需求

11. 工程决策与适用场景#

适用场景#

场景是否推荐原因
旅行规划助手生产化需要状态、工具、检索、观测、评测和恢复闭环
一次性脚本问答直接调用模型更简单
高风险工具调用可以加入 guardrail、interrupt、trace 和 eval gate
大规模知识问答可以把 retrieval、rerank、grounding、evaluation 拆清楚
简单静态 FAQ复杂运行时收益不高

工程决策表#

决策点推荐选择前提风险
是否沉淀 dataset有真实失败样本时沉淀trace 和反馈可用数据污染和隐私问题
是否设置发布门禁线上 Agent 影响用户体验时设置有 baseline 和 evaluator指标单一会误判
是否远程部署需要多人访问或长任务时部署有持久化和观测运行成本和权限复杂度
是否流式观测用户等待时间长时加入前端能消费事件泄露内部状态

性能、可靠性与安全边界#

性能:trace、eval、deployment、stream 都会增加网络、序列化和存储成本。
可靠性:可恢复机制要求状态 schema、幂等工具、稳定 dataset 和错误分类清楚。
安全:metadata、state、tool args、retrieved docs、dataset examples 都可能包含敏感信息。
可观测性:至少记录 thread_id、run_id、dataset version、deployment version、node/tool、策略命中、错误和耗时。

12. 常见误区与源码纠正#

误区:trace 就等于评测#

错误原因:

trace 能看到过程,很容易让人误以为看到过程就等于知道效果。

源码事实:

trace 记录发生了什么;feedback 和 evaluator 才把行为转成可比较的质量信号;dataset 和 experiment 才形成回归闭环。

工程影响:

只有 trace 没有 dataset,团队会停留在“事后看日志”,无法证明新版本是否真的变好。

误区:部署就是把脚本跑在服务器上#

错误原因:

本地 graph 看起来只是一段 Python 代码。

源码事实:

生产部署至少需要 graph 配置、远程 API、thread/run、持久化、观测、环境隔离和回滚策略。

工程影响:

如果只把脚本搬到服务器,长任务、并发、恢复、调试和版本回滚都会变成隐性风险。

误区:LLM-as-judge 可以替代所有人工反馈#

错误原因:

LLM evaluator 成本低、速度快、容易自动化。

源码事实:

LLM-as-judge 是 evaluator 的一种;LangSmith 也支持人工反馈、代码规则、pairwise comparison 和 annotation queue。

工程影响:

高风险场景必须结合人工标注、规则检查和线上反馈,否则评测分数可能和用户价值脱节。


13. 最终心智模型与掌握检查#

构建期心智模型#

Public Config / SDK
Normalize
Build Runtime / LangSmith Object
Attach Evaluation / Deployment / Storage / Trace

运行时心智模型#

Input / Example / Remote Request
Runtime Entry
Context / State / Config / Dataset
Core Execute
Trace / Score / Run / Deployment Output

分支与异常心智模型#

正常路径 → 返回协议对象并更新必要工程状态
分支路径 → 路由、降级、拒答、人工确认或发布阻断
可恢复异常 → retry / fallback / repair / resume
不可恢复异常 → trace + raise + failed example
保护上限 → 停止循环、预算、低分发布或高风险动作

一句话总结#

LangSmith Trace / Evaluation / Dataset把 Agent 从“能运行的源码机制”推进到“可验证、可部署、可观测、可持续改进的工程系统”,它依赖公开 SDK、配置契约、runtime 状态和 LangSmith 工程对象共同形成闭环。

掌握检查#

  • 能说清公开入口与真实执行入口的区别。
  • 能画出构建期对象关系。
  • 能画出运行时对象流转。
  • 能解释至少一个核心函数的伪代码。
  • 能指出同步、异步、流式或批量路径的边界。
  • 能说明异常在哪里抛出、在哪里处理。
  • 能说明正常结束和保护性终止的区别。
  • 能区分公共 API、扩展接口、平台能力和内部实现。
  • 能根据旅行规划助手场景判断是否应该使用该抽象。

14. 参考资料与下一篇衔接#

官方概念文档#

  1. LangSmith Evaluation
    https://docs.langchain.com/langsmith/evaluation

  2. Evaluation concepts
    https://docs.langchain.com/langsmith/evaluation-concepts

  3. Evaluation quickstart
    https://docs.langchain.com/langsmith/evaluation-quickstart

  4. Manage datasets
    https://docs.langchain.com/langsmith/manage-datasets

  5. Log user feedback using the SDK
    https://docs.langchain.com/langsmith/attach-user-feedback

官方 API Reference#

  1. Client
    https://reference.langchain.com/python/langsmith/client/Client

  2. Feedback data format
    https://docs.langchain.com/langsmith/feedback-data-format

官方源码#

  1. python/langsmith/client.py
    https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/client.py

  2. python/langsmith/run_helpers.py
    https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/run_helpers.py

  3. libs/core/langchain_core/runnables/config.py
    https://github.com/langchain-ai/langchain/blob/langchain-core==1.4.8/libs/core/langchain_core/runnables/config.py

下一篇衔接#

下一篇进入:

第 21 篇:LangGraph / LangChain Agent 部署与生产运行机制源码解剖

需要继续回答:

这项能力如何进入旅行规划助手的真实工程链路?
它和前一篇能力如何协作?
哪些边界需要通过源码证据确认?
LangSmith 源码深潜:Trace、Evaluation 与 Dataset 闭环机制解剖
https://jupiter-ws.cn/posts/agent-frameworks/20_langsmith_trace_evaluation_dataset_feedback_source_deep_dive/
作者
Jupiter
发布于
2026-03-24
许可协议
CC BY-NC-SA 4.0