# IIT Manager Agent 服务层实现指南 > **版本:** V2.9 > **更新日期:** 2026-02-05 > **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md) > > **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 工具定义 ```typescript 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 完整实现 ```typescript 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, context?: { projectId: string } ): Promise { 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 { 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 { // 字段映射 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 { 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 { // 根据 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 { 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 完整实现 ```typescript 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 { // ⚠️ 第一层:正则快速通道(< 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 { 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 { 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 扩展实现 ```typescript 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 { 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 { // 保存反馈到数据库 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 完整实现 ```typescript 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 { 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 完整实现 ```typescript export class ReportService { private redcap: RedcapAdapter; private memoryService: MemoryService; async generateWeeklyReport(projectId: string): Promise { 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 完整实现 ```typescript 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 { 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 { 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 { // 获取最近一周的对话 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 { 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 { return conversations.reduce((acc, conv) => { if (!acc[conv.userId]) acc[conv.userId] = []; acc[conv.userId].push(conv); return acc; }, {} as Record); } private async updateProfileFromConversations( projectId: string, userId: string, conversations: any[] ): Promise { // 分析对话模式,推断用户偏好 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