用 FastAPI 把 LangGraph Chatbot 服务化:从本地到生产的部署实战

发布时间:2026/10/20 8:34:08
用 FastAPI 把 LangGraph Chatbot 服务化:从本地到生产的部署实战 先说个我反复见到的场景很多开发者在本地 Notebook 里用 LangGraph 搭了一个 Chatbot演示效果很棒给同事看也没问题。但一旦要往生产走立刻卡住——没有 API 接口前端不知道怎么接没有鉴权没有日志两个用户同时提问图的状态互相污染。这些问题不是 LangGraph 本身的问题而是它只是一套“逻辑编排框架”不是一个“服务”。所以这篇实战教程要解决的就是如何用 FastAPI 把 LangGraph 机器人安全、稳定、可维护地暴露成 HTTP 服务。这篇内容适合两类人一类是刚把 LangGraph 的基本概念学完还没跑过完整服务的开发者另一类是做后端接入被迫去读 LangGraph 源码想快速搞清楚部署要点的同学。我会把项目结构、State 设计、节点逻辑、FastAPI 接口封装、流式输出、服务部署、压测与踩坑全部走一遍给出可以直接照着改的代码而不是那种“核心代码省略”的摆拍教程。1. 先聊清楚LangGraph 写好的机器人为什么还要包一层 FastAPI1.1 从 Notebook 到生产中间隔着一层“服务化”在 Notebook 里跑 LangGraph逻辑上就是一个函数调用你丢进去一个用户消息它把所有节点走完最后返回回答。这个过程本身不关心外部世界也不关心别人怎么调用它。但真实业务里Chatbot 必须被嵌进网页、App、客服工作台甚至企业内部 IM 工具。这些前端不会直接 import 你的 Python 代码大家只认 HTTP、WebSocket 这一套东西。所以“部署”这个问题的本质是把 LangGraph 从一个可以被 Python 调用的对象变成一个可以被任意语言请求的服务。FastAPI 在这里承担的是胶水层它负责接收请求、解析参数、调用图、把结果转成 JSON 或流式文本返回给调用方。这里有个常见的思路误区有人觉得 FastAPI 只是加了一个app.post(/chat)装饰器而已真正的工作量在 LangGraph 内部。我实际做完后的结论正好相反——LangGraph 的逻辑只要设计得清楚封装成 API 是非常快的反而是部署阶段的状态隔离、并发控制、错误返回这些“周边问题”占了整个项目的大部分时间。1.2 FastAPI 不是唯一选择但它是当前性价比最高的方案部署 Chatbot 的 API 层市面上有 Flask、Django、FastAPI、甚至直接用 LangServe 这类专用工具。LangServe 确实能和 LangGraph 无缝配合但它的定制能力在某些场景下不够灵活比如你想对请求做特殊的前置校验、想自定义流式协议格式、想和现有鉴权中间件整合时还是自己用 FastAPI 包一层更顺手。我选 FastAPI 的理由很直接原生 async 支持和 LangGraph 新版本的异步接口ainvoke、astream_events配合非常顺滑。Pydantic 做参数校验能省掉大量手写 if-else。自带的/docs接口在联调阶段省了不知道多少沟通成本前端同学自己点开页面就能看到请求格式。要注意的是“async”并不等于“所有代码都要写成 async”。LangGraph 的接口同时提供了invoke和ainvoke两种风格在 FastAPI 里也能用同步的def或异步的async def定义路由。这两者的选择直接影响并发表现后面我用专门一节来讲这个坑。2. LangGraph 的五个核心概念先建立正确心智模型2.1 StateGraph 是一张菜谱不是一条管道很多人第一次看 LangGraph 的示例代码以为它就是一个顺序执行的列表第一步调大模型第二步调工具第三步输出。这个理解在简单 demo 里是够用的但会让你在看条件分支时一头雾水。准确的比喻是这样StateGraph 是一张菜谱描述了做一道菜的各种可能路径。你可以选择先切菜还是先热锅取决于厨师看到当前食材之后的条件判断。菜谱本身是不变的但每次执行时走过的路径可能不同。在代码层面StateGraph接收一个 State 类型描述然后通过add_node注册处理函数通过add_edge和add_conditional_edges定义节点之间的走向。最后调用compile()得到一个 Runnable这个 Runnable 既能被直接调用也能被 FastAPI 包装成接口。2.2 State 是厨师手里的小本本LangGraph 里的 State 是整个图的灵魂。它不是简单的函数入参而是一个跨节点共享的、累积更新的数据对象。我习惯把它理解成厨师手里的小本本菜做到一半本子上记着“客人不吃香菜”“锅已经热了”“菜已经切好”。每个节点都在读这个小本本也可能往里追加新记录。节点之间不直接传参全部通过 State 协作。具体到代码State 是一个TypedDict字段可以是普通类型也可以用特殊的注解来定义“如何合并多个节点写入的值”。比如messages字段通常使用add_messages这个 reducer这样每个节点往 State 里添加的消息会被自动追加到列表尾部而不是覆盖掉之前的内容。这也是 LangGraph 和普通流程编排工具最大的区别它不是简单地把上一个函数的输出传给下一个函数而是维护一个完整的状态视图。多轮对话里的历史消息、已经收集到的用户信息、工具调用返回的结果都可以放在 State 里。2.3 Node、Edge、Conditional Edge 各自扮演的角色Node 是一个普通的 Python 函数输入是当前的 State 字典输出是一个字典表示“我要往小本本上追加的内容”。比如大模型节点读取 State 里的消息列表调用模型服务得到一个回答然后返回{messages: [AIMessage(...)]}。Edge 是一条固定路径告诉执行引擎“做完这个节点下一步去哪个节点”。大多数简单流程就是一条直线START - agent - tools - agent - END中间的循环靠 Edge 和 Conditional Edge 组合实现。Conditional Edge 是裁判它接收 State根据当前内容决定走哪条路。比如判断大模型返回的最后一条消息里有没有工具调用请求有就跳转去执行工具节点没有就直接结束。这个“有环”的设计是 Agent 类 Chatbot 的基础。它让机器人不是拿一次模型结果就交差而是能进入“思考 - 调用工具 - 得到结果 - 再思考”的循环直到模型认为自己已经能回答用户问题为止。2.4 Checkpointer多轮对话的记忆机制如果只有一个 StateGraph那么每次执行都是从空白状态开始。这对于单轮问答没问题但 Chatbot 必须记住用户上一句说了什么。LangGraph 给出的方案是 Checkpointer检查点。Checkpointer 的作用是保存每次执行结束时的 State 快照。当你下一次调用图时可以通过config里的thread_id指定从哪条会话历史继续。这样同一个 Graph 对象可以服务成千上万个用户只要他们在调用时带上自己的会话 ID。这个设计非常巧妙。它把“记忆”从提示词拼接的脏活中解放了出来每次调用只需要传入当前用户说的话之前的历史由框架自动从检查点恢复并合并进 State。在实际部署时默认的MemorySaver是把检查点存在内存里的进程一重启全丢。生产上如果不想丢失会话需要换用外部存储的 Checkpointer官方提供了基于 SQLite、Postgres 等的实现方案。这块后面部署部分会展开讲。2.5 什么时候该怀疑自己的图设计我见过很多新手把 LangGraph 的图画得极其复杂五六条并行链路、十几个条件分支结果第一次跑就完全失控。这里想分享一个经验标准如果你的 Chatbot 只有一个大模型调用、最多加一两个工具那图结构通常不超过三个节点。简单场景不要强行加循环。比如只做一个“查资料 - 总结回答”的流程顺序图就够了。只有当你需要模型自主决定“要不要继续调用工具”时才需要包含环的 Agent 架构。判断图设计是否合理的办法是问自己让一个人类执行这套流程时需要在什么时候做判断、需要哪些信息把这些判断点、信息流画出来图就自然成形了。3. 搭建一个可运行的 Chatbot从零开始的项目骨架3.1 需求场景一个能查资料、能查时间的客服机器人为了不让教程停留在概念层面我设计了一个具体场景某电商平台的客服机器人。它能回答商品知识库里的常见问题也能在用户问“现在几点”“今天星期几”时动态获取服务器当前时间。这个需求虽然简单但覆盖了 LangGraph 最常见的几个能力点多轮对话、知识库检索型工具、动态时间型工具、条件路由。整个项目的逻辑只有三种路径用户提问后模型直接回答模型觉得需要查知识库走检索工具后回答模型觉得需要知道当前时间走时间工具后回答。偶尔还有连续调用工具的情况比如用户先问“现在几点”再追问“这个时间店铺还营业吗”模型需要先后调用两个工具。这个规模非常适合作为实战起点它能让你看清 LangGraph 在真实项目中的样子又不会大到让人看不下去。3.2 项目结构入口、图、Schema、配置分层我习惯把项目拆成四个文件很小的项目也会这么做chatbot/ main.py # FastAPI 入口 agent.py # LangGraph 图定义 schemas.py # 请求和响应模型 config.py # 环境变量加载 requirements.txt这么分的原因很简单agent.py里不应该出现任何 FastAPI 装饰器它只负责构建一个可执行的 Runnable 对象这样以后即使换掉 API 层图的逻辑完全不用动。schemas.py单独放 Pydantic 模型方便联调时一眼看到请求格式。config.py读取密钥和模型底座配置避免在代码里硬编码。3.3 State 定义和节点实现先定义 State。这个项目的 State 非常简单但是包含了两个典型字段from typing import Annotated, TypedDict from langgraph.graph import add_messages class ChatState(TypedDict): messages: Annotated[list, add_messages] user_name: strmessages字段用add_messages注解保证多个节点返回的消息能自动追加合并。user_name字段是普通覆盖型字段用来演示“非累积”数据的处理。在节点层面我会把“调用模型”和“执行工具”分成两个节点。调用模型的节点读取当前所有消息把模型回复作为新消息追加进 Statefrom langchain_core.messages import HumanMessage, AIMessage from langchain_core.tools import tool tool def get_server_time() - str: 返回服务器当前的日期和星期几。 from datetime import datetime now datetime.now() return now.strftime(%Y-%m-%d %H:%M:%S %A) tool def search_knowledge_base(query: str) - str: 在客服知识库中检索与问题相关的条目。 kb { 退货: 支持七天无理由退货需要商品完好且包装齐全。, 发货: 付款后48小时内发货顺丰快递。, 保修: 电子类产品保修一年凭购买记录享受服务。, } for key, value in kb.items(): if key in query: return value return 知识库中没有直接匹配的答案请尝试转人工客服。需要强调一个细节tool装饰的这个函数最终是否被模型真正调用取决于模型服务商的工具调用能力。如果你使用的是不支持 Function Calling 的模型需要自己写解析逻辑判断模型输出文本里是否包含某个特定关键词。不过在主流大模型都支持工具调用协议的今天直接走框架的标准路径即可。接下来是两个节点和一个路由判断from langgraph.graph import StateGraph, START, END from langchain_openai import ChatOpenAI llm ChatOpenAI( api_keycfg.MODEL_API_KEY, base_urlcfg.MODEL_API_BASE, modelcfg.MODEL_NAME, ).bind_tools([get_server_time, search_knowledge_base]) def agent_node(state: ChatState): messages state[messages] response llm.invoke(messages) return {messages: [response]} def tools_node(state: ChatState): last_message state[messages][-1] tool_calls last_message.tool_calls results [] for call in tool_calls: tool_name call[name] tool_args call[args] if tool_name get_server_time: output get_server_time.invoke(tool_args) elif tool_name search_knowledge_base: output search_knowledge_base.invoke(tool_args) else: output f不认识的工具: {tool_name} results.append(ToolMessage(contentoutput, tool_call_idcall[id])) return {messages: results} def route_after_agent(state: ChatState): last_message state[messages][-1] if getattr(last_message, tool_calls, None): return tools return END这里有一个容易踩进去的坑必须用getattr(last_message, tool_calls, None)而不是直接访问last_message.tool_calls因为当模型决定直接回答用户时返回的消息对象上根本没有这个属性直接访问会引发 AttributeError。3.4 图的装配与编译把节点和边拼起来def build_graph(): graph StateGraph(ChatState) graph.add_node(agent, agent_node) graph.add_node(tools, tools_node) graph.add_edge(START, agent) graph.add_conditional_edges( agent, route_after_agent, {tools: tools, END: END} ) graph.add_edge(tools, agent) return graph.compile()这个图的执行逻辑是进入 agent 节点调大模型。如果模型返回需要调用工具的请求就走 tools 节点执行完工具后回到 agent 节点让模型基于工具结果生成最终回答。如果模型直接返回了文本就直接结束。编译后得到的对象本质上是一个 LangChain Runnable既支持同步调用也支持异步调用。千万不要在agent.py里调用compile()后立刻执行更好的做法是提供一个工厂函数让 FastAPI 层在需要时构建实例。这样测试时可以灵活地替换节点实现不会污染全局状态。3.5 关于知识库检索的取舍说明上面示例里的知识库用了字典匹配纯属演示。生产环境中替换成真正的向量检索时可以并行进行。如果你做了某次 POC 项目就用了一个轻量的文本检索实现把知识库文档切块后用 Embedding 模型转成向量维护在本地文件中。注意这里完全可以不用外部数据库因为它更适合在成熟的项目里引入专用向量库而初学者先从内存字典开始把整个 LangGraph 链路跑通再替换检索实现才是正确的节奏。替换检索实现时只需要修改search_knowledge_base这一个函数的内部逻辑图结构、API 层完全不用动。这种低耦合正是把工具独立成节点带来的好处。4. 用 FastAPI 把图包成 API三种接口风格怎么选4.1 普通问答接口POST /chat最常见的场景是用户发一条消息服务端跑完整个图后一次性返回完整回答。对应代码如下from fastapi import FastAPI from pydantic import BaseModel from langchain_core.messages import HumanMessage app FastAPI(titleChatbot API) class ChatRequest(BaseModel): message: str thread_id: str default class ChatResponse(BaseModel): reply: str thread_id: str graph build_graph() app.post(/chat) async def chat(req: ChatRequest): config {configurable: {thread_id: req.thread_id}} result await graph.ainvoke( {messages: [HumanMessage(contentreq.message)]}, config ) last_message result[messages][-1] return ChatResponse(replylast_message.content, thread_idreq.thread_id)这里有一个很多人忽略的点ainvoke里的输入字典里的messages不需要带上历史消息只要传当前用户输入就行。LangGraph 的 Checkpointer 会自动从 thread_id 对应的会话中恢复历史消息并和当前消息合并。如果你手动把历史消息一起传进去反而会出现重复累积。4.2 流式输出SSE 是 Chatbot 的标配现在很多 Chatbot 前端都要求打字机效果也就是用户消息发出去后页面上的文字一个 token 一个 token 地蹦出来。FastAPI 可以基于 StreamingResponse 实现 Server-Sent Events。LangGraph 的异步流式事件astream_events会返回非常细粒度的事件流包括模型调用前、模型调用后、每个 token 生成等。我们需要筛选出真正包含 token 内容的事件from fastapi.responses import StreamingResponse app.post(/chat/stream) async def chat_stream(req: ChatRequest): async def event_generator(): config {configurable: {thread_id: req.thread_id}} async for event in graph.astream_events( {messages: [HumanMessage(contentreq.message)]}, config, versionv1 ): if event[event] on_chat_model_stream: chunk event[data][chunk] if chunk.content: yield fdata: {chunk.content}\n\n return StreamingResponse( event_generator(), media_typetext/event-stream )前端的调用方式很简单用原生 fetch 就能处理这种格式const response await fetch(/chat/stream, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({message: 今天能发货吗, thread_id: user-123}) }); const reader response.body.getReader(); const decoder new TextDecoder(); while (true) { const {done, value} await reader.read(); if (done) break; const text decoder.decode(value); // 每一行都是 data: xxx直接追加到页面即可 }4.3 实时双向对话什么时候才需要 WebSocket如果你的 Chatbot 不是简单的“一问一答”而是需要支持服务器主动推送消息、用户随时打断、多轮上下文动态修正那就需要考虑 WebSocket。FastAPI 对 WebSocket 支持得很好LangGraph 的异步接口也可以直接在 WebSocket 处理器里使用。但我个人的建议是周到的做法是先用 SSE因为它足够覆盖大多数客服机器人场景。SSE 走标准 HTTP面向负载均衡、日志、缓存都有成熟的方案。WebSocket 是长连接部署时还需要考虑连接保持、断线重连、多实例会话路由问题复杂度上一个数量级。只有当产品经理明确提出了“打断回复”这类需求时再上 WebSocket不要提前为用不上的功能买单。4.4 健康检查与元信息接口部署到服务器之后运维第一件事就是探活。加一个/health接口成本极低价值却很大app.get(/health) async def health(): return {status: ok, model: cfg.MODEL_NAME}模型名要记得脱敏。我踩过公司内部把模型商内部代号暴露到公网探活接口的问题——看起来无关紧要其实给了别人刺探技术栈特征的机会。更稳妥的做法是返回{status: ok}即可。5. 部署到服务器那些新同学最容易踩的现实坑5.1 Uvicorn 启动参数workers 不是越多越好在服务器上启动服务大多数教程只会写一句uvicorn main:app --host 0.0.0.0 --port 8000。但这只适用于本地调试真正线上要注意两个参数workers 和 timeout。如果是两核四线程的小机器我建议先从一个进程开始。Chatbot 的瓶颈通常不在 Python 代码本身而是模型 API 的响应速度和并发限制。当ainvoke抛出 429 限流错误时不是你的服务进程不够多而是上游模型服务商认为你在短时间内的请求太多了。更合理的做法是先根据模型服务的限流要求推算 QPS 上限再反推需要的 worker 数。比如模型 API 允许每分钟 60 次请求你的单次请求平均 2 秒那么一个 worker 每分钟最多处理约 30 个请求对应需要 2 个 worker。启动命令示例uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2 --timeout-keep-alive 305.2 进程守护与开机自启直接用nohup启动服务不适合长期运行进程一挂就没人拉起来了。我通常用一个 systemd 服务来托管配置文件可以存成/etc/systemd/system/chatbot.service[Unit] DescriptionChatbot API Afternetwork.target [Service] Userwww-data WorkingDirectory/opt/chatbot EnvironmentFile/opt/chatbot/.env ExecStart/opt/chatbot/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 --workers 2 Restartalways RestartSec3 [Install] WantedBymulti-user.target注意这里--host 127.0.0.1而不是0.0.0.0。推荐的做法是服务只监听本机回环地址再通过 Nginx 反向代理对外提供服务。这样请求先经过 Nginx再进 Uvicorn既方便统一配置 HTTPS 证书又能在 Uvicorn 前面加一层限流和访问日志。5.3 密钥管理与环境变量我见过不止一个项目把模型 API 的密钥直接硬编码在config.py里然后整个仓库传上代码托管平台结果密钥被爬虫扫走一夜之间产生大额账单。这是所有部署环节里最要命的问题。正确的做法是把所有敏感配置放进环境变量项目里只存在一个.env.example文件作为配置模板MODEL_API_KEYsk-xxxx MODEL_API_BASEhttps://your-model-provider.example MODEL_NAMEyour-chat-model-name.env文件要写进.gitignore。代码里用os.getenv读取缺失时直接报错中断启动不要给默认空值否则服务会在毫不知情的情况下跑在一个没有密钥的状态里。配置读取代码可以非常硬核import os class Config: MODEL_API_KEY os.getenv(MODEL_API_KEY, ) MODEL_API_BASE os.getenv(MODEL_API_BASE, ) MODEL_NAME os.getenv(MODEL_NAME, default-chat-model) classmethod def validate(cls): if not cls.MODEL_API_KEY: raise RuntimeError(MODEL_API_KEY is not set)然后在构建图之前调用一次Config.validate()让服务启动即失败而不是请求时失败。5.4 多实例下的状态一致性如果你的服务是多个 Uvicorn worker 或多台机器默认的内存 Checkpointer 就会出问题用户第一次请求被 worker A 处理第二次被 worker B 处理B 的内存里没有 A 的会话状态LangGraph 会当作新会话重新开始。这个问题最容易发生在 Nginx 轮询分发请求的场景而且症状非常诡异——用户第二句话的上下文服务端完全不知道。两种解法一是用 Nginx 的 IP hash 或sticky session让同一个用户始终落到同一个 worker但这在机器重启、动态扩缩容时依然会失效二是把 Checkpointer 换成外部共享存储比如 SQLite 文件或者更猛一点的方案直接换用官方提供的基于 Postgres 的实现。多实例部署时务必选择后者。如果你在使用持久化的 Checkpointer别忘了同时配置好数据库的连接池容量否则高并发时数据库连接会成为新的瓶颈。6. 测试与压测别让“本地正常”骗了你6.1 三个必须有测试覆盖的用例第一个是单轮问答不带 thread_id 的历史依赖直接问一个问题断言返回内容非空。第二个是多轮问答两次请求使用相同 thread_id第二次提问时要能在回答中感受到第一次提问的上下文存在。第三个是工具调用问一个必须触发工具的问题比如“现在是几点”断言最终回答内容和工具返回值有对应关系。如果没有工具调用结果说明你的模型服务商工具调用协议没配对。这三个测试用例不需要很完善它们的作用只是在每次改动图逻辑后快速回归防止你某天把条件路由写坏而不自知。6.2 模拟并发压测压测不是看服务能不能扛住而是看服务 QPS 上去后错误率是否保持在一个可接受范围。我常用的一个很简单的小脚本import asyncio import httpx async def single_request(client, i): try: resp await client.post( http://127.0.0.1:8000/chat, json{message: 你好, thread_id: fuser-{i}}, timeout30.0 ) return resp.status_code except httpx.ReadTimeout: return 408 async def main(): async with httpx.AsyncClient() as client: tasks [single_request(client, i) for i in range(200)] results await asyncio.gather(*tasks) success results.count(200) print(fsuccess: {success}/200) asyncio.run(main())并发 200 个请求不是目的真正的价值是观察服务在并发场景下会不会因为锁、连接池、Checkpointer 竞争而出现意外的 500。有一次我把内存 Checkpointer 换成了 SQLite 版本发现默认的 SQLite 连接不允许跨线程共享LangGraph 在多请求并发时直接抛异常压测脚本帮我立刻定位了问题。如果不做这步测试线上的故障反馈周期会慢得多。6.3 日志与可观测性FastAPI 默认的日志只有访问日志看不到 LangGraph 内部每个节点的耗时和中间结果。我的做法是给图节点包裹一个带日志的装饰器把节点名、输入摘要、耗时打印出来def logged_node(name: str): def decorator(func): def wrapper(state: ChatState): start time.time() result func(state) elapsed time.time() - start logger.info(fnode{name} elapsed{elapsed:.3f}s) return result return wrapper return decorator这个装饰器用在add_node时会给你带来非常直观的性能画像。哪个节点耗时异常一目了然。错误返回方面FastAPI 里可以用全局异常处理器捕获未预期的异常from fastapi.responses import JSONResponse app.exception_handler(Exception) async def unhandled_exception_handler(request, exc): logger.exception(Unhandled error: %s, exc) return JSONResponse( status_code500, content{detail: internal server error, error_type: type(exc).__name__} )注意不要在异常响应里直接回传异常对象字符串这既可能泄露路径或密钥信息也是不必要的敏感信息暴露。7. 跑完整个链路我最想让你记住的东西7.1 图、API、部署是三套心智做这个项目最大的感受是你实际上同时在用三套心智模型设计图时考虑的是状态流转和条件路由写 API 层考虑的是请求校验、响应格式和流式协议部署时考虑的是进程、密钥、会话状态一致性。这三件事如果混在一起思考会被来回切换的成本拖垮。比较有效率的方式是先把图在本地单独调通再包 API最后才上服务器。每完成一个阶段就测试一遍不要指望一次性把所有层都写完再启动那样排查问题时会分不清问题到底来自哪一层。7.2 几个让我少掉头发的实操细节模型消息对象的tool_calls属性在模型直接回答时不存在一定要用getattr提前防护这一步能帮你避开线上 95% 的随机报错。流式接口和普通接口不要用同一个处理函数因为astream_events产生的事件里包含大量工具调用的内部 token如果不过滤直接推给前端页面会出现一堆类似tool_call的奇怪内容。部署后第一时间用/health探活然后用最真实的业务请求跑一遍完整链路。很多问题只有部署到新环境后才会暴露比如模型服务的base_url端口在你的服务器上根本不通但本地开发时因为走了某个代理而一切正常。7.3 一个很少被提到的隐藏技巧最后分享一个我在压测阶段发现的做法在压测脚本里主动构造一个工具调用失败的场景——比如让知识库检索工具抛异常然后观察整个图的行为。正常情况会直接 500但如果你希望机器人优雅地回复“暂时无法提供该信息”可以在tools_node里捕获异常返回一条ToolMessage内容是一个兜底文案。因为工具节点执行完后还会回到 agent 节点模型会基于这个兜底文案组织回答用户感知到的就是一个礼貌的异常提示而不是接口报错。这个模式我称之为“工具降级”它能让 Chatbot 在生产环境中显得比很多大厂产品还稳定。真正上线前建议把所有工具都试着打一遍异常确保每个可能的故障点都有相应的兜底文案。到这里这条从 LangGraph 到 FastAPI 再到服务器的完整链路就走通了。剩下的工作就是把你自己的业务工具、知识库、提示词模板替换进来把代码里的小写演示文案换成真实内容。跑一次生产环境的链路回过来看这篇教程你会发现所谓“部署”最难的其实不是框架而是对状态的理解和对边界的处理。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询