Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md
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

1124 lines
36 KiB
Markdown
Raw 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.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<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 完整实现
```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<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 扩展实现
```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<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 完整实现
```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<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 完整实现
```typescript
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 完整实现
```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<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