☰
LangGraph部署全解:FastAPI、LangGraph Server与Platform选型指南
2026/10/8 20:43:17 网站建设 项目流程

跑通一个 LangGraph 的 Agent,在我的经验里从来都不是难事:定义好节点、给边加上条件路由、调用一下graph.compile(),本地拿一段文本或者一个thread_id直接往下灌,输出就出来了。真正开始头疼的,是把它部署成“别人也能用的服务”——你会发现,脚本世界里那套“跑完就结束”的思维基本失效。LangGraph 图本质上是一个带持久化、可中断、可能等待人工介入的状态机,把它塞进一个普通的 Web 进程,远不像包一个 Flask 接口那么简单。

这篇文章想完整聊聊 LangGraph 从脚本到服务的三条部署路径:FastAPI 手写封装、官方 LangGraph Server、以及 LangGraph Platform。我不会只列步骤,而是会讲清楚每条路径解决什么问题、留下什么坑、适合什么样的项目和团队。如果你正在用 FastAPI + LangChain + LangGraph 搭 AI Agent,正卡在“本地能跑但不知道怎么上线”这一步,这篇应该能给你省下不少折腾时间。

1. 先搞清楚部署难点:LangGraph 应用服务化到底难在哪

很多人第一次把 LangGraph 往 Web 服务里搬的时候,第一反应是:写个 FastAPI 端点,收到请求就调一下compiled_graph.invoke(),把结果返回不就行了吗?表面看确实是这样,但实际踩过几轮之后,你会发现麻烦都藏在“图的生命周期”和“HTTP 请求的生命周期”不匹配这件事上。

1.1 图不是“跑一次就结束”的程序

普通 Python 脚本是一次性的:读输入、跑逻辑、输出、退出。但 LangGraph 图是一个有状态的状态机,它天然支持 checkpoint(检查点)、Human-in-the-loop(人工介入)、时间旅行(time travel)这些能力。也就是说,同一个 thread 的对话可以被中断、持久化、恢复,甚至回到之前的某个状态重新执行。

这就带来一个最基本的部署问题:如果进程重启了,这轮对话还能不能继续?脚本模式完全不需要回答这个问题,因为每次跑都是从头开始。而一旦做成服务,用户可能上午问了一半,下午回来继续问。如果状态只存在进程内存里,重启就全丢了。

所以“部署”的第一步,其实是先想清楚状态要放在哪里。这个选择会直接决定后续所有架构。

1.2 图的执行周期比 HTTP 请求长得多

普通 API 的请求响应是毫秒级到秒级,但一个 Agent 图的执行链路可能是这样的:用户输入 → LLM 第一次生成(5-20 秒) → 决定调用工具 → 工具执行(可能调外部服务,又是几秒到几十秒) → LLM 汇总(再花十几秒) → 输出。整个链路跑完,一两分钟很正常。

更麻烦的是,LangGraph 还支持interrupt()。图执行到某个节点,可能会停下来等人单击“确认”或者填一个表单,这个“人等”的过程可能是几分钟,也可能是几天。

当你把这种长周期执行放进 HTTP 服务的模型里,问题就接连出现了:客户端等不等得起?服务端的连接和线程被占住了怎么办?中途进程崩溃了,已经执行到一半的图怎么恢复?这些都不是靠写一个端点能解决的。

1.3 “部署路径”的实质是信任边界不同

我后来想明白了一件事:三条部署路径的区别,表面看是工具不同,本质上是“你愿意把哪些东西交给框架托管”。FastAPI 自封装,是你自己管状态、并发、流式、可观测性;用 LangGraph Server,是把 HTTP 层和状态层交给官方的那一套抽象;上 LangGraph Platform,则是把整个执行生命周期都托管出去。这三条路线的选型,本质上是在“控制力”和“省心事”之间作权衡,没有绝对的好坏,只有合不合适的阶段。

2. 路径一:FastAPI 手写封装,把 CompiledGraph 变成 HTTP 接口

先聊最朴素、也最容易被低估的一条路:自己用 FastAPI 封装一个服务。很多项目其实只需要一个内部接口、一个内部工具,并不值得为它引入整套平台。而且手写一遍封装,能强迫你真正理解 CompiledGraph 的生命周期,后面迁移到官方方案时心里也有底。

2.1 为什么是 FastAPI

FastAPI 几乎是 Python 服务化最顺手的方案:原生 async 支持、Pydantic 做请求校验、自动生成 OpenAPI schema。对于 LangGraph 这种本身就是 Python 生态的框架来说,它是最低摩擦的 HTTP 壳。实际上很多生产项目就是用 FastAPI + LangChain + LangGraph 搭的,我在标题相关的那套 Agent 方案里,第一版也是这么做的。这个组合的好处是每层都可以单独替换:FastAPI 管接口,LangChain 管模型和工具调用抽象,LangGraph 管状态和业务流程编排。

2.2 最小可用实现:一个 Invoke 端点

先看一个最基础的实现,不含流式,先把“脚本变接口”这件事落地:

# app.py from fastapi import FastAPI from fastapi.responses import StreamingResponse from pydantic import BaseModel from agent_builder import build_agent app = FastAPI() compiled_graph = build_agent() # 进程启动时只编译一次 class InvokeRequest(BaseModel): thread_id: str messages: list[dict] @app.post("/invoke") def invoke(req: InvokeRequest): config = {"configurable": {"thread_id": req.thread_id}} result = compiled_graph.invoke({"messages": req.messages}, config=config) return {"messages": result["messages"]}

这段代码本身很简单,但里面有一个非常关键的细节:compiled_graph必须在模块级别只编译一次,不能放在函数里每次请求都编译。compile()这个过程会做状态 schema 校验、节点检查、边结构预处理,开销比你想象的大,而且毫无必要。我在早期版本里干过这种傻事,结果并发一上来,服务端的编译 CPU 占用直接飙满。

2.3 thread_id 是一等公民,不是可选项

注意请求体里我放了一个thread_id,这不是可有可无的东西。LangGraph 的 checkpointer 把所有对话状态都挂在 thread 上:同一个 thread_id 的连续请求共享历史上下文,不同的 thread_id 完全隔离。它就像数据库里的主键——没有它,你的图就退化成无状态函数,每次请求都是空对话,所谓“多轮记忆”自然就失效了。

所以在自封装这条路径上,第一步就是想清楚thread_id从哪来。是由前端生成还是后端生成?用户刷新页面是否会导致丢失?这些问题在写脚本时完全不存在,但一旦做服务,就是第一道绕不过去的坎。我后面会单独讲 thread_id 的完整设计,这里先记住一个结论:请求体里必须显式携带它,不要每次服务端自动生成新的。

2.4 流式响应:用 SSE 而不是硬等

invoke的问题在于,客户端要等到图全部跑完才能收到一个完整响应。如果整个 Agent 链路要跑几十秒,用户体验会很差,而且中间层如果还有网关、负载均衡,长连接还容易被切断。所以只要不是内部极低并发的场景,我都建议直接用流式接口。

LangGraph 本身提供graph.stream(),配合 FastAPI 的StreamingResponse就能很轻地做成 SSE(Server-Sent Events)接口:

import json from fastapi.responses import StreamingResponse @app.post("/stream") def stream(req: InvokeRequest): config = {"configurable": {"thread_id": req.thread_id}} def generate(): for chunk in compiled_graph.stream( {"messages": req.messages}, config=config, stream_mode="updates" ): yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" return StreamingResponse(generate(), media_type="text/event-stream")

为什么选 SSE 而不是 WebSocket?因为 SSE 基于 HTTP,基础设施兼容性最好,Nginx、各种网关都能直接透传,不需要额外维护连接状态。WebSocket 当然能做双向通信,但你的场景是“用户发一次,Agent 流式回一次”,这个模型 SSE 已经覆盖了,没必要引入更高的复杂度。前端用EventSource或者 fetch reader 都能接。

2.5 这条路上最容易踩的三个坑

超时问题。Agent 执行链路长,外部模型 API 偶尔会卡住,如果不在客户端和网关层设置合理的超时,连接会被一直占住。建议在服务入口设置一个略高于 Agent 最坏执行时间的超时阈值,同时让前端能够中断请求并释放资源。

同步阻塞问题。如果 FastAPI 的端点用async def,但里面调用的是同步的graph.invoke(),那整个事件循环会被阻塞。LangGraph 其实提供了ainvoke/astream,但要注意异步调用时 checkpointer 也需要安装异步驱动。早期MemorySaver的异步实现有过一些坑,如果并发要求高,建议直接上PostgresSaver或RedisSaver并启用异步版本,别在内存版上死磕。

幂等性问题。请求重试是不可避免的,但一个 Agent 执行过程中可能已经调用了外部工具,比如发了邮件、扣了钱、创建了订单。重试时如果原样再发一遍,这些有副作用的工具就可能被执行两次。解决思路是引入一个任务去重机制:在请求体里带run_id,或者在消费端记录每个 thread 的最后一次提交指纹,重复提交直接返回上次结果。

2.6 适合什么人走这条路

真需要做鉴权、限流、日志、审计,希望完全掌控 HTTP 行为的团队;内部调用、并发不大、不需要长时间挂起的任务;以及部署环境受限,不方便引入官方 Docker 镜像的场景。反过来说,如果团队有好几个人一起开发 Agent,需要可视化调试工具,或者你的业务流程需要定时触发、异步后台执行、人工审批挂起,那靠手写 FastAPI 把这些全做了会非常痛苦,就是下一条路径该出场的时候了。

3. 路径二:LangGraph Server 原生部署,把调试与持久化交给官方

LangGraph 官方很早就意识到“部署”这个环节的价值,推出了一整套服务化方案:LangGraph Server。它不是让你自己封装接口,而是直接把图编译成有一个标准 REST API 的服务,配合 LangGraph Studio 这类调试工具,开发体验会比自封装舒服不少。

3.1 langgraph.json:图的部署声明

LangGraph Server 的核心是一个配置文件,langgraph.json。它的作用是把“哪个文件里的哪个编译对象,以什么名字暴露出去”声明清楚:

{ "dependencies": ["."], "graphs": { "agent": "./agent.py:app" }, "env": ".env", "python_version": "3.11" }

在agent.py里,你需要暴露一个编译后的图对象:

from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver def create_graph(): builder = StateGraph(...) builder.add_node(...) builder.add_edge(...) return builder.compile(checkpointer=MemorySaver()) app = create_graph()

只要这个文件存在,运行langgraph dev就能在本地拉起一个开发服务,并且自动带你进入 LangGraph Studio 的可视化调试界面。我第一次用这个工具的时候,最大的感受是:终于不用靠 print 和日志猜图走到哪一步了。你能直接看到每个节点当前的状态、消息列表、工具调用结果,甚至可以把图回滚到之前某个节点,修改参数重新执行。这种调试能力对复杂 Agent 流程来说,几乎是刚需。

3.2 Server 内置的 API 模型:Assistant、Thread、Run

LangGraph Server 内置的 REST API 把服务层抽象成了三个概念,理解它们,后面用 Platform 时也能立刻上手:

  • Assistant:图定义和运行时配置的绑定。可以理解成服务器上的“一份图模板”。
  • Thread:一次会话/一个任务的持久化载体,对应 LangGraph 里的 thread_id。
  • Run:真正执行图的一次运行。一个 thread 上可以有多次 run,每次 run 会生成一个 run_id。

实际的使用流程是:先创建 thread,然后在 thread 上提交 run。比如用 curl 创建线程:

curl -X POST http://localhost:8123/threads \ -H "Content-Type: application/json" \ -d '{"assistant_id": "agent"}'

拿到 thread_id 之后,再对它提交消息:

curl -X POST http://localhost:8123/threads/your-thread-id/runs \ -H "Content-Type: application/json" \ -d '{"messages": [{"role": "user", "content": "你好"}]}'

这个设计最舒服的地方在于:它把“对话级状态”直接从业务层剥离了。你不需要自己去实现“根据 thread_id 读取历史消息再塞回图里”,服务端全包了。而且这些 API 会自动生成 OpenAPI schema,前端联调、SDK 生成都省了很多手工劳动。

3.3 生产化:用 Docker 跑起来,并配置持久化存储

开发时可以用langgraph dev,但生产环境自然要容器化。LangGraph 官方提供了构建好的服务镜像,配合 CLI 可以做本地构建。基本流程是:

langgraph build -t my-agent:latest

这个命令会根据 langgraph.json 生成一个包含你的代码和官方服务层的镜像,然后你可以用langgraph up在本地跑起来,或者自己推送到自建的容器仓库里部署。

这里有一个坑:默认的开发配置用的是内存存储,一旦服务重启,所有对话状态全部消失。生产环境必须配置持久化的 checkpointer。最常见的方案是 Postgres 或 Redis,在 langgraph.json 里声明:

{ "dependencies": ["."], "graphs": { "agent": "./agent.py:app" }, "checkpointer": { "type": "postgres", "config": { "postgres_uri": "postgresql://user:pass@host/db" } } }

如果走 Docker 部署,通常是配环境变量而不是把数据库地址直接写在代码里。另外 LangGraph Server 的镜像本身比较大,构建时间长,CI/CD 流水线的超时时间要放宽,这个不提前做好心理准备,很容易在第一次上线时被打个措手不及。

3.4 走这条路前先想清楚的事

LangGraph Server 帮我们解决了“自己封装 HTTP 接口”的大部分重复劳动,但它的控制面同样是官方的抽象:如果你需要一些完全自定义的 HTTP 接口(比如一个非流式的批处理入口、一个需要特殊鉴权逻辑的回调),会发现自由度比 FastAPI 低不少。它的定位是“标准 Agent 服务”,不是“任何功能的 Web 框架”。

我个人的判断是:团队多人协作开发 Agent、需要共享调试环境、需要状态持久化但不想从头搭基础设施时,LangGraph Server 是非常舒服的中间态。它比自封装多了一层标准化的 API 模型,又比 Platform 少了一些云端依赖,适合在自建服务器上部署。

4. 路径三:LangGraph Platform:把执行生命周期整体托管

如果说 LangGraph Server 解决了“把图跑成一个服务”,那 LangGraph Platform 解决的是“把图跑成一个可靠的生产任务系统”。这个阶段最典型的诉求已经不是接口长什么样了,而是:进程崩了能不能自动恢复?任务能不能定时触发?人审流程挂起后怎么恢复?所有执行过程能不能被完整观测?

4.1 Platform 比 Server 多出来的关键能力

这里列一下我在实际使用中感受最明显的差异点:

持久化与 Durable Execution:Platform 把每个 run 的执行状态持久化,进程崩溃、机器重启后,任务会自动恢复继续执行,而不是从头再来。对于长耗时 Agent 流程,这个能力非常有价值。

后台执行与定时任务:LangGraph Server 本身不太强调跑后台任务,但到了生产场景,“凌晨定时拉取数据后生成报告并推送通知”这类需求非常常见。Platform 支持 cron 调度,在提交 run 的时候带上调度表达式,平台就会按时触发,不需要你额外搭一套 Celery 之类的任务系统。

人机协同的工程化支持:LangGraph 的interrupt()机制在 Platform 上被完整落地成了一套 API 行为。图执行到中断节点时会主动挂起并返回等待状态,之后你通过特定接口提交“确认继续”或“修改输入”,图会从暂停的位置接着跑。这套流程如果自己用 FastAPI 封装,要处理挂起状态存储、恢复时机、并发保护,非常痛苦。

可观测性:Platform 自带 tracing 与执行历史,配合 LangSmith 能做到从用户输入到每一步工具调用的全链路追踪。相比自建日志,这个体验差距是代际的。

4.2 托管方式:云版还是自托管

LangGraph Platform 有两种形态:托管的 LangGraph Cloud,以及可以部署到自己环境的自托管版本。

云版适合想快速上线、不想碰运维的团队:直接把代码仓库连到平台,配置好环境变量,平台自动构建服务、管理数据库和队列。自托管则适合数据必须留在内部网络、或合规要求更高的场景,通常需要你维护 Kubernetes 环境和一些基础设施组件。

无论哪种形态,langgraph.json 仍然是核心入口。生产环境下配置会像这样:

{ "dependencies": ["./agent"], "graphs": { "agent": "./agent/graph.py:graph", "planner": "./planner/graph.py:graph" }, "env": ".env", "checkpointer": { "type": "redis", "config": { "redis_uri": "redis://..." } } }

可以同时暴露多个图,每个图作为一个 assistant,服务端会为每个 assistant 独立管理线程和运行。

4.3 代价:自由度、成本和调试边界

Platform 不是银弹。第一个代价是“黑盒”:托管的执行引擎你改不了,遇到平台本身的 bug 或者不符合预期行为时,只能等官方修复,或者退回自研。第二个代价是成本,特别是云版按调用量、存储量计费的模式,流量上来之后费用会相当可观。第三个代价是迁移成本:一旦你的业务深度依赖 Platform 的 cron、后台 run、interrupt 恢复协议,未来想迁回自建方案会非常费劲。

所以我的建议很直接:如果你还在做 Demo、验证业务可行性,先别上 Platform;如果业务已经进入生产阶段,并且你发现自己在 FastAPI 方案里开始手写“任务恢复”“定时触发”“挂起恢复”这些通用能力时,就该认真考虑把它换掉了。

5. 三条路径怎么选:一次讲清楚决策清单

前面三条路径都过了一遍,这里把对比整理成一张表,方便你做决策的时候直接对照参考。

对比项路径一:FastAPI 自封装路径二:LangGraph Server路径三:LangGraph Platform
启动成本低,写代码即可中,需要配置与镜像高,需要平台账号或自建基础设施
状态持久化自己接 checkpointer内置接口,需配 Postgres/Redis托管,开箱即用
可视化调试无,需要打日志LangGraph Studio 支持平台支持 + 云端 tracing
HTTP API完全自定义官方标准 REST API官方 REST API + SDK
流式支持手写 SSE原生支持原生支持
定时任务与后台执行自建任务队列不内置内置 cron / background runs
人机协同挂起恢复自己实现,较痛苦接口支持但需定制成熟方案
运维复杂度低中高云版最低,自托管较高
定制自由度最高中低

决策清单,按我自己的经验排序的话:

单个服务、内部工具、并发不大,明确只需要一个 HTTP 包装,选路径一。这是成本最低的方案,代码都在自己手里,将来迁移也最没有包袱。

团队共同开发,Agent 流程复杂到需要可视化调试,或者需要标准化给前端对接,选路径二。LangGraph Server 的价值不在“少写几个端点”,而在开发协作和 API 模型的标准化。这里的调试收益,只要用过一次 LangGraph Studio 就很难退回纯日志模式。

生产环境、长耗时任务、需要定时触发、需要人工审批、要求进程崩溃后任务能恢复,选路径三。这些能力自己从零搭,每个都是不小的工程,Platform 的意义是把“执行生命周期”整体托管。

还有一个务实的打法:先用路径一快速验证业务逻辑,把 Agent 的编排和工具调用打磨成熟,再在项目标准化阶段按需迁到路径二或三。LangGraph 的核心业务代码和部署方案本身就是解耦的,迁移时改动量通常集中在配置和接口调用层,业务图结构基本不动。我已经在项目里这么干过两次,比一开始就纠结“上不上平台”要省心得多。

6. 无论选哪条路,部署前都要想清楚的三个问题

最后这部分是我最想强调的。因为不管走哪条部署路径,下面这三个问题都是躲不掉的,而且大概率会在你上线后第一周内出来找你。

6.1 thread_id 怎么设计才算合格

千万别用“每轮对话自动生成一个 uuid”这种偷懒方案。用户刷新一下页面 thread_id 就没了,那这个 Agent 就跟失忆症患者一样,永远不记得上一轮聊了什么。thread_id 本质上扮演的是业务路由、状态隔离和审计的主键,我在实际项目里推荐复合结构,比如:

{customer_id}:{session_id}

或者

{tenant_id}:{user_id}:{conversation_id}

这样做有三个好处:多租户天然隔离;后续检查数据、排障时能快速定位到具体用户;如果你用的是支持前缀分片的存储,还能按这个结构做水平扩展。还有一个坑:同一个 thread_id 上不要并发跑多个 run。LangGraph 的 checkpointer 在同一个 thread 上并发写入时会有竞争,状态可能互相覆盖。如果用户的同一个会话里需要同时执行多个并行任务,在业务层创建多个 thread,而不是塞进同一个 thread_id。

6.2 并发与异步:别让 FastAPI 的 async 反噬你

很多人在 FastAPI 端点上用async def,图里却调用同步的graph.invoke()。一个耗时的图跑起来,事件循环被死死占住,其他所有请求全部排队,整个服务的并发能力直接归零。正确的姿势是:用ainvoke/astream,并且给 checkpointer 配异步驱动。

这里还想提醒一个容易被忽略的点:Agent 里的工具调用(Tool)也可能是同步的。比如工具里直接用了requests.get(),那么就算图本身是异步的,这个工具调用一样会阻塞。LangGraph 的工具节点设计时最好统一走异步签名,或者用asyncio.to_thread把阻塞调用丢到线程池里。这个细节在本地跑脚本时完全没感觉,一旦上线并发上来,就是性能瓶颈的重灾区。

另外,服务端不仅要考虑 Agent 本身的并发,还要考虑外部依赖的连接池:LLM API 的客户端连接、数据库连接、Redis 连接,都需要单独配置合理的池大小。模型响应慢不是你不配连接池的理由,恰恰因为模型响应慢,池子里的连接被占住的时间才更长,才更容易打满。

6.3 可观测性:没有 trace 的 Agent 服务等于盲盒

脚本时代,你可以在图里到处print,看看每一步走到哪了。服务化之后,print 基本失效,你只知道用户问了一句什么,最后回了一句什么,中间哪个工具被调用了、哪个节点抛异常了,全靠猜。

所以部署前一定把 tracing 接好。LangGraph 对 LangSmith 的集成几乎是开箱即用的:

import os os.environ["LANGCHAIN_TRACING_V2"] = "true" os.environ["LANGCHAIN_API_KEY"] = "your-api-key" os.environ["LANGCHAIN_PROJECT"] = "your-agent-name"

只要这几个环境变量在,LangGraph 会自动把每次 run 的节点执行、模型调用、工具返回全部上报。整个链路里每次 LLM 调用耗多久、工具返回了什么报文,都是可视的。不用 LangSmith 的话,用 Langfuse、Langtrace 这类兼容 OpenTelemetry 的工具也行,但关键是:一定要有 trace。

除了外部 tracing,服务入口的业务日志也别省。我的习惯是给每次请求打这么几个字段:thread_id、用户标识、跑的图名称、工具调用次数、总耗时、结束状态。上线第一天就能拿这些指标回答“Agent 到底跑得快不快、在哪些环节卡住了”。

最后分享一点个人实际体会。三条路径我陆续都用过,最大的感受是别把部署路径当成技术排名问题,它更像一个边界问题:你到底希望自己的 Agent 系统里有多少东西是“自己可控的”,又有多少是“交给框架托管”。如果只跑通 MVP,FastAPI 完全够;如果要天天和各种业务系统握手,LangGraph Server 的 API 模型会替你省掉很多事;如果真到了生产阶段,线程恢复、定时任务、人工确认这些才是大头,Platform 的意义不在“部署”本身,而在把整套执行生命周期托管掉。

你自己搭也好、用官方也好,先想清楚三件事:状态放哪、并发怎么管、出了问题能不能看见。想清楚这三件事,任何一条路径都不会把你带进死胡同。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询