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

553 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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