从「黑盒」到「考卷」: LangSmith 的可观测与量化评测
面向用 LangChain / LangGraph 构建 Agent 与 RAG 的开发者,用一个「客服问答」的例子把 Trace、Monitor、Dataset、Evaluator、Experiment 五个概念讲透,并给出可直接照抄的最小代码,让 Agent 既「看得见」又「量得出」。
Leo
2026.09.04 · 更新于 2026.09.10
从「黑盒」到「考卷」: LangSmith 的可观测与量化评测
为什么写这篇
刚接触 Agent 的时候,调试靠 console.log 就够用——一个 chain 里就两步,打印出来一目了然。可一旦代码长成这个样子:向量库召回 → 重排 → LLM 生成 → 工具调用 → 再生成,问题就来了:
- 用户说答错了,但你不知道是召回错了,还是模型编造了知识库里没有的内容;
- 模型有时传错工具参数,你还得靠它自己承认;
- token 每天在涨,不知道烧在哪一步;
- 改了一版 prompt,感觉变好了——但「感觉」不能写进周报。
这时候需要两个能力,缺一不可:
- 可观测性(Observability):每次运行像航班黑匣子一样被完整记录下来,任何一步都能回放、定位;
- 量化评测(Evaluation):把「好不好」从主观感觉变成一个个分数,改动前后能对比、能回归。
LangSmith 把这套东西做成了开箱即用的平台,核心就是五个概念:
一句话记法:Trace 是全过程录像,Monitor 是体检报告,Dataset 是考卷,Evaluator 是阅卷官,Experiment 是一场考试。
先用一张类比表建立直觉,下文逐个展开:
| 概念 | 类比 | 解决什么问题 | 关键对象 |
|---|---|---|---|
| Trace | 航班黑匣子 | 一次运行全过程可回放 | Run / run tree |
| Monitor | 医院体检单 | 长期趋势、异常、成本监控 | 指标看板 / 告警 |
| Dataset | 考试试卷 | 用什么题来检验效果 | Example(题 + 标准答案) |
| Evaluator | 阅卷官 | 把一次回答换算成分数 | 打分函数 → feedback |
| Experiment | 一场考试 | 对一套 Agent 配置的量化体检 | experiment(可横向对比) |
一、Trace:改一行环境变量,全程自动回放
一句话:Trace 是 LangSmith 自动采集的「运行流水账」——你的 LangGraph 图、LangChain 链每跑一次,框架就把每一步的输入输出、工具参数与返回值、token 消耗、耗时、报错都记录下来,你可以在网页上点开任意一层查看。
为什么几乎不用改代码?因为 LangChain / LangGraph 的核心抽象是 Runnable,框架本身就内置了回调(callback)埋点机制。SDK 一检测到追踪环境变量,就会为每次 invoke 自动创建一棵「运行树」(run tree):根节点是一次完整运行,子节点是链里的每一层、图中的每个节点、每次工具调用、每次模型调用。
你只需要在环境变量里配上三样东西:
# 标识「你是谁」,凭它在 LangSmith 平台定位数据
LANGCHAIN_API_KEY=lsv2_xxxxxxxxxxxx
# 标识「哪个项目」,同一应用的数据归档到一起
LANGCHAIN_PROJECT=my-rag
# 一键开启追踪(true 即可,SDK 读到后自动打点)
LANGCHAIN_TRACING_V2=true
业界常把这三样合称 LangSmith 三要素:
API_KEY(身份)、PROJECT(归类)、TRACING_V2(开关)。这是面试里最容易考到的细节。
配好环境变量后,业务代码一行不用动。下面是一个最小可运行的示例(RAG 的骨架):
import "dotenv/config";
import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { Milvus } from "@langchain/community/vectorstores/milvus";
const llm = new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 });
const vectorStore = await Milvus.fromExistingCollection(
new OpenAIEmbeddings({ model: process.env.EMBEDDING_MODEL }),
{ collectionName: "rag_docs", url: "http://localhost:19530" },
);
const retriever = vectorStore.asRetriever({ k: 4 });
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是客服助手,仅根据上下文回答,不知道就说不知道。\n\n{context}"],
["human", "{question}"],
]);
const chain = prompt.pipe(llm);
const State = Annotation.Root({
question: Annotation,
answer: Annotation,
});
async function retrieve(state) {
const docs = await retriever.invoke(state.question); // 图节点一:召回
return { answer: docs }; // 示意:实际应挂 context
}
async function generate(state) {
const answer = await chain.invoke({ context: "", question: state.question }); // 图节点二:生成
return { answer };
}
const ragApp = new StateGraph(State)
.addNode("retrieve", retrieve)
.addNode("generate", generate)
.addEdge(START, "retrieve")
.addEdge("retrieve", "generate")
.addEdge("generate", END)
.compile();
await ragApp.invoke({ question: "无理由退货是几天?" });
跑完后打开 LangSmith 的项目页,就能看到这次的完整链路:图里 retrieve、generate 两个节点各自成一格,点进去能看到该节点的输入、输出;模型调用那一格能看 prompt 全文、completion、token 用量、延迟;哪一步抛了异常会标红,异常堆栈直接贴出来。双击任意一次运行,还能拿到可分享的 trace 链接,方便甩给同事「看这条」。
要点:
- 无侵入:基于 Runnable 回调自动埋点,日常代码零改动,只需三行环境变量;
- 按项目隔离:
LANGCHAIN_PROJECT决定数据落在哪个项目,不同应用别混用一个项目; - 细到节点:图中的每个 node、chain 的每一层、tool 的入参与返回值、模型调用的 token/耗时都可单独点开;
- 错误可回放:一次失败的运行保留完整快照与报错栈,比猜「哪一步挂了」高效得多;
- 不止 LangChain:
@langchain/langgraph的图、@langchain/core的链天然可追踪;如果你要追踪一段普通函数,可用langsmith导出的traceable()包一层,原理是手动建 run tree。
二、Monitor:把一次次运行聚合成趋势
一句话:Trace 看「单次放大镜」,Monitor 看「长期望远镜」——它把成千上万次 trace 聚合成指标曲线与看板,让你看到调用量、错误率、延迟、token 成本的变化趋势,并能在指标异常时告警。
单个 trace 只能回答「这次发生了什么」。但线上 Agent 每天跑几千次,你需要回答的是「整体健不健康」:
| 关注维度 | 典型指标 | 能发现什么 |
|---|---|---|
| 用量 | 调用次数、会话数、工具被调用了几次 | 某个 tool 是不是变成了高频但没用的动作 |
| 成本 | 每次运行 token、按模型拆分费用 | 模型越换越贵?某条链异常烧钱? |
| 质量 | 错误率、feedback 分数均值 | 上线后回答质量有没有滑 |
| 性能 | 平均 / P95 延迟 | 哪类问题越来越慢 |
Monitor 的背后其实还是 trace——每次运行的指标被抽出来、按时间切片做统计,所以它和 Trace 是同一条数据管道的两个视图,不是两套系统。
要点:
- 聚合视角:单看会忽略,拉长曲线才能看出趋势与周期性异常;
- 可设阈值告警:给指标设期望值(比如错误率 > 5% 就提醒),把「人肉盯」变成「系统盯」;
- 适合盯线上:改动发版后,用 Monitor 观察指标是否有回归;
- 面试可这样说:Trace 保证「查得到」,Monitor 保证「看得见趋势、防得住劣化」。
三、Dataset:一份「问题 + 标准答案」的考卷
一句话:Dataset 是一组测试样例,每条 example 至少含 inputs(问题/入参),通常还带 outputs(标准答案,作为评分参照),用来统一口径地检验你的 Agent。
没有考卷就没法考试。数据集的定位就是固定一份「题」,让不同版本、不同模型的 Agent 都在同一张卷子上作答,分数才可比。
一条样例长这样:
example
├── inputs: { question: "金卡会员有什么折扣?" } ← 题:喂给 Agent 的入参
└── outputs: { answer: "金卡享 95 折。" } ← 标准答案:评分参照(可选)
用 langsmith 的 Client 建数据集,代码量很小:
import "dotenv/config";
import { Client } from "langsmith";
const client = new Client({ apiKey: process.env.LANGCHAIN_API_KEY });
const NAME = "rag-eval"; // 数据集名,之后跑实验按名字引用
let dataset;
try {
dataset = await client.readDataset({ datasetName: NAME }); // 已存在则复用
} catch {
dataset = await client.createDataset(NAME, { description: "客服 RAG 回归集" });
}
await client.createExamples([
{
dataset_id: dataset.id,
inputs: { question: "无理由退货要几天内申请?" },
outputs: { answer: "签收后 7 天内可无理由退货。" },
},
{
dataset_id: dataset.id,
inputs: { question: "支持哪些支付方式?" },
outputs: { answer: "微信、支付宝、云闪付、花呗/信用卡分期。" },
},
// …… 想覆盖多少场景就加多少条
]);
数据集也可以直接在 LangSmith 网页的 Dataset 页面里手工增删改,SDK 建数据更适合批量、或从线上日志回流。
要点:
- 覆盖要刻意:把易错问题、边界情况(涉及退款、保修、物流)都放进去,别只放「模型本来就会的」;
- 标准答案可选但建议有:像忠实度这类无参考 RAG 指标用不到它,但人工核对和精确匹配类指标需要它;
- 复用是关键价值:一份数据集反复用于回归、对比,才有「横比」的意义;
- 配环境变量:同样靠
LANGCHAIN_API_KEY鉴权,代码里的client就是说话人身份。
四、Evaluator:从多个维度打分的「阅卷官」
一句话:Evaluator 是一个把「一次运行」换算成「一个或几个分数」的函数——收到被测模型的回答(以及问题、召回片段、可选的参考答案),返回 { key, score, comment } 这样的评分对象,分数会上报成对应那条 trace 的 feedback。
评估器大致分三类:
| 类型 | 原理 | 典型用法 | 例子 |
|---|---|---|---|
| 代码 / 规则 | 直接比对 | 精确匹配、关键词命中、JSON 结构校验 | answer.includes("7天") |
| LLM-as-judge | 让另一个 LLM 当裁判 | 语义类、开放类指标 | 忠实度、有用性 |
| 人工 | 人在 UI 里打分 | 抽样复核 | 星标 / 点赞 |
对 RAG 场景,最常用的就是 LLM-as-judge,衡量回答在「知识库约束」下到底行不行。LangChain 官方的 openevals 包内置了这三个维度的评测 prompt,开箱即用:
| 指标(feedback key) | 中文常称 | 评判角度 | 专门抓的问题 |
|---|---|---|---|
rag_groundedness | 忠实度 / 接地气 | 回答是否被检索到的上下文支撑 | 幻觉:编造知识库没有的内容 |
rag_helpfulness | 回答有用性 | 是否切题、解决用户问题 | 答非所问、空话套话 |
rag_retrieval_relevance | 检索相关性 | 召回的片段是否和问题相关 | 检索出一堆无关文档,源头就错了 |
用 openevals 生成三个「阅卷官」,每个都是给定一局输入输出、返回一个分数的函数:
import {
createLLMAsJudge,
RAG_GROUNDEDNESS_PROMPT,
RAG_HELPFULNESS_PROMPT,
RAG_RETRIEVAL_RELEVANCE_PROMPT,
} from "openevals";
import { ChatOpenAI } from "@langchain/openai";
// 裁判 LLM(最好与被测模型不同、temperature=0)
const judge = new ChatOpenAI({ model: process.env.MODEL_NAME, temperature: 0 });
// 忠实度阅卷官:有没有照着上下文说人话(防幻觉)
const groundedness = createLLMAsJudge({
prompt: RAG_GROUNDEDNESS_PROMPT,
feedbackKey: "rag_groundedness", // 这个 key 会作为指标名出现在报表里
judge,
continuous: true, // 输出 0~1 连续分,而不是非黑即白
});
// 有用性阅卷官:有没有切题
const helpfulness = createLLMAsJudge({
prompt: RAG_HELPFULNESS_PROMPT,
feedbackKey: "rag_helpfulness",
judge,
continuous: true,
});
// 检索相关性阅卷官:召回片段和问题相关吗
const retrievalRelevance = createLLMAsJudge({
prompt: RAG_RETRIEVAL_RELEVANCE_PROMPT,
feedbackKey: "rag_retrieval_relevance",
judge,
continuous: true,
});
阅卷官是被 evaluate() 调用的,它收到的入参约定是 { inputs, outputs, referenceOutputs }:
inputs—— 数据集里的问题/入参;outputs—— 被测 Agent 自己跑出来的回答(含你返回的召回片段 context);referenceOutputs—— 数据集里配的标准答案(只有数据集里写了才有)。
比如把三个阅卷官包装成能被 evaluate 直接使用的评估器函数:
// outputs 来自被测 Agent 的返回值:{ answer, context: string[] }
export const ragEvaluators = [
({ outputs }) =>
groundedness({ context: { documents: outputs.context }, outputs: { answer: outputs.answer } }),
({ inputs, outputs }) =>
helpfulness({ inputs, outputs: { answer: outputs.answer } }),
({ inputs, outputs }) =>
retrievalRelevance({ inputs, context: { documents: outputs.context } }),
];
如果你要自己写一个规则型评估器,也极其直白——它本质上就是个返回分数的函数:
// 规则型:答案里必须包含「7 天」
async function containsAnswer({ outputs, referenceOutputs }) {
const keyword = (referenceOutputs?.answer ?? "").match(/[\d]+天/)?.[0];
const hit = keyword ? outputs.answer.includes(keyword) : false;
return { key: "contains_7_days", score: hit ? 1 : 0 }; // 上报成 feedback
}
要点:
- 返回值即分数:返回
{ key, score, comment? },score 一般是 0~1;不带 key 时默认用函数名; - 多维度叠加:同一条回答可以同时挂忠实度、有用性、检索相关性几个分数,互不干扰;
- judge 与被测模型最好分开:用第三方 / 更稳的模型当裁判,避免「自己判自己」的偏差;
- RAG 三个黄金指标:忠实度(防幻觉)、有用性(答非所问)、检索相关性(源头准不准)——这是做 RAG 评测的必背三连,也是面试最常被问的量化维度。
五、Experiment:在同一张考卷上批量跑分
一句话:Experiment 是一次「考试」——evaluate() 拿到你的 Agent、指定 dataset 和 evaluator,会遍历考卷上每题让 Agent 作答、再用阅卷官逐题打分,最后产出一份带名字的实验报告,可和其他实验横向对比。
到了这一步,前面所有概念才真正闭环:Dataset 出题 → Agent 作答(trace 全程记录)→ Evaluator 打分(feedback 上报)→ 汇总成 Experiment 报告。而对比不同实验,你就能回答「新 prompt 到底有没有更好」「换模型值不值」这类问题。
import "dotenv/config";
import { evaluate } from "langsmith/evaluation";
// 复用上文的阅卷官
import { ragEvaluators } from "./evaluators.js";
/** 被测 Agent:给定问题走「召回 + 生成」,返回 { answer, context } */
async function runRag({ question }) {
const docs = await retriever.invoke(question);
const context = docs.map((d) => d.pageContent);
const answer = await chain.invoke({
context: context.join("\n\n"),
question,
});
return { answer, context }; // context 会交给检索相关性阅卷官
}
// 在数据集上批量作答 + 打分
const result = await evaluate(runRag, {
data: "rag-eval", // 用哪张考卷(数据集名)
evaluators: ragEvaluators, // 用哪些阅卷官
experimentPrefix: "rag-qwen-v3", // 实验名前缀,方便日后区分
maxConcurrency: 4, // 并发跑,省时间
});
// evaluate 返回的是异步流,逐个排空即触发逐条跑分
for await (const _row of result) {
/* drain */
}
console.log("实验名:", result.experimentName);
console.log("各指标汇总分数可在 LangSmith UI 查看");
跑完后打开 Experiment 页面你会看到:
- 每题一行,列出被测模型原始回答 + 召回片段 + 每个阅卷官的分数,能点进去看对应 trace;
- 顶部是该实验各指标的平均分(如 groundedness 0.92 / helpfulness 0.88 / retrieval_relevance 0.80);
- 挑出两条不同的实验(比如换模型前 / 后),平台可以直接做逐题对比,一眼看到哪几题被新配置救回来了、哪几题反而退步。
实验命名建议带上可区分的标识(模型、prompt 版本、召回 topK 等),例如 rag-qwen-v3、rag-qwen-v3-topk6——这样对比表里的列名就是自解释的。
要点:
- evaluate = Agent × 数据集 × 评估器 的一次批量考试,产出可对比的实验;
- 回归利器:改 prompt / 换模型 / 调召回参数前,先在同一数据集上跑一版「基线实验」,改完再跑一版,diff 分数说话;
- 可追溯:实验里每个分数都链到真实 trace,分数异常时能下钻到具体那次运行;
- 并发可控:
maxConcurrency控制同时跑多少题,别把服务打爆; - 异步流要排空:
for await消费完,实验才真正跑完、分数才完整。
总结:一张图看清五者关系 + 怎么选
把全流程串起来看,就是下面这条流水线:
决策速查表
| 你的处境 | 用哪个概念 | 怎么做 |
|---|---|---|
| 刚接上 LangChain,想先看跑得对不对 | Trace | 配好三行环境变量,跑一次去项目页看链路 |
| 上线了,想盯稳定性 / 成本 | Monitor | 看指标曲线,给错误率设告警 |
| 想系统检验 Agent 效果 | Dataset + Evaluator | 先建覆盖易错点的考卷,用 openevals 当阅卷官 |
| 改了一版 prompt / 换了模型,心里没底 | Experiment | 同一数据集跑两版实验,diff 分数 |
| 同事说「你那个 Agent 到底行不行」 | Dataset + Experiment | 给他看实验报告里的量化分数与对比 |
面试一句话怎么答
「Agent 的效果要靠两件事兜底:可观测和量化。可观测我靠 LangSmith 的 Trace(每次运行全链路回放、错误可定位)+ Monitor(指标趋势与告警);量化我靠 Dataset(固定一份问题+标准答案的考卷)+ Evaluator(用 openevals 的 LLM-as-judge 从忠实度、回答有用性、检索相关性打分)+ Experiment(在数据集上批量跑分、横比不同配置)。改之前先留基线分数,改完跑同一张考卷,让数据说话。」
收个尾
做完这套,你会发现自己对 Agent 的认知从「凭感觉调 prompt」升级成了「用考卷 + 分数迭代」——这也正是生产级 Agent 和玩具 demo 的分水岭。回到开头的三个 RAG 黄金指标:忠实度管「别胡编」、检索相关性管「别找错」、回答有用性管「别跑题」,三个维度都有分数、可对比、可回归,你才真正拥有了把 Agent 越调越好的方向盘。
后续可以再深挖的方向:给自定义工具调用过程加 traceable 追踪、用 Experiment 对比不同检索 topK、把线上坏样本自动回流成数据集——每一步都会让这张「考卷」越来越难,也让你的 Agent 越来越稳。
读者留言
COMMENTS · 0发表留言