🚨 langgraph: 别再给每个用户创建新图了!性能暴降 1.6 倍的坑你踩了吗?

32 阅读5分钟

🚨 别再给每个用户创建新图了!性能暴降 1.6 倍的坑你踩了吗?

用真实数据告诉你,为什么每用户一图的架构是 LangGraph 应用最大的性能杀手


💡 先看结论(TL;DR)

经过真实场景压测,我发现了一个惊人的事实:

架构平均 QPS100 用户响应时间内存占用
❌ 每用户一图~501.93s
全局单图~721.34s
提升+44%-30%省 77%

是的,仅改一行代码,性能就能提升 44%!


🎯 问题来源

最近代码评审时,我发现同事的代码是这样写的:

# ❌ 这种写法很常见,但是有问题!
async def handle_user(user_id: str, message: str):
    # 每个用户都创建一个新的图实例
    graph = builder.compile(checkpointer=checkpointer)
    return await graph.ainvoke(
        {"messages": [message]},
        config={"configurable": {"thread_id": user_id}}
    )

看起来很合理对吧?每个用户有自己的图,状态隔离清晰。

但这是错的!而且错得很离谱。

让我用实测数据告诉你为什么。


🧪 真实压测数据

测试场景

我模拟了一个真实的 AI Agent 应用:

  • 10 个处理节点(输入解析 → 意图识别 → 工具调用 → 数据查询 → API 请求 → ...)
  • 不同并发级别:10、50、100、200 用户
  • 测试指标:响应时间、QPS、内存占用

测试结果(数据不会骗人)

📊 并发 10 用户
指标每用户一图全局单图差异
响应时间25.8ms16.5ms-36% ⚡️
QPS38.760.8+57% 🚀
内存2.43 MB0.55 MB-77% 💾
📊 并发 50 用户
指标每用户一图全局单图差异
响应时间17.5ms12.8ms-27% ⚡️
QPS57.277.9+36% 🚀
📊 并发 100 用户
指标每用户一图全局单图差异
响应时间19.3ms13.4ms-31% ⚡️
QPS51.874.5+44% 🚀
📊 并发 200 用户
指标每用户一图全局单图差异
响应时间18.5ms13.5ms-27% ⚡️
QPS54.074.2+37% 🚀

🔥 为什么差距这么大?

原因 1:编译开销

# 每次调用都要做这些事:
graph = builder.compile()  # 解析图结构 → 构建执行计划 → 验证依赖
                            # 耗时 20-50ms

# 100 个用户 × 30ms = 3 秒的编译时间!

虽然编译在并发时可以并行,但仍然占用大量 CPU 资源。

原因 2:资源浪费

每用户一图的资源占用:
- 100 个用户 = 100 个图实例
- 100 个独立连接池(可能 10,000+ 连接!)
- 100 个独立缓存(无法共享)

全局单图的资源占用:
- 所有用户 = 1 个图实例
- 1 个共享连接池(100 个连接)
- 1 个共享缓存(命中率更高)

原因 3:无法享受框架优化

LangGraph 的很多优化是基于单图实例设计的:

  • 执行计划缓存
  • 跨用户的状态共享
  • 连接池复用

每用户一图,这些优化全部失效。


✅ 正确的做法

只需改动 一行代码

# 之前(错误)
async def handle_user(user_id: str, message: str):
    graph = builder.compile()  # ❌ 删除这行
    return await graph.ainvoke(...)

# 之后(正确)
# 在应用启动时编译一次(全局单例)
graph = builder.compile(checkpointer=checkpointer)

async def handle_user(user_id: str, message: str):
    # 所有用户共享同一个图
    return await graph.ainvoke(
        {"messages": [message]},
        config={"configurable": {"thread_id": user_id}}  # 用 thread_id 隔离
    )

就这么简单!性能提升 44%,内存节省 77%。


🎨 理解 LangGraph 的设计哲学

LangGraph 的设计理念是:

Graph = 蓝图(类似类定义)
thread_id = 实例(类似对象实例)

正确的类比

# ❌ 错误:每个用户重新定义类
def handle_user(user_id):
    User = type('User', (), {})  # 每次动态创建类
    user = User()

# ✅ 正确:定义一次,创建多个实例
class User: pass  # 定义一次
user1 = User()    # 创建实例
user2 = User()    # 创建实例

状态隔离不需要多个图

全局图 (共享)
    ├── thread_id: "user1" → 独立的 checkpoint
    ├── thread_id: "user2" → 独立的 checkpoint
    └── thread_id: "user3" → 独立的 checkpoint

就像数据库表设计:

  • ❌ 错误:每个用户创建一个新表
  • ✅ 正确:一个表,用 user_id 隔离数据

📚 官方也是这样做的

我翻遍了 LangGraph 的官方测试代码,所有示例都是编译一次,多次调用

# 官方测试代码示例
agent = create_agent(model, tools, checkpointer)

# 多次调用,用 thread_id 隔离
agent.invoke(state, config={"configurable": {"thread_id": "user1"}})
agent.invoke(state, config={"configurable": {"thread_id": "user2"}})
agent.invoke(state, config={"configurable": {"thread_id": "user3"}})

没有任何官方示例为每个用户重新编译图。


🎁 额外福利:代码更简洁

除了性能提升,你的代码也会变得更简单:

方面每用户一图全局单图
代码行数多(需要管理生命周期)少(无需管理)
资源管理复杂(需要清理图实例)简单(框架自动管理)
调试难度高(100 个图实例)低(1 个图实例)
监控 & 日志困难容易

🚀 立即行动

如果你的项目正在用每用户一图

迁移只需要 2 步:

  1. 把图的编译移到应用启动时
  2. 删除所有重复编译的代码
# app.py
from langgraph.graph import StateGraph
from langgraph.checkpoint.postgres import PostgresSaver

# 步骤 1:启动时编译一次
checkpointer = PostgresSaver.from_conn_string(DATABASE_URL)
graph = builder.compile(checkpointer=checkpointer)

# 步骤 2:所有用户共享这个图
@app.post("/chat")
async def chat(user_id: str, message: str):
    result = await graph.ainvoke(
        {"messages": [message]},
        config={"configurable": {"thread_id": user_id}}
    )
    return result

就这么简单!


📝 总结

维度每用户一图全局单图
性能慢 30-40%快 44% 🚀
内存浪费 77%省 77% 💾
代码复杂简洁
维护困难容易 🔧
官方推荐

结论:全局单图 + thread_id 隔离是 LangGraph 应用的唯一正确做法。


🤝 一起讨论

你的 LangGraph 应用是怎么架构的?有没有踩过类似的坑?

欢迎在评论区分享你的经验!👇


测试代码libs/langchain_v1/tests/graph_benchmark.py 完整报告:包含详细的测试环境、代码示例和数据分析


觉得有帮助的话,请点赞收藏,让更多人看到!

#LangGraph #Python #性能优化 #AI #Agent #LLM