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>
18 KiB
18 KiB
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