LangSmith:从链路追踪到 RAG 自动化评估

31 阅读4分钟

做过 LangGraph Agent 或 RAG 之后,很快就会遇到一个问题:

程序虽然能跑,但内部到底发生了什么?

一次请求经过了哪些节点?Retriever 返回了什么?模型真正收到的 Prompt 是什么?哪一步最慢?Token 消耗多少?某次异常究竟出在哪个 Run?

再往后还会遇到另一个问题:

这个 Agent 或 RAG 到底“好不好”?

靠人工随便问几个问题,只能得到主观感受。真正进入工程阶段之后,我们需要的是可观测、可回归、可量化。

LangSmith 主要解决的就是这两类问题。

本文默认你已经熟悉 LangChain、LangGraph 和 RAG,不再展开解释向量数据库、Embedding 或 Graph 编排,而是直接以一个现成的 RAG Agent 为对象,学习 LangSmith 的几个核心能力:

能力解决的问题
Trace一次请求内部到底发生了什么
Monitoring一段时间内整体运行得怎么样
Dataset如何管理标准测试样本
Evaluator如何定义评估指标
Experiment如何批量跑测试并比较结果

最终我们会完成这样一个闭环:

Agent / RAG
    ↓
Trace
    ↓
Monitoring
    ↓
Dataset
    ↓
Evaluator
    ↓
Experiment

一、接入 LangSmith

LangSmith 的接入成本很低。

进入:

https://smith.langchain.com/

创建 API Key。

然后在现有项目的 .env 中加入:

LANGCHAIN_API_KEY=你的_LangSmith_API_Key

LANGCHAIN_PROJECT=langsmith-test

LANGCHAIN_TRACING_V2=true

这三个变量分别表示:

LANGCHAIN_API_KEY
→ LangSmith 身份认证

LANGCHAIN_PROJECT
→ Trace 归属哪个项目

LANGCHAIN_TRACING_V2
→ 开启链路追踪

如果项目本身还使用模型和向量数据库,那么 .env 可能类似:

# ===== LangSmith =====

LANGCHAIN_API_KEY=xxx
LANGCHAIN_PROJECT=langsmith-test
LANGCHAIN_TRACING_V2=true


# ===== LLM =====

OPENAI_API_KEY=xxx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-plus


# ===== Embedding =====

EMBEDDING_MODEL=text-embedding-v3


# ===== Milvus =====

MILVUS_URI=http://localhost:19530
MILVUS_COLLECTION=rag_docs

这里最重要的是区分:

LANGCHAIN_API_KEY

用于 LangSmith。

而:

OPENAI_API_KEY

用于模型服务。

LangSmith 本身并不负责调用你的业务模型,它负责记录和分析调用过程。

当环境变量配置完成之后,LangChain / LangGraph 运行时就可以自动将 Trace 上报到 LangSmith。


二、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);
}

运行之后,进入 LangSmith:

Tracing
→ langsmith-test

可以看到类似:

LangGraph
├── step_ok
└── step_throw

这里要先理解两个概念。

Trace 表示一次完整调用。

Run 表示 Trace 中的某一个具体执行单元。

所以一个复杂 Agent 可能是:

Trace
│
├── retrieve
│   └── VectorStoreRetriever
│
├── tool_call
│
└── generate
    └── ChatOpenAI

点开任意 Run,可以看到:

Input
Output
Error
Attributes
Latency

如果节点报错,还能看到完整异常堆栈。

这和传统日志最大的区别是:

日志通常是一堆按时间输出的字符串,而 Trace 是有父子关系的执行树。


三、在真实 RAG 中看 Trace

假设我们已经有一个 LangGraph RAG:

START
  ↓
retrieve
  ↓
generate
  ↓
END

其中:

async function retrieve(state) {

  const docs =
    await retriever.invoke(
      state.question
    );

  return {
    context: docs
  };
}

生成节点:

async function generate(state) {

  const contextText =
    state.context
      .map(doc => doc.pageContent)
      .join("\n\n");

  const answer =
    await chain.invoke({
      context: contextText,
      question: state.question
    });

  return {
    answer
  };
}

运行:

node src/cli.mjs "无理由退货要在几天内?"

终端可能只看到:

无理由退货需在自签收之日起 7 天内申请。

但 LangSmith 中可以看到完整调用树:

LangGraph
├── retrieve
│   └── VectorStoreRetriever
└── generate
    └── qwen-plus

这时候 Trace 就非常有用了。

点击 VectorStoreRetriever,可以看到用户问题以及实际召回的文档。

点击 generate,可以看到它收到的 question 和 context。

继续点击模型 Run,可以看到真正发送给模型的 System Prompt、User Message、模型输出,以及调用耗时等信息。

因此,当 RAG 回答异常时,就可以快速判断:

是 Retriever 召回错了?

还是 Context 正确,但模型回答错了?

还是 Prompt 拼接出了问题?

还是某一步耗时异常?

这也是 LangSmith 在 Agent 调试阶段最直接的价值。


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

Trace 解决的是:

这一条请求发生了什么?

Monitoring 解决的是:

这段时间整个 Agent 运行得怎么样?

LangSmith Monitoring 中可以观察 Trace Count、成功与失败情况、Latency,以及 LLM Calls、Cost & Tokens、Tools、Run Types 等统计信息。

例如:

Success
Error

可以用来观察整体成功率。

Latency 则可以帮助判断性能是否发生变化。

因此可以把二者简单区分为:

Trace
→ 单次调用诊断

Monitoring
→ 整体运行趋势

调试一个具体问题时看 Trace。

观察线上 Agent 的整体健康状态时看 Monitoring。


五、从“可观测”进入“可评估”

到这里解决的是:

Agent 是怎么运行的?

下一步要解决:

Agent 运行得好不好?

LangSmith 为此提供了 Dataset、Evaluator 和 Experiment。

三者之间的关系非常简单:

Dataset
   ↓
测试数据

Evaluator
   ↓
评分规则

Experiment
   ↓
让 Agent 批量执行 Dataset
并使用 Evaluator 打分

假设我们的 RAG 提供:

const {
  answer,
  context
} = await ask(question);

那么现在要做的,就是准备一组标准测试样本。


六、Dataset:建立固定测试集

安装:

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 年。"
    }
  }

];

然后创建 Dataset:

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(example => ({

      dataset_id:
        dataset.id,

      inputs:
        example.inputs,

      outputs:
        example.outputs
    }))
  );
}

main();

运行:

node src/evals/build_dataset.mjs

进入:

Datasets & Experiments

就可以看到:

Inputs
Reference Outputs

这里的:

Inputs

是给 Agent 的测试输入。

而:

Reference Outputs

是预先准备的参考答案。

Dataset 的价值在于,把原本零散的人工测试问题变成一套固定的回归测试集。

以后修改 Prompt、Retriever、模型或者 RAG 参数,都可以重新跑同一批 Dataset。


七、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";

定义 Judge:

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
  });

Groundedness:

const ragGroundednessJudge =
  createLLMAsJudge({

    prompt:
      RAG_GROUNDEDNESS_PROMPT,

    feedbackKey:
      "rag_groundedness",

    judge,

    continuous: true
  });

Helpfulness:

const ragHelpfulnessJudge =
  createLLMAsJudge({

    prompt:
      RAG_HELPFULNESS_PROMPT,

    feedbackKey:
      "rag_helpfulness",

    judge,

    continuous: true
  });

Retrieval Relevance:

const ragRetrievalRelevanceJudge =
  createLLMAsJudge({

    prompt:
      RAG_RETRIEVAL_RELEVANCE_PROMPT,

    feedbackKey:
      "rag_retrieval_relevance",

    judge,

    continuous: true
  });

然后分别封装成 Evaluator:

export async function
ragGroundednessEvaluator({
  outputs
}) {

  return ragGroundednessJudge({

    context: {
      documents:
        outputs.context
    },

    outputs: {
      answer:
        outputs.answer
    }
  });
}

Helpfulness:

export async function
ragHelpfulnessEvaluator({
  inputs,
  outputs
}) {

  return ragHelpfulnessJudge({

    inputs,

    outputs: {
      answer:
        outputs.answer
    }
  });
}

Retrieval Relevance:

export async function
ragRetrievalRelevanceEvaluator({
  inputs,
  outputs
}) {

  return ragRetrievalRelevanceJudge({

    inputs,

    context: {
      documents:
        outputs.context
    }
  });
}

最后统一导出:

export const ragEvaluators = [

  ragGroundednessEvaluator,

  ragHelpfulnessEvaluator,

  ragRetrievalRelevanceEvaluator
];

八、三个 RAG 指标到底在比较什么

这里非常值得单独理解。

Groundedness:

Answer
  VS
Context

它关注:

模型说的内容是否有检索材料支撑?
有没有脱离上下文胡编?

Helpfulness:

Answer
  VS
Question

它关注:

回答有没有真正解决用户的问题?
是否答非所问?

Retrieval Relevance:

Context
  VS
Question

它关注:

Retriever 返回的文档到底相关不相关?

于是一个 RAG 问题可以被拆成:

检索对不对?
↓
Retrieval Relevance

检索正确之后,
回答有没有依据?
↓
Groundedness

有依据以后,
回答是否真正有用?
↓
Helpfulness

这比单独给 RAG 一个“总分”更容易定位问题。


九、Reference Output 和这三个指标的关系

这里有一个非常容易误解的地方。

Dataset 中虽然保存了:

Reference Outputs

但刚才这三个 Evaluator 并没有直接使用它。

因为它们分别比较的是:

Groundedness
Answer vs Context

Helpfulness
Answer vs Question

Retrieval Relevance
Context vs Question

比如:

Reference Output:

金卡会员享 95 折,
同时拥有专属客服和每月优惠券。

而 Agent 实际只回答:

金卡会员享 95 折。

某些指标依然可能给高分。

因为这个回答:

没有脱离 Context
而且确实回答了 Question

如果业务还希望评价:

Actual Answer
VS
Reference Answer

那么应该再增加答案 Correctness 一类的 Evaluator。

所以 Dataset 中的 Reference Output 和 Evaluator 是两个独立概念。

Dataset 可以保存标准答案。

但最终哪些字段参与评分,由 Evaluator 决定。


十、Experiment:真正跑一次完整评估

创建:

src/evals/run_eval.mjs

首先把 RAG 包装成一个评测目标:

async function runRagAgent(inputs) {

  const {
    answer,
    context
  } =
    await ask(
      inputs.question
    );

  return {

    answer,

    context:
      context.map(
        doc =>
          doc.pageContent
      )
  };
}

然后调用 LangSmith 的:

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。


十一、Experiment 页面怎么看

进入:

Datasets & Experiments
→ rag-eval-v1
→ Experiments

可以看到类似:

Inputs
Reference Outputs
Outputs
rag_groundedness
rag_helpfulness
rag_retrieval_relevance

其中:

Inputs

是 Dataset 中的问题。

Reference Outputs

是 Dataset 中的参考答案。

Outputs

是 Agent 实际生成的结果。

后面的各个 rag_* 字段就是 Evaluator 给出的评分。

这时评估就不再是:

“感觉回答还不错”

而变成:

Groundedness = ?
Helpfulness = ?
Retrieval Relevance = ?

这就是所谓的量化评估。


十二、Experiment 的真正价值在“比较”

只跑一次 Experiment 的意义有限。

更有价值的用法是:

Experiment A
→ 原 Prompt

Experiment B
→ 新 Prompt

或者:

Experiment A
→ Retriever k = 4

Experiment B
→ Retriever k = 2

或者:

Experiment A
→ 模型 A

Experiment B
→ 模型 B

然后在同一套 Dataset 上比较。

因为测试集没有变化,所以你可以观察修改究竟让哪些指标变好了,哪些指标变差了。

这才是 Dataset + Experiment 最核心的工程意义:

修改前
↓
跑 Experiment

修改后
↓
再跑 Experiment

↓
用数据判断修改是否真的有效

十三、把 LangSmith 的完整逻辑串起来

最终可以把 LangSmith 理解成两部分。

第一部分是 Observability:

Agent
  ↓
Trace
  ↓
Run
  ↓
Input / Output
Error
Latency
Token
Tool Call
LLM Call

再向上汇总:

Trace
  ↓
Monitoring
  ↓
整体调用量
错误情况
耗时趋势
Token / Cost

第二部分是 Evaluation:

Dataset
  ↓
Agent
  ↓
Outputs
  ↓
Evaluator
  ↓
Scores
  ↓
Experiment

所以它不是单纯的 Trace Viewer。

它把:

调试
+
监控
+
评估

串成了一套完整工作流。


十四、总结

如果只记住 LangSmith 的五个核心概念,可以记成这张表:

概念一句话理解
Trace一次 Agent 调用的完整链路
Monitoring多次调用形成的整体运行统计
Dataset固定的测试样本集合
Evaluator自动评分规则
Experiment在 Dataset 上批量运行并评分的一次实验

对于已经能够开发 LangGraph Agent 或 RAG 的工程师来说,LangSmith 真正解决的不是:

“怎么让 Agent 跑起来?”

而是:

“Agent 跑起来以后,我怎么知道它内部发生了什么?”

以及:

“我修改了一版 Agent,怎么证明它真的比上一版更好?”

前者由 Trace 和 Monitoring 解决。

后者由 Dataset、Evaluator 和 Experiment 解决。

当 Agent 从 Demo 走向真实项目时,这两种能力往往比继续增加更多节点、更多工具调用更重要:

可观测
+
可评估

这也是 LangSmith 最值得学习的地方。