FastAPI+LangGraph构建可生产AI工作流的工程实践

📅 2026/7/14 7:44:40
FastAPI+LangGraph构建可生产AI工作流的工程实践
1. 项目概述为什么用 FastAPI LangGraph 搭建 AI 工作流而不是直接写个 Python 脚本“Building AI Workflows with FastAPI and LangGraph (Step-by-Step Guide)”这个标题乍看是技术组合拳但背后藏着一个非常现实的工程判断当你的 AI 应用从“能跑通”迈向“可交付、可维护、可协作、可监控”时单靠if-elif-else链或手写状态机就彻底失灵了。我做过 7 个以上落地到生产环境的 AI 服务最早那几个就是纯 Python 脚本封装成 Flask 接口——结果上线第三周客户提了个需求“能不能让模型先判断用户问题是否涉及时效性如果是再调用天气 API 补充上下文最后才进 RAG 流程”——我盯着那 300 行嵌套try/except和硬编码的time.sleep(2)发了十分钟呆最终重写了整个调度逻辑。FastAPI 不只是“比 Flask 快一点”的替代品它是为现代 AI 工程设计的协议层自动 OpenAPI 文档、原生异步支持、依赖注入机制、Pydantic v2 的强类型校验——这些不是锦上添花而是防止你在第 5 个客户定制需求时把request.body()解析错两次的底层护栏。LangGraph 更不是“又一个图计算库”它是把 LangChain 的链式思维Chain升级为有向无环图DAG的工程范式转换器。你不再需要在RunnableSequence里塞 12 个.with_config(...)来绕过中间节点失败而是用StateGraph显式定义“如果 LLM 输出含关键词‘退款’跳转到客服策略节点否则进入知识库检索节点”。这种显式状态流转让 QA 同学能对着流程图直接提测试用例运维同学能基于节点耗时做熔断配置法务同事能指着某个human_review_node说“这里必须加审计日志”。标题里的“Step-by-Step”不是教学套路而是强调每个步骤都对应一个真实踩坑现场比如第二步选StateGraph而非MessageGraph是因为后者强制所有节点输入输出都是BaseMessage而我们实际业务中 60% 的节点要处理结构化 JSON如订单 ID、用户画像标签硬转消息对象会导致 Pydantic 模型校验失效再比如第五步的checkpointer必须用 Redis 而非内存是因为某次灰度发布时3 台实例共享同一个内存检查点导致用户 A 在实例 1 提交的多轮对话状态在实例 2 上被覆盖成用户 B 的会话——这种故障不会出现在本地调试中只有压测流量打散到多实例时才爆发。所以这篇指南的每一步本质都是把血泪教训翻译成可复现的代码段落。2. 核心架构设计与技术选型逻辑为什么是 FastAPI LangGraph而不是其他组合2.1 FastAPI 的不可替代性不只是快而是“可验证、可演进、可协同”很多人第一反应是“我用 Flask 也能 return JSON何必换”——这就像问“我用手摇咖啡机也能萃取咖啡何必买半自动”关键不在萃取动作本身而在整套工作流的可控性。FastAPI 的核心价值体现在三个刚性需求上第一接口契约的机器可读性。AI 工作流的前端Web/App、后端调度服务、下游向量库/LLM 网关往往由不同团队维护。如果用 Flask 手写jsonify({result: xxx, error_code: 400})前端同学只能靠文档截图猜字段含义而 FastAPI 基于 Pydantic 的BaseModel定义自动生成符合 OpenAPI 3.0 规范的交互式文档。实操中我们曾用 Swagger UI 让产品同学直接在/docs页面发起请求实时看到input_schema中user_query: str Field(..., min_length2, max_length500)的约束效果当场否决了“允许空查询触发默认推荐”的需求——这种即时反馈比开三次评审会高效得多。更重要的是这份 OpenAPI 文档可直接生成 TypeScript 客户端 SDK用openapi-typescript前端调用时连response.data.result的类型提示都有避免了response.get(resutl)这类低级拼写错误。第二异步 I/O 的零成本接入。AI 工作流的瓶颈从来不在 CPU而在等待等向量库返回相似结果平均 320ms、等 LLM API 响应平均 1.8s、等外部系统回调如支付网关确认。FastAPI 原生支持async def路由且其事件循环与httpx.AsyncClient、aioredis等异步库无缝兼容。我们对比过同步阻塞方案用requests.get调用向量库QPS 卡在 42换成httpx.AsyncClient FastAPI 异步路由同样硬件下 QPS 提升至 217。这不是理论值——在某电商大促期间实时商品推荐工作流峰值 QPS 达 189所有节点意图识别→库存校验→个性化排序→文案生成均异步执行平均延迟稳定在 1.2s 内。而 Flask 的同步模型需依赖 Gunicorn 多 worker但每个 worker 仍会因 I/O 阻塞而闲置资源利用率不足 35%。第三依赖注入的工程解耦能力。AI 工作流常需动态切换组件开发环境用FakeLLM模拟响应测试环境对接Ollama生产环境走Azure OpenAI。FastAPI 的Depends()机制让这种切换变成配置项。例如定义async def get_llm_client( config: Annotated[Settings, Depends(get_settings)] ) - BaseLLM: if config.env dev: return FakeLLM() elif config.env prod: return AzureChatOpenAI( azure_deploymentconfig.llm_deployment, api_version2023-05-15 )路由中直接声明llm: BaseLLM Depends(get_llm_client)无需修改任何业务逻辑。我们曾用此机制在 15 分钟内完成从本地 Ollama 到 Azure 的全链路切换而 Flask 方案需在每个路由函数里手动if-else初始化客户端漏掉一个节点就会导致服务雪崩。提示别被“Fast”字面意思误导——它的核心优势是开发体验和工程健壮性而非单纯吞吐量。如果你的场景是单机离线批量处理用concurrent.futures.ProcessPoolExecutor可能更合适但凡涉及 HTTP 对接、多环境部署、跨团队协作FastAPI 就是事实标准。2.2 LangGraph 的范式革命从“链式调用”到“状态驱动图”LangChain 的RunnableSequence解决了“顺序执行”问题但 AI 工作流的真实世界是分支、循环、条件跳转、状态共享。LangGraph 的StateGraph正是为此而生。我们拆解三个关键设计选择为什么选StateGraph而非MessageGraphMessageGraph强制所有节点输入输出为List[BaseMessage]这是为纯对话场景设计的。但实际业务中工作流常需传递结构化数据比如电商客服工作流节点 A 输出{order_id: ORD-789, status: pending}节点 B 需基于order_id查询数据库节点 C 需将status与用户情绪分析结果合并生成响应。若强行塞进BaseMessage需反复json.loads(message.content)且 Pydantic 模型校验失效。StateGraph允许自定义状态类class WorkflowState(TypedDict): user_query: str order_id: Optional[str] retrieval_results: List[Document] final_response: str needs_human_review: bool每个节点函数签名明确为def node_a(state: WorkflowState) - WorkflowStateIDE 能自动补全字段类型安全直接提升 300% 开发效率。为什么用add_conditional_edges而非硬编码if-else条件分支是 AI 工作流的高频需求。LangGraph 将分支逻辑与执行逻辑分离add_conditional_edges(llm_node, route_to_next_node)中的route_to_next_node函数只负责返回下一个节点名如retrieval_node或human_review_node不包含任何业务代码。这意味着你可以单独单元测试路由逻辑def test_route_to_next_node(): # 模拟 LLM 输出含敏感词 state {user_query: 我要投诉你们的客服, needs_human_review: True} assert route_to_next_node(state) human_review_node而传统方案中分支逻辑散落在各节点内部测试需启动完整工作流耗时且不稳定。为什么必须配checkpointerAI 工作流的“状态”不是临时变量而是业务资产。用户多轮对话中第 3 轮提问可能依赖第 1 轮的实体识别结果。LangGraph 的checkpointer将状态持久化到 Redis/PostgreSQL确保实例重启后用户会话不丢失多实例部署时同一用户请求路由到任意实例都能恢复状态可回溯任意时间点的状态快照用于问题排查。我们曾用 Redis checkpointer 定位一个诡异 Bug用户反馈“第 5 次提问时答案突然变短”查日志发现是某节点未正确更新final_response字段checkpointer 的历史快照显示该字段在第 4 轮后被置空直接定位到问题节点。注意LangGraph 的MemorySaver内存检查点仅适用于单实例开发环境。生产必须用RedisSaver或PostgresSaver否则多实例下状态竞争会导致数据错乱。我们吃过亏——灰度期用MemorySaver3 台实例共享内存用户 A 的会话状态被用户 B 覆盖导致客服误将退货请求当作咨询处理。2.3 组合的化学反应FastAPI 的“协议层” LangGraph 的“执行层”FastAPI 和 LangGraph 的结合不是简单叠加而是形成分层架构FastAPI 层协议层负责 HTTP 协议解析、请求校验、响应序列化、错误标准化统一返回{code: 500, message: LLM timeout}、CORS 配置、JWT 认证。它像交通警察只管“谁可以进来、带什么证件、往哪条路走”不管车里运什么货。LangGraph 层执行层专注业务逻辑编排。它接收 FastAPI 解析后的干净数据如WorkflowRequestPydantic 模型在StateGraph中流转调用 LLM、向量库、数据库等“货物”最终输出结构化结果。它像物流调度中心只管“这批货怎么分拣、哪些要人工复核、哪些直送客户”。这种分层让团队协作清晰前端工程师只关心 FastAPI 的 OpenAPI 文档算法工程师在 LangGraph 图中增删节点无需碰 HTTP 代码运维工程师只需监控 FastAPI 的/health接口和 LangGraph 的节点耗时指标。我们曾用此架构支撑某金融客户的智能投顾工作流算法团队在两周内新增“监管合规检查”节点调用规则引擎 API全程未改动 FastAPI 的任何一行路由代码上线零故障。3. 实操全流程详解从零搭建可运行的 AI 工作流3.1 环境准备与依赖安装避开版本地狱的 3 个关键点AI 工具链的版本冲突是最大隐形杀手。我们实测过 17 种组合最终锁定以下版本2024 年 10 月验证# 创建隔离环境强烈建议 python -m venv ai_workflow_env source ai_workflow_env/bin/activate # Linux/Mac # ai_workflow_env\Scripts\activate # Windows # 安装核心依赖注意顺序和版本 pip install fastapi[all]0.115.0 # all 包含 uvicorn、pydantic pip install langgraph0.2.50 # 必须 0.2.45修复了 checkpointer 并发 bug pip install langchain-core0.2.29 # 与 langgraph 0.2.50 兼容 pip install redis4.6.0 # Redis clientcheckpointer 依赖 pip install httpx0.27.0 # 异步 HTTP 客户端关键避坑点不要用pip install langchainLangChain 官方包已转向langchain-community但 LangGraph 依赖的是精简的langchain-core。装全量langchain会导致langgraph的StateGraph导入失败报ModuleNotFoundError: No module named langchain.schema因为新版本路径已改为langchain_core.schema。Redis 版本必须 4.5.0LangGraph 的RedisSaver在 4.4.x 中存在连接池泄漏 Bug高并发下 Redis 连接数暴增最终触发maxclients限制。我们曾因此在压测中遭遇 100% 请求超时降级到 4.6.0 后恢复正常。Uvicorn 启动参数必须加--workers 4FastAPI 默认单进程但 LangGraph 的checkpointer在多 worker 下需共享 Redis 连接。若不指定 workersUvicorn 会启动多个独立进程每个进程创建自己的 Redis 连接导致状态不同步。正确启动命令uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --reload实操心得每次新建项目先运行pip list | grep -E (fastapi|langgraph|redis)确认版本比事后 debug 节省 3 小时。我们把版本检查写成 pre-commit hook提交前自动校验。3.2 定义工作流状态与节点用 TypedDict 构建类型安全的“数据高速公路”LangGraph 的StateGraph要求明确定义状态结构。我们摒弃了字符串键名的松散模式采用TypedDictAnnotated实现强类型from typing import TypedDict, List, Optional, Annotated from langgraph.graph import StateGraph, START, END from langchain_core.documents import Document class WorkflowState(TypedDict): # 必填基础字段 user_query: Annotated[str, 用户原始输入长度 2-500 字符] session_id: Annotated[str, 唯一会话 ID用于 checkpointer 关联] # 可选中间结果字段按需填充 intent: Annotated[Optional[str], 意图识别结果如 refund, tracking] order_id: Annotated[Optional[str], 从用户查询中提取的订单号] retrieval_results: Annotated[List[Document], 向量库检索返回的文档列表] llm_response: Annotated[Optional[str], 大模型生成的原始响应] final_response: Annotated[str, 最终返回给用户的结构化响应] needs_human_review: Annotated[bool, 是否需人工审核默认 False] # 初始化图 workflow StateGraph(WorkflowState)每个字段的工程意义session_id是 checkpointer 的主键必须全局唯一。我们用uuid.uuid4().hex[:12]生成而非时间戳防并发重复。retrieval_results类型为List[Document]而非List[dict]确保后续节点可直接调用doc.page_content避免反复json.loads。needs_human_review设为bool而非str强制节点逻辑二元判断杜绝true/True/yes等字符串歧义。节点函数编写规范每个节点必须是纯函数无副作用输入WorkflowState输出WorkflowState的子集只更新需变更的字段def intent_recognition_node(state: WorkflowState) - WorkflowState: 意图识别节点用轻量模型判断用户问题类型 # 模拟调用本地意图分类模型实际可用 spaCy 或 ONNX 模型 if 退款 in state[user_query] or return in state[user_query].lower(): intent refund order_id extract_order_id(state[user_query]) # 自定义提取函数 else: intent general order_id None return { intent: intent, order_id: order_id, # 注意不返回未变更的字段LangGraph 会自动合并 } # 注册节点 workflow.add_node(intent_node, intent_recognition_node)提示节点函数名必须与add_node第一参数一致否则add_edge时会报Node not found。我们曾因intent_nodevsintent_recognition_node命名不一致调试 2 小时才发现。3.3 构建状态图与条件分支用add_conditional_edges实现业务逻辑可视化LangGraph 的核心是add_conditional_edges它将“判断逻辑”与“执行逻辑”解耦。以电商客服工作流为例def route_after_intent(state: WorkflowState) - str: 路由函数根据意图决定下一步 if state[intent] refund: return refund_check_node # 进入退款专用流程 elif state[intent] tracking: return tracking_node # 进入物流查询流程 else: return retrieval_node # 默认进入知识库检索 # 添加条件边注意第一个参数是源节点名第二个是路由函数 workflow.add_conditional_edges( intent_node, route_after_intent, { refund_check_node: refund_check_node, tracking_node: tracking_node, retrieval_node: retrieval_node } )关键细节route_after_intent函数必须返回字符串节点名不能返回None或数字。我们曾返回0导致图构建失败错误信息晦涩TypeError: unhashable type: int。字典映射中的键必须与route_after_intent的返回值完全匹配包括大小写。Refund_Check_Node和refund_check_node被视为不同节点。END节点需显式添加workflow.add_edge(final_response_node, END)否则工作流永不结束。完整图构建示例# 添加所有节点 workflow.add_node(intent_node, intent_recognition_node) workflow.add_node(refund_check_node, refund_validation_node) workflow.add_node(retrieval_node, vector_retrieval_node) workflow.add_node(llm_generate_node, llm_generation_node) workflow.add_node(final_response_node, format_final_response) # 添加边 workflow.add_edge(START, intent_node) workflow.add_conditional_edges(intent_node, route_after_intent, {...}) workflow.add_edge(retrieval_node, llm_generate_node) workflow.add_edge(llm_generate_node, final_response_node) workflow.add_edge(final_response_node, END) # 编译图必须 app workflow.compile(checkpointerRedisSaver.from_url(redis://localhost:6379/0))实操心得compile()是关键步骤它验证图结构并生成可执行对象。未调用compile()直接运行会报AttributeError: StateGraph object has no attribute ainvoke。我们把compile()放在main.py底部并加注释# 图编译必须在所有 add_node/add_edge 之后。3.4 FastAPI 路由集成将 LangGraph 工作流暴露为 RESTful 接口FastAPI 路由需完成三件事接收请求、调用工作流、返回响应。我们采用BackgroundTasks处理长耗时工作流避免 HTTP 超时from fastapi import FastAPI, BackgroundTasks, HTTPException, status from pydantic import BaseModel from typing import Dict, Any class WorkflowRequest(BaseModel): user_query: str session_id: Optional[str] None # 前端可传不传则服务端生成 class WorkflowResponse(BaseModel): session_id: str final_response: str status: str success app FastAPI(titleAI Workflow Service) app.post(/v1/workflow, response_modelWorkflowResponse) async def run_workflow( request: WorkflowRequest, background_tasks: BackgroundTasks ): # 1. 生成 session_id若未提供 session_id request.session_id or uuid.uuid4().hex[:12] # 2. 构建初始状态 initial_state WorkflowState( user_queryrequest.user_query, session_idsession_id, final_response, # 初始化 needs_human_reviewFalse ) # 3. 异步调用 LangGraph 工作流 try: # 注意ainvoke 是异步方法需 await result await app.ainvoke( inputinitial_state, config{configurable: {thread_id: session_id}} # checkpointer 关键配置 ) return WorkflowResponse( session_idsession_id, final_responseresult[final_response] ) except Exception as e: # 统一异常处理 raise HTTPException( status_codestatus.HTTP_500_INTERNAL_SERVER_ERROR, detailfWorkflow execution failed: {str(e)} )关键配置说明config{configurable: {thread_id: session_id}}是 checkpointer 的生命线。thread_id必须与session_id一致否则无法恢复会话状态。ainvoke是异步调用必须await。若用invoke同步Uvicorn 事件循环会被阻塞QPS 归零。BackgroundTasks在此处非必需因ainvoke已异步但预留扩展空间如需在工作流后发送 Slack 通知可background_tasks.add_task(send_slack_alert, result)。注意FastAPI 的response_model会自动校验返回数据结构。若final_response_node返回的final_response是None会触发ValidationError需在节点中确保必填字段不为空。3.5 Redis Checkpointer 配置与监控让状态持久化真正可靠LangGraph 的RedisSaver需精细配置否则高并发下易出错from langgraph.checkpoint.redis import RedisSaver import redis # 生产级 Redis 连接配置 redis_client redis.Redis( hostlocalhost, port6379, db0, decode_responsesTrue, # 自动解码 bytes 为 str socket_connect_timeout5, # 连接超时 5s socket_timeout10, # 读写超时 10s retry_on_timeoutTrue, # 超时自动重试 health_check_interval30 # 每 30s 健康检查 ) # 初始化 checkpointer checkpointer RedisSaver(redis_client) # 编译图时传入 app workflow.compile(checkpointercheckpointer)监控与故障排查Redis 内存监控每个会话状态默认保存 10 轮历史每轮约 5KB1000 个活跃会话占 50MB。我们设置 Redismaxmemory 2gbmaxmemory-policy allkeys-lru防内存溢出。状态恢复验证在final_response_node中添加日志def final_response_node(state: WorkflowState) - WorkflowState: logger.info(fSession {state[session_id]} state keys: {list(state.keys())}) return {final_response: fHello! Your order {state.get(order_id, N/A)} is {state.get(intent, unknown)}}故障模拟手动删除 Redis 中某thread_id的 key再请求同一session_id观察是否重建新会话应有日志No checkpoint found for thread_id...。实操心得在docker-compose.yml中为 Redis 单独配置restart: unless-stopped并挂载redis.conf启用save 60 1000每 60 秒至少 1000 次写入则持久化避免容器重启丢状态。4. 常见问题与实战排障那些文档里不会写的血泪教训4.1 工作流卡死/无限循环3 步定位法LangGraph 工作流最恐怖的 Bug 是“静默卡死”——HTTP 请求无响应日志无报错CPU 占用率 100%。我们总结出三步定位法第一步检查add_edge是否形成闭环常见错误是误将END当作普通节点# ❌ 错误END 是保留字不能作为源节点 workflow.add_edge(final_response_node, END) # 正确 workflow.add_edge(END, intent_node) # ❌ 导致无限循环第二步验证route_to_next_node是否总返回有效节点名路由函数若返回None或不存在的节点名LangGraph 会静默忽略工作流停滞def buggy_route(state: WorkflowState) - str: if state[user_query].startswith(A): return node_a # ❌ 缺少 elsePython 默认返回 None # ✅ 修复必须覆盖所有分支 def fixed_route(state: WorkflowState) - str: if state[user_query].startswith(A): return node_a else: return default_node # 或抛出异常第三步启用 LangGraph 调试日志在启动前添加import logging logging.getLogger(langgraph).setLevel(logging.DEBUG)日志中会出现Executing node intent_node、Transitioning to retrieval_node等记录。若卡在某节点后无日志说明该节点函数陷入死循环如无限while True或网络超时。提示在节点函数开头加logger.debug(fEntering {node_name} with state keys: {list(state.keys())})可快速确认是否进入节点。4.2 Checkpointer 失效Redis 连接与线程 ID 的双重陷阱Checkpointer 失效表现为“每次请求都新建会话历史状态丢失”。原因通常有两个陷阱一thread_id与session_id不一致前端传session_idabc123但config中写thread_iddef456# ❌ 错误thread_id 硬编码 await app.ainvoke(inputstate, config{configurable: {thread_id: def456}}) # ✅ 正确动态绑定 await app.ainvoke(inputstate, config{configurable: {thread_id: state[session_id]}})陷阱二Redis 连接被多线程共享Uvicorn 的--workers 4启动 4 个进程若RedisSaver使用全局单例连接会导致连接竞争。正确做法是每个 worker 初始化独立连接# ✅ 在每个 worker 中初始化放在 app startup event app.on_event(startup) async def startup_event(): global checkpointer checkpointer RedisSaver.from_url(redis://localhost:6379/0) app workflow.compile(checkpointercheckpointer)验证方法在 Redis CLI 中执行KEYS *应看到类似checkpoint:abc123:12345的 keyabc123是 session_id。若 key 为空说明 checkpointer 未生效。4.3 FastAPI 与 LangGraph 类型冲突Pydantic v2 的隐性战争FastAPI 依赖 Pydantic v2而旧版 LangChain 使用 v1类型不兼容会导致ValidationError。典型症状是 POST 请求返回422 Unprocessable Entity错误信息含value is not a valid dict。解决方案强制统一 Pydantic 版本pip install pydantic2.0,3.0 # 然后重装 langgraph它会自动适配 pip install --force-reinstall langgraph0.2.50根本原因Pydantic v2 的BaseModel默认extraforbid而 LangGraph 的StateGraph内部使用dict存储状态。若WorkflowState定义了user_query: str但initial_state中多传了timestamp: int字段v2 会严格拒绝。解决方法是在WorkflowState中显式允许额外字段class WorkflowState(TypedDict): user_query: str session_id: str # 其他字段... # 允许额外字段Pydantic v2 兼容 def __init__(self, **kwargs): super().__init__(**kwargs)或更稳妥地在 FastAPI 的WorkflowRequest中严格限定字段不传多余参数。4.4 性能瓶颈诊断从 200ms 到 2s 的延迟真相工作流平均延迟突增常见原因及排查命令现象可能原因诊断命令解决方案所有节点延迟均匀增加Redis 连接池耗尽redis-cli info clients | grep connected_clients1000 需扩容增加max_connections200到 RedisSaverllm_generate_node延迟飙升LLM API 限流curl -v https://api.openai.com/v1/chat/completions测速配置httpx.AsyncClient(timeout30.0)retrieval_node延迟波动向量库负载不均kubectl top pods -n vector-dbK8s增加向量库副本加负载均衡首次请求慢5sUvicorn worker 启动慢time uvicorn main:app --workers 1预热启动后立即curl http://localhost:8000/health终极性能工具在main.py中添加 Prometheus 指标from prometheus_fastapi_instrumentator import Instrumentator Instrumentator().instrument(app).expose(app)访问/metrics查看http_request_duration_seconds_bucket精准定位慢请求区间。实操心得我们给每个节点加耗时日志import time def node_with_timer(node_func): async def wrapper(state: WorkflowState): start time.time() result await node_func(state) duration time.time() - start logger.info(fNode {node_func.__name__} took {duration:.3f}s) return result return wrapper上线后发现intent_node平均 800ms远超预期定位到是正则表达式未编译修复后降至 12ms。5. 生产就绪增强认证、监控、灰度与可观测性5.1 JWT 认证集成保护工作流不被滥用AI 工作流是计算密集型服务必须防刷。FastAPI 的 JWT 认证可无缝集成from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import JWTError, jwt security HTTPBearer() app.post(/v1/workflow) async def run_workflow( request: WorkflowRequest, credentials: HTTPAuthorizationCredentials Depends(security) ): try: payload jwt.decode( credentials.credentials, SECRET_KEY, algorithms[HS256] ) user_id payload.get(sub) if not user_id: raise HTTPException(status_code401, detailInvalid token) except JWTError: raise HTTPException(status_code401, detailInvalid token) # 继续工作流...生产要点SECRET_KEY必须从环境变量读取绝不可硬编码。Token 中嵌入scope字段如scope: [workflow:read]实现细粒度权限控制。用joserfc替代python-jose后者已停止维护支持RS256非对称加密。5.2 日志结构化与链路追踪用 OpenTelemetry 连接 FastAPI 与 LangGraph分布式追踪是排查跨服务问题的关键。我们用 OpenTelemetry 实现全链路from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter # 初始化 tracer