Skip to content

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)

最终输出给用户
python
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 是常态。

python
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-ExecuteReAct
适用场景用户明确的任务(如"帮我下单")开放式探索(如"研究一下这个行业")
平均每任务 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 没有状态持久化,它根本不记得"昨天查过哪部手机"。

状态持久化要存什么

python
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 = ""                # 长期记忆摘要

存储方案

python
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
PostgreSQL5-20ms手动清理用户偏好等长期记忆加索引,定期 vacuum
MongoDB3-10msTTL 索引非结构化复杂状态适当分片
本地内存<0.1ms单机测试,不可用于生产限单个进程

记忆管理:短期记忆 + 长期记忆

Agent 的记忆管理是另一个容易踩坑的地方。不加限制地保留所有历史,token 消耗会爆炸;删得太狠,Agent 又会"失忆"。

记忆管理的三层架构

一条数据流的完整链路:

用户输入
  → 短期记忆窗口(保留最近 4000 token 的对话)
    → 同时从长期记忆向量库检索相关记忆(top-3 相似度,取 metadata.text)
      → 组合 prompt:系统提示词 + 检索到的长期记忆 + 短期记忆窗口 + 用户输入
        → LLM 生成回复
          → 工具调用结果写入短期记忆
            → 短期记忆超限 → 触发摘要压缩,历史部分迁移到向量库
python
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 检查当前计划是否仍然合理,不合理就重新规划。

python
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 次都是同一个错误。解决方案:设置最大迭代次数 + 检测重复失败模式。

一个更精细的检测方案:

python
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 系统上?"

关键对接点:

  1. Spring Boot 封装 Agent 服务:用 Spring Boot 包装 Agent 的入口,暴露 REST/gRPC 接口,复用现有的注册中心(Nacos)、配置中心(Apollo)、链路追踪(SkyWalking)。Agent 的 LLM 调用用 RestTemplate 或 WebClient 包装,复用现有的超时、重试、熔断配置。

  2. 状态持久化复用现有基础设施:Redis 集群、MySQL 分库分表、PostgreSQL 等,不用新引入组件。AgentState 的序列化复用 Java 的 Jackson 或 Protobuf,不走 Python 的 pickle。

  3. 异步处理:Agent 任务通常耗时较长(5-20 秒),用 Spring 的 @Async 或 Reactive 编程,避免阻塞 Tomcat 线程池。Tomcat 默认线程池 200,如果 50 个 Agent 任务各占 10 秒,其他 150 个请求就要排队等。换成 WebFlux 后,同样的 Agent 任务吞吐量提升了 3 倍。

  4. 序列化性能:Java 的 AgentState 序列化到 Redis 时,用 Protobuf 代替 JSON,序列化大小可减少 60%,速度提升 3-5 倍。以 1000 个会话状态批量读取为例,JSON 序列化 15KB 每个,网络传输 150ms;Protobuf 5KB 每个,传输 50ms。

java
// 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 这些新趋势,表示你持续关注行业动态。

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。