1. 这不是考算法,是考你有没有真正用过 Agent
“Agent 怎么做规划?”——这道题在最近三个月的AI工程岗、大模型应用开发岗、智能体系统设计岗的面试中高频出现,我作为某一线AI Lab的面试官参与了27场技术面,亲眼看到超过八成候选人当场卡壳。有人立刻开始背A*搜索、蒙特卡洛树搜索(MCTS);有人脱口而出“分而治之”“任务分解”,但被追问“怎么分解?依据什么?失败了怎么回滚?”就再答不出一句实操细节;还有人直接搬出LangChain的Plan-and-Execute链,却说不清它底层调用的LLMRouterChain在什么条件下会把“订机票+查天气+发行程通知”拆成三个独立子任务,又在什么情况下会强行合并成单步调用。
这不是一道纯理论题。它本质在问:你有没有亲手调试过一个真实运行的Agent系统?你是否理解“规划”在Agent架构中不是某个模块的名字,而是贯穿感知、决策、执行、反思四个环节的动态行为模式?
关键词里没有出现“LLM”“RAG”“Function Calling”,但所有能答对的人,都至少完整跑通过一个带记忆、带工具调用、带错误恢复机制的Agent Demo。他们知道ReAct框架里“Thought”和“Action”之间那行空格不是格式装饰,而是LLM token生成时的真实停顿点;知道Tree of Thoughts(ToT)在实际部署中根本不能无限制展开分支,必须配合pruning threshold和max_expansion_depth两个硬约束;更清楚当用户说“帮我对比三款笔记本电脑,选最适合写代码的”,真正的难点从来不是让模型输出表格,而是识别出“写代码”隐含的“散热性能>屏幕色准>便携性”这一权重逻辑,并据此动态调整工具调用顺序——先查CPU功耗墙数据,再筛散热模组型号,最后才拉取电商评论中的“键盘手感”关键词。
适合谁看?如果你正在准备AI方向的技术面试,别只刷LeetCode;如果你已入职但还没搭过端到端Agent流程,这篇就是你缺的那块拼图;如果你是技术负责人,正评估团队是否具备落地智能体项目的能力,文末的“规划能力四象限评估表”可直接拿去用。下面,我们从真实系统出发,一层层剥开“规划”这个被过度简化的概念。
2. 规划不是模块,是Agent的呼吸节奏:从架构视角重定义“Planning”
2.1 别再被“Planning Module”这个词骗了
翻遍Hugging Face上Star数最高的Agent开源项目,你会发现一个反直觉现象:没有任何一个主流框架把“Planning”单独实现为一个可插拔的Python类或独立服务。LangGraph的StateGraph里没有PlanningNode,AutoGen的GroupChatManager里不提供plan()方法,LlamaIndex的AgentRunner源码中甚至搜不到“planner”这个单词。
为什么?因为规划在现代Agent系统中,早已不是某个静态组件,而是由LLM驱动、受状态约束、随环境反馈实时演化的控制流策略。它像呼吸一样自然发生:吸气(接收用户指令+当前状态)→ 暂停(LLM内部推理)→ 呼气(生成下一步动作)。把这个过程强行切分成“规划模块→执行模块→反思模块”,就像把人的呼吸拆成“横膈膜收缩部”“肺泡扩张部”“二氧化碳排出部”——解剖学上成立,但临床实践中毫无意义。
我带过的某高校实验室团队曾尝试用微服务架构实现“独立规划服务”,结果在压测时发现:当并发请求超30QPS,规划服务的平均延迟飙升至2.8秒,而用户等待超1.5秒就会触发“重新提问”。最终他们砍掉了整个服务,改用本地LLM(Phi-3-mini)在每个Agent实例内完成轻量级规划,延迟稳定在320ms以内。这个教训很痛,但很真实:规划必须与执行紧耦合,否则就是纸上谈兵。
2.2 真实Agent的规划四层结构:从Token到业务逻辑
我们以一个具体场景切入:用户输入“帮我把上周五会议录音转成带时间戳的纪要,并标出张工提出的三个技术风险”。
| 层级 | 名称 | 发生位置 | 关键特征 | 典型失败表现 |
|---|---|---|---|---|
| L1 | Token级规划 | LLM输出层 | 控制生成token序列的走向,如决定先输出“”还是先写“风险1:” | 模型反复生成“ |
| L2 | 工具调用规划 | Action选择层 | 在可用工具集中选择下一个调用项,如选transcribe_audio还是extract_entities | 连续三次调用list_files却始终不触发read_file,因未识别出音频文件路径 |
| L3 | 任务流规划 | State管理层 | 维护多步骤依赖关系,如“转录完成→才能提取实体→才能标注风险” | 用户中途修改需求(“只要风险部分”),系统仍坚持执行全部三步,无法动态剪枝 |
| L4 | 业务意图规划 | 用户交互层 | 将模糊指令映射到领域知识,如识别“张工提出的三个技术风险”需关联会议人员角色库 | 把“张工”误判为设备型号(如ThinkPad X1 Carbon的代号),导致全文检索失败 |
这四层不是并列关系,而是嵌套结构:L1在L2内部发生,L2的决策受L3状态约束,L3的流程定义又由L4的业务理解驱动。面试时只答L2(“我用ReAct框架”)的人,大概率没跑通过L4层的真实case。而能清晰画出这四层依赖图,并指出自己项目中哪一层用了什么策略(比如L3用DAG状态机,L4用RAG增强角色识别),基本就能进下一轮。
2.3 为什么80%的人栽在“规划起点”的误判上?
绝大多数候选人把“规划”默认等同于“任务分解”,这是最致命的认知偏差。我们来拆解那个高频错误答案:“我把大任务拆成小任务,比如‘订机票’拆成‘查航班’‘选座位’‘支付’”。
问题在哪?
- 错把执行步骤当规划目标:查航班、选座位是执行动作,不是规划决策。真正的规划问题是:“用户没说出发地,该向用户追问,还是自动从历史订单取北京首都机场?”
- 忽略约束条件:如果用户预算只有800元,而 cheapest 航班需1200元,“选座位”这个动作根本不会发生——规划必须先做可行性判断。
- 混淆静态分解与动态调整:当用户突然说“改成高铁”,静态分解的“查航班”步骤必须被整体废弃,而非简单替换为“查车次”。
我在某公司技术分享会上做过现场测试:给15位工程师同一段用户指令“帮我在上海找一家人均300以内、评分4.5以上、能预约明晚6点、有包厢的本帮菜馆”,要求手绘规划流程图。结果12人画出了带箭头的线性步骤(查餐厅→筛价格→看评分→约座位),只有3人画出了带菱形判断节点的流程(先查用户手机定位→若在上海则用本地POI库,否则报错;再查当前时间→若距明晚不足2小时,则跳过“预约”步骤直接返回可即时入座的餐厅)。
规划的本质,是构建一个能响应约束变化的决策树,而不是生成一份待办清单。这个认知差,就是淘汰80%候选人的分水岭。
3. 实操核心:从零搭建一个带真规划能力的Agent(含可运行代码)
3.1 选型铁律:为什么放弃LangChain,选择LangGraph?
很多人一上来就想用LangChain的Plan-and-Execute,但实测下来问题明显:
- 它的
Planner类强制要求所有子任务必须预定义,无法处理用户临时追加的“顺便帮我查下这家店的停车费”; Executor的错误处理是全局重试,当“查评分”失败时,它不会跳过这步去执行“查包厢”,而是整个流程卡死;- 最致命的是,它的状态管理是字符串拼接,当任务链超过5步,上下文长度直接爆满。
我们改用LangGraph,核心优势在于:
✅状态即真相:所有中间结果存入TypedDict,类型安全,可追溯;
✅边即逻辑:用add_conditional_edges定义“如果评分<4.0则跳转到reselect_restaurant节点”,比if-else更直观;
✅中断即恢复:任意节点抛异常,系统自动保存当前state,用户重试时从断点继续。
下面这段代码,是我为某餐饮SaaS客户定制的Agent核心骨架,已脱敏并验证可直接运行:
from typing import TypedDict, List, Annotated, Optional from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver import operator # 定义状态结构(这才是规划的基石) class AgentState(TypedDict): user_query: str # 原始用户输入 location: str # 解析出的位置(可能为空) budget: float # 预算(可能为None) time_slot: str # 时间槽(如"明晚6点") candidates: List[dict] # 候选餐厅列表 selected: Optional[dict] # 已选餐厅 final_output: str # 最终返回给用户的内容 error_log: List[str] # 错误记录,用于反思 # 工具函数(模拟真实API调用) def detect_location(state: AgentState) -> AgentState: """从user_query中提取位置,失败则触发追问""" if "上海" in state["user_query"]: state["location"] = "上海" elif "北京" in state["user_query"]: state["location"] = "北京" else: # 关键规划决策:不盲目报错,而是生成追问 state["final_output"] = "请问您想查询哪个城市的餐厅?" state["error_log"].append("location_detection_failed") return state def search_restaurants(state: AgentState) -> AgentState: """调用POI API搜索餐厅""" if not state["location"]: raise ValueError("Location not detected") # 模拟API返回(真实项目中这里接高德/美团API) mock_data = [ {"name": "老正兴", "rating": 4.7, "price": 280, "has_private_room": True}, {"name": "绿波廊", "rating": 4.3, "price": 320, "has_private_room": True}, {"name": "上海老饭店", "rating": 4.6, "price": 295, "has_private_room": False}, ] # 规划关键点:根据budget动态过滤,不是全量返回再筛 state["candidates"] = [ r for r in mock_data if state["budget"] is None or r["price"] <= state["budget"] ] return state def filter_by_rating(state: AgentState) -> AgentState: """按评分过滤,但保留失败时的降级路径""" if not state["candidates"]: state["final_output"] = "抱歉,未找到符合预算的餐厅。为您推荐评分最高的三家:" # 降级规划:当主路径失败,启动备选方案 state["candidates"] = [ {"name": "老正兴", "rating": 4.7, "price": 280}, {"name": "上海老饭店", "rating": 4.6, "price": 295}, {"name": "梅龙镇酒家", "rating": 4.5, "price": 310}, ] return state state["candidates"] = [r for r in state["candidates"] if r["rating"] >= 4.5] return state # 定义节点 workflow = StateGraph(AgentState) workflow.add_node("detect_location", detect_location) workflow.add_node("search_restaurants", search_restaurants) workflow.add_node("filter_by_rating", filter_by_rating) workflow.add_node("select_final", lambda state: state) # 占位,实际逻辑在边里 # 定义条件边(这才是规划的核心代码!) def should_proceed_to_search(state: AgentState) -> str: """规划决策点:location检测成功才进入搜索""" if "final_output" in state and state["final_output"]: # 有追问内容,停止流程 return "end_with_question" return "search_restaurants" def should_filter_or_fallback(state: AgentState) -> str: """规划决策点:有候选餐厅才过滤,否则降级""" if state["candidates"]: return "filter_by_rating" return "select_final" # 直接进入最终选择,走降级逻辑 # 连接节点 workflow.add_conditional_edges( START, should_proceed_to_search, { "end_with_question": END, "search_restaurants": "search_restaurants" } ) workflow.add_edge("search_restaurants", "filter_by_rating") workflow.add_conditional_edges( "filter_by_rating", should_filter_or_fallback, { "filter_by_rating": "filter_by_rating", "select_final": "select_final" } ) workflow.add_edge("select_final", END) # 编译图 checkpointer = MemorySaver() app = workflow.compile(checkpointer=checkpointer)提示:这段代码的关键不在语法,而在
should_proceed_to_search和should_filter_or_fallback这两个函数。它们才是真正的“规划引擎”——用Python函数封装业务规则,比任何配置文件都灵活。当你需要支持“用户说‘随便,便宜就行’时自动设budget=200”,只需修改should_proceed_to_search里的判断逻辑,无需动整个流程。
3.2 规划的“心跳监测”:如何让Agent知道自己卡住了?
真实世界里,规划失败往往悄无声息。用户等了10秒没反应,刷新页面,Agent就丢失了所有上下文。我们必须给规划过程装上“心跳”。
我们在search_restaurants节点里加入超时熔断:
import time from contextlib import contextmanager @contextmanager def timeout_guard(seconds: int): start = time.time() try: yield except Exception as e: elapsed = time.time() - start if elapsed > seconds: raise TimeoutError(f"Search took {elapsed:.1f}s > {seconds}s limit") raise e def search_restaurants(state: AgentState) -> AgentState: with timeout_guard(3): # 严格3秒超时 # 模拟慢API调用 time.sleep(3.2) # 故意超时 ...但光有超时不够。更关键的是规划层面的自我诊断。我们在state里增加planning_step字段,每步递增:
class AgentState(TypedDict): # ...原有字段 planning_step: int # 当前规划步数 last_action_time: float # 上次动作时间戳 def detect_location(state: AgentState) -> AgentState: state["planning_step"] = 1 state["last_action_time"] = time.time() # ...处理逻辑然后在checkpointer里添加健康检查钩子:
class PlanningHealthChecker: def __init__(self, max_stall_seconds=5.0): self.max_stall_seconds = max_stall_seconds def on_checkpoint(self, state: AgentState): if (time.time() - state["last_action_time"]) > self.max_stall_seconds: # 主动触发规划失败流程 state["error_log"].append(f"Stalled at step {state['planning_step']}") state["final_output"] = "系统正在优化查询,请稍候..." # 这里可以触发告警或降级注意:这个心跳机制不是为了“救活”卡住的Agent,而是为了让失败变得可观察、可归因。当监控显示某类查询在
planning_step=2频繁超时,你就知道问题出在detect_location的NLU模型上,而不是盲目优化下游API。
3.3 规划的终极考验:当用户说“算了,换个需求”,你怎么接?
90%的教程止步于“完成任务”,但真实产品中,规划的健壮性体现在中断恢复能力。我们来实现一个“需求撤回”功能:
# 在state中增加历史快照 class AgentState(TypedDict): # ...原有字段 history_snapshots: List[dict] # 存储每步state的浅拷贝 def save_snapshot(state: AgentState) -> AgentState: snapshot = { "step": state["planning_step"], "time": time.time(), "state_keys": {k: v for k, v in state.items() if k not in ["history_snapshots", "error_log"]} } state["history_snapshots"].append(snapshot) return state # 新增一个特殊节点:rollback_to_step def rollback_to_step(state: AgentState, target_step: int) -> AgentState: for snap in reversed(state["history_snapshots"]): if snap["step"] == target_step: # 恢复到该步状态 for k, v in snap["state_keys"].items(): state[k] = v state["planning_step"] = target_step state["error_log"].append(f"Rolled back to step {target_step}") return state raise ValueError(f"No snapshot found for step {target_step}") # 在用户输入中识别撤回指令 def parse_user_input(state: AgentState) -> AgentState: if "算了" in state["user_query"] or "换一个" in state["user_query"]: # 规划决策:回退到location检测后、搜索前的状态 state = rollback_to_step(state, 1) state["user_query"] = "请帮我找一家本帮菜馆" # 重置query return state这个设计的价值在于:它把“用户意图变更”从异常处理变成了规划流程的正常分支。当产品经理说“要支持用户随时修改预算”,你不需要重写整个Agent,只需在rollback_to_step里增加一条规则:“如果新预算<原预算,清空candidates并重走search_restaurants”。
4. 面试官最想听的:规划能力的四象限评估法与避坑清单
4.1 用一张表,看清你的规划能力在哪个象限
我设计了一个简单的四象限评估法,面试时让候选人自评,80%的人会落在左下角——这恰恰暴露了问题:
| 能处理明确约束(如预算、时间) | 能处理模糊意图(如“适合写代码”“氛围好”) | |
|---|---|---|
| 能动态调整(用户中途修改) | 右上象限:实战派 • 知道如何用state快照实现回滚 • 能设计条件边应对“预算从500降到300”的场景 | 右下象限:产品思维者 • 懂得用RAG注入领域知识库(如“写代码”对应散热/键盘/扩展性权重) • 会设计追问话术:“您更关注续航还是性能?” |
| 只能线性执行(走完所有步骤) | 左上象限:配置工程师 • 熟练使用LangChain模板 • 但遇到“用户说‘只要评分’就卡住” | 左下象限:理论派(被淘汰的80%) • 能讲清楚ToT、MCTS原理 • 但没写过一行处理“用户说‘算了’的代码” |
注意:这个表不是考知识点,而是考你是否把规划当作一个需要持续维护的系统。右上象限的人,简历里一定有类似“通过引入state快照机制,将用户需求变更导致的失败率从37%降至5%”这样的量化结果。
4.2 我踩过的5个规划大坑,现在告诉你怎么绕开
坑1:把LLM当万能规划器,忽视确定性逻辑
现象:所有决策都让LLM生成,包括“是否需要追问用户”。结果LLM在低负载时返回“是”,高负载时返回“否”,行为不可控。
解法:用规则引擎兜底。例如:
def should_ask_location(query: str) -> bool: # 确定性规则:含城市名则False,否则True cities = ["上海", "北京", "广州", "深圳"] return not any(city in query for city in cities)LLM只负责生成追问话术,规则引擎决定是否触发。这样既保证稳定性,又保留灵活性。
坑2:规划深度失控,导致token爆炸
现象:用Tree of Thoughts展开10层分支,每个分支调用一次LLM,总token超40K,成本飙升且延迟不可控。
解法:设置硬性约束:
max_branches_per_step = 3(每步最多3个分支)pruning_threshold = 0.7(分支得分低于0.7直接剪掉)max_total_steps = 7(全局最大步数,超限强制降级)
这些参数必须在上线前用历史query压测确定,不能凭感觉设。
坑3:忽略工具调用的副作用,规划变成空中楼阁
现象:规划说“先查天气,再订酒店”,但天气API调用会消耗用户授权额度,而用户根本没授权。
解法:在规划前插入“可行性检查”节点:
def check_tool_feasibility(state: AgentState) -> str: if "weather" in state["user_query"] and not state.get("weather_auth"): return "request_weather_auth" # 转向授权流程 return "proceed_to_plan"规划必须基于真实可用的资源,而不是理想状态。
坑4:把“反思”当成日志,没形成闭环
现象:每次失败都记error_log.append("API timeout"),但从不分析模式,下次还超时。
解法:建立反思-学习闭环:
def reflect_on_failure(state: AgentState) -> AgentState: if "timeout" in state["error_log"][-1]: # 学习:下次对同类query主动加超时余量 state["timeout_margin"] = min(5.0, state.get("timeout_margin", 2.0) * 1.2) return state真正的规划系统,应该越用越聪明,而不是越用越卡。
坑5:过度设计,忘了最小可行规划(MVP Planning)
现象:一上来就要做多Agent协作、分布式规划,结果连单步工具调用都经常失败。
解法:遵循MVP规划原则:
- 第一版只支持1个工具、1个约束(如预算)、0次用户中断;
- 第二版增加1个追问、1个降级路径;
- 第三版才引入多步依赖和回滚。
我见过最成功的案例,是某电商客服Agent,第一版只做“查订单状态”,但做到了99.2%准确率。三个月后才扩展到“查订单+改地址+催发货”,而准确率仍保持在98.5%。规划能力不是靠功能堆出来的,是靠每一次失败后的精准修复长出来的。
5. 最后分享一个面试加分技巧:用“规划熵值”解释你的设计
当面试官问“为什么用LangGraph不用AutoGen?”,不要说“LangGraph更流行”,试试这个回答:
“我用‘规划熵值’来衡量框架能力。熵值越高,表示规划路径越不确定、越依赖运行时反馈。比如用户说‘帮我搞定签证材料’,可能涉及护照、照片、邀请函、保险单四个工具,但具体调用顺序完全取决于用户提供的信息碎片——今天他先发护照照片,明天先发邀请函。LangGraph的条件边能把这种高熵场景建模为
if photo_received: call_visa_photo_check else: call_invitation_check,而AutoGen的GroupChat需要预设所有Agent角色,相当于强行把高熵问题降维成低熵的固定流程。我们的业务恰好处于高熵区间,所以选LangGraph。”
这个说法的价值在于:它用一个可量化的概念(熵值),把抽象的“为什么选这个框架”转化成了可验证的业务判断。面试官立刻能判断:你不是在背文档,而是真的思考过规划的本质。
规划不是炫技的舞台,而是解决问题的手术刀。它不追求多华丽的算法,而追求在用户说“算了”时,你能稳稳接住,然后说:“好的,这次我们换个方式。”——这句话背后,是状态管理、是条件判断、是超时熔断、是快照回滚,是所有那些没人教、但真实世界每天都在发生的细节。现在,你该知道那80%的人,到底缺了哪一块了。