Files
HaHafeng 0c590854b5 docs(iit): Add IIT Manager Agent V2.9 development plan with multi-agent architecture
Features:
- Add V2.9 enhancements: Cron Skill, User Profiling, Feedback Loop, Multi-Intent Handling
- Create modular development plan documents (database, engines, services, memory, tasks)
- Add V2.5/V2.6/V2.8/V2.9 design documents for architecture evolution
- Add system design white papers and implementation guides

Architecture:
- Dual-Brain Architecture (SOP + ReAct engines)
- Three-layer memory system (Flow Log, Hot Memory, History Book)
- ProfilerService for personalized responses
- SchedulerService with Cron Skill support

Also includes:
- Frontend nginx config updates
- Backend test scripts for WeChat signature
- Database backup files

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 22:33:26 +08:00

18 KiB
Raw Permalink Blame History

IIT Manager Agent 记忆系统实现指南 (V2.8)

版本: V2.8
更新日期: 2026-02-05
关联文档: IIT Manager Agent V2.6 综合开发计划


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)

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)

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)

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

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<void> {
    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<ConversationResult[]> {
    const queryEmbedding = await this.embeddings.embed(query);
    
    const results = await prisma.$queryRaw<ConversationResult[]>`
      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<HotMemory> {
    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<void> {
    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<void> {
    // 从最近的对话中提取关键信息更新热记忆
    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<void> {
    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<WeeklyReport[]> {
    const queryEmbedding = await this.embeddings.embed(query);
    
    return prisma.$queryRaw<WeeklyReport[]>`
      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<void> {
    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<string> {
    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<void> {
    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 索引设计

-- 向量索引(用于相似度搜索)
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 缓存策略

// 热记忆缓存(使用 Redis
class MemoryCache {
  private redis: Redis;
  
  async getHotMemory(projectId: string): Promise<HotMemory | null> {
    const cached = await this.redis.get(`hot_memory:${projectId}`);
    return cached ? JSON.parse(cached) : null;
  }
  
  async setHotMemory(projectId: string, memory: HotMemory): Promise<void> {
    await this.redis.set(
      `hot_memory:${projectId}`,
      JSON.stringify(memory),
      'EX', 300 // 5分钟缓存
    );
  }
}

5.3 Token 预算控制

// 限制记忆上下文的 token 数量
async getContextForPrompt(projectId: string, intent: IntentResult): Promise<string> {
  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