8.3 KiB
8.3 KiB
聊天系统与存档架构技术文档
本文档详细说明了 LLM 聊天系统的双层数据架构(滚动压缩 + 全局存档)以及通用的存档系统实现。
1. 核心架构概述
为了解决 LLM API Token 限制与游戏数据持久化之间的矛盾,本项目采用了 “双层数据分离” 策略:
| 特性 | API 上下文 (Context Window) | 全局存档 (Global Chat Log) |
|---|---|---|
| 位置 | DeepSeekService / DoubaoService |
LLMChatManager |
| 生命周期 | 临时,随对话进行滚动修剪 | 永久,贯穿游戏存档 |
| 用途 | 维持 API 短期记忆,控制成本 | 历史回看、剧情记录 |
| 数据结构 | List<Message> (限制长度) |
List<Message> (无限增长) |
2. 滚动压缩 (Rolling Compression)
2.1 原理
为了防止对话历史无限增长导致 API 请求失败或费用激增,服务层实现了自动修剪逻辑。
2.2 实现细节
- 脚本:
DeepSeekService.cs,DoubaoService.cs - 参数:
maxContextWindow(默认 20 条) - 逻辑 (
TrimHistory):- 保护 System Prompt: 列表索引
0的消息(系统提示词)永远不会被删除。 - 移除旧消息: 当
messageHistory.Count > maxContextWindow时,从索引1开始移除最早的消息,直到数量满足限制。
- 保护 System Prompt: 列表索引
2.3 代码示例
private void TrimHistory()
{
// 始终保留 index 0 (System Prompt)
if (messageHistory.Count > maxContextWindow)
{
// 从 index 1 开始移除多余的消息
int removeCount = messageHistory.Count - maxContextWindow;
if (removeCount > 0)
{
messageHistory.RemoveRange(1, removeCount);
}
}
}
3. 通用存档系统 (Core.SaveSystem)
本项目实现了一套基于接口的、高内聚低耦合的存档系统。
3.1 核心组件
ISaveable 接口
所有需要存档的系统(如背包、任务、玩家状态)都必须实现此接口。
public interface ISaveable
{
string GetSaveID(); // 返回唯一标识符 (例如 "Inventory")
string CaptureState(); // 返回序列化后的 JSON 字符串
void RestoreState(string json); // 根据 JSON 恢复状态
}
SaveManager 管理器
- 单例模式:
SaveManager.Instance - 职责:
- 维护
saveables列表。 - 处理文件读写 (
savegame.json)。 - 晚注册支持: 维护
loadedDataCache,即使某个模块在游戏运行后期才加载(如动态生成的 UI),在注册时也能立即获取到它的存档数据。
- 维护
3.2 扩展指南:如何添加新存档模块
假设你要为“背包系统”添加存档功能:
- 实现接口: 让
InventoryManager实现ISaveable。 - 定义数据: 创建一个私有类
InventoryData用于序列化。 - 注册:
void Start() { SaveManager.Instance.Register(this); } void OnDestroy() { SaveManager.Instance.Unregister(this); } - 实现方法:
GetSaveID()-> 返回 "InventorySystem"。CaptureState()->JsonUtility.ToJson(myData).RestoreState(json)->JsonUtility.FromJson<InventoryData>(json).
4. 聊天记录的持久化流程
LLMChatManager 实现了 ISaveable 接口,负责将 fullChatLog 写入磁盘。
4.1 数据流向
- 用户发送:
- UI 调用
LLMChatManager.SendUserMessage。 - 消息被添加到
fullChatLog-> 立即触发 SaveGame。 - 消息被转发给
DeepSeekService-> 添加到临时列表 -> 触发滚动压缩 -> 发送 API。
- UI 调用
- AI 回复:
- API 回调返回文本。
- 消息被添加到
fullChatLog-> 立即触发 SaveGame。 - 消息被添加到临时列表 -> 触发滚动压缩。
- UI 更新显示。
4.2 UI 层面的历史记录呈现
新增了 ChatHistoryPanelController.cs 专门用于呈现完整的对话历史。
- 触发机制: 当包含该脚本的 UI 面板(如 Reach UI 的 History Panel)被激活(
OnEnable)时,会自动调用RefreshHistoryUI()。 - 数据来源: 直接读取
LLMChatManager.fullChatLog。 - 过滤规则: 仅实例化
role为user或assistant的消息,过滤掉系统提示词(system)。 - 性能考量: 相比于
TabletController主聊天窗口保留最近 50 条消息的滚动删除机制,History Panel 会全量渲染所有历史记录(每次打开时清理旧 UI 节点并重新生成),适合供玩家通过专门的历史界面进行完整回顾。
4.2.1 聊天气泡换行与最大宽度
聊天窗口(AssistPanel)与历史窗口(HistoryPanel)共用同一个气泡预设体 MessageRow V2,气泡的自动换行行为由预设体与 MessageAligner 共同决定。
- 预设体关键点:
- 文本组件为
TextMeshProUGUI,必须开启 Word Wrapping。 - 气泡背景(Bubble)通过布局组件自适应内容尺寸。
- 文本组件为
- 动态限宽:
MessageAligner会优先按父级ScrollRect的Viewport实际宽度计算“有效最大气泡宽度”,并使用viewportWidthRatio进行比例限制。maxBubbleWidth作为硬上限,用于避免在超宽 UI 下出现过长单行。
- 排查清单(不换行/溢出屏幕):
ScrollRect.m_Viewport必须正确指向对应的 Viewport(否则无法得到准确可用宽度)。MessageAligner.bubbleBackground必须绑定到 Bubble 的Image,否则无法定位到正确的文本组件进行限宽。MessageAligner.clampToViewportWidth需保持启用;如需要更早换行,调小viewportWidthRatio(例如 0.6)。- 气泡预设体的根节点与 Bubble/Text 的
RectTransform不应保留非零AnchoredPosition或固定宽度,否则会干扰布局系统对宽度与换行的接管。
4.3 存档文件
存档位于 Application.persistentDataPath/savegame.json。
格式示例:
{
"keys": ["ChatSystem"],
"values": ["{\"logs\":[{\"role\":\"user\",\"content\":\"你好\"},...]}"]
}
4.4 预置 Facts (Preset Facts)
为了在“新游戏无存档”的情况下让 AI 一开始就具备关键世界事实(例如钥匙位置),项目支持从 StreamingAssets 读取预置 Facts,并写入 ContextMemoryManager.facts,随后注入到 L1 的 System Prompt。
- 预置文件路径:
Assets/StreamingAssets/LLM/preset_facts.json - 加载时机:
PresetFactsInstaller在检测到savegame.json不存在时才会加载预置 Facts,并且不会主动写入savegame.json,以避免影响“新游戏判定/流程”。若savegame.json存在但缺少ContextMemory条目,也会自动补齐预置 Facts。 - 落盘时机: 当首次触发
SaveManager.SaveGame()(例如玩家首次聊天)时,Facts 才会随其它系统一起写入savegame.json。
4.5 通过 UnityEvent 动态注入 Facts
项目提供 FactsSequence 工作流,允许在交互/任务等 UnityEvent 触发时,把一组 FactOperation 写入 ContextMemoryManager.facts。
- 配置方式: 在场景中创建一个父物体挂
FactsSequence,子物体挂FactOperationBehaviour配置 type/key/value/source。 - 触发方式: 在任意 UnityEvent 中直接绑定
FactsSequence.Apply()。 - 写入策略: 对 Facts 做 ADD/UPDATE/DELETE 后,会更新 system prompt,并可选立即存档。
4.6 Function Calling(严格 JSON + 调试密钥门槛)
项目支持 AI 在回复末尾输出严格 JSON 指令,并由运行时解析执行(例如开门/解锁)。
- JSON 协议:
{"text":"给玩家看的文本","commands":[{"type":"UnlockState","targetId":"door_main_lock","value":true}]} - 安全门槛: 可选启用“调试密钥门槛”(形如
[2310487514]),用于 Debug 阶段方便测试;正式流程可关闭该门槛并改用剧情状态 Facts 作为执行条件(例如状态-ECHO7-已被识破=是后才允许执行关键命令)。 - 显示规则: UI 与
fullChatLog只记录text(玩家可见文本),不会把命令 JSON 显示/存入历史。 - 扩展指令: 已支持
Task(任务状态)、SetActive(激活/隐藏物体)、Narration(播放旁白)。