☰
LangGraph 部署实战:从本地脚本到 FastAPI 与 K8s 的三条路径
2026/10/8 16:42:40 网站建设 项目流程

1. 从脚本到服务,为什么部署这一步总卡人

LangGraph 这个框架,写过 demo 的人都知道,本地跑起来是真舒服。一个StateGraph,几个节点函数,边一连,graph.invoke()一调,控制台刷刷打印,状态流转清清楚楚。但只要你想把它从“我电脑上能跑”变成“别人也能用”,问题就来了:脚本怎么变成服务?服务怎么打包?打包完怎么放到服务器上?服务器上怎么保证它一直活着?

我见过太多人卡在这一步。不是 LangGraph 本身难,而是从脚本到服务之间,隔着一整套工程化的东西。你写脚本的时候,输入是硬编码的,输出是print的,状态是内存里的,跑完就没了。但做成服务,输入得从 HTTP 请求来,输出得序列化成 JSON 返回,状态得考虑并发和持久化,进程得考虑崩溃重启,资源得考虑隔离和限制。

这篇文章就是冲着这个问题来的。我会把 LangGraph 从脚本到服务的三条部署路径拆开讲清楚:本地进程直跑、FastAPI 包装成 HTTP 服务、Docker 容器化再上 K8s 编排。三条路径对应三种场景,复杂度递增,但每一步的动机和取舍我都会说明白。如果你现在手里有一个 LangGraph 脚本,想把它变成别人能调用的服务,或者你正在纠结要不要上 Docker、要不要上 K8s,那这篇内容应该能帮你省下不少试错时间。

先给一个全局的判断:不是所有 LangGraph 项目都需要上 K8s。我见过有人为了一个日调用量不到一百次的小工具,硬是搭了一套 K8s 集群,结果运维成本比开发成本还高。部署路径的选择,核心看三个维度:调用量、并发要求、团队运维能力。下面我会按这三条路径逐一展开,每条路径都给出具体的操作步骤、参数配置和踩坑记录。

2. 三条部署路径的整体设计与选型逻辑

2.1 路径一:本地进程直跑,适合验证和单机工具

这是最轻的一条路。你的 LangGraph 脚本写完之后,不包装任何 Web 框架,直接用一个入口脚本启动,通过命令行参数或者读取本地文件来接收输入,结果写到文件或者打印到终端。适合的场景很明确:个人本地工具、一次性批处理任务、算法验证阶段。

这条路径的核心优势是零额外依赖。你不需要装 FastAPI,不需要配 Uvicorn,不需要写 Dockerfile。Python 环境里装好 LangGraph 和相关模型依赖,直接python run.py就完事。对于还在调 prompt、调图结构、调状态定义的阶段,这条路径效率最高,因为改完代码直接重跑,没有构建、没有镜像、没有部署。

但它的局限也很明显。第一,没有并发能力,同一时间只能处理一个请求,第二个请求得排队。第二,没有服务发现,别人想用你的能力,只能你把脚本拷给他,或者他 SSH 到你的机器上跑。第三,没有健康检查,进程挂了没人知道,得手动重启。所以这条路径只适合验证阶段和纯本地工具,一旦要对外提供服务,就得往路径二走。

2.2 路径二:FastAPI 包装成 HTTP 服务,适合中小规模对外服务

这是目前最主流的一条路。LangGraph 负责编排逻辑,FastAPI 负责 HTTP 接口层,Uvicorn 负责跑 ASGI 服务。三者分工明确:LangGraph 管“怎么思考”,FastAPI 管“怎么接收和返回”,Uvicorn 管“怎么监听端口和处理连接”。

为什么选 FastAPI 而不是 Flask?核心原因是异步支持。LangGraph 的很多操作,尤其是调用大模型 API 的时候,是 IO 密集型的,用异步能显著提升并发吞吐。Flask 是同步框架,虽然也能跑,但在高并发场景下线程池会被迅速占满。FastAPI 原生支持async def,配合httpx或者各家模型 SDK 的异步客户端,单进程就能扛住不错的并发量。另外 FastAPI 自带 OpenAPI 文档,接口定义完自动生成/docs页面,调试和对接都方便。

这条路径的典型架构是:客户端发 HTTP 请求到 FastAPI 的某个路由,路由函数里调用 LangGraph 的graph.ainvoke()或者graph.astream(),拿到结果后序列化成 JSON 返回。状态管理上,如果是无状态请求,每次调用传入完整输入即可;如果需要多轮对话,就得引入会话 ID 和外部存储(比如 Redis)来保存状态。

2.3 路径三:Docker 容器化加 K8s 编排,适合规模化生产环境

当你的服务需要多实例、需要自动扩缩容、需要滚动更新、需要资源隔离的时候,就得上容器和编排。Docker 解决的是“环境一致性”问题:你的 LangGraph 服务依赖哪些 Python 包、哪个版本的 CUDA、哪些系统库,全部打包进镜像,换台机器跑起来行为一致。K8s 解决的是“编排”问题:多少个副本、怎么负载均衡、挂了怎么重启、资源不够怎么扩容、怎么灰度发布。

这条路径的复杂度是前两条的好几倍。你需要写 Dockerfile、构建镜像、推送到镜像仓库、写 K8s 的 Deployment 和 Service 配置、配 Ingress、配 ConfigMap 和 Secret、配健康检查探针、配资源限制。如果团队里没有专门的运维,这条路的维护成本会很高。所以我的建议是:日调用量稳定超过一万次,或者有明确的弹性伸缩需求,再考虑上 K8s。否则路径二加个进程守护工具(比如 systemd)就够用了。

三条路径的对比我整理成了一张表,方便你快速判断自己该走哪条:

维度路径一:本地直跑路径二:FastAPI 服务路径三:Docker + K8s
适用场景验证、单机工具中小规模对外服务规模化生产环境
并发能力无中等(异步)高(多副本)
环境一致性依赖本机依赖本机镜像保证
运维成本极低低高
扩缩容不支持手动自动
健康检查无可加原生支持
推荐调用量个人使用日千到万级日万级以上

3. 路径二实操:用 FastAPI 把 LangGraph 脚本包成服务

3.1 项目目录结构怎么设计才不乱

很多人写 FastAPI 项目,所有代码堆在一个main.py里,几百行下去自己都找不到东西。LangGraph 本身就有图定义、节点函数、状态类型、工具函数这些模块,再加上 FastAPI 的路由、依赖、配置,不分开根本没法维护。我推荐的结构是这样的:

langgraph-service/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 入口,注册路由 │ ├── config.py # 配置管理,读环境变量 │ ├── graph/ │ │ ├── __init__.py │ │ ├── state.py # 状态类型定义 │ │ ├── nodes.py # 节点函数 │ │ └── builder.py # 图构建逻辑 │ ├── api/ │ │ ├── __init__.py │ │ ├── routes.py # 路由定义 │ │ └── schemas.py # 请求/响应模型 │ └── services/ │ └── graph_service.py # 业务逻辑封装 ├── tests/ ├── requirements.txt ├── Dockerfile └── .env

这个结构的关键在于分层。graph/目录只关心 LangGraph 的图逻辑,不关心 HTTP;api/目录只关心请求解析和响应构造,不关心图怎么跑;services/目录做中间层,把图的调用封装成业务方法。这样改图逻辑不影响接口,改接口不影响图逻辑。

3.2 状态定义与请求模型的分离

这里有个容易踩的坑:很多人直接把 LangGraph 的State类型拿来做 FastAPI 的请求体模型。这俩东西看起来都是数据结构,但职责完全不同。LangGraph 的State是图内部流转的状态,可能包含中间变量、临时标记、内部 ID;而 API 的请求模型是外部契约,只应该包含客户端需要传的字段。

我的做法是分开定义。graph/state.py里定义图状态:

from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class GraphState(TypedDict): messages: Annotated[list, add_messages] session_id: str user_input: str intermediate_result: str

api/schemas.py里定义请求和响应:

from pydantic import BaseModel, Field class ChatRequest(BaseModel): session_id: str = Field(..., description="会话标识") message: str = Field(..., min_length=1, description="用户输入") class ChatResponse(BaseModel): session_id: str reply: str status: str = "ok"

然后在services/graph_service.py里做转换:把ChatRequest转成GraphState,调图,再把结果转成ChatResponse。这层转换看起来多此一举,但等你需要改接口字段而不想动图逻辑的时候,就知道它的价值了。

3.3 路由设计与异步调用

路由这块,核心是把 LangGraph 的调用正确地异步化。LangGraph 编译后的图对象支持ainvoke和astream两种异步调用方式。ainvoke是一次性返回最终结果,astream是流式返回中间状态。如果你的场景需要流式输出(比如打字机效果),就用astream配合 FastAPI 的StreamingResponse。

先看非流式的写法:

from fastapi import APIRouter, HTTPException from app.api.schemas import ChatRequest, ChatResponse from app.services.graph_service import run_graph router = APIRouter() @router.post("/chat", response_model=ChatResponse) async def chat(req: ChatRequest): try: result = await run_graph(req.session_id, req.message) return ChatResponse(session_id=req.session_id, reply=result) except Exception as e: raise HTTPException(status_code=500, detail=str(e))

流式的写法稍微复杂一点:

from fastapi.responses import StreamingResponse @router.post("/chat/stream") async def chat_stream(req: ChatRequest): async def event_generator(): async for chunk in stream_graph(req.session_id, req.message): yield f"data: {chunk}\n\n" return StreamingResponse(event_generator(), media_type="text/event-stream")

注意:流式接口用 SSE 的时候,记得在 Nginx 或者网关层关掉缓冲,否则客户端会等到全部生成完才收到数据,流式就白做了。

3.4 启动配置与 Uvicorn 参数调优

启动命令看起来简单,但参数配不对,性能和稳定性都会受影响。最基础的启动方式:

uvicorn app.main:app --host 0.0.0.0 --port 8000

生产环境我一般会加上这些参数:

uvicorn app.main:app \ --host 0.0.0.0 \ --port 8000 \ --workers 4 \ --loop uvloop \ --http httptools \ --timeout-keep-alive 30 \ --log-level info

--workers 4表示起 4 个工作进程,一般设为 CPU 核数。但这里有个坑:如果你的 LangGraph 图里持有大量内存状态,多 worker 会导致内存翻倍。这种情况下要么减少 worker 数量,要么把状态外置到 Redis。--loop uvloop用 uvloop 替换默认事件循环,IO 性能有明显提升。--http httptools用 httptools 做 HTTP 解析,比默认的快。

还有一个常见问题是Uvicorn 日志丢失。如果你发现访问日志打不出来,检查两点:一是--log-level是不是设成了warning以上;二是如果你在代码里用了logging模块,确认 logger 的 handler 和 Uvicorn 的配置没有冲突。我一般会在main.py里显式配置 logging:

import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" )

3.5 进程守护与开机自启

FastAPI 服务跑起来之后,你得保证它挂了能自动重启、机器重启能自动拉起。最省事的方案是 systemd。写一个 service 文件:

[Unit] Description=LangGraph FastAPI Service After=network.target [Service] User=appuser WorkingDirectory=/opt/langgraph-service ExecStart=/opt/langgraph-service/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 Restart=always RestartSec=5 Environment="PYTHONUNBUFFERED=1" [Install] WantedBy=multi-user.target

放到/etc/systemd/system/langgraph.service,然后systemctl daemon-reload && systemctl enable --now langgraph。Restart=always保证进程崩溃后 5 秒自动重启,enable保证开机自启。这套组合在单机部署场景下非常稳,我用了好几年没出过问题。

4. 路径三实操:Docker 容器化与 K8s 编排要点

4.1 Dockerfile 怎么写才能又小又快

LangGraph 服务的镜像,最容易犯的错是直接FROM python:3.11然后pip install -r requirements.txt,结果镜像两个 G。问题出在基础镜像太大、构建缓存没利用、依赖装了一堆用不上的。

我的做法是分阶段构建加精简基础镜像。先看一个典型的 Dockerfile:

FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY app/ ./app/ ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

几个关键点:python:3.11-slim比完整版小很多;分阶段构建把编译工具留在 builder 阶段,最终镜像不带;--no-cache-dir避免 pip 缓存占空间;PYTHONDONTWRITEBYTECODE=1避免生成.pyc文件。

如果你的服务需要 GPU 跑本地模型,基础镜像要换成nvidia/cuda系列的,并且需要装对应的 PyTorch 版本。这种情况下镜像会大很多,构建时间也长,建议把模型文件通过挂载卷的方式外置,不要打进镜像。

4.2 容器运行的资源限制与网络配置

镜像构建好之后,本地跑起来验证:

docker run -d \ --name langgraph-svc \ -p 8000:8000 \ --memory 2g \ --cpus 2 \ -e MODEL_API_KEY=your_key \ -v /data/models:/app/models \ langgraph-service:v1

--memory 2g和--cpus 2是资源限制,防止单个容器吃光宿主机资源。-e传环境变量,敏感信息不要写进镜像。-v挂载模型目录,避免镜像过大。

网络这块,如果多个容器需要互相通信(比如 LangGraph 服务要连 Redis 存会话状态),建议用自定义 bridge 网络:

docker network create langgraph-net docker run -d --name redis --network langgraph-net redis:7-alpine docker run -d --name langgraph-svc --network langgraph-net langgraph-service:v1

同一个网络里的容器可以用容器名直接互相访问,不用管 IP。

4.3 K8s 部署的核心配置拆解

上了 K8s,核心是三个资源对象:Deployment、Service、Ingress。Deployment 管副本和更新,Service 管内部负载均衡,Ingress 管外部访问。

Deployment 的关键配置:

apiVersion: apps/v1 kind: Deployment metadata: name: langgraph-svc spec: replicas: 3 selector: matchLabels: app: langgraph-svc template: metadata: labels: app: langgraph-svc spec: containers: - name: app image: registry.example.com/langgraph-service:v1 ports: - containerPort: 8000 resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "2" readinessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8000 initialDelaySeconds: 30 periodSeconds: 10 env: - name: MODEL_API_KEY valueFrom: secretKeyRef: name: langgraph-secrets key: model-api-key

replicas: 3起三个副本。resources里的requests是调度依据,limits是硬上限。readinessProbe决定 Pod 什么时候可以接流量,livenessProbe决定 Pod 什么时候重启。这两个探针必须配,否则流量可能打到还没启动完的 Pod 上,或者挂死的 Pod 一直不重启。

Service 配置:

apiVersion: v1 kind: Service metadata: name: langgraph-svc spec: selector: app: langgraph-svc ports: - port: 80 targetPort: 8000 type: ClusterIP

Ingress 配置:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: langgraph-ingress annotations: nginx.ingress.kubernetes.io/proxy-read-timeout: "300" spec: rules: - host: langgraph.example.com http: paths: - path: / pathType: Prefix backend: service: name: langgraph-svc port: number: 80

注意:流式接口的proxy-read-timeout要调大,默认 60 秒可能不够,长文本生成容易超时断开。

4.4 配置与密钥管理

K8s 里配置和密钥要分开管。普通配置用 ConfigMap,敏感信息用 Secret。创建 Secret:

kubectl create secret generic langgraph-secrets \ --from-literal=model-api-key=your_key \ --from-literal=redis-password=your_password

然后在 Deployment 里通过valueFrom.secretKeyRef引用。这样密钥不会出现在镜像里,也不会出现在代码仓库里。ConfigMap 类似,用来存非敏感的配置项,比如模型名称、超时时间、日志级别。

4.5 滚动更新与回滚

K8s 的滚动更新是默认行为,改镜像版本后kubectl apply就会触发。关键参数是maxSurge和maxUnavailable:

strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0

maxSurge: 1表示更新时最多多起一个 Pod,maxUnavailable: 0表示更新过程中可用 Pod 数量不能减少。这样能保证更新期间服务不中断。如果新版本有问题,kubectl rollout undo deployment/langgraph-svc一键回滚到上一版。

5. 常见问题与排查技巧实录

5.1 服务启动类问题

问题一:Uvicorn 启动报Address already in use

端口被占了。先lsof -i :8000或者netstat -tlnp | grep 8000找到占用进程,要么杀掉,要么换端口。Docker 场景下检查是不是有同名容器还在跑,docker ps -a看一下。

问题二:Docker Desktop 启动失败,提示虚拟化未开启

Windows 上装 Docker Desktop,如果 BIOS 里没开虚拟化,会报virtualization support not detected。进 BIOS 打开 VT-x 或者 AMD-V,然后在 Windows 功能里确认Hyper-V和虚拟机平台都勾上了。如果还是不行,检查是不是装了其他虚拟化软件冲突。

问题三:容器内服务启动正常,但外部访问不通

先确认端口映射对不对,docker run -p 8000:8000前面是宿主机端口,后面是容器端口。然后确认服务监听的是0.0.0.0而不是127.0.0.1,监听127.0.0.1的话容器外访问不到。最后检查防火墙规则。

5.2 性能与稳定性问题

问题四:并发一高就超时

先看是不是 worker 数量不够。Uvicorn 单 worker 是单进程,异步虽然能处理并发,但 CPU 密集操作会阻塞事件循环。如果图里有大量同步计算,考虑加 worker 或者把计算部分放到线程池里跑。另外检查模型 API 调用是不是同步的,同步调用会阻塞整个事件循环,必须换成异步客户端。

问题五:内存持续增长不释放

LangGraph 的图如果持有大量状态,多轮对话场景下内存会累积。检查是不是把会话状态存在了进程内存里,如果是,改成存 Redis 或者数据库。另外检查有没有循环引用导致 GC 回收不掉,可以用tracemalloc或者objgraph排查。

问题六:K8s 里 Pod 频繁重启

先kubectl describe pod <pod-name>看事件,常见原因是livenessProbe失败。如果服务启动慢,initialDelaySeconds要调大。如果是内存超限被 OOMKill,调大limits.memory或者排查内存泄漏。还有一种情况是探针路径写错了,服务根本没这个接口,那肯定一直失败。

5.3 排查速查表

现象可能原因排查动作
启动报端口占用端口被其他进程占用lsof -i :端口找进程
外部访问不通监听地址或端口映射错误检查0.0.0.0和-p参数
并发超时worker 不足或同步阻塞加 worker,换异步客户端
内存增长状态未外置或内存泄漏状态存 Redis,用 tracemalloc 排查
Pod 频繁重启探针失败或 OOMkubectl describe pod看事件
流式输出卡顿网关缓冲未关闭关 Nginx/Ingress 缓冲
日志丢失日志级别或 handler 冲突显式配置 logging

5.4 几条踩坑心得

第一条,不要在容器里跑模型训练或者大模型推理,除非你有 GPU 直通。容器里的 GPU 支持需要额外配置,而且资源隔离做不好会影响其他容器。模型推理建议单独部署,LangGraph 服务通过 API 调用。

第二条,K8s 的 ConfigMap 更新不会自动重启 Pod。改了 ConfigMap 之后,要么手动kubectl rollout restart deployment,要么用工具做自动 reload。这个坑我踩过,改完配置发现没生效,排查半天才发现 Pod 没重启。

第三条,Docker 镜像的 tag 不要用latest。生产环境用latest会导致回滚困难,因为你不知道上一个latest是哪个版本。用语义化版本号或者 git commit hash 做 tag,回滚的时候明确知道回哪个。

第四条,健康检查接口要轻量。/health接口不要做复杂逻辑,就返回个 200 就行。如果健康检查里去连数据库、调模型,探针超时会误判,导致 Pod 被反复重启。

6. 三条路径的迁移时机与扩展方向

6.1 什么时候该从路径一升到路径二

判断标准很简单:当第二个人需要调用你的 LangGraph 能力时。如果只有你自己用,本地脚本足够了。但一旦有同事、有前端、有其他服务需要调,就得包成 HTTP 服务。另一个信号是需要并发,本地脚本一次只能跑一个任务,两个请求就得排队,这时候 FastAPI 的异步能力就体现出价值了。

从路径一升路径二,改动量其实不大。核心工作是把脚本里的输入输出改成请求响应模型,把graph.invoke()改成graph.ainvoke(),然后加一层路由。图本身的逻辑基本不用动,这也是 LangGraph 设计得好的地方,编排逻辑和运行方式解耦。

6.2 什么时候该从路径二升到路径三

三个信号:调用量稳定增长、需要弹性伸缩、需要多环境一致性。如果日调用量到了万级,单机 FastAPI 即使加 worker 也扛不住,就得考虑多实例加负载均衡。如果流量有明显波峰波谷,比如白天高晚上低,K8s 的 HPA 能自动扩缩容,省资源。如果团队有多套环境(开发、测试、生产),Docker 镜像能保证环境一致,避免“我本地能跑”的问题。

但升级之前算一笔账:K8s 集群本身的资源开销、运维人力、学习成本。如果这些成本超过了你节省的服务器费用和运维时间,那就先别升。我见过小团队硬上 K8s,结果一半时间在修集群,一半时间在写业务,得不偿失。

6.3 后续可以扩展的方向

第一个方向是状态持久化。目前路径二和路径三的会话状态如果存在内存里,多副本场景下会丢。可以接 Redis 或者 PostgreSQL 做 checkpointer,LangGraph 本身支持SqliteSaver和PostgresSaver,换成分布式的就行。

第二个方向是可观测性。加 LangSmith 或者 OpenTelemetry,把每次图执行的链路、耗时、token 消耗都记录下来。生产环境没有可观测性就是盲跑,出了问题只能猜。

第三个方向是多模型路由。在 LangGraph 的节点里根据任务类型路由到不同的模型,简单任务走小模型,复杂任务走大模型,成本和效果都能兼顾。

第四个方向是灰度发布。K8s 里可以用两个 Deployment 加 Service 的权重配置做灰度,新版本先接 10% 流量,观察没问题再全量。这个在路径三里是原生支持的,路径二就得自己写网关逻辑。

部署这件事,没有一步到位的方案,只有适合当前阶段的方案。我自己的习惯是先用路径一把逻辑跑通,确定图结构和 prompt 稳定了,再升路径二对外服务,等调用量真的上来了再考虑路径三。每次升级都只解决当前最痛的问题,不提前过度设计。这套节奏用了几年,踩坑最少,返工也最少。

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

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

立即咨询