高级功能
🚀 性能优化
让 LangGraph.js 应用更快、更省钱、更稳定:从监控、缓存到并发与限流
📚 学习目标
学完这篇文章后,你将能够:
- 为 Graph 的关键节点添加可读的性能指标(耗时、次数、成本估算)
- 用缓存、批处理、并发控制降低 LLM 与工具调用的总体成本
- 给 Graph 加上合理的“上限”(recursion、重试、超时),避免失控
- 把性能问题拆成可定位的瓶颈:模型慢、工具慢、状态膨胀、分支过多
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- JavaScript 的
async/await - 基础的缓存概念(key/value、TTL)
1️⃣ 先别优化:先量化(Measure First)
LangGraph 的“慢”,通常来自三类:
- 模型调用慢:网络 + 模型推理 + 输出 token
- 工具调用慢:数据库/API/IO
- 状态变大:messages 越积越多,LLM 输入越来越长
你要做的第一件事不是改代码,而是加一个“最小可用”的监控。
2️⃣ 节点耗时监控:一个轻量装饰器
最常见的做法:对每个节点包一层 wrapper,记录执行耗时。
type NodeFn<S, U> = (state: S) => Promise<U> | U;
export function withTiming<S, U>(nodeName: string, node: NodeFn<S, U>) {
return async (state: S): Promise<U> => {
const start = performance.now();
try {
const result = await node(state);
const ms = performance.now() - start;
console.log(`[timing] ${nodeName}: ${ms.toFixed(1)}ms`);
return result;
} catch (err) {
const ms = performance.now() - start;
console.error(`[timing] ${nodeName} failed after ${ms.toFixed(1)}ms`, err);
throw err;
}
};
}代码解析:
- 这段代码不依赖 LangGraph API,本质是一个函数装饰器。
- 你可以把它用在“工具节点”“检索节点”“模型节点”等所有耗时点。
3️⃣ 控制 Graph 上限:循环、重试与超时
性能优化里最容易被忽略的不是“快”,而是“别跑爆”。
1. recursionLimit:避免无限循环
当你使用 ReAct Loop(Agent -> Tools -> Agent)时,一定要设置上限。
await app.invoke(
{ messages: [{ role: 'user', content: '...' }] },
{
recursionLimit: 20,
configurable: {
thread_id: 'user-1:session-1',
},
}
);2. 工具调用超时:把外部慢依赖隔离
function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
p.then(
(v) => {
clearTimeout(timer);
resolve(v);
},
(e) => {
clearTimeout(timer);
reject(e);
}
);
});
}
async function searchWithTimeout(query: string) {
return withTimeout(fetch(`https://example.com?q=${encodeURIComponent(query)}`).then(r => r.json()), 5000);
}4️⃣ 让状态别长胖:messages 裁剪与摘要
聊天应用的性能问题,很多不是“执行慢”,而是“输入越来越长”。
一个可操作的策略是:
- 保留最近 N 轮消息
- 更早的消息压缩为“摘要”字段
import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';
const State = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (current, update) => {
const next = current.concat(update);
const keep = 20;
return next.length > keep ? next.slice(next.length - keep) : next;
},
default: () => [],
}),
summary: Annotation<string>({
default: () => '',
}),
});说明:
- 这不是“唯一正确答案”,但它能把最常见的成本爆炸问题压住。
- 更完善的方案:把“摘要更新”做成一个独立节点,用模型对历史做压缩。
5️⃣ 缓存:对重复查询/重复工具调用很有效
最适合缓存的通常是:
- 文档检索结果(RAG)
- 结构化工具调用结果(天气、汇率、配置)
type CacheValue<T> = { value: T; expiresAt: number };
export class SimpleTTLCache<T> {
private map = new Map<string, CacheValue<T>>();
constructor(private ttlMs: number) {}
get(key: string): T | undefined {
const hit = this.map.get(key);
if (!hit) return undefined;
if (Date.now() > hit.expiresAt) {
this.map.delete(key);
return undefined;
}
return hit.value;
}
set(key: string, value: T) {
this.map.set(key, { value, expiresAt: Date.now() + this.ttlMs });
}
}💡 练习题
- 实现题:给你的“检索节点”加上 TTL 缓存(5 分钟),缓存 key 使用
query + topK。 - 压测题:连续调用 50 次 Graph,打印每个节点的 P50/P95(可以先用数组收集耗时)。
- 设计题:如果你要做“多用户同时聊天”,你会把缓存做成进程内、Redis 还是数据库?为什么?
📚 参考资源
官方文档
✅ 总结
本章要点:
- 性能优化从“可测量”开始:先给节点加 timing,再决定优化点。
- 通过上限(recursion/timeout/retry)避免系统失控。
- 控制状态体积(messages 裁剪/摘要)往往比“微优化”更有效。
- 缓存与并发控制是“省钱”的关键手段。
下一步:当你准备上线,请补齐《测试策略》,确保你每次改动都能快速回归验证。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。