✦ Puxiaoshuai · Time is a river painted on scrolls · Walk to the water’s end, sit and watch the clouds rise

langchainlanggraphlangsmith2026.09.04 · 30 min read

从「黑盒」到「考卷」: LangSmith 的可观测与量化评测

面向用 LangChain / LangGraph 构建 Agent 与 RAG 的开发者,用一个「客服问答」的例子把 Trace、Monitor、Dataset、Evaluator、Experiment 五个概念讲透,并给出可直接照抄的最小代码,让 Agent 既「看得见」又「量得出」。

L

Leo

2026.09.04 · Updated 2026.09.10

2 views
从「黑盒」到「考卷」: LangSmith 的可观测与量化评测

从「黑盒」到「考卷」: LangSmith 的可观测与量化评测

为什么写这篇

刚接触 Agent 的时候,调试靠 console.log 就够用——一个 chain 里就两步,打印出来一目了然。可一旦代码长成这个样子:向量库召回 → 重排 → LLM 生成 → 工具调用 → 再生成,问题就来了:

  • 用户说答错了,但你不知道是召回错了,还是模型编造了知识库里没有的内容;
  • 模型有时传错工具参数,你还得靠它自己承认;
  • token 每天在涨,不知道烧在哪一步;
  • 改了一版 prompt,感觉变好了——但「感觉」不能写进周报。

这时候需要两个能力,缺一不可:

  1. 可观测性(Observability):每次运行像航班黑匣子一样被完整记录下来,任何一步都能回放、定位;
  2. 量化评测(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 越来越稳。

L

Leo

Blogger

Independent developer / Blogger and the maintainer of the original blog “大道至简”. Migrating years of posts and shiyu from WordPress to Next.js.

Reader comments

COMMENTS · 0

Leave a comment

Comments are shown after moderation · be kind0/100