做过 LangGraph Agent 或 RAG 之后,很快就会遇到一个问题:
程序虽然能跑,但内部到底发生了什么?
一次请求经过了哪些节点?Retriever 返回了什么?模型真正收到的 Prompt 是什么?哪一步最慢?Token 消耗多少?某次异常究竟出在哪个 Run?
再往后还会遇到另一个问题:
这个 Agent 或 RAG 到底"好不好"?
靠人工随便问几个问题,只能得到主观感受。真正进入工程阶段之后,我们需要的是可观测、可回归、可量化。
LangSmith 主要解决的就是这两类问题。
本文默认你已经熟悉 LangChain、LangGraph 和 RAG,不再展开解释向量数据库、Embedding 或 Graph 编排,而是直接以一个现成的 RAG Agent 为对象,学习 LangSmith 的几个核心能力:
| 能力 | 解决的问题 |
|---|---|
| Trace | 一次请求内部到底发生了什么 |
| Monitoring | 一段时间内整体运行得怎么样 |
| Dataset | 如何管理标准测试样本 |
| Evaluator | 如何定义评估指标 |
| Experiment | 如何批量跑测试并比较结果 |
最终我们会完成这样一个闭环:
markdown
Agent / RAG
↓
Trace
↓
Monitoring
↓
Dataset
↓
Evaluator
↓
Experiment
一、接入 LangSmith
LangSmith 的接入成本很低。
进入:
arduino
https://smith.langchain.com/
创建 API Key。
然后在现有项目的 .env 中加入:
ini
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 可能类似:
ini
# ===== 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:
vbnet
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,可以看到:
css
Input
Output
Error
Attributes
Latency
如果节点报错,还能看到完整异常堆栈。
这和传统日志最大的区别是:
日志通常是一堆按时间输出的字符串,而 Trace 是有父子关系的执行树。
三、在真实 RAG 中看 Trace
假设我们已经有一个 LangGraph RAG:
sql
START
↓
retrieve
↓
generate
↓
END
其中:
javascript
async function retrieve(state) {
const docs =
await retriever.invoke(
state.question
);
return {
context: docs
};
}
生成节点:
ini
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
};
}
运行:
bash
node src/cli.mjs "无理由退货要在几天内?"
终端可能只看到:
无理由退货需在自签收之日起 7 天内申请。
但 LangSmith 中可以看到完整调用树:
markdown
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 等统计信息。
例如:
javascript
Success
Error
可以用来观察整体成功率。
Latency 则可以帮助判断性能是否发生变化。
因此可以把二者简单区分为:
Trace
→ 单次调用诊断
Monitoring
→ 整体运行趋势
调试一个具体问题时看 Trace。
观察线上 Agent 的整体健康状态时看 Monitoring。
五、从"可观测"进入"可评估"
到这里解决的是:
Agent 是怎么运行的?
下一步要解决:
Agent 运行得好不好?
LangSmith 为此提供了 Dataset、Evaluator 和 Experiment。
三者之间的关系非常简单:
Dataset
↓
测试数据
Evaluator
↓
评分规则
Experiment
↓
让 Agent 批量执行 Dataset
并使用 Evaluator 打分
假设我们的 RAG 提供:
csharp
const {
answer,
context
} = await ask(question);
那么现在要做的,就是准备一组标准测试样本。
六、Dataset:建立固定测试集
安装:
pnpm install langsmith
创建:
bash
src/evals/build_dataset.mjs
例如:
arduino
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:
ini
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();
运行:
bash
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
创建:
bash
src/evals/evaluators.mjs
导入:
javascript
import {
createLLMAsJudge,
RAG_GROUNDEDNESS_PROMPT,
RAG_HELPFULNESS_PROMPT,
RAG_RETRIEVAL_RELEVANCE_PROMPT
} from "openevals";
import {
ChatOpenAI
} from "@langchain/openai";
定义 Judge:
php
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:
php
const ragGroundednessJudge =
createLLMAsJudge({
prompt:
RAG_GROUNDEDNESS_PROMPT,
feedbackKey:
"rag_groundedness",
judge,
continuous: true
});
Helpfulness:
php
const ragHelpfulnessJudge =
createLLMAsJudge({
prompt:
RAG_HELPFULNESS_PROMPT,
feedbackKey:
"rag_helpfulness",
judge,
continuous: true
});
Retrieval Relevance:
php
const ragRetrievalRelevanceJudge =
createLLMAsJudge({
prompt:
RAG_RETRIEVAL_RELEVANCE_PROMPT,
feedbackKey:
"rag_retrieval_relevance",
judge,
continuous: true
});
然后分别封装成 Evaluator:
php
export async function
ragGroundednessEvaluator({
outputs
}) {
return ragGroundednessJudge({
context: {
documents:
outputs.context
},
outputs: {
answer:
outputs.answer
}
});
}
Helpfulness:
javascript
export async function
ragHelpfulnessEvaluator({
inputs,
outputs
}) {
return ragHelpfulnessJudge({
inputs,
outputs: {
answer:
outputs.answer
}
});
}
Retrieval Relevance:
javascript
export async function
ragRetrievalRelevanceEvaluator({
inputs,
outputs
}) {
return ragRetrievalRelevanceJudge({
inputs,
context: {
documents:
outputs.context
}
});
}
最后统一导出:
ini
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:真正跑一次完整评估
创建:
bash
src/evals/run_eval.mjs
首先把 RAG 包装成一个评测目标:
javascript
async function runRagAgent(inputs) {
const {
answer,
context
} =
await ask(
inputs.question
);
return {
answer,
context:
context.map(
doc =>
doc.pageContent
)
};
}
然后调用 LangSmith 的:
scss
evaluate()
完整代码:
javascript
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();
运行:
bash
node src/evals/run_eval.mjs
LangSmith 会创建一次新的 Experiment。
十一、Experiment 页面怎么看
进入:
javascript
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 给出的评分。
这时评估就不再是:
"感觉回答还不错"
而变成:
ini
Groundedness = ?
Helpfulness = ?
Retrieval Relevance = ?
这就是所谓的量化评估。
十二、Experiment 的真正价值在"比较"
只跑一次 Experiment 的意义有限。
更有价值的用法是:
css
Experiment A
→ 原 Prompt
Experiment B
→ 新 Prompt
或者:
ini
Experiment A
→ Retriever k = 4
Experiment B
→ Retriever k = 2
或者:
css
Experiment A
→ 模型 A
Experiment B
→ 模型 B
然后在同一套 Dataset 上比较。
因为测试集没有变化,所以你可以观察修改究竟让哪些指标变好了,哪些指标变差了。
这才是 Dataset + Experiment 最核心的工程意义:
修改前
↓
跑 Experiment
修改后
↓
再跑 Experiment
↓
用数据判断修改是否真的有效
十三、把 LangSmith 的完整逻辑串起来
最终可以把 LangSmith 理解成两部分。
第一部分是 Observability:
vbnet
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。
它把:
diff
调试
+
监控
+
评估
串成了一套完整工作流。
十四、总结
如果只记住 LangSmith 的五个核心概念,可以记成这张表:
| 概念 | 一句话理解 |
|---|---|
| Trace | 一次 Agent 调用的完整链路 |
| Monitoring | 多次调用形成的整体运行统计 |
| Dataset | 固定的测试样本集合 |
| Evaluator | 自动评分规则 |
| Experiment | 在 Dataset 上批量运行并评分的一次实验 |
对于已经能够开发 LangGraph Agent 或 RAG 的工程师来说,LangSmith 真正解决的不是:
"怎么让 Agent 跑起来?"
而是:
"Agent 跑起来以后,我怎么知道它内部发生了什么?"
以及:
"我修改了一版 Agent,怎么证明它真的比上一版更好?"
前者由 Trace 和 Monitoring 解决。
后者由 Dataset、Evaluator 和 Experiment 解决。
当 Agent 从 Demo 走向真实项目时,这两种能力往往比继续增加更多节点、更多工具调用更重要:
diff
可观测
+
可评估
这也是 LangSmith 最值得学习的地方。