You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kev/Drawer/AI/docs/observation-summary-design.md

194 lines
14 KiB
Markdown

1 month ago
# 结构化会话存储 - 设计文档
## 1. 文档信息
| 项目 | 说明 |
|------|------|
| 功能名称 | 结构化会话存储Structured Conversation Store |
| 版本 | 3.0 |
| 对应需求 | docs/observation-summary-requirements.mdv3.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** | 纯文本消息 | RoleUser/Assistant/System, Contentstring |
| **SpecialEntry** | 特殊消息(卡片) | Type见下, PayloadYAML 字符串) |
- **实现类型**`ConversationEntry` 为抽象基类;具体为 `TextConversationEntry`、`SpecialConversationEntry`。持久化 DTO 使用 `Kind`"Text" / "Special")、`ConversationEntryMapper` 与 `ConversationEntryDto` 互转。
- 顺序由列表下标保证;新增条目一律追加(`AppendText` / `AppendSpecial`)。
### 3.2 存储的归属
- 存储与**会话**一一对应:每个 `ChatSession`(或等价会话对象)持有一份「会话存储」实例(`ConversationStore` 类型,其内部持有 `IList<ConversationEntry>`)。即**存储由 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 的 PayloadYAML至少包含
- **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, itemsname, 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、itemsname、valueText、description、fieldType、options`ParameterSetMessage` 的 LoadFromJson/LoadFromYaml 及序列化输出一致。
- **Table**YAML 含 title、columnNames、rows或 totalRowCount/maxPreviewRows + 行数据);与 `TableDataMessage` 的 ColumnNames/Rows/TotalRowCount/MaxPreviewRows 一致。
- **ColumnMatch**YAML 含 title、requiredColumns、previewColumns、mappingsrequiredColumn/matchedColumn`ColumnMatchMessage` 一致。
- **WorkflowStatus**YAML 含 title、stepsid、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/AIMessageType.TextContent = Content
- **SpecialEntry**:用 `SpecialMessageDeserializer.Deserialize(type, payloadYaml, formRegistry)` 得到 `ISpecialMessage`,构造 `ChatMessageModel`AuthorType.AI对应 MessageTypeSpecialContent = 反序列化结果)。
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)`
- **TextEntrySystem**:若已在步骤 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<ConversationEntry>`(内部 `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}.yamlLoad/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、ConversationEntryMapperSpecialMessage 的 YAML 往返;**GetHistoryForLlm** 仅含文本或短占位、不含原始 YAML。
- 集成:写入若干 TextEntry + SpecialEntry 后UI 视图与 Prompt 视图结果符合预期;切换会话后从持久化恢复存储再构建 UI卡片与顺序正确。
- 回归LLM 请求体中无大段 YAML会话列表与进入会话后行为一致。
---