# 聊天系统与存档架构技术文档 本文档详细说明了 LLM 聊天系统的双层数据架构(滚动压缩 + 全局存档)以及通用的存档系统实现。 ## 1. 核心架构概述 为了解决 LLM API Token 限制与游戏数据持久化之间的矛盾,本项目采用了 **“双层数据分离”** 策略: | 特性 | API 上下文 (Context Window) | 全局存档 (Global Chat Log) | | :--- | :--- | :--- | | **位置** | `DeepSeekService` / `DoubaoService` | `LLMChatManager` | | **生命周期** | 临时,随对话进行滚动修剪 | 永久,贯穿游戏存档 | | **用途** | 维持 API 短期记忆,控制成本 | 历史回看、剧情记录 | | **数据结构** | `List` (限制长度) | `List` (无限增长) | --- ## 2. 滚动压缩 (Rolling Compression) ### 2.1 原理 为了防止对话历史无限增长导致 API 请求失败或费用激增,服务层实现了自动修剪逻辑。 ### 2.2 实现细节 * **脚本**: `DeepSeekService.cs`, `DoubaoService.cs` * **参数**: `maxContextWindow` (默认 20 条) * **逻辑 (`TrimHistory`)**: 1. **保护 System Prompt**: 列表索引 `0` 的消息(系统提示词)永远不会被删除。 2. **移除旧消息**: 当 `messageHistory.Count > maxContextWindow` 时,从索引 `1` 开始移除最早的消息,直到数量满足限制。 ### 2.3 代码示例 ```csharp 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` 接口 所有需要存档的系统(如背包、任务、玩家状态)都必须实现此接口。 ```csharp public interface ISaveable { string GetSaveID(); // 返回唯一标识符 (例如 "Inventory") string CaptureState(); // 返回序列化后的 JSON 字符串 void RestoreState(string json); // 根据 JSON 恢复状态 } ``` #### `SaveManager` 管理器 * **单例模式**: `SaveManager.Instance` * **职责**: * 维护 `saveables` 列表。 * 处理文件读写 (`savegame.json`)。 * **晚注册支持**: 维护 `loadedDataCache`,即使某个模块在游戏运行后期才加载(如动态生成的 UI),在注册时也能立即获取到它的存档数据。 ### 3.2 扩展指南:如何添加新存档模块 假设你要为“背包系统”添加存档功能: 1. **实现接口**: 让 `InventoryManager` 实现 `ISaveable`。 2. **定义数据**: 创建一个私有类 `InventoryData` 用于序列化。 3. **注册**: ```csharp void Start() { SaveManager.Instance.Register(this); } void OnDestroy() { SaveManager.Instance.Unregister(this); } ``` 4. **实现方法**: * `GetSaveID()` -> 返回 "InventorySystem"。 * `CaptureState()` -> `JsonUtility.ToJson(myData)`. * `RestoreState(json)` -> `JsonUtility.FromJson(json)`. --- ## 4. 聊天记录的持久化流程 `LLMChatManager` 实现了 `ISaveable` 接口,负责将 `fullChatLog` 写入磁盘。 ### 4.1 数据流向 1. **用户发送**: * UI 调用 `LLMChatManager.SendUserMessage`。 * 消息被添加到 `fullChatLog` -> **立即触发 SaveGame**。 * 消息被转发给 `DeepSeekService` -> 添加到临时列表 -> **触发滚动压缩** -> 发送 API。 2. **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`。 格式示例: ```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`(播放旁白)。