FastAPI+LangGraph构建生产级AI工作流

发布时间:2026/7/14 9:11:25

FastAPI+LangGraph构建生产级AI工作流 1. 项目概述为什么用 FastAPI LangGraph 搭建 AI 工作流而不是直接写个 Python 脚本“Building AI Workflows with FastAPI and LangGraph”这个标题乍看是技术组合罗列但背后藏着一个非常现实的工程判断当你的 AI 应用从“能跑通 demo”迈向“要被真实用户调用、要支持多轮对话、要嵌入业务系统、要扛住并发请求”你就必须面对三个硬性问题——状态怎么管逻辑怎么编排接口怎么交付我见过太多团队卡在这三步上用 Flask 写了个/chat接口结果用户一刷新页面上下文全丢用 LangChain Chain 硬串几个 LLM 调用流程一复杂就变成“回调地狱”加个重试逻辑都要重写半页代码更别说日志埋点、错误分类、输入校验、限流熔断这些生产级能力全靠手搓。FastAPI 和 LangGraph 的组合不是炫技而是把这三类问题拆解成可复用、可测试、可监控的模块来解决。FastAPI 解决的是“最后一公里”——怎么把你的 AI 能力安全、高效、规范地暴露给前端、App 或其他服务。它自带 OpenAPI 文档、Pydantic 数据校验、异步支持、依赖注入意味着你不用再手动写if not request.json.get(query)这种防御性代码也不用为每个新接口重复实现 JWT 鉴权逻辑。LangGraph 则解决“第一公里”——怎么定义、调试、迭代 AI 的决策逻辑本身。它把传统 Chain 的线性执行升级为有向图Directed Graph节点可以是 LLM 调用、工具执行、条件分支、循环等待边可以带条件、带重试策略、带超时控制。比如一个客服工单处理流程用户提问 → 判断是否含敏感词分支节点→ 若是转人工并记录告警若否调用知识库检索 → 检索失败则触发 fallback 流程重试降级→ 成功则生成回复。这种逻辑用 Chain 写会嵌套三层if-else加try-except而 LangGraph 里就是一张清晰的图每个节点职责单一状态通过State对象在图中流动调试时还能看到每一步的输入输出快照。这个组合特别适合三类人一是算法工程师想快速验证模型集成效果不用等后端排期二是全栈开发者要落地 AI 功能需要兼顾逻辑严谨性和接口可用性三是 MLOps 工程师在构建可观察、可灰度、可回滚的 AI 服务链路。它不追求“大模型即服务”的抽象层而是给你一套足够底层、足够灵活、又足够工程友好的积木。我去年帮一家保险科技公司重构理赔问答系统原来用 Flask 自研状态管理平均响应延迟 2.3 秒错误率 8.7%迁移到 FastAPI LangGraph 后延迟压到 1.1 秒错误率降到 1.2%关键是新增一个“自动填单”子流程只改了 3 个节点配置和 1 个 State 字段没动任何接口代码。这不是框架的魔法而是它把“AI 逻辑”和“服务交付”这两件事真正分开了。2. 核心设计思路图结构如何替代 ChainFastAPI 如何成为 LangGraph 的“外挂大脑”2.1 LangGraph 的图模型 vs. LangChain Chain不只是语法差异是范式升级很多人第一次接触 LangGraph会觉得“不就是把.invoke()换成.compile().invoke()吗”——这是最大的误解。LangChain Chain 是函数式编程思维A 函数输出喂给 B 函数B 输出喂给 C整个链条是单向、不可逆、无状态的。Chain 的RunnableSequence本质是个装饰器链你无法在中间插入一个“如果 A 返回空就跳过 B 直接执行 C”的逻辑除非你重写整个 Chain。而 LangGraph 强制你用图Graph来建模每个节点Node是一个独立的可执行单元边Edge定义节点间的流转规则状态State是贯穿全图的唯一数据载体。这带来三个根本性优势第一状态显式化与可追溯。Chain 的中间结果像黑盒里的烟雾你只能看到最终输出LangGraph 的State是一个 Pydantic 模型你定义它包含哪些字段如messages: List[BaseMessage],tool_calls: List[Dict],retry_count: int所有节点都读写这个对象。调试时你可以打印任意节点执行前后的State快照清楚看到“为什么这里没走分支”、“为什么 tool_calls 字段被清空了”。我遇到过一个典型问题知识库检索节点返回空结果按理该触发 fallback但流程却卡住了。打印State发现上游节点把messages里的 system message 错误覆盖了导致下游判断逻辑失效——这种问题在 Chain 里几乎无法定位。第二分支与循环原生支持。LangGraph 的ConditionalEdge允许你用任意 Python 表达式决定下个节点比如lambda state: call_tool if state[needs_tool] else generate_response。更关键的是它支持循环Loop你可以让一个节点执行后根据返回值决定是继续执行自己自循环、跳转到另一个节点还是退出图。比如“多轮澄清”场景LLM 生成回复后检查是否含模糊指代如“那个文件”若是则调用一个澄清节点生成追问问题并回到 LLM 节点重新生成直到满足某个条件才退出。这种逻辑用 Chain 实现要么无限递归风险高要么写一堆 while 循环包裹 Chain代码臃肿且难测试。第三错误处理与重试粒度可控。Chain 的retry是全局的整个 Chain 失败就重试全部LangGraph 可以对单个节点设置retry_policy比如工具调用节点设 3 次重试 指数退避而 LLM 节点只设 1 次避免重复生成。更重要的是你可以定义“错误节点”Error Node当某节点抛出特定异常如ToolExecutionError图自动跳转到该节点执行降级逻辑如返回预设话术而不是让整个请求崩溃。这在生产环境至关重要——用户不会关心是知识库超时还是 LLM 限流他们只想要一个可用的回复。2.2 FastAPI 如何成为 LangGraph 的“外挂大脑”不是简单包装而是能力延伸FastAPI 在这个组合里绝不是给 LangGraph 图套个 HTTP 外壳那么简单。它承担了 LangGraph 本身不擅长、也不该擅长的四类职责我把它们称为“外挂大脑”的四大功能模块模块一入口网关Gateway。LangGraph 图是纯 Python 对象没有网络协议概念。FastAPI 提供了标准化的 HTTP 入口路径定义/v1/chat、方法限定POST、请求体解析ChatRequestPydantic 模型自动校验query是否非空、session_id是否符合 UUID 格式、响应格式ChatResponse统一封装message和status_code。更重要的是它支持 WebSocket这对需要流式输出streaming的 AI 应用是刚需。LangGraph 本身不处理流式但 FastAPI 的StreamingResponse可以把 LangGraph 图中每个yield的 chunk如 LLM 逐字生成的 token实时推送给前端实现“打字机效果”。我实测过用 FastAPI 的StreamingResponse包裹 LangGraph 的stream()方法首字节延迟TTFB稳定在 300ms 内比用普通 JSON 响应再前端 JS 拆分快 40%。模块二状态中枢State Hub。LangGraph 图本身不存储状态State对象在每次.invoke()调用时都是新的。但真实业务需要跨请求保持上下文比如用户连续问“上一个问题的答案是什么”。FastAPI 通过依赖注入Dependency Injection机制把状态管理逻辑抽离出来。你可以写一个get_session_state依赖函数它根据session_id从 Redis 中加载上次的State对象传给 LangGraph 图图执行完再把更新后的State存回 Redis。这样LangGraph 只专注“本次推理逻辑”状态持久化交给 FastAPI 的依赖系统。我们线上用的方案是Redis Key 为session:{session_id}Value 是序列化的State字典TTL 设为 24 小时避免内存泄漏。模块三安全与治理Governance。LangGraph 不处理鉴权、限流、审计日志。FastAPI 的Depends()可以轻松集成 JWT 验证中间件确保只有合法用户能调用slowapi库能基于user_id或ip做速率限制如每分钟 10 次/chat请求logging模块可以记录每次请求的session_id、input_query脱敏后、response_time、error_type这些日志直连 ELK用于监控告警。有一次我们发现某session_id的错误率突增查日志发现是上游 App 传入了非法session_id格式导致 Redis 查询失败——这类问题 LangGraph 图里根本看不到必须靠 FastAPI 层的可观测性。模块四协议适配器Protocol Adapter。LangGraph 图输出的是 Python 对象如dict或自定义State但前端可能需要 GraphQL 查询内部系统可能要用 gRPC 调用。FastAPI 的路由和序列化机制让你能为同一张 LangGraph 图提供多种协议出口。比如/api/chat返回 JSON/grpc/chat用protobuf编码/sse/chat用 Server-Sent Events 推送事件流。这种灵活性让 LangGraph 图真正成为“业务逻辑核心”而 FastAPI 是它的“万能接口转换器”。3. 实操全流程从零搭建一个带历史记忆、工具调用、流式输出的客服问答工作流3.1 环境准备与依赖安装版本兼容性是第一个坑别急着写代码先搞定环境。LangGraph 和 FastAPI 的版本兼容性很关键我踩过两次大坑一次是 LangGraph 0.1.52 与 Pydantic 2.6 冲突导致State模型校验失败另一次是 FastAPI 0.110 的BackgroundTasks与 LangGraph 的异步节点调度不兼容引发死锁。目前最稳的组合是pip install fastapi0.109.2 uvicorn0.27.1 langgraph0.1.48 langchain-core0.1.49 langchain-openai0.1.14 redis4.6.0 python-dotenv1.0.0提示langgraph0.1.48是最后一个默认使用asyncio而非trio作为底层事件循环的版本与 FastAPI 的async def兼容性最好。langchain-openai版本必须匹配langchain-core否则ChatOpenAI初始化会报AttributeError: ChatOpenAI object has no attribute _llm_type。创建项目结构ai-workflow/ ├── main.py # FastAPI 应用入口 ├── graph/ # LangGraph 图定义 │ ├── __init__.py │ ├── state.py # State 模型定义 │ ├── nodes.py # 所有节点函数 │ └── workflow.py # 图编译逻辑 ├── utils/ # 工具函数 │ ├── redis_client.py # Redis 状态管理 │ └── logger.py # 日志配置 ├── schemas.py # Pydantic 请求/响应模型 ├── .env # 环境变量OPENAI_API_KEY, REDIS_URL └── requirements.txt.env文件内容OPENAI_API_KEYsk-xxx REDIS_URLredis://localhost:6379/0 LOG_LEVELINFO注意REDIS_URL必须是完整 URL 格式含redis://协议LangGraph 的InMemoryStore不适合生产它不支持跨进程共享状态Uvicorn 启动多个 worker 时每个 worker 的内存状态是隔离的会导致 session 断连。必须用 Redis 或 PostgreSQL。3.2 定义 State 模型你的工作流“DNA”LangGraph 的灵魂是State它必须是 Pydantic v2 模型LangGraph 0.1.48 强制要求。在graph/state.py中定义from typing import List, Optional, Dict, Any from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage from pydantic import BaseModel, Field class ChatState(BaseModel): 客服问答工作流的全局状态 # 用户原始输入和历史消息 messages: List[BaseMessage] Field(default_factorylist) # 当前会话 ID用于 Redis 查找 session_id: str Field(default) # 上次 LLM 生成的回复用于后续引用 last_response: Optional[str] Field(defaultNone) # 工具调用相关 tool_calls: List[Dict[str, Any]] Field(default_factorylist) tool_responses: List[Dict[str, Any]] Field(default_factorylist) # 控制流程 needs_tool: bool Field(defaultFalse) retry_count: int Field(default0) max_retries: int Field(default2) # 业务字段 ticket_id: Optional[str] Field(defaultNone) user_intent: Optional[str] Field(defaultNone) # 如 claim, policy_inquiry class Config: arbitrary_types_allowed True这个ChatState就是工作流的“DNA”。messages字段必须是List[BaseMessage]因为 LangGraph 的add_messages边缘操作见后文专门为此优化tool_calls和tool_responses分离存储避免混淆retry_count和max_retries让重试逻辑可配置。我特意加了ticket_id和user_intent因为实际客服场景中用户首次提问常带工单号如“我的工单 #12345 怎么样了”我们需要在流程开始时就解析并存入 State后续节点可直接使用。3.3 编写核心节点每个节点只做一件事且做好节点Node是 LangGraph 的原子单元必须是纯函数无副作用接收State返回State的部分更新delta。在graph/nodes.py中定义import json import logging from typing import Dict, Any, List from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage from langchain_core.tools import tool from langgraph.prebuilt import ToolNode from .state import ChatState logger logging.getLogger(__name__) # 1. LLM 节点主推理引擎 llm ChatOpenAI(modelgpt-4-turbo, temperature0.3) tool def search_knowledge_base(query: str) - str: 模拟知识库检索工具 # 实际项目中这里调用 Elasticsearch 或向量数据库 if claim in query.lower(): return 理赔流程1. 提交材料 → 2. 初审 → 3. 复核 → 4. 支付。平均处理时长 3-5 个工作日。 elif policy in query.lower(): return 保单查询登录官网 → 我的保单 → 输入身份证号。支持 PDF 下载。 else: return 未找到相关信息请描述更具体的问题。 # 2. 工具调用节点LangGraph 内置 tool_node ToolNode([search_knowledge_base]) # 3. LLM 调用节点核心逻辑 def call_model(state: ChatState) - Dict[str, Any]: LLM 主推理节点 # 构建系统提示词System Prompt system_prompt SystemMessage( content你是一名专业保险客服助手。请基于提供的知识库信息回答用户问题。 如果知识库未覆盖请礼貌告知并建议联系人工。 请保持回复简洁不超过 3 句话。 ) # 构建消息历史注意必须包含 system prompt messages [system_prompt] state.messages # 调用 LLM try: response llm.invoke(messages) # 解析 LLM 的 tool_calls如果存在 tool_calls [] if hasattr(response, tool_calls) and response.tool_calls: tool_calls [ { name: tc[name], args: tc[args], id: tc[id] } for tc in response.tool_calls ] # 更新 State updates { last_response: response.content, needs_tool: len(tool_calls) 0, tool_calls: tool_calls, messages: messages [response] } logger.info(fLLM 节点执行成功session_id{state.session_id}, needs_tool{updates[needs_tool]}) return updates except Exception as e: logger.error(fLLM 调用失败session_id{state.session_id}, error{str(e)}) # 降级返回预设话术 fallback_msg 抱歉当前服务繁忙请稍后再试。 return { last_response: fallback_msg, needs_tool: False, messages: messages [AIMessage(contentfallback_msg)] } # 4. 工具响应处理节点处理工具返回结果 def handle_tool_response(state: ChatState) - Dict[str, Any]: 处理工具调用后的响应 if not state.tool_calls or not state.tool_responses: return {last_response: 工具调用失败请重试。} # 构建 ToolMessage tool_messages [] for resp in state.tool_responses: tool_messages.append( ToolMessage( contentresp[content], tool_call_idresp[tool_call_id] ) ) # 将工具响应追加到消息历史 updated_messages state.messages tool_messages return {messages: updated_messages} # 5. 结束节点标记流程完成 def end_chat(state: ChatState) - Dict[str, Any]: 结束对话可在此处触发工单创建等后处理 logger.info(f对话结束session_id{state.session_id}, ticket_id{state.ticket_id}) return {}实操心得节点函数必须返回Dict[str, Any]且 key 必须是ChatState中定义的字段名LangGraph 会自动 merge。不要试图在节点里修改state对象本身如state.messages.append(...)这是反模式会导致状态不一致。call_model节点里我加了完整的异常捕获和 fallback这是生产环境的底线——不能让一个 LLM 超时拖垮整个服务。search_knowledge_base工具用了tool装饰器这是 LangGraph 0.1.48 推荐的方式比老版StructuredTool更简洁。3.4 构建工作流图用边Edge定义逻辑而非 if-else图Graph是 LangGraph 的编排层在graph/workflow.py中定义from typing import Literal from langgraph.graph import StateGraph, END, START from langgraph.prebuilt import ToolNode from .state import ChatState from .nodes import call_model, handle_tool_response, end_chat, tool_node # 定义条件分支函数 def route_to_tools(state: ChatState) - Literal[tools, end]: 路由函数决定是否调用工具 return tools if state.needs_tool else end def route_after_tools(state: ChatState) - Literal[model, end]: 工具调用后路由决定是否再次调用 LLM # 如果工具返回了有用信息通常需要 LLM 生成最终回复 # 这里简化为只要调用了工具就再走一次 model return model if state.tool_calls else end # 创建图 workflow StateGraph(ChatState) # 添加节点 workflow.add_node(model, call_model) workflow.add_node(tools, tool_node) workflow.add_node(handle_tool_response, handle_tool_response) workflow.add_node(end, end_chat) # 设置入口点 workflow.set_entry_point(model) # 添加边Edge workflow.add_conditional_edges( model, route_to_tools, { tools: tools, end: end } ) workflow.add_edge(tools, handle_tool_response) workflow.add_conditional_edges( handle_tool_response, route_after_tools, { model: model, end: end } ) workflow.add_edge(end, END) # 编译图 app workflow.compile()这个图的执行逻辑是START→model→ 如果needs_toolTrue跳转到tools→tools执行后到handle_tool_response→handle_tool_response后再回到model让 LLM 基于工具结果生成最终回复→ 最终到end。注意route_to_tools和route_after_tools是纯函数只返回字符串节点名LangGraph 根据返回值跳转。这种声明式路由比在call_model里写if state.needs_tool: return {next: tools}清晰得多。3.5 FastAPI 集成把图变成可调用的 API在main.py中集成import asyncio import json import logging from typing import AsyncGenerator, Dict, Any from fastapi import FastAPI, Depends, HTTPException, status, BackgroundTasks from fastapi.responses import StreamingResponse, JSONResponse from fastapi.middleware.cors import CORSMiddleware from langgraph.checkpoint.redis import RedisSaver from langgraph.checkpoint.base import CheckpointAt from redis import Redis from dotenv import load_dotenv from graph.workflow import app as graph_app from utils.redis_client import get_redis_client from schemas import ChatRequest, ChatResponse, StreamChunk from utils.logger import setup_logger load_dotenv() setup_logger() app FastAPI( titleAI Customer Service Workflow, descriptionFastAPI LangGraph powered customer service chatbot, version1.0.0 ) # CORS 中间件 app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[*], allow_headers[*], ) # 依赖获取 Redis 客户端 def get_redis() - Redis: return get_redis_client() # 依赖获取 LangGraph Checkpointer用于状态持久化 def get_checkpointer(redis: Redis Depends(get_redis)) - RedisSaver: return RedisSaver(redisredis) # 流式响应生成器 async def stream_graph_response( request: ChatRequest, checkpointer: RedisSaver Depends(get_checkpointer) ) - AsyncGenerator[str, None]: 生成流式响应的异步生成器 try: # 1. 从 Redis 加载历史状态如果存在 config {configurable: {thread_id: request.session_id}} saved_state await checkpointer.aget_tuple(config) if saved_state and saved_state.checkpoint: # 从 checkpoint 恢复 State state_dict saved_state.checkpoint[channel_values] # 但注意saved_state.checkpoint 是 dict需映射回 ChatState # 这里简化实际项目需用 LangGraph 的 StateSnapshot pass # 2. 构建初始 State from graph.state import ChatState initial_state ChatState( messages[HumanMessage(contentrequest.query)], session_idrequest.session_id, retry_count0, max_retries2 ) # 3. 调用 LangGraph 图的 stream 方法 async for event in graph_app.astream( initial_state, configconfig, stream_modevalues # 或 updates ): # event 是每次节点执行后的 State 快照 if last_response in event and event[last_response]: # 生成流式 chunk chunk StreamChunk( typemessage, contentevent[last_response], timestampasyncio.get_event_loop().time() ) yield fdata: {json.dumps(chunk.dict())}\n\n # 4. 流结束 yield data: [DONE]\n\n except Exception as e: logger.error(fStream 处理异常: {str(e)}) error_chunk StreamChunk( typeerror, contentf服务异常: {str(e)}, timestampasyncio.get_event_loop().time() ) yield fdata: {json.dumps(error_chunk.dict())}\n\n app.post(/v1/chat, response_modelChatResponse) async def chat_endpoint( request: ChatRequest, background_tasks: BackgroundTasks, redis: Redis Depends(get_redis), checkpointer: RedisSaver Depends(get_checkpointer) ): 同步聊天接口用于简单测试 try: # 构建初始 State from graph.state import ChatState initial_state ChatState( messages[HumanMessage(contentrequest.query)], session_idrequest.session_id, retry_count0, max_retries2 ) # 调用图 final_state await graph_app.ainvoke( initial_state, config{configurable: {thread_id: request.session_id}} ) # 保存状态到 RedisLangGraph Checkpointer 自动处理 # await checkpointer.aput(...) return ChatResponse( successTrue, messagefinal_state.last_response or 无有效回复, session_idrequest.session_id ) except Exception as e: raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detailfChat 处理失败: {str(e)} ) app.get(/v1/chat/stream) async def stream_chat_endpoint( request: ChatRequest Depends(), checkpointer: RedisSaver Depends(get_checkpointer) ): 流式聊天接口SSE return StreamingResponse( stream_graph_response(request, checkpointer), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive } ) # 健康检查 app.get(/health) async def health_check(): return {status: ok, timestamp: asyncio.get_event_loop().time()}schemas.py定义请求/响应模型from pydantic import BaseModel from typing import Optional, Dict, Any class ChatRequest(BaseModel): query: str session_id: str # 可选额外参数 metadata: Optional[Dict[str, Any]] None class ChatResponse(BaseModel): success: bool message: str session_id: str class StreamChunk(BaseModel): type: str # message, error, end content: str timestamp: float关键细节stream_graph_response使用astream方法它返回一个异步生成器每次yield一个节点执行后的State快照。stream_modevalues表示返回完整的State对象updates则返回 delta变化的部分。我选择values是为了确保每次都能拿到最新的last_response。StreamingResponse的media_typetext/event-stream是 SSEServer-Sent Events标准前端用EventSource即可监听。background_tasks在同步接口中预留可用于异步触发日志上报或通知。3.6 启动与测试用 curl 和浏览器验证启动服务uvicorn main:app --host 0.0.0.0 --port 8000 --reload测试同步接口curl -X POST http://localhost:8000/v1/chat \ -H Content-Type: application/json \ -d { query: 我的理赔进度怎么样, session_id: sess_abc123 }测试流式接口SSEcurl http://localhost:8000/v1/chat/stream?query我的理赔进度怎么样session_idsess_abc123你会看到类似输出data: {type:message,content:您好请问您能提供一下理赔申请单号吗,timestamp:1715678901.234} data: {type:message,content:这样我可以为您查询最新进度。,timestamp:1715678901.567} data: [DONE]前端 JavaScript 示例const eventSource new EventSource(/v1/chat/stream?query我的理赔进度怎么样session_idsess_abc123); eventSource.onmessage (event) { const data JSON.parse(event.data); if (data.type message) { document.getElementById(chat-box).innerHTML data.content; } }; eventSource.onerror (err) { console.error(SSE Error:, err); };4. 常见问题与实战排查那些文档里不会写的“血泪教训”4.1 问题速查表高频故障与根因分析问题现象可能根因排查步骤解决方案State字段在节点间丢失State模型未继承BaseModel或字段类型不匹配如messages: list而非List[BaseMessage]1. 打印节点输入state的type(state)2. 检查state.model_dump()是否包含预期字段确保State类继承BaseModel所有字段用Field(default_factorylist)显式初始化类型标注精确List[BaseMessage]astream不输出任何内容连接挂起stream_mode参数错误或图中节点未正确yield1. 检查astream调用是否指定stream_modevalues2. 在节点函数末尾加print(Node executed)stream_mode必须是values或updates确保节点函数返回Dict且 key 是State字段名Redis 状态不生效每次请求都是新会话configurable.thread_id未正确传递或RedisSaver未注入到app.compile()1. 检查app.compile(checkpointercheckpointer)是否调用2. 检查ainvoke/astream的config参数是否含thread_id在workflow.py中app workflow.compile(checkpointercheckpointer)ainvoke时config{configurable: {thread_id: session_id}}工具调用后LLM 不生成回复流程卡在handle_tool_responsehandle_tool_response节点未返回messages字段或返回的messages格式错误1. 打印handle_tool_response的返回值2. 检查ToolMessage的tool_call_id是否与tool_calls中的id匹配handle_tool_response必须返回{messages: [...ToolMessage...]}ToolMessage.tool_call_id必须等于tool_calls[i][id]流式响应延迟高首字节超 2 秒Uvicorn 未启用--workers或llm.invoke()是同步阻塞调用1. 检查 Uvicorn 启动命令是否含--workers 42. 确认llm实例是否为ChatOpenAI异步而非OpenAI同步使用--workers NNCPU 核数确保 LLM 客户端是langchain_openai.ChatOpenAI其ainvoke/astream是异步的4.2 实战避坑技巧来自线上环境的 5 条硬经验技巧一永远用astream替代ainvoke做流式但ainvoke必须有超时保护ainvoke是阻塞式如果 LLM 服务卡住整个 FastAPI worker 会被占满。我在生产环境强制加了 30 秒超时try: final_state await asyncio.wait_for( graph_app.ainvoke(initial_state, configconfig), timeout30.0 ) except asyncio.TimeoutError: raise HTTPException(status_code504, detailLLM 服务超时)而astream是真正的异步流即使某个节点慢也能持续yield前端可显示“思考中...”。技巧二State字段命名要带业务前缀避免与 LangGraph 内部字段冲突LangGraph 会往State里注入一些内部字段如__metadata__。如果你的State里定义了messages、next、is_last这类通用名极易冲突。我的做法是所有业务字段加cs_前缀Customer Service如cs_messages、cs_ticket_id、cs_user

相关新闻