今年 6 月,KRAFTON 与 NVIDIA 将 PUBG Ally 带入公开 Beta。这个 AI 队友不再把每局游戏当成孤立会话:它能够记住玩家偏好的武器、常去的跳点和过往战绩,并在下一局继续使用这些信息。跨局记忆让 AI 从“会聊、会打”迈向“有连续性”,但也带来更难的问题:角色记得过去之后,它的立场、策略和说话方式是否应该随经历变化?
这正是 PHASE-Tree 关注的状态演化难题。记忆只是输入,成长则需要一套可控、可解释、不会失控的状态更新机制。
记住事实,不等于形成角色状态
一个简单的记忆系统可能只保存事件:
{
"event": "玩家在山地决赛圈选择绕后",
"outcome": "成功淘汰两名敌人",
"timestamp": "match-1842"
}
这类记录对检索很有用,但它没有说明 AI 应该如何改变自己。要让角色呈现出“成长”,至少需要区分三层信息:
- 事件记忆:发生了什么,例如玩家喜欢 M416,或者曾经因为过早冲锋输掉比赛。
- 归纳认知:从多次事件中提炼出的判断,例如“玩家在资源充足时更愿意主动进攻”。
- 当前状态:角色此刻的信任度、谨慎程度、主动性、情绪和战术偏好。
事件是证据,认知是解释,状态才是下一次决策真正会读取的变量。如果每次对话都只把历史记录拼进 prompt,AI 可能“说自己记得”,却没有稳定地改变行为。
可以把角色状态抽象成一个随时间变化的向量:
state(t+1) = clamp(
state(t)
+ learning(event, outcome, feedback)
- decay(time, contradiction)
+ phase_transition(trigger),
valid_range
)
这里的关键不是公式本身,而是把状态更新从生成文本中分离出来。语言模型负责理解事件、提出候选更新和表达角色;规则或状态机负责确认哪些变化可以落地。
PHASE-Tree:用阶段和分支组织变化
“成长”不应该意味着所有数值都持续上升。一个经历过背叛的角色可能变得谨慎,也可能因为后来重新建立合作而恢复信任。角色的变化更像一棵树:
- 阶段(Phase)规定角色当前处于什么发展区间,例如初识、磨合、信任或冲突。
- 轴(Axis)描述可连续变化的属性,例如信任、冒险倾向、依赖程度和表达直接性。
- 触发器(Trigger)来自游戏事件,例如连续获胜、关键时刻被救援、违反约定或长时间没有互动。
- 转移条件(Transition)决定何时进入新阶段,并规定进入后可以改变哪些行为。
例如,“初识”阶段的 AI 可能更多询问玩家意图;进入“信任”阶段后,它可以主动提出战术建议;如果玩家多次违背共同计划,角色进入“戒备”分支,语气和行动都会收缩。
这种设计有两个实际好处。第一,状态变化有边界,产品团队能审查每条转移路径。第二,调试时可以回答“角色为什么这样说”,而不是只能回看一长串 prompt。
一个可改造的最小状态引擎
下面的 Python 示例演示了最小实现。它不是完整游戏 AI,而是可以嵌入服务端的状态更新核心:事件经过规则判断后,修改角色状态并检查阶段转移。运行前只需要 Python 3.10 或更高版本。
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class AllyState:
phase: str = "初识"
trust: float = 0.20
caution: float = 0.50
initiative: float = 0.40
facts: Dict[str, str] = field(default_factory=dict)
def apply_event(self, event: str, outcome: str) -> None:
self.facts[event] = outcome
if event == "player_rescued_ally":
self.trust += 0.20
self.initiative += 0.05
elif event == "player_broke_plan":
self.trust -= 0.15
self.caution += 0.20
elif event == "team_won":
self.trust += 0.05
self.initiative += 0.05
self.trust = min(1.0, max(0.0, self.trust))
self.caution = min(1.0, max(0.0, self.caution))
self.initiative = min(1.0, max(0.0, self.initiative))
self._update_phase()
def _update_phase(self) -> None:
if self.trust >= 0.70 and self.caution < 0.65:
self.phase = "信任"
elif self.trust < 0.15 and self.caution >= 0.70:
self.phase = "戒备"
elif self.trust >= 0.40:
self.phase = "磨合"
else:
self.phase = "初识"
state = AllyState()
for event, outcome in [
("player_rescued_ally", "玩家在决赛圈救起队友"),
("team_won", "小队获胜"),
("player_broke_plan", "玩家再次独自冲锋"),
]:
state.apply_event(event, outcome)
print({
"phase": state.phase,
"trust": round(state.trust, 2),
"caution": round(state.caution, 2),
"initiative": round(state.initiative, 2),
})
在真实项目中,可以把 apply_event 前置为一个事件解析层:由模型将自然语言日志转换为受限的事件类型和参数,再由服务端校验。不要让模型直接返回任意状态值,否则一次异常输出就可能把角色从“合作”跳到“绝对忠诚”。
让状态影响行为,而不只是影响台词
状态演化的验收标准不应是“角色说话更像人”,而应是同一个事件在不同状态下产生不同决策。例如:
输入:敌人在东侧,玩家弹药不足。
初识: “东侧可能有敌人。你想绕开还是观察?”
信任: “我掩护你换弹,我们从北侧压过去。”
戒备: “我不会跟着你冲东侧。先撤到掩体,再决定。”
三种表达背后应该对应不同的行动约束:是否主动标记目标、是否共享资源、是否跟随玩家进入高风险区域。否则,状态只是一个会被 prompt 读取的装饰字段。
可以将决策上下文拆为稳定规则和动态信息:
{
"character_policy": {
"never": ["泄露其他玩家隐私", "绕过战术安全限制"],
"phase_rules": {
"信任": {"share_loot": true, "risk_tolerance": 0.75},
"戒备": {"share_loot": false, "risk_tolerance": 0.25}
}
},
"runtime_state": {
"phase": "信任",
"trust": 0.78,
"recent_events": ["player_rescued_ally", "team_won"]
}
}
模型可以在这个上下文中生成候选计划,但最终动作仍应经过游戏规则、权限和安全检查。
工程落地时要处理的边界
状态不能无限累积。 跨局记忆会越积越多,需要摘要、过期和冲突处理。一次偶然失误不应该永久改变角色;重复发生且结果一致的事件,才适合提升为长期认知。
状态转移必须可回放。 每次更新记录旧状态、事件、规则版本和新状态。出现“角色突然变了”的问题时,开发者才能复现完整路径。
玩家应当拥有可感知的反馈。 角色不必展示内部数值,但可以通过战术建议、资源分配和关系台词让玩家理解变化来自哪里。若成长完全不可预测,玩家感受到的可能不是深度,而是不稳定。
模型输出需要受限。 采用枚举事件、数值范围、阶段白名单和人工审核样本,可以降低提示注入、幻觉归因以及状态漂移的风险。
评测要覆盖长线轨迹。 单轮对话测试无法证明角色会成长。应准备包含成功、失败、矛盾、冷却期和关系修复的多局剧本,检查角色是否保持连续性,也检查它是否有能力改变。
采用清单
可以按下面的顺序启动一个可控版本:
- 先定义 5 到 10 个可观测状态轴,并为每个轴设定范围和含义。
- 将游戏日志归一化为有限事件类型,区分短期记忆和长期认知。
- 用阶段树写出允许的转移条件,禁止模型自行创建新阶段。
- 让状态同时影响计划、动作和台词,而不是只改 prompt 中的描述。
- 为每次转移保存可回放日志,并用长线剧本做回归测试。
- 观察玩家是否能理解变化、是否愿意继续与角色合作,再调整变化速度。
跨局记忆解决的是“AI 还记得什么”,PHASE-Tree 要解决的是“这些经历如何改变它”。当事件、认知、状态和行为之间形成可追踪的闭环,AI 队友才会从一组会话技巧,变成一个能够在长期关系中保持连续性、同时允许变化的游戏角色。