1. 为什么“17 种架构”这个数字值得认真对待
第一次看到“现代 AI 代理设计:17 种架构的系统化实战合集”这个标题,我的反应是:又来了一个堆概念的合集。但翻完手头几个用 LangChain、LangGraph 和 Jupyter Notebook 落地的项目之后,我改变了看法——17 这个数字其实卡在一个很微妙的位置上。少于 10 种,覆盖不了从单轮工具调用到多代理协作的完整光谱;多于 20 种,大部分就会退化成论文里的变体,工程上根本用不上。17 种刚好能把“一个 LLM 加几个工具”到“一群代理互相评审”这条主线讲完整,而且每一种都能对应到真实项目里踩过的坑。
这篇东西不是论文导读,也不是 API 手册。我想做的是把这 17 种架构按“能力递进”的顺序拆开,讲清楚每一种解决什么问题、在什么场景下该用、用 LangGraph 这类图编排框架怎么落地、以及我在 Jupyter Notebook 里调试时总结出来的那些不太上得了台面的技巧。如果你正在用 LangChain 搭第一个代理,或者已经在用 LangGraph 做多代理编排但总觉得哪里别扭,这篇应该能帮你省下不少试错时间。核心关键词就三个:AI 代理、架构、LangGraph,其余像 Jupyter Notebook、LangChain 这些是贯穿始终的工具链。
先说清楚一个前提:这 17 种架构不是互斥的选项,而是可以嵌套组合的积木。一个生产级的代理系统,往往是“ReAct 循环 + 工具路由 + 记忆分层 + 多代理评审”的叠加体。所以别指望看完就能选一个“最优架构”直接抄,重点是理解每种架构的适用边界,然后在自己的场景里做减法。
2. 从单代理到多代理:17 种架构的分层逻辑
2.1 按“控制流复杂度”而不是“功能”来分类
网上很多代理架构的整理是按功能分的:问答代理、代码代理、检索代理、规划代理……这种分法看着清楚,实际用起来很坑,因为同一个功能可以用完全不同的控制流实现,性能差好几倍。我更喜欢按控制流复杂度来分层,从“线性执行”到“图状编排”再到“涌现式协作”,一共四层。
第一层是单次调用型,包括最朴素的 Prompt 直出、函数调用(Function Calling)、以及带结构化输出的 JSON 模式。这一层没有循环,一次请求一次响应,适合分类、抽取、格式化这类确定性任务。很多人一上来就上 ReAct,其实大部分场景根本不需要循环,白白增加延迟和 token 消耗。
第二层是单代理循环型,核心是 ReAct(Reasoning + Acting)及其变体。代理在一个循环里反复“思考—行动—观察”,直到任务完成或达到步数上限。这一层是当前绝大多数 AI 代理的真实形态,LangChain 的 AgentExecutor、LangGraph 的 ReAct 模板都属于这里。变体包括 Plan-and-Execute、Reflexion、以及带自我修正的循环。
第三层是多代理协作型,包括主管-工人(Supervisor-Worker)、辩论(Debate)、评审(Critique)、以及层级式(Hierarchical)编排。这一层的关键不是“多个代理”,而是代理之间有明确的信息传递协议和终止条件。LangGraph 的 StateGraph 就是为这一层设计的。
第四层是动态涌现型,包括市场机制、群体协作、以及基于环境反馈的自组织架构。这一层目前工程落地案例还不多,但在一些复杂仿真和优化任务里已经能看到雏形。
2.2 17 种架构的完整清单与一句话定位
把四层展开,我整理出的 17 种架构如下表。这张表建议先扫一眼,后面会挑重点展开。
| 编号 | 架构名称 | 所属层级 | 一句话定位 |
|---|---|---|---|
| 1 | 直接 Prompt 调用 | 单次调用 | 无状态、无工具,最省成本 |
| 2 | 结构化输出模式 | 单次调用 | 强制 JSON/Schema,便于下游解析 |
| 3 | 函数调用(Function Calling) | 单次调用 | 模型自主选择工具,单轮 |
| 4 | 工具路由(Tool Routing) | 单次调用 | 先分类再分发,降低误调用 |
| 5 | ReAct 循环 | 单代理循环 | 思考-行动-观察,最经典 |
| 6 | Plan-and-Execute | 单代理循环 | 先出计划再逐步执行 |
| 7 | Reflexion 自我反思 | 单代理循环 | 失败后生成反思再重试 |
| 8 | 带记忆的对话代理 | 单代理循环 | 短期+长期记忆分层 |
| 9 | RAG 增强代理 | 单代理循环 | 检索与生成交替进行 |
| 10 | 代码解释器代理 | 单代理循环 | 在沙箱里执行代码验证 |
| 11 | 主管-工人模式 | 多代理协作 | 一个调度多个执行 |
| 12 | 辩论模式 | 多代理协作 | 多视角对抗后收敛 |
| 13 | 评审-修正模式 | 多代理协作 | 生成者与评审者分离 |
| 14 | 层级式编排 | 多代理协作 | 多层主管,适合大任务 |
| 15 | 并行扇出-汇聚 | 多代理协作 | 同时跑多个子任务再合并 |
| 16 | 市场/竞标机制 | 动态涌现 | 代理按效用竞争任务 |
| 17 | 群体自组织 | 动态涌现 | 无中心,靠规则涌现 |
这张表里,1 到 10 是单代理范畴,11 到 15 是多代理协作,16 和 17 属于探索性架构。实际项目里用得最多的是 5、6、8、9、11、13 这六种,其余要么是它们的变体,要么是特定场景才用得上。
2.3 为什么用 LangGraph 而不是纯 LangChain 来表达这些架构
LangChain 的 AgentExecutor 本质上只实现了第 5 种(ReAct 循环)和它的几个变体,控制流是写死的:模型输出动作、执行工具、把结果塞回上下文、再调用模型。你想加一个“评审节点”或者“并行分支”,就得自己写循环和状态管理,很快就变成一团意大利面。
LangGraph 的核心抽象是状态图:节点是函数,边是转移条件,整个代理的执行过程就是状态在图上流动。这个抽象的好处是,上面 17 种架构里有 14 种可以直接用“节点+条件边”表达出来,剩下 3 种(市场、群体)也能用动态生成节点的方式近似。我在 Jupyter Notebook 里做原型时,基本流程是:先用 LangGraph 把控制流画出来,跑通逻辑,再把稳定的部分固化成生产代码。Notebook 的好处是每个节点可以单独执行、单独打印状态,调试多代理系统时这一点太重要了。
提示:LangGraph 的状态定义建议用 TypedDict 而不是 Pydantic 模型,前者在 Notebook 里打印更干净,后者在需要严格校验时再换。这个取舍我后面会细说。
3. 单代理循环:ReAct 及其五个关键变体
3.1 ReAct 循环的最小实现与三个致命细节
ReAct 是第 5 种架构,也是所有代理架构的基石。它的逻辑用一句话说就是:让模型输出“思考”和“行动”,执行行动得到“观察”,把观察拼回上下文,再让模型继续。听起来简单,但我在实际项目里踩过的坑几乎都集中在这三个细节上。
第一个细节是停止条件。很多教程只写“直到模型输出最终答案”,但真实场景里模型会陷入死循环,反复调用同一个工具。我的做法是设三重保险:最大步数(一般 8 到 12 步)、重复动作检测(连续两次相同工具+相同参数就强制终止)、以及超时。在 LangGraph 里,这三重保险分别对应递归限制、条件边里的状态检查、以及节点内的超时装饰器。
第二个细节是观察结果的截断。工具返回的内容可能很长,比如一次网页抓取返回几万字,直接塞回上下文会爆 token。我的经验是:结构化数据保留完整,非结构化文本按“前 500 字 + 后 500 字 + 中间摘要”的方式压缩。摘要可以用一个小模型生成,也可以用规则截取。这一步不做,代理跑到第三步就会因为上下文超限而失败。
第三个细节是思考内容的可见性。ReAct 的“思考”部分到底要不要展示给用户?我的建议是:调试阶段全部打印,生产环境只展示行动和最终答案。原因很简单,模型的思考过程经常包含自我矛盾和对工具的误判,展示出来会让用户困惑。但在 Notebook 里调试时,把思考打印出来是定位问题最快的方式。
# LangGraph 里 ReAct 循环的核心结构(简化版) from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] steps: int def should_continue(state: AgentState): last = state["messages"][-1] if state["steps"] >= 10: return "end" if hasattr(last, "tool_calls") and last.tool_calls: return "tools" return "end" graph = StateGraph(AgentState) graph.add_node("agent", call_model) graph.add_node("tools", execute_tools) graph.set_entry_point("agent") graph.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END}) graph.add_edge("tools", "agent")这段代码里steps字段就是步数保险,should_continue里同时检查了工具调用和步数。实际项目里我还会在execute_tools里加一个动作指纹缓存,防止重复调用。
3.2 Plan-and-Execute:什么时候“先想清楚”比“边做边想”更好
第 6 种架构 Plan-and-Execute 的核心思想是:先用一次模型调用生成完整计划,再逐步执行每一步。它和 ReAct 的区别在于,ReAct 是每一步都重新决策,Plan-and-Execute 是决策一次、执行多次。
这个架构适合什么场景?我的判断标准是:任务步骤之间依赖关系明确、且总步数可预估。比如“查三个城市的天气并对比”,这种任务用 Plan-and-Execute 就很合适,因为计划是稳定的。反过来,如果任务需要根据中间结果动态调整方向,比如“帮我找一篇合适的论文并总结”,那 ReAct 更合适,因为你不知道检索结果会把你引向哪里。
实测下来,Plan-and-Execute 的 token 消耗通常比 ReAct 低 30% 到 50%,因为不需要每步都带上完整历史。但它的缺点是计划一旦生成就很难改,如果第一步就错了,后面全错。我的折中方案是“计划+局部重规划”:生成计划后,每执行完一步检查一次是否偏离目标,偏离超过阈值就重新生成剩余步骤。这个逻辑在 LangGraph 里用一个条件边就能实现。
3.3 Reflexion 与记忆分层:让代理从失败里学到东西
第 7 种 Reflexion 和第 8 种带记忆的对话代理,经常被混在一起讲,但它们的侧重点不同。Reflexion 关注的是单次任务内的自我修正:代理执行失败后,生成一段“反思文本”,把反思加入上下文再重试。记忆分层关注的是跨会话的信息保留:短期记忆是当前对话,长期记忆是向量库里的历史经验。
我在一个客服代理项目里同时用了这两个。Reflexion 负责处理“这次回答被用户否定了”的情况,记忆分层负责“这个用户上周问过类似问题”。具体实现上,Reflexion 的反思文本我建议限制在 200 字以内,太长了会挤占上下文;长期记忆的检索我建议用“最近性+相关性”的混合排序,纯向量相似度会漏掉时间敏感的信息。
注意:Reflexion 有一个反直觉的坑——反思次数不是越多越好。我试过让代理连续反思 5 次,结果它开始“过度反思”,把原本正确的部分也改错了。实测 2 到 3 次是比较稳的上限。
3.4 RAG 增强代理与代码解释器:两种“外部能力”的接入方式
第 9 种 RAG 增强代理和第 10 种代码解释器代理,本质上是给代理接入两种不同的外部能力:前者接入知识,后者接入计算。它们的共同点是都需要一个“验证”环节——RAG 要验证检索到的内容是否真的相关,代码解释器要验证代码是否真的能跑通。
RAG 增强代理最常见的坑是“检索到不相关内容但模型硬用”。我的解法是在检索后加一个轻量的相关性打分节点,分数低于阈值就触发重新检索或直接告诉模型“没有找到相关资料”。这个打分节点可以用一个小模型,也可以用 BM25 加向量分数的加权组合。
代码解释器代理的坑更隐蔽:模型生成的代码经常在边界条件上出错,比如空列表、除零、编码问题。我的做法是在沙箱里执行代码时,强制捕获所有异常并把异常信息作为观察结果返回,让模型自己修。实测下来,加上异常反馈后,代码任务的一次通过率能从 40% 左右提到 70% 以上。
4. 多代理协作:从主管模式到并行扇出
4.1 主管-工人模式:最实用的多代理起点
第 11 种主管-工人模式是我最推荐的多代理入门架构。它的结构很简单:一个主管代理负责理解任务、拆解子任务、分发给工人代理,工人代理执行完把结果交回主管,主管决定是继续分发还是汇总输出。
这个架构的关键设计点是主管的决策粒度。主管可以只做一次分发(一次性拆解所有子任务),也可以做多轮分发(根据工人返回结果决定下一步)。前者实现简单但灵活性差,后者灵活但主管容易变成瓶颈。我的经验是:子任务数量少于 5 个时用一次性分发,多于 5 个或者子任务之间有依赖时用多轮分发。
在 LangGraph 里,主管-工人模式用一个中心节点加多个工人节点实现,主管节点输出下一个要调用的工人名称,条件边根据这个名称路由。这里有个细节:工人节点的返回值要统一格式,否则主管解析起来会很痛苦。我一般要求所有工人返回{"status": "success/failure", "result": "...", "confidence": 0.0-1.0}这样的结构。
4.2 辩论与评审-修正:两种让代理“自我纠错”的路径
第 12 种辩论模式和第 13 种评审-修正模式,都是通过引入“第二个视角”来提高输出质量,但机制不同。辩论模式是让多个代理对同一问题给出不同答案,然后通过多轮辩论收敛到一个共识;评审-修正模式是一个代理生成、另一个代理评审、生成者根据评审意见修正。
辩论模式适合开放性问题,比如方案设计、风险评估,因为这类问题没有唯一正确答案,多视角能暴露盲区。评审-修正模式适合有明确标准的问题,比如代码审查、文案校对,因为评审者有明确的检查清单。
我在实际项目里用得更多的是评审-修正,因为它的成本可控(两倍代理数量),而辩论模式通常需要 3 个以上代理且多轮交互,成本是评审-修正的好几倍。评审-修正的一个关键技巧是:评审者的 prompt 里要明确列出检查维度,否则它会给出“看起来不错”这种无效反馈。我一般会写 5 到 7 个具体维度,比如“事实准确性、逻辑连贯性、格式合规性、语气一致性”等。
4.3 层级式编排与并行扇出:处理大任务的两个手段
第 14 种层级式编排和第 15 种并行扇出-汇聚,解决的是“任务太大,一个主管管不过来”的问题。层级式编排是主管下面再设子主管,形成树状结构;并行扇出是把无依赖的子任务同时执行,最后汇聚结果。
层级式编排的坑在于信息在层级间传递时会失真。子主管向上汇报时如果只给结论不给依据,顶层主管就很难做判断。我的做法是要求每一层汇报时都带上“结论+关键依据+置信度”三件套,顶层主管根据置信度决定是否需要深入某一层。
并行扇出的坑在于结果合并。多个子任务同时跑,返回的结果格式可能不一致,合并时容易出错。我的做法是定义一个统一的合并 schema,每个子任务必须按 schema 返回,合并节点只做 schema 校验和拼接,不做语义理解。语义理解留给最后的汇总代理。
# 并行扇出的 LangGraph 结构示意 from langgraph.graph import StateGraph, END def fan_out(state): # 返回多个子任务,触发并行分支 return [{"task": t} for t in state["subtasks"]] def merge(state): # 汇聚所有分支结果 return {"final": combine(state["results"])} graph = StateGraph(State) graph.add_node("planner", plan) graph.add_node("worker", execute) graph.add_node("merger", merge) graph.add_edge("planner", "worker") graph.add_edge("worker", "merger") graph.add_edge("merger", END)这段代码里worker节点会被并行触发多次,LangGraph 会自动处理分支和汇聚。实测下来,3 到 5 个并行分支的加速比最明显,超过 8 个分支后因为合并开销增加,收益递减。
4.4 动态涌现架构:市场机制与群体自组织的现实边界
第 16 种市场/竞标机制和第 17 种群体自组织,属于探索性架构,工程落地案例还不多,但思路值得了解。市场机制是让代理对任务“竞标”,报价低的代理获得任务,适合任务同质化、代理能力可量化的场景。群体自组织是代理之间通过局部规则交互,整体涌现出有序行为,适合仿真和优化类任务。
我在一个资源调度的小实验里试过市场机制,结论是:报价函数的设计比代理本身更重要。如果报价只考虑成本不考虑质量,最后会选出一堆便宜但不可靠的代理。我的做法是报价 = 成本权重 × 预估成本 + 质量权重 × 历史失败率,两个权重根据任务重要性动态调整。
群体自组织我目前只在仿真环境里跑过,真实项目里还没敢用,因为它的行为不可预测,调试起来非常痛苦。如果你的场景对可解释性有要求,建议先跳过这两种。
5. 在 Jupyter Notebook 里调试代理架构的实操流程
5.1 为什么 Notebook 是代理调试的最佳场所
代理系统的调试和普通程序不一样,它的“bug”往往不是崩溃,而是行为不符合预期:该调用工具的时候不调用、该停止的时候不停、该传递的信息传丢了。这类问题用传统断点调试很难定位,因为你没法在模型内部打断点。Notebook 的优势在于每个节点可以单独执行、状态可以随时打印、中间结果可以反复重放。
我的标准调试流程是:先在 Notebook 里把每个节点写成独立函数,用假数据跑通;再把节点组装成 LangGraph 图,用真实模型跑;最后把稳定的图导出成生产代码。这个流程的关键是“假数据跑通”这一步,很多人跳过它直接上真实模型,结果模型的不确定性掩盖了逻辑错误,排查起来事倍功半。
提示:Jupyter Notebook 的默认保存路径建议改到一个专门的代理项目目录,避免和系统临时文件混在一起。改路径的方法是在启动前设置配置,或者在 Notebook 里用
%cd魔法命令切换。这个细节看似无关紧要,但项目文件一多,路径混乱会浪费大量时间。
5.2 状态可视化:把代理的“思考过程”画出来
LangGraph 的状态是字典,直接打印会很难看。我的做法是写一个小的可视化函数,把状态里的消息列表渲染成“角色:内容”的格式,工具调用单独高亮。这样一眼就能看出代理在哪一步做了什么决策。
def render_state(state): for msg in state["messages"]: role = msg.get("role", "unknown") content = msg.get("content", "") if msg.get("tool_calls"): for tc in msg["tool_calls"]: print(f"[TOOL] {tc['name']}({tc['args']})") print(f"[{role.upper()}] {content[:200]}") print(f"--- steps: {state.get('steps', 0)} ---")这个函数在调试多代理系统时特别有用,因为你能看到消息在不同代理之间是怎么传递的。我经常用它发现“主管把任务分给了错误的工人”或者“工人返回的结果主管没解析”这类问题。
5.3 断点续跑与状态快照:省下重复调用的钱
代理调试最烧钱的地方是每次改动都要从头跑一遍。LangGraph 支持状态快照,可以在任意节点保存状态,下次从快照恢复。我的做法是在每个关键节点后保存快照到本地文件,调试下游节点时直接从快照加载,跳过上游的模型调用。
这个技巧在调试多代理系统时价值尤其大,因为多代理的调用链很长,从头跑一次可能要几十次模型调用。用快照后,我通常只需要重跑改动的那一个节点,成本能降到原来的十分之一以下。
import pickle def save_snapshot(state, path): with open(path, "wb") as f: pickle.dump(state, f) def load_snapshot(path): with open(path, "rb") as f: return pickle.load(f)注意快照里如果包含模型客户端的连接对象,反序列化会失败。我的做法是快照只保存纯数据(消息、步骤数、中间结果),客户端对象在加载后重新创建。
5.4 从 Notebook 到生产:哪些东西必须改掉
Notebook 里跑通的代码不能直接上生产,有几样东西必须改。第一是硬编码的 API 密钥和模型名称,要改成环境变量或配置中心。第二是打印语句,要改成结构化日志。第三是同步调用,生产环境通常需要异步或并发。第四是快照文件,生产环境要用数据库或对象存储替代本地文件。
我一般会保留 Notebook 作为“架构原型”,生产代码单独一个仓库,两者共享核心的节点函数。这样架构调整时先在 Notebook 里验证,验证通过再把节点函数同步到生产仓库。这个流程听起来麻烦,但比直接在生