Facts 不是摘要,而是状态锚点
在《回声》中,Facts 用来表达关键世界事实,例如电力状态、门锁状态、道具位置、任务阶段和剧情结论。它们不追求文学表达,而追求短、稳、可验证。
{
"facts": [
{
"key": "道具-钥匙-红",
"value": "在餐厅后厨的金属柜第二层",
"source": "Preset",
"timestamp": 0
}
],
"counter": 0
}
key 结构采用“域-对象-字段”。例如 世界-电力、门-主门-锁状态、状态-ECHO7-已被识破。这种写法并不追求文学表达,而是服务于系统读取和实验对照验证。
预置与事件注入
新游戏无存档时,系统可以从 StreamingAssets 读取预置 Facts,使 ECHO-7 在第一轮对话前就知道基础世界状态。随着玩家交互、任务推进或剧情节点触发,FactsSequence 又可以通过 UnityEvent 批量 Add、Update 或 Delete。
开局注入关键事实,例如钥匙位置、地堡状态、当前阶段目标。
玩家恢复电力、阅读文档、触发旁白或完成任务时更新 Facts。
ContextMemoryManager 把当前 Facts 重新组织进 System Prompt。
这种做法的价值在于,玩家看到的线索和模型读取的状态来自同一套系统记录。模型可以解释和重述,回答不再依赖凭空猜测。
严格 JSON 命令
Function Calling 在《回声》中不是让模型“随便调函数”,而是让模型把可见文本与行为请求拆开。玩家看到 text,系统读取 commands。
{
"text": "门已经解锁,我正在为你打开。",
"commands": [
{ "type": "UnlockState", "targetId": "door_main_lock", "value": true },
{ "type": "Door", "targetId": "door_main", "action": "Open" }
]
}
本项目的命令类型包括 UnlockState、Door、Event、Task、SetActive 和 Narration。它们必须指向场景中注册过的目标对象,并经由路由器和 Receiver 作用到 Unity 组件。
真实代码节选:解析器只接受能被 JsonUtility 映射到命令信封的 JSON,路由器再按 targetId 分发到注册过的 Receiver。
private static bool TryParseEnvelope(string json, out LLMCommandEnvelope envelope)
{
envelope = null;
try
{
envelope = JsonUtility.FromJson<LLMCommandEnvelope>(json);
}
catch (Exception)
{
envelope = null;
}
if (envelope == null) return false;
if (envelope.commands == null && string.IsNullOrWhiteSpace(envelope.text)) return false;
return true;
}
public int ExecuteAll(LLMCommand[] commands)
{
if (commands == null || commands.Length == 0) return 0;
int executed = 0;
for (int i = 0; i < commands.Length; i++)
{
var cmd = commands[i];
if (cmd == null) continue;
if (string.IsNullOrWhiteSpace(cmd.targetId)) continue;
if (!map.TryGetValue(cmd.targetId, out var receiver) || receiver == null) continue;
if (receiver.TryExecute(cmd)) executed++;
}
if (debugLogs)
Debug.Log($"[LLMCommandRouter] Executed commands={executed}/{commands.Length}", this);
return executed;
}
拒绝与兜底
- 命令格式不合法:只显示可见文本,不执行副作用。
- 命令类型不在白名单:拒绝执行,并可记录调试日志。
- targetId 不存在:拒绝执行,避免“说对了但操作错对象”。
- 剧情门槛不满足:保留角色回应,但不改变关键状态。
调试阶段可以用本地密钥门槛快速测试,但公开站点和论文展示中不应暴露真实密钥。正式流程更适合用 Facts 和剧情阶段作为权限依据,例如只有当 状态-ECHO7-已被识破=是 后,关键结局命令才允许进入执行链路。
真实代码节选:剧情门槛通过 Facts 判断,不把“模型说可以”当成执行凭证。
private bool IsStoryCommandStateUnlocked()
{
if (contextMemory == null) return false;
if (contextMemory.facts == null) return false;
if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactKey)) return true;
for (int i = 0; i < contextMemory.facts.Count; i++)
{
var f = contextMemory.facts[i];
if (f == null) continue;
if (!string.Equals(f.key, restrictCommandsRequireFactKey, StringComparison.Ordinal)) continue;
string v = f.value ?? "";
if (string.IsNullOrWhiteSpace(restrictCommandsRequireFactValueContains)) return true;
return v.IndexOf(restrictCommandsRequireFactValueContains, StringComparison.Ordinal) >= 0;
}
return false;
}
最重要的安全边界是:模型输出可以提出请求,最终是否执行仍由游戏系统决定。