Nodejs也能写Agent - 21.LangGraph篇 - 可观测性与 Tracing
上一篇我们把评估体系立起来了:golden set、关键词 + 工具双检、CI 通过率门槛。评估能告诉你 结果对不对。
但 FAIL case 往桌上一扔,下一句问题更扎心:为什么错了?卡在哪一步?是 Prompt 飘了,还是工具参数传歪了? 只看最终 content,你只能猜。这时候需要的是可观测性——把 agent ↔ tools 的每一步摊开:Tracing、结构化日志、关键指标。
老规矩,本文以官网最新文档核对过(LangSmith Observability、Trace 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 中间那几跳全是黑盒。
二、本地先看清: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 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_start、tool_call、tool_result、agent_end、agent_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 别忘了后台回调那一项。
常见坑
- 生产未开 Tracing:出问题只能凭用户口述猜调用链。
- 日志没有
threadId:多会话并发时对不上同一会话的多次请求。 - 只有裸
console.log:没法按event/toolName聚合,做不了大盘。 - 以为 Tracing 能替代日志(或反过来):一个看单次细节树,一个看事件流与统计,互补。
LANGSMITH_API_KEY写进仓库:放.env+.gitignore。- Serverless 用了后台回调还提前冻结进程:trace 没刷完就丢;设
LANGCHAIN_CALLBACKS_BACKGROUND=false。 - 只看最终答案调试 ReAct:中间 tool 轨迹全黑——先上
streamEvents。
可观测性回答 发生了什么 。下一关更本质:模型每一次推理,窗口里到底塞了什么——历史消息、RAG 片段、工具结果一股脑堆进去,窗口爆了或噪声太多,再漂亮的 trace 也救不了胡话。
更多推荐



所有评论(0)