同一件装备的两个问题

设想虚构游戏《灰烬回廊》里一件叫“余烬护腕”的装备。玩家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的三段职责里有更详细的说明。

常见的失败模式

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

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

这套设计的边界

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

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