做 AI Agent 工程这两年,我最大的体会是:市面上聊 Agent 的内容极多,但大多停在概念演示或者“Prompt 调优八股”的层面,真正开发落地时根本不够用。我每天的处境是另外一回事——模型输出不稳定、工具调用超时、用户并发一上来整个任务流就乱,甚至同一个方案在不同人嘴里能给出三种“Agent 的定义”。所以这篇文章我想换个角度,把 Agent 当纯粹的软件工程问题来拆。
标题里的“七要素”是指任何一个可用的 Agent 系统都必须具备的七个组成部分,而“七个决策点”是我在项目生命周期里总结出来的七个关键选型节点。两者不是割裂的概念清单,前者是架构视角,后者是落地顺序。全文会围绕一条主线展开:先从工程视角拆开 Agent 的组成,再给出一个可以直接参考的最小实现骨架,最后聊到并发、服务化和中台化这类规模化问题。适合刚入门想做 AI Agent 开发的人,也适合已经被 LangGraph、FastAPI、工具协议这类词折磨过的同学。
1. 七要素是拆解维度,不是功能清单:先分清谁负责什么
市面上常见的 Agent 架构图往往画得很漂亮,但落到代码里就含糊了。我习惯把 Agent 系统拆成七个要素,每个要素对应一个明确的职责边界。这样拆不是为了写文档好看,而是为了在出问题时能更快定位:模型答错了、工具调错了、记忆串了、任务卡住了,分别该去查哪一个模块。
1.1 模型层:Agent 的推理核心和它的硬约束
模型层是整个系统的心脏。术语上可以叫它 LLM、基础模型或者推理引擎,但工程上必须把它当成一个“有缺陷的计算单元”来对待。
选型时有几个硬指标绕不开:上下文窗口、最大输出 token、每 token 成本、首字延迟和吞吐。举个实际例子,如果你做的是客服工单分类 Agent,用户的一条消息可能只有几十个字,但系统 Prompt 加工具说明动辄上千 token,再加上历史上下文,一次调用的实际输入可能乘到三五倍。上下文塞得越满,响应越慢、账单越贵,到窗口边缘还得做裁剪,这就给后续的记忆层设计留下了约束。
另一个容易被忽略的点是模型层要封装成 adapter。我在项目里习惯写一个统一的LLMClient接口,底下去适配 OpenAI-compatible API 或者本地部署的 vLLM 服务。这样后面换模型、做灰度对比、或者接私有化部署时,其他六个要素基本不用动。
1.2 工具层:连接真实世界的“手脚”,也是最容易失控的部分
工具层是 Agent 触达外部世界的唯一途径。它不只是在代码里定义一个函数那么简单,而是包括函数名、描述、入参 JSON Schema、校验逻辑、超时设置、错误返回格式这一整套注册机制。
为什么这么较真?因为模型不会凭空知道你的函数该怎么调。它只能根据工具描述和参数 Schema 去生成调用请求,所以描述写得不清楚,入参校验不严格,模型就开始自由发挥。我在生产环境里见过一个惨痛案例:数据分析 Agent 的某个工具没有对参数做枚举校验,模型生成了一个根本不存在的时间格式,工具抛出异常后又没有兜底逻辑,整个任务从头再来,白白浪费了十几次模型调用。
工具层还有一个高危点:执行环境。很多教程喜欢让模型直接输出一段表达式然后 eval 执行,这在本地 Demo 里没问题,一旦接上公网服务就是重大风险。工程做法是显式定义工具白名单,所有入参走 Schema 校验,对操作型工具要考虑幂等性——同一个操作如果因为网络超时被执行第二次,结果不能是灾难性的。
1.3 记忆层:短期记忆和长期记忆要分开设计
记忆层解决的是“上下文从哪来”的问题。很多人对记忆的理解就是“把历史消息都塞给模型”,这其实是最粗放的短期记忆方案,塞到一定程度就会遭到上下文窗口和成本的制裁。
短期记忆的工程实现通常包含两块:原始对话历史的工作区,以及负责控制窗口的裁剪/摘要机制。工程上常用策略是保留最近 N 轮完整对话,更早的内容交给模型生成摘要后保留摘要文本,这样既保留关键信息又不至于无限膨胀。长期记忆则解决跨会话复用的问题,比如把用户偏好、项目状态、历史决策抽出来存到向量库或 KV 存储里,在新会话开始时把相关记忆注入系统 Prompt。这也是向量库在 Agent 项目里真正应该出现的场景——不是无脑做知识库问答,而是做记忆召回。
1.4 规划层:从单步决策到多步任务拆解
规划层负责回答“下一步干什么”。最简单的规划是 ReAct 模式:模型观察到当前状态,决定调用某个工具,看到结果后再决定下一步,循环往复。再往前一步是 Plan-and-Execute:模型先把大任务拆解成一系列步骤,然后逐步执行、逐步修正。
我可以给一个更直观的场景。如果你做的是商品价格监控 Agent,任务可能是“每天早八点查询几个采购渠道的价格,汇总成一份报告”。这类任务流程相对固定,用 Plan-and-Execute 会更好,因为它先列出计划,中间每一步都有明确的输入输出,出问题也容易定位。反过来,如果任务本身变数很大,比如“帮我把这个临时需求处理一下”,那 ReAct 式的自由规划就更合适。规划层的工程要点是设定最大步数上限,否则模型可能在一个失败分支里无限循环,把 token 烧光。
1.5 提示词层:让模型稳定输出的工程化手段
提示词层不只是“写一段好的人设”,而是在系统 Prompt 里固化行为约束。我的建议是三段式结构:角色与任务边界、可用工具与调用规则、输出格式与兜底策略。
这里有一个工程上的关键选择:输出格式尽量用 structured output 或者 function calling,不要靠“请返回 JSON”这种指令去碰运气。比如你需要模型从用户消息里抽取字段,与其让它返回一段 JSON 再用正则去解析,不如直接声明一个extract_fields工具,让模型以工具调用的形式输出结构化参数,再在代码里完成校验和落库。这样既稳定,又省去了字符串解析的脆弱环节。
1.6 多智能体协作:什么时候该拆,什么时候不该拆
多智能体协作是七要素里最容易被过度使用的部分。它的本质是把系统拆成多个角色,每个角色拥有独立的 Prompt 甚至独立的模型,由一个协调者负责调度。典型的架构是 Supervisor + Worker:Supervisor 收到任务后分发给对应的 Worker,Worker 执行完把结果汇报回来。
但拆成多个 Agent 是有代价的。每个 Agent 都是一次模型调用链,多一次拆分就多一层延迟和失败概率,而且多个 Agent 之间的状态同步很容易出问题。我的经验是:如果单个 Agent 加一个路由分类器就能解决问题,就别拆;只有当技能边界清晰、任务类型差异大、单一 Prompt 已经互相打架时,才值得拆。判断标准很简单——你是在用拆分降低整体复杂度,还是在用拆分制造新的复杂度。
1.7 反馈闭环:要素七不是锦上添花,是最后一道保险
很多团队把反馈闭环当成上线之后才补的东西,这是顺序搞反了。反馈闭环包括日志采集、链路追踪、用户反馈回收和评测集回归。模型升级、Prompt 调整、工具改动,任何一项变更都可能让 Agent 行为发生不可预期的漂移,没有评测集兜底,你根本无法判断这次改动是变好还是变坏。
我倾向于从第一天就把关键交互日志结构化地落下来,形成一个可复跑的回归测试集。后面每次改动,先在测试集上跑一遍,再决定要不要上全量。这比上线之后靠用户投诉来发现问题,要省太多事。
2. 一个最小可用 Agent 的骨架:看完就能自己落地的 Python 实现
只谈要素不上代码,对工程人来说等于白说。这一节我给你一个可以直接参考的最小实现骨架,分三个层次:先手写 ReAct 循环理解本质,再引入 LangGraph 做状态机改造,最后补上内存、日志、重试这三个隐藏模块。
2.1 手写 ReAct 循环只需要几十行核心代码
先看最简单的手写版本。ReAct 循环的本质是:把“模型生成的内容”和“工具执行结果”当成消息序列来回拼接,直到模型不再调用工具、直接给出最终答案。
MAX_STEPS = 8 def react_loop(task: str, tools: list, llm, memory): messages = memory.load_session() messages.append({"role": "user", "content": task}) for step in range(MAX_STEPS): resp = llm.chat(messages, tools=tools) if resp.finish_reason == "tool_calls": # 把模型的工具调用消息追加到上下文里 messages.append(resp.tool_call_message) for tool_call in resp.tool_calls: observation = execute_tool(tool_call, tools) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": observation }) continue # 模型不再调用工具,直接返回文本答案 memory.save_session(messages + [resp.message]) return resp.text raise RuntimeError(f"Agent 超过最大步数 {MAX_STEPS},判定失败")这个循环里最容易忽略的坑,是工具调用消息和 tool 结果消息的格式必须严格匹配协议。ChatCompletion 系列接口要求 tool 消息必须带有对应的tool_call_id,一旦串了或者漏了,模型可能直接把前面的工具结果都当成平铺文本,逻辑直接跑偏。
2.2 引入 LangGraph 后的状态机改造:节点与条件路由
手写循环能跑,但一旦出现分支逻辑就不好维护了。比如你要做“先规划再执行,执行中找人审”这种带条件跳转的流程,手写循环会变成一个 if-else 层层嵌套的怪物。这时就该上状态机框架,我在项目里常用 LangGraph,因为它把 Agent 流程做成显式图结构,调试和扩展都清晰很多。
from langgraph.graph import StateGraph, END class AgentState(TypedDict): task: str plan: list[str] rounds: int answer: str graph = StateGraph(AgentState) graph.add_node("planner", planner) graph.add_node("executor", executor) graph.add_node("reflect", reflect) graph.set_entry_point("planner") def route(state: AgentState): if state["rounds"] >= MAX_ROUNDS: return "reflect" # 计划是否已完成?完成则直接生成答案,否则继续执行 return "executor" if state["plan"] else "answer" graph.add_conditional_edges("planner", route, { "executor": "executor", "answer": END, })这个骨架虽然简略,但它体现的状态机改造思路是通用的:节点负责具体工作,条件路由负责流程走向。与其把逻辑散在一大段 while 循环里,不如让每个节点只做一件事,条件路由独立成函数,出问题时看状态参数就能定位。
2.3 骨架里必须有的三个隐藏模块:内存、日志、重试策略
手写代码时最容易漏掉的三个模块,恰恰是生产环境里最要命的三个问题。
第一个是内存控制。不要把整个会话期所有消息无限堆下去,要设定上下文窗口的上限。我在MAX_STEPS之外还会设定MAX_CONTEXT_TOKENS,超过阈值就把早期消息压缩成摘要。
第二个是日志。每一轮循环至少要记录:步数、调用的工具名、入参、工具结果摘要、累计 token 数。这些日志不是给人 debug 用的,而是后面做评测集和链路追踪的数据源。
第三个是重试策略。模型接口偶发超时、工具接口偶发 5xx,都触发重试。但重试有个原则:只有幂等操作可以安全重试。查询类工具重试没问题,但“下单”“发消息”“删除数据”这类操作重试可能就是事故。所以我在工具注册表里给每个工具加了一个字段idempotent: bool,重试逻辑遇到非幂等操作会直接抛错人工介入,而不是盲目重发。
3. 七个工程决策点,按项目的生命周期排好序
七要素是架构拆解的静态维度,但真实的项目是动态推进的。我把最重要的选型时机压缩成七个决策点,按项目从立项到上线的顺序排列。每个决策点都对应着前面某个要素的实现方式,想清楚再做,能省掉大量返工。
3.1 立项期:这个需求真的需要 Agent 吗
决策点一永远是好消息:很多时候你根本不需要 Agent。判断标准是任务路径的不确定性。如果任务从输入到输出最多两三个分支,每个分支的处理逻辑固定,那就写普通工作流:一个 if-else、一段管道代码、甚至一个 cron 脚本就够了。非得塞模型进去,反而引入不可控的输出抖动和额外成本。
我见过最典型的反面案例是自动回消息类需求。需求方说“帮我做一个智能回复助手”,实际调研后才发现,真正用户只需要三到五种固定话术,根据关键词做简单路由就能覆盖 90% 的情况。这种情况下,一个规则引擎加模板系统比任何 Agent 都稳。Agent 的用武之地是:任务开放、步骤不确定、需要模型根据中间结果动态决定下一步。想清楚这一点,你的项目一开始就赢了一半。
3.2 建模期:编排层选择固定工作流还是自主循环
如果确实需要 Agent,下一个决策点是编排模式。固定工作流适合流程明确、步骤固定的场景,比如“查天气 → 生成穿衣建议 → 发送给用户”,每个环节独立成函数,串起来就行了。自主循环适合开放任务,模型自己决定调用什么工具、什么顺序。两者也可以混合:外层用固定工作流约束大阶段,内层在某个阶段里跑自主循环。
这个决策直接影响你的技术栈。固定工作流用 LangChain 的 Chain 或者纯 Python 都能做;自主循环建议上 LangGraph 这类带显式状态管理的框架。我在内部定过一个原则:如果流程的分支数超过五个,就不要再手写 while 循环了,全部迁移到图上。
3.3 模型选型期:上下文、延迟、并发与成本怎么平衡
模型选型不能只看“最强的是谁”,要平衡四个维度:上下文长度、首字延迟、并发吞吐和单次调用成本。一个常见做法是大小模型分层:用便宜的小模型做路由分类、信息抽取,把复杂推理任务交给大模型。比如客服系统里,先让小模型判断用户意图,再决定是否进入高成本的大模型链路。这样整体账单能降 40% 以上,响应速度却更稳。
如果业务场景对私有化部署有强需求,就绕不开本地模型的选型。本地模型的好处是延迟可控、数据不出域、没有单次调用费,坏处是并发吞吐和效果通常弱于商用大模型。我的建议是先用商用模型验证产品逻辑,再在规模化阶段评估有没有必要迁移到本地模型,不要一开始就在模型层纠结。
3.4 工具集成期:注册协议、超时与错误语义
决策点四发生在写第一个工具的时候。工具注册协议上,主流的做法是原生 function calling,也就是把工具描述和参数 Schema 直接声明给模型。现在还有一个趋势是 MCP 这类标准化工具协议,把工具列表、调用和结果格式标准化,降低多业务之间的集成成本。但标准化的前提是大家都遵守同一套规范,如果你只是内部两三个系统对接,MCP 带来的额外复杂度可能大于收益。
工具的超时和错误语义同样要在注册阶段定清楚。我给每个工具规定了三件事:最长执行时间、失败时的错误码、失败后是否允许重试。模型看到工具返回的错误文本后,会决定是换一种方式再试一次还是直接放弃。如果错误文本含糊,模型会在同一类错误上反复横跳,白白烧掉十几轮 token。
3.5 记忆设计期:记忆做到多厚才需要向量库
记忆设计决策的核心是:哪些信息需要跨会话保留,哪些只活在当前会话里。很多项目一上来就上向量库,结果召回质量差、维护成本高、效果还未必比简单的 KV 缓存好。如果记忆需求只是“记住用户名字、最近一次选择、当前任务编号”,用 Redis 存 JSON 就够了,完全不需要向量。
需要向量库的信号是:你确实要基于语义相似度去召回历史信息。典型场景是知识库检索、历史工单匹配、多轮对话里的相似问题联想。即便如此,也要先做好文本切分和召回排序,再谈向量库选型。记忆做的过程里我会遵循一条原则:能用结构化字段解决的不放向量库,能放向量库解决的不塞完整上下文。
3.6 提示词工程期:用工具说明和 Few-shot 锁住输出格式
提示词工程在框架里常常被低估,但它决定了整个系统的稳定性下限。我的做法是:系统 Prompt 只负责宏观约束和边界声明,具体行为的引导尽量靠工具描述和少量示例完成。工具描述写得越精确,模型越不容易调用错工具。比如“查询用户订单”和“按订单号查询订单详情”看起来差不多,但前者模糊,后者明确,模型在模糊描述下更可能产生歧义调用。
Few-shot 示例对格式稳定特别有效。我曾经接手过一个抽卡类 Agent,模型输出字段偶尔会把价格和折扣顺序搞反,无论怎么在 Prompt 里强调都没用。后来在工具定义的 description 里补了一个标准示例,问题立刻消失。这个经验说明:与其跟模型反复讲“不要”“必须”,不如给它一个可以直接模仿的范式。
3.7 上线期:评测集、监控指标与降级方案
最后一个决策点发生在上线前夜:你拿什么证明这个 Agent“能上线”。我要求任何 Agent 项目上线前必须有三样东西:一份带 golden answer 的评测集;一组在线监控指标;一个明确的降级方案。
评测集不用大,五十到一百条覆盖核心场景就够,但必须是真实用户数据脱敏后的样本。监控指标也不追求多,我常用的五个是:任务完成率、平均步数、工具调用失败率、单任务 token 消耗、用户侧响应延迟。降级方案指当 Agent 连续失败或者核心模型服务抖动时,系统能否回退到固定流程、人工接管的通道是否存在。这三个都齐了,上线才不至于提心吊胆。
4. 从单 Agent 到并发服务化:那些 Demo 里看不出来的差距
本地把 Agent 跑通,和把它变成一个能扛真实流量的服务,中间隔着一整座山。这一章专门聊并发服务化和中台问题,也是最近 AI Agent 圈子里讨论最多、踩坑最狠的地方。
4.1 模型和 HTTP 服务组装后的正确姿势:慢响应不代表坏设计
Agent 服务最大的特点就是慢。一个任务可能包含三到五轮模型调用,每一轮都要两到五秒,整体响应奔着一二十秒去。如果你用同步 HTTP 接口直接暴露给前端,结果就是前端请求长时间挂起,网关超时,用户反复刷新,触发更多并发……然后雪崩。
我推荐的做法是区分两类任务。面向实时交互的简单任务,用 SSE 或者 WebSocket 做流式输出,模型每输出一段内容就推给前端,用户感知响应快。面向后台的复杂任务,干脆上任务队列:Agent 服务只负责消费任务,跑完后把结果写到存储里,前端轮询或者订阅通知。把这两类拆开,你的服务模型会健康很多。
4.2 并发时最容易炸的三个位置:超时、限流、状态串线
并发一上来,最先炸的永远是这三个位置。
第一个是超时。模型调用是外部依赖,接口偶发变慢太正常了。每个模型调用都要有超时控制,并且超时之后要有重试策略和熔断机制。我习惯给模型调用设两到三倍于 P99 延迟的超时值,连续失败超过阈值就熔断,把流量切到备用模型或者降级方案。
第二个是限流。Agent 的一个任务会放大成多轮模型调用和工具调用,所以限流不能只看用户请求数,还要看模型 API 的并发配额。我在网关层做两层限流:按用户限 QPS,按模型供应商限总吞吐。这样单个毒瘤用户拖垮不了全站。
第三个是状态串线,这也是最隐蔽的坑。多个用户同时在跑 Agent,如果会话状态没有和用户 ID 正确绑定,就会出现 A 用户的上下文跑到 B 用户那里。排查这类 bug 非常痛苦,我会在每个 Agent 实例上强制一个session_id,所有 memory、trace、工具调用记录都归到同一个 session 下,谁也不能例外。
4.3 Agent 中台到底要不要建,怎么取舍
“Agent 中台”是这两年的热词,我的态度是:它是个好东西,但绝大多数团队用不上。中台本质是把工具注册、模型路由、Prompt 配置、会话存储、评测能力这些公共组件抽成一个独立服务,让多个业务线复用。如果你手头只有两三个试点项目,建中台等于给项目上了双重复杂度。
真正适合建中台的企业级信号有三个:业务线超过五个且都在做 Agent;多个业务需要共享同一套工具注册表;你希望统一管理模型成本和 Prompt 版本迭代。中台的建设不是把聪明 Agent 圈起来,而是把治理能力沉淀下来。我见过不少团队把“开源 Agent 框架”部署了一遍就当是中台,这其实还差得远。
另外提一句技术栈上的趋势。现在很多团队处理高并发接入层时倾向用 Rust 或 Go 写网关和服务注册,业务层继续用 Python 的 FastAPI 加 LangGraph 写 Agent 逻辑,两边通过标准协议通信。这套组合本质上是在高并发入口和复杂业务编排之间做了隔离。如果你还没有明确的性能瓶颈,没必要一上来就引入 Rust,先用简单架构跑通,等真的到了十万级并发再说。
5. 给正在做 Agent 工程的人:几条底层经验
文章最后,分享几条我在实践里反复验证过的经验。
第一,先用“窄 Agent”解决真问题,再谈通用大管家。全知全能的超级 Agent 在今天的技术条件下既不现实,也难以维护。把一个具体场景做到 90% 的完成率,远比做一个样样通样样松的通用助手有价值。
第二,从第一天就保证系统可干预。Agent 一定会犯错,所以系统里要留有人工接管的入口:任务卡死时能手动终止、工具执行错误时能插入人工修正、对话过程能看到完整链路。没有这些干预手段,上线后你会被投诉淹没。
第三,学习路径别跳级。我的建议是:先手写一个 ReAct 循环理解原理,再用 LangChain 做工具编排和记忆管理,接着把流程迁移到 LangGraph 这类状态机框架,最后围绕并发、评测和中台化做工程化改造。每一步都是下一步的地基,直接抄大型开源项目其实很难内化成自己的东西。
Agent 工程到现在也没有标准答案,但把七要素和七个决策点想清楚,至少能让你在面对模型抖动、工具失控、并发压垮服务这些问题时,知道该往哪里看、该改哪一层。