把章节变成可单步观察的过程
分类:游戏 AI 与逻辑建模 / 规划与搜索
先说结论
HTN 从设计者给出的任务分解规则生成可执行计划,MCTS 通过多次模拟估计行动结果。HTN 更依赖明确的领域知识,MCTS 更依赖可快速复制和推进的状态模型。
两种“想下一步”的数字
HTN 任务:取得食物
方法 1:有钱 -> 去商店 -> 购买(预计成本 4)
方法 2:有工具 -> 去森林 -> 采集(预计成本 7)
当前有钱,选择方法 1
MCTS 根节点有 3 个动作,每个先模拟 100 次
A 胜率 62%,B 胜率 48%,C 胜率 55%
当前选择 A,并继续把更多模拟预算投向有希望的分支
这些数字只说明决策过程。HTN 的成本来自领域定义,MCTS 的胜率来自采样,二者都不自动保证现实游戏中的全局最优。
两种完全不同的“想下一步”
想象两个生活场景。
第一个场景是准备一顿晚餐:
目标:完成晚餐
如果食材齐全:
处理食材 -> 烹饪 -> 装盘
如果食材不全:
列清单 -> 购买食材 -> 处理食材 -> 烹饪 -> 装盘
这类问题有熟悉的步骤结构。我们知道“大任务通常怎样拆成小任务”,只需根据当前条件选择合适的拆法。这接近 HTN。
第二个场景是在复杂棋局中选择落子:
可选动作很多
对手会回应
越往后分支越多
无法完整搜索到终局
这时可以把计算预算集中到看起来更有希望的分支,通过多次模拟估计动作价值。这接近 MCTS。
一句话区分:
HTN 用领域知识把任务逐层分解成可执行步骤。
MCTS 用反复采样在巨大决策树中估计更好的动作。
先运行两个最小例子
先不要同时理解整套算法。下面让 HTN 和 MCTS 各自解决一个很小的问题,并观察代码实际改变了什么。
HTN 第一步:根据状态选择任务列表
using System;
using System.Collections.Generic;
static List<string> PlanDinner(WorldState world)
{
if (world.HasFood)
{
return new List<string>
{
"PrepareFood",
"Cook",
"Serve"
};
}
return new List<string>
{
"BuyFood",
"PrepareFood",
"Cook",
"Serve"
};
}
WorldState world = new(HasFood: false, MealReady: false);
Console.WriteLine(string.Join(" -> ", PlanDinner(world)));
record WorldState(bool HasFood, bool MealReady);
输出是:
BuyFood -> PrepareFood -> Cook -> Serve
这已经包含 HTN 的最小思想:PlanDinner 是复合任务,根据世界状态选择一种分解方法,返回的字符串是尚未真正执行的原子任务序列。
HTN 第二步:不能只看第一个条件
第一版的问题是:它只在选方法时检查 HasFood,没有验证后面的每一步是否真的可执行。加入原子任务和状态模拟:
static bool TryBuildPlan(
WorldState start,
IReadOnlyList<PrimitiveTask> tasks,
out List<string> plan)
{
WorldState simulated = start;
plan = new List<string>();
foreach (PrimitiveTask task in tasks)
{
if (!task.CanRun(simulated))
{
plan.Clear();
return false;
}
plan.Add(task.Name);
simulated = task.Apply(simulated);
}
return true;
}
record PrimitiveTask(
string Name,
Func<WorldState, bool> CanRun,
Func<WorldState, WorldState> Apply);
关键是 simulated:规划器不修改真实世界,而是在副本上推演。假设 BuyFood 成功后把 HasFood 设为 true,后面的 Cook 才能通过前置条件。
一次失败跟踪可能是:
BuyFood -> 可执行,模拟状态 HasFood = true
PrepareFood -> 可执行
Cook -> 炉灶损坏,CanRun = false
整条方法失败,清空临时计划,再尝试其他方法
这解释了为什么 HTN 不能“找到第一个看起来适用的方法就立刻执行”:必须先完整分解并验证,失败时还要回退临时状态。
MCTS 第一步:先保存每个动作的统计
MCTS 不预先写死完整任务列表。假设 NPC 有三个动作:进攻、防守、治疗。每次模拟后记录访问次数和累计奖励:
ActionStats attack = new("Attack");
attack.Record(1.0); // 第一次模拟获胜
attack.Record(0.0); // 第二次模拟失败
Console.WriteLine(
$"{attack.Name}: visits={attack.Visits}, " +
$"average={attack.AverageReward:F2}");
sealed class ActionStats
{
public string Name { get; }
public int Visits { get; private set; }
public double TotalReward { get; private set; }
public double AverageReward =>
Visits == 0 ? 0 : TotalReward / Visits;
public ActionStats(string name) => Name = name;
public void Record(double reward)
{
Visits++;
TotalReward += reward;
}
}
输出:
Attack: visits=2, average=0.50
MCTS 最终不是问“某次模拟赢了吗”,而是比较计算预算内积累的统计结果。
MCTS 第二步:既利用好动作,也探索少试的动作
如果只选平均奖励最高的动作,一个早期碰巧获胜的动作可能永远霸占搜索。选择阶段通常加入探索项。下面是常见 UCB1 形式的最小代码:
static double UcbScore(
ActionStats action,
int parentVisits,
double exploration = 1.414)
{
if (action.Visits == 0)
return double.PositiveInfinity;
double exploit = action.AverageReward;
double explore = exploration * Math.Sqrt(
Math.Log(parentVisits) / action.Visits);
return exploit + explore;
}
给出一组具体统计:
根节点共访问 20 次
Attack:访问 10 次,平均奖励 0.70
Defend:访问 2 次,平均奖励 0.55
Attack 的利用价值更高,但 Defend 因为尝试次数少,会获得更大的探索加成。搜索继续后,统计会逐渐说明它是真的有潜力,还是仅仅没被充分验证。
两段代码的根本差别
HTN:代码先定义“任务怎样分解”,运行时生成一条可执行计划。
MCTS:代码先定义“动作、状态转移和奖励”,运行时反复模拟并更新统计。
后文的 HTN 分解栈和 MCTS 四阶段,都是对这两个最小例子的扩展。阅读时可以持续追问:HTN 当前在验证哪条任务链;MCTS 当前在更新哪个节点的访问次数与奖励。
HTN:分层任务网络
HTN 是 Hierarchical Task Network,核心对象通常包括:
复合任务 Compound Task
方法 Method
原子任务 Primitive Task
世界状态 World State
复合任务
复合任务描述“想完成什么”,但本身还不能直接执行:
准备晚餐
到达目的地
恢复队伍状态
取得某件物品
方法
方法描述在特定前提下,复合任务可以怎样分解:
准备晚餐
方法 A:食材齐全
处理食材 -> 烹饪 -> 装盘
方法 B:食材不足且商店可用
购买食材 -> 处理食材 -> 烹饪 -> 装盘
一个方法通常包含:
适用条件
子任务列表
子任务顺序或偏序约束
原子任务
原子任务是规划器不再继续分解的步骤:
移动到商店
购买指定食材
启动烹饪
原子任务通常要声明:
前置条件:执行前必须成立。
效果:执行后世界状态怎样变化。
代价:用于比较方案时的成本信息。
具体系统是否让原子任务直接绑定运行时动作,是实现选择。规划和执行也可以分离。
HTN 怎样逐步生成计划
初始状态:
有主食 = true
有配菜 = false
商店开放 = true
初始任务:
准备晚餐
第一步:选择适用方法
检查“食材齐全”方法:
有主食 && 有配菜
结果:false
检查“食材不足且可购买”方法:
缺少食材 && 商店开放
结果:true
于是分解为:
购买配菜 -> 处理食材 -> 烹饪 -> 装盘
第二步:验证原子任务
规划器按顺序模拟每个原子任务:
购买配菜
前置条件:商店开放
效果:有配菜 = true
处理食材
前置条件:食材齐全
效果:食材已处理 = true
这里的“模拟”只修改规划器内部的预测状态,不应提前改变真实世界。
第三步:得到可执行计划
购买配菜
处理食材
烹饪
装盘
运行时再逐步执行原子任务,并观察真实状态是否仍满足前置条件。
HTN 的最小伪代码
Plan(task, state):
if task is Primitive:
if PreconditionsMet(task, state):
nextState = ApplyEffects(task, state)
return [task], nextState
return Failure
for method in ApplicableMethods(task, state):
plan = []
simulatedState = Copy(state)
for subtask in method.subtasks:
partialPlan, simulatedState =
Plan(subtask, simulatedState)
if partialPlan failed:
break and try next method
append partialPlan to plan
if every subtask succeeded:
return plan, simulatedState
return Failure
这段伪代码表达的是顺序任务的深度优先分解。是否:
回溯到其他方法
寻找成本最低计划
支持偏序任务
缓存中间结果
限制搜索深度
都属于具体规划器的能力与策略,不能从“HTN”三个字自动推出。
HTN 的关键状态
调试 HTN 时至少要区分:
真实世界状态:执行系统当前确认的事实。
模拟世界状态:规划过程中应用预测效果后的副本。
待分解任务栈:还没有完成规划的任务。
候选方法:当前复合任务可尝试的分解方式。
已生成计划:已经确定的原子任务序列。
执行游标:运行时执行到计划的哪一步。
把模拟状态直接写回真实状态,会让“计划成功”看起来像“动作已经完成”,这是严重的状态边界错误。
HTN 的正常、边界与失败路径
正常路径:第一个方法可完整分解
方法条件成立
所有原子任务前置条件成立
得到完整计划
执行时世界状态与预测一致
边界路径:方法前半段可行,后半段失败
购买食材成功进入模拟计划
但后续任务发现设备不可用
如果规划器支持回溯,应撤销该方法在模拟状态上的效果,再尝试其他方法。
是否支持回溯是实现能力;如果不支持,应明确报告“选择的方法无法完成”,而不是返回残缺计划冒充成功。
边界路径:执行时状态变化
规划阶段商店开放,走到一半商店关闭:
当前原子任务前置条件失效
合法策略可能包括:
从当前真实状态重新规划
把失败交给上层决策
执行预先定义的补偿任务
采用哪一种是执行协议选择。补偿或回退行为不能凭空添加,必须有明确规则。
失败路径:没有任何适用方法
规划器应保留可诊断信息:
哪个复合任务无法分解
检查过哪些方法
每个方法缺少什么前提
当时的世界状态版本
返回空计划可能同时表示“什么都不必做”和“规划失败”,因此需要明确区分。
MCTS:蒙特卡洛树搜索
MCTS 是 Monte Carlo Tree Search。它通常反复执行四个阶段:
Selection:沿当前搜索树选择一个节点。
Expansion:为未完全展开的节点加入新分支。
Simulation:从新节点继续模拟一段过程。
Backpropagation:把模拟结果沿路径回传。
每个搜索节点通常记录:
状态或状态引用
从父节点到这里采取的动作
访问次数
累计价值
子节点
尚未展开的动作
MCTS 返回的是计算预算下的估计结果,不保证找到全局最优动作。
MCTS 怎样逐步搜索
假设当前有三个可选动作:
A:稳妥推进
B:快速尝试
C:保守等待
第一轮
根节点还没有展开:
Expansion:选择 A,创建子节点。
Simulation:从 A 后随机或按轻量策略模拟。
Backpropagation:将结果回传给 A 和根节点。
如果模拟收益为正:
A.visitCount += 1
A.totalValue += reward
root.visitCount += 1
后续轮次
搜索树逐渐拥有 A、B、C 三个子节点。Selection 要平衡:
利用:多访问当前平均收益高的分支。
探索:给访问较少的分支更多机会。
常见选择公式之一是 UCT:
score =
averageValue
+ explorationConstant
* sqrt(ln(parentVisits) / childVisits)
公式结构是常见算法知识;探索常数取多少、奖励怎样归一化、零访问节点怎样处理,都是实现与问题建模选择。
达到预算后
根节点选择最终动作时,常见做法包括:
选择访问次数最多的子节点
或选择平均价值最高的子节点
两者不总是等价。最终选择规则必须明确,而不能只说“使用 MCTS”。
MCTS 最小伪代码
Search(rootState, budget):
root = CreateNode(rootState)
repeat until budget exhausted:
node = root
state = Copy(rootState)
while node is fully expanded and not terminal:
node = SelectChild(node)
state = Apply(node.action, state)
if state is not terminal and node has untried actions:
action = ChooseUntriedAction(node)
state = Apply(action, state)
node = AddChild(node, action, state)
reward = Simulate(state)
while node exists:
node.visitCount += 1
node.totalValue += reward from node's perspective
node = node.parent
return ChooseFinalAction(root)
需要特别注意“从谁的视角回传价值”。在双方对抗问题中:
轮到另一方行动时,价值视角可能需要翻转或重新解释。
具体处理取决于奖励定义和节点视角。
MCTS 的关键状态
一次搜索至少涉及:
根状态快照
搜索树节点统计
未尝试动作集合
模拟状态
随机数状态或种子
剩余时间、迭代次数或其他预算
奖励值及其视角
为了复现问题,调试记录应包含:
根状态标识
随机种子
预算
每个根动作的访问次数和平均价值
最终选择规则
MCTS 的正常、边界与失败路径
正常路径:预算内形成统计差异
A:访问 120 次,平均价值 0.58
B:访问 35 次,平均价值 0.41
C:访问 15 次,平均价值 0.22
如果最终规则是“访问次数最多”,则选择 A。
这些数字只是说明统计形态的示例,不是任何系统应硬编码的阈值。
边界路径:根节点只有一个合法动作
仍可以直接返回唯一动作,不必进行无意义搜索。是否仍运行模拟来估计后续风险,是调用方需求。
边界路径:模拟在深度限制处结束
真实终局太远时,Simulation 可能在预算或深度上限处停止,转而使用估值函数。
深度上限与估值函数属于具体实现选择。估值偏差会直接影响搜索结果。
边界路径:不确定或随机转移
如果同一动作可能产生不同结果,模拟器必须按问题模型采样这些转移。若状态转移概率未知,MCTS 不会自动创造可信概率。
失败路径:奖励尺度不一致
如果某些模拟返回:
胜负值:-1 到 1
另一些却返回:
资源分:0 到 10000
搜索统计会被尺度主导。奖励的含义、范围与视角必须统一。
失败路径:模拟器与真实规则不一致
MCTS 的结论依赖模拟器。如果模拟阶段漏掉关键约束,即使迭代很多次,也只会更确信一个建立在错误模型上的答案。
HTN 与 MCTS 的核心差异
| 维度 | HTN | MCTS |
|---|---|---|
| 主要输入 | 任务、方法、世界状态 | 状态、合法动作、转移与奖励模型 |
| 核心过程 | 分解任务 | 采样搜索 |
| 领域知识 | 显式写在方法和任务层级中 | 可写在选择、模拟和估值策略中 |
| 主要输出 | 可执行任务序列或网络 | 当前状态下建议的动作 |
| 可解释性 | 通常较强,可说明用了哪个方法 | 需要查看访问次数、价值和模拟统计 |
| 计算形态 | 受分解方法与回溯影响 | 可随预算渐进改善估计 |
| 典型风险 | 方法覆盖不全、状态预测失真 | 模拟器偏差、奖励偏差、预算不足 |
进阶:能否组合使用
HTN 与 MCTS 并不互斥。
一种组合方式是:
HTN 负责把长期任务拆成结构化候选方案。
MCTS 负责在某个高分支局部决策中选择当前动作。
另一种方式是:
HTN 方法生成合法的高层动作。
MCTS 只在这些高层动作之间搜索。
组合会减少某些搜索分支,但也会引入新的边界:
HTN 如果漏掉方法,MCTS 永远看不到对应方案。
MCTS 的统计价值怎样反馈给 HTN,需要统一代价语义。
重新规划时,旧搜索树能否复用需要验证状态一致性。
具体组合架构属于实现选择,不是通用算法必然要求。
代价与适用边界
HTN 适合
领域步骤结构明确
设计者能提供可靠的任务分解知识
希望计划具有较强可解释性
动作前置条件和效果可以建模
HTN 的代价:
维护方法库需要持续领域建模
未覆盖的情形可能直接无法规划
回溯与成本优化会增加搜索复杂度
执行世界与模拟世界不一致时需要重新规划协议
MCTS 适合
合法动作与状态转移可以模拟
分支很多,完整搜索不可行
允许用预算换取逐渐改善的估计
结果可通过重复模拟评价
MCTS 的代价:
需要大量模拟
结果受随机性、奖励与模拟策略影响
实时预算很小时统计可能不稳定
连续动作空间通常还需要专门的动作生成策略
不适合硬套的情况
HTN 不擅长凭空发现领域方法之外的新分解结构。
MCTS 不擅长:
没有可信模拟器的问题
一次错误探索代价不可接受且不能离线模拟的问题
奖励极其稀疏、很难在预算内观察到的问题
通用算法与实现选择
可验证的通用机制
HTN 通过方法把复合任务递归分解为原子任务。
方法具有适用条件,原子任务具有可执行条件与效果模型。
MCTS 反复执行选择、扩展、模拟和回传。
MCTS 使用访问与价值统计在探索和利用之间取得平衡。
两种模型的结论都依赖输入状态与领域模型的正确性。
必须由具体实现确定
HTN 是否回溯、是否优化代价、是否支持偏序
HTN 计划失效后的重规划协议
MCTS 的预算单位与大小
MCTS 的探索常数、模拟策略和奖励尺度
最终动作按访问次数还是平均价值选择
随机种子与并行搜索方式
两种模型组合时的状态和价值接口
最后总结
HTN 的问题是:
按照我们掌握的领域步骤,这个任务应该怎样拆解?
MCTS 的问题是:
在有限计算预算内,哪些动作经过更多模拟后更值得选择?
判断使用哪一种,不应只看系统是否“智能”,而应确认:
是否存在可靠的层级任务知识
是否能够可信地模拟动作结果
需要完整计划还是当前动作
更看重可解释结构还是预算内搜索
失败时能否说明是无方法、无动作、模型错误还是预算不足