上一篇我们把评估体系立起来了:golden set、关键词 + 工具双检、CI 通过率门槛。评估能告诉你 结果对不对

但 FAIL case 往桌上一扔,下一句问题更扎心:为什么错了?卡在哪一步?是 Prompt 飘了,还是工具参数传歪了? 只看最终 content,你只能猜。这时候需要的是可观测性——把 agent ↔ tools 的每一步摊开:Tracing、结构化日志、关键指标。

老规矩,本文以官网最新文档核对过(LangSmith ObservabilityTrace with LangChain)。createAgent 开箱就能挂 LangSmith;本篇本地先用 stream / streamEvents 看清轨迹(不必先有 API Key),联调/生产再开自动上报。别再抄 createReactAgent

在这里插入图片描述

一、Agent 调试为什么特别难

传统 Web:一次请求 → 一条调用链,日志基本线性。Agent 不是:

难点 说明
多轮消息 messages 越积越长,要看见每轮 LLM 的入参 / 出参
工具调用链 agent → tools → agent 循环,每步 args 与 result 都得可见
条件分支 addConditionalEdges 走了哪条路?
状态演进 Checkpointer、interrupt / resume,同一 thread_id 多次 invoke 怎么变的
异步与流式 stream / streamEvents 的事件顺序 vs 最终 State

没有 trace,你往往只能 console.log 最后一轮答案——ReAct 中间那几跳全是黑盒。

Eval FAIL

为什么错?

本地 streamEvents

LangSmith Trace

结构化日志

还原决策路径

二、本地先看清:stream / streamEvents

没开 LangSmith 也能调试。Compiled graph(createAgent 底层就是)支持流式接口:

  • stream:按模式吐 State 增量 / 更新(适合看节点推进)
  • streamEvents:更细的事件流(LLM 起止、tool 起止等)——本地还原轨迹的利器

下面用 createAgent + 天气工具,把轨迹打到控制台:

import { createAgent } from "langchain";
import { ChatOllama } from "@langchain/ollama";
import { tool } from "@langchain/core/tools";
import * as z from "zod";

const getWeather = tool(
  async ({ city }: { city: string }) => `${city}:晴,25°C`,
  {
    name: "get_weather",
    description: "查询城市天气",
    schema: z.object({ city: z.string() }),
  }
);

const llm = new ChatOllama({ model: "qwen2.5:7b", temperature: 0 });
const agent = createAgent({
  model: llm,
  tools: [getWeather],
  systemPrompt: "需要天气信息时调用 get_weather。",
});

const input = {
  messages: [{ role: "user", content: "北京天气怎么样?" }],
};

// 细粒度事件:看清 LLM / tool 何时进出
for await (const ev of agent.streamEvents(input, { version: "v2" })) {
  if (ev.event === "on_chat_model_end") {
    const msg = ev.data?.output;
    const tools = msg?.tool_calls?.map((c: { name: string }) => c.name);
    console.log("[llm]", tools?.length ? `tool_calls=${tools}` : "final text");
  }
  if (ev.event === "on_tool_end") {
    console.log("[tool]", ev.name, "→", String(ev.data?.output).slice(0, 80));
  }
}

你会大致看到:先 LLM 决定调 get_weather → tool 返回「北京:晴,25°C」→ 再 LLM 吐出人话总结。这比只盯最终 content 有用一百倍。

手写 StateGraph + ToolNode同样有 stream / streamEvents——心智一模一样:编排层出事件,零件层还是 LangChain。

三、LangSmith Tracing:环境变量就能开

LangSmith 是 LangChain 生态的可观测平台。对 createAgent / LangGraph,设好环境变量后,正常 invoke 就会自动上报——不必给业务代码撒满埋点。

必配环境变量

变量 作用
LANGSMITH_TRACING 设为 true 启用自动 trace
LANGSMITH_API_KEY 账号 API Key(只放 .env,别进 Git)
LANGSMITH_PROJECT 项目名;不设则进 default
LANGSMITH_ENDPOINT 默认美区 https://api.smith.langchain.com;EU 等区域要改对应 endpoint

应用入口集中读 env 即可(dotenv / 部署平台注入都行):

// 应用启动最早处;API Key 从 .env 注入,勿写死在代码里
process.env.LANGSMITH_TRACING ??= "true";
process.env.LANGSMITH_PROJECT ??= "nodejs-agent-blog";
// LANGSMITH_API_KEY 已由环境提供

import { createAgent } from "langchain";
import { ChatOllama } from "@langchain/ollama";
import { tool } from "@langchain/core/tools";
import * as z from "zod";

const getWeather = tool(
  async ({ city }: { city: string }) => `${city}:晴,25°C`,
  {
    name: "get_weather",
    description: "查询城市天气",
    schema: z.object({ city: z.string() }),
  }
);

const agent = createAgent({
  model: new ChatOllama({ model: "qwen2.5:7b", temperature: 0 }),
  tools: [getWeather],
});

// 开启 Tracing 后,这次 invoke 自动出现在 LangSmith
const result = await agent.invoke(
  { messages: [{ role: "user", content: "北京天气怎么样?" }] },
  {
    configurable: { thread_id: "trace-demo-1" },
    metadata: {
      source: "blog-12",
      user_query_preview: "北京天气",
    },
  }
);

Trace 长什么样

Root: agent.invoke

Child: ChatOllama

Child: get_weather

Child: ChatOllama

  • Root Run:一次 agent.invoke / graph.invoke
  • Child Run:节点里的 LLM 调用、tool 执行
  • thread_id / metadata:按会话筛选、按来源过滤;和 Checkpointer 的会话钥匙对齐最好

JS 里多记一句:后台回调

官网对 LangChain.js + LangSmith 的建议:

环境 建议
普通 Node 长进程 LANGCHAIN_CALLBACKS_BACKGROUND=true,降低等待上报的延迟
Serverless / 短命函数 LANGCHAIN_CALLBACKS_BACKGROUND=false等 trace 刷完再退出,否则请求结束了 span 还没送出去

非 LangChain 调用也可以用 LangSmith 的 traceable 包一层——本篇不展开,需要时查官网即可。

四、结构化日志:和 Tracing 互补

Tracing 擅长「按请求还原整棵调用树」;结构化日志擅长「按事件聚合、进 ELK/Loki、做告警」。别互相替代。

建议每条日志至少有:

字段 说明
timestamp ISO 8601
level info / warn / error
threadId 会话 ID,与 Checkpointer 一致
runId 单次 invoke 唯一 ID(可选,便于对齐 LangSmith)
event agent_starttool_calltool_resultagent_endagent_error
toolName 工具相关事件时带上
durationMs agent_end / tool_result 时记录耗时
import { createAgent } from "langchain";
import { ChatOllama } from "@langchain/ollama";

interface LogEvent {
  timestamp: string;
  level: "info" | "warn" | "error";
  threadId: string;
  event: string;
  toolName?: string;
  durationMs?: number;
  [key: string]: unknown;
}

function log(event: Omit<LogEvent, "timestamp" | "level"> & { level?: LogEvent["level"] }) {
  const entry: LogEvent = {
    timestamp: new Date().toISOString(),
    level: event.level ?? "info",
    ...event,
  };
  console.log(JSON.stringify(entry));
}

async function invokeWithLogging(
  agent: ReturnType<typeof createAgent>,
  input: { messages: { role: string; content: string }[] },
  threadId: string
) {
  const start = Date.now();
  log({ threadId, event: "agent_start" });

  try {
    const result = await agent.invoke(input, {
      configurable: { thread_id: threadId },
    });
    log({
      threadId,
      event: "agent_end",
      durationMs: Date.now() - start,
    });
    return result;
  } catch (err) {
    log({
      threadId,
      event: "agent_error",
      level: "error",
      durationMs: Date.now() - start,
      error: String(err),
    });
    throw err;
  }
}

const agent = createAgent({
  model: new ChatOllama({ model: "qwen2.5:7b", temperature: 0 }),
  tools: [],
});

await invokeWithLogging(
  agent,
  { messages: [{ role: "user", content: "你好" }] },
  "session-abc-123"
);

想在日志里看到 tool_call / tool_result:在上一节的 streamEvents 循环里,命中 on_tool_start / on_tool_end 时同样 log({ event: "tool_call", toolName: ... }) 即可——一套事件,两种出口(控制台调试 + JSON 采集)。

五、关键指标(点到为止)

生产除了单次 trace,还要聚合:

指标 用途
latency p50 / p95,发现慢查询
token 用量 控成本;可从 trace / 回调里取
tool 成功率 工具挂了还是模型乱调
error 率 超时、429、模型不可用
拦截率 安全 Guard 挡住了多少(后文安全篇)

大盘用 Prometheus / Grafana 或云厂商监控都行;本篇不搭整套运维。

六、本地 vs 生产:怎么选兵器

场景 推荐手段
开发机 stream / streamEvents + 结构化 console;可不接 LangSmith
联调 / 预发 LangSmith Tracing + JSON 日志
生产 LangSmith(或等价 APM)+ 日志采集 + 指标大盘

上线前务必验证:LANGSMITH_TRACING=true 且 Key 正确时,dashboard 里真能看到 run。Serverless 别忘了后台回调那一项。

常见坑

  1. 生产未开 Tracing:出问题只能凭用户口述猜调用链。
  2. 日志没有 threadId:多会话并发时对不上同一会话的多次请求。
  3. 只有裸 console.log:没法按 event / toolName 聚合,做不了大盘。
  4. 以为 Tracing 能替代日志(或反过来):一个看单次细节树,一个看事件流与统计,互补。
  5. LANGSMITH_API_KEY 写进仓库:放 .env + .gitignore
  6. Serverless 用了后台回调还提前冻结进程:trace 没刷完就丢;设 LANGCHAIN_CALLBACKS_BACKGROUND=false
  7. 只看最终答案调试 ReAct:中间 tool 轨迹全黑——先上 streamEvents

可观测性回答 发生了什么 。下一关更本质:模型每一次推理,窗口里到底塞了什么——历史消息、RAG 片段、工具结果一股脑堆进去,窗口爆了或噪声太多,再漂亮的 trace 也救不了胡话。

更多推荐