1. 七要素:先给 Agent 画一张解剖图
很多人聊 AI Agent,一上来就争论“它到底是不是有自主意识”“未来会不会取代人”,但落到工程上完全不是这么回事。我在一线做后端和 AI 应用,最大的感受是:Agent 不过是以大模型为推理核心、通过感知-规划-行动-反馈循环去完成目标任务的软件系统。听起来很玄,拆开看就是七个要素。理解了这七块板子,你再看任何框架、任何架构都不会懵。
1.1 模型:一切推理的起点
第一个要素是大模型本身,也就是推理引擎。无论是 GPT 系、Claude、还是开源模型,Agent 所有智能行为都建立在这个底座上。工程选型时我一般看三个指标:一是上下文窗口够不够用,二是是否支持稳定的函数调用(function calling),三是服务商的限流策略和响应延迟。
很多项目失败不是因为 Agent 逻辑写得不好,而是模型选错了。比如工具调用支持差的模型,你让它输出 JSON 参数,它给你扯一段废话;上下文窗口短的模型,塞几轮工具结果就溢出了。所以模型是骨架,后续每个要素都建立在它之上。
1.2 规划:把目标拆成可执行的步骤
规划就是让模型想清楚“先做什么、再做什么”。常见的手法有 ReAct 模式,也就是“思考-行动-观察”循环;也有 Plan-and-Execute 模式,先输出一个完整计划再逐步执行。我做个类比:规划就像你写工作周报,先列总目标,再拆成“周一发布、周二验证、周三复盘”这样的细项。
工程实现时,规划不一定是独立模块。很多时候你只是在 Prompt 里加一句“请先分解问题,再逐步解决”,或者用一个规划节点单独生成任务列表。但要注意,规划越自由,越容易失控。所以给模型限定动作空间非常重要,比如只能调用五类工具,禁止无限自我发散。
1.3 记忆:短期状态和长期知识的区别
记忆是 Agent 和单轮 Chat 最大的区别之一。短期记忆就是当前会话里的对话历史、中间状态;长期记忆则是能被后续会话复用的用户偏好、事实知识、历史结果。我用一个容易踩坑的理解:记忆不是越大越好,而是要分层。
短期记忆直接塞进上下文,调用模型时拼到 Prompt 后面。长期记忆一般落到数据库或向量库,通过语义检索把相关片段捞回来。很多 Agent 一开始做得很好,跑一段时间就变傻,就是因为只堆聊天记录,没有做摘要和淘汰机制。之后我们在决策点里会专门聊怎么管记忆。
1.4 工具:Agent 的“手和脚”
工具是 Agent 与外部世界交互的接口。搜索、计算器、数据库查询、发邮件、操作浏览器,本质上都是一个个被模型调用的函数。没有工具的大模型只是一个“嘴强王者”,能聊但干不了活。有了工具,它才真正“下地干活”。
工程上,工具被描述为一段 JSON Schema 外加一个对应函数。模型看到 Schema 后,决定“我需要调用哪个工具、传入什么参数”。这一步直接决定了 Agent 的能力边界。我的经验是:工具不在多,在于接口稳定、描述清楚。一个语义含糊的工具描述,会诱导模型反复传错参数。
1.5 行动:把模型意图变成系统操作
规划定好了方向,工具定义了能力,行动则是真正执行调用的那一下。行动模块负责解析模型的输出,校验参数,调用对应函数,然后把结果拿回来。
这里有个容易忽略的问题:行动必须带容错。工具执行可能超时、可能返回异常,你必须把这些异常包装成“模型能看懂的错误信息”再喂回去。否则模型下一次调用还会撞同一个坑。行动计划是模型对世界的假设,行动结果才是世界的真相,两者必须在这个环节对齐。
1.6 反馈:闭环是 Agent 的灵魂
没有反馈的循环只能算“脚本”。Agent 每执行一步,都要观察结果、检查是否完成目标,再决定是继续修正还是结束。反馈可以是工具返回的数据,可以是环境状态,也可以是用户补充的信息。
我见过很多把 Agent 做成“一次性输出”的项目,本质没有循环,那不叫 Agent。真正有用的 Agent 一定是把“行动->观察->再规划”这条回路跑起来的。这一步也是工程调试最投入精力的地方,因为反馈未必符合预期,你需要设计中止条件,防止它无限循环。
1.7 安全:生产环境不容商量的一层
最后一个要素常常被忽略,但它决定了 Agent 能否上线。安全包括:敏感操作之前的二次确认、输出内容的合规过滤、任务边界限制、对异常指令的拒答。这不是“加个敏感词库”那么简单,而是要在 Agent 的控制流中插入护栏(guardrail)节点。
我的建议是:涉及真实世界后果的操作,比如发邮件、转账、删数据,必须在行动节点前加一层人工审批。模型可以无边无际地天马行空,系统边界必须严格收敛。这七个要素不是可选项,而是一个完整 Agent 的必备项。接下来,我们把视角从解剖学切换到工程决策。
2. 七个决策点:动手前先回答的问题
解剖学告诉你 Agent 有哪些零件,工程实现告诉你这些零件怎么组装。我把它归纳成七个必须提前拍板的决策点。每个决策点没有绝对对错,只有适不适合你的业务场景。下面逐个拆开讲。
2.1 决策点一:框架选型,用 LangGraph 还是自研
这是第一个要命的问题。市面上有 LangChain/LangGraph、Haystack、Spring AI Agent、Rust 生态里的 Rig 等,还有低代码平台如扣子(Coze)。选型不能跟风,要看你团队的技术栈和要解决的问题。
如果你已经重度使用 Python,且业务流程复杂、需要精细控制循环和状态,LangGraph 是目前最顺手的方案。它把 Agent 画成一张有向图,节点是逻辑执行块,边是条件路由,非常适合做“多步规划、工具调用、掉头重试”的循环。Spring AI Agent 适合 Java 团队,能直接和 Spring Boot 生态融合,但灵活度不如 LangGraph。Rust 适合对性能和并发要求极高的场景,但模型生态和社区资料相对少,开发成本偏高。
我的观点是:自研框架不推荐从零开始。你不会想自己维护 Prompt 拼接、消息历史、工具调用协议这些轮子。除非你的业务极其特殊,且团队有充足精力,否则站在成熟框架的肩上做二次开发更划算。选型的最终标准是“团队能否长期维护”,不是“谁家 Star 最多”。
2.2 决策点二:控制流,线性 Chain 还是状态图
控制流是 Agent 的中枢神经。最早期大家用 Linear Chain,一锤子买卖:调用模型,拿结果,完事。后来发现任务经常要“根据中间结果决定下一步”,于是 LangChain 发明了各种 Router。但这些组合越来越拧巴,所以有了明确的图状态机,像 LangGraph 那样把每个环节定义成节点,节点之间的边代表转移条件。
工程上,我强烈建议复杂的 Agent 用状态图而不是互相嵌套的 Chain。因为图结构天然适合“循环”和“分支”:比如“如果工具执行失败,回到规划节点重新生成”;“如果检测到任务完成,走到结束节点”。图还容易可视化,调试时你能看到当前卡在哪个节点,数据流是怎样的。
用图不是让所有逻辑都画成密密麻麻的节点。节点粒度要适中,太粗没法复用,太细调试难。我通常把“模型调用”“工具执行”“条件判断”“输出格式化”各设计成独立节点,简单直接。
2.3 决策点三:记忆生命周期,怎么存、怎么更新、怎么忘
前文提到记忆要分层,但真正落地时你会遇到更头疼的问题:上下文窗口不够用了怎么办?旧记忆和当前任务冲突怎么办?不同用户的记忆怎么隔离?
我的实践是把记忆生命周期分成三个阶段。第一阶段是“工作记忆”,也就是当前会话的上下文,通常用消息列表保存,加一个最大轮数截断;第二阶段是“摘要记忆”,当对话超过阈值,让模型把前面内容压缩成摘要,再接续后面的对话;第三阶段是“长期记忆”,把一些关键事实写入向量库或 KV 存储,下次会话检索复用。
“遗忘”同样重要。我会对长期记忆加时间戳和饱和度,超过一段时间没被命中的内容定期淘汰。否则记忆库会堆满噪声,检索时反而把无关内容捞回来,干扰模型判断。记忆系统的设计目标是“在最恰当的时机,给模型最少但最关键的信息”。
2.4 决策点四:工具协议,统一 Schema 还是自由文本调用
模型调用工具不是魔法,它遵循一套协议。业界普遍推荐的协议是 OpenAI Function Calling,也就是把每个工具定义成一段包含 name、description、parameters 的 JSON Schema。模型看到这些信息后,选择工具并生成符合 Schema 的参数。
千万别小看工具描述,它决定了模型调用准不准。我踩过最大的坑是:工具描述写得太简略,模型频繁传错枚举值;或者在参数里写“任意字符串”,结果模型填了一堆废话。正确做法是给每个参数写示例、写约束、写默认值,并且把潜在的常见误用写进描述里。
服务端还要做统一的工具注册中心。每次新加工具,只需注册一个函数加一个 Schema,不用改控制流。工具执行异常时,要捕获错误并转成“给模型的正常反馈”,而不是直接让整个 Agent 崩溃。工具协议是接口工程,稳定比丰富重要得多。
2.5 决策点五:Token 预算,如何不烧钱又能保证效果
Token 是 Agent 里绕不开的成本。一次复杂任务可能要调用模型十几次,每条历史消息、每段工具结果都会重复计入上下文,Token 消耗会肉眼可见地涨。很多团队上线前没算这笔账,月底账单直接傻眼。
我的成本控制有四个抓手。一是估算公式:每次请求 Token 消耗 = 系统 Prompt + 历史消息 + 工具描述 + 当前输出上限。工具描述往往是隐性大户,工具数量越多,每次请求都背着全量 Schema。二是裁剪策略:历史消息按轮剪,只保留最近的 N 条变动,加上摘要兜底。三是结构化输出:让模型用 JSON Schema 输出,尽管会多花一点 Token,但能显著减少“参数传错”导致的无效重试。四是缓存:相同工具结果、相同上文前缀可以做缓存,避免重复计算。
预算不是死的。我会按用户等级或任务复杂度设置不同档位:简单任务用轻量模型,复杂任务才启用大模型。最终效果是“能省则省,关键动作不省”。
2.6 决策点六:并发与状态隔离,Agent 怎么扛得住流量
“AI Agent 怎么扛并发”是近期很多人关心的问题。瓶颈不在于模型吞吐,而在于 Agent 的状态管理。每个用户的会话都有自己的上下文、自己的待执行计划、自己的记忆,如果把所有状态扔进一个全局变量,并发一高必然串号。
我的年轻团队现在用 FastAPI + LangGraph 做部署。核心思路是:把 Agent 状态做成可序列化的对象,每个请求进来时创建一个独立的实例,执行完把状态和最终结果持久化到数据库或 Redis。服务节点保持无状态,这样横向上可以随意扩容,扛并发靠加机器而不是靠单机硬撑。
异步化也很关键。工具调用如果是网络请求,要使用 async 方式,避免进程阻塞。数据库连接池、Redis 连接池要单独管理,不能让每个 Session 都新建连接。另外,模型 API 本身的限流也要在网关层做排队和退避,否则上游一限流,下游所有请求都撞墙。
2.7 决策点七:可观测与评测,没有监控就别上线
最后一个决策点最容易被忽略:你拿什么证明 Agent 表现好?纯看用户反馈太慢,必须建设可观测性和离线评测两套体系。
可观测性要做三件事:日志、追踪、指标。日志记录每一轮“规划了什么、调用了哪个工具、返回了什么结果”;追踪把同一个任务的所有动作串起来,方便回放;指标则统计成功率、平均轮数、平均耗时时长。没有这些,你根本无法判断是模型问题、工具问题还是 Prompt 问题。
评测是我反复强调的重点。准备一套带标准答案的评估集,覆盖主要任务路径和边界情况。每次改 Prompt、换模型、调参数,都跑一遍评测集,看完成任务的比例和工具调用的准确率。线上表现和离线评测常常不一致,但离线评测依然是最快的回归手段。如果预算有限,先跑三十条典型 case,也比裸上线靠用户骂要强。
3. 实操:用 FastAPI + LangGraph 搭一个能跑并发的 Agent
讲完理论和选型,我直接分享一套可复用的工程实现。这里混用了 Python 生态的代表性技术:FastAPI 提供 HTTP 服务,LangGraph 管理 Agent 流程,LangChain 提供模型封装。这个组合不是唯一解,但非常清晰,适合中小团队快速落地。
3.1 工程目录与依赖规划
项目结构我会拆成四层,避免所有逻辑塞进一个文件:
app/ main.py # FastAPI 入口,负责会话创建和请求代理 agent/ graph.py # 定义 Agent 状态图 tools.py # 工具注册中心 state.py # 会话状态模型 memory/ store.py # 历史消息和长期记忆存储 service/ agent_runner.py # 独立执行 Agent 的封装依赖方面,核心是fastapi、langchain、langgraph、openai。如果不需要复杂的封装,其实可以直接用langgraph配合任何模型的 API。我不建议为了用框架而用框架,代码里看到多少抽象,取决于你实际需要多少控制。
3.2 Agent 状态图的核心代码
LangGraph 的核心是定义状态类型和节点函数。下面是一个简化但完整可运行的例子:
from typing import TypedDict, Annotated, Literal from langgraph.graph import StateGraph, END from langgraph.graph.message import add_messages class AgentState(TypedDict): # 使用 add_messages 注解,LangGraph 会自动合并新消息到历史列表 messages: Annotated[list, add_messages] tool_plan: list[str] step: int done: bool def plan_node(state: AgentState): # 构造规划 Prompt,让模型决定调用哪个工具、传入什么参数 # 实际代码可以调用 LLM 或直接基于规则路由 return {"step": state["step"] + 1} def tool_node(state: AgentState): # 执行工具注册中心里被选中的函数 return {"messages": [{"role": "assistant", "content": "工具执行完成"}]} def should_continue(state: AgentState) -> Literal["tools", "finish"]: if state["done"] or state["step"] >= 5: return "finish" return "tools" def build_graph(): graph = StateGraph(AgentState) graph.add_node("plan", plan_node) graph.add_node("tools", tool_node) graph.set_entry_point("plan") graph.add_conditional_edges("plan", should_continue, {"tools": "tools", "finish": "finish"}) graph.add_edge("tools", "plan") graph.add_edge("finish", END) return graph.compile()这段代码的关键点是add_messages,它保证消息历史不会被覆盖,而是追加。step字段用来兜底,防止 Agent 陷入死循环,最多跑五轮。实际项目中,tool_node会解析模型选出的工具名和参数,再调用工具,最后把工具结果作为消息写回状态。
3.3 FastAPI 接入与并发隔离
FastAPI 接入时要注意:每个请求必须独立创建 Agent 实例,不能全局复用。LangGraph 的compile()返回一个可调用对象,你可以为每个会话创建状态初始化值:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): session_id: str user_message: str class AgentSession: def __init__(self, session_id: str): self.session_id = session_id self.history = [] async def run(self, message: str) -> str: graph = build_graph() initial_state = {"messages": [{"role": "user", "content": message}], "tool_plan": [], "step": 0, "done": False} result = await graph.ainvoke(initial_state) return result["messages"][-1]["content"] @sessions = {} @app.post("/chat") async def chat(req: ChatRequest): session = sessions.setdefault(req.session_id, AgentSession(req.session_id)) answer = await session.run(req.user_message) return {"answer": answer}这里sessions是演示用的进程内字典,生产环境应该换成 Redis。这里想强调的是:每个 session 拥有独立的 Agent 状态,流量再大也不会影响彼此。进程内字典的缺点是重启丢失、单机内存瓶颈,但只要换成 Redis,整个架构就变得可以横向扩展。FastAPI 的async还能撑住大量 IO 等待,适合 Agent 这种“推理耗时长、算力占用小”的服务。
3.4 部署时还需要注意的几个细节
部署 Agent 服务和部署普通 Web 服务有区别。首先是超时设计,模型推理可能耗时数十秒,网关和负载均衡器要配置与之匹配的超时时间;其次是流式输出,如果前端希望打字机效果,需要把后端的响应改成 SSE(Server-Sent Events);最后是 Graceful Shutdown,Agent 执行到一半时进程退出,正在进行的任务是直接丢弃还是标记重跑,要提前设计。
内存方面,LangGraph 状态对象不能无界增长。每个 Session 结束前,要把状态序列化后存到持久层,并释放内存引用。我见过最典型的线上事故是:Session 对象被 Hono 上全局引用后,上下文越来越多,内存直线上升,最后 OOM。定期清理空闲 Session 是必须做的事。
4. 真实踩坑实录:常见问题与排查技巧
最后这部分是我希望大家少走弯路的地方。下面这些坑,都是我在生产环境里真实遇到过的,直接给结论。
4.1 模型无限循环,工具调用停不下来
现象是:Agent 一直在“调用工具-观察结果-继续调用”,永远不输出最终答案,日志里 step 数值狂奔。原因通常有两个:一是终止条件写得太松,模型认为任务永远没完成;二是工具结果里出现了它不理解的新信息,导致它反复探索。
解法有三个。第一,给 Agent 的 Prompt 里明确写“如果你已经获取到关键信息,请直接给出最终答案”;第二,设置最大迭代次数,比如 5 次或 10 次,超过后强制结束并返回已收集信息;第三,检查是不是工具描述不清晰,某些工具返回错了结果导致模型误判。我一般先查流程日志,看最后几轮“观察”的内容是什么,再决定是改 Prompt 还是改终止条件。
4.2 并发一高,上下文串了
现象是:用户 A 的对话里突然冒出用户 B 的信息,越查越诡异。十有八九是全局变量存了会话状态,或者消息历史用了类级别的可变变量。Python 里特别容易踩这个坑,因为默认参数是可变的,多个请求如果共享同一个 List,就会互相污染。
解法很简单:每个请求实例化状态,消息历史用copy.deepcopy或者在进入节点时通过add_messages创建新列表。LangGraph 的Annotated和add_messages其实已经帮你处理了合并逻辑,但如果你自己手写state["messages"].append(...),并发时就会出事。记住:状态不可变,每次返回新状态,是 LangGraph 的使用铁律。
4.3 Token 消耗暴涨,工具异常反复重试
现象是:某天调用量突然翻倍,排查发现同一个工具失败后,模型锲而不舍地重试了七八次。问题出在工具异常信息太笼统。如果返回“调用失败”,模型并不知道怎么改参数,只能盲试。
解法是把异常转成“可行动的提示”。比如:“天气接口超时,建议 3 秒后重试,或改用备用城市编码”。模型看到具体原因后,才能做出正确的下一步决策。同时,在工具节点加幂等和重试策略,比如对只读接口最多重试两次,对写接口必须人工确认。这个策略既省 Token,也避免把线上系统打崩溃。
4.4 上线前没有评测集,改一个配置心里没底
现象不是故障,而是恐惧:改了 Prompt 之后,不知道效果是变好还是变坏。没有评测集,你只能靠手动点几个 Case 靠感觉判断,这在 Agent 系统里非常危险。
我现在的做法是:维护两个评测集。第一个是“核心路径”,覆盖典型用户问题,比如“帮我查快递”“帮我订会议”;第二个是“边界与对抗”,覆盖模糊指令、空缺参数、敏感操作等。每次改动先跑核心路径,要求通过率不低于之前;再跑边界集,记录失败案例并人工判断。有了这套流程,我敢每周迭代 Prompt 和模型版本。
注意:Agent 的评测不是一次性的。模型厂商升级版本、工具接口调整、业务规则变化,都可能让此前通过率很高的评测集一夜失效。所以评测集本身也要维护,定期检查是否还有效、是否覆盖了最新的业务场景。
最后再分享一个小技巧:给 Agent 的执行过程加可视化回放。无论是用 LangGraph 自带的绘图,还是自己写一个日志面板,能看到每一步“思考了什么、调用了哪个工具、返回了什么结果”,能让你调试效率提升一个量级。我第一次做回放面板时,才发现很多所谓的“模型不听话”,其实是工具返回的数据格式和我预期不一致。Agent 工程是系统工程,光盯着模型 Prompt 是不够的,七个要素、七个决策点缺一不可。记住:先拆解,再决策,最后用日志和评测去验证你的每一个选择。