做过 LangGraph Agent 或 RAG 之后,很快就会遇到两个问题:程序能跑,但内部到底发生了什么?以及,这个 Agent 到底好不好?LangSmith 主要解决的就是这两类问题。本文默认你已熟悉 LangChain、LangGraph 和 RAG,直接以一个现成的 RAG Agent 为对象,串起 Trace、Monitoring、Dataset、Evaluator、Experiment 五个核心能力。

一、接入 LangSmith

进入 smith.langchain.com 创建 API Key,然后在 .env 中加入:

LANGCHAIN_API_KEY=你的_LangSmith_API_Key
LANGCHAIN_PROJECT=langsmith-test
LANGCHAIN_TRACING_V2=true

三者分别表示身份认证、Trace 归属项目、开启链路追踪。注意区分:LANGCHAIN_API_KEY 用于 LangSmith,OPENAI_API_KEY 用于模型服务。LangSmith 本身不负责调用业务模型,只负责记录和分析调用过程。配置完成后,LangChain / LangGraph 运行时即可自动上报 Trace。

二、Trace:看清一次 Agent 执行

先看一个最小 LangGraph:

import "dotenv/config";
import { Annotation, START, END, StateGraph } from "@langchain/langgraph";

const StateAnnotation = Annotation.Root({
  text: Annotation({ reducer: (_prev, next) => next, default: () => "" })
});

const stepOk = (state) => ({ text: `${state.text}[ok]` });
const stepThrow = () => { throw new Error("DemoError: 节点内故意抛出异常"); };

const graph = new StateGraph(StateAnnotation)
  .addNode("step_ok", stepOk)
  .addNode("step_throw", stepThrow)
  .addEdge(START, "step_ok")
  .addEdge("step_ok", "step_throw")
  .addEdge("step_throw", END)
  .compile();

try { await graph.invoke({ text: "start" }); }
catch (error) { console.error(error.message); }

运行后进入 Tracing → langsmith-test,可以看到 LangGraph → step_ok → step_throw 的调用树。这里要理解两个概念:Trace 表示一次完整调用,Run 表示 Trace 中的某一个具体执行单元。点开任意 Run,可以看到 Input、Output、Error、Attributes、Latency;节点报错时还能看到完整异常堆栈。与传统日志最大的区别是:日志是按时间输出的字符串,而 Trace 是有父子关系的执行树。

三、在真实 RAG 中看 Trace

假设已有 START → retrieve → generate → END 的 LangGraph RAG,其中 retrieve 调用 retriever.invoke(state.question),generate 把 context 拼接后调用 chain。运行 node src/cli.mjs "无理由退货要在几天内?",终端只看到最终答案,但 LangSmith 中能看到完整调用树:LangGraph → retrieve → VectorStoreRetriever,以及 generate → qwen-plus。

点击 VectorStoreRetriever 可以看到用户问题与实际召回的文档;点击 generate 可以看到它收到的 question 和 context;继续点击模型 Run,可以看到真正发送给模型的 System Prompt、User Message、模型输出与耗时。因此当 RAG 回答异常时,可以快速判断:是 Retriever 召回错了?还是 Context 正确但模型回答错了?还是 Prompt 拼接出了问题?还是某一步耗时异常?

四、Monitoring:从单次请求看向整体

Trace 解决“这一条请求发生了什么”,Monitoring 解决“这段时间整个 Agent 运行得怎么样”。LangSmith Monitoring 中可以观察 Trace Count、成功与失败情况、Latency,以及 LLM Calls、Cost & Tokens、Tools、Run Types 等统计信息。简单区分:调试具体问题看 Trace,观察线上整体健康状态看 Monitoring。

五、Dataset:建立固定测试集

从“可观测”进入“可评估”,需要 Dataset、Evaluator、Experiment 三者配合:Dataset 是测试数据,Evaluator 是评分规则,Experiment 让 Agent 批量执行 Dataset 并用 Evaluator 打分。

安装 pnpm install langsmith,创建 src/evals/build_dataset.mjs:

import "dotenv/config";
import { Client } from "langsmith";

const DATASET_NAME = "rag-eval-v1";
const EXAMPLES = [
  { inputs: { question: "无理由退货要在几天内申请?" },
    outputs: { answer: "自签收之日起 7 天内支持无理由退货。" } },
  { inputs: { question: "满多少元包邮?" },
    outputs: { answer: "满 99 元包邮,部分大件商品和冷链商品除外。" } },
  { inputs: { question: "手机保修多久?" },
    outputs: { answer: "手机、平板和耳机全国联保 1 年。" } }
];

async function main() {
  const client = new Client({ apiKey: process.env.LANGCHAIN_API_KEY });
  let dataset;
  try { dataset = await client.readDataset({ datasetName: DATASET_NAME }); }
  catch {
    dataset = await client.createDataset(DATASET_NAME, { description: "RAG Agent 回归评估集" });
  }
  await client.createExamples(EXAMPLES.map(e => ({
    dataset_id: dataset.id, inputs: e.inputs, outputs: e.outputs
  })));
}
main();

运行后进入 Datasets & Experiments,可以看到 Inputs 与 Reference Outputs。Dataset 的价值在于把零散的人工测试问题变成固定回归测试集,以后修改 Prompt、Retriever、模型或 RAG 参数,都可以重跑同一批数据。

六、Evaluator:给 RAG 定义评分维度

本文使用 OpenEvals 内置的三个 RAG 指标:Groundedness(答案是否被检索上下文支撑)、Helpfulness(回答是否切题、是否解决用户问题)、Retrieval Relevance(检索内容是否与问题相关)。安装 pnpm install openevals,创建 src/evals/evaluators.mjs:

import { createLLMAsJudge, RAG_GROUNDEDNESS_PROMPT, RAG_HELPFULNESS_PROMPT, RAG_RETRIEVAL_RELEVANCE_PROMPT } from "openevals";
import { ChatOpenAI } from "@langchain/openai";

const judge = new ChatOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  model: process.env.MODEL_NAME ?? "qwen-plus",
  temperature: 0
});

const ragGroundednessJudge = createLLMAsJudge({
  prompt: RAG_GROUNDEDNESS_PROMPT, feedbackKey: "rag_groundedness", judge, continuous: true
});
const ragHelpfulnessJudge = createLLMAsJudge({
  prompt: RAG_HELPFULNESS_PROMPT, feedbackKey: "rag_helpfulness", judge, continuous: true
});
const ragRetrievalRelevanceJudge = createLLMAsJudge({
  prompt: RAG_RETRIEVAL_RELEVANCE_PROMPT, feedbackKey: "rag_retrieval_relevance", judge, continuous: true
});

export async function ragGroundednessEvaluator({ outputs }) {
  return ragGroundednessJudge({ context: { documents: outputs.context }, outputs: { answer: outputs.answer } });
}
export async function ragHelpfulnessEvaluator({ inputs, outputs }) {
  return ragHelpfulnessJudge({ inputs, outputs: { answer: outputs.answer } });
}
export async function ragRetrievalRelevanceEvaluator({ inputs, outputs }) {
  return ragRetrievalRelevanceJudge({ inputs, context: { documents: outputs.context } });
}

export const ragEvaluators = [
  ragGroundednessEvaluator, ragHelpfulnessEvaluator, ragRetrievalRelevanceEvaluator
];

三个指标到底在比较什么?Groundedness 比较 Answer 与 Context,关注模型说的内容是否有检索材料支撑;Helpfulness 比较 Answer 与 Question,关注是否答非所问;Retrieval Relevance 比较 Context 与 Question,关注 Retriever 返回的文档是否相关。于是一个 RAG 问题可以被拆成:检索对不对 → 检索正确后回答有没有依据 → 有依据后回答是否真正有用。这比单独给一个“总分”更容易定位问题。

七、Reference Output 与三个指标的关系

这里有一个容易误解的地方:Dataset 中虽然保存了 Reference Outputs,但上述三个 Evaluator 并没有直接使用它,因为它们分别比较的是 Answer vs Context、Answer vs Question、Context vs Question。比如 Reference Output 是“金卡会员享 95 折,同时拥有专属客服和每月优惠券”,而 Agent 只回答“金卡会员享 95 折”,某些指标依然可能给高分,因为这个回答没有脱离 Context,也确实回答了 Question。如果业务还希望评价 Actual Answer 与 Reference Answer 的差异,应再增加答案 Correctness 一类的 Evaluator。Dataset 可以保存标准答案,但最终哪些字段参与评分,由 Evaluator 决定。

八、Experiment:跑一次完整评估

创建 src/evals/run_eval.mjs,先把 RAG 包装成评测目标,再调用 evaluate():

import "dotenv/config";
import { Client } from "langsmith";
import { evaluate } from "langsmith/evaluation";
import { ask } from "../rag_agent.mjs";
import { ragEvaluators } from "./evaluators.mjs";

const DATASET_NAME = "rag-eval-v1";
const client = new Client({ apiKey: process.env.LANGCHAIN_API_KEY });

async function runRagAgent(inputs) {
  const { answer, context } = await ask(inputs.question);
  return { answer, context: context.map(doc => doc.pageContent) };
}

async function main() {
  const result = await evaluate(runRagAgent, {
    data: DATASET_NAME,
    evaluators: ragEvaluators,
    client,
    experimentPrefix: `rag-openevals-${process.env.MODEL_NAME ?? "qwen"}`,
    maxConcurrency: 2
  });
  for await (const _row of result) { /* 等待所有样例完成 */ }
  console.log("✅ 评测完成");
  console.log("实验名:", result.experimentName);
}
main();

运行 node src/evals/run_eval.mjs,LangSmith 会创建一次新的 Experiment。进入 Datasets & Experiments → rag-eval-v1 → Experiments,可以看到 Inputs、Reference Outputs、Outputs,以及 rag_groundedness、rag_helpfulness、rag_retrieval_relevance 等评分字段。评估不再是“感觉回答还不错”,而变成可量化的分数。

Experiment 的真正价值在比较:Experiment A 用原 Prompt,Experiment B 用新 Prompt;或者 Retriever k=4 对比 k=2;或者模型 A 对比模型 B。因为测试集没有变化,可以观察修改究竟让哪些指标变好、哪些变差。这才是 Dataset + Experiment 最核心的工程意义:修改前跑一次,修改后再跑一次,用数据判断修改是否真的有效。

九、把完整逻辑串起来

LangSmith 可以理解成两部分。第一部分是 Observability:Agent → Trace → Run(Input / Output / Error / Latency / Token / Tool Call / LLM Call),再向上汇总为 Monitoring(整体调用量、错误情况、耗时趋势、Token / Cost)。第二部分是 Evaluation:Dataset → Agent → Outputs → Evaluator → Scores → Experiment。它不是单纯的 Trace Viewer,而是把调试、监控、评估串成一套完整工作流。

五个核心概念可以记成一张表:Trace 是一次 Agent 调用的完整链路;Monitoring 是多次调用形成的整体运行统计;Dataset 是固定的测试样本集合;Evaluator 是自动评分规则;Experiment 是在 Dataset 上批量运行并评分的一次实验。对于已经能开发 LangGraph Agent 或 RAG 的工程师来说,LangSmith 真正解决的不是“怎么让 Agent 跑起来”,而是“跑起来以后怎么知道内部发生了什么”,以及“修改一版后怎么证明它真的比上一版更好”。前者由 Trace 和 Monitoring 解决,后者由 Dataset、Evaluator 和 Experiment 解决。当 Agent 从 Demo 走向真实项目,可观测与可评估往往比继续增加更多节点、更多工具调用更重要。