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>
36 KiB
36 KiB
IIT Manager Agent 服务层实现指南
版本: V2.9
更新日期: 2026-02-05
关联文档: IIT Manager Agent V2.6 综合开发计划V2.9 更新:
- 新增
ProfilerService用户画像服务ChatService增加反馈循环功能SchedulerService支持 Cron Skill 触发
1. 服务层总览
| 服务 | 职责 | Phase |
|---|---|---|
ToolsService |
统一工具管理(字段映射 + 执行) | 1 |
ChatService |
消息路由(双脑入口)+ 反馈收集 | 2 |
IntentService |
意图识别(混合路由) | 5 |
MemoryService |
记忆管理(V2.8 架构) | 2-3 |
SchedulerService |
定时任务调度 + Cron Skill | 4 |
ReportService |
报告生成 | 4 |
ProfilerService |
用户画像管理(V2.9) | 4 |
VisionService |
视觉识别(延后) | 6 |
┌─────────────────────────────────────────────────────────────────────┐
│ 路由层 (Router) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ ChatService │ │VisionService │ │ API Routes │ │Scheduler │ │
│ │ (文本路由) │ │ (图片识别) │ │ (REST API) │ │ (定时) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 工具层 (ToolsService) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │read_clinical_data│ │write_clinical_data│ │search_protocol │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2. ToolsService - 统一工具管理
文件路径:
backend/src/modules/iit-manager/services/ToolsService.ts
2.1 核心职责
- 定义可供 LLM 调用的工具
- 实现字段名映射(第一层防御)
- 实现空结果兜底(第三层防御)
- 区分只读/读写工具权限
2.2 工具定义
export const TOOL_DEFINITIONS = [
// ===== 只读工具(ReAct 可用)=====
{
name: "read_clinical_data",
description: "读取 REDCap 临床数据。可指定患者ID和字段列表",
parameters: {
type: "object",
properties: {
record_id: { type: "string", description: "患者ID,如 P001" },
fields: {
type: "array",
items: { type: "string" },
description: "要读取的字段名列表,如 ['age', 'gender', 'ecog']"
}
},
required: ["record_id", "fields"]
}
},
{
name: "search_protocol",
description: "检索研究方案文档。返回与问题相关的方案内容",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "搜索关键词或问题" }
},
required: ["query"]
}
},
{
name: "get_project_stats",
description: "获取项目统计数据,如入组人数、完成率等",
parameters: {
type: "object",
properties: {
project_id: { type: "string" },
stat_type: {
type: "string",
enum: ["enrollment", "completion", "ae_summary", "query_status"]
}
},
required: ["project_id", "stat_type"]
}
},
{
name: "check_visit_window",
description: "检查访视是否在方案允许的时间窗口内",
parameters: {
type: "object",
properties: {
record_id: { type: "string" },
visit_id: { type: "string" },
actual_date: { type: "string", format: "date" }
},
required: ["record_id", "visit_id", "actual_date"]
}
},
// ===== 读写工具(仅 SOP 可用)=====
{
name: "write_clinical_data",
description: "写入 REDCap 临床数据(需人工确认)",
parameters: {
type: "object",
properties: {
record_id: { type: "string" },
form_name: { type: "string" },
data: { type: "object" }
},
required: ["record_id", "form_name", "data"]
}
},
{
name: "manage_issue",
description: "创建或更新质疑(Query)",
parameters: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "close", "update"] },
record_id: { type: "string" },
field: { type: "string" },
message: { type: "string" }
},
required: ["action", "record_id", "field", "message"]
}
}
];
2.3 完整实现
import { prisma } from '../../common/prisma';
import { RedcapAdapter } from '../adapters/RedcapAdapter';
import { DifyClient } from '../../common/rag/DifyClient';
export class ToolsService {
private redcap: RedcapAdapter;
private dify: DifyClient;
constructor(redcap: RedcapAdapter, dify: DifyClient) {
this.redcap = redcap;
this.dify = dify;
}
/**
* 获取工具定义(供 LLM 使用)
*/
getToolDefinitions() {
return TOOL_DEFINITIONS;
}
/**
* 统一执行入口
*/
async executeTool(
toolName: string,
args: Record<string, any>,
context?: { projectId: string }
): Promise<any> {
const projectId = context?.projectId || args.project_id;
switch (toolName) {
case 'read_clinical_data':
return this.readClinicalData(projectId, args.record_id, args.fields);
case 'search_protocol':
return this.searchProtocol(projectId, args.query);
case 'get_project_stats':
return this.getProjectStats(args.project_id, args.stat_type);
case 'check_visit_window':
return this.checkVisitWindow(args);
case 'write_clinical_data':
return this.writeClinicalData(args);
case 'manage_issue':
return this.manageIssue(args);
default:
throw new Error(`未知工具: ${toolName}`);
}
}
// ===== 字段映射(第一层防御)=====
private async mapFields(projectId: string, fields: string[]): Promise<string[]> {
const mappings = await prisma.iitFieldMapping.findMany({
where: { projectId }
});
const mappingDict = Object.fromEntries(
mappings.map(m => [m.aliasName.toLowerCase(), m.actualName])
);
return fields.map(f => {
const mapped = mappingDict[f.toLowerCase()];
if (mapped) {
console.log(`[ToolsService] 字段映射: ${f} -> ${mapped}`);
}
return mapped || f;
});
}
// ===== 工具实现 =====
private async readClinicalData(
projectId: string,
recordId: string,
fields: string[]
): Promise<any> {
// 字段映射
const mappedFields = await this.mapFields(projectId, fields);
try {
const data = await this.redcap.getRecord(recordId, mappedFields);
// ⚠️ 空结果兜底(第三层防御)
if (!data || Object.keys(data).length === 0) {
return {
success: false,
message: `未找到患者 ${recordId} 的相关数据。请检查患者ID是否正确。`,
hint: '可用 get_project_stats 查看项目中的患者列表'
};
}
return { success: true, data };
} catch (error) {
return {
success: false,
message: `读取数据失败: ${error.message}`,
hint: '请检查字段名是否正确'
};
}
}
private async searchProtocol(projectId: string, query: string): Promise<any> {
const results = await this.dify.retrieve(projectId, query);
if (!results || results.length === 0) {
return {
success: false,
message: '未找到相关的方案内容',
hint: '可以尝试更具体的关键词'
};
}
return {
success: true,
results: results.slice(0, 3).map(r => ({
content: r.content,
source: r.metadata?.source,
relevance: r.score
}))
};
}
private async getProjectStats(projectId: string, statType: string): Promise<any> {
// 根据 statType 查询不同统计数据
switch (statType) {
case 'enrollment':
return this.getEnrollmentStats(projectId);
case 'completion':
return this.getCompletionStats(projectId);
case 'ae_summary':
return this.getAESummary(projectId);
case 'query_status':
return this.getQueryStatus(projectId);
default:
return { error: `未知的统计类型: ${statType}` };
}
}
private async checkVisitWindow(args: {
record_id: string;
visit_id: string;
actual_date: string;
}): Promise<any> {
const baseline = await this.getBaselineDate(args.record_id);
const window = await this.getVisitWindow(args.visit_id);
const actualDays = this.daysBetween(baseline, args.actual_date);
const inWindow = actualDays >= window.minDays && actualDays <= window.maxDays;
return {
inWindow,
expectedRange: `Day ${window.minDays} - Day ${window.maxDays}`,
actualDay: actualDays,
deviation: inWindow ? 0 : Math.min(
Math.abs(actualDays - window.minDays),
Math.abs(actualDays - window.maxDays)
)
};
}
// ... 其他工具实现省略
}
3. IntentService - 意图识别
文件路径:
backend/src/modules/iit-manager/services/IntentService.ts
3.1 核心职责
- 混合路由:正则快速通道 + LLM 后备
- 识别用户意图类型(QC_TASK / QA_QUERY / UNCLEAR)
- 提取实体信息(record_id, visit 等)
- 生成追问句(当信息不全时)
3.2 意图类型
| 意图类型 | 描述 | 路由目标 |
|---|---|---|
QC_TASK |
质控任务 | SopEngine |
QA_QUERY |
模糊查询 | ReActEngine |
PROTOCOL_QA |
方案问题 | DifyClient |
REPORT |
报表请求 | ReportService |
HELP |
帮助请求 | 静态回复 |
UNCLEAR |
信息不全 | 追问 |
3.3 完整实现
import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
export interface IntentResult {
type: 'QC_TASK' | 'QA_QUERY' | 'PROTOCOL_QA' | 'REPORT' | 'HELP' | 'UNCLEAR';
confidence: number;
entities: {
record_id?: string;
visit?: string;
date_range?: string;
};
source: 'fast_path' | 'llm' | 'fallback';
missing_info?: string;
clarification_question?: string;
}
export class IntentService {
private llm = LLMFactory.create('qwen');
/**
* 主入口:混合路由
*/
async detect(message: string, history: Message[]): Promise<IntentResult> {
// ⚠️ 第一层:正则快速通道(< 10ms)
const fastResult = this.fastPathDetect(message);
if (fastResult) {
console.log('[IntentService] 命中快速通道');
return fastResult;
}
// 第二层:LLM 智能识别(复杂长句)
return this.llmDetect(message, history);
}
/**
* 带降级的检测(LLM 不可用时回退)
*/
async detectWithFallback(message: string, history: Message[]): Promise<IntentResult> {
try {
return await this.detect(message, history);
} catch (error) {
console.warn('[IntentService] LLM 不可用,回退到关键词匹配');
return this.keywordFallback(message);
}
}
// ===== 快速通道:正则匹配 =====
private fastPathDetect(message: string): IntentResult | null {
// 质控任务
const qcMatch = message.match(/^(质控|检查|校验|QC)\s*(ID|患者|病人)?[=:]?\s*([A-Z]?\d+)/i);
if (qcMatch) {
return {
type: 'QC_TASK',
confidence: 0.9,
entities: { record_id: qcMatch[3] },
source: 'fast_path'
};
}
// 报表/报告
if (/^(报表|报告|周报|日报|统计)/.test(message)) {
return { type: 'REPORT', confidence: 0.9, entities: {}, source: 'fast_path' };
}
// 帮助
if (/^(帮助|help|怎么用|使用说明)$/i.test(message)) {
return { type: 'HELP', confidence: 1.0, entities: {}, source: 'fast_path' };
}
// 不匹配,走 LLM
return null;
}
// ===== LLM 智能识别 =====
private async llmDetect(message: string, history: Message[]): Promise<IntentResult> {
const prompt = `你是一个临床研究助手的"分诊台"。请分析用户输入,返回 JSON。
用户输入: "${message}"
分类标准:
1. QC_TASK: 明确的质控、检查、录入指令(如"检查P001的入排标准")
2. QA_QUERY: 模糊的查询、分析、统计问题(如"查下那个发烧的病人是谁")
3. PROTOCOL_QA: 关于研究方案的问题(如"访视窗口是多少天")
4. UNCLEAR: 指代不清,缺少关键信息(如"他怎么样了?")
返回格式:
{
"type": "QC_TASK" | "QA_QUERY" | "PROTOCOL_QA" | "UNCLEAR",
"confidence": 0.0-1.0,
"entities": { "record_id": "...", "visit": "..." },
"missing_info": "如果 UNCLEAR,说明缺什么信息",
"clarification_question": "如果 UNCLEAR,生成追问句"
}`;
const response = await this.llm.chat([
{ role: 'system', content: prompt },
...history.slice(-3),
{ role: 'user', content: message }
]);
try {
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
const result = JSON.parse(jsonMatch?.[0] || '{}');
result.source = 'llm';
return result;
} catch (e) {
// 解析失败,回退
return this.keywordFallback(message);
}
}
// ===== 降级策略 =====
private keywordFallback(message: string): IntentResult {
if (/质控|检查|校验|QC|入排/.test(message)) {
return { type: 'QC_TASK', confidence: 0.6, entities: {}, source: 'fallback' };
}
if (/方案|访视|窗口|知情同意/.test(message)) {
return { type: 'PROTOCOL_QA', confidence: 0.6, entities: {}, source: 'fallback' };
}
return { type: 'QA_QUERY', confidence: 0.5, entities: {}, source: 'fallback' };
}
}
4. ChatService - 消息路由
文件路径:
backend/src/modules/iit-manager/services/ChatService.ts(扩展现有)
4.1 核心职责
- 接收企业微信消息
- 路由到正确的引擎(SOP / ReAct)
- 集成记忆系统
- 实现追问机制
- V2.9 新增:收集用户反馈(👍/👎)
4.2 扩展实现
export class ChatService {
private intentService: IntentService;
private sopEngine: SopEngine;
private reactEngine: ReActEngine;
private memoryService: MemoryService;
private sessionMemory: SessionMemory;
private wechatService: WechatService;
private profilerService: ProfilerService; // V2.9 新增
async handleMessage(userId: string, message: string): Promise<string> {
const projectId = await this.getUserProject(userId);
// ⚠️ 立即发送"正在思考..."状态(解决延迟感)
await this.wechatService.sendTypingStatus(userId);
// 获取会话历史
const history = this.sessionMemory.getHistory(userId);
// 意图识别
const intent = await this.intentService.detectWithFallback(message, history);
// 保存用户消息到长期记忆
await this.memoryService.saveConversation({
projectId, userId, role: 'user', content: message, intent: intent.type
});
// ===== 追问机制 =====
if (intent.type === 'UNCLEAR') {
const clarification = intent.clarification_question
|| `请问您能具体说明一下吗?`;
this.sessionMemory.addMessage(userId, 'assistant', clarification);
return clarification;
}
// ===== 获取记忆上下文(V2.8) =====
const memoryContext = await this.memoryService.getContextForPrompt(projectId, intent);
// ===== 路由到引擎 =====
let response: string;
if (intent.type === 'QC_TASK') {
// 走 SOP 引擎
const skill = await this.getSkillConfig(projectId, 'qc_process');
const result = await this.sopEngine.run(skill.config, {
recordId: intent.entities.record_id,
projectId
});
response = this.formatSopResult(result);
} else if (intent.type === 'QA_QUERY') {
// 走 ReAct 引擎
const result = await this.reactEngine.run(message, {
history,
memoryContext,
projectId
});
// ⚠️ 保存 Trace(仅供 Admin 调试,不发给用户)
await this.reactEngine.saveTrace(projectId, userId, message, result);
response = result.content;
} else if (intent.type === 'PROTOCOL_QA') {
// 走知识库
const results = await this.difyClient.retrieve(projectId, message);
response = this.formatProtocolResults(results);
} else if (intent.type === 'HELP') {
response = this.getHelpMessage();
} else {
response = '抱歉,我不太理解您的问题。您可以说"帮助"查看使用说明。';
}
// 保存助手回复到长期记忆(返回对话 ID 用于反馈关联)
const conversationId = await this.memoryService.saveConversation({
projectId, userId, role: 'assistant', content: response
});
// 更新会话记忆
this.sessionMemory.addMessage(userId, 'assistant', response);
return response;
}
// ===== V2.9 新增:反馈循环 =====
/**
* 处理用户反馈(👍/👎)
*/
async handleFeedback(
conversationId: string,
feedback: 'thumbs_up' | 'thumbs_down',
reason?: string
): Promise<void> {
// 保存反馈到数据库
await prisma.iitConversationHistory.update({
where: { id: conversationId },
data: { feedback, feedbackReason: reason }
});
// 如果是负反馈,更新用户画像
if (feedback === 'thumbs_down' && reason) {
const conversation = await prisma.iitConversationHistory.findUnique({
where: { id: conversationId }
});
if (conversation) {
await this.profilerService.updateFromFeedback(
conversation.projectId,
conversation.userId,
reason
);
}
}
}
}
5. SchedulerService - 定时任务调度
文件路径:
backend/src/modules/iit-manager/services/SchedulerService.ts
5.1 核心职责
- 基于 pg-boss 实现定时任务
- 调度周报生成
- 调度记忆卷叠(V2.8)
- V2.9 新增:Cron Skill 主动触发(访视提醒等)
5.2 完整实现
import PgBoss from 'pg-boss';
export class SchedulerService {
private boss: PgBoss;
private reportService: ReportService;
private memoryService: MemoryService;
private wechatService: WechatService;
async init() {
this.boss = new PgBoss(process.env.DATABASE_URL!);
await this.boss.start();
// 注册任务处理器
await this.boss.work('weekly-report', this.handleWeeklyReport.bind(this));
await this.boss.work('memory-rollup', this.handleMemoryRollup.bind(this));
await this.boss.work('sop-continue', this.handleSopContinue.bind(this));
await this.boss.work('cron-skill', this.handleCronSkill.bind(this)); // V2.9 新增
await this.boss.work('profile-refresh', this.handleProfileRefresh.bind(this)); // V2.9 新增
// 配置定时任务
// 每周一早上 9 点生成周报
await this.boss.schedule('weekly-report', '0 9 * * 1', { projectId: 'all' });
// 每天凌晨 2 点执行记忆卷叠
await this.boss.schedule('memory-rollup', '0 2 * * *', { projectId: 'all' });
// 每天凌晨 3 点刷新用户画像(V2.9)
await this.boss.schedule('profile-refresh', '0 3 * * *', { projectId: 'all' });
// V2.9 新增:注册所有 Cron Skill
await this.registerCronSkills();
}
/**
* V2.9 新增:注册数据库中的 Cron Skill
*/
private async registerCronSkills(): Promise<void> {
const cronSkills = await prisma.iitSkill.findMany({
where: {
triggerType: 'cron',
isActive: true
}
});
for (const skill of cronSkills) {
if (skill.cronSchedule) {
await this.boss.schedule(
`cron-skill`,
skill.cronSchedule,
{ skillId: skill.id, projectId: skill.projectId }
);
console.log(`[Scheduler] 注册 Cron Skill: ${skill.name} (${skill.cronSchedule})`);
}
}
}
private async handleWeeklyReport(job: Job) {
const { projectId } = job.data;
// 生成周报
const report = await this.reportService.generateWeeklyReport(projectId);
// 发送到管理员群
await this.wechatService.sendToAdmins(report);
// 保存到历史书(V2.8)
await this.memoryService.saveWeeklyReport(projectId, report);
}
private async handleMemoryRollup(job: Job) {
const { projectId } = job.data;
// V2.8 记忆卷叠:将一周的对话总结为周报
await this.memoryService.rollupWeeklyMemory(projectId);
// 更新热记忆
await this.memoryService.refreshHotMemory(projectId);
}
private async handleSopContinue(job: Job) {
const { taskRunId } = job.data;
// 恢复执行 SOP 任务
await this.sopEngine.continueFromCheckpoint(taskRunId);
}
/**
* V2.9 新增:处理 Cron Skill 触发
*/
private async handleCronSkill(job: Job) {
const { skillId, projectId } = job.data;
const skill = await prisma.iitSkill.findUnique({
where: { id: skillId }
});
if (!skill || !skill.isActive) {
console.log(`[Scheduler] Skill ${skillId} 已禁用或不存在,跳过`);
return;
}
console.log(`[Scheduler] 执行 Cron Skill: ${skill.name}`);
// 获取项目的用户画像,确定通知对象
const profiles = await this.profilerService.getAllProfiles(projectId);
// 运行 SOP 流程
const result = await this.sopEngine.run(skill.config, {
projectId,
triggerType: 'cron',
profiles
});
// 根据结果发送通知
if (result.status === 'COMPLETED' && result.output) {
for (const profile of profiles) {
// 检查用户的最佳通知时间(简化版:假设 Cron 已按时间配置)
await this.wechatService.sendToUser(
profile.userId,
this.formatNotification(result.output, profile)
);
}
}
}
/**
* V2.9 新增:刷新用户画像
*/
private async handleProfileRefresh(job: Job) {
const { projectId } = job.data;
await this.profilerService.refreshProfiles(projectId);
console.log(`[Scheduler] 用户画像刷新完成: ${projectId}`);
}
/**
* 根据用户画像格式化通知内容
*/
private formatNotification(content: string, profile: UserProfile): string {
// 根据用户偏好调整通知格式
if (profile.preference?.includes('简洁')) {
// 简洁模式:只保留关键信息
const lines = content.split('\n').filter(l => l.trim());
return lines.slice(0, 3).join('\n') + (lines.length > 3 ? '\n...' : '');
}
return content;
}
}
6. ReportService - 报告生成
文件路径:
backend/src/modules/iit-manager/services/ReportService.ts
6.1 完整实现
export class ReportService {
private redcap: RedcapAdapter;
private memoryService: MemoryService;
async generateWeeklyReport(projectId: string): Promise<string> {
const stats = await this.getProjectStats(projectId);
const weekRange = this.getWeekRange();
// 获取本周的关键对话
const keyConversations = await this.memoryService.getWeeklyKeyConversations(projectId);
const report = `
📊 **${stats.projectName} 周报**
📅 ${weekRange}
**入组进度**
- 本周新入组:${stats.newEnrollments} 例
- 累计入组:${stats.totalEnrollments} / ${stats.targetEnrollments} 例
- 完成率:${stats.completionRate}%
**数据质量**
- 待处理质疑:${stats.pendingQueries} 条
- 本周关闭质疑:${stats.closedQueries} 条
- 方案偏离:${stats.protocolDeviations} 例
**AE/SAE**
- 本周新增 AE:${stats.newAEs} 例
- 本周新增 SAE:${stats.newSAEs} 例
**本周关键决策**
${keyConversations.decisions.map(d => `- ${d.date}: ${d.content}`).join('\n')}
**下周重点**
${stats.upcomingVisits.map(v => `- ${v.patientId}: ${v.visitName} (${v.dueDate})`).join('\n')}
`;
return report.trim();
}
private getWeekRange(): string {
const now = new Date();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay() + 1);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
return `${this.formatDate(weekStart)} ~ ${this.formatDate(weekEnd)}`;
}
private formatDate(date: Date): string {
return date.toISOString().split('T')[0];
}
}
7. ProfilerService - 用户画像管理(V2.9)
文件路径:
backend/src/modules/iit-manager/services/ProfilerService.ts
7.1 核心职责
- 管理用户画像(偏好、关注点、最佳通知时间)
- 根据反馈自动调整偏好
- 为主动消息提供个性化参数
7.2 完整实现
import { prisma } from '../../common/prisma';
import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
interface UserProfile {
role: string; // PI | CRC | CRA | PM
preference: string; // 简洁汇报 | 详细步骤
focusAreas: string[]; // AE | 入组进度 | 访视安排
bestNotifyTime: string; // HH:mm 格式
restrictions: string[]; // 禁令列表
feedbackStats: {
thumbsUp: number;
thumbsDown: number;
};
}
export class ProfilerService {
private llm = LLMFactory.create('qwen');
/**
* 获取用户画像(从 project_memory 中解析)
*/
async getUserProfile(projectId: string, userId: string): Promise<UserProfile | null> {
const memory = await prisma.iitProjectMemory.findUnique({
where: { projectId }
});
if (!memory) return null;
// 从 Markdown 中解析用户画像
return this.parseProfileFromMarkdown(memory.content, userId);
}
/**
* 根据反馈更新用户偏好
*/
async updateFromFeedback(
projectId: string,
userId: string,
feedbackReason: string
): Promise<void> {
const memory = await prisma.iitProjectMemory.findUnique({
where: { projectId }
});
if (!memory) return;
// 根据反馈原因推断偏好调整
const adjustment = this.inferAdjustment(feedbackReason);
// 使用 LLM 更新 Markdown 中的用户画像
const updatedContent = await this.updateProfileInMarkdown(
memory.content,
userId,
adjustment
);
await prisma.iitProjectMemory.update({
where: { projectId },
data: {
content: updatedContent,
lastUpdatedBy: 'profiler_job'
}
});
}
/**
* 周期性刷新用户画像(每日凌晨运行)
*/
async refreshProfiles(projectId: string): Promise<void> {
// 获取最近一周的对话
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
const conversations = await prisma.iitConversationHistory.findMany({
where: {
projectId,
createdAt: { gte: weekAgo }
},
orderBy: { createdAt: 'desc' }
});
// 按用户分组
const userConversations = this.groupByUser(conversations);
// 对每个用户更新画像
for (const [userId, convs] of Object.entries(userConversations)) {
await this.updateProfileFromConversations(projectId, userId, convs);
}
}
// ===== 私有方法 =====
private inferAdjustment(reason: string): { key: string; action: string } {
switch (reason) {
case 'too_long':
return { key: 'preference', action: '更简洁的回复' };
case 'inaccurate':
return { key: 'restriction', action: '注意数据准确性' };
case 'unclear':
return { key: 'preference', action: '更详细的解释' };
default:
return { key: 'feedback', action: `用户不满意: ${reason}` };
}
}
private async updateProfileInMarkdown(
content: string,
userId: string,
adjustment: { key: string; action: string }
): Promise<string> {
const prompt = `请更新以下 Markdown 中 ${userId} 的用户画像部分,增加一条偏好记录:${adjustment.action}
原内容:
${content}
请只返回更新后的完整 Markdown,不要添加任何解释。`;
const response = await this.llm.chat([
{ role: 'user', content: prompt }
]);
return response.content;
}
private parseProfileFromMarkdown(content: string, userId: string): UserProfile | null {
// 使用正则从 Markdown 中提取用户画像
const userSection = content.match(
new RegExp(`## .*\\(user_id: ${userId}\\)[\\s\\S]*?(?=##|$)`)
);
if (!userSection) return null;
const section = userSection[0];
return {
role: this.extractField(section, '角色') || 'CRC',
preference: this.extractField(section, '偏好') || '默认',
focusAreas: this.extractList(section, '关注点'),
bestNotifyTime: this.extractField(section, '最佳通知时间') || '09:00',
restrictions: this.extractList(section, '禁令'),
feedbackStats: this.extractFeedbackStats(section)
};
}
private extractField(text: string, field: string): string | null {
const match = text.match(new RegExp(`\\*\\*${field}\\*\\*:\\s*(.+)`));
return match ? match[1].trim() : null;
}
private extractList(text: string, field: string): string[] {
const match = text.match(new RegExp(`\\*\\*${field}\\*\\*:\\s*(.+)`));
if (!match) return [];
return match[1].split(/[,、,]/).map(s => s.trim());
}
private extractFeedbackStats(text: string): { thumbsUp: number; thumbsDown: number } {
const match = text.match(/👍\s*(\d+)\s*\/\s*👎\s*(\d+)/);
return {
thumbsUp: match ? parseInt(match[1]) : 0,
thumbsDown: match ? parseInt(match[2]) : 0
};
}
private groupByUser(conversations: any[]): Record<string, any[]> {
return conversations.reduce((acc, conv) => {
if (!acc[conv.userId]) acc[conv.userId] = [];
acc[conv.userId].push(conv);
return acc;
}, {} as Record<string, any[]>);
}
private async updateProfileFromConversations(
projectId: string,
userId: string,
conversations: any[]
): Promise<void> {
// 分析对话模式,推断用户偏好
const analysis = await this.analyzeConversationPatterns(conversations);
if (analysis.hasSignificantChanges) {
const memory = await prisma.iitProjectMemory.findUnique({
where: { projectId }
});
if (memory) {
const updatedContent = await this.updateProfileInMarkdown(
memory.content,
userId,
{ key: 'behavior', action: analysis.summary }
);
await prisma.iitProjectMemory.update({
where: { projectId },
data: {
content: updatedContent,
lastUpdatedBy: 'profiler_job'
}
});
}
}
}
private async analyzeConversationPatterns(conversations: any[]): Promise<{
hasSignificantChanges: boolean;
summary: string;
}> {
if (conversations.length < 5) {
return { hasSignificantChanges: false, summary: '' };
}
// 分析反馈统计
const thumbsUp = conversations.filter(c => c.feedback === 'thumbs_up').length;
const thumbsDown = conversations.filter(c => c.feedback === 'thumbs_down').length;
if (thumbsDown > thumbsUp && thumbsDown >= 3) {
return {
hasSignificantChanges: true,
summary: `近期负反馈较多(👎${thumbsDown}/👍${thumbsUp}),需调整回复策略`
};
}
return { hasSignificantChanges: false, summary: '' };
}
}
8. 服务依赖关系
┌──────────────────┐
│ ChatService │
└────────┬─────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌───────────┐
│Intent │ │Memory │ │Session │ │Profiler│ │Scheduler │
│Service │ │Service │ │Memory │ │Service │ │Service │
└────────┘ └──────────┘ └──────────┘ └────────┘ └───────────┘
│ │ │ │
▼ │ │ │
┌─────────────────────────────────────────────────────────┐
│ Engines │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ SopEngine │ │ ReActEngine │ │ SoftRuleEngine │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────┐
│ToolsService │
└──────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│RedcapAdapter │ │ DifyClient │
└──────────────┘ └──────────────┘
V2.9 新增服务流:
SchedulerService ──[cron-skill]──> SopEngine ──> ProfilerService ──> WechatService
↑
ChatService ──[feedback]──> MemoryService ──────────────┘
文档维护人:AI Agent
最后更新:2026-02-05