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.
结构化会话存储 - 设计文档
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<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 的 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)。
- 步骤:
- 清空当前
ChatMessages。
- 遍历会话存储的每条条目:
- TextEntry:按 Role 构造
ChatMessageModel(User/AI,MessageType.Text,Content = Content)。
- SpecialEntry:用
SpecialMessageDeserializer.Deserialize(type, payloadYaml, formRegistry) 得到 ISpecialMessage,构造 ChatMessageModel(AuthorType.AI,对应 MessageType,SpecialContent = 反序列化结果)。
- 跳过 Role=System 的 TextEntry(或按产品决定是否展示);最后一条消息标记等 UI 状态。
- 数据来源:仅读会话存储,不读
ChatSession.History。
5.2 Prompt 视图(从存储 → ChatHistory,供 LLM 使用)
- 入口:如
session.GetHistoryForLlm(guidePrompt?) 或 ConversationStore.BuildHistoryForLlm(guidePrompt?),返回 ChatHistory。
- 步骤:
- 创建空的
ChatHistory。
- 若需要 System,在开头插入一条
ChatMessageContent(AuthorRole.System, guidePrompt)。
- 遍历会话存储:
- TextEntry(User/Assistant):原样追加
ChatMessageContent(Role, Content)。
- TextEntry(System):若已在步骤 2 统一插入则跳过,或按需追加。
- SpecialEntry:不追加原始 YAML;可选(a)跳过,(b)追加一条短文本 Assistant 占位(如「[已展示表单:xxx]」)。
- 返回该
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}.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;会话列表与进入会话后行为一致。