LangSmith源码学习路线:第 20 篇 Trace、Evaluation 与 Dataset 闭环源码解剖
核心问题: LangSmith 如何把线上 Agent trace、用户反馈、数据集和 evaluator 串成“发现问题 → 生成样本 → 回归评测 → 证明变好”的工程闭环?
源码主线:
traceable / callbacks→Run / Trace→Feedback→Dataset Example→Evaluator→Experiment→Regression Gate前置文章: 第 13 篇
Streaming / Observability、第 19 篇Time Travel / Debug Replay依赖基线:
langsmith当前正式版、langgraph==1.2.7、langchain==1.3.11、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 串成“发现问题 → 生成样本 → 回归评测 → 证明变好”的工程闭环?
学习目标
完成本篇后,读者必须能够:
- 画出LangSmith Trace / Evaluation / Dataset的构建期对象关系。
- 解释一次运行时调用如何进入主链。
- 说明关键分支、异常和停止条件。
- 区分公共 API、SDK 契约、平台能力和内部实现。
- 根据旅行规划助手场景设计验证或上线闭环。
能力边界
| 能力 | 本篇是否覆盖 | 说明 |
|---|---|---|
| 构建期对象组装 | 是 | 解释公开参数如何变成可运行或可评测对象 |
| 运行时主链 | 是 | 解释 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 / Run | Run | 一次调用链及其子步骤记录 | 普通日志行 |
| Feedback | Feedback | 针对 run 或 span 的评分、标签和备注 | 最终答案本身 |
| Dataset | Dataset | 评测样本集合 | 线上 trace 全量备份 |
| Evaluator | code / LLM-as-judge / human | 对输出或轨迹打分 | 业务目标自动达成 |
| Experiment | Experiment | 一次模型/提示词/代码版本在 dataset 上的评测运行 | 单次手工测试 |
与相邻抽象的边界
| 对象 | 负责什么 | 不负责什么 | 与本篇对象的关系 |
|---|---|---|---|
RunnableConfig | 传递 metadata、tags、callbacks | 不保存业务状态 | 让 trace 和部署观测可关联 |
Pregel / Graph Runtime | 执行 graph、stream、checkpoint | 不自动证明业务变好 | 提供可观测运行底座 |
| LangSmith | trace、feedback、evaluation、deployment | 不替业务定义成功标准 | 支撑验证和上线闭环 |
3. 完整执行链路
本篇的主线不是“调用 LangSmith API 得到一个结果”,而是观察一次 Agent 调用如何被记录、沉淀、评分,并反过来约束下一次发布。最小闭环可以拆成七个对象:RunnableConfig.callbacks、LangSmith Run、trace span、Example、evaluator、Feedback、regression experiment。
从一次 Runnable 调用观察对象变化
from langchain_core.runnables import RunnableLambdafrom 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_id 与 parent_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关键文件
| 优先级 | 文件 | 核心对象 | 阅读目的 |
|---|---|---|---|
| 1 | langsmith/client.py | Client | 理解 dataset、feedback、experiment SDK 入口 |
| 2 | langsmith/run_helpers.py | traceable | 理解 tracing 装饰器入口 |
| 3 | langsmith/evaluation/ | evaluator helpers | 理解评测运行组织方式 |
| 4 | langchain_core/runnables/config.py | RunnableConfig | 理解 metadata/tags 如何进入 trace |
| 5 | LangSmith Evaluation docs | Evaluation 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、project | Client 与默认 tracing 上下文 |
RunnableConfig | 合并 callbacks、tags、metadata、run_name | 可向子 Runnable 传播的 callback manager |
| 样本 dict | 拆成 inputs、outputs、metadata | Example 与 Dataset |
| 评分函数 | 规范化为接收 run / example 的 evaluator | experiment 可调用的评分器 |
构建期的产物本身通常不执行模型调用。它们的价值是把运行时需要的“观察通道”和“评测契约”提前整理好,避免真正执行时才临时猜测字段含义。
Trace 配置构建
真实源码签名
from langsmith import Client, traceablefrom 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_start、on_llm_start、on_tool_end 事件都不需要重新读取环境,只要沿着 config 找到 handler 即可。
源码观察点应放在三处:Client 如何保存 endpoint 与认证信息,callback handler 如何在 start/end/error 时调用 client,RunnableConfig 如何在组合链中向子 Runnable 传播。这样才能解释为什么在根链上设置 tags 和 metadata,子 span 通常也能在 LangSmith 中继承到上下文。
框架这样设计,是为了把观测从业务逻辑中剥离出来。业务函数仍然只写输入输出;trace 能力通过 callback 协议插入。这样既能给普通函数加 @traceable,也能给复杂 Runnable 图加统一追踪。
关键分支与异常路径
| 条件 | 构建期行为 | 运行时影响 |
|---|---|---|
| 未配置 api key | client 可构造失败或 tracing 被禁用 | 业务链可选择继续,但不会上传 trace |
| project 未显式指定 | 使用默认 project 或环境变量 | run 会落到默认项目,影响后续检索 |
| 用户已有 callbacks | 追加 LangSmith handler 而不是覆盖 | 保留日志、指标、UI 流式输出等其他能力 |
| metadata 不可序列化 | 构建或上传时清洗/失败 | 避免把无法 JSON 化的对象写入 run |
源码证据
langsmith/client.py::Clientlangsmith/run_helpers.py::traceablelangchain_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 和多条 Example。Example.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_datasetlangsmith/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.outputs 和 example.outputs 分别取预测值与参考值。
对象形态变化是从“评分规则”变成“反馈生成器”。evaluator 的返回 dict 会被 LangSmith 解释为 feedback:key 是指标名,score 是可聚合的数值,comment 和 metadata 是排障证据。
源码观察点包括 evaluator 返回值的规范化、异常捕获、以及 experiment 如何把评分结果挂到对应 run 上。阅读时不要只看 judge prompt,要追踪评分结果如何成为 LangSmith 的 feedback 记录。
框架这样设计,是因为评测标准经常变化。把 evaluator 做成普通函数,用户可以混合 exact match、规则、embedding 相似度、LLM-as-judge 和人工标注,而 experiment runner 只需要消费统一 feedback 协议。
关键分支与异常路径
| 条件 | 构建期行为 | 运行时影响 |
|---|---|---|
| evaluator 返回 bool | 规范化为 score | 可聚合但解释信息少 |
| evaluator 返回多个指标 | 生成多条 feedback | 可以分别分析正确性、格式、安全性 |
| judge model 未配置 | 构建期失败或跳过该 evaluator | experiment 仍可运行其他 evaluator |
| 指标 key 不稳定 | 构建期应固定命名 | 否则跨实验无法比较 |
源码证据
- LangSmith Evaluation API
- LangSmith evaluator 返回协议
构建期产物
| 产物 | 保存的信息 | 运行时用途 |
|---|---|---|
Client | endpoint、api key、workspace/project | 上传 run、example、feedback、experiment |
RunnableConfig | callbacks、tags、metadata、run_name | 把 trace 上下文传入每次 Runnable 调用 |
Dataset / Example | inputs、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 record | feedback 对象或响应 |
| 批量评测 | client.evaluate(target, data=..., evaluators=...) | experiment + example runs | experiment 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_*_start 与 on_*_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_start | inputs、tags、metadata、run_id | 观测失败应降级,不应破坏业务主链 |
| 子调用 | on_tool_start / on_chat_model_start | parent_run_id、serialized、messages | 形成 trace span |
| 成功 | on_chain_end / on_llm_end | outputs、token usage、latency | patch run 并可触发 evaluator |
| 失败 | on_chain_error / on_tool_error | exception、节点名、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 release9. 关键分支、异常与边界
本章回答:trace、feedback、dataset、evaluation 偏离正常主链时,应该如何分流?重点是区分业务失败、观测失败、评分失败和发布门禁失败。它们的处理策略不一样。
分支矩阵
| 分支类型 | 触发条件 | 核心对象 | 结果 |
|---|---|---|---|
| Trace 分支 | callback 上传失败、metadata 不可序列化 | run / callback event | 业务继续或降级记录 |
| Feedback 分支 | run_id 缺失、用户差评、重复评价 | feedback | 写入失败、待审或进入样本池 |
| Evaluation 分支 | target 抛错、evaluator 抛错、judge 不确定 | experiment run / feedback | 失败样本、人工复核或跳过指标 |
| Regression 分支 | candidate 分数低于 baseline | dataset / 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_id 和 source_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 retry | LangSmith API 临时失败 | 业务调用已经结束且本地无缓存 | run patch 必须可按 run_id 重试 |
| Feedback retry | 用户反馈提交网络失败 | run_id 不存在或权限不足 | feedback key + source event 应可去重 |
| Evaluator fallback | judge 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 的对象协议,而不是另起一套日志格式。
扩展点总览
| 扩展点 | 输入对象 | 输出对象 | 推荐边界 |
|---|---|---|---|
| 自定义 evaluator | Run + Example | feedback payload | 只负责评分,不负责发布 |
| Dataset 转换 | production run + review label | curated Example | 先脱敏、去重、补 reference |
| 部署流水线协作 | experiment summary | release decision | CI/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.outputs 和 example.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. 参考资料与下一篇衔接
官方概念文档
-
LangSmith Evaluation
https://docs.langchain.com/langsmith/evaluation -
Evaluation concepts
https://docs.langchain.com/langsmith/evaluation-concepts -
Evaluation quickstart
https://docs.langchain.com/langsmith/evaluation-quickstart -
Manage datasets
https://docs.langchain.com/langsmith/manage-datasets -
Log user feedback using the SDK
https://docs.langchain.com/langsmith/attach-user-feedback
官方 API Reference
官方源码
-
python/langsmith/client.py
https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/client.py -
python/langsmith/run_helpers.py
https://github.com/langchain-ai/langsmith-sdk/blob/main/python/langsmith/run_helpers.py -
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 部署与生产运行机制源码解剖需要继续回答:
这项能力如何进入旅行规划助手的真实工程链路?它和前一篇能力如何协作?哪些边界需要通过源码证据确认?