同一件装备的两个问题

设想虚构游戏《灰烬回廊》里一件叫“余烬护腕”的装备。玩家A问:“余烬护腕从哪里掉落?”玩家B问:“我要拿到余烬护腕,最快怎么走?”这两个问题在搜索框里几乎长得一样,对系统的要求却相差很远。

玩家A的问题是一次实体属性查询。知识库里只要有一条“余烬护腕:由铁匠合成”的记录,答案就出来了。这类问题图谱当然能答,甚至一张普通表格也能答。

玩家B的问题不同。假设合成余烬护腕需要三种材料:黑铁锭、灰烬晶、潮汐鳞片。黑铁锭来自矿区,而矿区被一座断掉的吊桥挡着;灰烬晶要完成支线“守灯人的委托”才能拿到,支线又要求玩家先解锁钟塔区域;潮汐鳞片可以从商人处购买,也可以靠钓鱼获得。这些数字和关系都是示例,用来说明结构。

玩家B真正需要的不是“护腕由什么合成”,而是“我从当前状态出发,还需要做哪几件事,按什么顺序做”。这是一个目标规划问题,在事实查询与目标规划的差别里,我们把它和单纯的实体、关系查询分成了不同层级。本文只讲这一层的数据结构:目标导向图(Goal-Oriented Graph,即以“达成某个目标”为中心组织的依赖图)。

普通实体图为什么不够用

常见的游戏知识图谱以实体为中心:物品、角色、地图、任务作为节点,“属于”“位于”“出现在”作为边。这种图适合回答“它是什么”“它在哪”,也是游戏知识图谱的基础形态。

问题出在边的含义太宽松。假设图里有一条“灰烬晶——关联——守灯人的委托”的边,它既可能表示“灰烬晶是委托的奖励”,也可能表示“委托需要灰烬晶”,甚至只是“两者出现在同一篇攻略里”。规划算法一旦分不清这些方向,就会得出荒谬的路线,例如先要灰烬晶才能开始拿灰烬晶的支线。

目标导向图的关键改动,是让每条边都有明确、可计算的语义。爱游戏的做法是把边限定在一组小而稳定的类型里,而不是任由自由文本描述关系。

边的六种语义

边类型 含义 示例 规划时的处理
需要(requires) 合成或使用前必须持有 余烬护腕 需要 黑铁锭向前展开,成为子目标
产出(yields) 完成后获得 守灯人的委托 产出 灰烬晶 反向查找“谁能产出该材料”
解锁(unlocks) 完成后开放区域或功能 修复吊桥 解锁 矿区 作为通行前置
前置(precedes) 必须先完成的任务顺序 钟塔任务 先于 守灯人的委托 决定步骤先后
可选(alternative) 多个来源任选其一 潮汐鳞片:购买 或 钓鱼 比较代价后选择
互斥(excludes) 选了一个就排除另一个 结盟“渔村”与“盐帮”不可兼得 剪掉冲突分支并提示后果

这张表看着朴素,却决定了后面所有算法能不能成立。“需要”和“产出”方向相反,一个从目标往前追,一个从材料往后找;“解锁”和“前置”都表达先后,但前者关于区域和功能,后者关于任务顺序;“可选”意味着这里是一个选择点,规划器必须计算代价,而不能全部塞进步骤清单;“互斥”是最容易被忽视的,它会让一条看似最短的路线在某个玩家眼里变成“会毁掉存档进度的路线”。

从目标出发:依赖展开

有了带类型的图,规划可以拆成几个固定步骤。

  1. 确定目标节点:把玩家的话解析成图上的一个节点,比如“余烬护腕”。这一步由语言模型完成,而不是图算法。
  2. 向前展开依赖:沿着“需要”“前置”“解锁”边递归展开,直到到达不再有依赖的叶子节点,比如“击杀矿区守卫”这样的基础动作。
  3. 处理可选分支:遇到“可选”边,先不选,而是保留为一个带选项的节点,等后面统一比较。
  4. 检测环:如果展开过程中回到已经访问过的节点,说明数据可能有误,或者游戏本身存在循环依赖,需要标记而不是无限展开。
  5. 得到依赖子图:这张子图只包含与目标有关的节点,往往比整个游戏图谱小得多。

环的问题值得单独说。真实的游戏数据里,环有两种来源:一种是数据错误,比如“需要”边被误标了方向;另一种是设计上的“软循环”,例如某个道具既能合成也能从任务奖励里拿到,但两条路径本身并不互相依赖。前者要回到数据源修正,后者要在展开时用“路径上已访问节点集合”来区分,遇到可选来源时分别记账,避免把软循环误判为死循环。

按玩家进度剪枝:只留下“剩余步骤”

依赖子图给出的是“从零开始”的全部工作。玩家往往已经做了很多,因此下一步要根据进度剪枝。

假设玩家的存档显示:吊桥已修复、矿区已开放,背包里已经有两块黑铁锭。剪枝规则是:已完成的节点直接移除,其下游的“解锁”“前置”依赖一并视为已满足;已持有的材料按数量抵扣。这样,“修复吊桥”和“获取黑铁锭”从清单里消失,剩下的是灰烬晶和潮汐鳞片两条支线。

这一步有两个细节。其一,进度信息可能是过期的或不完整的:玩家换过存档,或者刚在别的设备上完成了任务。爱游戏AI会在结果里写明“按你已完成吊桥修复计算”,让玩家能一眼发现假设不成立。其二,剪枝必须传递地进行:如果某个节点已完成,它的所有仅仅为它服务的子依赖都不该再出现,否则清单里会出现“你已经不需要做但仍被列出”的步骤。

最短不等于最快

剪枝后,图里可能仍有多个选择点,例如潮汐鳞片“购买”还是“钓鱼”,灰烬晶的支线要不要先做钟塔区域。这时算法层面的“最短路径”就不够了,因为边数最少不等于玩家花的时间最少。

每个节点和边需要携带几类属性:预计耗时(假设一次钓鱼约十分钟,购买约一分钟但需要金币)、风险(是否有可能失败、是否要面对难度较高的战斗)、资源代价(要花掉的金币或消耗品)、可并行性(钓鱼时可以同时挂机等待任务冷却吗)。这些数字都是设计示例,实际取值需要来自内容数据,而不是拍脑袋。

有了这些属性,规划器不再是“找最短路”,而是在一个多目标问题里挑一个合理的平衡。举个例子,路径甲需要连续三次挑战同一个精英怪,路径乙多走两个区域但没有战斗。按边数,乙更长;按玩家体验,乙对战斗不熟练的玩家可能更快,也更稳。因此输出时我们倾向于给出一条推荐路线,并在旁边简要列出“如果你更想省时间/更想避战斗,可以改走另一条”,让权衡显式化。

进度变了要重算,而不是继续照抄

规划结果不是一次性的文档。玩家走到第二步时可能顺手完成了第四步,也可能死亡后丢失了刚拿到的材料,这些事件都会改变“剩余步骤”。我们倾向于把规划做成可以随时重跑的函数:输入是目标节点加当前存档状态,输出是当前的剩余步骤,而不是把第一次算出的清单存起来慢慢划掉。

重算的触发条件要克制。背包变化、任务状态变化、区域解锁这三类事件才需要重算,普通的移动和战斗不需要。否则清单会一直跳动,玩家反而不知道该跟着哪一版走。

输出:不是一张图,而是一份可执行的步骤

玩家不需要看到图。规划结果最终要落成一份有序的步骤清单,每一步包含三样东西:做什么、在哪里做、为什么必须做。例如:“1. 前往钟塔区域并完成开门任务(灰烬晶支线的前置)。2. 接取守灯人的委托,获得灰烬晶。3. 在商人处购买潮汐鳞片,或在河边钓鱼获取。4. 回到铁匠处合成。”

其中“为什么必须做”这一栏,不只是说明性文字,它同时也是校验手段:如果玩家跳过了某一步,系统可以指出后面哪一步会因此受阻。可并行的步骤应该合并成同一层级,标注“可与上一步同时进行”。当大模型负责把结果说成自然语言时,它只能重述图给出的步骤,不能自己增删。关于这种“语言模型解析目标、图系统找路径、再组织结果”的分工,在Graph Agent的三段职责里有更详细的说明。

常见的失败模式

失败做法为什么会错 更好的做法
把所有关联都当成依赖“攻略里同时出现”不代表“需要”,会产生多余步骤 只用带类型的边,来源不明的边降权
把可选来源当作必需 清单里同时出现购买和钓鱼,玩家不知道该做哪个可选节点单独建模,比较后只保留一个
忽略互斥推荐了会锁死另一条支线的路线互斥边参与规划,并在结果中提示后果
不做剪枝 把已完成的步骤再列一遍,损失信任 按存档进度剪枝,并写明所依据的进度
使用过期的边 版本更新后解锁条件改变,路线失效 边带版本和有效期字段,规划时校验
只优化边数 找到一条“短但难”的路引入耗时、风险与并行度属性

其中“过期的边”是最容易在长期运营里出问题的一项。数据一旦不再随版本更新,目标导向图就会自信地给出错误路线,比没有路线还糟。

这套设计的边界

目标导向图擅长“有明确目标、依赖关系清晰”的问题,比如合成装备、解锁区域、完成成就。对于开放探索、玩家自己决定要怎么玩的场景,它没有一个唯一的“最优路线”可以计算,此时应该只提供依赖信息,而不是替玩家规划。爱游戏的做法是让规划的输出可以随时被玩家覆盖:不想走推荐路线时,玩家可以指定“我不想打这个精英怪”,图上对应节点被临时标记为不可用,路线重算。

装备为什么“好查难规划”,答案其实很朴素:查询只需要一条记录,规划需要一张带语义的图、玩家的进度,以及对代价的判断。三者缺一,得到的都只是一份看起来合理的清单。