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>
This commit is contained in:
552
docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md
Normal file
552
docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# 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<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 索引设计
|
||||
|
||||
```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<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 预算控制
|
||||
|
||||
```typescript
|
||||
// 限制记忆上下文的 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
|
||||
Reference in New Issue
Block a user