
前几天在社区里有人甩给我一句话AI Agent 已经聊了两年能聊明白的人很多能把它放到生产环境里稳定跑起来的人很少。这话有点扎心但确实是现状。我自己跟 Agent 打了大半年交道从最开始在 Jupyter Notebook 里调 API到后来用 FastAPI 把 Agent 包成服务给业务方调用中间踩过无数个坑。回头总结下来真正决定一个 Agent 能不能落地的不是某个模型的推理能力有多强而是你有没有把它的“七要素”想清楚以及在七个关键决策点上做了正确选择。这篇文章想把这两个框架讲透。它不是概念科普而是工程视角的拆解先看 Agent 由哪些零件组成再看从 Demo 到生产你要拍板哪些事最后给一套可以直接抄作业的 FastAPI LangGraph 实现思路。适合后端工程师、AI 应用开发者以及那些已经跑通原型、正准备把它推上线的团队看。1. 七要素拆解Agent 的工程骨架1.1 Agent 不是“提示词 API”而是一条感知-决策-执行闭环很多人第一次接触 Agent以为它就是把提示词写长一点然后循环调用大模型。这个理解不算错但离工程实现差得很远。如果只做一次“模型调用 返回结果”那叫单轮问答不叫 Agent。Agent 的本质是一个闭环拿到的用户请求先进入上下文模型基于当前状态给出下一步动作动作可能是一个回答也可能是一个工具调用工具返回结果后再喂回模型模型继续决策直到它认为任务完成。这个循环跑起来以后你就不得不面对一系列工程问题模型怎么知道自己有哪些工具可用上一次调用产生了什么中间结果下一次要如何继承如果某一步工具报错是重试还是终止这些问题的答案全都落在“七要素”上。我把这七个要素按角色分一下模型是大脑提示词是行为准则工具是手脚记忆是工作台规划是思考路径多智能体协作是分工体制编排是控制中枢。1.2 七要素逐个过一遍先搞清楚每个零件在干什么先聊模型。模型是 Agent 的推理引擎决定了它能理解多复杂的指令、能生成多稳定的结构输出。但工程上要注意模型不是越强越好而是越匹配越好。你做一个只查数据库的问答 Agent用顶级大模型就是浪费做一个要自我纠错的复杂任务 Agent用小模型就很容易陷入死循环。这个取舍后面会展开先记住一句话模型选型本质是“能力下限”的选型。第二个要素是提示词。在 Agent 工程里提示词的作用被很多人低估它不是写一段“你是一个助手”就完事。你要在提示词里说清楚Agent 的角色边界是什么、它握有哪些工具、每个工具的适用场景、遇到不确定信息时应该怎么办、输出格式必须遵守什么。一个好的 Agent 提示词读起来更像一份员工手册而不是一句咒语。第三个是工具。工具是 Agent 与外部世界交互的通道可以是函数调用、HTTP API、数据库查询、代码解释器。没有工具的 Agent 只能纸上谈兵有了工具才算真正“下地干活”。工程上工具的定义要尽量原子化一个工具只做一件事描述要足够清晰否则模型很容易在选工具时犹豫或选错。第四个是记忆。记忆分短期和长期。短期记忆指当前任务上下文里的对话历史和中间结果长期记忆指跨会话的用户偏好、历史结论、领域知识。记忆放在哪、怎么压缩、怎么防止上下文爆炸是 Agent 工程里最容易出问题的一环。第五个是规划。规划能力让 Agent 能把一个大任务拆成多个小步骤而不是指望一次调用就得到最终答案。实现规划的方式有很多种让模型直接输出步骤、用 ReAct 模式交替推理和行动、或者外挂一个 Planner 模块。工程上规划需要控制和兜底否则模型会“想太多”把简单任务拆成八步反而拉低效率和成功率。第六个是多智能体协作。当你把任务拆给多个角色 Agent 时就涉及谁来分配任务、谁汇总结果、它们之间怎么通信。多 Agent 不是银弹它只有在任务本身具备天然分工、或者需要不同角色视角时才有价值。这点我会在决策点里细说。第七个是编排。编排层负责把上面所有要素串起来决定 Agent 循环的启动条件、每个节点的执行顺序、出错时的回退策略。在代码层面它就是一个状态机。LangGraph、AutoGen 这些框架解决的就是编排问题你可以用它们也可以自己写一个但绝对不能不写。1.3 缺一个会怎样从真实翻车案例看短板效应缺模型的 Agent 不成立缺工具的叫聊天机器人但其余几个要素缺一个都会在特定场景翻车。举个我自己的例子。早期做一个自动写周报的 Agent当时觉得提示词写得够详细、工具也接上了但跑了一周发现它经常把上周的数据和这周的数据混在一起。原因就是没有做记忆隔离每次会话都带着上一轮的向量检索结果模型分不清哪些是当前周期的数据。后来单独加了一块“临时工作区”每次任务开始前清空只允许工具调用写入本次结果问题立刻解决。这是缺“记忆管理”的典型症状。再举个例子有个朋友用低代码平台搭客服 Agent单轮对话效果很好但用户一旦连续追问几句它就答非所问。拆开看发现平台默认把整段对话历史全部塞给模型旧信息把关键问题淹没了。后来把历史压缩成摘要、只保留最近两轮原文准确率立刻回到正常水平。这说明提示词再强也兜不住糟糕的上下文组织。所以我一直建议团队在画架构图之前先用七要素检查一下自己的方案模型选型有没有依据、工具清单是否完整、记忆策略是空白还是拍脑袋、编排节点有没有定义清楚。任何一项是空白都要在进入编码之前想办法补上否则后面全是返工。2. 七个决策点从 Demo 到生产的分岔路口2.1 决策一单 Agent 还是多 Agent先问业务复杂度很多团队看到别人用多 Agent 协作觉得很酷上来就设计五个角色老板 Agent、分析师 Agent、写手 Agent、审核 Agent。结果跑起来后互相踢皮球一个任务要循环七八轮才完Token 消耗翻了三倍准确率反而没提升。我的经验是能用单 Agent 解决的单 Agent 解决。单 Agent 只有一个上下文、一套状态机调试成本低、Token 消耗少、行为更可控。什么时候才考虑多 Agent一是任务里确实存在冲突的目标比如既要忠实原文又要压缩到两百字让一个模型同时满足容易两头不讨好拆成“摘要 Agent”和“润色 Agent”反而各司其职二是工具类型差异太大比如一个 Agent 只操作数据库、另一个 Agent 只调用外部内容接口拆开可以隔离权限和错误域。如果你用的是扣子这类低代码平台更要克制平台把多 Agent 入口做得太顺滑很容易让你误以为“加一个 Agent 就能加一分能力”。实际上每次多一个 Agent你就多了一条需要排查的链路。先让单 Agent 跑通再用日志数据证明瓶颈出在“角色冲突”上再拆也不迟。2.2 决策二模型选型先算 token 账别只看跑分很多新人对“AI Agent token 是什么意思”有疑问。你可以把 token 理解为模型处理文本的最小单位一个中文汉字大约对应 1 到 2 个 token一段千字文章大约是一千多 token。模型每次调用都会消耗输入 token 和输出 token输入 token 是丢给模型的全部内容输出 token 是模型吐出来的内容。Agent 循环跑 N 轮就是把“每轮的输入 token 输出 token”累加起来。这里有个容易被忽略的事Agent 的 Token 消耗不是一次性的而是循环累积的。假设一次任务要调用模型五次每次输入两千 token、输出五百 token那一次任务就是一万两千五百 token。如果每次都把完整历史带进去这个数字会随着任务步骤增长。所以模型选型时必须算账你的任务平均要循环多少轮、每轮保留多少上下文、预期日调用量是多少用这些数字去对比模型价格。你不能只因为某模型跑分高就选它否则成本会失控。具体账可以这样算设定每个会话平均 5 轮每轮输入和工具返回共 3000 token输出 800 token一个会话就是 19000 token。日活一万个会话就是 1.9 亿 token。按当前主流商用模型的价格这已经是一笔很大的开销了。所以生产级 Agent 必须做上下文压缩甚至给不同任务分配合适的模型简单意图用小模型复杂推理才上大模型。2.3 决策三工具协议选 function calling 还是 MCP别被概念带偏工具要暴露给模型必须有一个协议层。目前最主流的是 OpenAI 的 function calling它通过 JSON Schema 描述工具入参模型在需要调用时返回一个结构化的 function call 对象你的代码再解析它、执行本地函数、把结果塞回消息列表。这套机制已经成了事实标准LangChain、LlamaIndex 都支持。MCPModel Context Protocol是最近很热的协议它更像一套标准化客户端-服务器协议目标是让工具、数据源能以统一方式接入不同模型和框架。它的价值在于生态互通一个 MCP 服务可以被多个 Agent 框架复用。但工程上我不会为了追新立刻全面切 MCP。如果你的 Agent 是内部闭环、工具就七八个 HTTP 接口直接定义 JSON Schema 就够如果团队有多个 Agent 项目或者要接入第三方工具生态MCP 才值得考虑。切换的成本不只是代码改造还有调试链路的复杂度增加原来一个模型工具调用报错直接看函数日志现在要查 MCP Server 日志。工具协议还有一个关键点描述质量。同一个工具描述写成“获取天气”和“根据经纬度获取指定城市的实时天气入参需使用城市拼音全称”会让模型调用的准确率差别很大。模型在选工具时是在做语义匹配描述里必须包含触发场景、必填参数、参数格式约定最好附一个或两个典型示例。2.4 决策四记忆放在对话窗口、摘要还是向量库分场景定记忆方案是 Agent 工程里最容易拍脑袋的模块。我见过最偷懒的做法是所有历史消息原封不动全部塞给模型直到打爆上下文窗口也见过最折腾的做法不管什么场景先建向量库把所有对话记录嵌入进去最后检索质量一团糟。正确的姿势是先分类型。短期记忆服务当前任务必须保证模型能“记得住”前几步做了什么放对话窗口最直接但当对话轮数变多就得做裁剪把早期多轮对话压缩成摘要只保留最近两三轮的原文。长期记忆服务跨会话比如用户偏好、历史订单信息这些数据通常结构化程度较高放在数据库按用户 ID 查询可能比向量检索更准。向量库只适合非结构化语义检索比如“从历史文档里找出和当前问题相似的内容”而不是所有记忆的默认方案。这里还要提一个技巧上下文压缩不能等快爆了才做。Agent 每循环一次都要检查当前对话窗口的 token 占用。超过了阈值就把早期消息转成摘要用“用户名说要预订周五的机票已在周三确认出票”这种高度压缩的句子替换掉完整对话。压缩后的摘要也参与下一次循环这样模型既不会丢关键信息又不会让字节数无限膨胀。2.5 决策五状态机是 Agent 工程的底盘框架只是辅助Agent 循环本质上是一个状态机有初始状态、中间状态、终止状态每轮模型输出触发一次状态迁移。很多团队忽略状态管理直接把 while 循环写到流程里结果一旦需要断点续跑、人工介入、超时恢复代码就纠缠成一团。我自己更推荐把“状态”显式建模不管用不用框架。至少要定义清楚会话 ID 用于隔离不同用户的任务、当前节点位置决定模型下一步能做什么、已收集的工具结果、要传给下一轮的消息列表、任务终止条件。把这些状态字段放进一个结构体或者数据库记录里Agent 循环每次从状态里恢复现场而不是靠一堆全局变量。用 LangGraph 这类框架能省很多事它把状态迁移变成一张图节点和边都是显式声明的。但我提醒一句别把图铺得特别大再开始写代码先画一条主干接收输入 → 调用模型 → 执行工具 → 判断是否结束 → 更新状态。跑通主干以后再往上面加条件分支和回退逻辑。框架的作用是降低状态管理的门槛不是替你决定流程怎么设计流程依然要靠业务逻辑来定。2.6 决策六并发瓶颈不在模型在状态隔离“AI Agent 怎么扛并发”是最近被问烂的问题。先说结论模型调用本身是天然并发的你发多少个请求都行真正的瓶颈在于会话状态能不能隔离。假如你用全局变量保存每个 Agent 的中间状态那并发一多必然串号假如你用单线程 while 循环跑任务那一个慢任务会堵住后面所有请求。正确的做法是把 Agent 的每次任务包装成一个可独立运行的工作单元这个单元内包含独立的上下文、工具调用记录、状态数据。不同任务之间完全不共享可变数据只共享工具和配置。在 Python 里可以用 asyncio 做协程并发也可以用 Celery 这类任务队列把任务丢给多个 Worker 执行。一个容易遗漏的点是长任务与短任务的处理差异。如果 Agent 任务普遍在十几秒内完成用同步接口等结果就行如果任务经常要一两分钟甚至更久客户端等不了就要改成异步任务模式接口立即返回任务 ID执行完通过 Webhook 或轮询通知结果。这件决策要在架构初期定下来不然后面改接口语义会牵动所有调用方。2.7 决策七没有评估体系Agent 就无法迭代很多团队把 Agent 上线之后就开始摆烂因为不知道什么叫“好”什么叫“不好”。传统的离线指标只能衡量单轮回答的准确性而 Agent 是过程性的工具选择对不对、步骤顺序对不对、纠错能力好不好、最终结果完整度如何这些都需要一套评估体系。我的做法是先建评测集收集三四十个典型业务场景每个场景标注标准答案和关键过程要求。每次改提示词、换模型、改工具描述都用同一批场景跑一遍记录成功率、平均轮数、Token 消耗、工具调用准确率。不求每次都有提升但必须知道变化方向。没有这套基线你连“这次改动是变好了还是变坏了”都判断不了。评估还有一个容易忽略的维度失败模式。你要知道 Agent 在什么条件下会失败是工具调用格式出错、上下文溢出还是模型产生了幻觉步骤。把失败样例收集起来每周过一遍这比看任何跑分都有价值。因为生产环境的问题很少是模型能力不够更多是边界情况没兜住。3. 实操用 FastAPI LangGraph 落地一个能扛并发的 Agent 服务3.1 为什么用 FastAPI 和 LangGraph 搭骨架什么时候不选它如果你的技术栈是 PythonFastAPI 配合 LangGraph 是目前我比较推荐的生产级组合。FastAPI 负责对外提供 HTTP 接口它原生支持 async天然契合 Agent 这种大量 I/O 等待的场景LangGraph 负责 Agent 的状态机和节点编排把前一节讲的决策点落成代码。经常有人问能不能用 Rust 写 Agent可以Rust 的优势是并发性能好、内存占用低适合做高吞吐的基础设施层但 Agent 生态里的工具链和框架还是 Python 更成熟团队维护成本也低。另外如果团队是 Java 背景Spring AI 也可以但它的生态和 RAG、工具协议丰富度目前和 Python 社区还有差距。我的建议是别被语言性能绑架Agent 的瓶颈在模型调用和状态设计不在那几毫秒的语言开销。下面的例子我故意把代码写得偏薄没有堆砌框架 API重点是展示“状态、工具、循环、并发”这四个核心怎么组织。你拿到手以后可以按自己的业务替换工具函数和状态字段。3.2 先定义状态和工具让模型知道它能做什么第一步是定义 Agent 会话状态。用 TypedDict 做类型声明LangGraph 会把它当成状态容器每次节点返回会更新对应字段。from typing import Annotated, TypedDict from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] session_id: str task_done: bool result: strmessages 用 add_messages 注解LangGraph 会自动把新消息追加进去省去手动拼接历史。session_id 用来隔离不同用户的任务task_done 是终止标志result 存最终答案。工具函数定义在另一个模块里。这里拿一个“查询订单状态”的工具做例子实际项目中改成查数据库、调内部 API 都行。关键是给函数写清楚 docstring 和参数注释模型会拿这些描述去匹配工具。def query_order_status(order_id: str) - str: 根据订单号查询订单的当前状态。 订单号格式ORD-2025-XXXX。 返回结果包含订单状态、物流公司和最新节点。 # 这里模拟一次数据库或接口调用 data {ORD-2025-0001: 已发货顺丰速运预计三天内到达} return data.get(order_id, 未查询到该订单请检查订单号是否正确)工具描述里写了订单号格式能显著减少模型传参错误。这一步花两分钟效果比换更强的模型明显得多。3.3 把循环写成图Agent 的四个节点LangGraph 里 Agent 循环通常拆成四个节点call_model把当前消息列表发给模型请求工具调用或最终回答。execute_tools如果模型返回的是工具调用执行对应函数。check_finish判断任务是否该结束。build_final_answer组装最终结果写入 state。核心代码如下。from langchain_openai import ChatOpenAI from langchain_core.tools import tool tool def query_order_status_tool(order_id: str) - str: 根据订单号查询订单的当前状态。订单号格式ORD-2025-XXXX。 return query_order_status(order_id) tools [query_order_status_tool] llm ChatOpenAI(modelgpt-4o-mini, temperature0) llm_with_tools llm.bind_tools(tools) def call_model(state: AgentState): response llm_with_tools.invoke(state[messages]) return {messages: [response]} def execute_tools(state: AgentState): last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: results [] for tc in last_message.tool_calls: fn globals()[tc[name]] result fn.invoke(tc[args]) results.append( ToolMessage(contentstr(result), tool_call_idtc[id]) ) return {messages: results} return {messages: []} def check_finish(state: AgentState): last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: return {task_done: False} return {task_done: True, result: last_message.content}然后把这些节点组装成图。graph StateGraph(AgentState) graph.add_node(call_model, call_model) graph.add_node(execute_tools, execute_tools) graph.add_node(check_finish, check_finish) graph.set_entry_point(call_model) graph.add_edge(call_model, execute_tools) graph.add_edge(execute_tools, check_finish) graph.add_conditional_edges( check_finish, lambda state: call_model if not state[task_done] else final, {call_model: call_model, final: END} ) app graph.compile()这个图表达的逻辑是模型输出后如果有工具调用就执行工具再把工具返回结果追加进消息然后回到模型节点继续决策如果没有工具调用说明任务完成直接返回结果。这样就是一个完整的 ReAct 循环。跑一个简单调用。initial_state { messages: [{role: user, content: 查一下 ORD-2025-0001 到哪了}], session_id: u-001, task_done: False, result: } output app.invoke(initial_state) print(output[result])3.4 并发与上下文压缩两个必须处理的工程点先看并发。FastAPI 天然支持 async把 Agent 调用放进 async 接口里可以同时服务大量请求。但这里有一个坑LangGraph 的 invoke 是同步阻塞的直接放在 async 接口里会卡住事件循环。解决办法是用 asyncio.to_thread 把阻塞调用丢到线程池。from fastapi import FastAPI import asyncio app FastAPI() app.post(/agent/run) async def run_agent(request: AgentRequest): initial_state { messages: [{role: user, content: request.query}], session_id: request.session_id, task_done: False, result: } result await asyncio.to_thread(app.invoke, initial_state) return {status: ok, result: result[result]}这样单机就能扛住不错的并发量。如果一台机器不够下一步不是优化代码而是把任务丢到 Celery/RQ 队列多个 Worker 横向扩展。要注意一旦上了任务队列接口语义就要从“同步返回结果”改成“返回任务 ID 客户端轮询/Webhook”因为 Worker 执行是异步的。再看上下文压缩。假设工具返回结果很长Agent 反复循环几轮后 Token 消耗会暴涨。我采取的策略是在 call_model 之前先检查 state 里的消息总 token 数超过阈值就把最老的消息压缩。这里给出一个极简实现实际项目中可以用 LangChain 的 summarize 工具或自定义摘要节点。def maybe_compress(state: AgentState, max_tokens: int 4000): total estimate_tokens(state[messages]) if total max_tokens: return state head state[messages][:2] # 保留系统提示和最早一条用户消息 tail state[messages][-4:] # 保留最近两轮对话 summary summarize_messages(state[messages][2:-4]) state[messages] head [summary] tail return statecompress 之后塞进 call_model 之前调用。注意 summary 必须本身是一段可以被模型理解的消息而不是给开发人员看的记录。把历史消息总结成“用户想预订周五的机票已对比三个航班倾向上午出发”模型读到这段信息就能继续后续任务。这个技巧能把一个长任务的 Token 消耗压掉一半以上而且对最终结果影响很小。4. 常见问题与排查技巧实录4.1 高频翻车问题速查表下面这张表是我在维护多个 Agent 服务时积累的每个问题都配了排查方向。现象典型原因排查路径Agent 反复调用同一个工具陷入死循环工具返回结果没有改变状态模型误以为任务未完成在 check_finish 节点加最大迭代次数检查工具返回内容是否包含足够的终止信号上下文越界请求直接被模型拒绝历史消息无压缩Token 总量超过窗口统计每轮消息 token加压缩策略把长工具结果改为存库只放摘要进上下文工具参数频繁传错工具描述含糊、示例缺失改写工具 docstring补充参数格式和典型示例在工具函数内做参数校验并友好报错多用户并发时结果串号全局变量保存了会话状态状态必须按 session_id 隔离代码审查里禁止模块级可变字典模型应该调用工具却直接编了个答案工具列表没注入到模型绑定流程检查是否执行了 bind_tools确认模型支持 function calling任务到一半超时同步阻塞调用卡住事件循环用 asyncio.to_thread 或改异步 Worker接口语义改为任务队列每次改提示词效果忽好忽坏评测集缺失靠直觉调参建固定评测集记录每次改动的成功率、平均轮数、Token 消耗Agent 把旧数据混进新任务记忆没有按任务隔离每次任务开始重置短期记忆长期记忆查询加业务维度过滤条件4.2 三次实战排查过程还原第一次翻车是在线上跑了一个自动写摘要的 Agent用户反馈“回答越来越啰嗦”。看了完整 trace 发现系统把前几轮生成的长摘要又当输入喂给了模型模型基于摘要再生成摘要内容不断膨胀。解决方案是把中间摘要单独存放不进入下一轮消息列表模型每次只基于原始材料生成。第二次是并发压测时出现了串号两个用户的订单状态互相写错了。定位后发现在早期原型里为了图方便用了模块级 dict 存状态后来接入 FastAPI 也没改。排查过程花了一晚上原因是问题只在并发高时才偶发单条请求复现不了。最终把所有状态改成显式传入 LangGraph 的 state问题消失。第三次是模型在工具调用返回后仍然重复调用同一个数据库查询。原因是工具返回“查询成功共 3 条记录”模型看到数据格式是列表认为还要再查一次“完整详情”而完整详情接口并不存在。我在工具描述里显式写明“这是唯一可用的查询接口返回结果已包含全部字段”同时在图上加了最大循环次数为 6 的熔断。从那以后同类问题再也没有出现。这三起案例都在印证一件事Agent 的多数故障不是模型不够聪明而是状态管理、上下文组织、工具描述这些工程细节没做到位。每一个问题都能在七要素和七个决策点里找到对应位置。排查的顺序也应该是固定的先看状态对不对再看上下文有没有污染最后才怀疑是模型能力问题。最后再分享一点个人经验。如果你现在准备从零开始做一个 Agent 项目我的建议是先把第一天要做的事情列成四行定义好状态结构、定义好工具函数和描述、确定终止条件、确定评测集。四件事做完再开始搭 LangGraph 或直接写循环。这四件事里任何一件没想清楚后面都会以翻车的形式来找你。我踩过这些坑希望你少踩几个。