# 结构化会话存储 - 设计文档 ## 1. 文档信息 | 项目 | 说明 | |------|------| | 功能名称 | 结构化会话存储(Structured Conversation Store) | | 版本 | 3.0 | | 对应需求 | docs/observation-summary-requirements.md(v3.0) | --- ## 2. 总体设计 ### 2.1 设计原则 - **单一事实来源**:会话级**结构化存储**(Conversation Store)是唯一真相源;所有消息与卡片按时间顺序以结构化条目形式存在其中。 - **UI 与 Prompt 为视图**:不直接使用「混入特殊消息的 ChatHistory」;UI 列表与发给 LLM 的历史均**由存储派生**。 - **LLM History 为派生视图**:`ChatHistory` 仅在调用 LLM 时,从会话存储**按规则投影**得到(仅文本或文本+短占位),不参与持久化真相、不承担「存卡片」职责。 - **YAML 承载载荷**:特殊消息的载荷采用 YAML(对 AI 与人可读性更好);存储整体持久化也可采用 YAML。 ### 2.2 架构概览 ``` ┌─────────────────────────────────────────┐ │ 结构化会话存储(Conversation Store) │ │ 唯一事实来源:按序的条目列表 │ │ - TextEntry(Role, Content) │ │ - SpecialEntry(Type, YamlPayload) │ └───────────────────┬─────────────────────┘ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ │ UI 视图 │ │ Prompt 视图 │ │ 持久化 │ │ 遍历 Store → │ │ 遍历 Store → │ │ Store → YAML/JSON │ │ ChatMessages │ │ ChatHistory │ │ 加载时反序列化 → Store │ │ (文本 + 卡片) │ │ (仅文本或+短占位) │ │ │ └───────────────────────┘ └───────────────────────┘ └───────────────────────┘ │ │ ▼ ▼ ┌───────────────────────┐ ┌───────────────────────┐ │ MainWindowViewModel │ │ KernelService │ │ LoadMessagesFromStore│ │ AskAsync 使用 │ │ 绑定到界面 │ │ GetHistoryForLlm() │ └───────────────────────┘ └───────────────────────┘ ``` --- ## 3. 会话存储的数据结构 ### 3.1 条目类型(抽象) 每条记录为以下两种之一(或通过 type 区分的并集): | 条目类型 | 含义 | 主要字段 | |----------|------|----------| | **TextEntry** | 纯文本消息 | Role(User/Assistant/System), Content(string) | | **SpecialEntry** | 特殊消息(卡片) | Type(见下), Payload(YAML 字符串) | - **实现类型**:`ConversationEntry` 为抽象基类;具体为 `TextConversationEntry`、`SpecialConversationEntry`。持久化 DTO 使用 `Kind`("Text" / "Special")、`ConversationEntryMapper` 与 `ConversationEntryDto` 互转。 - 顺序由列表下标保证;新增条目一律追加(`AppendText` / `AppendSpecial`)。 ### 3.2 存储的归属 - 存储与**会话**一一对应:每个 `ChatSession`(或等价会话对象)持有一份「会话存储」实例(`ConversationStore` 类型,其内部持有 `IList`)。即**存储由 ChatSession 持有**,每会话一份。 - 原 `ChatSession.History`(`ChatHistory`)可保留用于**兼容或过渡**,但**不再作为真相源**;在「构建发给 LLM 的历史」时,由 `GetHistoryForLlm()` 从存储生成新的 `ChatHistory` 传入后端,而不是直接读 `session.History`。 ### 3.3 System 消息的处理 - 若产品需要「每条会话有一条 System 引导词」,可在存储中**不**存 System,而在生成 Prompt 视图时**在生成的 ChatHistory 头部插入一条 System**(内容为 guidePrompt);或约定存储中允许一条 Role=System 的 TextEntry(仅一条、仅作引导)。具体由实现决定;原则是 System 不混入摘要、不存大段 YAML。 --- ## 4. 特殊消息 YAML 载荷格式 ### 4.1 通用字段 每条 SpecialEntry 的 Payload(YAML)至少包含: - **type**:`Form` | `ParameterSet` | `Table` | `ColumnMatch` | `WorkflowStatus` | `KnowledgeBase` | `XyzLoadCard` | `GriddingParamCard`(与 `SpecialMessageTypes` / `ISpecialMessage` 实现一致) - **id**:可选,实例唯一 id ### 4.2 各类型载荷约定 | type | 主要字段 | 说明 | |------|----------|------| | Form | formId, title, status, fields, submitLabel | formId 用于恢复时从 FormRegistry 取 Definition。 | | ParameterSet | title, items(name, valueText, description, fieldType, options) | 与 ParameterSetMessage 可序列化字段一致。 | | Table | title, columnNames, rows/totalRowCount/maxPreviewRows | 与 TableDataMessage 一致。 | | ColumnMatch | title, requiredColumns, previewColumns, mappings | 与 ColumnMatchMessage 一致。 | | WorkflowStatus | title, steps | 可选。 | - 所有字段可 YAML 序列化;长文本可截断。序列化/反序列化由统一工具(如 `SpecialMessageSerializer`)完成,与存储读写解耦。 ### 4.3 与 ISpecialMessage 的对应 - **Form**:YAML 含 formId、title、status、fields(含 id/label/type/currentValue 等)、submitLabel;与 `FormRequestMessage`(Definition + FieldsWithValues)可互转;恢复时经 IFormRegistry 根据 formId 取 FormDefinition。 - **ParameterSet**:YAML 含 title、items(name、valueText、description、fieldType、options);与 `ParameterSetMessage` 的 LoadFromJson/LoadFromYaml 及序列化输出一致。 - **Table**:YAML 含 title、columnNames、rows(或 totalRowCount/maxPreviewRows + 行数据);与 `TableDataMessage` 的 ColumnNames/Rows/TotalRowCount/MaxPreviewRows 一致。 - **ColumnMatch**:YAML 含 title、requiredColumns、previewColumns、mappings(requiredColumn/matchedColumn);与 `ColumnMatchMessage` 一致。 - **WorkflowStatus**:YAML 含 title、steps(id、displayName、order、status 等);与 `WorkflowStatusMessage` 一致;可选实现。 - 上述约定保证 `SpecialMessageSerializer.Serialize(ISpecialMessage)` 与 `SpecialMessageDeserializer.Deserialize(type, yaml, formRegistry?)` 往返无损(Form 在 formId 缺失时降级为占位)。 --- ## 5. 两种视图的构建 ### 5.1 UI 视图(从存储 → ChatMessages) - **入口**:如 `LoadMessagesFromSession(session)` 或 `LoadMessagesFromStore(store)`。 - **步骤**: 1. 清空当前 `ChatMessages`。 2. 遍历会话存储的每条条目: - **TextEntry**:按 Role 构造 `ChatMessageModel`(User/AI,MessageType.Text,Content = Content)。 - **SpecialEntry**:用 `SpecialMessageDeserializer.Deserialize(type, payloadYaml, formRegistry)` 得到 `ISpecialMessage`,构造 `ChatMessageModel`(AuthorType.AI,对应 MessageType,SpecialContent = 反序列化结果)。 3. 跳过 Role=System 的 TextEntry(或按产品决定是否展示);最后一条消息标记等 UI 状态。 - **数据来源**:仅读会话存储,不读 `ChatSession.History`。 ### 5.2 Prompt 视图(从存储 → ChatHistory,供 LLM 使用) - **入口**:如 `session.GetHistoryForLlm(guidePrompt?)` 或 `ConversationStore.BuildHistoryForLlm(guidePrompt?)`,返回 `ChatHistory`。 - **步骤**: 1. 创建空的 `ChatHistory`。 2. 若需要 System,在开头插入一条 `ChatMessageContent(AuthorRole.System, guidePrompt)`。 3. 遍历会话存储: - **TextEntry**(User/Assistant):原样追加 `ChatMessageContent(Role, Content)`。 - **TextEntry(System)**:若已在步骤 2 统一插入则跳过,或按需追加。 - **SpecialEntry**:**不**追加原始 YAML;可选(a)跳过,(b)追加一条短文本 Assistant 占位(如「[已展示表单:xxx]」)。 4. 返回该 `ChatHistory`。 - **使用处**:`KernelService.AskAsync` / `AskStreamAsync` 调用时,传入「从存储生成的 ChatHistory」,而不是 `session.History`;若仍保留 `session.History`,则不再将其作为发给 LLM 的输入。 --- ## 6. 写入与持久化 ### 6.1 写入存储(不入 History) - **用户发送文本**:向会话存储追加一条 `TextEntry(User, content)`;同时若需更新 UI,可基于存储重新构建 UI 视图或仅追加一条到当前 `ChatMessages`(与「仅从存储构建」策略二选一或混合)。 - **助手回复文本**:向会话存储追加一条 `TextEntry(Assistant, content)`。 - **出现特殊消息(表单/参数集/表格/列匹配等)**:将 `ISpecialMessage` 序列化为 YAML,向会话存储追加一条 `SpecialEntry(type, yamlPayload)`。 - **不再**向 `ChatSession.History` 追加「带前缀的 Assistant」;History 仅由 Prompt 视图在需要时生成。 ### 6.2 持久化格式 - **实现**:`SessionStorage` 按会话保存到 `{Sessions}/{sessionId}.yaml`;内容为 `SessionFileDto`(sessionId, title, createdAt, updatedAt, workflowMode, **entries**)。entries 来自 `session.Store`,经 `ConversationEntryMapper.ToDtoList` 得到 `ConversationEntryDto` 列表;加载时 `store.LoadFromEntries(dto.Entries)` 还原 Store,再构造 `ChatSession(..., store)`。 - **条目格式**:每项为 `{ kind: "Text", role, content }` 或 `{ kind: "Special", type, payload }`,payload 为 YAML 字符串。 - **格式**:整体 YAML;与特殊消息载荷一致。 --- ## 7. 模块职责 | 模块 | 职责 | |------|------| | **ConversationStore** | 持有 `IReadOnlyList`(内部 `List`);提供 AppendText / AppendSpecial / RemoveLast / Clear;提供 ToYaml()、LoadFromYaml()、LoadFromEntries();触发 StoreChanged。**不**提供 BuildHistoryForLlm(由 ChatSession 提供)。 | | **ChatSession** | 持有 ConversationStore;提供 **GetHistoryForLlm(guidePrompt)** 从 Store 生成 ChatHistory;保留 History 仅作兼容;AddUserMessage/AddAssistantMessage/AppendSpecialEntry 写入 Store(并可选同步 History)。 | | **SpecialMessageSerializer** | `ISpecialMessage` ↔ YAML 字符串(仅载荷);不关心存储格式。 | | **SpecialMessageDeserializer** | 根据 type + YAML 还原 `ISpecialMessage`;Form 需 IFormRegistry;提供 GetShortDescriptionForLlm 供 Prompt 视图占位。 | | **SessionStorage** | 会话级持久化:Save(session) 将 session.Store 写入 {sessionId}.yaml;Load/LoadFromYaml 反序列化为 ConversationStore 后构造 ChatSession。 | | **MainWindowViewModel** | 添加消息时写入**会话存储**(经 ChatSession);加载会话时 **LoadMessagesFromSession(session)** 从 session.Store 遍历构建 ChatMessages,不依赖 History。 | | **KernelService** | AskAsync/AskStreamAsync 使用 **session.GetHistoryForLlm(guidePrompt)**,不使用 session.History 作为请求体。 | --- ## 8. 与现有代码的衔接 | 现有 | 变更方向 | |------|----------| | ChatSession.History | 不再作为唯一事实来源;改为由「会话存储」在调用 LLM 时生成 ChatHistory。可保留属性用于过渡或只读兼容。 | | EnsureFormsYamlInSystem | 仅作兼容(可能只更新 History);发给 LLM 的 System 由 **GetHistoryForLlm(guidePrompt)** 在生成 ChatHistory 时插入,调用方传入 guidePrompt 即可;不再写入 formsYaml、不再维护摘要。 | | AddUserMessage / AddAssistantMessage | 语义改为「追加到会话存储」的 TextEntry;若仍需同步到旧 History 可保留一份兼容写入,但真相源为存储。 | | AddFormMessage / AddParameterSetMessage / SubmitForm 中添加 Table | 在向 UI 添加卡片的同时,向**会话存储**追加 SpecialEntry,不向 History 追加带前缀的 Content。 | | LoadMessagesFromSession | 改为从**会话存储**遍历构建 ChatMessages;不再解析 History 中的 Content 前缀。 | --- ## 9. 错误与边界 - **反序列化失败**:SpecialEntry 的 payload 无效或 type 未知时,该条在 UI 视图中可显示为占位文本或跳过,并打日志。 - **Form 的 Definition 缺失**:恢复时 formId 在 FormRegistry 中不存在则降级为占位或只读文本。 - **顺序**:严格按「写入顺序 = 存储顺序」;单线程 UI 下追加即保证顺序。 --- ## 10. 测试建议 - 单元:存储的序列化/反序列化(ConversationStore.ToYaml/LoadFromEntries、ConversationEntryMapper);SpecialMessage 的 YAML 往返;**GetHistoryForLlm** 仅含文本或短占位、不含原始 YAML。 - 集成:写入若干 TextEntry + SpecialEntry 后,UI 视图与 Prompt 视图结果符合预期;切换会话后从持久化恢复存储再构建 UI,卡片与顺序正确。 - 回归:LLM 请求体中无大段 YAML;会话列表与进入会话后行为一致。 ---