Agent 架构设计:Plan-then-Execute vs ReAct 循环,Agent 的状态持久化与记忆管理
为什么 Agent 架构是面试必考题
Agent 是 2025-2026 年 AI 应用最热的方向,没有之一。面试官问 Agent 架构,本质上是在问:你能不能把一个 LLM 从"问答机器人"升级成"能自主完成任务的执行者"。
我面过 30+ 候选人,三分之二倒在这个问题上。典型状态:
- 能背出 ReAct 论文摘要的,这是基础分
- 能写代码实现 Plan-then-Execute 的,这是加分
- 能说出生产环境下 Agent 每轮对话的平均 token 消耗、记忆管理的内存配额、状态持久化的 Redis 读写 QPS 应该怎么设计的,这是满分
这个问题的难度在于,它不光考你对 LLM 的理解,还考你对系统设计、状态管理、异常处理的理解。纯后端转过来的同学,最容易在"Agent 的短期记忆到底该存多少"和"Agent 状态怎么序列化"这两个点上翻车。
两种主流架构:Plan-then-Execute vs ReAct
Plan-then-Execute(计划先行)
核心思路:模型先制定完整计划,然后按顺序执行。适合任务明确、步骤固定的场景。
完整时序流程:
用户输入: "帮我对比这三款手机的配置和价格"
↓
LLM 生成计划(单次推理,约 200-400 token)
↓ 计划内容:
Step 1: search_product("iPhone 16 Pro Max") → 获取配置和价格
Step 2: search_product("Samsung Galaxy S25 Ultra") → 获取配置和价格
Step 3: search_product("Xiaomi 15 Pro") → 获取配置和价格
Step 4: compare_products(step1_result, step2_result, step3_result) → 生成对比表
↓
依次执行 Step 1 → Step 2 → Step 3 → Step 4(每个步骤 1 次工具调用)
↓
汇总结果,LLM 最后一次推理生成最终输出(约 300-500 token)
↓
最终输出给用户class PlanExecuteAgent:
def __init__(self, llm, tools):
self.llm = llm
self.tools = {t.name: t for t in tools}
def run(self, task: str) -> str:
# Step 1: 制定计划
plan = self.llm.chat(f"为以下任务制定执行计划:{task}")
# Step 2: 按计划执行
results = []
for step in plan.steps:
result = self.execute_step(step)
results.append(result)
# Step 3: 汇总结果
return self.llm.chat(f"基于以下结果回答问题:{results}")
def execute_step(self, step):
tool = self.tools.get(step.tool_name)
if not tool:
return f"工具 {step.tool_name} 不存在"
return tool.run(**step.parameters)优点:执行路径可预测,便于调试和审计。缺点:不能应对中间步骤失败的情况,计划一旦制定就难以调整。
ReAct 循环(思考-行动-观察-再思考)
ReAct(Reasoning + Acting)是 Yao et al. 在 2023 年提出的模式,核心是让模型在每一步都交替输出"思考"和"行动",然后观察结果,决定下一步怎么做。
ReAct 单步执行时序(以一次查询天气为例):
用户输入: "北京明天能去爬山吗?我需要知道天气和空气质量"
↓
LLM 第 1 轮推理:
Thought: "用户想知道明天北京的天气和空气质量,我可以先查天气。"
Action: get_weather("北京", "2026-07-24")
↓
工具返回: {"temperature": 35, "humidity": 80%, "condition": "雷阵雨"}
↓
LLM 第 2 轮推理:
Thought: "温度 35 度,湿度 80%,雷阵雨,不太适合爬山。但还需要确认空气质量。"
Action: get_air_quality("北京")
↓
工具返回: {"AQI": 85, "pm2.5": 65, "level": "良"}
↓
LLM 第 3 轮推理:
Thought: "AQI 85 属于良,不算太差。但明天有雷阵雨,温度 35 度湿度 80%,户外爬山风险高。"
Final Answer: "建议明天不要去爬山。北京明天有雷阵雨,气温 35°C 湿度 80%,体感闷热,雷雨天气山区有滑坡风险。空气质量 AQI 85 属于良,不是主要问题,但天气条件本身不适合户外登山。"
↓
最终输出给用户每轮对话的典型 token 消耗分布:Thought 约 50-150 token,Action 约 30-80 token(含工具名和参数),Observation 取决于工具返回的数据量(从 50 token 到 5000 token 不等)。一次 10 步的 ReAct 循环,仅系统提示词加上对话历史,上下文窗口消耗 3000-8000 token 是常态。
class ReActAgent:
def __init__(self, llm, tools, max_iterations=10):
self.llm = llm
self.tools = {t.name: t for t in tools}
self.max_iterations = max_iterations
def run(self, task: str) -> str:
messages = [{"role": "user", "content": task}]
for i in range(self.max_iterations):
response = self.llm.chat(messages, tools=list(self.tools.values()))
if response.tool_calls:
# 执行工具调用
for tc in response.tool_calls:
tool = self.tools[tc.function.name]
result = tool.run(**tc.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": str(result)
})
else:
# 模型输出最终答案,结束
return response.content
return "已达最大迭代次数,任务中止"优点:灵活,能处理不确定的任务路径;中间步骤失败后可以调整策略。缺点:可能陷入循环;执行路径不固定,调试困难;token 消耗大。
选型对比:从生产数据看
| 维度 | Plan-then-Execute | ReAct |
|---|---|---|
| 适用场景 | 用户明确的任务(如"帮我下单") | 开放式探索(如"研究一下这个行业") |
| 平均每任务 token 消耗 | 约 1500-3000 token(计划+执行+汇总各一次) | 约 4000-12000 token(每步都要 LLM 调用) |
| 平均端到端延迟 | 2-5 秒(LLM 调用 2-3 次) | 5-20 秒(LLM 调用 5-15 次) |
| 调试难度 | 低(路径固定,可逐行打日志) | 高(路径不固定,需要完整 tracing) |
| 失败恢复 | 差(计划失败需从头重来) | 好(可在任意步骤调整策略) |
| 典型线上占比 | 约 30% 的场景 | 约 70% 的场景 |
线上数据:我在某 AI 客服系统上看到的实际占比——70% 的客服会话走 ReAct 模式(因为用户问题不可预测),30% 走 Plan-then-Execute(比如"退换货流程"这类固定流程)。但 ReAct 的 token 消耗是 Plan-then-Execute 的 3-4 倍,也就是说 ReAct 场景消耗了 85% 的 token 预算。
实际选型建议:用 ReAct 做高层任务规划,用 Plan-then-Execute 做子任务的具体执行。这就是目前最主流的混合架构。比如 OpenAI 的 Assistant API 就是这种思路——先定义一个多步骤的 function calling 计划,但每一步执行时又允许 LLM 根据结果动态调整。
状态持久化:Agent 的长期记忆
Agent 不能只活在当前对话里。用户可能说"我昨天查过的那部手机,帮我看看它的配件"。如果 Agent 没有状态持久化,它根本不记得"昨天查过哪部手机"。
状态持久化要存什么
class AgentState:
def __init__(self, session_id: str):
self.session_id: str = session_id
self.conversation_history: list[dict] = [] # 完整对话历史
self.tool_call_log: list[dict] = [] # 工具调用记录
self.intermediate_results: dict = {} # 中间结果缓存
self.current_step: str = "" # 当前执行步骤
self.memory_summary: str = "" # 长期记忆摘要存储方案
import redis.asyncio as aioredis
class RedisStateStore:
"""
基于 Redis 的 Agent 状态存储,适合高频读写场景。
"""
def __init__(self, redis_client, ttl_seconds=3600):
self.redis = redis_client
self.ttl = ttl_seconds # 1 小时过期,防止状态堆积
async def save(self, agent_state: AgentState):
key = f"agent:state:{agent_state.session_id}"
serialized = json.dumps(agent_state.__dict__, ensure_ascii=False)
# 序列化后的 AgentState 大小:约 5-50KB(取决于对话长度)
# 线上实测:单节点 Redis 6 可支撑 5000+ 并发 Agent 会话的状态读写
# QPS 估算:5000 会话 × 每会话平均 10 次读写/分钟 = 833 QPS
# 单个 Redis 节点 6 万 QPS 完全够用
await self.redis.setex(key, self.ttl, serialized)
async def load(self, session_id: str) -> AgentState | None:
key = f"agent:state:{session_id}"
data = await self.redis.get(key)
if not data:
return None
state = AgentState(session_id)
state.__dict__ = json.loads(data)
return state踩坑实录:某线上 Agent 系统,用 Redis 存状态,默认 TTL 设了 24 小时。结果一天下来,Redis 内存从 2GB 涨到 8GB,触发了 maxmemory-policy 驱逐。查日志发现是部分 Agent 死循环,一直在写入新状态,旧的又没到过期时间。最终改成了 1 小时 TTL + 每次更新时检查 key 大小,超过 50KB 就截断最早的历史记录。
另一个踩坑:同一系统中的 AgentState 序列化用 pickle,没有考虑版本兼容问题。模型升级后 AgentState 增加了新字段,旧序列化数据反序列化失败,导致所有历史会话恢复报错。后来统一改成 JSON + 版本号字段,反序列化时按版本兼容处理。
其他存储方案的对比:
| 方案 | 读写延迟 | 支持 TTL | 适合场景 | 典型配置 |
|---|---|---|---|---|
| Redis | <1ms | 原生支持 | 高频短期会话 | 4GB 内存,TTL 1h |
| PostgreSQL | 5-20ms | 手动清理 | 用户偏好等长期记忆 | 加索引,定期 vacuum |
| MongoDB | 3-10ms | TTL 索引 | 非结构化复杂状态 | 适当分片 |
| 本地内存 | <0.1ms | 无 | 单机测试,不可用于生产 | 限单个进程 |
记忆管理:短期记忆 + 长期记忆
Agent 的记忆管理是另一个容易踩坑的地方。不加限制地保留所有历史,token 消耗会爆炸;删得太狠,Agent 又会"失忆"。
记忆管理的三层架构
一条数据流的完整链路:
用户输入
→ 短期记忆窗口(保留最近 4000 token 的对话)
→ 同时从长期记忆向量库检索相关记忆(top-3 相似度,取 metadata.text)
→ 组合 prompt:系统提示词 + 检索到的长期记忆 + 短期记忆窗口 + 用户输入
→ LLM 生成回复
→ 工具调用结果写入短期记忆
→ 短期记忆超限 → 触发摘要压缩,历史部分迁移到向量库class AgentMemory:
def __init__(self, llm, vector_store, max_short_term_tokens=4000):
self.llm = llm
self.vector_store = vector_store # 向量数据库,存长期记忆
self.max_short_term_tokens = max_short_term_tokens
self.short_term: list[dict] = [] # 当前会话的对话历史
def add_message(self, message: dict):
self.short_term.append(message)
# 短期记忆超限时,触发摘要压缩
if self.count_tokens(self.short_term) > self.max_short_term_tokens:
self._compress()
def _compress(self):
"""将短期记忆压缩为摘要,存入长期记忆"""
# 取最近的几条消息保留,前面的压缩成摘要
recent = self.short_term[-5:] # 保留最近 5 轮对话
history = self.short_term[:-5]
summary = self.llm.chat(
f"请将以下对话压缩为 200 字以内的摘要,保留关键信息:{history}"
)
# 将摘要向量化存入长期记忆
vector = self.embed(summary)
self.vector_store.add(
id=f"summary_{int(time.time())}",
vector=vector,
metadata={"text": summary, "session_id": self.session_id}
)
self.short_term = recent
def query_memory(self, query: str) -> str:
"""从长期记忆中检索相关记忆"""
results = self.vector_store.similarity_search(query, k=3)
return "\n".join([r.metadata["text"] for r in results])核心机制:
- 短期记忆:当前会话的完整对话历史,滚动窗口(比如保留最近 4000 token)
- 摘要压缩:短期记忆超限时,将早期对话压缩成摘要,存入向量数据库
- 长期记忆:跨会话的知识,通过向量检索在需要时召回
记忆管理的常见坑
坑 1:不加限制地保留所有历史 一个 10 轮对话的 Agent 会话,如果每轮都包含工具调用的输入输出,token 消耗轻松破万。解决方案:设定明确的 token 预算上限,超出后丢弃最早的历史。
坑 2:摘要丢失关键细节 压缩摘要时,LLM 可能忽略一些看似不重要但实际关键的信息。比如用户说过"我是 chrome 浏览器用户",这个信息在 5 轮对话后触发摘要压缩时被丢弃了。第 8 轮 Agent 要给用户推荐浏览器插件时,完全不知道用户用的什么浏览器。解决方案:保留最近几轮对话的完整记录,只压缩更早的;同时识别出"用户画像"类信息单独存储。
坑 3:跨会话记忆遗忘 用户今天和一个 Agent 对话了 20 轮,明天再来,Agent 完全不记得。解决方案:每次会话结束时,用 LLM 生成一个会话摘要,存入用户维度的记忆表,下次启动时加载。
坑 4:向量检索的"近义词陷阱" 用户说"帮我查快递",向量检索可能召回"帮我查物流"的摘要,但漏掉了"我的包裹到哪了"。解决方案:增加 query 改写步骤,对用户输入做同义词扩展后再检索。
线上实测数据:某 Agent 系统上线摘要压缩后,单会话平均 token 消耗从 12000 降到 4500,减少了 62.5%。但用户满意度下降了 3 个百分点——因为压缩丢失了部分上下文。最终调参后定在 6000 token 窗口,满意度回升到 98%。
高阶话题:Agent 的可靠性工程
Plan 漂移
Agent 执行到第三步发现计划不合理,需要重新规划。解决方案:在 Plan-then-Execute 架构中增加"计划重评估"机制——每执行完一个步骤,让 LLM 检查当前计划是否仍然合理,不合理就重新规划。
class AdaptivePlanAgent(PlanExecuteAgent):
def run(self, task: str) -> str:
plan = self.llm.chat(f"制定计划:{task}")
results = []
for i, step in enumerate(plan.steps):
result = self.execute_step(step)
results.append(result)
# 每步执行后,让 LLM 评估计划是否仍然合理
if i < len(plan.steps) - 1:
should_replan = self.llm.chat(
f"任务:{task}\n已完成步骤:{i+1}\n结果:{result}\n"
f"剩余计划:{plan.steps[i+1:]}\n"
f"是否需要重新规划?回答 yes 或 no"
)
if should_replan.strip().lower() == "yes":
plan = self.llm.chat(
f"基于已完成的结果重新规划剩余步骤:{results}"
)
return self.llm.chat(f"汇总结果:{results}")线上数据:某问答 Agent 上线后,10% 的任务在第三步之后触发了重新规划。主要原因是 LLM 在做第一步计划时无法预知第二步的返回结果,所以计划本身就是"猜测"。加了重评估后,任务完成率从 78% 提升到 94%。
Agent 死循环
模型反复尝试同一个操作——比如"查询用户 ID"查询了 5 次都是同一个错误。解决方案:设置最大迭代次数 + 检测重复失败模式。
一个更精细的检测方案:
def detect_dead_loop(tool_call_log: list[dict], max_duplicate=3) -> bool:
"""检测最近 N 次工具调用是否存在重复失败模式"""
if len(tool_call_log) < max_duplicate:
return False
recent = tool_call_log[-max_duplicate:]
# 检查是否调用了同一个工具、相同的参数、得到了相同的错误
first = recent[0]
return all(
t["tool_name"] == first["tool_name"]
and t["arguments"] == first["arguments"]
and t["error"] == first["error"]
for t in recent
)线上踩坑:某 Agent 上线第一周,有 2.3% 的会话进入了死循环,平均每个死循环会话消耗了 28000 token 才被 max_iterations 截断。加了重复模式检测后,死循环在 3 次重复后自动触发熔断,平均 token 消耗降到 4000。
幻觉蔓延
一次错误判断导致后续所有步骤都基于错误前提。比如 Agent 误以为"用户 A 是管理员",后续所有操作都基于这个错误假设。解决方案:关键步骤的确认机制——在调用敏感操作前,让 LLM 输出确认信息,由用户或系统二次确认。
与 Java 生态的对接
8 年 Java 后端转 Agent 的同学,面试时一定会被问到:"你之前的 Java 微服务经验怎么用在 Agent 系统上?"
关键对接点:
Spring Boot 封装 Agent 服务:用 Spring Boot 包装 Agent 的入口,暴露 REST/gRPC 接口,复用现有的注册中心(Nacos)、配置中心(Apollo)、链路追踪(SkyWalking)。Agent 的 LLM 调用用 RestTemplate 或 WebClient 包装,复用现有的超时、重试、熔断配置。
状态持久化复用现有基础设施:Redis 集群、MySQL 分库分表、PostgreSQL 等,不用新引入组件。AgentState 的序列化复用 Java 的 Jackson 或 Protobuf,不走 Python 的 pickle。
异步处理:Agent 任务通常耗时较长(5-20 秒),用 Spring 的 @Async 或 Reactive 编程,避免阻塞 Tomcat 线程池。Tomcat 默认线程池 200,如果 50 个 Agent 任务各占 10 秒,其他 150 个请求就要排队等。换成 WebFlux 后,同样的 Agent 任务吞吐量提升了 3 倍。
序列化性能:Java 的 AgentState 序列化到 Redis 时,用 Protobuf 代替 JSON,序列化大小可减少 60%,速度提升 3-5 倍。以 1000 个会话状态批量读取为例,JSON 序列化 15KB 每个,网络传输 150ms;Protobuf 5KB 每个,传输 50ms。
// Java Agent 服务的核心入口,复用 Spring Boot 生态
@RestController
@RequestMapping("/agent")
public class AgentController {
private final AgentService agentService;
@PostMapping("/chat")
public Mono<ResponseEntity<AgentResponse>> chat(
@RequestBody AgentRequest request) {
return agentService.process(request)
.timeout(Duration.ofSeconds(30))
.map(ResponseEntity::ok)
.onErrorResume(TimeoutException.class,
e -> Mono.just(ResponseEntity.status(504).build()));
}
}面试高频追问
Q1:Agent 的上下文窗口如何管理? 追问意图:考察对 LLM 成本的理解。回答要点:定好 token 预算,短期记忆窗口内保留最新的对话,早期的用摘要压缩;长期记忆通过向量检索按需召回。一个实测数据:GPT-4 的 128K 上下文窗口,如果塞满工具调用日志,单次推理成本约 0.06 美元,10 轮对话下来成本就 0.6 美元。不控制窗口,成本先炸。
Q2:生产环境中 Agent 的延迟怎么优化? 追问意图:考察系统设计能力。回答要点:工具调用并行化(多个独立工具可以同时调用)、LLM 推理用流式输出、关键路径上做 caching(相同的工具调用结果可以缓存 5 分钟)。一个具体方案:用 CompletableFuture 把多个独立的工具调用做成并行,比如同时查天气和查空气质量,比串行快 40%。
Q3:Agent 怎么处理工具调用失败? 追问意图:考察异常处理。回答要点:重试(带指数退避,初始 1s,最大 30s)、降级(工具不可用时用 LLM 的预训练知识回答)、熔断(连续失败 3 次后暂停使用该工具 30 秒,参考 Hystrix 的半开状态)。注意:不要对所有工具统一重试策略,读操作(如查询)可以重试,写操作(如下单)必须幂等才能重试。
Q4:多个 Agent 怎么协作? 追问意图:考察多 Agent 架构理解。回答要点:可以分层(一个 Orchestrator Agent 管理多个 Worker Agent)、可以角色化(如 Researcher Agent + Writer Agent + Reviewer Agent),关键是要有共享的状态中心和消息队列。2025 年最新的 A2A 协议(Agent-to-Agent)就是 Google 提出的多 Agent 协作标准,定义了 Agent 之间的能力发现和任务路由。面试提到这个,说明你关注业界前沿。
Q5:Agent 的 tool calling 和传统微服务的 RPC 调用有什么区别? 追问意图:考你对 Agent 特殊性的理解。回答要点:传统 RPC 调用参数固定、返回格式固定、调用链路确定;Agent 的 tool calling 参数由 LLM 生成,可能不合法、遗漏必填字段、参数类型错误。所以 Agent 的 tool schema 定义必须比传统的 API 文档更严格——需要 enum 值、格式约束、必填标记,并且工具函数本身要做参数校验和友好错误提示。
2025-2026 年 Agent 架构的新趋势
MCP(Model Context Protocol)的影响
Anthropic 在 2024 年底推出的 MCP 协议,正在改变 Agent 的工具接入方式。传统 Agent 每接入一个新工具,需要写适配器代码 + 注册 tool schema。MCP 标准化了工具发现和调用的协议,任何 MCP 兼容的工具都可以被 Agent 动态发现和使用。
MCP 对 Agent 架构的影响:以前 Agent 的 tools 是静态注册的(启动时写死在代码里),MCP 让 tools 变成动态发现的。Agent 启动时通过 MCP 协议向工具服务器查询可用工具列表,然后动态构造 tool schema 传给 LLM。
A2A(Agent-to-Agent)协议
Google 2025 年推出的 A2A 协议,定义了 Agent 之间的通信标准。面试场景下提到 A2A,说明你关注跨 Agent 协作的标准化方案。
A2A 的核心概念:
- Agent Card:每个 Agent 的能力声明,类似微服务的 API 文档
- Task:Agent 之间的任务单元,支持异步和流式
- 能力发现:一个 Agent 可以查询另一个 Agent 的 Agent Card 来了解它能做什么
总结
Agent 架构不是非此即彼的选择题。Plan-then-Execute 和 ReAct 各有适用场景,实际生产中更常见的是混合方案——用 ReAct 的灵活性做任务规划,用 Plan-then-Execute 的可预测性做子任务执行。
状态持久化和记忆管理是 Agent 系统的"基础设施",比 Agent 的逻辑本身更考验系统设计能力。面试官问到这个层面,说明他真正关心的不是"Agent 能不能跑",而是"Agent 能不能在生产环境稳定跑"。
记住:没有持久化能力的 Agent 是玩具,没有记忆管理的 Agent 是金鱼。
面试送分题:如果面试官问你"你怎么设计一个生产级别的 Agent",你的回答结构应该是:先讲架构选型(Plan-then-Execute vs ReAct 的权衡),再讲状态持久化(Redis + 数据库),再讲记忆管理(短期+长期),再讲可靠性(Plan 漂移、死循环、幻觉蔓延),每一层都带线上数据和踩坑案例。最后收尾加上 MCP 和 A2A 这些新趋势,表示你持续关注行业动态。