智能体面试准备(十七):HTN 任务分解与规划——让 Agent 面对复杂目标会自己拆计划

智能体面试准备(十七):HTN 任务分解与规划——让 Agent 面对复杂目标会自己拆计划

前面几篇讲了单 Agent(ReAct)、多 Agent(B15)、安全(B16)、工具调用(B18 待发)。但有一个核心能力一直没讲:当目标很复杂、步骤很长时,Agent 怎么知道"先做什么、再做什么、怎么拆"?这就是任务规划(Planning)。这一篇聚焦最工程化的规划范式——HTN(Hierarchical Task Network,分层任务网络),对比 ReAct 式"边走边看",讲清楚"先计划后执行"的价值、实现与自我纠错。每节给:原理 → 图解 → 代码 → 面试速答 + 高频追问。


一、为什么 ReAct 不够:规划能力的缺失

ReAct 是"thought → action → observation"循环,属于反应式(reactive):每步只基于当前观察决定下一步。面对多步长程任务有两个痛点:

ReAct 的困境: 目标: "调研竞品并出一份对比报告" 问题1: 没有全局计划,容易走一步看一步、东一榔头西一棒 问题2: 中途失败不知道"回到哪一步重来",只能从头再来

规划式(deliberative)Agent 的答案:先生成一份结构化计划,再按步执行,失败可回滚到计划节点

反应式 规划式 ┌────────────────┐ ┌──────────────────────┐ │ 想一步做一步 │ │ 先 Plan 出任务树 │ │ 易迷路/难回滚 │ │ 再按节点执行+可回滚 │ └────────────────┘ └──────────────────────┘

二、HTN 是什么:用"任务网络"描述怎么拆

HTN 的核心概念:

  • 原始任务(Primitive Task):不可再分、可直接执行(如"打开浏览器""调用 search_api")。
  • 复合任务(Compound Task):需进一步拆解(如"调研竞品")。
  • 方法(Method):把复合任务拆成子任务序列的规则。
  • 任务网络:复合任务 + 方法 + 约束,构成一棵可展开的任务树。
复合任务: 写竞品报告 └─ 方法 M1: [调研竞品, 整理数据, 撰写报告, 排版导出] ├─ 调研竞品(复合) │ └─ M1a: [搜索, 筛选Top5, 抓官网] ├─ 整理数据(复合) → [清洗, 建表] ├─ 撰写报告(原始) → 调用 write_agent └─ 排版导出(原始) → 调用 export_tool

可以看成"自顶向下的递归分解",直到所有叶子都是原始任务。


三、HTN vs ReAct:一张表说清

维度ReActHTN 规划
决策时机边执行边决策先计划再执行
全局观弱(只看当前 observation)强(有完整任务树)
可回滚易(回退到某计划节点)
适合任务单步/短程/探索性多步/长程/可分解
可控性高(计划可人工审阅)
LLM 用量每步一次计划一次 + 每步一次

面试速答:什么时候用 HTN 而不是 ReAct?当任务可预先分解、步骤长、要求可控可回滚(如"订机票+酒店+行程"),用 HTN;当任务探索性强、无法预先计划(如"在网页里找某个隐藏信息"),ReAct 更合适。工程上常两者结合:HTN 出高层计划,叶子节点用 ReAct 执行。


四、最小可运行:一个 LLM 驱动的 HTN 规划器

思路:让 LLM 充当"分解器",把复合任务递归拆成计划 JSON;执行器逐节点跑,失败时触发重规划。

importjson,openaidefdecompose(task,depth=0,max_depth=4):"""让 LLM 把复合任务拆成子任务列表(原始任务直接返回)"""ifdepth>=max_depth:return[{"type":"primitive","name":task}]prompt=f"""把任务拆成有序子任务JSON。若是不可再分的具体动作,返回 type=primitive;否则返回 type=compound 及其子任务列表。任务:{task}"""resp=openai.chat.completions.create(model="gpt-4o",messages=[{"role":"user","content":prompt}])plan=json.loads(resp.choices[0].message.content)out=[]forsubinplan["steps"]:ifsub["type"]=="compound":out+=decompose(sub["name"],depth+1,max_depth)else:out.append({"type":"primitive","name":sub["name"]})returnoutdefexecute(plan):results=[]forstepinplan:try:r=run_tool(step["name"])# 叶子节点调用具体工具/Agentresults.append((step["name"],"OK",r))exceptExceptionase:return{"failed_at":step["name"],"done":results,"error":str(e)}return{"done":results}plan=decompose("调研竞品并出对比报告")result=execute(plan)

五、自我纠错:失败如何重规划

HTN 的真正威力在"计划是可执行的树",所以失败可定位到节点并重规划,而非全盘重来:

执行: [搜索] OK → [抓官网] 失败(网站 403) ↓ 重规划: 把"抓官网"替换为 [换源搜索]→[抓镜像]→[摘要] ↓ 继续: 从失败节点往后执行,前面 OK 的不重做

实现上给执行器加一个"重规划钩子":

defexecute_with_replan(plan,max_retry=2):i=0whilei<len(plan):try:run_tool(plan[i]["name"]);i+=1exceptExceptionase:ifmax_retry==0:raise# 从失败节点重新分解该子目标plan[i:]=decompose(plan[i]["name"]+f"(避免:{e})")max_retry-=1return"done"

面试速答:HTN 的自我纠错比 ReAct 强在哪?ReAct 失败往往只能从头或靠模型"回忆";HTN 有显式任务树,能精准定位失败节点、只对该子树重规划,已完成的兄弟节点不浪费,效率和可控性都更高。


五之二、HTN 与现有框架的混合落地

纯 HTN 适合"可预先分解"的任务,但真实业务常是"部分可知、部分要探索"。工程上的主流折中是分层混合架构

高层: HTN 规划器(LLM 出任务树,可人工审阅) │ ├─ 节点A(复合) → 派给 ReAct Agent 执行(探索性子任务) ├─ 节点B(原始) → 直接调工具 └─ 节点C(复合) → 派给多 Agent 协作(见 B15)

这种"计划在上、执行在下"的结构,兼顾了可控性和灵活性:计划层保证不跑偏、可回滚,执行层用 ReAct/多 Agent 处理不确定性。

一个把 HTN 和 ReAct 拼起来的伪代码:

defrun_agent(goal):plan=decompose(goal)# HTN: 先出计划fornodeinplan:ifnode.needs_explore:# 探索性子任务交给 ReActnode.result=react_loop(node.desc)else:# 确定性子任务直接执行node.result=dispatch(node.tool,node.args)returnsummarize(plan)

面试速答:为什么工业级 Agent 多用 HTN+ReAct 混合而非纯 HTN 或纯 ReAct?纯 HTN 难处理未知分支,纯 ReAct 难控长程;混合让"已知的提前规划、未知的现场反应",兼顾可控与灵活。


六、面试速答 + 高频追问清单(汇总)

速答 TOP 8:
1. HTN = 分层任务网络,复合任务按方法递归拆成原始任务树。
2. 三类要素:复合任务 / 原始任务 / 方法(拆解规则)。
3. ReAct 反应式、HTN 规划式;长程可控任务选 HTN。
4. HTN 优势:全局计划、可回滚、可人工审阅。
5. 工程上常 HTN 出高层计划 + 叶子用 ReAct 执行。
6. 自我纠错靠"失败定位到节点 + 子树下重规划"。
7. max_depth 防止无限递归分解。
8. LLM 在这里当"分解器",也可换成规则引擎。

追问清单:
- HTN 的计划质量问题怎么保证(LLM 拆错了怎么办)?
- 和经典规划(如 PDDL/STRIPS)比,LLM-HTN 的优劣势?
- 多 Agent 下,HTN 的计划如何分配给不同 Agent(结合 B15)?
- 计划里的约束(先后顺序/互斥)怎么表达?
- 怎么让计划可解释、可被人工编辑?


七、下一篇预告

规划讲完"怎么拆",下一篇(B18)讲Function Calling 全链路——Agent 真正"动手"的最后一公里:从工具 schema 定义、模型如何决定调用、参数解析、并行调用到错误处理。评论区告诉我你想先听哪个。