# IIT Manager Agent 记忆系统实现指南 (V2.8) > **版本:** V2.8 > **更新日期:** 2026-02-05 > **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md) --- ## 1. 记忆架构概览 ### 1.1 设计原则 临床研究项目周期长达 1-3 年,需要"项目级长期记忆"来跨会话保持上下文。V2.8 架构采用**三层记忆体系**: | 层级 | 名称 | 存储位置 | 生命周期 | 检索方式 | |------|------|----------|----------|----------| | L1 | 流水账 | conversation_history | 30天 | 按需向量检索 | | L2 | 热记忆 | project_memory | 持久 | 每次注入 | | L3 | 历史书 | weekly_reports | 持久 | 按意图检索 | > **注意**:用户偏好和患者关键信息统一存储在 `project_memory` 的 Markdown 中,无需单独表。 ### 1.2 架构图 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ MemoryService │ │ │ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │ │ │ 流水账(L1) │ │ 热记忆(L2) │ │ 历史书(L3) │ │ │ │ conversation_ │ │ project_memory │ │ weekly_reports │ │ │ │ history │ │ │ │ │ │ │ │ │ │ - 项目元信息 │ │ - 周报卷叠 │ │ │ │ 30天自动过期 │ │ - 当前状态 │ │ - 关键决策归档 │ │ │ │ 向量化存储 │ │ - 关键决策 │ │ │ │ │ └────────────────┘ └────────────────┘ └────────────────────────┘ │ │ │ │ │ │ │ └───────────────────┴───────────────────────┘ │ │ │ │ │ ┌───────▼───────┐ │ │ │ getContext │ │ │ │ ForPrompt() │ │ │ └───────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 2. 数据模型 ### 2.1 流水账 (conversation_history) ```prisma model iit_conversation_history { id String @id @default(cuid()) project_id String user_id String role String // 'user' | 'assistant' content String intent String? // 意图标记 embedding Unsupported("vector(1536)")? created_at DateTime @default(now()) expires_at DateTime // 30天后过期 @@index([project_id, user_id, created_at]) @@index([project_id, expires_at]) } ``` ### 2.2 热记忆 (project_memory) ```prisma model iit_project_memory { id String @id @default(cuid()) project_id String type String // 'meta' | 'status' | 'decision' | 'preference' key String value Json priority Int @default(0) // 优先级,高的先注入 created_at DateTime @default(now()) updated_at DateTime @updatedAt @@unique([project_id, type, key]) @@index([project_id, type]) } ``` ### 2.3 历史书 - 周报 (weekly_reports) ```prisma model iit_weekly_reports { id String @id @default(cuid()) project_id String week_start DateTime week_end DateTime summary String // 周报摘要(Markdown) key_events Json // 关键事件 JSON metrics Json // 统计指标 embedding Unsupported("vector(1536)")? created_at DateTime @default(now()) @@unique([project_id, week_start]) @@index([project_id, week_start]) } ``` --- ## 3. MemoryService 完整实现 > **文件路径**: `backend/src/modules/iit-manager/services/MemoryService.ts` ```typescript import { prisma } from '../../common/prisma'; import { OpenAIEmbeddings } from '../../common/llm/embeddings'; import { LLMFactory } from '../../common/llm/adapters/LLMFactory'; export class MemoryService { private embeddings: OpenAIEmbeddings; private llm = LLMFactory.create('qwen'); constructor() { this.embeddings = new OpenAIEmbeddings(); } // ===== 1. 流水账操作 ===== async saveConversation(data: { projectId: string; userId: string; role: 'user' | 'assistant'; content: string; intent?: string; }): Promise { const embedding = await this.embeddings.embed(data.content); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 30); await prisma.iit_conversation_history.create({ data: { project_id: data.projectId, user_id: data.userId, role: data.role, content: data.content, intent: data.intent, embedding: embedding, expires_at: expiresAt } }); } async searchConversations( projectId: string, query: string, limit: number = 5 ): Promise { const queryEmbedding = await this.embeddings.embed(query); const results = await prisma.$queryRaw` SELECT id, content, role, intent, created_at, 1 - (embedding <=> ${queryEmbedding}::vector) as similarity FROM iit_conversation_history WHERE project_id = ${projectId} AND expires_at > NOW() ORDER BY embedding <=> ${queryEmbedding}::vector LIMIT ${limit} `; return results; } // ===== 2. 热记忆操作 ===== async getHotMemory(projectId: string): Promise { const memories = await prisma.iit_project_memory.findMany({ where: { project_id: projectId }, orderBy: { priority: 'desc' } }); return { meta: memories.filter(m => m.type === 'meta'), status: memories.filter(m => m.type === 'status'), decisions: memories.filter(m => m.type === 'decision'), preferences: memories.filter(m => m.type === 'preference') }; } async updateHotMemory( projectId: string, type: string, key: string, value: any, priority: number = 0 ): Promise { await prisma.iit_project_memory.upsert({ where: { project_id_type_key: { project_id: projectId, type, key } }, update: { value, priority }, create: { project_id: projectId, type, key, value, priority } }); } async refreshHotMemory(projectId: string): Promise { // 从最近的对话中提取关键信息更新热记忆 const recentConversations = await prisma.iit_conversation_history.findMany({ where: { project_id: projectId }, orderBy: { created_at: 'desc' }, take: 50 }); if (recentConversations.length === 0) return; // 使用 LLM 提取关键信息 const prompt = `分析以下对话,提取需要长期记住的关键信息: ${recentConversations.map(c => `${c.role}: ${c.content}`).join('\n')} 请提取以下类别的信息(JSON格式): 1. status: 项目当前状态变化 2. decisions: 重要决策 3. preferences: 用户偏好 返回格式: { "status": [{"key": "...", "value": "...", "priority": 0-10}], "decisions": [...], "preferences": [...] }`; const response = await this.llm.chat([{ role: 'user', content: prompt }]); try { const extracted = JSON.parse(response.content); for (const [type, items] of Object.entries(extracted)) { for (const item of items as any[]) { await this.updateHotMemory(projectId, type, item.key, item.value, item.priority); } } } catch (e) { console.error('[MemoryService] 热记忆提取失败:', e); } } // ===== 3. 历史书操作 ===== async saveWeeklyReport(projectId: string, report: string): Promise { const weekStart = this.getWeekStart(); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); const embedding = await this.embeddings.embed(report); await prisma.iit_weekly_reports.upsert({ where: { project_id_week_start: { project_id: projectId, week_start: weekStart } }, update: { summary: report, embedding }, create: { project_id: projectId, week_start: weekStart, week_end: weekEnd, summary: report, key_events: {}, metrics: {}, embedding } }); } async searchWeeklyReports( projectId: string, query: string, limit: number = 3 ): Promise { const queryEmbedding = await this.embeddings.embed(query); return prisma.$queryRaw` SELECT id, week_start, week_end, summary, 1 - (embedding <=> ${queryEmbedding}::vector) as similarity FROM iit_weekly_reports WHERE project_id = ${projectId} ORDER BY embedding <=> ${queryEmbedding}::vector LIMIT ${limit} `; } async rollupWeeklyMemory(projectId: string): Promise { const weekStart = this.getWeekStart(); const weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); // 获取本周对话 const conversations = await prisma.iit_conversation_history.findMany({ where: { project_id: projectId, created_at: { gte: weekStart, lte: weekEnd } }, orderBy: { created_at: 'asc' } }); if (conversations.length === 0) return; // 使用 LLM 生成周报摘要 const prompt = `你是一个临床研究项目的记录员。请根据本周的对话记录,生成一份周报摘要。 对话记录: ${conversations.map(c => `[${c.created_at.toISOString()}] ${c.role}: ${c.content}`).join('\n')} 要求: 1. 提取关键事件和决策 2. 统计主要指标 3. 记录重要问题和解决方案 4. 简洁明了,不超过500字`; const response = await this.llm.chat([{ role: 'user', content: prompt }]); await this.saveWeeklyReport(projectId, response.content); } // ===== 4. 意图驱动的上下文组装 ===== async getContextForPrompt( projectId: string, intent: IntentResult ): Promise { const parts: string[] = []; // ⚠️ L2 热记忆:始终注入 const hotMemory = await this.getHotMemory(projectId); parts.push(this.formatHotMemory(hotMemory)); // ⚠️ L3 历史书:按意图检索 if (intent.type === 'QA_QUERY') { // 模糊查询需要历史上下文 const relatedReports = await this.searchWeeklyReports(projectId, intent.entities?.query || '', 2); if (relatedReports.length > 0) { parts.push('## 相关历史记录\n' + relatedReports.map(r => r.summary).join('\n---\n')); } } // ⚠️ L1 流水账:仅按需检索(当历史书不足时) if (parts.length < 3 && intent.type === 'QA_QUERY') { const relatedConversations = await this.searchConversations(projectId, intent.entities?.query || '', 3); if (relatedConversations.length > 0) { parts.push('## 相关对话记录\n' + relatedConversations.map(c => `${c.role}: ${c.content}`).join('\n')); } } return parts.join('\n\n'); } private formatHotMemory(hotMemory: HotMemory): string { const sections: string[] = []; if (hotMemory.meta.length > 0) { sections.push('## 项目信息\n' + hotMemory.meta.map(m => `- ${m.key}: ${JSON.stringify(m.value)}`).join('\n')); } if (hotMemory.status.length > 0) { sections.push('## 当前状态\n' + hotMemory.status.map(s => `- ${s.key}: ${JSON.stringify(s.value)}`).join('\n')); } if (hotMemory.decisions.length > 0) { sections.push('## 关键决策\n' + hotMemory.decisions.map(d => `- ${d.key}: ${JSON.stringify(d.value)}`).join('\n')); } return sections.join('\n\n'); } private getWeekStart(): Date { const now = new Date(); const weekStart = new Date(now); weekStart.setDate(now.getDate() - now.getDay() + 1); weekStart.setHours(0, 0, 0, 0); return weekStart; } // ===== 5. 清理过期数据 ===== async cleanupExpiredData(): Promise { await prisma.iit_conversation_history.deleteMany({ where: { expires_at: { lt: new Date() } } }); } } // ===== 类型定义 ===== interface HotMemory { meta: Array<{ key: string; value: any }>; status: Array<{ key: string; value: any }>; decisions: Array<{ key: string; value: any }>; preferences: Array<{ key: string; value: any }>; } interface ConversationResult { id: string; content: string; role: string; intent: string | null; created_at: Date; similarity: number; } interface WeeklyReport { id: string; week_start: Date; week_end: Date; summary: string; similarity: number; } interface IntentResult { type: string; entities?: { record_id?: string; query?: string; }; } ``` --- ## 4. 记忆信息映射 ### 4.1 信息类型与存储位置 | 信息类型 | 存储位置 | 更新机制 | |----------|----------|----------| | 项目名称、PI、入组目标 | project_memory (meta) | 初始化时写入 | | 当前入组人数、进度百分比 | project_memory (status) | 每日定时更新 | | 用户做出的关键决策 | project_memory (decision) | 对话中实时提取 | | 用户偏好设置 | project_memory (preference) | 用户明确表达时 | | 每次对话原文 | conversation_history | 对话实时写入 | | 每周总结 | weekly_reports | 周一凌晨卷叠 | | 患者特殊情况 | project_memory (当前状态) | 在热记忆中维护 | ### 4.2 检索策略 ``` ┌─────────────────────────────────────────────────────────────┐ │ 意图识别结果 │ └───────────────────────────┬─────────────────────────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ▼ ▼ ▼ QC_TASK QA_QUERY PROTOCOL_QA │ │ │ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 热记忆 + SOP │ │热记忆 + 历史书│ │ 热记忆 + RAG │ │ 状态同步 │ │ + 流水账检索 │ │ 知识库检索 │ └──────────────┘ └──────────────┘ └──────────────┘ ``` --- ## 5. 性能优化建议 ### 5.1 索引设计 ```sql -- 向量索引(用于相似度搜索) CREATE INDEX idx_conversation_embedding ON iit_conversation_history USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); CREATE INDEX idx_weekly_reports_embedding ON iit_weekly_reports USING ivfflat (embedding vector_cosine_ops) WITH (lists = 50); -- 复合索引(用于过滤) CREATE INDEX idx_conversation_project_user_time ON iit_conversation_history (project_id, user_id, created_at DESC); ``` ### 5.2 缓存策略 ```typescript // 热记忆缓存(使用 Redis) class MemoryCache { private redis: Redis; async getHotMemory(projectId: string): Promise { const cached = await this.redis.get(`hot_memory:${projectId}`); return cached ? JSON.parse(cached) : null; } async setHotMemory(projectId: string, memory: HotMemory): Promise { await this.redis.set( `hot_memory:${projectId}`, JSON.stringify(memory), 'EX', 300 // 5分钟缓存 ); } } ``` ### 5.3 Token 预算控制 ```typescript // 限制记忆上下文的 token 数量 async getContextForPrompt(projectId: string, intent: IntentResult): Promise { const MAX_TOKENS = 2000; let context = ''; let tokenCount = 0; // 优先注入热记忆 const hotMemory = await this.getHotMemory(projectId); const hotMemoryStr = this.formatHotMemory(hotMemory); tokenCount += this.estimateTokens(hotMemoryStr); context += hotMemoryStr; // 按优先级继续添加 if (tokenCount < MAX_TOKENS) { // 添加历史书内容... } return context; } ``` --- ## 6. 验收标准 | 功能 | 验收标准 | |------|----------| | 流水账存储 | 对话消息 100ms 内写入成功 | | 向量检索 | 相似度搜索 500ms 内返回 | | 热记忆注入 | 每次请求正确注入项目上下文 | | 周报卷叠 | 周一凌晨自动生成周报 | | 过期清理 | 30天前的对话自动删除 | --- **文档维护人**:AI Agent **最后更新**:2026-02-05