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>
553 lines
18 KiB
Markdown
553 lines
18 KiB
Markdown
# 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
|