conversation_history_aggregate_models 模块技术解析
1. 模块概述
conversation_history_aggregate_models 模块定义了系统中用于表示和处理对话历史的核心数据模型。该模块位于系统的领域层,为整个对话管理系统提供了数据结构基础。
1.1 问题空间与设计目标
在构建一个智能对话系统时,我们需要解决以下关键问题:
- 上下文保持:如何有效存储和传递对话上下文,使系统能够理解用户的后续问题
- 知识关联:如何记录系统在生成回答时引用的知识库内容,增强回答的可追溯性
- 多角色消息:如何区分和处理不同角色(用户、助手、系统)的消息
- 数据持久化:如何高效地将复杂的对话数据存储到数据库中,同时保持查询性能
1.2 核心设计洞察
该模块采用了聚合模型的设计思想,将对话历史的各个相关组件组织在一起,形成一个内聚的领域模型。主要设计原则包括:
- 职责分离:不同数据结构负责不同的功能(如
History用于历史记录,Message用于详细消息) - 数据库友好:自定义类型实现了标准的数据库序列化接口
- 可扩展性:使用 JSON 类型存储复杂结构,便于未来扩展
- 数据完整性:通过 GORM 钩子确保数据创建时的一致性
2. 核心组件深度解析
2.1 History 结构体
type History struct {
Query string // 用户查询文本
Answer string // 系统响应文本
CreateAt time.Time // 历史记录创建时间
KnowledgeReferences References // 回答中使用的知识引用
}
设计意图:History 结构体代表一个简化的对话历史条目,主要用于快速展示和上下文传递。它包含用户的查询和系统的回答,以及相关的知识引用。
使用场景:
- 在会话摘要中显示历史对话
- 为 LLM 提供上下文信息
- 快速检索历史问答对
2.2 Message 结构体
type Message struct {
ID string // 消息唯一标识符
SessionID string // 所属会话ID
RequestID string // API请求标识符
Content string // 消息文本内容
Role string // 消息角色:"user", "assistant", "system"
KnowledgeReferences References // 响应中使用的知识块引用
AgentSteps AgentSteps // 代理执行步骤(仅用于助手消息)
MentionedItems MentionedItems // 用户消息中提到的知识库和文件
IsCompleted bool // 消息生成是否完成
CreatedAt time.Time // 消息创建时间戳
UpdatedAt time.Time // 最后更新时间戳
DeletedAt gorm.DeletedAt // 软删除时间戳
}
设计意图:Message 是该模块中最核心的数据结构,代表一个完整的对话消息。它包含了消息的所有元数据、内容以及相关的附加信息。
关键设计决策:
-
角色区分:通过
Role字段明确区分不同来源的消息,这对于构建上下文和生成响应至关重要。 -
知识引用分离:
KnowledgeReferences字段专门用于存储系统在生成回答时引用的知识块,增强了回答的可解释性。 -
代理步骤存储:
AgentSteps字段存储了代理的详细推理过程和工具调用,但设计注释明确指出这些信息"存储用于用户历史显示,但不包含在 LLM 上下文中以避免冗余"。这是一个重要的性能优化决策。 -
提及项记录:
MentionedItems字段专门用于记录用户在消息中 @ 提及的知识库或文件,支持更精确的知识检索。 -
软删除支持:通过
gorm.DeletedAt实现软删除,保留数据的同时不影响正常查询。
GORM 钩子:
func (m *Message) BeforeCreate(tx *gorm.DB) (err error) {
m.ID = uuid.New().String()
if m.KnowledgeReferences == nil {
m.KnowledgeReferences = make(References, 0)
}
if m.AgentSteps == nil {
m.AgentSteps = make(AgentSteps, 0)
}
if m.MentionedItems == nil {
m.MentionedItems = make(MentionedItems, 0)
}
return nil
}
这个钩子确保了在创建新消息记录时:
- 自动生成 UUID 作为消息 ID
- 初始化所有可能为 nil 的切片字段,避免后续操作中的空指针异常
2.3 MentionedItem 和 MentionedItems
type MentionedItem struct {
ID string // 项目ID
Name string // 项目名称
Type string // 类型:"kb" 表示知识库,"file" 表示文件
KBType string // 知识库类型:"document" 或 "faq"(仅适用于 kb 类型)
}
type MentionedItems []MentionedItem
设计意图:这组类型用于表示用户在消息中提及的知识库或文件。MentionedItems 是一个自定义切片类型,实现了数据库序列化接口。
数据库序列化实现:
func (m MentionedItems) Value() (driver.Value, error) {
if m == nil {
return json.Marshal([]MentionedItem{})
}
return json.Marshal(m)
}
func (m *MentionedItems) Scan(value interface{}) error {
if value == nil {
*m = make(MentionedItems, 0)
return nil
}
b, ok := value.([]byte)
if !ok {
*m = make(MentionedItems, 0)
return nil
}
return json.Unmarshal(b, m)
}
这些方法实现了 driver.Valuer 和 sql.Scanner 接口,使得 MentionedItems 类型可以直接与数据库交互,自动进行 JSON 序列化和反序列化。
2.4 AgentSteps
type AgentSteps []AgentStep
设计意图:AgentSteps 是一个代理执行步骤的集合,用于存储代理的推理过程和工具调用。与 MentionedItems 类似,它也实现了数据库序列化接口。
func (a AgentSteps) Value() (driver.Value, error) {
if a == nil {
return json.Marshal([]AgentStep{})
}
return json.Marshal(a)
}
func (a *AgentSteps) Scan(value interface{}) error {
if value == nil {
*a = make(AgentSteps, 0)
return nil
}
b, ok := value.([]byte)
if !ok {
*a = make(AgentSteps, 0)
return nil
}
return json.Unmarshal(b, a)
}
3. 架构与数据流程
3.1 模块在系统中的位置
conversation_history_aggregate_models 模块位于系统的核心领域层,为多个上层模块提供数据模型支持:
┌─────────────────────────────────────────────────────────────────┐
│ application_services_and_orchestration │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ conversation_context_and_memory_services │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ message_history_service │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│ uses
│
┌─────────────────────────────────────────────────────────────────┐
│ core_domain_types_and_interfaces │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ agent_conversation_and_runtime_contracts │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ message_history_and_mentions_contracts │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ conversation_history_aggregate_models │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲
│ uses
│
┌─────────────────────────────────────────────────────────────────┐
│ data_access_repositories │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ content_and_knowledge_management_repositories │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ conversation_history_repositories │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
3.2 典型数据流程
以下是一个典型的对话消息处理流程:
-
消息创建:当用户发送消息时,系统创建一个
Message对象,设置Role为 "user",并记录用户提及的项目到MentionedItems。 -
消息持久化:通过
conversation_history_repositories模块将Message对象保存到数据库。 -
上下文加载:在生成响应前,
message_history_service从数据库加载相关的历史消息,构建对话上下文。 -
响应生成:系统生成响应,创建另一个
Message对象,设置Role为 "assistant",记录使用的知识引用到KnowledgeReferences,以及代理执行步骤到AgentSteps。 -
响应持久化:将助手的响应消息也保存到数据库。
-
历史展示:当用户查看对话历史时,系统可能会将
Message对象转换为更简洁的History对象进行展示。
4. 设计决策与权衡
4.1 结构化数据 vs JSON 存储
决策:对于复杂的嵌套结构(如 KnowledgeReferences、AgentSteps、MentionedItems),采用 JSON 类型存储在数据库中。
权衡分析:
- 优点:
- 灵活性:可以轻松修改这些结构的定义,无需数据库迁移
- 简化查询:在大多数情况下,我们需要完整的对象,而不是单独的字段
- 减少表连接:避免了为每个嵌套结构创建单独的表
- 缺点:
- 难以进行复杂的嵌套查询
- 没有数据库级别的类型检查
- 可能占用更多存储空间
适用场景:这种设计适用于这些嵌套结构主要作为一个整体进行读写,不需要经常对其内部字段进行单独查询的情况。
4.2 History vs Message 分离
决策:提供两种不同的数据结构,History 用于简化的历史记录,Message 用于完整的消息表示。
权衡分析:
- 优点:
- 关注点分离:
History专注于问答对的基本信息,而Message包含所有元数据 - 性能优化:在只需要基本历史信息的场景下,使用
History可以减少数据传输量 - 灵活性:可以根据不同的使用场景选择合适的数据结构
- 关注点分离:
- 缺点:
- 数据重复:两种结构可能包含相同的信息
- 转换开销:在两种结构之间转换时需要额外的处理
- 一致性维护:需要确保两种结构中的相关信息保持一致
4.3 AgentSteps 的存储策略
决策:存储 AgentSteps 但在 LLM 上下文中不包含它们。
权衡分析:
- 优点:
- 用户体验:用户可以查看系统的推理过程,增加透明度
- 调试支持:开发人员可以通过查看这些步骤调试问题
- 上下文效率:避免在每次请求时向 LLM 发送大量冗余信息
- 缺点:
- 存储开销:需要额外的存储空间来保存这些步骤
- 数据同步:需要确保这些步骤与实际的推理过程保持同步
5. 使用指南与注意事项
5.1 基本使用示例
创建用户消息
userMessage := &types.Message{
SessionID: sessionID,
RequestID: requestID,
Content: "你好,我想了解关于产品的信息",
Role: "user",
MentionedItems: types.MentionedItems{
{
ID: "kb-123",
Name: "产品知识库",
Type: "kb",
KBType: "document",
},
},
IsCompleted: true,
}
// 通过 GORM 保存到数据库
db.Create(userMessage)
创建助手消息
assistantMessage := &types.Message{
SessionID: sessionID,
RequestID: requestID,
Content: "根据产品知识库,我们的产品具有以下特点...",
Role: "assistant",
KnowledgeReferences: knowledgeRefs, // 假设这是之前准备好的 References
AgentSteps: agentSteps, // 假设这是之前记录的 AgentSteps
IsCompleted: true,
}
// 通过 GORM 保存到数据库
db.Create(assistantMessage)
5.2 注意事项与陷阱
-
不要手动设置 ID:
Message的 ID 会在BeforeCreate钩子中自动生成,手动设置可能会被覆盖。 -
初始化切片字段:虽然
BeforeCreate钩子会初始化 nil 切片,但在其他场景下创建Message对象时,最好手动初始化这些切片,以避免空指针异常。 -
正确处理角色:确保正确设置
Role字段,系统的很多功能依赖于这个字段来区分消息来源。 -
注意 JSON 字段的查询限制:由于
KnowledgeReferences、AgentSteps和MentionedItems存储为 JSON,对它们内部字段的查询会比较困难,需要使用数据库特定的 JSON 查询语法。 -
AgentSteps 的大小管理:
AgentSteps可能会变得很大,特别是在复杂的推理场景下。需要考虑实现某种机制来限制其大小,或者在不需要时不存储这些信息。
6. 模块关系与依赖
该模块与以下模块有紧密的依赖关系:
-
session_lifecycle_api:使用该模块的数据结构管理会话的生命周期。
-
message_history_service:核心服务,使用这些数据结构提供消息历史管理功能。
-
conversation_history_repositories:负责持久化该模块定义的数据结构。
7. 总结
conversation_history_aggregate_models 模块是系统中对话管理功能的核心数据模型层。它通过精心设计的数据结构,解决了对话上下文保持、知识关联、多角色消息处理等关键问题。
该模块的主要价值在于:
- 提供了清晰、内聚的领域模型
- 平衡了灵活性和性能的需求
- 为上层服务提供了可靠的数据基础
通过理解该模块的设计思想和实现细节,开发者可以更好地使用和扩展系统的对话管理功能。