1. 这道题到底在考什么:从“会调API”到“会做规划”的分水岭
面到“Agent 怎么做规划”这道题的时候,很多候选人第一反应是背概念:ReAct、Plan-and-Execute、Tree of Thoughts、Reflexion,名词一串一串往外蹦。但只要追问一句“那你这个 Planner 具体怎么写的,失败重试怎么处理”,场面立刻就冷下来了。我面过不少人,也被人面过,这道题真正筛掉的不是“不知道名词的人”,而是只会把大模型当接口调、从没认真想过“下一步该干什么”这件事本身有多难的人。
先把话说透:Agent 的规划,本质上是在不确定环境里做决策序列的生成与修正。它和传统程序里写死的 if-else 流程最大的区别在于——你事先不知道要走几步、每步的结果对不对、环境会不会变。所以规划要解决的是三个层次的问题:任务怎么拆、步骤怎么排、执行崩了怎么救。这三层任何一层没想清楚,Agent 就只是个“会聊天的脚本”。
为什么这道题淘汰率这么高?因为它同时考察三样东西,缺一不可。第一是抽象能力:你能不能把“帮我订个出差行程”这种模糊需求,拆成可执行、可验证的子任务。第二是工程能力:拆完之后,状态怎么存、上下文怎么管、工具调用失败怎么兜底,这些全是脏活。第三是边界意识:你知不知道什么任务根本不该交给 Agent 规划,什么任务必须人来兜底。大部分人只准备了第一层,后两层一问就露馅。
我见过最典型的翻车现场是这样的:候选人说“我用 ReAct,让模型自己思考然后调工具”。我问,那模型思考跑偏了怎么办?他说“加个 prompt 让它别跑偏”。我再问,如果它连续三次调同一个工具都失败呢?他愣住,说“应该不会吧”。这就是分水岭——把规划当成一次性的 prompt 技巧,还是当成一个有状态、可恢复、可观测的系统。前者是玩具,后者才是能上生产的 Agent。
所以这篇我不打算给你一份“标准答案”去背,而是把这道题拆开,讲清楚一个合格从业者脑子里应该有的那套规划框架。你把这套东西讲出来,面试官基本就知道你是真做过还是只读过论文。下面从任务拆解、规划范式选型、执行中的状态管理、失败恢复、以及怎么评估规划质量几个角度,一层层往下讲。
2. 任务拆解:规划的第一步不是“想”,而是“看清边界”
2.1 先判断这个任务该不该由 Agent 规划
很多人一上来就想着怎么拆,其实第一步应该是判断这个任务适不适合做规划。我踩过的坑就是:拿到需求就兴奋地开始设计 Planner,结果发现这个任务根本不需要动态规划,写死流程反而更稳、更便宜、更好维护。
判断标准我总结成三条,你可以直接拿去用:
- 步骤是否可预知:如果任务永远是“读文件→解析→写库”这三步,那叫工作流,不叫规划。硬套 Agent 只会引入不确定性。真正需要规划的是步骤数量、顺序、甚至目标本身都可能变的任务。
- 环境是否动态:工具返回的结果会不会影响下一步走哪条路?如果不会,那就是线性流程。会,才需要规划器做分支决策。
- 失败代价是否可控:规划意味着试错,试错意味着可能做错事。如果一步错就造成不可逆后果(比如转账、删数据),那规划必须加人工确认节点,或者干脆不让 Agent 自主规划。
我一般会用一个很土但很有效的问法反问自己:“如果我把这个任务的执行过程录下来,下次遇到一模一样的输入,我能不能直接重放?”能重放,就别上 Agent 规划,浪费 token 还增加故障面。不能重放,才值得上。
2.2 拆解的粒度:拆到“可验证”为止,不是拆到“看起来细”为止
确定要做规划之后,拆解粒度是第二个大坑。新手容易走两个极端:要么拆得太粗(“第一步:理解需求;第二步:完成它”),要么拆得太细(把一次 HTTP 请求都拆成三步)。我的经验是——拆到每个子任务都有明确的“完成判据”为止。
什么叫完成判据?就是这个子任务做完之后,你能用一个客观标准判断它成没成。比如“查询某城市的天气”这个子任务,完成判据是“拿到了结构化的温度、天气字段”,而不是“模型说它查完了”。前者可验证,后者不可验证,不可验证的步骤就是规划里的定时炸弹。
举个具体例子。任务:“帮我分析这份销售数据并给出下季度建议”。粗拆是“读数据→分析→给建议”,这没法执行。按可验证粒度拆应该是:
- 读取数据文件,校验字段完整性(判据:字段列表与预期一致,行数大于 0)
- 按区域和品类做聚合统计(判据:输出一张维度齐全的汇总表)
- 识别异常波动项(判据:列出偏离均值超过阈值的项,且每项有数值支撑)
- 基于统计结果生成建议(判据:每条建议都能追溯到具体数据)
你看,每一步都有判据,这样规划器在执行时才能知道“这步到底过没过”,而不是稀里糊涂往下走。
2.3 依赖关系与并行机会:别把能并行的活排成串行
拆完子任务,还要理清它们之间的依赖。这一步很多人直接跳过,导致规划出来的执行序列又慢又脆。我习惯画一张依赖表,把每个子任务的前置依赖和可否并行标出来。
| 子任务 | 前置依赖 | 可否并行 | 失败影响 |
|---|---|---|---|
| 读取数据 | 无 | 否 | 阻断全部 |
| 聚合统计 | 读取数据 | 否 | 阻断分析 |
| 异常识别 | 聚合统计 | 是 | 仅影响建议质量 |
| 生成建议 | 聚合统计、异常识别 | 否 | 最终产出 |
这张表的价值在于:它直接决定了你的规划器要不要支持并行调度,以及失败时该回滚到哪一步。比如“异常识别”失败了,其实不影响“生成建议”的主流程,那规划器就应该能降级继续,而不是整个任务重来。这种“部分失败可容忍”的设计,是区分玩具和工程的关键细节,面试时能主动提出来,加分非常明显。
3. 规划范式选型:ReAct、Plan-and-Execute 到底怎么选
3.1 ReAct 的本质是“边走边想”,适合短链路
ReAct 的核心就一句话:思考(Reason)和行动(Act)交替进行,每一步都基于上一步的观察(Observation)决定下一步。它的优点是灵活,环境一变马上能调整;缺点是每一步都要调一次模型,链路一长 token 成本爆炸,而且容易“走着走着忘了最初目标”。
我实测下来,ReAct 适合步骤数在 5 步以内、每步结果强依赖上一步的任务。比如“查一下这个报错怎么解决”——先搜、看结果、再决定要不要换个关键词搜、最后总结。这种任务用 ReAct 很自然。但如果你让它规划一个 20 步的复杂流程,它大概率会在第 8 步开始跑偏,因为上下文里塞了太多中间观察,模型注意力被稀释了。
3.2 Plan-and-Execute 是“先排计划再执行”,适合长链路
Plan-and-Execute 的思路是:先用一个 Planner 把整个计划列出来,然后 Executor 按计划一步步执行,执行中如果发现计划有问题再触发 Replan。它的优点是全局视角强、token 更省(计划只生成一次),缺点是计划一旦定死,中途环境大变时反应慢。
我的选型经验是这样的:
- 任务步骤可预判、环境相对稳定→ Plan-and-Execute。比如批量数据处理、报告生成。
- 任务步骤不可预判、环境频繁变化→ ReAct。比如交互式排障、探索式检索。
- 两者都要→ 混合:外层用 Plan-and-Execute 定大框架,每个子任务内部用 ReAct 灵活执行。这也是我现在大部分项目里的默认架构。
面试时如果你能说出“我不用单一范式,而是分层组合”,面试官基本会眼前一亮,因为这说明你真做过复杂任务。
3.3 一个可落地的混合规划器结构
我把常用的混合结构拆给你看,这套东西直接能抄:
class HybridPlanner: def __init__(self, llm, tools, max_replan=3): self.llm = llm self.tools = tools self.max_replan = max_replan def plan(self, goal, context): # 外层:生成高层计划,每个步骤是一个子目标 prompt = f"目标:{goal}\n可用工具:{list(self.tools)}\n请输出步骤列表,每步包含子目标和完成判据。" plan = self.llm.generate(prompt) return self._parse_plan(plan) def execute_step(self, step, state): # 内层:对单个子目标用 ReAct 循环执行 for i in range(5): # 单步最多 5 轮思考 thought = self.llm.reason(step, state) if thought.is_done: return thought.result action = self.llm.decide_action(thought, self.tools) observation = self.tools.call(action) state.append(observation) return None # 单步失败 def run(self, goal): plan = self.plan(goal, []) state = [] for step in plan: result = self.execute_step(step, state) if result is None: if self.max_replan <= 0: raise RuntimeError("规划失败,超出重规划次数") self.max_replan -= 1 plan = self.replan(goal, state, failed_step=step) else: state.append(result) return state这段代码的重点不在语法,而在两个设计决策:一是外层计划带“完成判据”,二是内层单步有轮次上限。前者让 Replan 有依据,后者防止单步死循环。面试时你把这两个点讲出来,比背十个范式名字都管用。
4. 执行中的状态管理:规划能不能落地,全看这里
4.1 上下文不是越长越好,要学会“压缩与丢弃”
规划执行到第 10 步的时候,上下文里已经堆了 9 步的观察结果。如果你全塞给模型,一是贵,二是模型会被无关信息干扰。我的做法是分层管理状态:
- 全局状态:目标、已完成步骤的结论摘要、当前计划。这部分永远保留。
- 近期状态:最近 2-3 步的完整观察。这部分保留原文。
- 历史状态:更早的步骤只保留结论,原始观察丢弃或存到外部。
这个策略我称之为“结论上浮,细节下沉”。模型规划下一步时,需要的是“前面做完了什么、结论是什么”,而不是“第 3 步那个接口返回的原始 JSON 长啥样”。把细节下沉到外部存储,需要时再按需检索,上下文能省一大半。
4.2 状态要可序列化,否则失败恢复就是空谈
我踩过最惨的坑:Agent 跑到第 15 步崩了,想从第 14 步恢复,结果发现状态全在内存里的对象引用,根本存不下来,只能从头再来。从那以后我定了个死规矩——Agent 的每一步状态都必须能序列化成 JSON。
具体来说,状态里至少要包含:当前计划、已完成步骤及其结论、当前步骤的中间观察、已调用过的工具及参数、重试计数。这些东西全部用基础类型(字符串、数字、列表、字典)表达,不存对象引用。这样崩了之后,读回 JSON 就能接着跑。
提示:状态序列化这件事,最好在设计阶段就定死,别等出了事故再补。我见过太多项目因为状态不可序列化,导致“断点续跑”功能永远做不出来。
4.3 工具调用的幂等性:规划器最容易忽略的工程细节
规划器决定“重试”的时候,它默认重试是安全的。但如果那个工具是“下单”“发消息”这种非幂等操作,重试就会造成重复副作用。这个问题在面试里很少有人主动提,但它是真实生产环境的头号事故来源。
我的处理方式是给每个工具打上幂等标记,规划器在决定重试前先查这个标记:
| 工具类型 | 幂等 | 重试策略 |
|---|---|---|
| 查询类(搜索、读文件) | 是 | 直接重试 |
| 计算类(统计、转换) | 是 | 直接重试 |
| 写入类(下单、发消息) | 否 | 先查状态再决定,或加去重键 |
| 外部副作用类 | 否 | 必须人工确认 |
这张表你直接放进规划器的工具注册表里,重试逻辑就有了依据。面试时提这一嘴,面试官会知道你考虑过真实副作用,而不是只在沙盒里跑过 demo。
5. 失败恢复与重规划:区分“能救”和“该放弃”
5.1 失败分三类,处理方式完全不同
规划执行失败,不能一律“重试”或一律“放弃”。我把它分成三类:
- 瞬时失败:网络抖动、限流。这类直接退避重试,通常 2-3 次就好。
- 逻辑失败:工具返回了结果,但结果不符合完成判据。比如要求返回结构化数据,结果返回了一段自然语言。这类要触发局部重规划,换个工具或换个参数再试。
- 目标失败:发现最初的目标本身不可达,或者前提假设错了。这类必须全局重规划,甚至直接上报人工。
区分这三类的意义在于:避免用重试去解决逻辑问题。我见过太多 Agent 对着一个永远返回错误格式的接口重试 10 次,纯属浪费。逻辑失败就该换路子,不是硬刚。
5.2 重规划不是“重新来一遍”,而是“带着教训重来”
新手写重规划,往往是清空状态从头开始。这是大忌,因为你会重复踩同一个坑。正确的重规划应该把失败原因作为约束条件传给 Planner。比如:
def replan(self, goal, state, failed_step, failure_reason): prompt = f""" 原目标:{goal} 已完成:{summarize(state)} 失败步骤:{failed_step} 失败原因:{failure_reason} 请生成新的计划,注意避开上述失败原因,且不要重复已完成的步骤。 """ return self.llm.generate(prompt)关键就在那句“避开上述失败原因,且不要重复已完成的步骤”。前者防止重蹈覆辙,后者节省成本。这个细节面试时讲出来,能直接体现你的工程成熟度。
5.3 重规划的次数上限与熔断
重规划不能无限循环,否则一个死任务能把你的 token 预算烧穿。我一般设两个阈值:单步重试上限(比如 3 次)和全局重规划上限(比如 3 次)。任一超限就熔断,把当前状态打包上报,交给人处理。
熔断的时候,上报的内容要包含:目标、当前计划、已完成步骤、失败点、失败原因、已尝试过的方案。这份“事故报告”越完整,人工接手越快。我甚至会把这份报告做成固定模板,因为排查线上问题时,格式统一能省大量沟通成本。
6. 怎么评估一个规划器好不好:别只看“跑通了没”
6.1 三个核心指标:成功率、步数、成本
“跑通了”只是最低标准。真正评估规划器,我盯三个指标:
- 任务成功率:在固定测试集上,端到端完成的比例。注意是端到端,不是单步。
- 平均步数:完成同类任务平均走多少步。步数越少通常说明规划越高效,但也不能为了少而牺牲成功率。
- 单位任务成本:token 消耗加工具调用次数。这个指标直接决定你能不能上生产。
这三个指标要一起看。我见过一个规划器成功率很高,但平均步数是别人的三倍,成本高到没法用。也见过步数很省的,但成功率只有六成。好的规划器是在成功率和成本之间找到平衡点,而不是单点最优。
6.2 用“对抗性测试集”暴露规划缺陷
正常测试集只能验证“顺利情况”,暴露不了规划缺陷。我习惯专门构造一批对抗性用例:
- 工具返回空结果时,规划器会不会死循环?
- 中途某个工具超时,会不会正确降级?
- 目标本身有歧义时,规划器会不会先澄清再动手?
- 计划执行到一半,环境变了,会不会触发重规划?
这批用例不用多,二三十个就够,但每一个都对应一类真实故障。每次改完规划逻辑,先跑这批用例,能挡掉大部分回归问题。
6.3 可观测性:规划过程必须能“回放”
最后一个容易被忽略的点是可观测性。规划器跑完一个任务,你得能回答:它为什么走了这条路?在哪一步犹豫过?哪次重试是必要的,哪次是浪费的?
我的做法是把每一步的“思考-行动-观察”三元组全部落盘,带上时间戳和 token 消耗。这样出问题时能完整回放,优化时也有数据支撑。没有可观测性的规划器,就像没有日志的后端服务,出了问题只能靠猜。
7. 面试时怎么答这道题:把框架讲成故事
回到最初那道题。如果你现在被问“Agent 怎么做规划”,我建议你按这个顺序讲,而不是一上来堆名词:
先讲判断——我会先判断这个任务该不该用 Agent 规划,标准是步骤可不可预知、环境动不动、失败代价可不可控。再讲拆解——拆到每个子任务有可验证的完成判据,并理清依赖和并行机会。然后讲选型——短链路用 ReAct,长链路用 Plan-and-Execute,复杂任务用分层混合。接着讲执行——状态分层管理、可序列化、工具幂等标记。最后讲恢复和评估——失败分三类处理、重规划带上失败约束、用成功率和成本双指标评估。
这套讲下来,面试官听到的不是名词,而是一个有边界、有取舍、有兜底的系统。这正是那 80% 被淘汰的人缺的东西——他们知道规划是什么,但不知道规划在真实系统里长什么样、会在哪里坏、坏了怎么修。
我自己带项目这些年最大的体会是:Agent 规划这件事,难的不是让模型想出步骤,而是让整个系统在模型想错的时候还能体面地收场。把这句话想明白,这道题你就答到点子上了。