LangSmith 入门:让你的Agent链路可追踪

19 阅读5分钟

你有没有过这种体验:用 LangChain 写了个 RAG 问答,结果答非所问,你盯着代码半天,不知道是检索环节出问题,还是生成环节在胡说八道,甚至不知道到底调了哪个模型、传了什么 prompt、烧了多少 token。

这就是 LLM 应用的"盲盒感"——你不是在调试一个程序,而是在摸黑一个概率系统。

LangSmith 就是来解决这个问题的。今天这篇先讲清楚它是什么、能干什么,以及那个最神奇的体验:为什么只配三个环境变量,一行埋点代码都不用写,整个应用就"看得见"了。

一、LangSmith 是什么

一句话:LangSmith 是 LangChain 官方出的 LLM 应用开发平台,覆盖从调试、评测到上线监控的完整生命周期。

它不生产模型(模型还是你调用的 OpenAI / DashScope / 你自己的),而是站在你的应用和模型之间,把调用链路上的每一步都录下来,让你能看见、能评测、能监控。

你的应用 ──调用──▶ 模型
    │                │
    └──── 埋点上报 ──▶ LangSmith 平台(记录每一步的输入/输出/耗时/成本/token)

二、六大模块,解决六个问题

模块解决什么问题你会看到的关键概念
可观测性 / Tracing调试:看不到中间发生了什么Trace、Span、Run
评测 / Evaluation改完 prompt 怎么知道变好变坏Dataset、Example、Evaluator
Prompt 工程集中管理 prompt、做 A/BPrompt Hub、Playground
监控 & 告警线上质量兜底Dashboard、Alert、规则
反馈收集拿真实用户反馈反哺评测User feedback、标注队列
部署企业版托管 agentDeployment

其中 追踪(Tracing) 和 评测(Evaluation) 是九成人的两个核心场景。本系列第一篇先讲追踪,评测放到第二篇。

三、零侵入接入:三个环境变量搞定一切

最惊艳的地方来了。假设你有一个 RAG 应用,核心代码长这样(用 LangGraph 画图):

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

const GraphState = Annotation.Root({
  question: Annotation,
  context: Annotation,
  answer: Annotation,
});

async function retrieve(state) {
  const docs = await retriever.invoke(state.question); // Milvus 向量检索
  return { context: docs };
}

async function generate(state) {
  const contextText = state.context.map(d => d.pageContent).join("\n\n");
  const answer = await chain.invoke({ context: contextText, question: state.question });
  return { answer };
}

const workflow = new StateGraph(GraphState)
  .addNode("retrieve", retrieve)
  .addNode("generate", generate)
  .addEdge(START, "retrieve")
  .addEdge("retrieve", "generate")
  .addEdge("generate", END);

export const ragApp = workflow.compile();

注意:上面这段代码里,你没有写任何跟 LangSmith 有关的代码。 没有埋点、没有 trace() 调用、没有 callback。

你只需要在 .env 里配三行:

LANGSMITH_TRACING=true       # 开关
LANGSMITH_API_KEY=lsv2_xxx   # 鉴权
LANGSMITH_PROJECT=rag_demo   # 归属项目

然后照常跑 ragApp.invoke({ question: "..." })。打开 LangSmith 网页,你会看到这次调用被完整地拆成了 trace:

ragApp.invoke
  ├─ retrieve(Milvus 检索,耗时 xx ms)
  └─ generate
       ├─ ChatPromptTemplate(填充模板)
       ├─ ChatOpenAI(LLM 调用,消耗 xx token)
       └─ StringOutputParser(解析输出)

每一步的输入、输出、耗时、token 消耗,全都在。这就是零侵入追踪。

四、为什么配一下就能用?(原理拆解)

这不是魔法,是两个机制叠加:

1. dotenv/config 把 .env 变成环境变量

文件顶部 import "dotenv/config",把 .env 里的 KEY=VALUE 加载进 process.env。

2. SDK 内置了 auto-instrumentation

LangChain / LangGraph 的 SDK 内置了 tracing 回调,进程启动时会主动去读那几个约定俗成的变量名:

  • LANGSMITH_TRACING → 决定要不要上报
  • LANGSMITH_API_KEY → 决定"我是谁",用来鉴权
  • LANGSMITH_PROJECT → 决定上报到哪个项目

只要名字对得上,SDK 就自动把每次 invoke() 的调用树序列化成 trace,通过 API 发到 smith.langchain.com。

这种"读环境变量 + 自动上报"的套路,业界叫 auto-instrumentation(自动埋点),跟 OpenTelemetry 的自动埋点是同一个思路。所以"配一下就能用"的本质是:有人(SDK 作者)已经替你写好了埋点,你只需要用约定的方式打开它。

一个容易忽略的坑:变量名必须完全一致

SDK 只认固定名字。如果你 .env 里写 LANGSMITH_API_KEY,代码里却读 LANGCHAIN_API_KEY,读出来就是 undefined,然后静默回退到默认行为——你以为配了,其实没生效。

五、底座:为什么这些东西能拼起来(LCEL / Runnable)

聊 LangSmith 就绕不开 LangChain 的底座。你代码里那些能 .invoke() 的东西——prompt、llm、parser、retriever、甚至编译好的 ragApp——本质都是同一个抽象基类 Runnable 的子类。

Runnable 规定了统一的协议:

类别方法作用
执行invoke / batch / stream怎么跑
组合pipe / bind / withFallbacks怎么拼

最关键的一条设计:组合器本身也是 Runnable。

const chain = RunnableSequence.from([prompt, llm, new StringOutputParser()]);

RunnableSequence 把三个 Runnable 串成一条链,而它自己又是一个 Runnable,所以它能 .invoke()、能继续 .pipe()。数据流是这样:

question → prompt(填模板)→ llm(生成)→ StringOutputParser(剥成字符串)

"任意小积木都能拼成更大积木、接口始终不变"——这就是 LCEL(LangChain Expression Language)能存在的前提,也是 LangSmith 能统一追踪所有这些异构组件的原因(它们都有同一个 invoke 入口)。

六、所以呢?

到这里你应该明白三件事:

  1. LangSmith 解决的是"看不见"的问题 —— LLM 应用不可调试的本质,是缺少可观测性。
  2. 接入成本几乎为零 —— 三个环境变量,靠的是 SDK 预埋的 auto-instrumentation。
  3. 零侵入的前提是统一协议 —— 所有组件都是 Runnable,SDK 才能在一个统一的入口挂上追踪钩子。

但"看得见"只是第一步。下一篇文章回答一个更实际的问题:我改了 prompt 或模型,怎么用数据证明回答"变好了"? 那就要进入 LangSmith 的第二个核心能力——评测(Evaluation)。