☰
FastAPI + LangGraph 打造可上线 Chatbot:从零到部署实战
2026/10/20 17:26:48 网站建设 项目流程

1. 为什么我最终选择了 FastAPI + LangGraph 这套组合来搭 Chatbot

先说结论:如果你手头正好有个基于 LangChain / LangGraph 跑通的机器人原型,想把它变成一个真正能给用户用的服务,FastAPI 几乎是现阶段最省心、最合理的封装层。我前前后后试过直接用 Chainlit 套壳、用 Gradio 拉界面、甚至用 Flask 硬撸,最后都因为各种原因放弃了,直到把 FastAPI 作为统一入口,整个工程才算真正稳定下来。

这套组合核心解决的是三个问题:第一,LangGraph 负责把“多轮对话的状态流转”这种复杂逻辑管起来,让机器人不再是简单的“一问一答”,而是真正有记忆、会走分支的 Agent;第二,FastAPI 负责把 LangGraph 跑出来的结果包装成标准的 HTTP 接口,让前端、小程序、第三方系统都能轻松对接;第三,两者通过异步和流式响应天然契合,用户打字的时候能看到一个字一个字往外蹦,体验直接上一个档次。

适合看这篇内容的人,我大概分成三类:一类是刚把 LangChain 文档刷完、想上手 LangGraph 但不知道从哪下手的初学者;一类是已经写了个 Chatbot 脚本、但不知道怎么变成线上服务的中级开发者;还有一类是准备在项目里正式引入 Agent 架构、需要一套稳妥落地方案的技术负责人。这篇就当是一份从零到上线的手记,我踩过的坑你就别再踩了。

2. 项目整体设计与技术选型思路拆解

2.1 为什么状态管理是 Chatbot 的核心命门

做聊天机器人,最大的坑不是模型不会说话,而是“记不住事”。你用开放接口调 GPT 类模型,默认就是无状态的,用户上一句说“帮我查一下明天的天气”,下一句问“那后天呢”,模型根本不知道“那”指的是什么。

LangGraph 解决这个问题的思路特别像写前端状态管理。它把每一次对话都看作一张“图”,图的节点是各种处理逻辑(比如意图识别、检索知识库、调用工具、生成回复),边是状态流转的规则。状态(State)会在这个图里一路传递,所有节点都能读写这份共享状态,自然就能把上下文带住了。

我见过不少人在自己代码里用全局变量或者 Redis 缓存硬拼上下文,短期跑着没问题,一旦遇到多轮工具调用、分支判断多的场景,代码直接变成意大利面条。LangGraph 的好处是,状态结构是显式定义的,每个节点能改哪些字段、节点之间怎么跳转,全部在配置里看得清清楚楚,调试时看状态快照就能定位问题。

2.2 FastAPI 作为服务层,赢在生态和异步机制

FastAPI 是我个人非常偏爱的 Python Web 框架,它跟 LangGraph 放在一起属于“天生一对”。LangGraph 的运行时本身支持异步,FastAPI 对异步的支持又是出了名的顺滑,两边都是原生 async 语法,不用做任何桥接转换。

另外一个关键点是流式输出。现在的聊天机器人如果等模型把整段话生成完再一次性返回,用户基本等不及。FastAPI 可以借助 StreamingResponse 一点一点把 token 推给前端,配合 WebSocket 或者 SSE,体验跟真实的大模型产品没有区别。

更重要的是,FastAPI 自带了接口文档,路径 /docs 打开就是 Swagger 界面,前后端联调的时候让同事直接在页面上试参数,省掉一大堆嘴皮子功夫。部署的时候,FastAPI 应用可以用 Uvicorn 或者 Gunicorn + Uvicorn Worker 直接拉起,跟 LangGraph 的状态检查点持久化功能配合,整个服务非常规范。

2.3 我为什么不用 Flask 或者纯 LangServe

其实 LangChain 官方也有一个 LangServe 项目,就是专门把 Chain / Agent 封装成 REST API 的。但实际用下来,LangServe 的问题在于定制化能力还是不够灵活——比如我想要一个自定义的鉴权中间件、想自己控制接口返回的错误码结构、想精确管理 WebSocket 连接生命周期,直接改源码又不划算,而 FastAPI 这些全部是原生能力,随手就能写。

Flask 就更不用提了,同步框架在流式输出这个环节特别吃亏。真用 Flask 跑 SSE,得配一堆线程池和队列来模拟异步,部署时的坑比解决的问题还多。当然,我这么说不是否定 Flask,只是针对 LangGraph 这个场景,FastAPI 是更省力的那个。

3. 搭建环境与依赖配置

3.1 Python 版本与虚拟环境准备

建议直接用 Python 3.11 以上版本,LangGraph 的新特性在 3.11 上最稳定,3.12 我也试过,部分底层依赖还没完全跟上。个人习惯在项目根目录下用内置的 venv 建虚拟环境,干净利落。

mkdir langgraph-chatbot cd langgraph-chatbot python3.11 -m venv venv source venv/bin/activate

激活之后顺手把 pip 升级一下,避免后面装包的时候遇到版本解析报错。

3.2 核心依赖清单

我实际项目里用到的依赖清单如下,每个包都标注了它的作用:

依赖包版本建议用途说明
langgraph最新稳定版构建 Agent 状态图、管理多轮对话流程
langchain-openai最新稳定版兼容各类 OpenAI 协议的大模型接口
langchain-community最新稳定版提供工具调用、检索器等集成组件
fastapi0.100 以上Web 服务框架
uvicorn0.23 以上ASGI 服务器,负责真正跑起服务
sse-starlette最新稳定版简化 Server-Sent Events 流式响应

安装命令一条搞定:

pip install "langgraph" "langchain-openai" "langchain-community" "fastapi" "uvicorn" "sse-starlette"

3.3 环境变量配置与模型接入

模型接入我用的是环境变量方式,方便不同环境切换配置。在项目根目录建一个 .env 文件:

OPENAI_API_KEY=你的密钥 OPENAI_BASE_URL=https://你的模型服务地址 MODEL_NAME=gpt-4o-mini

然后写一个配置文件 config.py 统一读取:

import os from dotenv import load_dotenv load_dotenv() OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL") MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")

这里有个提醒:国内环境访问模型服务时,请务必把 base_url 指向合规的模型服务地址,别在代码里硬编码乱七八糟的代理。我后来发现直接在请求层做 Host 替换,比改 LangChain 内部配置省心得多,还没有兼容性问题。

4. LangGraph 核心概念与 Chatbot 状态图设计

4.1 从 StateGraph 开始理解 LangGraph

LangGraph 的核心抽象是 StateGraph,它把整个 Agent 交互流程定义成一张有向图。图中每个节点是一个函数,负责处理一部分逻辑;节点之间通过边连接,表示状态流转的路径。运行时,一个请求会按照图的结构从一个节点走向下一个节点,直到到达结束节点。

我习惯把 StateGraph 类比成做饭的流程图:先洗菜,再切菜,然后炒菜,每一步做完食材的状态就变了。LangGraph 的 State 好比案板上的食材集合,每个节点只关注自己需要处理的字段,做完就往共享状态里写结果。

4.2 定义自己的状态数据结构

状态数据结构是整张图的灵魂,它决定了每个节点能读什么、能写什么。我用 TypedDict 来定义:

from typing_extensions import TypedDict class ChatbotState(TypedDict): messages: list # 对话历史,核心上下文 user_intent: str # 用户意图,比如"查天气"、"问知识" retrieved_docs: list # 知识库检索出来的相关内容 final_answer: str # 最终生成的回复

这里的 messages 字段我专门做成了 LangChain 的消息对象列表,里面存的是经过规范化处理的对话历史。LangGraph 有个 MessageGraph 的简化封装,但我还是用标准 StateGraph,灵活度更高,后面扩展工具调用和条件判断都方便。

4.3 图节点设计与节点间流转逻辑

节点是我设计时花时间最多的部分。一个生产可用的 Chatbot,至少需要下面这几个节点:

  • 意图识别节点:判断用户这句话是闲聊、提问、还是需要调工具。
  • 知识检索节点:如果问题涉及专业知识,就从向量库里找出相关内容。
  • 回复生成节点:把上下文、检索结果、意图信息汇总给大模型,生成最终回答。

节点之间的流转用条件边实现。LangGraph 里条件边的定义方式是在节点函数里返回一个决定“下一跳去哪”的字符串,或者让专门的 router 函数根据当前状态返回下一节点名称。

from langgraph.graph import StateGraph, END def intent_node(state: ChatbotState): # 调用模型判断意图 return {"user_intent": "knowledge_query"} def retrieve_node(state: ChatbotState): # 执行知识检索 return {"retrieved_docs": docs} def generate_node(state: ChatbotState): # 生成回复 return {"final_answer": answer} def router(state: ChatbotState): if state["user_intent"] == "knowledge_query": return "retrieve" return "generate" graph = StateGraph(ChatbotState) graph.add_node("intent", intent_node) graph.add_node("retrieve", retrieve_node) graph.add_node("generate", generate_node) graph.set_entry_point("intent") graph.add_conditional_edges( "intent", router, {"retrieve": "retrieve", "generate": "generate"} ) graph.add_edge("retrieve", "generate") graph.add_edge("generate", END)

4.4 记忆机制与状态持久化

如果说状态图管的是“单次会话内部的流转”,那记忆机制管的就是“跨会话的长期记忆”。LangGraph 提供了 BaseCheckpointSaver 这个接口,可以把每一步的图状态保存下来,达成断点续跑和对话恢复的效果。

我生产环境用的是 PostgreSQL 保存检查点。配置非常简单:

from langgraph.checkpoint.postgres import PostgresSaver checkpointer = PostgresSaver.from_conn_string("postgresql://user:pass@localhost/langgraph") graph = graph.compile(checkpointer=checkpointer)

这样每次对话的 messages 状态都会落到数据库里,服务重启、横向扩容都不会丢上下文。用户下次再来,只要带上同一个 thread_id,LangGraph 会自动把之前的状态捞出来接续对话。

5. FastAPI 集成层实现

5.1 创建 FastAPI 应用实例与基础配置

服务层我用 FastAPI 封装,整个应用入口写在 app.py 里。代码结构可以保持简洁:

from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel app = FastAPI(title="LangGraph Chatbot API", version="1.0.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

CORS 中间件必须配好,不然前端本地调试的时候直接跨域报错,特别坑。如果你部署在公司内网,建议把 allow_origins 限定到具体域名,不要图省事用 *,安全起见。

5.2 请求体与响应体结构定义

用 Pydantic 定义接口的输入输出,好处是 FastAPI 自动帮你做参数校验、生成文档。我用下面这种方式定义请求结构:

class ChatRequest(BaseModel): message: str thread_id: str user_id: str | None = None class ChatResponse(BaseModel): answer: str thread_id: str

这里 thread_id 特别关键,它是 LangGraph 状态持久化的会话标识。前端每次对话都带上同一个 thread_id,机器人才知道这是在接续同一次对话。

5.3 同步与异步接口实现

LangGraph 的图对象编译后可以直接 invoke 调用。FastAPI 路由里面我用 async 定义接口,但调用图的时候要注意,如果图内部跑的是同步工具函数,直接在 async 函数里调用可能阻塞事件循环。

我的做法是:普通接口用线程池隔离,流式接口用单独的处理方式。先看普通版本:

from fastapi.concurrency import run_in_threadpool @app.post("/chat") async def chat(req: ChatRequest): config = {"configurable": {"thread_id": req.thread_id}} result = await run_in_threadpool(graph.invoke, {"messages": [("user", req.message)]}, config) return ChatResponse(answer=result["final_answer"], thread_id=req.thread_id)

run_in_threadpool 会把耗时的 invoke 调用扔到线程池,不阻塞事件循环,这样并发请求过来的时候服务不会卡死。

5.4 流式输出与事件推送

流式输出是聊天机器人体验的分水岭。LangGraph 提供了 astream_events 方法,可以拿到每个节点的 token 级输出。FastAPI 侧我用 SSE 把这些事件推给前端,代码不复杂,但效果立竿见影:

from sse_starlette.sse import EventSourceResponse from fastapi.responses import StreamingResponse @app.post("/chat/stream") async def chat_stream(req: ChatRequest): config = {"configurable": {"thread_id": req.thread_id}} async def event_generator(): async for event in graph.astream_events( {"messages": [("user", req.message)]}, config=config, version="v2" ): if event["event"] == "on_chat_model_stream": token = event["data"]["chunk"].content if token: yield {"event": "token", "data": token} yield {"event": "done", "data": ""} return EventSourceResponse(event_generator())

这里的关键是 version="v2" 这个参数,LangGraph 的 astream_events 返回的事件结构会随着版本变化,我一度在这个参数上踩了坑,后面排查章节会重点说。

6. 完整实操过程:从写代码到本地跑通

6.1 编写一个带知识库检索的 Chatbot 完整代码

为了让你能完整跑起来,我贴一个可以直接复制的最小可用版本,这个版本去掉了我生产环境的复杂业务逻辑,突出核心链路。

from typing_extensions import TypedDict from langgraph.graph import StateGraph, END from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage import config class ChatbotState(TypedDict): messages: list final_answer: str MODEL = ChatOpenAI( model=config.MODEL_NAME, api_key=config.OPENAI_API_KEY, base_url=config.OPENAI_BASE_URL, temperature=0.7 ) def generate(state: ChatbotState): response = MODEL.invoke(state["messages"]) return {"final_answer": response.content} graph = StateGraph(ChatbotState) graph.add_node("generate", generate) graph.set_entry_point("generate") graph.add_edge("generate", END) app_graph = graph.compile()

这个版本没有知识库、没有工具调用,就是最普通的对话。你可以先跑通这个最小链路,再逐步加节点。

6.2 添加记忆功能与多轮对话支持

做多轮对话之前,先搞清楚一个概念:LangGraph 本身不管对话历史,模型记不记得住上下文取决于你在消息列表里放了多少历史。最简单的多轮支持是这样:

def generate(state: ChatbotState): history = state["messages"] response = MODEL.invoke(history) return {"final_answer": response.content}

invoke 的时候把完整消息历史喂给模型,模型就能“记住”之前的对话。但历史不能无限长,需要做个截断,我的经验是保留最近十轮消息:

def trim_history(messages, max_rounds=10): return messages[-max_rounds * 2:]

这里乘以 2 是因为每轮对话包含用户消息和 AI 回复两条。

6.3 调用外部工具,让机器人具备动手能力

一个真正有用的 Chatbot 不能光会聊天,还得会查时间、查天气、算算术。LangGraph 里工具调用是这么做的:

from langchain_core.tools import tool @tool def get_current_time() -> str: """获取当前时间""" import datetime return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") tools = [get_current_time] MODEL_WITH_TOOLS = MODEL.bind_tools(tools) def should_use_tools(state: ChatbotState): result = MODEL_WITH_TOOLS.invoke(state["messages"]) if result.tool_calls: return "call_tool" return "respond" def call_tool(state: ChatbotState): result = MODEL_WITH_TOOLS.invoke(state["messages"]) for call in result.tool_calls: tool_result = get_current_time.invoke(call["args"]) state["messages"].append(AIMessage(content=result.content, tool_calls=result.tool_calls)) state["messages"].append(ToolMessage(content=str(tool_result), tool_call_id=call["id"])) return {"messages": state["messages"]}

注意 tool_calls 的处理逻辑,模型决定调工具时会生成一个 tool_call_id,你把这个 ID 和工具执行结果一起返回给模型,模型才能把这些信息串起来生成最终回答。

6.4 本地调试 Uvicorn 启动与前端联通

代码写完之后,启动服务非常简单:

uvicorn app:app --host 0.0.0.0 --port 8000 --reload

--reload 参数适合本地调试,改代码会自动重启。服务起来后在浏览器打开 http://localhost:8000/docs ,直接能在 Swagger 页面里测试 /chat 接口。

如果要从命令行快速验证:

curl -X POST "http://localhost:8000/chat" \ -H "Content-Type: application/json" \ -d '{"message": "你好", "thread_id": "test-001"}'

看到返回 JSON 里带 answer 字段,说明整个链路通了。

6.5 前端 WebSocket 或 SSE 对接实录

前端对接我直接用原生的 EventSource API,不需要额外依赖。后端接口换成 /chat/stream 返回 SSE 流后,前端这样消费:

const eventSource = new EventSource("http://localhost:8000/chat/stream"); eventSource.addEventListener("token", (event) => { const data = JSON.parse(event.data); // 把 token 追加到聊天窗口 appendToChatWindow(data); }); eventSource.addEventListener("done", () => { eventSource.close(); });

这里 EventSource 默认走 GET 请求,如果你需要 POST 方式传参,可以用 fetch 配合 ReadableStream 手动解析,或者后端把参数拼在 URL query 里。我上线的项目里改用了 WebSocket,因为要支持客户端中断生成,这块后面单独讲。

7. 部署上线与性能调优

7.1 使用 Gunicorn 多 Worker 部署

本地 Uvicorn 单进程只适合开发,生产环境我用 Gunicorn 管理多个 Uvicorn Worker,这样能充分利用多核 CPU:

gunicorn app:app \ --worker-class uvicorn.workers.UvicornWorker \ --workers 4 \ --bind 0.0.0.0:8000 \ --timeout 120 \ --graceful-timeout 30

workers 数量建议设置为 CPU 核心数乘 2 加 1。timeout 参数务必调大,模型推理时间一长,Worker 进程很容易被 Gunicorn 判定超时杀掉,默认 30 秒肯定不够。

7.2 Docker 容器化与镜像构建

容器化部署是现在的必选项。我的 Dockerfile 长这样:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["gunicorn", "app:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--workers", "4", "--bind", "0.0.0.0:8000"]

image 构建的时候注意把 .env 文件用环境变量注入的方式替代,不要把密钥打进镜像里。我用 docker-compose 管理整个服务栈,LangGraph 的 PostgreSQL 检查点容器也在里面。

7.3 会话恢复与横向扩容的坑

如果你用了多 Worker 部署,会发现一个致命问题:对话状态存哪里?如果存内存,那 Worker 1 处理的会话,下次请求被负载均衡分到 Worker 2,上下文全丢了。

解决方法就是我前面提到的 PostgreSQL 检查点。只要所有 Worker 连同一个数据库,LangGraph 就能跨进程恢复状态。这也是为什么我强调 LangGraph 的检查点持久化接口是生产级功能的原因。

7.4 性能调优经验分享

我上线后做了一轮压测,分享几个关键数据:单 Worker 并发处理能力大约在每秒 5 到 10 个请求,瓶颈主要在大模型 API 的响应时间。主要优化手段有三个:

  • 把流式响应的 token 缓存到本地,避免前端重复请求。
  • 给 FastAPI 加一层简单的内存缓存,命中缓存的知识库检索结果不用重新调模型。
  • 对超过 30 秒还没完成的生成任务,后端主动断开并向客户端返回友好提示。

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

8.1 LangGraph 事件流为空或结构不符

我遇到最多的坑就是 astream_events 事件结构不对。LangGraph 不同版本的事件流过滤条件不一样,我原来用的代码在某次升级之后突然拿不到 token。

排查思路是:先把事件全部打出来,看结构再写过滤逻辑。

async for event in graph.astream_events(inputs, config=config, version="v1"): print(event["event"], event["data"].keys())

看到结构后再针对性调整。最新版本推荐用 version="v2",事件过滤用event["event"] == "on_chat_model_stream",同时在节点里加上tags=["generate"]精确过滤,避免检索节点上的模型输出干扰。

8.2 FastAPI 接口超时与 Worker 被杀

如果用 Gunicorn 部署,模型响应时间长的时候会出现 504 或者连接中断。原因就是 Gunicorn 的 timeout 设置太短。

解决方法是 timeout 调大到 120 秒,同时加上 gunicorn 的 --graceful-timeout 参数,给 Worker 足够的收尾时间。如果你的业务预期响应时间超过 180 秒,建议改成异步任务模式,提交任务后前端轮询结果,而不是一直挂着连接等。

8.3 多线程下的共享变量污染

这是 Python 后端最容易忽视的问题。如果你把 LangGraph 编译后的图对象声明成模块全局变量,多个线程同时调用,LangChain 内部某些组件会互抢资源。

我的做法是:FastAPI 启动事件里创建图实例,每个请求通过依赖注入的方式获取,避免实例变量的竞争。

8.4 部署到服务器后本地能跑但线上报错

这类问题的排查顺序我总结成一张清单:

排查项检查方法
环境变量是否注入服务器上 echo $OPENAI_API_KEY 确认值
模型服务地址是否可通在服务器上 curl 测试模型服务
数据库连接是否超时检查 PostgreSQL 连接池配置
文件路径是否正确确认绝对路径和相对路径的差异

容器里最容易出错的坑是时区问题。LangGraph 检查点里的时间戳全部是 UTC 时间,如果业务需要展示本地时间,记得在代码里转换,别直接拿数据库里的时间戳展示。

8.5 CORS 与跨域问题排查

前端部署在另一个域名,后端请求报 CORS 错误。配了 allow_origins 还报错的话,大概率是 allow_credentials 没有同步设置或者中间件顺序有问题。FastAPI 的 CORS 中间件要放在路由注册之前。

9. LangGraph 进阶技巧与生产化建议

9.1 多 Agent 协作与子图设计

单图结构解决不了特别复杂的业务,这时候可以把整张图当作一个节点嵌入到另一张更大的图里。LangGraph 支持子图嵌套,我做过一个客服系统,主图负责意图分发,子图分别处理退货、物流、商品咨询,代码结构像模块化编程一样清晰。

9.2 人工接管机制

再聪明的机器人总有搞不定的时候。LangGraph 的 interrupt 机制可以暂停图运行,把控制权交给人工客服,人工处理完再通过 Command(resume=...) 恢复原图继续执行。这套机制落地的效果比我预想的好,安全感和用户体验提升非常明显。

9.3 可观测性与日志记录

生产环境必须做日志。我在每个节点加了一个统一的日志工具函数,记录节点名、输入状态摘要、耗时。对接上日志平台之后,定位问题从小时级缩短到分钟级。LangGraph 官方也提供了 Langfuse 或者 LangSmith 的集成能力,个人项目可以用 Langfuse 自托管,成本低效果也不错。

9.4 成本控制与 Token 优化

模型 API 的费用大头在上下文长度。我的策略是对系统提示词做压缩,把角色设定和业务规则做成精简版本,实测能把 token 消耗降低 25% 左右。另一个方案是在检索节点里对召回文档做重排,只保留最相关的三段,避免把整库内容都塞给模型。

10. 写在最后

这个项目从零到上线我大概花了三周时间,前两周都在踩 API 兼容性的坑,最后一周才是真正写业务逻辑。如果你正在规划类似的 Chatbot 服务,我的建议很简单:先用最小链路跑通 LangGraph 加 FastAPI 的骨架,再往里填业务节点。别一上来就设计十个节点的宏伟蓝图,先把一次完整对话的链路跑通,你才会真正理解状态流转是怎么回事。

最后分享一个小技巧:LangGraph 编译后的图,可以用 get_graph().draw_ascii() 在控制台把流程图打印出来,修改状态图时对照着看节点和边的连接关系,效率翻倍。

这个技术栈迭代速度很快,但核心思路不会变:状态流转 + 服务封装。你只要把这两个关键点吃透,后续不管框架怎么升级,都能快速跟上。

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

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

立即咨询