diff --git a/backend/src/modules/iit-manager/update-redcap-prod-config.ts b/backend/src/modules/iit-manager/update-redcap-prod-config.ts new file mode 100644 index 00000000..09534361 --- /dev/null +++ b/backend/src/modules/iit-manager/update-redcap-prod-config.ts @@ -0,0 +1,176 @@ +/** + * 更新 REDCap 生产环境配置 + * + * 将本地开发环境的 REDCap 配置更新为生产环境 + * + * 使用方法: + * 1. 开启 RDS PostgreSQL 外网访问 + * 2. 设置环境变量:DATABASE_URL=postgresql://airesearch:Xibahe%40fengzhibo117@pgm-2zex1m2y3r23hdn5xo.pg.rds.aliyuncs.com:5432/ai_clinical_research_test + * 3. 运行:npx tsx src/modules/iit-manager/update-redcap-prod-config.ts + * 4. 关闭 RDS PostgreSQL 外网访问 + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// 生产环境配置 +const PROD_CONFIG = { + name: 'Test0202', + redcapUrl: 'https://redcap.xunzhengyixue.com', + redcapApiToken: '93785593BED5A25DB3818D7C221FC045', + redcapProjectId: '16', // REDCap 项目 ID +}; + +async function main() { + console.log('🔍 查看当前项目配置...\n'); + + try { + // 1. 查看当前配置 + const existingProjects = await prisma.$queryRaw>` + SELECT id, name, redcap_url, redcap_api_token, redcap_project_id, status, dify_dataset_id + FROM iit_schema.projects + ORDER BY created_at DESC + `; + + if (existingProjects.length > 0) { + console.log('📋 当前项目配置:'); + existingProjects.forEach((p, i) => { + console.log(`\n项目 ${i + 1}:`); + console.log(` ID: ${p.id}`); + console.log(` 名称: ${p.name}`); + console.log(` REDCap URL: ${p.redcap_url}`); + console.log(` REDCap 项目ID: ${p.redcap_project_id}`); + console.log(` 状态: ${p.status}`); + console.log(` Dify Dataset: ${p.dify_dataset_id || '未配置'}`); + }); + + // 2. 检查是否已有生产环境配置 + const hasProdConfig = existingProjects.some( + p => p.redcap_url === PROD_CONFIG.redcapUrl + ); + + if (hasProdConfig) { + console.log('\n✅ 已存在生产环境配置,更新 API Token...'); + + await prisma.$executeRaw` + UPDATE iit_schema.projects + SET redcap_api_token = ${PROD_CONFIG.redcapApiToken}, + name = ${PROD_CONFIG.name}, + updated_at = NOW() + WHERE redcap_url = ${PROD_CONFIG.redcapUrl} + `; + + console.log('✅ 生产环境配置已更新'); + } else { + // 3. 如果只有本地配置,更新为生产环境 + const activeProject = existingProjects.find(p => p.status === 'active'); + + if (activeProject) { + console.log('\n🔄 将活跃项目更新为生产环境配置...'); + + await prisma.$executeRaw` + UPDATE iit_schema.projects + SET name = ${PROD_CONFIG.name}, + redcap_url = ${PROD_CONFIG.redcapUrl}, + redcap_api_token = ${PROD_CONFIG.redcapApiToken}, + updated_at = NOW() + WHERE id = ${activeProject.id} + `; + + console.log('✅ 项目配置已更新为生产环境'); + } else { + // 4. 创建新的生产环境配置 + console.log('\n➕ 创建新的生产环境项目配置...'); + + await prisma.$executeRaw` + INSERT INTO iit_schema.projects ( + id, name, redcap_url, redcap_api_token, redcap_project_id, + field_mappings, status, created_at, updated_at + ) VALUES ( + gen_random_uuid(), + ${PROD_CONFIG.name}, + ${PROD_CONFIG.redcapUrl}, + ${PROD_CONFIG.redcapApiToken}, + ${PROD_CONFIG.redcapProjectId}, + '{}'::jsonb, + 'active', + NOW(), + NOW() + ) + `; + + console.log('✅ 生产环境项目配置已创建'); + } + } + } else { + // 5. 数据库中没有项目,创建新的 + console.log('📋 数据库中没有项目配置,创建新的生产环境配置...'); + + await prisma.$executeRaw` + INSERT INTO iit_schema.projects ( + id, name, redcap_url, redcap_api_token, redcap_project_id, + field_mappings, status, created_at, updated_at + ) VALUES ( + gen_random_uuid(), + ${PROD_CONFIG.name}, + ${PROD_CONFIG.redcapUrl}, + ${PROD_CONFIG.redcapApiToken}, + ${PROD_CONFIG.redcapProjectId}, + '{}'::jsonb, + 'active', + NOW(), + NOW() + ) + `; + + console.log('✅ 生产环境项目配置已创建'); + } + + // 6. 验证最终配置 + console.log('\n📋 验证最终配置...\n'); + + const finalConfig = await prisma.$queryRaw>` + SELECT id, name, redcap_url, redcap_project_id, status + FROM iit_schema.projects + WHERE status = 'active' + `; + + if (finalConfig.length > 0) { + console.log('✅ 活跃项目配置:'); + finalConfig.forEach(p => { + console.log(` 名称: ${p.name}`); + console.log(` REDCap URL: ${p.redcap_url}`); + console.log(` REDCap 项目ID: ${p.redcap_project_id}`); + console.log(` ID: ${p.id}`); + }); + } + + console.log('\n🎉 配置完成!'); + console.log('\n📝 下一步:'); + console.log(' 1. 在 REDCap 控制中心启用 Data Entry Trigger (DET)'); + console.log(' 2. 配置 DET URL: https://iit.xunzhengyixue.com/api/v1/iit/webhooks/redcap'); + console.log(' 3. 测试数据同步'); + + } catch (error: any) { + console.error('❌ 操作失败:', error.message); + } finally { + await prisma.$disconnect(); + } +} + +main(); diff --git a/backend/test-wechat-signature.cjs b/backend/test-wechat-signature.cjs new file mode 100644 index 00000000..a3f1b831 --- /dev/null +++ b/backend/test-wechat-signature.cjs @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +const token = 'IitPatientWechat2026JanToken'; +const timestamp = '1234567890'; +const nonce = 'test123'; + +// 字典序排序 +const arr = [token, timestamp, nonce].sort(); +console.log('Sorted array:', arr); + +// 拼接字符串 +const str = arr.join(''); +console.log('Concatenated string:', str); + +// SHA1 加密 +const hash = crypto.createHash('sha1').update(str).digest('hex'); +console.log('Calculated signature:', hash); + +// 使用这个签名测试 +console.log(''); +console.log('Test URL:'); +console.log(`curl "https://iit.xunzhengyixue.com/wechat/patient/callback-plain?signature=${hash}×tamp=${timestamp}&nonce=${nonce}&echostr=hello"`); diff --git a/backend/test-wechat-signature.js b/backend/test-wechat-signature.js new file mode 100644 index 00000000..a3f1b831 --- /dev/null +++ b/backend/test-wechat-signature.js @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +const token = 'IitPatientWechat2026JanToken'; +const timestamp = '1234567890'; +const nonce = 'test123'; + +// 字典序排序 +const arr = [token, timestamp, nonce].sort(); +console.log('Sorted array:', arr); + +// 拼接字符串 +const str = arr.join(''); +console.log('Concatenated string:', str); + +// SHA1 加密 +const hash = crypto.createHash('sha1').update(str).digest('hex'); +console.log('Calculated signature:', hash); + +// 使用这个签名测试 +console.log(''); +console.log('Test URL:'); +console.log(`curl "https://iit.xunzhengyixue.com/wechat/patient/callback-plain?signature=${hash}×tamp=${timestamp}&nonce=${nonce}&echostr=hello"`); diff --git a/backups/rds_backup_20260128.dump b/backups/rds_backup_20260128.dump new file mode 100644 index 00000000..8a10bc34 Binary files /dev/null and b/backups/rds_backup_20260128.dump differ diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2 落地实施指南:极简架构版 (融合 Moltbot).md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2 落地实施指南:极简架构版 (融合 Moltbot).md new file mode 100644 index 00000000..c4f9dfec --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2 落地实施指南:极简架构版 (融合 Moltbot).md @@ -0,0 +1,341 @@ +# **IIT Manager Agent V2.2 落地实施指南:极简架构版 (融合 Moltbot)** + +**文档版本:** V2.2 (Moltbot Enhanced) + +**日期:** 2026-02-03 + +**适用对象:** 2人高效开发团队 + +**核心原则:** **Postgres-Only**, **No New Components**, **Service-First**, **Proactive & Personalized** + +**变更说明:** 在 V2.1 基础上,借鉴 Moltbot (Clawbot) 的主动性与记忆机制,增加了用户偏好存储、每日早报 Skill 和自我修正回路。 + +## **1\. 核心架构:回归本质 (The Pragmatic Architecture)** + +我们将“Brain-Hand-Tool”模型映射到现有的 Node.js \+ Postgres 技术栈上。 + +graph TD + subgraph "Layer 3: 数字化大脑 (The Brain)" + DB\[(Postgres DB)\] + DB \--\>|加载 Skills & Preferences| Engine\[QcEngineService\] + + subgraph "混合双引擎" + Engine \--\>|1. 执行 JSON Logic| Logic\[Engine A (硬规则)\] + Engine \--\>|2. 组装 Prompt (含偏好)| LLM\[Engine B (软指令)\] + end + end + + subgraph "Layer 2: 伪 MCP 接口层 (The Hand)" + LLM \--\>|3. 调用| Tools\[ToolsService (Class)\] + Logic \--\>|3. 调用| Tools + + Tools \--\>|read\_data| Tool1\[RedcapAdapter\] + Tools \--\>|search\_doc| Tool2\[VectorSearchService\] + Tools \--\>|send\_msg| Tool3\[WechatService\] + end + + subgraph "Layer 1: 基础设施 (The Tool)" + Tool1 \--\> REDCap + Tool2 \--\> pgvector + Tool3 \--\> WeCom + end + +## **2\. 数据层设计:Skills 即数据 (Skills as Data)** + +不再使用 OSS 存文件,直接使用 Postgres iit\_schema 存储结构化配置。**新增用户偏好表。** + +### **2.1 数据库 Schema** + +// schema.prisma + +// 核心技能表 +model IitSkill { + id String @id @default(uuid()) + projectId String // 绑定项目 ID (如 PID=16) + formName String // 绑定表单 (如 demographics) 或 场景 (如 daily\_briefing) + + // 核心配置 (JSON 类型) + // 包含 hard\_rules, soft\_instructions, schedule (cron表达式) + config Json + + isActive Boolean @default(true) + version Int @default(1) + updatedAt DateTime @updatedAt + + @@unique(\[projectId, formName\]) + @@map("iit\_skills") + @@schema("iit\_schema") +} + +// \[New\] 用户偏好表 (Moltbot 记忆机制) +model IitUserPreference { + id String @id @default(uuid()) + userId String // 绑定企业微信 UserID + projectId String // 绑定项目 + + // 偏好配置 (JSON) + // 例如: { "report\_style": "concise", "notification\_time": "08:30" } + preferences Json + + updatedAt DateTime @updatedAt + + @@unique(\[userId, projectId\]) + @@map("iit\_user\_preferences") + @@schema("iit\_schema") +} + +### **2.2 JSON 配置范例 (Skill)** + +**范例 1:质控 Skill** + +{ + "description": "人口学表单质控", + "hard\_rules": \[ + { + "field": "age", + "logic": { "\>=": \[{ "var": "age" }, 18\] }, + "message": "年龄必须 \>= 18 岁" + } + \], + "soft\_instructions": \[ + { + "instruction": "检查备注字段。如果包含'无法签署知情同意',请标记违规。", + "tools": \["read\_clinical\_data"\] + } + \] +} + +**范例 2:\[New\] 每日早报 Skill (Moltbot 主动性)** + +{ + "description": "每日项目进展早报", + "schedule": "0 30 8 \* \* \*", // 每天 08:30 触发 (pg-boss) + "soft\_instructions": \[ + { + "instruction": "请统计昨天的入组人数、待处理质疑数。生成一段简报发给 PI。注意参考用户的 report\_style 偏好。", + "tools": \["get\_project\_stats", "send\_wechat\_msg"\] + } + \] +} + +## **3\. 服务层设计:伪 MCP 实现 (Service-First)** + +不要引入 @modelcontextprotocol/sdk。创建一个标准的 Service 类来管理工具。 + +### **3.1 ToolsService.ts 实现规范** + +// backend/src/modules/iit-manager/services/ToolsService.ts + +import { RedcapAdapter } from '../adapters/RedcapAdapter'; +import { VectorSearchService } from '@/common/rag/vectorSearchService'; + +// 定义工具接口 (为了给 LLM 生成菜单) +export const TOOL\_DEFINITIONS \= \[ + { + name: "read\_clinical\_data", + description: "读取REDCap临床数据...", + parameters: { ... } + }, + { + name: "update\_user\_preference", // \[New\] 更新偏好工具 + description: "更新用户的偏好设置,如报告风格、通知时间。", + parameters: { + type: "object", + properties: { + key: { type: "string", description: "偏好键名,如 report\_style" }, + value: { type: "string", description: "偏好值,如 concise (简洁) 或 detailed (详细)" } + } + } + } +\]; + +export class ToolsService { + constructor( + private redcap: RedcapAdapter, + private rag: VectorSearchService, + private prisma: PrismaClient + ) {} + + // 统一执行入口 (LLM 只认识这个) + async executeTool(name: string, args: any) { + switch (name) { + case 'read\_clinical\_data': + return this.redcap.exportRecords(args); + + case 'update\_user\_preference': + // \[New\] 实现记忆更新 + return this.updatePreference(args); + + case 'raise\_query': + return this.savePendingAction(args); + + default: + throw new Error(\`未知工具: ${name}\`); + } + } +} + +## **4\. 业务层设计:混合引擎调度 (含自我修正)** + +### **4.1 执行逻辑 (The Pipeline)** + +// backend/src/modules/iit-manager/services/QcEngineService.ts + +export class QcEngineService { + + async runQualityCheck(projectId: string, formName: string, data: any, userId?: string) { + // 1\. 加载 Skill 和 用户偏好 + const skill \= await prisma.iitSkill.findUnique({...}); + const userPref \= userId ? await prisma.iitUserPreference.findUnique({ where: { userId, projectId } }) : null; + + if (\!skill) return; + + // 2\. 运行 Engine A (硬规则) + const engineA \= new HardRuleEngine(); + const hardErrors \= engineA.run(skill.config.hard\_rules, data); + + if (hardErrors.length \> 0\) { + await this.saveShadowState(hardErrors); + return; + } + + // 3\. 运行 Engine B (软指令) \- \[New\] 注入偏好 Context + const engineB \= new SoftRuleEngine(this.llm, this.toolsService); + + // 将偏好注入 Prompt: "用户偏好简洁的报告,请不要废话。" + const context \= { + ...data, + user\_preferences: userPref?.preferences + }; + + // \[New\] 开启自我修正回路 (Self-Correction Loop) + const softSuggestions \= await engineB.runWithRetry(skill.config.soft\_instructions, context, 3); + + if (softSuggestions) { + await this.saveShadowState(softSuggestions); + } + } +} + +### **4.2 自我修正机制 (Self-Correction Loop)** + +借鉴 Moltbot,当工具调用失败时,不报错,而是让 LLM 重试。 + +// SoftRuleEngine.ts (伪代码) + +async runWithRetry(instruction, context, maxRetries \= 3\) { + let history \= \[...\]; + + for (let i \= 0; i \< maxRetries; i++) { + const response \= await llm.chat(history); + + if (response.hasToolCall) { + try { + const result \= await tools.executeTool(response.toolName, response.args); + history.push({ role: 'tool', content: result }); + } catch (error) { + // \[New\] 捕获错误,喂回给 LLM + console.warn(\`Tool Error: ${error.message}\`); + history.push({ + role: 'user', + content: \`工具调用失败: ${error.message}。请检查参数并重试。\` + }); + continue; // 让 LLM 重新思考 + } + } else { + return response.content; + } + } +} + +## **5\. 实施路线图 (2人团队专属)** + +### **✅ Week 1: 基础建设 (Engine A)** + +1. 创建 iit\_skills 和 iit\_user\_preferences 表。 +2. 引入 json-logic-js 库。 +3. 实现 QcEngineService 的基础骨架,只跑硬规则。 + +### **🚧 Week 2: AI 接入与主动性 (Engine B)** + +1. 创建 ToolsService,实现基础工具。 +2. 实现 SoftRuleEngine 的 **自我修正回路**。 +3. **里程碑**:配置 daily\_briefing Skill,利用 pg-boss 定时触发,早上 8:30 收到测试推送。 + +### **📋 Week 3: 界面与优化** + +1. 开发 Admin 页面 (JSON Editor) 编辑 Skill。 +2. 完善影子状态 (Shadow State) 审核界面。 + +## **6\. 多轮对话与开放式问答支持** + +### **6.1 核心策略:Intent Routing \+ General Skill** + +不要把所有逻辑都写死在质控里。我们增加一个特殊的 Skill:general\_qa。 + +#### **A. 配置 general\_qa Skill** + +在数据库插入一条 formName \= 'general\_chat' 的记录: + +{ + "description": "通用问答与查询助手", + "soft\_instructions": \[ + { + "instruction": "你是本项目的智能助手。你可以回答关于项目进度、患者数据、方案细节的问题。请根据用户意图调用工具。", + "tools": \["read\_clinical\_data", "search\_protocol", "get\_project\_stats", "update\_user\_preference"\] + } + \] +} + +### **6.2 代码实现:聊天入口 (ChatService.ts)** + +// backend/src/modules/iit-manager/services/ChatService.ts + +export class ChatService { + constructor( + private qcEngine: QcEngineService, + private sessionMemory: SessionMemory + ) {} + + async handleUserMessage(userId: string, projectId: string, message: string) { + // 1\. 获取历史对话 & 用户偏好 + const history \= await this.sessionMemory.get(userId); + const userPref \= await prisma.iitUserPreference.findUnique({...}); + + // 2\. 加载通用 Skill + const generalSkill \= await prisma.iitSkill.findUnique({ + where: { projectId, formName: 'general\_chat' } + }); + + // 3\. 构造 Prompt (包含 History \+ Skill \+ Preferences) + const prompt \= \` + ${generalSkill.config.soft\_instructions\[0\].instruction} + + \[User Preferences\]: ${JSON.stringify(userPref?.preferences || {})} + + 历史对话: + ${history.map(h \=\> \`${h.role}: ${h.content}\`).join('\\n')} + + 用户当前问题: ${message} + \`; + + // 4\. 调用 LLM (Engine B 逻辑复用,含自我修正) + const response \= await this.qcEngine.runSoftAgent(prompt, generalSkill.config); + + // 5\. 更新记忆 + await this.sessionMemory.add(userId, { role: 'user', content: message }); + await this.sessionMemory.add(userId, { role: 'assistant', content: response }); + + return response; + } +} + +## **7\. Moltbot 理念借鉴总结** + +| 借鉴点 | IIT Manager 落地实现 | 价值 | +| :---- | :---- | :---- | +| **主动性 (Proactivity)** | **每日早报 Skill**:利用 pg-boss 定时触发 daily\_briefing Skill,主动推送企微消息。 | 让用户感觉系统是活的,而不是被动的数据库。 | +| **记忆 (Memory)** | **用户偏好表**:存储 report\_style, notification\_time 等,Prompt 动态注入。 | 提供千人千面的个性化体验。 | +| **自我修正 (Self-Correction)** | **重试回路**:Engine B 在工具报错时,将错误信息回传给 LLM,允许其修正参数重试。 | 极大提升系统鲁棒性,减少人工运维。 | + +**结论:这就是 IIT Manager Agent 的最终形态。既有医疗的严谨(硬规则),又有个性化的温度(偏好记忆),还有极简的架构(Postgres-Only)。开干吧!** \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2:工具泛化与灵活性提升指南.md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2:工具泛化与灵活性提升指南.md new file mode 100644 index 00000000..ea0b3933 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.2:工具泛化与灵活性提升指南.md @@ -0,0 +1,158 @@ +# **IIT Manager Agent V2.2:工具泛化与灵活性提升指南** + +**核心观点:** 灵活性来自“工具的设计粒度”,而不是“连接协议(MCP)”。 + +**解决方案:** 通过设计 3-5 个“瑞士军刀型”通用工具,替代 100 个“特种螺丝刀型”专用工具。 + +## **1\. 直击灵魂:MCP Server 真的更灵活吗?** + +你担心 V2.1 方案(Service Class)不够灵活,让我们来做个对比: + +| 维度 | MCP Server 方案 | V2.1 Service Class 方案 | 真相 | +| :---- | :---- | :---- | :---- | +| **新增工具** | 在 Server 进程里写代码 \-\> 重启 Server \-\> Agent 发现新工具 | 在 Service 类里写代码 \-\> 重启 Node.js \-\> Agent 发现新工具 | **工作量完全一样**。都要写代码逻辑。 | +| **工具调用** | Agent 发 JSON \-\> 网络传输 \-\> MCP 执行 | Agent 发 JSON \-\> 内存调用 \-\> Service 执行 | **V2.1 更快**。没有网络开销。 | +| **Agent 自主性** | LLM 看到的是 tool definitions (JSON) | LLM 看到的也是 tool definitions (JSON) | **LLM 根本分不清**你是 MCP 还是 Service。 | + +**结论:** + +* **MCP 的灵活性在于“解耦”**:比如你可以把 REDCap 工具给别人用,或者在运行时动态加载新的 Server。 +* **V2.1 的灵活性在于“敏捷”**:代码都在一个工程里,改起来最快。对于 2 人团队,**改代码 \> 调协议**。 + +## **2\. 你的痛点:是不是要写很多工具?** + +你担心:“如果有 100 个检查点,我是不是要写 check\_age, check\_gender, check\_bmi 一百个工具?” + +**绝对不需要!** 这就是我说的\*\*“工具泛化”\*\*。 + +### **错误的设计(特种螺丝刀)** + +写一堆具体的工具,导致 Service 类无限膨胀: + +* get\_patient\_age(id) +* get\_patient\_history(id) +* check\_informed\_consent(id) +* ... + +### **正确的设计(瑞士军刀)** + +只写 **3 个** 通用工具,就能覆盖 99% 的场景。Agent 会通过**参数**来体现灵活性。 + +#### **工具 1:read\_clinical\_data (全能查询器)** + +* **功能**:读取 REDCap 里的任意数据。 +* **参数**:record\_id (患者ID), fields (想查什么字段), event (哪个访视)。 +* **灵活性**: + * 查年龄?Agent 传 fields: \["age"\]。 + * 查病史?Agent 传 fields: \["medical\_history"\]。 + * **你只需要写这一个函数,Agent 就可以查任何东西。** + +#### **工具 2:eval\_logic (逻辑计算器)** + +* **功能**:处理复杂的数值计算或逻辑判断(利用 json-logic)。 +* **场景**:Agent 算不清 BMI,可以调这个工具帮它算。 + +#### **工具 3:manage\_issue (通用反馈器)** + +* **功能**:包括提出质疑、发送提醒、记录日志。 +* **参数**:type (QUERY | NOTIFY | LOG), message (内容), severity (严重程度)。 + +## **3\. 代码实现:如何实现“瑞士军刀”?** + +在你的 ToolsService.ts 中,只需要实现这几个通用方法。 + +// backend/src/modules/iit-manager/services/ToolsService.ts + +export const UNIVERSAL\_TOOL\_DEFS \= \[ + { + name: "read\_clinical\_data", + description: "通用数据查询工具。当需要检查患者的某项指标时使用。", + parameters: { + type: "object", + properties: { + record\_id: { type: "string" }, + // 关键:让 Agent 自己决定查什么字段 + fields: { + type: "array", + items: { type: "string" }, + description: "需要查询的REDCap变量名列表,如 \['age', 's\_cr', 'ae\_desc'\]" + } + }, + required: \["record\_id", "fields"\] + } + }, + { + name: "manage\_issue", + description: "通用问题处理工具。用于提出质疑或发送通知。", + parameters: { + type: "object", + properties: { + record\_id: { type: "string" }, + action\_type: { type: "string", enum: \["RAISE\_QUERY", "SEND\_WECHAT"\] }, + message: { type: "string" } + }, + required: \["record\_id", "action\_type", "message"\] + } + } +\]; + +export class ToolsService { + constructor(private redcapAdapter: RedcapAdapter) {} + + async execute(name: string, args: any) { + switch (name) { + case 'read\_clinical\_data': + // 一个函数,通过参数变化应对无限需求 + return await this.redcapAdapter.exportRecords({ + recordId: args.record\_id, + fields: args.fields // \<--- 动态字段 + }); + + case 'manage\_issue': + if (args.action\_type \=== 'RAISE\_QUERY') { + return await this.saveShadowQuery(args); + } else { + return await this.sendWechat(args); + } + } + } +} + +## **4\. 场景演示:Agent 的自主性如何体现?** + +哪怕你只提供了 2 个工具,Agent 依然表现得像个专家,因为**Skill (Prompt)** 在指导它如何组合使用这些工具。 + +### **场景 A:检查肝功能** + +* **Skill 配置**: "instruction": "检查 ALT 和 AST 是否超过正常值 3 倍。" +* **Agent 思考**: "我要查肝功能。" +* **Agent 行动**: 调用 read\_clinical\_data(fields=\['alt', 'ast'\])。 +* **系统响应**: {"alt": 150, "ast": 40} +* **Agent 判断**: "ALT 150 \> 40\*3。违规。" +* **Agent 行动**: 调用 manage\_issue(action\_type='RAISE\_QUERY', message='ALT异常')。 + +### **场景 B:检查入组年龄** + +* **Skill 配置**: "instruction": "确保患者年龄 \> 18。" +* **Agent 思考**: "我要查年龄。" +* **Agent 行动**: 调用 read\_clinical\_data(fields=\['age'\])。 +* **系统响应**: {"age": 16} +* **Agent 行动**: 调用 manage\_issue(action\_type='RAISE\_QUERY', message='年龄不合规')。 + +**看到没有?** + +你**没有写** check\_liver 和 check\_age 两个函数。 + +你只写了一个 read\_clinical\_data。 + +是 **Agent 的大脑(Prompt)** \+ **通用工具(Tool)** 实现了无限的灵活性。 + +## **5\. 总结** + +1. **MCP Server 不是灵活性的来源**:它只是一个连接标准。如果你的工具设计得烂(粒度太细),用 MCP 也一样累。 +2. **通用参数是王道**:不要写“查年龄”的工具,要写“查字段”的工具。让 Agent 填参数,这就是最大的灵活性。 +3. **V2.1 极简版足够强**:只要你的 ToolsService 按照“瑞士军刀”的思路去设计,2 个开发人员维护 100 个项目的逻辑完全没有压力。 + +**行动建议:** + +按照本文档的 **第 3 节**,把你的 ToolsService 重构一下,只保留 3-5 个通用方法。然后去测试一下 Agent 是否能聪明地自己填参数。 \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.3:健壮性设计与最佳实践.md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.3:健壮性设计与最佳实践.md new file mode 100644 index 00000000..c8453023 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.3:健壮性设计与最佳实践.md @@ -0,0 +1,162 @@ +# **IIT Manager Agent V2.3:健壮性设计与最佳实践** + +**核心目标:** 在极简架构下,解决 LLM 调用工具时的幻觉、参数错误和无结果问题。 + +**适用场景:** 2人团队,Node.js Service Class 架构。 + +## **1\. 核心风险分析:LLM 会犯什么错?** + +在让 Agent 调用 read\_clinical\_data 等工具时,通常会发生以下三类错误: + +| 错误类型 | 场景举例 | 后果 | +| :---- | :---- | :---- | +| **参数幻觉** | LLM 调用 read\_data(id="P001", fields=\["gender"\]),但 REDCap 里字段名叫 sex。 | API 返回空或报错,用户觉得 AI 很蠢。 | +| **意图漂移** | 用户问:“今天天气怎么样?” LLM 试图去 REDCap 查天气。 | 工具调用失败,浪费 Token。 | +| **格式错误** | LLM 返回的 JSON 少了一个括号,或者参数类型不对(字符串给了数字)。 | 程序 Crash,前端报错。 | + +## **2\. 解决方案:三层防御体系 (The Defense Layer)** + +不需要 MCP Server,我们在 Service Class 里加三层防御即可。 + +### **第一层:模糊映射 (The Translator)** + +**解决:** 字段名对不上的问题。 + +**原理:** 别指望 LLM 能记住 REDCap 几千个变量名。我们在代码里做一个“模糊翻译器”。 + +// backend/src/modules/iit-manager/services/ToolsService.ts + +class ToolsService { + // 1\. 定义常用字段映射字典 + private fieldMapping \= { + "gender": "sex", + "sex": "sex", + "性别": "sex", + "age": "age\_calculated", + "年龄": "age\_calculated", + "history": "medical\_history\_desc", + "病史": "medical\_history\_desc" + }; + + async executeTool(name: string, args: any) { + if (name \=== 'read\_clinical\_data') { + // 🛡️ 自动纠错逻辑 + const correctedFields \= args.fields.map(f \=\> { + // 如果字典里有,就替换;没有就保留原样 + return this.fieldMapping\[f.toLowerCase()\] || f; + }); + + console.log(\`\[Auto-Fix\] ${args.fields} \-\> ${correctedFields}\`); + + return this.redcapAdapter.exportRecords({ + ...args, + fields: correctedFields + }); + } + } +} + +### **第二层:容错重试 (The Self-Correction Loop)** + +**解决:** LLM 调用失败直接报错给用户的问题。 + +**原理:** 当工具报错时,**不要直接抛给用户**,而是把错误信息喂回给 LLM,让它重试。这是 Agent 能够“甚至比人更聪明”的关键。 + +// backend/src/modules/iit-manager/agents/QcAgent.ts + +async function runAgentLoop(prompt: string, maxRetries \= 3\) { + let history \= \[{ role: 'user', content: prompt }\]; + + for (let i \= 0; i \< maxRetries; i++) { + // 1\. LLM 思考 + const response \= await llm.chat(history); + + // 2\. 如果没有调用工具,直接返回结果 + if (\!response.hasToolCall) return response.content; + + // 3\. 尝试执行工具 + try { + const result \= await toolsService.execute(response.toolName, response.args); + + // 4\. ✅ 成功:把结果喂给 LLM,继续思考 + history.push({ role: 'tool', content: JSON.stringify(result) }); + + } catch (error) { + // 5\. ❌ 失败:触发自我修正机制 + console.warn(\`\[Agent Error\] 工具调用失败: ${error.message}\`); + + // 关键步骤:告诉 LLM 它错了,让它重试 + history.push({ + role: 'user', + content: \`系统报错:${error.message}。请检查你的参数(例如字段名是否存在),然后重试。\` + }); + // 循环继续,LLM 会在下一次迭代中修正参数 + } + } + + return "抱歉,系统尝试多次后仍然无法获取数据,请联系管理员。"; +} + +### **第三层:空结果兜底 (The Fallback)** + +**解决:** 查不到数据时体验不好的问题。 + +**原理:** 如果 REDCap 返回空,工具层要返回一段“人话”,而不是 null。 + +// ToolsService.ts + +async execute(name: string, args: any) { + // ... + const data \= await this.redcapAdapter.exportRecords(...); + + if (\!data || data.length \=== 0\) { + // 🛡️ 友好的空结果返回 + return { + status: "empty", + message: \`未找到 ID 为 ${args.record\_id} 的患者数据。请确认 ID 是否正确。\` + }; + } + return data; +} + +## **3\. 最佳实践:如何应对未知问题?** + +### **3.1 限制工具的“射程”** + +不要给 LLM 一个 sql\_query 工具让它随便查库。 + +**最佳实践**:只提供 read\_clinical\_data(record\_id, fields)。 + +* 如果用户问“天气怎么样”,LLM 发现没有 get\_weather 工具,它自己就会回答:“抱歉,我无法查询天气,我只能查询临床数据。”(这是 LLM 的自带能力)。 + +### **3.2 System Prompt 的“紧箍咒”** + +在 qc\_config.json 的 soft\_instructions 或者全局 System Prompt 中,明确边界。 + +你是一个临床研究助手。 +1\. 你只能回答与【肺癌研究】相关的问题。 +2\. 如果用户问无关问题(如天气、股票),请礼貌拒绝。 +3\. 在调用 \`read\_clinical\_data\` 时,请务必使用准确的英文变量名。如果不确定,请先调用 \`Youtube\` 查看字典。 + +### **3.3 调试与监控 (Admin Portal)** + +你们已经有了 **运营管理端**。利用起来! + +* 记录每一次 Agent 的思考过程(Thinking Trace)。 +* 如果你发现 Agent 总是把 age 拼成 agg,就在 **第一层防御(映射字典)** 里加一行配置。 +* 这就是\*\*“运营驱动开发”\*\*,比改代码快得多。 + +## **4\. 结论** + +**极简方案(Service Class)的灵活性和健壮性完全够用。** + +* **MCP Server** 只是把错误抛出来,它不会自动修错。 +* **Service Class** 允许你在本地代码里直接写 try-catch 和 Retry Loop,这对 2 人团队来说调试极其友好。 + +**行动指南:** + +1. **不用担心**用户乱问,Prompt 会拒绝。 +2. **不用担心**参数传错,写一个简单的 Mapping 字典去拦截。 +3. **不用担心**报错,实现 Self-Correction Loop(出错把错误扔回给 AI),让 AI 自己修。 + +**这就是目前 Agent 落地最务实、最抗造的模式。** \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.4:架构模式选型与 SOP 状态机推荐.md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.4:架构模式选型与 SOP 状态机推荐.md new file mode 100644 index 00000000..38a23fe8 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.4:架构模式选型与 SOP 状态机推荐.md @@ -0,0 +1,191 @@ +# **IIT Manager Agent V2.4:架构模式选型与 SOP 状态机推荐** + +**核心议题:** ReAct vs Planner vs State Machine —— 谁更适合医疗质控? + +**结论前置:** 弃用纯 Planner,限制使用 ReAct,全面拥抱 **SOP 状态机**。 + +## **1\. 深度解析:ReAct 与 Planner 适合我们吗?** + +### **1.1 ReAct (Reasoning \+ Acting)** + +* **原理**:Agent 像人一样“自言自语”。 + * *Thought*: "我需要查患者年龄。" \-\> *Action*: read\_data \-\> *Observation*: "16岁" \-\> *Thought*: "太小了,报错。" +* **在 IIT 中的地位**:**它就是你的 Engine B**。 + * 你现有的 runAgentLoop 代码本质上就是一个 ReAct 循环。 + * **优点**:灵活,能处理突发情况(比如发现数据缺了,它知道先去查)。 + * **缺点**:容易陷入死循环(反复查同一个数据),或者在思考步骤中消耗大量 Token。 + * **结论**:**保留使用**,但必须加“紧箍咒”(最大步数限制)。 + +### **1.2 Planner (Query-Planner-Execute-Reflect)** + +* **原理**:先写大纲,再干活。 + * *Plan*: 1\. 查总人数; 2\. 算脱落率; 3\. 写周报; 4\. 反思哪里写得不好。 +* **在 IIT 中的地位**:**仅适合“项目管理 Agent” (周报/统计)**。 + * 对于“数据质控”这种秒级响应的任务,Planner 太慢了(光生成计划就要 5-10 秒)。 + * **缺点**:对于 2 人团队,实现一个稳定的 Planner 很难(需要维护计划状态、断点恢复)。 + * **结论**:**质控场景禁用,报表场景可选**。 + +## **2\. 终极推荐:SOP 状态机 (SOP-Driven State Machine)** + +为什么医疗行业(GCP)最看重 Protocol(方案)?因为临床研究本质上就是一个**巨大的流程图**。 + +我们应该让 Agent **“照着流程图走”**,而不是让它“只有个模糊目标自己瞎想”。 + +### **2.1 什么是 SOP 状态机架构?** + +把你的 Skills JSON 进化成一个**有向图 (DAG)** 或 **流程图**。 + +* **节点 (Node)**:一个具体的检查步骤(可以是硬规则,也可以是软指令)。 +* **边 (Edge)**:下一步走哪里(通过了走 A,没通过走 B)。 + +### **2.2 为什么它既稳又活?** + +* **稳 (Stability)**:流程是写死的(先查年龄,再查病史,最后查合并用药)。Agent 不会乱跳步骤,不会漏查。 +* **活 (Flexibility)**:每个节点内部可以用 AI(Engine B)来灵活判断。 + +## **3\. 落地实施:如何改造 Skills JSON?** + +我们将 V2.1 的扁平配置升级为**流程配置**。 + +### **3.1 新的 Skill 结构 (qc\_process.json)** + +{ + "name": "肺癌入排质控流程", + "start\_node": "check\_age", // 入口 + + "nodes": { + // 节点 1: 硬规则 (CPU 执行) + "check\_age": { + "type": "hard\_rule", + "logic": { "\>=": \[{ "var": "age" }, 18\] }, + "on\_pass": "check\_history", // 通过了,去查病史 + "on\_fail": "end\_with\_error" // 没通过,直接结束 + }, + + // 节点 2: 软指令 (AI 执行 \- ReAct 模式) + "check\_history": { + "type": "soft\_instruction", + "instruction": "检查病史描述,排除'间质性肺炎'。", + "tools": \["read\_clinical\_data"\], + "on\_pass": "check\_meds", + "on\_fail": "review\_required" // AI 觉得有问题,转人工复核 + }, + + // 节点 3: 复杂逻辑 (AI 执行) + "check\_meds": { + "type": "soft\_instruction", + "instruction": "检查合并用药,排除靶向药。", + "on\_pass": "end\_success", + "on\_fail": "review\_required" + } + } +} + +## **4\. 代码实现:极简状态机引擎** + +不要引入 XState 这种复杂库,2 人团队自己写个 while 循环就够了。 + +// backend/src/modules/iit-manager/services/SopEngine.ts + +class SopEngine { + async run(skillConfig: any, data: any) { + let currentNodeId \= skillConfig.start\_node; + let context \= { ...data }; // 共享上下文 + + while (currentNodeId && currentNodeId \!== 'end') { + const node \= skillConfig.nodes\[currentNodeId\]; + + console.log(\`\[SOP\] Executing node: ${currentNodeId}\`); + + let result; + if (node.type \=== 'hard\_rule') { + // 调用 Engine A (V2.1 的逻辑) + result \= this.runHardRule(node, context); + } else if (node.type \=== 'soft\_instruction') { + // 调用 Engine B (ReAct 模式) + result \= await this.runSoftAgent(node, context); + } + + // 状态流转 + if (result.passed) { + currentNodeId \= node.on\_pass; + } else { + if (node.on\_fail \=== 'end\_with\_error') { + await this.saveShadowQuery(result.message); + return; + } + currentNodeId \= node.on\_fail; + } + } + } +} + +## **5\. 架构对比总结表** + +| 架构模式 | 稳定性 | 灵活性 | 复杂度 | 适用场景 | 推荐指数 | +| :---- | :---- | :---- | :---- | :---- | :---- | +| **ReAct (V2.1 Engine B)** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 中 | 单点模糊判断 (如: 这段话是不是病史) | ✅ **局部使用** | +| **Planner** | ⭐⭐ | ⭐⭐⭐⭐ | 高 | 复杂任务规划 (如: 生成全项目总结) | ❌ **质控不用** | +| **SOP 状态机 (V2.4)** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 低 | **临床试验全流程控制** (SOP 落地) | 👑 **核心架构** | + +## **6\. 深度释疑:SOP 会导致僵化吗?(Flexibility Check)** + +针对你担心的“每个字段都要配流程”和“浪费 LLM 能力”的问题,我们需要纠正一个认知误区。 + +### **6.1 误区:SOP \= 细粒度代码逻辑** + +* **错误做法**:把 400 个变量写成 400 个节点。节点1: 查年龄 \-\> 节点2: 查性别 \-\> 节点3: 查身高... + * **后果**:配置地狱,灵活性为零。 + +### **6.2 正解:SOP \= 粗粒度业务阶段 (Phases)** + +我们通过 **“粗颗粒度节点”** 来充分释放 LLM 的能力。一个 SOP 可能只有 3 个节点,但涵盖了无数逻辑。 + +* **节点 A (基线硬扫描)**: + * **类型**:硬规则(Hard Rule) + * **内容**:一次性加载 50 条 JSON Logic 规则。 + * **执行**:CPU 毫秒级跑完所有数值校验(年龄、身高、体重、化验值范围)。 + * **配置**:这是一个列表,不是流程图,配置很简单。 +* **节点 B (AI 综合研判)**: + * **类型**:软指令(Soft Instruction / ReAct) + * **Prompt**:“请作为医学专家,阅读患者的‘现病史’、‘既往史’和‘手术史’。请综合判断是否存在:严重心血管疾病、未控制的高血压或活动性感染。如果有,请引用原文说明。” + * **LLM 的灵活性**: + * 在这个节点**内部**,LLM 是完全自由的。 + * 它可以自己决定先看现病史还是既往史。 + * 它可以自己判断“高血压二级”算不算“未控制”。 + * **它依然在用 ReAct 调用工具**,只是我们把它限制在了“检查病史”这个大阶段里,不让它跑去“检查知情同意书”。 + +### **6.3 实战推演:如何拆解一个 400 变量的项目?** + +假设你的项目有 10 个表,400 个变量。 + +| 数据类型 | 变量数量 | 举例 | 处理策略 | 节点归属 | +| :---- | :---- | :---- | :---- | :---- | +| **人口学/基线** | 20 个 | 年龄、性别、身高、体重 | **Hard Rule** | **节点 A (基线检查)** | +| **实验室检查** | 300 个 | 血常规、生化、尿常规 | **Hard Rule** | **节点 A (基线检查)** | +| **体格检查** | 50 个 | 心率、血压、体温 | **Hard Rule** | **节点 A (基线检查)** | +| **既往病史** | 5 个 | 自由文本描述 | **Soft Instruction** | **节点 B (病史研判)** | +| **合并用药** | 5 个 | 自由文本药名 | **Soft Instruction** | **节点 C (用药研判)** | +| **不良事件** | 20 个 | 严重程度、转归情况 | **Soft Instruction** | **节点 D (安全监控)** | + +**结论**: + +* 尽管有 400 个变量,**SOP 流程图里其实只有 4 个节点**。 +* 你只需要写 **3 条** AI 指令(针对节点 B, C, D)。 +* 剩下的 370 个变量,全部用简单的 JSON 规则批量处理。 + +**这不仅没有浪费 LLM 的能力,反而让它更专注(只处理那 30 个最难的变量),并帮你省下了巨额的 Token 费用。** + +## **7\. 给 2 人团队的行动指南** + +1. **宏观上用 SOP**: + * 临床研究最讲究 SOP。用 JSON 定义好“先查A,再查B,最后查C”的流程。这能保证你的系统像瑞士钟表一样稳定。 + * 这解决了“不知道 AI 会不会乱跑”的恐惧。 +2. **微观上用 ReAct**: + * 在 SOP 的某个具体节点(比如“检查病史”节点)里,允许 AI 使用 ReAct 模式(思考-查库-判断)。 + * 这保留了处理非结构化数据的灵活性。 +3. **实现路径**: + * Phase 2.1: 先把之前的 hard\_rules 列表改成简单的 nodes 结构(线性执行)。 + * Phase 2.2: 在 nodes 里增加 on\_pass / on\_fail 跳转逻辑。 + +**一句话总结:用 SOP (状态机) 管住 Agent 的腿(流程),用 ReAct 管住 Agent 的嘴(推理)。这就是既稳定又灵活的最佳实践。** \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.6 智能化升级方案:双脑架构.md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.6 智能化升级方案:双脑架构.md new file mode 100644 index 00000000..c0ec0f8d --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent V2.6 智能化升级方案:双脑架构.md @@ -0,0 +1,167 @@ +# **IIT Manager Agent V2.6 智能化升级方案:双脑架构** + +**背景:** 响应产品经理关于“系统不够智能、交互僵硬”的反馈。 + +**核心目标:** 在保留 V2.5 **SOP 严谨性**(左脑)的基础上,引入 **ReAct 灵活性**(右脑),打造“懂人话”的智能助手。 + +**适用对象:** 2人高效开发团队 + +## **1\. 核心理念:为什么我们需要“两个大脑”?** + +我们之前的架构(V2.5)构建了一个完美的\*\*“左脑”**(逻辑、规则、流程),现在我们需要补上**“右脑”\*\*(直觉、推理、对话)。 + +| 维度 | 左脑 (SOP 状态机) \- V2.5 | 右脑 (ReAct Agent) \- V2.6 新增 | +| :---- | :---- | :---- | +| **擅长** | 执行标准流程、合规检查、填报表 | 处理模糊提问、多步查询、综合分析 | +| **典型指令** | "对 P001 进行入排质控" | "帮我查下最近那个发烧的病人是谁?" | +| **思维方式** | **线性执行** (Step 1 \-\> Step 2\) | **循环推理** (思考 \-\> 查库 \-\> 发现不够 \-\> 再查) | +| **安全性** | 极高 (写操作必须走这里) | **只读 (Read-Only)**,严禁直接修改数据 | +| **用户评价** | "靠谱但死板" | "聪明但不可控" | + +**解决方案:** + +我们不是要推翻 V2.5,而是要在它旁边挂载一个 **ReAct Agent**,并通过一个智能的 **意图路由器 (Router)** 来决定用哪个脑子。 + +## **2\. 架构升级:双脑路由模型 (The Dual-Brain Architecture)** + +我们在 ChatService 中引入一个由 LLM 驱动的**意图识别层**。 + +graph TD + User\[用户输入: "那个发烧的病人..."\] \--\> Router\[🧠 意图路由层 (LLM)\] + + Router \--\>|意图: 模糊查询/分析| ReAct\[🎨 右脑: ReAct Agent\] + Router \--\>|意图: 标准作业/写操作| SOP\[📐 左脑: SOP Engine\] + Router \--\>|意图: 信息缺失| AskBack\[❓ 追问机制\] + + subgraph "共享工具池 (ToolsService)" + T1\[read\_clinical\_data\] + T2\[search\_protocol\] + T3\[get\_project\_stats\] + end + + ReAct \--\>|调用 (只读)| T1 + ReAct \--\>|调用 (只读)| T2 + + SOP \--\>|调用 (读写)| T1 + SOP \--\>|调用 (读写)| T3 + +## **3\. 关键组件实现 (Phase 5\)** + +### **3.1 智能意图路由 (The Router)** + +不要用正则表达式,直接用 LLM 判断用户想干什么。 + +// backend/src/modules/iit-manager/services/IntentService.ts + +export class IntentService { + async detect(message: string, history: any\[\]): Promise\ { + const prompt \= \` + 你是一个临床研究助手的"分诊台"。请分析用户输入,返回 JSON。 + + 用户输入: "${message}" + + 分类标准: + 1\. QC\_TASK: 明确的质控、检查、录入指令(如"检查P001")。 + 2\. QA\_QUERY: 模糊的查询、分析、统计问题(如"查下那个发烧的...")。 + 3\. UNCLEAR: 指代不清,缺少关键信息(如"他怎么样了?")。 + + 返回格式: { "type": "QC\_TASK" | "QA\_QUERY" | "UNCLEAR", "reason": "...", "missing\_info": "..." } + \`; + + const result \= await this.llm.chat(prompt); + return JSON.parse(result); + } +} + +### **3.2 右脑:ReAct Agent (The Smart Loop)** + +PM 提到的 ReAct 是对的。我们需要一个循环,让 AI 自己决定调什么工具,调几次。 + +**核心逻辑:** + +1. **思考**:用户问“最近入组的女性患者平均年龄”,我需要先查最近入组名单,再查她们的年龄,最后计算。 +2. **行动**:调用 read\_clinical\_data(filter="recent")。 +3. **观察**:拿到 5 个患者数据。 +4. **思考**:数据有了,我自己算一下平均值。 +5. **回答**:35.5岁。 + +// backend/src/modules/iit-manager/engines/ReActEngine.ts + +export class ReActEngine { + constructor(private tools: ToolsService) {} + + async run(query: string, context: any) { + let messages \= \[ + { role: 'system', content: \`你是一个智能助手。你可以使用工具回答问题。请使用 ReAct 模式思考。\` }, + { role: 'user', content: query } + \]; + + // 最多思考 5 轮,防止死循环费钱 + for (let i \= 0; i \< 5; i++) { + const response \= await this.llm.chat(messages, { tools: this.tools.getDefinitions() }); + + // 1\. AI 决定结束 + if (\!response.hasToolCall) return response.content; + + // 2\. AI 决定调工具 + const toolResult \= await this.tools.execute(response.toolName, response.args); + + // 3\. 把结果喂回去 + messages.push({ role: 'tool', content: JSON.stringify(toolResult) }); + } + } +} + +### **3.3 追问机制 (Clarification)** + +这是解决“像个傻子”的关键。如果意图识别是 UNCLEAR,不要报错,要追问。 + +* **User**: "他怎么样了?" +* **Old Agent**: "未找到指令。" (傻子) +* **New Agent**: "请问您指的是哪位患者?是刚刚讨论的 P001 吗?" (智能) + +**实现**: + +在 ChatService 中,如果 IntentService 返回 UNCLEAR,直接把 missing\_info 包装成反问句返回给用户。 + +## **4\. 融合后的开发计划更新 (Merge into V2.5)** + +我们将 PM 的建议整合进开发计划,作为 **Week 4+ 的核心任务**。 + +### **Phase 5:智能化增强 (Smart Layer) \- 新增** + +| 时间 | 任务 | 说明 | +| :---- | :---- | :---- | +| **Day 22** | **意图识别路由** | 实现 IntentService,替代 Keyword 匹配。 | +| **Day 23** | **ReAct 引擎实现** | 实现多轮思考循环,对接 ToolsService(复用现有工具)。 | +| **Day 24** | **上下文记忆增强** | 升级 SessionMemory,支持由近及远的对话历史回溯。 | +| **Day 25** | **主动追问 UI** | 前端支持“建议气泡”(Suggestion Chips),方便用户点击回复追问。 | + +## **5\. 风险控制:给 ReAct 戴上镣铐** + +为了防止 PM 担心的“太灵活导致不可控”,我们需要加两条铁律: + +1. **右脑(ReAct)只读不写**: + * ReAct Agent 只能调用 read\_\* 和 search\_\* 类工具。 + * 如果它想修改数据(如 update\_record),必须引导用户:“看来你需要修改数据,这属于质控流程,请确认是否启动【修改 SOP】?” + * **原因**:避免 AI 在聊天中随口把临床数据改了,这是合规红线。 +2. **幻觉熔断**: + * ReAct 循环中,如果连续 2 次调用工具报错,强制终止并转人工。 + * 避免 AI 在那里自言自语浪费 Token。 + +## **6\. 结论** + +**你是对的,PM 也是对的。** + +* **V2.5 (SOP)** 是我们的**骨架**,保证我们站得稳(合规、准确)。 +* **PM 建议 (ReAct)** 是我们的**血肉**,让我们看起来像个人(灵活、智能)。 + +**最终策略:** + +继续执行 V2.5 的前 3 周计划(那是基础)。 + +在 **Week 4**,原本计划做“视觉识别”,现在建议**替换为“智能化增强(ReAct \+ 意图路由)”**。 + +* 理由:视觉识别是锦上添花,而“不被当成傻子”是用户留存的关键。 + +**行动:** 请批准将“视觉识别”延后,优先开发“双脑路由”模块。 \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent 架构决策白皮书:极简主义的胜利.md b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent 架构决策白皮书:极简主义的胜利.md new file mode 100644 index 00000000..83a5d38b --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/00-系统设计/IIT Manager Agent 架构决策白皮书:极简主义的胜利.md @@ -0,0 +1,118 @@ +# **IIT Manager Agent 架构决策白皮书:极简主义的胜利** + +**文档版本:** V1.0 + +**日期:** 2026-02-03 + +**面向对象:** 开发团队、架构师、项目相关人员 + +**核心议题:** 为什么我们放弃了 MCP 和 Skills,选择了 Postgres \+ Service Class? + +## **1\. 我们的现状与挑战 (Context)** + +在做出任何架构决策之前,必须诚实地面对我们当前的约束条件。 + +* **团队规模**:2 人(精英小队,但人力极其有限)。 +* **技术栈**:Node.js (Fastify) \+ PostgreSQL (Prisma) \+ Postgres-Only (pg-boss)。 +* **业务场景**: + * **医疗垂直领域**:对数据的准确性、安全性、合规性要求极高(GCP/FDA)。 + * **高并发写入**:未来 100+ 项目,47 家中心同时录入 REDCap。 + * **实时响应**:PI 在企业微信提问,期望秒级回复。 +* **核心痛点**:既要应对复杂的业务逻辑(不同项目的入排标准千差万别),又要保证系统的绝对稳定(医疗数据不能错),还要在有限的人力下快速交付。 + +## **2\. 为什么我们放弃了 "Skills" 和 "MCP Server"?** + +这两个概念在 AI Agent 领域非常火,但经过深度评估,对于现阶段的我们来说,它们是\*\*“屠龙之术”\*\*。 + +### **2.1 放弃 "Skills" (作为独立框架)** + +通常所说的 Skills 往往指代复杂的 Agent 编排框架(如 LangChain 中的 Tools/Skills 概念),或者需要专门的解析器来处理的复杂 DSL(领域特定语言)。 + +* **问题所在**: + * **学习成本高**:团队需要学习一套新的框架或语法。 + * **调试困难**:一旦 Agent 不按套路出牌,很难追踪是 Prompt 写错了,还是框架解析错了。 + * **过度设计**:对于绝大多数医疗质控逻辑(如 age \> 18),用自然语言让 LLM 去猜,不如直接写代码判断来得准确、高效、省钱。 +* **我们的替代方案**:**Postgres JSON 配置**。 + * 我们将“技能”降维为**数据**。 + * hard\_rules:用 JSON Logic 描述死规则,由 CPU 执行,100% 准确。 + * soft\_instructions:用一段 Prompt 描述软逻辑,由 LLM 执行。 + * **本质**:我们没有抛弃 Skills 的**理念**(将逻辑配置化),我们抛弃的是 Skills 的**复杂实现**。 + +### **2.2 放弃 "MCP Server" (Model Context Protocol)** + +MCP 是 Anthropic 推出的一种标准协议,用于连接 AI 和数据源。 + +* **问题所在**: + * **架构复杂度爆炸**:引入 MCP 意味着我们需要维护独立的 Server 进程、处理进程间通信 (IPC/RPC)、处理网络延迟和断连。 + * **运维负担**:对于 2 人团队,多维护一个服务就是多一份心智负担。 + * **牛刀杀鸡**:我们的 Agent 和 REDCap Adapter 都在同一个 Node.js 进程里,直接调用函数只要几纳秒;如果走 MCP 协议,需要序列化-\>发送-\>接收-\>反序列化,性能损耗巨大且毫无必要。 +* **我们的替代方案**:**Service Class (伪 MCP)**。 + * 我们在代码层面定义一个统一的 ToolsService 类。 + * 它向外暴露标准的接口(就像 MCP 一样),但内部直接调用函数。 + * **本质**:我们保留了 MCP 的**接口思想**(解耦、标准化),抛弃了 MCP 的**通信协议**。 + +## **3\. 我们的架构:极简主义的工程实践** + +我们将这套架构命名为 **"Postgres-Only \+ Service-First \+ SOP State Machine"**。 + +### **3.1 架构全景图** + +graph TD + subgraph "核心优势:全在内存里,全在库里" + DB\[(Postgres DB)\] + NodeJS\[Node.js 后端服务\] + + DB \--\>|1. 加载配置 (Skills)| NodeJS + + subgraph "Node.js 内部逻辑" + Engine\[QcEngineService\] + Engine \--\>|2. 执行| HardRule\[Engine A (CPU)\] + Engine \--\>|3. 思考| LLM\[Engine B (AI)\] + + LLM \--\>|4. 调用| Tools\[ToolsService\] + HardRule \--\>|4. 调用| Tools + + Tools \--\>|5. 执行| Adapter\[RedcapAdapter\] + end + + Adapter \--\>|6. 请求| External((REDCap)) + end + +### **3.2 核心组件解析** + +1. **Skills as Data (数据化技能)** + * **实现**:iit\_skills 表中的 JSON 字段。 + * **优势**:热更新。医生在后台改了配置,下一秒 Agent 就生效,无需重启服务。 +2. **Service as Tool (服务即工具)** + * **实现**:ToolsService 类。 + * **优势**: + * **极速**:函数调用,零延迟。 + * **稳定**:利用 TypeScript 的类型检查,编译期就能发现错误。 + * **可控**:我们可以轻松地在代码里加入重试、熔断、日志逻辑。 +3. **SOP State Machine (SOP 状态机)** + * **实现**:通过 JSON 定义的流程图(Check A \-\> Check B)。 + * **优势**:**确定性**。医疗流程不能随机应变,必须严格合规。状态机保证了 Agent 永远在轨道上运行。 +4. **Self-Correction Loop (自我修正回路)** + * **实现**:在 SoftRuleEngine 中捕获工具调用错误,反馈给 LLM 重试。 + * **优势**:大大提升了系统的鲁棒性,减少了因为 LLM "手滑" 导致的报错。 + +## **4\. 为什么这套架构更适合我们?(SWOT 分析)** + +| 维度 | Strengths (优势) | Weaknesses (劣势) | Opportunities (机会) | Threats (威胁) | +| :---- | :---- | :---- | :---- | :---- | +| **效率** | 开发极快,复用现有 Node.js 代码。 | 不支持跨语言调用(目前也不需要)。 | 快速迭代,最快速度上线 MVP。 | 随着业务极其复杂,单一 Service 类可能变大。 | +| **稳定性** | 链路极短,没有网络开销,没有新进程。 | 强耦合在 Node.js 生态内。 | 极低的出错率,适合医疗场景。 | 无。 | +| **成本** | **零新增成本**。不费服务器资源,不费 Token (Engine A)。 | 需自行维护 Service 代码。 | 将节省的 Token 成本转化为利润。 | 无。 | +| **扩展性** | 通过 JSON 配置即可支持新项目。 | 无法直接对接外部标准 MCP 工具。 | 形成行业壁垒:我们的 JSON 配置库就是核心资产。 | 如果未来 MCP 成为绝对主流,可能需要一层适配器。 | + +## **5\. 写给开发团队的话** + +兄弟们,我知道大家对新技术充满了热情,看到 "MCP"、"LangChain" 这样的词汇会觉得很兴奋。但在 IIT Manager 这个项目上,我们要**克制**。 + +1. **我们要的是“结果”,不是“概念”**。用户不在乎你用的是不是 MCP,他们只在乎\*\*“我问的问题能不能秒回”**,**“我的数据质控准不准”\*\*。 +2. **我们的护城河是“业务逻辑”,不是“胶水代码”**。把精力花在编写更精准的 iit\_skills 配置(JSON Logic \+ Prompt)上,这才是别人抄不走的壁垒。而 MCP 只是连接代码,谁都能写。 +3. **简单就是力量**。2 个人维护一套 Node.js \+ Postgres 系统,我们可以睡个安稳觉。如果引入了复杂的微服务和协议,我们的大部分时间都会花在修修补补上。 + +**现在的架构,是我们在有限资源下,能做出的最优雅、最务实、最强大的选择。** + +让我们基于这套 **V2.2 极简架构**,全速推进 Phase 2 的开发! \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/01-数据库设计.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/01-数据库设计.md new file mode 100644 index 00000000..f4897a80 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/01-数据库设计.md @@ -0,0 +1,436 @@ +# IIT Manager Agent 数据库设计 + +> **版本:** V2.9 +> **更新日期:** 2026-02-05 +> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md) +> +> **V2.9 更新**: +> - 扩展 `iit_skills` 表支持 Cron Skill(主动提醒) +> - 扩展 `iit_conversation_history` 表增加反馈字段 +> - 更新 `iit_project_memory` 内容结构(用户画像) + +--- + +## 1. 数据库表总览 + +| 表名 | 用途 | Phase | 优先级 | +|------|------|-------|--------| +| `iit_skills` | Skill 配置存储 | 1 | P0 | +| `iit_field_mapping` | 字段名映射字典 | 1 | P0 | +| `iit_task_run` | SOP 任务执行记录 | 2 | P0 | +| `iit_pending_actions` | 待处理的违规记录 | 2 | P0 | +| `iit_conversation_history` | 对话历史(流水账) | 2 | P1 | +| `iit_project_memory` | 项目级热记忆(Markdown) | 2 | P1 | +| `iit_weekly_reports` | 周报归档(历史书) | 4 | P1 | +| `iit_agent_trace` | ReAct 推理轨迹 | 5 | P2 | +| `iit_form_templates` | 表单模板(视觉识别) | 6 | 延后 | + +--- + +## 2. Phase 1:基础配置表 + +### 2.1 iit_skills - Skill 配置存储 + +```prisma +model IitSkill { + id String @id @default(uuid()) + projectId String // 绑定项目 + skillType String // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder + name String // 技能名称 + config Json // 核心配置 JSON(SOP 流程图) + isActive Boolean @default(true) + version Int @default(1) + + // V2.9 新增:主动触发能力 + triggerType String @default("webhook") // 'webhook' | 'cron' | 'event' + cronSchedule String? // Cron 表达式,如 "0 9 * * *" (每天9点) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([projectId, skillType]) + @@map("iit_skills") + @@schema("iit_schema") +} +``` + +**触发类型说明**: + +| triggerType | 触发方式 | 示例场景 | +|-------------|----------|----------| +| `webhook` | 用户消息触发(默认) | 质控任务、问答查询 | +| `cron` | 定时触发 | 访视提醒、周报生成 | +| `event` | 事件触发(预留) | AE 预警、数据变更通知 | + +**Skill 配置示例(SOP 流程图)**: + +```json +{ + "name": "肺癌研究入组质控", + "start_node": "baseline_check", + "nodes": { + "baseline_check": { + "type": "hard_rule", + "rules": [ + { "field": "age", "logic": { ">=": [{"var":"age"}, 18] }, "message": "年龄<18岁" }, + { "field": "age", "logic": { "<=": [{"var":"age"}, 75] }, "message": "年龄>75岁" }, + { "field": "ecog", "logic": { "<=": [{"var":"ecog"}, 2] }, "message": "ECOG>2" } + ], + "on_pass": "history_check", + "on_fail": "end_with_violation" + }, + "history_check": { + "type": "soft_instruction", + "instruction": "检查既往史,排除间质性肺炎、活动性感染、严重心血管疾病。", + "tools": ["read_clinical_data"], + "on_pass": "medication_check", + "on_fail": "end_review_required" + }, + "human_review": { + "type": "human_review", + "description": "需要 CRC 人工复核", + "on_approve": "end_success", + "on_reject": "end_rejected" + } + } +} +``` + +**Cron Skill 配置示例(V2.9 新增)**: + +```json +{ + "skillType": "visit_reminder", + "name": "每日访视提醒", + "triggerType": "cron", + "cronSchedule": "0 9 * * *", + "config": { + "start_node": "check_upcoming_visits", + "nodes": { + "check_upcoming_visits": { + "type": "soft_instruction", + "instruction": "查询未来 3 天内到期的访视,生成提醒列表", + "tools": ["read_clinical_data"], + "on_pass": "send_reminder", + "on_fail": "end_no_visits" + }, + "send_reminder": { + "type": "soft_instruction", + "instruction": "根据用户画像选择合适的通知方式和语气,发送提醒", + "tools": ["send_message"], + "on_pass": "end_success" + } + } + } +} +``` + +--- + +### 2.2 iit_field_mapping - 字段名映射字典 + +```prisma +model IitFieldMapping { + id String @id @default(uuid()) + projectId String // 项目级别映射 + aliasName String // LLM 可能传的名称(如 "gender", "性别") + actualName String // REDCap 实际字段名(如 "sex") + createdAt DateTime @default(now()) + + @@unique([projectId, aliasName]) + @@index([projectId]) + @@map("iit_field_mapping") + @@schema("iit_schema") +} +``` + +**初始化示例**: + +```sql +INSERT INTO iit_field_mapping (project_id, alias_name, actual_name) VALUES +('project-uuid', 'age', 'age_calculated'), +('project-uuid', '年龄', 'age_calculated'), +('project-uuid', 'ecog', 'ecog_score'), +('project-uuid', '既往史', 'medical_history_text'), +('project-uuid', 'history', 'medical_history_text'), +('project-uuid', '性别', 'sex'), +('project-uuid', 'gender', 'sex'); +``` + +--- + +## 3. Phase 2:SOP 执行与记忆表 + +### 3.1 iit_task_run - SOP 任务执行记录 + +```prisma +model IitTaskRun { + id String @id @default(uuid()) + projectId String + skillId String // 关联的 Skill + recordId String? // 关联的患者(如有) + triggeredBy String // 触发者 userId + status String // RUNNING | SUSPENDED | COMPLETED | FAILED + currentNode String? // 当前执行到的节点 + trace Json? // 执行轨迹 + resumeCallback String? // SUSPENDED 时,恢复后的下一步 + rejectCallback String? // SUSPENDED 被拒绝时的下一步 + suspendedAt DateTime? + resumedAt DateTime? + completedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([projectId, status]) + @@index([recordId]) + @@map("iit_task_run") + @@schema("iit_schema") +} +``` + +--- + +### 3.2 iit_pending_actions - 待处理的违规记录 + +```prisma +model IitPendingAction { + id String @id @default(uuid()) + projectId String + taskRunId String // 来源任务 + recordId String? // 关联患者 + actionType String // violation | warning | review_required + field String? // 违规字段 + message String // 违规描述 + severity String // error | warning | info + resolvedBy String? // 处理人 + resolvedAt DateTime? + resolution String? // 处理结论 + createdAt DateTime @default(now()) + + @@index([projectId, actionType]) + @@index([recordId]) + @@map("iit_pending_actions") + @@schema("iit_schema") +} +``` + +--- + +### 3.3 iit_conversation_history - 对话历史(流水账) + +> **V2.8 设计**:这是原始对话流水,不直接注入 Prompt,只用于生成周报 +> +> **V2.9 新增**:增加反馈字段,支持用户点赞/点踩 + +```prisma +model IitConversationHistory { + id String @id @default(uuid()) + projectId String + userId String + recordId String? // 关联的患者(如有) + role String // user | assistant + content String @db.Text + intent String? // 识别出的意图类型 + entities Json? // 提取的实体 { record_id, visit, ... } + + // V2.9 新增:反馈循环 + feedback String? // 'thumbs_up' | 'thumbs_down' | null + feedbackReason String? // 点踩原因:'too_long' | 'inaccurate' | 'unclear' + + createdAt DateTime @default(now()) + + @@index([projectId, userId]) + @@index([projectId, recordId]) + @@index([createdAt]) + @@map("iit_conversation_history") + @@schema("iit_schema") +} +``` + +--- + +### 3.4 iit_project_memory - 项目级热记忆 + +> **V2.8 核心表**:存储 Markdown 格式的热记忆,每次对话都注入 System Prompt +> +> **V2.9 扩展**:增加用户画像结构 + +```prisma +model IitProjectMemory { + id String @id @default(uuid()) + projectId String @unique + content String @db.Text // Markdown 格式的热记忆 + lastUpdatedBy String // 'system_daily_job' | 'admin_user_id' | 'profiler_job' + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("iit_project_memory") + @@schema("iit_schema") +} +``` + +**内容示例(V2.9 增强版)**: + +```markdown +# 用户画像 (User Profiles) + +## 张医生 (user_id: zhangyi) +- **角色**: PI +- **偏好**: 简洁汇报,只看结论,不要废话 +- **关注点**: AE、入组进度 +- **最佳通知时间**: 08:30 +- **禁令**: 回复不超过 100 字 +- **反馈统计**: 👍 12 / 👎 1 + +## 王护士 (user_id: wanghushi) +- **角色**: CRC +- **偏好**: 详细步骤,需要解释 +- **关注点**: 访视安排、数据录入 +- **最佳通知时间**: 09:00 + +# 当前状态 (Active Context) +- 当前阶段:入组冲刺期 +- 重点关注:P005 患者依从性差,需每日提醒 +- 本周目标:完成 3 例入组 +- P003 SAE 已判定为"可能无关"(2月5日 PI 决策) + +# 常见问题 (FAQ) +- 访视窗口:V1 Day 1±3, V2 Day 28±7 + +# 系统禁令 (Rules) +- 严禁在未授权情况下删除数据 +- 写操作必须经过人工确认 +``` + +--- + +## 4. Phase 4:周报归档 + +### 4.1 iit_weekly_reports - 周报归档(历史书) + +> **V2.8 核心表**:存储每周的关键决策、进度、踩坑记录 + +```prisma +model IitWeeklyReport { + id String @id @default(uuid()) + projectId String + weekNumber Int // 第几周(从项目开始计算) + weekStart DateTime // 周起始日期 + weekEnd DateTime // 周结束日期 + summary String @db.Text // Markdown 格式的周报内容 + metrics Json? // 结构化指标 { enrolled: 3, queries: 5, ... } + createdBy String // 'system_scheduler' | 'admin_user_id' + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([projectId, weekNumber]) + @@index([projectId]) + @@map("iit_weekly_reports") + @@schema("iit_schema") +} +``` + +**内容示例**: + +```markdown +## Week 5 (2026-02-03 ~ 2026-02-09) + +### 进度 +- 本周新入组:3 例 +- 累计入组:15 / 30 例 +- 完成率:50% + +### 关键决策 +- 2026-02-05: PI 会议决定放宽入排标准,允许 ECOG 3 分患者入组 +- 2026-02-07: 确认 P003 AE 与研究药物"可能无关" + +### 踩坑记录 +- 曾尝试自动录入化验单,因 OCR 精度不足失败,已回退为人工复核 + +### 待办 +- 清理 1 月份遗留的 Query +- 准备 2 月底期中分析数据 +``` + +--- + +## 5. Phase 5:ReAct 追踪表 + +### 5.1 iit_agent_trace - ReAct 推理轨迹 + +> 用于调试,不发送给用户,仅在 Admin 后台查看 + +```prisma +model IitAgentTrace { + id String @id @default(uuid()) + projectId String + userId String + query String @db.Text // 用户原始问题 + intentType String? // 识别的意图类型 + trace Json // ReAct 的完整思考过程 + tokenUsage Int? // 消耗的 Token 数 + duration Int? // 执行时长(ms) + success Boolean + createdAt DateTime @default(now()) + + @@index([projectId, createdAt]) + @@index([userId]) + @@map("iit_agent_trace") + @@schema("iit_schema") +} +``` + +--- + +## 6. Phase 6:视觉能力表(延后到 V3.0) + +### 6.1 iit_form_templates - 表单模板 + +```prisma +model IitFormTemplate { + id String @id @default(uuid()) + projectId String + formName String // REDCap 表单名称 + fieldSchema Json // 表单字段结构 + keywords String[] // 用于匹配的关键词 + createdAt DateTime @default(now()) + + @@map("iit_form_templates") + @@schema("iit_schema") +} +``` + +--- + +## 7. 数据库迁移命令 + +```bash +# 生成迁移 +npx prisma db push + +# 生成客户端 +npx prisma generate + +# 查看表结构 +npx prisma studio +``` + +--- + +## 8. 索引设计总结 + +| 表 | 索引 | 用途 | +|----|------|------| +| `iit_skills` | `[projectId, skillType]` (unique) | 按项目查询 Skill | +| `iit_field_mapping` | `[projectId, aliasName]` (unique) | 字段映射查询 | +| `iit_task_run` | `[projectId, status]` | 查询运行中/挂起的任务 | +| `iit_conversation_history` | `[projectId, userId]` | 按用户查询对话 | +| `iit_conversation_history` | `[projectId, recordId]` | 按患者查询对话 | +| `iit_conversation_history` | `[createdAt]` | 按时间范围查询 | +| `iit_weekly_reports` | `[projectId, weekNumber]` (unique) | 按周查询报告 | +| `iit_agent_trace` | `[projectId, createdAt]` | 按时间查询调试日志 | + +--- + +**文档维护人**:AI Agent +**最后更新**:2026-02-05 diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md new file mode 100644 index 00000000..0fbc82c7 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md @@ -0,0 +1,731 @@ +# IIT Manager Agent 核心引擎实现指南 + +> **版本:** V2.9 +> **更新日期:** 2026-02-05 +> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md) +> +> **V2.9 更新**:ReActEngine Prompt 优化,支持多意图处理(Chain of Thought) + +--- + +## 1. 引擎总览 + +| 引擎 | 职责 | 执行方式 | Phase | +|------|------|----------|-------| +| `HardRuleEngine` | 执行硬规则(JSON Logic) | CPU 执行,无 LLM | 1 | +| `SoftRuleEngine` | 执行软指令(LLM 推理) | LLM + 工具调用 | 2 | +| `SopEngine` | SOP 状态机调度 | 编排 Hard/Soft 引擎 | 2 | +| `ReActEngine` | 多步推理(思考-行动-观察) | LLM + 循环推理 | 5 | + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ SopEngine (状态机) │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ 节点A: HardRuleEngine → 节点B: SoftRuleEngine → 节点C: ... │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ToolsService (工具层) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. HardRuleEngine - 硬规则引擎 + +> **文件路径**: `backend/src/modules/iit-manager/engines/HardRuleEngine.ts` + +### 2.1 核心职责 + +- 执行 JSON Logic 规则,无需 LLM +- 返回结构化的违规列表 +- 执行时间 < 100ms + +### 2.2 完整实现 + +```typescript +import jsonLogic from 'json-logic-js'; + +export interface HardRule { + field: string; + logic: object; // JSON Logic 表达式 + message: string; + severity?: 'error' | 'warning' | 'info'; +} + +export interface RuleResult { + field: string; + message: string; + severity: string; + value?: any; +} + +export class HardRuleEngine { + /** + * 执行硬规则检查 + * @param rules 规则数组 + * @param data 待检查的数据 + * @returns 违规列表(空数组表示全部通过) + */ + run(rules: HardRule[], data: Record): RuleResult[] { + const errors: RuleResult[] = []; + + for (const rule of rules) { + try { + const passed = jsonLogic.apply(rule.logic, data); + + if (!passed) { + errors.push({ + field: rule.field, + message: rule.message, + severity: rule.severity || 'error', + value: data[rule.field] + }); + } + } catch (error) { + // 规则执行异常,记录但不中断 + errors.push({ + field: rule.field, + message: `规则执行异常: ${error.message}`, + severity: 'error' + }); + } + } + + return errors; + } + + /** + * 检查是否全部通过 + */ + isAllPassed(rules: HardRule[], data: Record): boolean { + return this.run(rules, data).length === 0; + } +} +``` + +### 2.3 JSON Logic 常用表达式 + +```javascript +// 范围检查 +{ "and": [ + { ">=": [{ "var": "age" }, 18] }, + { "<=": [{ "var": "age" }, 75] } +]} + +// 非空检查 +{ "!!": { "var": "informed_consent_date" } } + +// 日期比较(需自定义操作符) +{ "<=": [{ "var": "icf_date" }, { "var": "enrollment_date" }] } + +// 枚举值检查 +{ "in": [{ "var": "ecog" }, [0, 1, 2]] } + +// 字符串包含 +{ "in": ["肺炎", { "var": "medical_history" }] } +``` + +--- + +## 3. SoftRuleEngine - 软规则引擎 + +> **文件路径**: `backend/src/modules/iit-manager/engines/SoftRuleEngine.ts` + +### 3.1 核心职责 + +- 执行需要 LLM 推理的软指令 +- 支持工具调用 +- 实现自我修正回路(最多 3 次重试) + +### 3.2 完整实现 + +```typescript +import { LLMFactory } from '../../common/llm/adapters/LLMFactory'; +import { ToolsService } from '../services/ToolsService'; + +export interface SoftRuleResult { + passed: boolean; + reason: string; + confidence?: number; + evidence?: any; +} + +export class SoftRuleEngine { + private llm = LLMFactory.create('qwen'); + private tools: ToolsService; + + constructor(tools: ToolsService) { + this.tools = tools; + } + + /** + * 执行软指令,带自我修正回路 + */ + async runWithRetry( + instruction: string, + context: any, + maxRetries: number = 3 + ): Promise { + const systemPrompt = `你是一个临床研究质控专家。请根据以下指令和数据进行判断。 + +指令: ${instruction} + +请返回 JSON 格式: +{ + "passed": true/false, + "reason": "判断理由", + "confidence": 0.0-1.0, + "evidence": "支持判断的证据" +}`; + + let history: Message[] = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: `数据: ${JSON.stringify(context)}` } + ]; + + for (let i = 0; i < maxRetries; i++) { + const response = await this.llm.chat(history, { + tools: this.tools.getToolDefinitions() + }); + + // AI 决定调用工具 + if (response.toolCalls && response.toolCalls.length > 0) { + for (const toolCall of response.toolCalls) { + try { + const result = await this.tools.executeTool( + toolCall.name, + toolCall.args + ); + history.push({ + role: 'tool', + toolCallId: toolCall.id, + content: JSON.stringify(result) + }); + } catch (error) { + // ⚠️ 自我修正:告诉 LLM 它错了 + history.push({ + role: 'user', + content: `工具调用失败: ${error.message}。请检查参数并重试,或直接根据已有信息判断。` + }); + continue; + } + } + } else { + // AI 返回最终答案 + return this.parseResult(response.content); + } + } + + return { + passed: false, + reason: '多次重试后仍失败,需人工复核', + confidence: 0 + }; + } + + private parseResult(content: string): SoftRuleResult { + try { + // 提取 JSON(可能被 Markdown 包裹) + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + return JSON.parse(jsonMatch[0]); + } + } catch (e) { + // 解析失败 + } + + // 回退:尝试从文本推断 + const passed = content.includes('通过') || content.includes('符合'); + return { + passed, + reason: content.slice(0, 200), + confidence: 0.5 + }; + } +} +``` + +--- + +## 4. SopEngine - SOP 状态机引擎 + +> **文件路径**: `backend/src/modules/iit-manager/engines/SopEngine.ts` + +### 4.1 核心职责 + +- 解析 Skill 配置中的 SOP 流程图 +- 按顺序执行节点(Hard → Soft → Human Review) +- 实现状态持久化(防止服务重启丢失进度) +- 实现 SUSPENDED 挂起机制(解决"死锁"风险) + +### 4.2 类型定义 + +```typescript +interface SopConfig { + name: string; + start_node: string; + nodes: { + [nodeId: string]: SopNode; + }; +} + +interface SopNode { + type: 'hard_rule' | 'soft_instruction' | 'human_review'; + // hard_rule 类型 + rules?: HardRule[]; + // soft_instruction 类型 + instruction?: string; + tools?: string[]; + // human_review 类型 + description?: string; + // 流转 + on_pass: string; + on_fail: string; + on_approve?: string; // human_review 专用 + on_reject?: string; // human_review 专用 + on_error?: string; +} + +interface SopResult { + status: 'COMPLETED' | 'SUSPENDED' | 'FAILED'; + finalState: string; + trace: TraceItem[]; + taskRunId?: string; + message?: string; +} + +interface TraceItem { + node: string; + timestamp: Date; + result?: any; +} +``` + +### 4.3 完整实现 + +```typescript +import PgBoss from 'pg-boss'; +import { prisma } from '../../common/prisma'; +import { HardRuleEngine } from './HardRuleEngine'; +import { SoftRuleEngine } from './SoftRuleEngine'; + +export class SopEngine { + private hardEngine: HardRuleEngine; + private softEngine: SoftRuleEngine; + private pgBoss: PgBoss; + + constructor( + hardEngine: HardRuleEngine, + softEngine: SoftRuleEngine, + pgBoss: PgBoss + ) { + this.hardEngine = hardEngine; + this.softEngine = softEngine; + this.pgBoss = pgBoss; + } + + /** + * 执行 SOP 流程 + */ + async run( + skillConfig: SopConfig, + data: any, + taskRunId?: string + ): Promise { + // 创建或恢复任务记录 + const task = taskRunId + ? await this.resumeTask(taskRunId) + : await this.createTask(skillConfig, data); + + let currentNodeId = task.currentNode || skillConfig.start_node; + let context = { ...data }; + const trace: TraceItem[] = task.trace ? JSON.parse(task.trace) : []; + + while (currentNodeId && !currentNodeId.startsWith('end')) { + const node = skillConfig.nodes[currentNodeId]; + trace.push({ node: currentNodeId, timestamp: new Date() }); + + // ⚠️ 每步持久化,防止服务重启丢失进度 + await prisma.iitTaskRun.update({ + where: { id: task.id }, + data: { + currentNode: currentNodeId, + trace: JSON.stringify(trace), + updatedAt: new Date() + } + }); + + // 检查是否需要人工介入 + if (node.type === 'human_review') { + return await this.handleHumanReview(task.id, node, data, trace); + } + + // 执行节点 + let result: NodeResult; + try { + if (node.type === 'hard_rule') { + const errors = this.hardEngine.run(node.rules || [], context); + result = { passed: errors.length === 0, errors }; + } else { + result = await this.softEngine.runWithRetry( + node.instruction || '', + context + ); + } + } catch (error) { + // 执行异常,走 on_error 分支 + currentNodeId = node.on_error || 'end_error'; + continue; + } + + // 记录违规 + if (!result.passed) { + await this.savePendingAction(task.id, data.recordId, result); + } + + // 状态流转 + currentNodeId = result.passed ? node.on_pass : node.on_fail; + } + + // 完成任务 + await prisma.iitTaskRun.update({ + where: { id: task.id }, + data: { + status: 'COMPLETED', + currentNode: currentNodeId, + completedAt: new Date() + } + }); + + return { + status: 'COMPLETED', + finalState: currentNodeId, + trace + }; + } + + /** + * ⚠️ 处理人工复核节点 - SUSPENDED 挂起机制 + */ + private async handleHumanReview( + taskRunId: string, + node: SopNode, + data: any, + trace: TraceItem[] + ): Promise { + // 挂起任务,不再占用 Worker + await prisma.iitTaskRun.update({ + where: { id: taskRunId }, + data: { + status: 'SUSPENDED', + suspendedAt: new Date(), + resumeCallback: node.on_approve, + rejectCallback: node.on_reject || 'end_rejected' + } + }); + + // 通知相关人员 + await this.notifyReviewRequired(data, node); + + return { + status: 'SUSPENDED', + finalState: 'human_review', + trace, + taskRunId, + message: '任务已挂起,等待人工复核' + }; + } + + /** + * ⚠️ 唤醒机制 - CRC 确认后继续执行 + */ + async resumeFromSuspended( + taskRunId: string, + decision: 'approve' | 'reject' + ): Promise { + const task = await prisma.iitTaskRun.findUnique({ + where: { id: taskRunId } + }); + + if (!task || task.status !== 'SUSPENDED') { + throw new Error('任务不在挂起状态'); + } + + const nextNode = decision === 'approve' + ? task.resumeCallback + : task.rejectCallback || 'end_rejected'; + + // 更新状态 + await prisma.iitTaskRun.update({ + where: { id: taskRunId }, + data: { + status: 'RUNNING', + currentNode: nextNode, + resumedAt: new Date() + } + }); + + // 创建新的 Job 继续执行 + await this.pgBoss.send('sop-continue', { taskRunId }); + } + + // ... 辅助方法省略 +} +``` + +--- + +## 5. ReActEngine - 多步推理引擎 + +> **文件路径**: `backend/src/modules/iit-manager/engines/ReActEngine.ts` + +### 5.1 核心职责 + +- 处理模糊查询,支持多步推理 +- 循环执行"思考 → 行动 → 观察" +- **只允许调用只读工具**(安全约束) +- **只返回 Final Answer**(解决"多嘴"问题) +- **V2.9 新增**:支持多意图处理(用户一句话包含多个任务) + +### 5.2 安全约束 + +```typescript +// 工具白名单:ReAct 只能调用只读工具 +private readonly READONLY_TOOLS = [ + 'read_clinical_data', + 'search_protocol', + 'get_project_stats', + 'check_visit_window' +]; + +// 资源限制 +private maxIterations = 5; // 最大迭代次数 +private maxTokens = 4000; // Token 预算 +``` + +### 5.3 完整实现 + +```typescript +import { LLMFactory } from '../../common/llm/adapters/LLMFactory'; +import { ToolsService } from '../services/ToolsService'; +import { prisma } from '../../common/prisma'; + +export interface ReActResult { + success: boolean; + content: string; + trace: TraceItem[]; +} + +interface TraceItem { + iteration: number; + thought?: string; + action?: string; + observation?: string; +} + +export class ReActEngine { + private llm = LLMFactory.create('qwen'); + private tools: ToolsService; + + private readonly READONLY_TOOLS = [ + 'read_clinical_data', + 'search_protocol', + 'get_project_stats', + 'check_visit_window' + ]; + + private maxIterations = 5; + private maxTokens = 4000; + private tokenCounter = 0; + + constructor(tools: ToolsService) { + this.tools = tools; + } + + async run(query: string, context: AgentContext): Promise { + this.tokenCounter = 0; + + // V2.9 优化:支持多意图处理的 Prompt + const systemPrompt = `你是一个临床研究智能助手。你可以使用以下工具回答问题: +${this.formatToolDescriptions(this.READONLY_TOOLS)} + +请按照 ReAct 模式思考: +1. 思考:分析问题,决定下一步 +2. 行动:调用工具获取信息 +3. 观察:查看工具返回结果 +4. 重复以上步骤,直到能回答用户问题 + +## 多任务处理原则 (V2.9) + +当用户的消息包含多个任务时,请遵循以下步骤: + +**Step 1: 意图拆解** +- 识别用户消息中的所有独立意图 +- 按依赖关系排序:先处理前置任务 +- 示例:"帮我查一下 P003 的 ECOG,然后录入今天的访视数据" + → 意图1: 查询 P003 ECOG(只读) + → 意图2: 录入访视数据(需要引导) + +**Step 2: 顺序执行** +- 每次只聚焦处理一个意图 +- 完成后明确告知用户,并继续下一个 +- 遇到写操作,暂停并引导用户确认 + +**Step 3: 统一回复** +- 在最终回复中按顺序总结所有任务结果 +- 格式: + 1. [任务1] 已完成 / 结果:... + 2. [任务2] 需要确认 / 引导操作 + +注意:你只能查询数据,不能修改数据。如果用户需要修改操作,请引导他们使用正式的质控流程。`; + + let messages: Message[] = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: query } + ]; + + const trace: TraceItem[] = []; + + for (let i = 0; i < this.maxIterations; i++) { + // Token 预算检查 + if (this.tokenCounter > this.maxTokens) { + return { + success: false, + content: '抱歉,这个问题比较复杂,请尝试更具体的描述。', + trace + }; + } + + const response = await this.llm.chat(messages, { + tools: this.getReadonlyToolDefinitions() + }); + + this.tokenCounter += response.usage?.total_tokens || 0; + trace.push({ iteration: i, thought: response.content }); + + // AI 决定结束,返回 Final Answer + if (!response.toolCalls || response.toolCalls.length === 0) { + return { success: true, content: response.content, trace }; + } + + // 执行工具调用 + let errorCount = 0; + for (const toolCall of response.toolCalls) { + // ⚠️ 安全检查:只允许只读工具 + if (!this.READONLY_TOOLS.includes(toolCall.name)) { + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: `错误:工具 ${toolCall.name} 不在允许列表中。你只能使用只读工具查询数据。如果需要修改数据,请引导用户使用"质控"命令。` + }); + errorCount++; + continue; + } + + try { + const result = await this.tools.executeTool(toolCall.name, toolCall.args); + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: JSON.stringify(result) + }); + trace[i].observation = JSON.stringify(result).slice(0, 200); + } catch (error) { + errorCount++; + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: `工具调用失败: ${error.message}` + }); + } + } + + // ⚠️ 幻觉熔断:连续 2 次工具调用失败 + if (errorCount >= 2) { + return { + success: false, + content: '抱歉,我遇到了一些技术问题,无法获取相关信息。请稍后重试或联系管理员。', + trace + }; + } + } + + return { + success: false, + content: '抱歉,我无法在有限步骤内完成这个查询。请尝试更具体的问题。', + trace + }; + } + + /** + * ⚠️ 保存 Trace 到数据库(仅供 Admin 调试) + */ + async saveTrace( + projectId: string, + userId: string, + query: string, + result: ReActResult + ): Promise { + await prisma.iitAgentTrace.create({ + data: { + projectId, + userId, + query, + trace: result.trace, + tokenUsage: this.tokenCounter, + success: result.success, + createdAt: new Date() + } + }); + } + + private getReadonlyToolDefinitions() { + return this.tools.getToolDefinitions() + .filter(t => this.READONLY_TOOLS.includes(t.name)); + } + + private formatToolDescriptions(toolNames: string[]): string { + return toolNames.map(name => { + const def = this.tools.getToolDefinition(name); + return `- ${name}: ${def?.description || ''}`; + }).join('\n'); + } +} +``` + +--- + +## 6. 引擎集成示意 + +```typescript +// 初始化 +const hardEngine = new HardRuleEngine(); +const toolsService = new ToolsService(redcapAdapter, difyClient); +const softEngine = new SoftRuleEngine(toolsService); +const sopEngine = new SopEngine(hardEngine, softEngine, pgBoss); +const reactEngine = new ReActEngine(toolsService); + +// 在 ChatService 中使用 +async handleMessage(userId: string, message: string) { + const intent = await this.intentService.detect(message); + + if (intent.type === 'QC_TASK') { + // 走 SOP 引擎 + return await this.sopEngine.run(skillConfig, data); + } else if (intent.type === 'QA_QUERY') { + // 走 ReAct 引擎 + const result = await this.reactEngine.run(message, context); + // ⚠️ 只返回 Final Answer,不返回中间思考 + await this.reactEngine.saveTrace(projectId, userId, message, result); + return result.content; + } +} +``` + +--- + +**文档维护人**:AI Agent +**最后更新**:2026-02-05 diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md new file mode 100644 index 00000000..3f3baac3 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md @@ -0,0 +1,1123 @@ +# 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 diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md new file mode 100644 index 00000000..4da17e1d --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md @@ -0,0 +1,552 @@ +# 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 { + 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 { + const queryEmbedding = await this.embeddings.embed(query); + + const results = await prisma.$queryRaw` + 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 { + 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 { + 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 { + // 从最近的对话中提取关键信息更新热记忆 + 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 { + 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 { + const queryEmbedding = await this.embeddings.embed(query); + + return prisma.$queryRaw` + 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 { + 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 { + 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 { + 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 { + const cached = await this.redis.get(`hot_memory:${projectId}`); + return cached ? JSON.parse(cached) : null; + } + + async setHotMemory(projectId: string, memory: HotMemory): Promise { + 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 { + 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 diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/05-开发阶段与任务清单.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/05-开发阶段与任务清单.md new file mode 100644 index 00000000..001c6ec1 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/05-开发阶段与任务清单.md @@ -0,0 +1,281 @@ +# IIT Manager Agent 开发阶段与任务清单 + +> **版本:** V2.9 +> **更新日期:** 2026-02-05 +> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md) +> +> **V2.9 更新**: +> - Phase 3 新增反馈循环任务 +> - Phase 4 新增 ProfilerService 和 Cron Skill 任务 +> - Phase 5 新增多意图处理任务 + +--- + +## 1. 开发阶段总览 + +``` +Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6 +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 基础工具层 │ ──▶ │ SOP 引擎 │ ──▶ │ ReAct 引擎 │ ──▶ │ 调度系统 │ ──▶ │ 智能路由 │ ──▶ │ 视觉能力 │ +│ │ │ + 记忆L2 │ │ + 记忆L1 │ │ + 记忆L3 │ │ │ │ (延后) │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + ▼ ▼ ▼ ▼ ▼ ▼ + ToolsService SopEngine ReActEngine SchedulerService IntentService VisionService + FieldMapping HotMemory FlowMemory WeeklyReports MixedRouting (Postponed) + HardRuleEngine SoftRuleEngine AgentTrace ReportService StreamingFB +``` + +--- + +## 2. Phase 1: 基础工具层 + +### 2.1 目标 + +- 搭建可复用的工具框架 +- 实现字段映射机制 +- 建立硬规则引擎 + +### 2.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P1-01 | 创建 `iit_skills` 表 | 高 | 待开始 | - | +| P1-02 | 创建 `iit_field_mapping` 表 | 高 | 待开始 | - | +| P1-03 | 实现 `ToolsService` | 高 | 待开始 | P1-01 | +| P1-04 | 实现 `read_clinical_data` 工具 | 高 | 待开始 | P1-03 | +| P1-05 | 实现 `search_protocol` 工具 | 高 | 待开始 | P1-03 | +| P1-06 | 实现 `HardRuleEngine` | 高 | 待开始 | - | +| P1-07 | 集成字段映射到 ToolsService | 中 | 待开始 | P1-02, P1-03 | +| P1-08 | 单元测试覆盖 | 中 | 待开始 | P1-01~P1-07 | + +### 2.3 验收标准 + +- [ ] 工具可通过名称调用 +- [ ] 字段映射正确生效(LLM 用 "年龄" → 实际调用 "dem_age") +- [ ] 硬规则拦截生效 +- [ ] 测试覆盖率 > 80% + +--- + +## 3. Phase 2: SOP 引擎 + 热记忆 + +### 3.1 目标 + +- 实现状态机驱动的 SOP 执行 +- 搭建热记忆层(L2) +- 支持人工确认机制 + +### 3.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P2-01 | 创建 `iit_task_run` 表 | 高 | 待开始 | - | +| P2-02 | 创建 `iit_pending_actions` 表 | 高 | 待开始 | - | +| P2-03 | 创建 `iit_project_memory` 表 | 高 | 待开始 | - | +| P2-04 | 实现 `SopEngine` 核心状态机 | 高 | 待开始 | P2-01 | +| P2-05 | 实现 `SoftRuleEngine` | 高 | 待开始 | - | +| P2-06 | 集成 SoftRuleEngine 到 SOP | 高 | 待开始 | P2-04, P2-05 | +| P2-07 | 实现 SUSPENDED 状态机制 | 高 | 待开始 | P2-04 | +| P2-08 | 实现人工确认流程 | 高 | 待开始 | P2-02, P2-07 | +| P2-09 | 实现 `MemoryService` 热记忆 | 中 | 待开始 | P2-03 | +| P2-10 | 集成热记忆到 SOP 上下文 | 中 | 待开始 | P2-04, P2-09 | +| P2-11 | 端到端测试:质控 SOP | 中 | 待开始 | P2-01~P2-10 | + +### 3.3 验收标准 + +- [ ] 质控任务可自动执行完整 SOP +- [ ] 写操作正确等待人工确认 +- [ ] SUSPENDED 状态正确持久化 +- [ ] 热记忆正确注入 SOP 上下文 +- [ ] 手动恢复执行成功 + +--- + +## 4. Phase 3: ReAct 引擎 + 流水账 + +### 4.1 目标 + +- 实现 ReAct 模式的灵活查询 +- 搭建流水账层(L1) +- 实现只读安全约束 + +### 4.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P3-01 | 创建 `iit_conversation_history` 表 | 高 | 待开始 | - | +| P3-02 | 创建 `iit_agent_trace` 表 | 中 | 待开始 | - | +| P3-03 | 实现 `ReActEngine` 核心循环 | 高 | 待开始 | P1-03 | +| P3-04 | 实现只读工具白名单 | 高 | 待开始 | P3-03 | +| P3-05 | 实现 `MemoryService` 流水账 | 高 | 待开始 | P3-01 | +| P3-06 | 实现向量化存储(pgvector) | 中 | 待开始 | P3-05 | +| P3-07 | 实现相似度检索 | 中 | 待开始 | P3-06 | +| P3-08 | 实现 Trace 记录机制 | 中 | 待开始 | P3-02, P3-03 | +| P3-09 | 实现流式反馈机制 | 高 | 待开始 | P3-03 | +| P3-10 | 实现 "正在思考" 状态提示 | 中 | 待开始 | P3-09 | +| P3-11 | 集成流水账到 ReAct 上下文 | 中 | 待开始 | P3-03, P3-05 | +| P3-12 | **[V2.9]** 扩展对话表支持反馈字段 | 中 | 待开始 | P3-01 | +| P3-13 | **[V2.9]** 实现反馈收集接口 | 中 | 待开始 | P3-12 | +| P3-14 | 端到端测试:模糊查询 | 中 | 待开始 | P3-01~P3-13 | + +### 4.3 验收标准 + +- [ ] ReAct 可正确推理并调用工具 +- [ ] 只读约束生效(无法调用写入工具) +- [ ] 流水账正确存储和检索 +- [ ] Trace 记录可供调试 +- [ ] 流式反馈 < 2秒首字节 +- [ ] "正在思考" 状态正确显示 +- [ ] **[V2.9]** 反馈按钮可正确收集用户反馈 + +--- + +## 5. Phase 4: 调度系统 + 历史书 + +### 5.1 目标 + +- 实现定时任务调度 +- 实现周报生成 +- 搭建历史书层(L3) + +### 5.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P4-01 | 创建 `iit_weekly_reports` 表 | 高 | 待开始 | - | +| P4-02 | 实现 `SchedulerService`(pg-boss) | 高 | 待开始 | - | +| P4-03 | 实现 `ReportService` | 高 | 待开始 | P4-01 | +| P4-04 | 实现周报自动生成 | 高 | 待开始 | P4-02, P4-03 | +| P4-05 | 实现记忆卷叠机制 | 中 | 待开始 | P4-01, P3-05 | +| P4-06 | 实现历史书检索 | 中 | 待开始 | P4-01 | +| P4-07 | 集成历史书到上下文组装 | 中 | 待开始 | P4-06 | +| P4-08 | **[V2.9]** 实现 `ProfilerService` | 中 | 待开始 | P2-03 | +| P4-09 | **[V2.9]** 扩展 Skill 表支持 Cron 触发 | 中 | 待开始 | P1-01 | +| P4-10 | **[V2.9]** 实现 Cron Skill 调度 | 中 | 待开始 | P4-02, P4-09 | +| P4-11 | **[V2.9]** 实现访视提醒 Skill | 中 | 待开始 | P4-10 | +| P4-12 | **[V2.9]** 集成用户画像到通知个性化 | 低 | 待开始 | P4-08, P4-11 | +| P4-13 | 端到端测试:周报生成 | 中 | 待开始 | P4-01~P4-12 | + +### 5.3 验收标准 + +- [ ] 周报每周一自动生成 +- [ ] 记忆卷叠每日自动执行 +- [ ] 历史书检索正确召回 +- [ ] **[V2.9]** 用户画像正确存储在 project_memory +- [ ] **[V2.9]** Cron Skill 按时触发 +- [ ] **[V2.9]** 访视提醒正确发送给目标用户 + +--- + +## 6. Phase 5: 智能路由 + +### 6.1 目标 + +- 实现意图识别服务 +- 实现混合路由(正则 + LLM) +- 实现追问机制 + +### 6.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P5-01 | 实现 `IntentService` | 高 | 待开始 | - | +| P5-02 | 实现正则快速通道 | 高 | 待开始 | P5-01 | +| P5-03 | 实现 LLM 意图识别 | 高 | 待开始 | P5-01 | +| P5-04 | 实现降级策略 | 中 | 待开始 | P5-01~P5-03 | +| P5-05 | 实现追问机制 | 中 | 待开始 | P5-01 | +| P5-06 | 扩展 `ChatService` 集成路由 | 高 | 待开始 | P5-01~P5-05 | +| P5-07 | **[V2.9]** 优化 ReAct Prompt 支持多意图 | 中 | 待开始 | P3-03 | +| P5-08 | 端到端测试:路由分发 | 中 | 待开始 | P5-01~P5-07 | + +### 6.3 验收标准 + +- [ ] 简单指令 < 50ms 命中快速通道 +- [ ] 复杂句子正确识别意图 +- [ ] UNCLEAR 情况正确追问 +- [ ] LLM 不可用时正确降级 +- [ ] **[V2.9]** 多意图消息正确拆分并顺序执行 + +--- + +## 7. Phase 6: 视觉能力(延后) + +> ⚠️ **注意**:根据风险评估,视觉能力延后到核心功能稳定后再开发。 + +### 7.1 目标 + +- 实现图片识别能力 +- 支持知情同意书识别 +- 支持 CRF 扫描件识别 + +### 7.2 任务清单 + +| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 | +|--------|----------|--------|------|----------| +| P6-01 | 评估 GPT-4V / 通义千问-VL | 低 | 延后 | P1~P5 完成 | +| P6-02 | 实现 `VisionService` | 低 | 延后 | P6-01 | +| P6-03 | 集成到 ChatService | 低 | 延后 | P6-02 | +| P6-04 | 端到端测试 | 低 | 延后 | P6-03 | + +### 7.3 延后原因 + +1. 核心功能优先级更高 +2. 视觉能力成本较高 +3. 需要更多真实场景验证 + +--- + +## 8. 里程碑与依赖关系 + +```mermaid +gantt + title IIT Manager Agent 开发里程碑 + dateFormat YYYY-MM-DD + section Phase 1 + 基础工具层 :p1, 2026-02-10, 14d + section Phase 2 + SOP 引擎 + 热记忆 :p2, after p1, 21d + section Phase 3 + ReAct 引擎 + 流水账 :p3, after p2, 14d + section Phase 4 + 调度系统 + 历史书 :p4, after p3, 14d + section Phase 5 + 智能路由 :p5, after p4, 7d + section Phase 6 + 视觉能力 :p6, after p5, 14d +``` + +--- + +## 9. 风险与对策 + +| 风险 | 影响 | 对策 | 已整合 | +|------|------|------|--------| +| ReAct 决策链过长 | 延迟 > 10秒 | 流式反馈 + "正在思考" 状态 | ✅ | +| 混合意图难分类 | 用户困惑 | UNCLEAR + 追问机制 | ✅ | +| ReAct 误调写入工具 | 数据风险 | 只读工具白名单 | ✅ | +| UI 无响应感 | 体验差 | 流式反馈 + 状态提示 | ✅ | +| SOP 中途被打断 | 任务丢失 | SUSPENDED 状态 + 恢复机制 | ✅ | +| 视觉能力分散精力 | 核心功能延迟 | 延后到 Phase 6 | ✅ | +| **[V2.9]** 用户多意图混乱 | 任务遗漏 | ReAct Prompt 多意图拆分 | ✅ | +| **[V2.9]** 回复不符用户偏好 | 体验差 | 反馈循环 + 用户画像 | ✅ | +| **[V2.9]** 主动提醒打扰用户 | 用户投诉 | 最佳通知时间 + 个性化 | ✅ | + +--- + +## 10. 性能指标 + +| 指标 | 目标值 | 测量方法 | +|------|--------|----------| +| 快速通道响应 | < 50ms | 正则匹配耗时 | +| LLM 意图识别 | < 1s | API 调用耗时 | +| SOP 单步执行 | < 2s | 包含工具调用 | +| ReAct 完整推理 | < 10s | 最多 5 轮循环 | +| 流式首字节 | < 2s | 第一个 token | +| 周报生成 | < 30s | 后台任务 | +| 向量检索 | < 500ms | Top-5 结果 | + +--- + +**文档维护人**:AI Agent +**最后更新**:2026-02-05 diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.5 综合开发计划.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.5 综合开发计划.md new file mode 100644 index 00000000..92b5622e --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.5 综合开发计划.md @@ -0,0 +1,1438 @@ +# IIT Manager Agent V2.6 综合开发计划 + +> **版本:** V2.6(极简架构 + SOP状态机 + 双脑路由) +> **日期:** 2026-02-02 +> **团队规模:** 2人 +> **预估周期:** 6周 +> **核心目标:** 实现数据质控 Agent 的完整闭环 + 智能化交互 + 扩展能力层 + +--- + +## 0. 架构适配性评估 + +本架构设计充分考虑了临床研究的多种业务场景,通过 **SOP状态机 + 双引擎 + 可扩展工具层** 的设计,实现了良好的适配性和扩展性。 + +### 0.1 目标场景覆盖度 + +| 场景 | 覆盖度 | 核心支撑组件 | 备注 | +|------|--------|-------------|------| +| **1. 拍照识别 + 自动录入** | 🟡 60% | VisionService + ToolsService | 需新增视觉能力 | +| **2. 数据质控** | 🟢 95% | HardRuleEngine + SoftRuleEngine | 核心场景,完全覆盖 | +| **3. 入排标准判断** | 🟢 90% | SopEngine + 硬规则配置 | 配置 Skill 即可 | +| **4. 方案偏离检测** | 🟢 80% | SoftRuleEngine + search_protocol | 需配置访视窗口规则 | +| **5. AE事件检测** | 🟢 80% | 硬规则触发 + 软指令评估 | 需配置AE识别规则 | +| **6. 伦理合规检测** | 🟢 80% | HardRuleEngine | 配置伦理规则即可 | +| **7. 定期报告生成** | 🟡 50% | SchedulerService + ReportService | 需新增定时任务 | + +### 0.2 架构扩展性评价 + +**为什么说这套架构适配性强?** + +| 扩展维度 | 实现方式 | 复杂度 | +|----------|----------|--------| +| **新增质控规则** | 在 `iit_skills` 表插入 JSON 配置 | ⭐ 低 | +| **新增业务场景** | 新增 Skill 类型 + 配置 SOP 流程 | ⭐⭐ 中低 | +| **新增工具能力** | 在 ToolsService 增加工具定义 | ⭐⭐ 中低 | +| **新增数据源** | 新增 Adapter(如 OdmAdapter) | ⭐⭐⭐ 中 | +| **新增交互入口** | 复用 ChatService 路由逻辑 | ⭐⭐ 中低 | + +**核心优势**: + +1. **配置驱动**:新业务场景主要是"写配置"而非"写代码" +2. **插件式工具**:ToolsService 支持动态注册新工具 +3. **引擎复用**:所有场景共享 HardRuleEngine / SoftRuleEngine +4. **SOP 状态机**:流程可配置、可追溯、可审计 + +### 0.3 完整架构图(含扩展能力) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 入口层 (Multi-Channel) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │ 企业微信文本 │ │ 企业微信图片 │ │ PC Workbench │ │ 定时触发 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 路由层 (Router) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │ ChatService │ │VisionService │ │ API Routes │ │Scheduler │ │ +│ │ (文本路由) │ │ (图片识别) │ │ (REST API) │ │ (定时) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 调度层 (SopEngine) │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Skill 配置 (Postgres) │ │ +│ │ • qc_process (质控流程) • ae_detection (AE检测) │ │ +│ │ • inclusion_check (入排) • protocol_deviation (方案偏离) │ │ +│ │ • ethics_check (伦理) • weekly_report (周报) │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────┼──────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │HardRuleEngine│ │SoftRuleEngine│ │ReportService │ │ +│ │ (CPU规则) │ │ (LLM推理) │ │ (报告生成) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 工具层 (ToolsService) │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │read_clinical_data│ │write_clinical_data│ │search_protocol │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │check_visit_window│ │assess_ae_causality│ │check_ethics │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │get_project_stats │ │manage_issue │ │send_notification│ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 适配器层 (Adapters) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │RedcapAdapter │ │ DifyClient │ │WechatAdapter │ │VLMAdapter│ │ +│ │ (EDC数据) │ │ (知识库) │ │ (消息推送) │ │ (视觉) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 1. 架构决策总结 + +本计划基于以下 5 份架构设计文档的综合审查: + +| 文档 | 核心观点 | 状态 | +|------|----------|------| +| **架构决策白皮书** | 放弃 MCP/复杂框架,采用 Postgres-Only + Service-First | ✅ 认可 | +| **V2.2 实施指南** | 混合双引擎(硬规则 + 软指令)+ 用户偏好 | ✅ 认可 | +| **V2.2 工具泛化** | 3-5 个"瑞士军刀"通用工具替代 100 个专用工具 | ✅ 认可 | +| **V2.3 健壮性设计** | 三层防御(模糊映射 + 容错重试 + 空结果兜底) | ✅ 认可 | +| **V2.4 SOP状态机** | 粗粒度 SOP 节点 + 节点内 ReAct | ✅ 认可 | + +### 1.1 最终架构选型:双脑路由模型 + +> **核心理念**:左脑(SOP) 保证严谨合规,右脑(ReAct) 提供灵活智能 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 企业微信 / 前端入口 │ +│ [文本消息] [图片消息] [定时触发] │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 🧠 意图路由层 (IntentService) │ +│ LLM 驱动,非正则匹配 │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ 输入: "帮我查下那个发烧的病人是谁?" │ │ +│ │ 输出: { type: "QA_QUERY", entities: {...}, needsClarification } │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ 📐 左脑 (SOP) │ │ 🎨 右脑 (ReAct) │ │ ❓ 追问机制 │ +│ 结构化任务 │ │ 开放性查询 │ │ 信息不全 │ +│ 写操作必经 │ │ 只读不写 │ │ 主动澄清 │ +│ │ │ │ │ │ +│ • 质控流程 │ │ • 多步推理 │ │ • "请问您指的是 │ +│ • 入排判断 │ │ • 模糊查询 │ │ 哪位患者?" │ +│ • 数据录入 │ │ • 统计分析 │ │ │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ SopEngine (状态机) │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ 节点A: HardRuleEngine → 节点B: SoftRuleEngine → 节点C: ... │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ToolsService (工具层) │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ 🔓 只读工具 (ReAct 可用) │ 🔒 读写工具 (仅 SOP 可用) │ │ +│ │ • read_clinical_data │ • write_clinical_data │ │ +│ │ • search_protocol │ • manage_issue │ │ +│ │ • get_project_stats │ • update_record │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 适配器层 (Adapters) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ +│ │RedcapAdapter │ │ DifyClient │ │WechatAdapter │ │VLMAdapter│ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**双脑对比**: + +| 维度 | 左脑 (SOP 状态机) | 右脑 (ReAct Agent) | +|------|-------------------|-------------------| +| **擅长** | 执行标准流程、合规检查 | 处理模糊提问、多步查询 | +| **典型指令** | "对 P001 进行入排质控" | "帮我查下最近那个发烧的病人" | +| **思维方式** | 线性执行 (Step 1 → Step 2) | 循环推理 (思考→查库→再思考) | +| **数据权限** | 读写皆可 | **只读 (Read-Only)** | +| **用户评价** | "靠谱但死板" | "聪明但不可控" | + +### 1.2 核心设计原则 + +| 原则 | 描述 | +|------|------| +| **Postgres-Only** | 无 Redis,用 pg-boss 替代队列,用 Postgres 存储 Skill 配置 | +| **Service-First** | 不用 MCP Server,用 Service Class 实现工具调用 | +| **混合双引擎** | 硬规则(CPU) + 软指令(LLM),能用规则的不用 AI | +| **SOP 状态机** | 粗粒度节点控制流程,节点内 ReAct 保持灵活性 | +| **三层防御** | 字段映射 + 自我修正 + 空结果兜底 | + +--- + +## 2. 现有代码资产盘点 + +### 2.1 已完成的组件 + +| 组件 | 路径 | 状态 | 备注 | +|------|------|------|------| +| ChatService | `services/ChatService.ts` | ✅ 可复用 | 需扩展路由逻辑 | +| SessionMemory | `agents/SessionMemory.ts` | ✅ 可复用 | 内存存储,支持过期清理 | +| RedcapAdapter | `adapters/RedcapAdapter.ts` | ✅ 可复用 | 核心数据访问层 | +| WechatService | `services/WechatService.ts` | ✅ 可复用 | 企业微信消息推送 | +| DifyClient | `common/rag/DifyClient.ts` | ✅ 可复用 | 知识库检索 | +| LLMFactory | `common/llm/adapters/LLMFactory.ts` | ✅ 可复用 | 统一 LLM 调用 | + +### 2.2 待开发的组件 + +| 组件 | 优先级 | 说明 | +|------|--------|------| +| `iit_skills` 表 | P0 | Skill 配置存储 | +| `iit_field_mapping` 表 | P0 | 字段名映射字典 | +| `ToolsService` 类 | P0 | 统一工具管理 | +| `HardRuleEngine` 类 | P0 | JSON Logic 执行器 | +| `SoftRuleEngine` 类 | P1 | LLM 推理 + 自我修正 | +| `SopEngine` 类 | P1 | 状态机调度器 | +| `iit_user_preferences` 表 | P2 | 用户偏好存储 | + +--- + +## 3. 开发阶段规划 + +### Phase 1:基础设施层(Week 1) + +#### Day 1-2:数据库 Schema 扩展 + +**目标**:创建 Skill 配置和字段映射表 + +##### 任务清单 + +- [ ] **T1.1** 设计 `iit_skills` 表结构 + ```prisma + model IitSkill { + id String @id @default(uuid()) + projectId String // 绑定项目 + skillType String // qc_process | daily_briefing | general_chat + name String // 技能名称 + config Json // 核心配置 JSON + isActive Boolean @default(true) + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([projectId, skillType]) + @@map("iit_skills") + @@schema("iit_schema") + } + ``` + +- [ ] **T1.2** 设计 `iit_field_mapping` 表结构 + ```prisma + model IitFieldMapping { + id String @id @default(uuid()) + projectId String // 项目级别映射 + aliasName String // LLM 可能传的名称(如 "gender", "性别") + actualName String // REDCap 实际字段名(如 "sex") + createdAt DateTime @default(now()) + + @@unique([projectId, aliasName]) + @@map("iit_field_mapping") + @@schema("iit_schema") + } + ``` + +- [ ] **T1.3** 执行数据库迁移 + ```bash + npx prisma db push + npx prisma generate + ``` + +- [ ] **T1.4** 插入测试数据(第一个 Skill 配置) + +**验收标准**: +- [ ] 表创建成功 +- [ ] 能正常 CRUD 操作 +- [ ] 测试 Skill 配置可查询 + +--- + +#### Day 3-4:ToolsService 实现 + +**目标**:创建统一的工具管理层 + +##### 任务清单 + +- [ ] **T2.1** 创建 `ToolsService.ts` + ``` + backend/src/modules/iit-manager/services/ToolsService.ts + ``` + +- [ ] **T2.2** 实现工具定义(TOOL_DEFINITIONS) + ```typescript + export const TOOL_DEFINITIONS = [ + { + name: "read_clinical_data", + description: "读取 REDCap 临床数据", + parameters: { + type: "object", + properties: { + record_id: { type: "string" }, + fields: { type: "array", items: { type: "string" } } + }, + required: ["record_id", "fields"] + } + }, + { + name: "search_protocol", + description: "检索研究方案文档", + parameters: { ... } + }, + { + name: "manage_issue", + description: "提出质疑或发送通知", + parameters: { ... } + }, + { + name: "get_project_stats", + description: "获取项目统计数据", + parameters: { ... } + } + ]; + ``` + +- [ ] **T2.3** 实现字段映射逻辑(第一层防御) + ```typescript + 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 => mappingDict[f.toLowerCase()] || f); + } + ``` + +- [ ] **T2.4** 实现 `executeTool()` 统一入口 + +- [ ] **T2.5** 实现空结果兜底(第三层防御) + +**验收标准**: +- [ ] 4 个工具定义完成 +- [ ] 字段映射逻辑生效 +- [ ] 空结果返回友好消息 + +--- + +#### Day 5:HardRuleEngine 实现 + +**目标**:创建 CPU 执行的硬规则引擎 + +##### 任务清单 + +- [ ] **T3.1** 安装 json-logic-js + ```bash + npm install json-logic-js + npm install -D @types/json-logic-js + ``` + +- [ ] **T3.2** 创建 `HardRuleEngine.ts` + ``` + backend/src/modules/iit-manager/engines/HardRuleEngine.ts + ``` + +- [ ] **T3.3** 实现规则执行逻辑 + ```typescript + import jsonLogic from 'json-logic-js'; + + export class HardRuleEngine { + run(rules: HardRule[], data: Record): RuleResult[] { + const errors: RuleResult[] = []; + + for (const rule of rules) { + const passed = jsonLogic.apply(rule.logic, data); + if (!passed) { + errors.push({ + field: rule.field, + message: rule.message, + severity: rule.severity || 'error' + }); + } + } + + return errors; + } + } + ``` + +- [ ] **T3.4** 编写单元测试 + +**验收标准**: +- [ ] 能正确执行 `{ ">=": [{ "var": "age" }, 18] }` 类规则 +- [ ] 返回结构化错误列表 +- [ ] 单元测试覆盖主要场景 + +--- + +### Phase 2:引擎层实现(Week 2) + +#### Day 6-7:SoftRuleEngine 实现 + +**目标**:创建 LLM 推理引擎(含自我修正) + +##### 任务清单 + +- [ ] **T4.1** 创建 `SoftRuleEngine.ts` + ``` + backend/src/modules/iit-manager/engines/SoftRuleEngine.ts + ``` + +- [ ] **T4.2** 实现自我修正回路(第二层防御) + ```typescript + async runWithRetry( + instruction: string, + context: any, + maxRetries: number = 3 + ): Promise { + let history: Message[] = [...]; + + for (let i = 0; i < maxRetries; i++) { + const response = await this.llm.chat(history); + + if (response.hasToolCall) { + try { + const result = await this.tools.executeTool( + response.toolName, + response.args + ); + history.push({ role: 'tool', content: JSON.stringify(result) }); + } catch (error) { + // 自我修正:告诉 LLM 它错了 + history.push({ + role: 'user', + content: `工具调用失败: ${error.message}。请检查参数并重试。` + }); + continue; + } + } else { + return this.parseResult(response.content); + } + } + + return { passed: false, reason: '多次重试后仍失败' }; + } + ``` + +- [ ] **T4.3** 实现结果解析(要求 LLM 返回结构化 JSON) + +- [ ] **T4.4** 编写单元测试 + +**验收标准**: +- [ ] 能调用工具并获取结果 +- [ ] 工具失败时能自动重试 +- [ ] 返回结构化判断结果 + +--- + +#### Day 8-9:SopEngine 实现 + +**目标**:创建 SOP 状态机调度器 + +##### 任务清单 + +- [ ] **T5.1** 创建 `SopEngine.ts` + ``` + backend/src/modules/iit-manager/engines/SopEngine.ts + ``` + +- [ ] **T5.2** 定义 Skill 配置格式(SOP 流程图) + ```typescript + interface SopConfig { + name: string; + start_node: string; + nodes: { + [nodeId: string]: { + type: 'hard_rule' | 'soft_instruction'; + // hard_rule 类型 + rules?: HardRule[]; + // soft_instruction 类型 + instruction?: string; + tools?: string[]; + // 流转 + on_pass: string; + on_fail: string; + on_error?: string; + } + } + } + ``` + +- [ ] **T5.3** 实现状态机执行逻辑 + ```typescript + async run(skillConfig: SopConfig, data: any): Promise { + let currentNodeId = skillConfig.start_node; + let context = { ...data }; + const trace: TraceItem[] = []; + + while (currentNodeId && !currentNodeId.startsWith('end')) { + const node = skillConfig.nodes[currentNodeId]; + trace.push({ node: currentNodeId, timestamp: new Date() }); + + let result: NodeResult; + + if (node.type === 'hard_rule') { + result = this.hardEngine.run(node.rules, context); + } else { + result = await this.softEngine.runWithRetry(node.instruction, context); + } + + // 状态流转 + currentNodeId = result.passed ? node.on_pass : node.on_fail; + + // 记录违规 + if (!result.passed) { + await this.savePendingAction(result); + } + } + + return { trace, finalState: currentNodeId }; + } + ``` + +- [ ] **T5.4** 实现违规记录保存(Shadow State) + +- [ ] **T5.5** 编写集成测试 + +**验收标准**: +- [ ] 能按流程图顺序执行节点 +- [ ] 硬规则和软指令都能正确调度 +- [ ] 违规记录保存到 `iit_pending_actions` 表 + +--- + +#### Day 10:ChatService 集成 + +**目标**:将 SopEngine 集成到现有 ChatService + +##### 任务清单 + +- [ ] **T6.1** 扩展意图识别逻辑 + ```typescript + private detectIntent(message: string): Intent { + // 识别质控任务 + if (/质控|检查|校验|QC/.test(message)) { + return { type: 'qc_task', ... }; + } + // 其他意图... + } + ``` + +- [ ] **T6.2** 增加质控任务路由 + ```typescript + async handleMessage(userId: string, message: string): Promise { + const intent = this.detectIntent(message); + + if (intent.type === 'qc_task') { + // 路由到 SopEngine + return this.handleQcTask(userId, intent); + } + + // 普通问答继续走原有逻辑 + return this.handleGeneralChat(userId, message); + } + ``` + +- [ ] **T6.3** 实现 `handleQcTask()` 方法 + +**验收标准**: +- [ ] 质控任务走 SopEngine +- [ ] 普通问答走原有 LLM 逻辑 +- [ ] 两条路径互不干扰 + +--- + +### Phase 3:配置与测试(Week 3) + +#### Day 11-12:第一个完整 Skill 配置 + +**目标**:配置第一个项目的质控流程 + +##### 任务清单 + +- [ ] **T7.1** 设计肺癌研究质控流程(示例) + ```json + { + "name": "肺癌研究入组质控", + "start_node": "baseline_check", + "nodes": { + "baseline_check": { + "type": "hard_rule", + "rules": [ + { "field": "age", "logic": { ">=": [{"var":"age"}, 18] }, "message": "年龄<18岁" }, + { "field": "age", "logic": { "<=": [{"var":"age"}, 75] }, "message": "年龄>75岁" }, + { "field": "ecog", "logic": { "<=": [{"var":"ecog"}, 2] }, "message": "ECOG>2" } + ], + "on_pass": "history_check", + "on_fail": "end_with_violation" + }, + "history_check": { + "type": "soft_instruction", + "instruction": "检查既往史,排除间质性肺炎、活动性感染、严重心血管疾病。", + "tools": ["read_clinical_data"], + "on_pass": "medication_check", + "on_fail": "end_review_required" + }, + "medication_check": { + "type": "soft_instruction", + "instruction": "检查合并用药,排除其他抗肿瘤药物。", + "tools": ["read_clinical_data"], + "on_pass": "end_success", + "on_fail": "end_review_required" + } + } + } + ``` + +- [ ] **T7.2** 插入 Skill 配置到数据库 + +- [ ] **T7.3** 配置字段映射 + ```sql + INSERT INTO iit_field_mapping (project_id, alias_name, actual_name) VALUES + ('project-uuid', 'age', 'age_calculated'), + ('project-uuid', '年龄', 'age_calculated'), + ('project-uuid', 'ecog', 'ecog_score'), + ('project-uuid', '既往史', 'medical_history_text'), + ('project-uuid', 'history', 'medical_history_text'); + ``` + +**验收标准**: +- [ ] Skill 配置存储成功 +- [ ] 字段映射配置完成 + +--- + +#### Day 13-14:端到端测试 + +**目标**:完整流程测试 + +##### 测试场景 + +| 场景 | 输入 | 期望输出 | +|------|------|----------| +| **场景1:年龄不合规** | `{ "age": 16, "ecog": 1 }` | 触发硬规则违规,记录到 pending_actions | +| **场景2:病史违规** | `{ "age": 30, "ecog": 1, "history": "间质性肺炎" }` | AI 识别违规,转人工复核 | +| **场景3:全部通过** | `{ "age": 45, "ecog": 0, "history": "无特殊" }` | 流程正常结束 | +| **场景4:字段名映射** | Agent 传 `fields=["年龄"]` | 自动映射为 `age_calculated` | +| **场景5:工具失败重试** | REDCap 返回空 | Agent 收到友好提示,正确回复用户 | + +##### 任务清单 + +- [ ] **T8.1** 编写端到端测试脚本 +- [ ] **T8.2** 通过企业微信发送测试消息 +- [ ] **T8.3** 验证 pending_actions 记录 +- [ ] **T8.4** 验证 audit_log 记录 +- [ ] **T8.5** 性能测试(目标 < 5秒响应) + +**验收标准**: +- [ ] 5 个测试场景全部通过 +- [ ] 响应时间 < 5秒 +- [ ] 日志完整可追溯 + +--- + +#### Day 15:文档与收尾 + +##### 任务清单 + +- [ ] **T9.1** 更新 API 文档 +- [ ] **T9.2** 更新模块状态文档 +- [ ] **T9.3** 记录技术债务 +- [ ] **T9.4** 代码 Review 与合并 + +--- + +### Phase 4:定时任务与高级工具(Week 4 前半) + +> **目标**:实现定时报告、高级质控工具 + +#### Day 16-17:定时任务与报告生成 + +**目标**:实现每周自动生成研究进度报告 + +##### 任务清单 + +- [ ] **T10.1** 创建 `SchedulerService.ts`(基于 pg-boss) + ``` + backend/src/modules/iit-manager/services/SchedulerService.ts + ``` + +- [ ] **T10.2** 实现定时任务调度 + ```typescript + import PgBoss from 'pg-boss'; + + export class SchedulerService { + private boss: PgBoss; + + async init() { + this.boss = new PgBoss(process.env.DATABASE_URL); + await this.boss.start(); + + // 注册周报任务处理器 + await this.boss.work('weekly-report', this.handleWeeklyReport.bind(this)); + + // 每周一早上9点执行 + await this.boss.schedule('weekly-report', '0 9 * * 1', { projectId: 'all' }); + } + + private async handleWeeklyReport(job: Job) { + const report = await this.reportService.generateWeeklyReport(job.data.projectId); + await this.wechatService.sendToAdmins(report); + } + } + ``` + +- [ ] **T10.3** 创建 `ReportService.ts` + ``` + backend/src/modules/iit-manager/services/ReportService.ts + ``` + +- [ ] **T10.4** 实现周报生成逻辑 + ```typescript + export class ReportService { + async generateWeeklyReport(projectId: string): Promise { + const stats = await this.getProjectStats(projectId); + + const report = ` +📊 **${stats.projectName} 周报** +📅 ${stats.weekRange} + +**入组进度** +- 本周新入组:${stats.newEnrollments} 例 +- 累计入组:${stats.totalEnrollments} / ${stats.targetEnrollments} 例 +- 完成率:${stats.completionRate}% + +**数据质量** +- 待处理质疑:${stats.pendingQueries} 条 +- 本周关闭质疑:${stats.closedQueries} 条 +- 方案偏离:${stats.protocolDeviations} 例 + +**AE/SAE** +- 本周新增 AE:${stats.newAEs} 例 +- 本周新增 SAE:${stats.newSAEs} 例 + +**下周重点** +${stats.upcomingVisits.map(v => `- ${v.patientId}: ${v.visitName} (${v.dueDate})`).join('\n')} + `; + + return report; + } + } + ``` + +- [ ] **T10.5** 配置周报 Skill + ```json + { + "skillType": "weekly_report", + "config": { + "schedule": "0 9 * * 1", + "recipients": ["admin_group"], + "sections": ["enrollment", "data_quality", "ae_summary", "upcoming_visits"] + } + } + ``` + +**验收标准**: +- [ ] 每周一自动生成周报 +- [ ] 周报通过企业微信发送给管理员 +- [ ] 周报内容完整、格式美观 + +--- + +#### Day 18-19:高级质控工具扩展 + +**目标**:新增方案偏离、AE评估、伦理检查工具 + +##### 任务清单 + +- [ ] **T11.1** 新增 `check_visit_window` 工具(方案偏离检测) + ```typescript + { + name: "check_visit_window", + description: "检查访视是否在方案允许的时间窗口内", + parameters: { + record_id: { type: "string" }, + visit_id: { type: "string" }, + actual_date: { type: "string", format: "date" } + }, + handler: async (args) => { + 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) + ) + }; + } + } + ``` + +- [ ] **T11.2** 新增 `assess_ae_causality` 工具(AE因果关系评估) + ```typescript + { + name: "assess_ae_causality", + description: "评估不良事件与研究药物的因果关系", + parameters: { + record_id: { type: "string" }, + ae_id: { type: "string" } + }, + handler: async (args) => { + const ae = await this.getAEDetails(args.record_id, args.ae_id); + const drugInfo = await this.getDrugExposure(args.record_id); + + // 使用 SoftRuleEngine 评估 + const assessment = await this.softEngine.runWithRetry( + `根据以下信息评估AE与研究药物的因果关系: + AE信息:${JSON.stringify(ae)} + 用药信息:${JSON.stringify(drugInfo)} + 请给出:肯定相关/可能相关/可能无关/肯定无关/无法评估`, + { ae, drugInfo } + ); + + return assessment; + } + } + ``` + +- [ ] **T11.3** 新增 `check_ethics_compliance` 工具(伦理合规检查) + ```typescript + { + name: "check_ethics_compliance", + description: "检查是否符合伦理要求", + parameters: { + record_id: { type: "string" }, + check_type: { + type: "string", + enum: ["informed_consent", "age_requirement", "vulnerable_population"] + } + }, + handler: async (args) => { + const rules = { + informed_consent: { + logic: { "<=": [{ "var": "icf_date" }, { "var": "enrollment_date" }] }, + message: "入组日期早于知情同意签署日期" + }, + age_requirement: { + logic: { ">=": [{ "var": "age" }, 18] }, + message: "未成年受试者需要法定监护人签署同意书" + } + }; + + const data = await this.getRecordData(args.record_id); + return this.hardEngine.run([rules[args.check_type]], data); + } + } + ``` + +- [ ] **T11.4** 更新 ToolsService 工具列表 + +**验收标准**: +- [ ] 访视超窗能被正确检测 +- [ ] AE 因果关系能给出评估结论 +- [ ] 伦理违规能被识别 + +--- + +### Phase 5:智能化增强 - 双脑架构(Week 4 后半 - Week 5) + +> **目标**:实现 LLM 意图路由 + ReAct 多步推理 + 追问机制,让 Agent "懂人话" +> +> **优先级调整说明**:智能化交互比视觉识别更影响用户留存,优先实现 + +#### Day 20-21:意图路由层实现 + +**目标**:用 LLM 替代正则匹配,实现智能意图识别 + +##### 任务清单 + +- [ ] **T12.1** 创建 `IntentService.ts` + ``` + backend/src/modules/iit-manager/services/IntentService.ts + ``` + +- [ ] **T12.2** 实现 LLM 驱动的意图识别 + ```typescript + export class IntentService { + private llm; + + async detect(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 } + ]); + + return JSON.parse(response.content); + } + } + ``` + +- [ ] **T12.3** 实现降级策略(LLM 不可用时回退关键词匹配) + ```typescript + async detectWithFallback(message: string, history: Message[]): Promise { + try { + return await this.detect(message, history); + } catch (error) { + logger.warn('[IntentService] LLM 不可用,回退到关键词匹配'); + return this.keywordFallback(message); + } + } + + private keywordFallback(message: string): IntentResult { + if (/质控|检查|校验|QC|入排/.test(message)) { + return { type: 'QC_TASK', confidence: 0.6, entities: {} }; + } + return { type: 'QA_QUERY', confidence: 0.5, entities: {} }; + } + ``` + +- [ ] **T12.4** 集成到 ChatService 路由层 + +**验收标准**: +- [ ] "查下那个发烧的病人" → 识别为 QA_QUERY +- [ ] "对 P001 进行入排质控" → 识别为 QC_TASK +- [ ] "他怎么样了" → 识别为 UNCLEAR,生成追问句 +- [ ] LLM 服务中断时自动降级 + +--- + +#### Day 22-23:ReAct Agent 实现 + +**目标**:创建多步推理引擎,支持循环思考和工具调用 + +##### 任务清单 + +- [ ] **T13.1** 创建 `ReActEngine.ts` + ``` + backend/src/modules/iit-manager/engines/ReActEngine.ts + ``` + +- [ ] **T13.2** 实现 ReAct 循环(思考→行动→观察→再思考) + ```typescript + export class ReActEngine { + private maxIterations = 5; // 防止死循环 + private maxTokens = 4000; // Token 预算限制 + private tokenCounter = 0; + + // 工具白名单:ReAct 只能调用只读工具 + private readonly READONLY_TOOLS = [ + 'read_clinical_data', + 'search_protocol', + 'get_project_stats', + 'check_visit_window' + ]; + + async run(query: string, context: AgentContext): Promise { + const systemPrompt = `你是一个临床研究智能助手。你可以使用以下工具回答问题: +${this.formatToolDescriptions(this.READONLY_TOOLS)} + +请按照 ReAct 模式思考: +1. 思考:分析问题,决定下一步 +2. 行动:调用工具获取信息 +3. 观察:查看工具返回结果 +4. 重复以上步骤,直到能回答用户问题 + +注意:你只能查询数据,不能修改数据。如果用户需要修改操作,请引导他们使用正式的质控流程。`; + + let messages: Message[] = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: query } + ]; + + const trace: TraceItem[] = []; + + for (let i = 0; i < this.maxIterations; i++) { + // Token 预算检查 + if (this.tokenCounter > this.maxTokens) { + return { + success: false, + content: '抱歉,这个问题比较复杂,请尝试更具体的描述。', + trace + }; + } + + const response = await this.llm.chat(messages, { + tools: this.getReadonlyToolDefinitions() + }); + + this.tokenCounter += response.usage?.total_tokens || 0; + trace.push({ iteration: i, response: response.content }); + + // AI 决定结束 + if (!response.toolCalls || response.toolCalls.length === 0) { + return { success: true, content: response.content, trace }; + } + + // 执行工具调用 + let errorCount = 0; + for (const toolCall of response.toolCalls) { + // 安全检查:只允许只读工具 + if (!this.READONLY_TOOLS.includes(toolCall.name)) { + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: `错误:工具 ${toolCall.name} 不在允许列表中。你只能使用只读工具。` + }); + errorCount++; + continue; + } + + try { + const result = await this.tools.executeTool(toolCall.name, toolCall.args); + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: JSON.stringify(result) + }); + } catch (error) { + errorCount++; + messages.push({ + role: 'tool', + toolCallId: toolCall.id, + content: `工具调用失败: ${error.message}` + }); + } + } + + // 幻觉熔断:连续 2 次工具调用全部失败 + if (errorCount >= 2) { + return { + success: false, + content: '抱歉,我遇到了一些技术问题,无法获取相关信息。请稍后重试或联系管理员。', + trace + }; + } + } + + return { + success: false, + content: '抱歉,我无法在有限步骤内完成这个查询。请尝试更具体的问题。', + trace + }; + } + } + ``` + +- [ ] **T13.3** 实现 trace 日志记录(调试用) + ```typescript + private async saveTrace(userId: string, query: string, trace: TraceItem[]) { + await prisma.iitAgentTrace.create({ + data: { + userId, + query, + trace: JSON.stringify(trace), + createdAt: new Date() + } + }); + } + ``` + +- [ ] **T13.4** 编写单元测试 + +**验收标准**: +- [ ] "最近入组的女性患者平均年龄" → 自动调用工具查询并计算 +- [ ] 连续工具失败时触发熔断 +- [ ] Token 超预算时优雅终止 +- [ ] 尝试调用写工具时被拦截 + +--- + +#### Day 24:追问机制与上下文增强 + +**目标**:信息不全时主动追问,增强上下文记忆 + +##### 任务清单 + +- [ ] **T14.1** 实现追问机制 + ```typescript + // ChatService 中的追问逻辑 + async handleMessage(userId: string, message: string): Promise { + const history = this.sessionMemory.getHistory(userId); + const intent = await this.intentService.detect(message, history); + + // 信息不全,主动追问 + if (intent.type === 'UNCLEAR') { + const clarification = intent.clarification_question + || `请问您能具体说明一下吗?例如:${this.getSuggestions(intent)}`; + + this.sessionMemory.addMessage(userId, 'assistant', clarification); + return clarification; + } + + // 根据意图路由 + if (intent.type === 'QC_TASK') { + return this.sopEngine.run(intent); + } else { + return this.reactEngine.run(message, { history, intent }); + } + } + ``` + +- [ ] **T14.2** 增强 SessionMemory,支持实体记忆 + ```typescript + export class SessionMemory { + // 新增:记住对话中提到的实体 + private entityMemory: Map = new Map(); + + addEntityContext(userId: string, entities: Record) { + const existing = this.entityMemory.get(userId) || {}; + this.entityMemory.set(userId, { ...existing, ...entities }); + } + + resolveReference(userId: string, reference: string): string | null { + const context = this.entityMemory.get(userId); + if (!context) return null; + + // 解析 "他"、"这个患者" 等指代 + if (['他', '她', '这个患者', '那个病人'].includes(reference)) { + return context.lastMentionedPatient; + } + return null; + } + } + ``` + +- [ ] **T14.3** 实现指代消解 + ```typescript + // 在处理消息前,先解析指代 + private resolveReferences(userId: string, message: string): string { + const pronouns = ['他', '她', '这个患者', '那个病人']; + let resolved = message; + + for (const pronoun of pronouns) { + if (message.includes(pronoun)) { + const actual = this.sessionMemory.resolveReference(userId, pronoun); + if (actual) { + resolved = resolved.replace(pronoun, actual); + } + } + } + + return resolved; + } + ``` + +**验收标准**: +- [ ] "他怎么样了" → 回复 "请问您指的是哪位患者?" +- [ ] 如果上文提到 P001,"他怎么样了" → 自动解析为 P001 +- [ ] 对话上下文在会话内保持连贯 + +--- + +#### Day 25:集成测试与优化 + +**目标**:完成双脑架构的完整测试 + +##### 测试场景(Phase 5) + +| 场景 | 输入 | 期望输出 | +|------|------|----------| +| **场景6:模糊查询** | "查下最近入组的病人" | ReAct 自动查询并返回列表 | +| **场景7:多步推理** | "最近入组的女性平均年龄" | 多步工具调用 + 计算结果 | +| **场景8:追问** | "他怎么样了" | "请问您指的是哪位患者?" | +| **场景9:指代消解** | (上文提到P001) "他的入排状态" | 自动识别为 P001 | +| **场景10:写操作拦截** | (ReAct中) 尝试修改数据 | 被拦截并引导到 SOP 流程 | +| **场景11:熔断** | 连续工具失败 | 优雅终止并提示 | + +##### 任务清单 + +- [ ] **T15.1** 编写 Phase 5 测试脚本 +- [ ] **T15.2** 验证意图识别准确率(目标 > 85%) +- [ ] **T15.3** 验证 ReAct 多步推理成功率 +- [ ] **T15.4** 验证追问机制用户体验 + +**验收标准**: +- [ ] 11 个测试场景全部通过 +- [ ] 意图识别准确率 > 85% +- [ ] ReAct 平均迭代次数 < 3 +- [ ] 用户反馈"不再觉得像傻子" + +--- + +### Phase 6:视觉能力(Week 6) + +> **目标**:支持拍照上传 → 识别 → 自动录入 REDCap + +#### Day 26-27:视觉能力集成 + +**目标**:支持拍照上传 → 识别 → 自动录入 REDCap + +##### 任务清单 + +- [ ] **T16.1** 创建 `VisionService.ts` + ``` + backend/src/modules/iit-manager/services/VisionService.ts + ``` + +- [ ] **T16.2** 集成视觉大模型(推荐 Qwen-VL / GPT-4V) + ```typescript + export class VisionService { + private vlmAdapter: VLMAdapter; + + async extractFromImage(imageUrl: string, projectId: string): Promise { + // 1. 调用视觉模型识别内容 + const rawText = await this.vlmAdapter.recognize(imageUrl); + + // 2. 结构化提取 + const structured = await this.structureData(rawText, projectId); + + // 3. 匹配表单 + const formMatch = await this.matchForm(structured, projectId); + + return { rawText, structured, formMatch }; + } + } + ``` + +- [ ] **T16.3** 创建表单模板表 `iit_form_templates` + ```prisma + model IitFormTemplate { + id String @id @default(uuid()) + projectId String + formName String // REDCap 表单名称 + fieldSchema Json // 表单字段结构 + keywords String[] // 用于匹配的关键词 + createdAt DateTime @default(now()) + + @@map("iit_form_templates") + @@schema("iit_schema") + } + ``` + +- [ ] **T16.4** 扩展 `RedcapAdapter.writeRecord()` 写入能力 + +- [ ] **T16.5** ChatService 增加图片消息路由 + +**验收标准**: +- [ ] 上传化验单图片能识别内容 +- [ ] 自动匹配到正确的 REDCap 表单 +- [ ] 高置信度时自动录入,低置信度时人工确认 + +--- + +#### Day 28:最终集成测试 + +**目标**:完成全部场景测试 + +##### 测试场景(全量) + +| 场景 | 输入 | 期望输出 | +|------|------|----------| +| **场景12:拍照识别** | 上传化验单图片 | 识别内容,匹配表单,自动录入 | +| **场景13:访视超窗** | V3 访视超出窗口期 5 天 | 检测到方案偏离 | +| **场景14:AE评估** | SAE 事件数据 | 给出因果关系评估 | +| **场景15:伦理违规** | ICF 日期晚于入组日期 | 识别伦理违规 | +| **场景16:周报生成** | 触发定时任务 | 生成并发送周报 | + +##### 任务清单 + +- [ ] **T17.1** 编写全量测试脚本 +- [ ] **T17.2** 验证视觉识别准确率(目标 > 85%) +- [ ] **T17.3** 性能优化(图片处理 < 10s) +- [ ] **T17.4** 更新文档和部署指南 + +**验收标准**: +- [ ] 16 个测试场景全部通过 +- [ ] 图片识别准确率 > 85% +- [ ] 定时任务连续运行 7 天无故障 + +--- + +## 4. 风险与应对 + +### 4.1 基础架构风险 + +| 风险 | 影响 | 应对措施 | +|------|------|----------| +| JSON Logic 表达能力不足 | 复杂规则无法配置 | 支持 `function_name` 模式,调用预定义函数 | +| LLM 响应慢 | 用户体验差 | 硬规则先行,减少 LLM 调用 | +| 字段映射字典不全 | 工具调用失败 | 自我修正回路 + 日志记录 + 运营补充 | +| REDCap 不可用 | 流程中断 | 增加 `on_error` 分支,友好提示 | + +### 4.2 双脑架构风险 (Phase 5) + +| 风险 | 影响 | 应对措施 | +|------|------|----------| +| 意图识别错误 | 路由到错误分支 | 置信度阈值 + 低置信度时追问确认 | +| ReAct 死循环 | 烧钱、用户等待 | 最大迭代次数 = 5,Token 预算 = 4000 | +| ReAct 调用写工具 | 数据被误改 | **工具白名单**,只允许只读工具 | +| LLM 幻觉 | 返回错误信息 | 连续 2 次工具失败触发熔断 | +| LLM 服务中断 | 无法响应 | 降级到关键词匹配 + 友好提示 | +| Token 成本失控 | 费用超预算 | Token 计数 + 单次查询预算限制 | + +### 4.3 扩展能力风险 (Phase 4 & 6) + +| 风险 | 影响 | 应对措施 | +|------|------|----------| +| 视觉模型识别错误 | 录入错误数据 | 低置信度人工确认,高置信度才自动录入 | +| 定时任务失败 | 周报未发送 | pg-boss 自动重试 + 失败告警 | +| 图片处理超时 | 用户体验差 | 异步处理 + 进度提示 | + +--- + +## 5. 成功标准 + +### Phase 1-3 验收标准(核心质控) + +- [ ] 企业微信发送"质控 ID=001",3秒内收到回复 +- [ ] 硬规则违规能自动识别并记录 +- [ ] 软指令能正确调用工具并判断 +- [ ] 字段名映射生效 +- [ ] 工具失败能自动重试 +- [ ] 违规记录可在后台查看 + +### Phase 4 验收标准(定时任务与高级工具) + +- [ ] 访视超窗能被自动检测并记录 +- [ ] AE 事件能给出因果关系评估 +- [ ] 伦理违规能被识别并告警 +- [ ] 每周一自动生成并发送周报 +- [ ] 定时任务连续运行 7 天无故障 + +### Phase 5 验收标准(双脑架构) + +- [ ] 自然语言意图识别准确率 > 85% +- [ ] "查下最近入组的病人" → ReAct 自动查询返回 +- [ ] "他怎么样了" → 主动追问 "请问您指的是哪位患者?" +- [ ] 上文提到 P001 后,"他的状态" → 自动识别为 P001 +- [ ] ReAct 尝试调用写工具 → 被拦截并引导到 SOP +- [ ] LLM 服务中断 → 自动降级到关键词匹配 +- [ ] 用户反馈:**"不再觉得 Agent 像个傻子"** + +### Phase 6 验收标准(视觉能力) + +- [ ] 拍照上传化验单,自动识别并录入 REDCap +- [ ] 图片识别准确率 > 85% +- [ ] 低置信度时要求人工确认 + +### 性能指标 + +| 指标 | 目标值 | +|------|--------| +| 硬规则执行时间 | < 100ms | +| 软指令执行时间 | < 3s | +| 端到端响应时间 | < 5s | +| **意图识别时间** | < 1s | +| **ReAct 平均迭代次数** | < 3 | +| 图片识别+录入时间 | < 10s | +| 视觉识别准确率 | > 85% | +| **意图识别准确率** | > 85% | +| 自我修正成功率 | > 80% | + +--- + +## 6. 附录:文件路径清单 + +``` +backend/src/modules/iit-manager/ +├── services/ +│ ├── ChatService.ts # 扩展路由逻辑(双脑路由) +│ ├── IntentService.ts # 新建:LLM 意图识别 (Phase 5) +│ ├── ToolsService.ts # 新建:统一工具管理 +│ ├── SchedulerService.ts # 新建:定时任务调度 (Phase 4) +│ ├── ReportService.ts # 新建:报告生成服务 (Phase 4) +│ ├── VisionService.ts # 新建:视觉识别服务 (Phase 6) +│ └── WechatService.ts # 已存在 +├── engines/ +│ ├── HardRuleEngine.ts # 新建:JSON Logic 执行器 +│ ├── SoftRuleEngine.ts # 新建:LLM 推理引擎 +│ ├── SopEngine.ts # 新建:状态机调度器 +│ └── ReActEngine.ts # 新建:多步推理引擎 (Phase 5) +├── agents/ +│ └── SessionMemory.ts # 已存在,扩展实体记忆 (Phase 5) +├── adapters/ +│ ├── RedcapAdapter.ts # 已存在,扩展 writeRecord() +│ └── VLMAdapter.ts # 新建:视觉大模型适配器 (Phase 6) +└── types/ + └── index.ts # 扩展类型定义 +``` + +### 数据库新增表 + +``` +iit_schema.iit_skills # Skill 配置存储 +iit_schema.iit_field_mapping # 字段名映射 +iit_schema.iit_agent_trace # ReAct 推理轨迹 (Phase 5) +iit_schema.iit_form_templates # 表单模板 (Phase 6) +``` + +--- + +## 7. 参考文档 + +1. [架构决策白皮书:极简主义的胜利](../00-系统设计/IIT%20Manager%20Agent%20架构决策白皮书:极简主义的胜利.md) +2. [V2.2 落地实施指南](../00-系统设计/IIT%20Manager%20Agent%20V2.2%20落地实施指南:极简架构版%20(融合%20Moltbot).md) +3. [V2.2 工具泛化与灵活性提升指南](../00-系统设计/IIT%20Manager%20Agent%20V2.2:工具泛化与灵活性提升指南.md) +4. [V2.3 健壮性设计与最佳实践](../00-系统设计/IIT%20Manager%20Agent%20V2.3:健壮性设计与最佳实践.md) +5. [V2.4 架构模式选型与 SOP 状态机推荐](../00-系统设计/IIT%20Manager%20Agent%20V2.4:架构模式选型与%20SOP%20状态机推荐.md) +6. [V2.6 智能化升级方案:双脑架构](../00-系统设计/IIT%20Manager%20Agent%20V2.6%20智能化升级方案:双脑架构.md) + +--- + +**文档维护人**:AI Agent +**最后更新**:2026-02-02(V2.6 整合双脑架构) diff --git a/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.6 综合开发计划.md b/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.6 综合开发计划.md new file mode 100644 index 00000000..6fd990bf --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/04-开发计划/IIT Manager Agent V2.6 综合开发计划.md @@ -0,0 +1,327 @@ +# IIT Manager Agent V2.6 综合开发计划 + +> **版本:** V2.9(极简架构 + SOP状态机 + 双脑路由 + 三层记忆 + 主动性增强) +> **日期:** 2026-02-05 +> **团队规模:** 2人 +> **预估周期:** 6周 +> **核心目标:** 实现数据质控 Agent 的完整闭环 + 智能化交互 + 长期记忆 + 主动提醒 + 个性化 + +--- + +## 📚 文档导航 + +本开发计划已拆分为多个专项文档,便于查阅和维护: + +| 文档 | 内容 | 适用场景 | +|------|------|----------| +| **本文档** | 架构总览、设计原则、验收标准 | 项目概览、立项汇报 | +| [01-数据库设计](./01-数据库设计.md) | Prisma 模型、索引设计、初始化 SQL | 数据库开发 | +| [02-核心引擎实现指南](./02-核心引擎实现指南.md) | HardRuleEngine、SoftRuleEngine、SopEngine、ReActEngine | 引擎开发 | +| [03-服务层实现指南](./03-服务层实现指南.md) | ToolsService、ChatService、IntentService 等 | 服务开发 | +| [04-记忆系统实现指南](./04-记忆系统实现指南.md) | V2.8 三层记忆架构、MemoryService | 记忆功能开发 | +| [05-开发阶段与任务清单](./05-开发阶段与任务清单.md) | Phase 1-6 详细任务、里程碑、验收标准 | 项目管理、进度跟踪 | + +--- + +## 0. 架构适配性评估 + +本架构设计充分考虑了临床研究的多种业务场景,通过 **SOP状态机 + 双引擎 + 可扩展工具层** 的设计,实现了良好的适配性和扩展性。 + +### 0.1 目标场景覆盖度 + +| 场景 | 覆盖度 | 核心支撑组件 | 备注 | +|------|--------|-------------|------| +| **1. 拍照识别 + 自动录入** | 🟡 60% | VisionService + ToolsService | 延后到 V3.0 | +| **2. 数据质控** | 🟢 95% | HardRuleEngine + SoftRuleEngine | 核心场景 | +| **3. 入排标准判断** | 🟢 90% | SopEngine + 硬规则配置 | 配置 Skill 即可 | +| **4. 方案偏离检测** | 🟢 80% | SoftRuleEngine + search_protocol | 需配置访视窗口规则 | +| **5. AE事件检测** | 🟢 80% | 硬规则触发 + 软指令评估 | 需配置AE识别规则 | +| **6. 伦理合规检测** | 🟢 80% | HardRuleEngine | 配置伦理规则即可 | +| **7. 定期报告生成** | 🟡 50% | SchedulerService + ReportService | Phase 4 实现 | + +### 0.2 架构扩展性评价 + +| 扩展维度 | 实现方式 | 复杂度 | +|----------|----------|--------| +| **新增质控规则** | 在 `iit_skills` 表插入 JSON 配置 | ⭐ 低 | +| **新增业务场景** | 新增 Skill 类型 + 配置 SOP 流程 | ⭐⭐ 中低 | +| **新增工具能力** | 在 ToolsService 增加工具定义 | ⭐⭐ 中低 | +| **新增数据源** | 新增 Adapter(如 OdmAdapter) | ⭐⭐⭐ 中 | + +--- + +## 1. 架构决策总结 + +本计划基于以下架构设计文档的综合审查: + +| 文档 | 核心观点 | 状态 | +|------|----------|------| +| **架构决策白皮书** | Postgres-Only + Service-First | ✅ 认可 | +| **V2.2 实施指南** | 混合双引擎(硬规则 + 软指令) | ✅ 认可 | +| **V2.2 工具泛化** | 3-5 个通用工具替代 100 个专用工具 | ✅ 认可 | +| **V2.3 健壮性设计** | 三层防御(映射 + 重试 + 兜底) | ✅ 认可 | +| **V2.4 SOP状态机** | 粗粒度 SOP 节点 + 节点内 ReAct | ✅ 认可 | +| **V2.8 记忆系统** | 三层记忆(流水账 + 热记忆 + 历史书) | ✅ 认可 | +| **V2.9 主动性增强** | Cron Skill + 用户画像 + 反馈循环 | ✅ 认可 | + +### 1.0 V2.9 核心增强(新) + +> **目标**:让 Agent 从"被动应答"进化为"主动协作",同时根据用户反馈持续优化 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ V2.9 新增能力 │ +├──────────────────┬──────────────────┬───────────────────────────────┤ +│ 🔔 Cron Skill │ 👤 用户画像 │ 📊 反馈循环 │ +│ 主动触发 SOP │ 个性化响应 │ 持续优化 │ +│ 定时访视提醒 │ 最佳通知时间 │ 偏好自动调整 │ +└──────────────────┴──────────────────┴───────────────────────────────┘ +``` + +| 能力 | 实现方式 | 价值 | +|------|----------|------| +| **Cron Skill** | `iit_skills.triggerType = 'cron'` + pg-boss 调度 | 访视提醒、周报自动发送 | +| **用户画像** | `project_memory` 中存储用户偏好 Markdown | 回复风格个性化 | +| **反馈循环** | `conversation_history.feedback` 字段 | 持续改进回复质量 | +| **多意图处理** | ReAct Prompt 优化 + Chain of Thought | 一句话多任务 | + +### 1.1 双脑路由模型 + +> **核心理念**:左脑(SOP) 保证严谨合规,右脑(ReAct) 提供灵活智能 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 企业微信 / 前端入口 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 🧠 意图路由层 (IntentService) │ +│ 混合路由:正则快速通道 + LLM 后备 │ +└─────────────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ 📐 左脑 (SOP) │ │ 🎨 右脑 (ReAct) │ │ ❓ 追问机制 │ +│ 结构化任务 │ │ 开放性查询 │ │ 信息不全 │ +│ 写操作必经 │ │ 只读不写 │ │ 主动澄清 │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ ToolsService (工具层) │ +│ 🔓 只读工具 (ReAct 可用) │ 🔒 读写工具 (仅 SOP 可用) │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**双脑对比**: + +| 维度 | 左脑 (SOP 状态机) | 右脑 (ReAct Agent) | +|------|-------------------|-------------------| +| **擅长** | 执行标准流程、合规检查 | 处理模糊提问、多步查询 | +| **典型指令** | "对 P001 进行入排质控" | "帮我查下最近那个发烧的病人" | +| **数据权限** | 读写皆可 | **只读 (Read-Only)** | + +### 1.2 核心设计原则 + +| 原则 | 描述 | +|------|------| +| **Postgres-Only** | 无 Redis,用 pg-boss 替代队列 | +| **Service-First** | 不用 MCP Server,用 Service Class | +| **混合双引擎** | 硬规则(CPU) + 软指令(LLM) | +| **SOP 状态机** | 粗粒度节点 + 节点内灵活性 | +| **主动协作 (V2.9)** | Cron Skill 主动触发 + 用户画像个性化 | +| **反馈驱动 (V2.9)** | 用户反馈自动调整偏好 | +| **三层防御** | 字段映射 + 自我修正 + 空结果兜底 | +| **三层记忆** | 流水账(L1) + 热记忆(L2) + 历史书(L3) | + +### 1.3 三层记忆架构 (V2.8) + +> **关键洞察**:临床研究项目持续 1-3 年,Agent 必须具备长期记忆能力 + +| 层级 | 名称 | 存储位置 | 生命周期 | 检索方式 | +|------|------|----------|----------|----------| +| L1 | 流水账 | conversation_history | 30天 | 按需向量检索 | +| L2 | 热记忆 | project_memory | 持久 | 每次注入 | +| L3 | 历史书 | weekly_reports | 持久 | 按意图检索 | + +> 📖 详细实现请参阅 [04-记忆系统实现指南](./04-记忆系统实现指南.md) + +--- + +## 2. 组件总览 + +### 2.1 已完成组件 + +| 组件 | 路径 | 状态 | +|------|------|------| +| ChatService | `services/ChatService.ts` | ✅ 需扩展 | +| SessionMemory | `agents/SessionMemory.ts` | ✅ 可复用 | +| RedcapAdapter | `adapters/RedcapAdapter.ts` | ✅ 可复用 | +| WechatService | `services/WechatService.ts` | ✅ 可复用 | +| DifyClient | `common/rag/DifyClient.ts` | ✅ 可复用 | +| LLMFactory | `common/llm/adapters/LLMFactory.ts` | ✅ 可复用 | + +### 2.2 待开发组件 + +| 组件 | 优先级 | Phase | 详细文档 | +|------|--------|-------|----------| +| `iit_skills` 表 | P0 | 1 | [01-数据库设计](./01-数据库设计.md) | +| `iit_field_mapping` 表 | P0 | 1 | [01-数据库设计](./01-数据库设计.md) | +| `ToolsService` 类 | P0 | 1 | [03-服务层实现指南](./03-服务层实现指南.md) | +| `HardRuleEngine` 类 | P0 | 1 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) | +| `SoftRuleEngine` 类 | P1 | 2 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) | +| `SopEngine` 类 | P1 | 2 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) | +| `MemoryService` 类 | P1 | 2-3 | [04-记忆系统实现指南](./04-记忆系统实现指南.md) | +| `ReActEngine` 类 | P1 | 3 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) | +| `IntentService` 类 | P1 | 5 | [03-服务层实现指南](./03-服务层实现指南.md) | +| `SchedulerService` 类 | P2 | 4 | [03-服务层实现指南](./03-服务层实现指南.md) | + +--- + +## 3. 开发阶段总览 + +> 📖 详细任务清单请参阅 [05-开发阶段与任务清单](./05-开发阶段与任务清单.md) + +| Phase | 名称 | 周期 | 核心交付物 | +|-------|------|------|-----------| +| **Phase 1** | 基础工具层 | Week 1 | ToolsService, HardRuleEngine, 字段映射 | +| **Phase 2** | SOP 引擎 + 热记忆 | Week 2 | SopEngine, SoftRuleEngine, L2 热记忆 | +| **Phase 3** | ReAct 引擎 + 流水账 | Week 3 | ReActEngine, L1 流水账, 向量检索 | +| **Phase 4** | 调度系统 + 历史书 | Week 4 前半 | SchedulerService, ReportService, L3 历史书 | +| **Phase 5** | 智能路由 | Week 4 后半 - Week 5 | IntentService, 混合路由, 追问机制 | +| **Phase 6** | 视觉能力 | Week 6 (延后) | VisionService (延后到 V3.0) | + +``` +Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6 +┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ +│ 基础工具 │ ──▶ │SOP引擎 │ ──▶ │ReAct │ ──▶ │ 调度 │ ──▶ │智能路由 │ ──▶ │视觉能力 │ +│ 层 │ │+ 热记忆 │ │+ 流水账│ │+ 历史书│ │ │ │(延后) │ +└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ +``` + +--- + +## 4. 风险与应对 + +### 4.1 已采纳的风险对策 + +| 风险 | 已采纳对策 | 状态 | +|------|------------|------| +| **意图路由延迟感** | 混合路由(正则+LLM)+ 流式反馈 | ✅ 已整合 | +| **ReAct 多嘴** | 只返回 Final Answer,Trace 存日志 | ✅ 已整合 | +| **SOP 状态机死锁** | SUSPENDED 挂起机制 + 唤醒机制 | ✅ 已整合 | +| **Phase 6 过早** | 延后到 V3.0 | ✅ 已标记 | + +### 4.2 聚焦清单(P0 优先级) + +| 优先级 | 功能 | 理由 | +|--------|------|------| +| **P0** | ToolsService 健壮性 | 字段映射是地基 | +| **P0** | 每日早报 (Morning Brief) | 用户感知最强 | +| **P0** | 意图路由混合模式 | 保证响应速度 + 降低成本 | + +### 4.3 主要风险矩阵 + +| 风险类别 | 风险 | 应对措施 | +|----------|------|----------| +| **基础架构** | LLM 响应慢 | 硬规则先行 + 混合路由 | +| **双脑架构** | ReAct 死循环 | 最大迭代 5 次 + Token 预算 | +| **双脑架构** | ReAct 调用写工具 | 工具白名单(只读) | +| **SOP 状态机** | 人工复核死锁 | SUSPENDED + 唤醒机制 | +| **扩展能力** | 视觉模型识别错误 | 低置信度人工确认 | + +--- + +## 5. 成功标准 + +### 5.1 核心验收标准 + +| 阶段 | 验收标准 | +|------|----------| +| **Phase 1-3** | 质控任务 3秒内响应;服务重启后任务可恢复;对话历史持久化 | +| **Phase 4** | 周报每周一自动生成;定时任务连续 7 天无故障 | +| **Phase 5** | 意图识别准确率 > 85%;用户只收到 Final Answer | +| **Phase 6** | 图片识别准确率 > 85%(延后到 V3.0) | + +### 5.2 性能指标 + +| 指标 | 目标值 | +|------|--------| +| 硬规则执行时间 | < 100ms | +| 软指令执行时间 | < 3s | +| 端到端响应时间 | < 5s | +| 正则快速通道延迟 | < 50ms | +| 意图识别时间(LLM) | < 1s | +| 向量检索延迟 | < 100ms | +| ReAct 平均迭代次数 | < 3 | + +--- + +## 6. 文件路径清单 + +``` +backend/src/modules/iit-manager/ +├── services/ +│ ├── ChatService.ts # 扩展:双脑路由 + 记忆集成 +│ ├── IntentService.ts # 新建:意图识别 (Phase 5) +│ ├── ToolsService.ts # 新建:统一工具管理 (Phase 1) +│ ├── MemoryService.ts # 新建:三层记忆管理 (Phase 2-3) +│ ├── SchedulerService.ts # 新建:定时任务调度 (Phase 4) +│ └── ReportService.ts # 新建:报告生成 (Phase 4) +├── engines/ +│ ├── HardRuleEngine.ts # 新建:JSON Logic 执行器 (Phase 1) +│ ├── SoftRuleEngine.ts # 新建:LLM 推理引擎 (Phase 2) +│ ├── SopEngine.ts # 新建:状态机调度器 (Phase 2) +│ └── ReActEngine.ts # 新建:多步推理引擎 (Phase 3) +├── agents/ +│ └── SessionMemory.ts # 扩展:实体记忆 (Phase 5) +└── adapters/ + └── RedcapAdapter.ts # 扩展:writeRecord() +``` + +### 数据库新增表 + +> 📖 详细 Schema 请参阅 [01-数据库设计](./01-数据库设计.md) + +| 表名 | 用途 | Phase | +|------|------|-------| +| `iit_skills` | Skill 配置存储 | 1 | +| `iit_field_mapping` | 字段名映射 | 1 | +| `iit_task_run` | SOP 任务执行记录 | 2 | +| `iit_pending_actions` | 待确认操作 | 2 | +| `iit_project_memory` | 热记忆(L2) | 2 | +| `iit_conversation_history` | 流水账(L1) | 2 | +| `iit_weekly_reports` | 历史书-周报(L3) | 4 | +| `iit_agent_trace` | ReAct 推理轨迹 | 3 | + +--- + +## 7. 参考文档 + +### 架构设计文档 + +1. [架构决策白皮书:极简主义的胜利](../00-系统设计/IIT%20Manager%20Agent%20架构决策白皮书:极简主义的胜利.md) +2. [V2.2 落地实施指南](../00-系统设计/IIT%20Manager%20Agent%20V2.2%20落地实施指南.md) +3. [V2.3 健壮性设计与最佳实践](../00-系统设计/IIT%20Manager%20Agent%20V2.3.md) +4. [V2.4 SOP 状态机推荐](../00-系统设计/IIT%20Manager%20Agent%20V2.4.md) +5. [V2.6 双脑架构](../00-系统设计/IIT%20Manager%20Agent%20V2.6.md) + +### 审核文档 + +1. [潜在的具体风险与问题](../05-测试文档/潜在的具体风险与问题.md) +2. [V2.8 记忆系统设计](../05-测试文档/IIT%20Manager%20Agent%20V2.8.md) + +--- + +**文档维护人**:AI Agent +**最后更新**:2026-02-05 + +### 更新日志 + +| 版本 | 日期 | 更新内容 | +|------|------|----------| +| V2.6 | 2026-02-02 | 整合双脑架构 + 三层记忆体系 | +| V2.6.1 | 2026-02-05 | 整合团队风险审查建议;拆分为多个专项文档 | +| V2.6.2 | 2026-02-05 | 简化表结构:删除 `iit_user_preferences` 和 `iit_patient_notes`(合并到 `project_memory`) | +| V2.9 | 2026-02-05 | 主动性增强:Cron Skill、用户画像、反馈循环、多意图处理 | diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.7:基于“双层文本记忆”的极简架构.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.7:基于“双层文本记忆”的极简架构.md new file mode 100644 index 00000000..9b6e1177 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.7:基于“双层文本记忆”的极简架构.md @@ -0,0 +1,161 @@ +# **IIT Manager Agent V2.7:基于“双层文本记忆”的极简架构** + +**版本:** V2.7 (Clawbot Memory Edition) + +**日期:** 2026-02-05 + +**核心变更:** 废弃复杂的 pgvector 语义检索,采用 Clawbot 式的“流水账 \+ 沉淀物”双层文本记忆。 + +**优势:** 记忆可读、可改、透明、零黑盒。 + +## **1\. 记忆架构重构 (Memory Refactoring)** + +我们将记忆分为两层,完全模拟人类大脑运作,且摒弃不可解释的向量数据。 + +graph TD + subgraph "层级 1: 流水账 (Daily Stream)" + Log\[Conversation History\] + Note\[Raw Actions\] + desc1\[特点: Append-Only, 巨量, 易遗忘\] + end + + subgraph "层级 2: 沉淀物 (The Sediment)" + ProjectMem\[项目级 MEMORY.md\] + PatientMem\[患者级 MEMORY.md\] + UserMem\[用户级 MEMORY.md\] + desc2\[特点: 精炼, 结构化, 人类可编辑\] + end + + Log \--\>|每晚 Cron 归纳 (Gardening)| ProjectMem + Admin\[管理员/医生\] \--\>|手动修正/编辑| ProjectMem + + ProjectMem \--\>|注入 System Prompt| LLM + +## **2\. 数据库设计变更 (Schema Changes)** + +### **2.1 新增 iit\_memory\_files 表** + +我们不用磁盘文件,用数据库表来模拟文件,方便 Web 端管理。 + +model IitMemoryFile { + id String @id @default(uuid()) + projectId String + targetType String // PROJECT | PATIENT | USER + targetId String // 对应的 ID (如 projectId, recordId, userId) + + // 核心:这就是那个 MEMORY.md 的内容 + content String @db.Text + + lastUpdatedBy String // 'system\_daily\_job' 或 'user\_001' + updatedAt DateTime @updatedAt + + @@unique(\[projectId, targetType, targetId\]) + @@map("iit\_memory\_files") + @@schema("iit\_schema") +} + +### **2.2 废弃计划** + +* **废弃**:iit\_conversation\_history 中的 embedding 字段。 +* **废弃**:Phase 5 中的向量检索服务。 + +## **3\. 核心服务实现:MemoryService (V2.7)** + +### **3.1 读取记忆 (Read)** + +在 ChatService 启动对话前,简单的把文本读出来拼接到 Prompt 里。 + +// backend/src/modules/iit-manager/services/MemoryService.ts + +export class MemoryService { + + async getContext(projectId: string, userId: string, recordId?: string) { + // 1\. 获取项目级记忆 (规则、偏好) + const projectMem \= await this.loadMemory('PROJECT', projectId); + + // 2\. 获取用户级记忆 (习惯) + const userMem \= await this.loadMemory('USER', userId); + + // 3\. (可选) 获取患者级记忆 + const patientMem \= recordId ? await this.loadMemory('PATIENT', recordId) : ''; + + return \` +\=== 长期记忆 (Long-Term Memory) \=== +\[项目背景\]: +${projectMem} + +\[用户偏好\]: +${userMem} + +\[患者备注\]: +${patientMem} +\================================ + \`; + } + + private async loadMemory(type: string, id: string) { + const mem \= await prisma.iitMemoryFile.findUnique({ + where: { projectId\_targetType\_targetId: { ... } } + }); + return mem?.content || ''; + } +} + +### **3.2 记忆维护 (Gardening \- The Clawbot Way)** + +这是最精彩的部分。我们不实时更新长期记忆(太乱),而是每天晚上让 AI 当“园丁”,修剪记忆。 + +**任务:Daily Memory Consolidation** + +* **触发**:每天凌晨 2 点。 +* **输入**:当天的对话流水 (iit\_conversation\_history) \+ 旧的 MEMORY.md。 +* **Prompt**:"这是今天的对话流水。请提取其中新的重要事实(如用户偏好、新的关键决策、患者的新状态),合并到旧的记忆文件中。保持 Markdown 格式。如果没有新信息,保持不变。" +* **输出**:新的 Markdown 文本,覆盖数据库。 + +// SchedulerService.ts + +async runDailyMemoryConsolidation() { + const logs \= await this.getDailyLogs(); + const oldMem \= await this.memoryService.loadMemory('PROJECT', projectId); + + const newMem \= await this.llm.chat(\[ + { role: 'system', content: '你是记忆整理员...' }, + { role: 'user', content: \`旧记忆:\\n${oldMem}\\n\\n今日流水:\\n${logs}\` } + \]); + + await this.memoryService.saveMemory('PROJECT', projectId, newMem); +} + +## **4\. 调整后的开发计划 (Phase 5 重构)** + +我们将原计划 Week 5 的“向量检索”替换为“文本记忆系统”。 + +### **Week 5:Clawbot 式记忆系统** + +| 时间 | 任务 | 说明 | +| :---- | :---- | :---- | +| **Day 24** | **记忆表结构与 CRUD** | 创建 iit\_memory\_files 表,实现读写 API。 | +| **Day 25** | **记忆注入 Prompt** | 修改 ChatService,在 System Prompt 头部注入 Markdown 内容。 | +| **Day 26** | **记忆整理 Worker** | 实现“每日记忆整理”的 Cron Job (pg-boss)。 | +| **Day 27** | **记忆编辑 UI** | 在 Admin 后台增加一个简单的文本框,允许人工修改 MEMORY.md。 | + +## **5\. 为什么这比向量库好?** + +1. **完全透明 (White Box)**: + * 向量库出了问题(比如 AI 突然变傻),你只能重新 Embed,甚至不知道哪条数据坏了。 + * 文本记忆出了问题,你打开 Admin 界面,看到一行:“用户不喜欢红烧肉”,你把它删了,问题立刻解决。**这对医疗系统排查问题是无价的。** +2. **Token 可控**: + * 向量库如果不加限制,可能召回 5000 字的无关内容。 + * 文本记忆由“每日整理”压缩过,通常只有几百字的核心干货,Token 消耗极低。 +3. **技术栈简化**: + * 不需要 pgvector 插件(虽然你们装了,但不用也没关系)。 + * 不需要 Embedding 模型调用。 + * 纯字符串处理,Node.js 最擅长。 + +## **6\. 结论** + +这个建议**极具战略价值**。它把一个“高科技难题”(如何做语义检索)变成了一个“管理学问题”(如何整理笔记)。 + +**执行建议:** + +全盘接受这个建议。在 V2.6 架构中,**移除 Embedding 层,替换为 Memory File 层。** 这会让你们的系统在面对 PI 的刁钻问题时,显得更有“记性”,同时让你们的运维工作变得无比轻松。 \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:基于“周报卷叠”的终极记忆架构.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:基于“周报卷叠”的终极记忆架构.md new file mode 100644 index 00000000..762367c9 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:基于“周报卷叠”的终极记忆架构.md @@ -0,0 +1,134 @@ +# **IIT Manager Agent V2.8:基于“周报卷叠”的终极记忆架构** + +**版本:** V2.8 (The Chronicle Edition) + +**日期:** 2026-02-05 + +**核心变更:** 采纳“以周报代替向量检索”的建议。利用 LLM 的长窗口能力,通过周期性压缩数据,实现全生命周期的可读记忆。 + +**适用场景:** 1-3 年周期的临床研究项目。 + +## **1\. 核心理念:把“大数据”变成“厚书”** + +我们不再试图在海量碎片数据中大海捞针(Vector Search),而是把数据写成一本\*\*“编年史”\*\*。 + +* **每日**:忠实记录流水账。 +* **每周**:LLM 阅读本周流水账,写一页“历史书”(周报)。 +* **查询时**:LLM 直接阅读整本“历史书”。 + +### **优势分析** + +| 维度 | 向量检索 (V2.6/2.7) | 周报卷叠 (V2.8) | +| :---- | :---- | :---- | +| **准确性** | 模糊匹配,容易漏掉关键细节 | **全量阅读**,拥有上帝视角,极准 | +| **可解释性** | 黑盒向量,不知道 AI 看了啥 | **白盒周报**,医生可以随时翻阅、修正周报 | +| **技术难度** | 高 (Embedding, Vector DB) | **低** (Cron Job \+ LLM Summary) | +| **成本** | 检索便宜,但索引维护贵 | 生成周报耗 Token,但查询极其高效 | + +## **2\. 数据库设计 (Schema)** + +### **2.1 新增 iit\_weekly\_reports 表** + +model IitWeeklyReport { + id String @id @default(uuid()) + projectId String + + weekNumber Int // 例如: 202605 (2026年第5周) + startDate DateTime + endDate DateTime + + // 核心:LLM 生成的高浓缩总结 + // 包含:入组进度、发生的AE、关键沟通结论 + summary String @db.Text + + // 结构化数据 (可选,用于画图) + stats Json? // { "enrolled": 5, "queries": 2 } + + createdAt DateTime @default(now()) + + @@unique(\[projectId, weekNumber\]) + @@map("iit\_weekly\_reports") + @@schema("iit\_schema") +} + +## **3\. 核心流程实现** + +### **3.1 写入端:每周一凌晨的“编史官” (Cron Job)** + +**SchedulerService.ts:** + +// 每周一凌晨 02:00 执行 +async generateWeeklyMemory(projectId: string) { + // 1\. 获取上周所有的原始对话 & 操作日志 + const logs \= await this.rawLogs.get({ + projectId, + from: lastMonday, + to: thisSunday + }); + + // 2\. 调用 LLM 进行“有损压缩” + const prompt \= \` + 你是一个临床项目经理。请阅读上周的项目流水账,生成一份【周报记忆块】。 + 要求: + 1\. 忽略闲聊和无关信息。 + 2\. 重点记录:入组人数变化、新增不良事件(AE)、主要方案偏离、PI的关键决策。 + 3\. 格式为 Markdown,字数控制在 500 字以内。 + + 流水账数据: + ${JSON.stringify(logs)} + \`; + + const summary \= await this.llm.chat(prompt); + + // 3\. 存入数据库 (成为历史书的一页) + await prisma.iitWeeklyReport.create({ + data: { projectId, summary, ... } + }); +} + +### **3.2 读取端:查询时的“速读” (Context Injection)** + +当意图识别发现用户在问“过去”或“趋势”时,直接把**所有周报**读出来。 + +**ChatService.ts:** + +async buildHistoryContext(projectId: string): Promise\ { + // 1\. 取出该项目所有历史周报 (按时间正序) + // 3年也就 150 条,Postgres 毫秒级返回 + const reports \= await prisma.iitWeeklyReport.findMany({ + where: { projectId }, + orderBy: { weekNumber: 'asc' }, + select: { weekNumber: true, summary: true } + }); + + // 2\. 拼接成一本“书” + // 格式: + // \[Week 2026-01\]: 入组 2 人,无异常。 + // \[Week 2026-02\]: P003 发生 SAE,已上报。 + const chronicle \= reports.map(r \=\> \`\[Week ${r.weekNumber}\]: ${r.summary}\`).join('\\n'); + + return \` +\=== 项目编年史 (Project Chronicle) \=== +${chronicle} +\==================================== + \`; +} + +## **4\. 最终开发计划修正 (Phase 5\)** + +我们将 Week 5 的任务彻底简化: + +| 时间 | 任务 | 说明 | +| :---- | :---- | :---- | +| **Day 24** | **原始日志表** | 确保 iit\_conversation\_history 记录完整。 | +| **Day 25** | **周报表结构** | 创建 iit\_weekly\_reports 表。 | +| **Day 26** | **编史官 Worker** | 写一个 Cron Job,每周把 Log 压缩成 Report。 | +| **Day 27** | **记忆注入** | 在回答 QA\_QUERY 类问题时,将历史周报注入 Context。 | + +## **5\. 总结:为什么这是终极方案?** + +1. **解决了“上下文遗忘”**:150 个周报拼接起来,刚好填满 DeepSeek 的上下文窗口。AI 可以看到**完整**的项目生命周期,这是 RAG 切片做不到的。 +2. **解决了“幻觉”**:周报是持久化的,医生可以去后台检查某一周的周报写得对不对。如果 AI 瞎写,医生可以手动修正。**修正后的周报就是新的真理。** +3. **极简运维**:不需要维护向量库索引,不需要调优 Embedding 模型。就是简单的 SQL 查询和文本拼接。 + +**就用这个方案。这是目前为止最完美、最优雅的解决路径。** \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆信息映射指南.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆信息映射指南.md new file mode 100644 index 00000000..222ddb2c --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆信息映射指南.md @@ -0,0 +1,83 @@ +# **IIT Manager Agent V2.8:记忆信息映射指南** + +**核心逻辑:** \> \* **即时/全局信息** ![][image1] 存入 **Hot Memory (Markdown)** ![][image1] 每次对话都带。 + +* **历史/阶段信息** ![][image1] 存入 **Weekly Reports (Database)** ![][image1] 查历史时全量读取。 + +## **1\. 信息存储位置映射表** + +| 信息类型 | 存储位置 | 存储形式 | 举例 | 谁来维护? | +| :---- | :---- | :---- | :---- | :---- | +| **用户偏好** (User Preferences) | **Hot Memory** (Level 3\) | iit\_project\_memory 中的 Markdown 列表 | "PI 喜欢简报格式" "不要在周末发消息" | **人工编辑** (Admin) 或 **每日 AI 提炼** | +| **经常出现的上下文** (Frequent Context) | **Hot Memory** (Level 3\) | iit\_project\_memory 中的 Markdown 文本 | "当前处于入组冲刺阶段" "P001 是重点关注对象" | **每日 AI 提炼** (Cron Job) | +| **关键决策** (Key Decisions) | **Weekly Reports** (Level 2\) | iit\_weekly\_reports 中的 summary 字段 | "2026-02-01 决定放宽入排标准" "确认 P003 不良事件不相关" | **每周 AI 归档** (Scheduler) | +| **踩过的坑/经验教训** (Lessons Learned) | **双重存储** | **Hot** (如果是永久教训) **Weekly** (如果是历史记录) | **Hot**: "严禁直接调用 API 修改数据" **Weekly**: "W5 因参数错误导致同步失败" | **人工** (定规矩) **AI** (记流水) | + +## **2\. 详细存储机制** + +### **2.1 用户偏好 & 经常出现的上下文 \-\> Hot Memory (Markdown)** + +这部分信息需要\*\*“时刻生效”\*\*,所以必须存成轻量级的 Markdown,每次对话都注入 System Prompt。 + +* **存储表**:iit\_project\_memory (config 字段) +* **内容示例**: + \# User Preferences + \- \[PI\]: 汇报时只看数据,不要废话。 + \- \[CRC\]: 下午 2 点后比较忙,尽量上午推任务。 + + \# Active Context + \- 当前重点任务:清理 3 月份的 Query。 + \- 风险提示:P005 患者依从性差,需每日提醒。 + +* **更新机制**: + 1. **被动更新**:管理员在后台手动修改。 + 2. **主动更新**:每天凌晨,AI 扫描昨日对话,如果发现用户说了“以后别...”,自动追加到这里。 + +### **2.2 关键决策 & 踩过的坑 \-\> Weekly Reports (编年史)** + +这部分信息属于\*\*“项目历史”\*\*,不需要每次对话都挂在嘴边,但当用户问“回顾一下”时,需要能查到。 + +* **存储表**:iit\_weekly\_reports +* **内容示例 (Week 2026-05)**: + \[进度\]: 本周入组 3 人,累计 15 人。 + \[决策\]: 2月3日 PI 会议决定:暂停筛选 "间质性肺炎" 既往史患者。 + \[问题\]: 曾尝试自动录入化验单,但因 OCR 精度不足失败(踩坑记录),已回退为人工复核模式。 + +* **使用机制**: + * 当用户问:“我们之前为什么暂停筛选肺炎患者?” + * 意图识别判断为 QUERY\_HISTORY。 + * 系统一次性拉取过去所有周报 (150条记录),拼接成“编年史”,喂给 LLM 阅读并回答。 + +## **3\. 为什么这样设计?** + +1. **偏好 (Preferences)** 必须是 **Hot** 的: + * 如果存到周报里,AI 可能聊着聊着就忘了“老板不喜欢废话”这个规矩。只有放在 Hot Memory (System Prompt) 里,才能保证每一句回复都符合老板口味。 +2. **决策 (Decisions)** 必须是 **Time-Series** 的: + * 决策往往有时间背景。存到周报里,天然带有时间戳(Week 5 做的决定)。这样 AI 才能回答“上个月做了什么决定”。 +3. **踩坑 (Lessons)** 需要 **人工干预**: + * 如果只是 AI 自动总结,可能会漏。 + * V2.8 允许你在 Hot Memory 里手动写上一条:“【系统禁令】严禁在未授权情况下删除数据”。这条“人工植入的记忆”比任何 AI 总结都管用。 + +## **4\. 总结:信息流向图** + +graph TD + User\[用户输入\] \--\>|对话流| Raw\[原始日志\] + + subgraph "实时影响 (Hot)" + Raw \--\>|每日提炼| Prefs\[用户偏好\] + Raw \--\>|每日提炼| Context\[高频上下文\] + Admin\[人工干预\] \--\>|手动编辑| Prefs + end + + subgraph "历史归档 (History)" + Raw \--\>|每周汇总| Decisions\[关键决策\] + Raw \--\>|每周汇总| Lessons\[踩坑记录\] + end + + Prefs \--\>|注入| NextPrompt\[下一次对话\] + Context \--\>|注入| NextPrompt + + Decisions \-.-\>|按需查询| NextPrompt + + +[image1]: \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆检索与路由逻辑详解.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆检索与路由逻辑详解.md new file mode 100644 index 00000000..cccf3e79 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.8:记忆检索与路由逻辑详解.md @@ -0,0 +1,143 @@ +# **IIT Manager Agent V2.8:记忆检索与路由逻辑详解** + +**核心机制:** 意图驱动的按需检索 (Intent-Driven Retrieval) + +**目的:** 既保证 AI 懂当下(Hot),又保证 AI 懂历史(History),同时节省 Token。 + +## **1\. 记忆存储全景图 (Storage Map)** + +在 V2.8 中,我们有三个核心存储位置,各司其职: + +| 记忆类型 | 存储表 | 字段 | 内容特征 | 更新频率 | +| :---- | :---- | :---- | :---- | :---- | +| **1\. 流水账 (Raw Stream)** | iit\_conversation\_history | content (JSON) | 未经加工的原始对话。 | **实时** (每秒) | +| **2\. 热记忆 (Hot Context)** | iit\_project\_memory | config (Markdown) | 用户偏好、当前状态、系统禁令。 | **每日** (AI 提炼) | +| **3\. 历史书 (Weekly Reports)** | iit\_weekly\_reports | summary (Text) | 高度浓缩的周报、关键决策、踩坑记录。 | **每周** (AI 归档) | + +## **2\. 调取顺序与逻辑 (Retrieval Logic)** + +当 PI 发问时,系统并不是一股脑把所有记忆都塞进去,而是分三步走: + +### **第一步:无条件注入 (Always On)** + +**无论 PI 问什么,必须先注入“热记忆”。** + +* **来源**:iit\_project\_memory +* **内容**:用户偏好(如“PI 喜欢简报”)、当前项目状态。 +* **理由**:这是 Agent 的“人设”和“底线”,一刻也不能忘。 + +### **第二步:意图识别 (Intent Detection)** + +系统分析 PI 的问题,判断他想问“现在”还是“过去”。 + +* **情况 A:问现在/执行任务** (如 "查下 P001", "生成本周周报") + * **动作**:**不调取** 历史书。 + * **理由**:解决当下问题不需要翻阅 3 年前的老黄历,避免干扰 AI。 + * **Context** \= Hot Memory \+ Current Task Data +* **情况 B:问历史/趋势/回顾** (如 "回顾下去年的入组情况", "我们为什么暂停了筛选?") + * **动作**:**全量调取** 历史书 (iit\_weekly\_reports)。 + * **理由**:需要上帝视角。 + * **Context** \= Hot Memory \+ All Weekly Reports + +### **第三步:上下文组装 (Prompt Assembly)** + +最终发给 LLM 的 Prompt 是这样组装的: + +\[System Prompt\] +你是一个临床研究助手... + +\[Hot Memory\] (来自 iit\_project\_memory) +\- 用户偏好: 简洁、数据驱动 +\- 当前状态: 入组阶段 + +\[History Context\] (仅在情况 B 下注入) +Week 1: 启动项目... +Week 2: 发现 P001 不良事件... +... +Week 50: 决定放宽标准... + +\[User Question\] +我们之前为什么暂停筛选? + +## **3\. 实战场景演示 (Scenario Walkthrough)** + +### **场景 1:PI 问 "P001 入组了吗?"** + +1. **加载 Hot Memory**:获取偏好(简洁回复)。 +2. **意图识别**:QUERY\_DATA (查数据)。 +3. **决策**:**不需要** 查历史书。 +4. **行动**:调用 read\_clinical\_data 工具去 REDCap 查实时数据。 +5. **回答**:"已入组,时间是 2026-02-05。" + +### **场景 2:PI 问 "最近入组太慢了,我们之前有没有讨论过怎么解决?"** + +1. **加载 Hot Memory**:获取偏好。 +2. **意图识别**:QUERY\_HISTORY (查历史决策)。 +3. **决策**:**需要** 查历史书。 +4. **行动**: + * 从 iit\_weekly\_reports 拉取过去 20 周的 summary。 + * 拼接成 3000 字的文本。 + * 喂给 LLM。 +5. **LLM 思考**:阅读周报,发现 Week 12 记录了“增加受试者交通补贴”的决策,Week 15 记录了“在门诊增派 CRC”的决策。 +6. **回答**:"我们曾在第 12 周尝试增加补贴,第 15 周增派了 CRC,当时效果有短暂提升..." + +### **场景 3:PI 问 "我上次跟你说的那个只要看结果的规矩,你记得吗?"** + +1. **加载 Hot Memory**:获取偏好。 +2. **AI 自检**:在 Hot Memory 里看到了 \- \[PI\]: 只要看结果 这条记录。 +3. **回答**:"记得的,教授。您要求汇报时只列数据结论,不要冗长的过程描述。我会严格遵守。" + * *注意:这个问题不需要查周报,也不需要查流水账,直接看 Hot Memory 就行。* + +## **4\. 总结:给开发者的伪代码** + +// ChatService.ts + +async handleMessage(userId, message) { + // 1\. 总是加载热记忆 (Hot) + const hotMem \= await prisma.iitProjectMemory.findUnique(...); + + // 2\. 意图识别 + const intent \= await this.intentService.detect(message); + + let historyContext \= ""; + + // 3\. 按需加载历史书 (History) + if (intent.type \=== 'QUERY\_HISTORY' || intent.type \=== 'ANALYZE\_TREND') { + const reports \= await prisma.iitWeeklyReport.findMany({ + orderBy: { weekNumber: 'asc' } + }); + historyContext \= reports.map(r \=\> \`\[W${r.weekNumber}\]: ${r.summary}\`).join('\\n'); + } + + // 4\. 组装最终 Prompt + const finalPrompt \= \` + ${hotMem.content} + ${historyContext} + User: ${message} + \`; + + // 5\. 调用 LLM + return await this.llm.chat(finalPrompt); +} + +**记住这个口诀:** + +**“偏好时刻带,历史按需查,流水账只用来生成周报。”** + +## **5\. 复杂度控制与防失控指南 (Safety Guardrails)** + +为了防止系统变得“复杂不可控”,请严格遵守以下开发军规: + +### **5.1 容量限制 (The Cap)** + +* **Hot Memory**: 限制在 **2000 Tokens** 以内。如果超标,触发 MemoryPruningJob (记忆修剪任务),让 AI 自动总结或通知管理员手动删除。 +* **History Book**: 每次加载不超过 **50 周** 的周报。如果项目运行了 10 年,只加载最近 1 年的周报,或者先让 AI 生成“年度总结”。 + +### **5.2 隔离原则 (Isolation)** + +* **任务隔离**:执行 QC\_TASK (质控) 时,**严禁**加载 History Context。质控必须基于当下的事实,不能受历史干扰。 +* **数据隔离**:LLM 永远没有权限直接读取 iit\_conversation\_history 表。这条物理隔绝保证了 Agent 永远不会被海量噪音淹没。 + +### **5.3 人工介入 (The Kill Switch)** + +* **可读可改**:后台必须提供 Hot Memory 的编辑器。如果 Agent 记住了错误的规则(例如“不需要查年龄”),医生可以直接进去删掉那一行 Markdown。**这是系统失控时的“急停按钮”。** \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.9 补充设计:主动性与用户画像增强.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.9 补充设计:主动性与用户画像增强.md new file mode 100644 index 00000000..cfb7bfce --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/IIT Manager Agent V2.9 补充设计:主动性与用户画像增强.md @@ -0,0 +1,210 @@ +# **IIT Manager Agent V2.9 补充设计:主动性与用户画像增强** + +**目标:** 响应智能化评估中的 P0 需求,补齐“主动服务”和“个性化”短板,并增强复杂任务处理能力。 + +**原则:** 坚持 **Postgres-Only**,复用现有的 **Skill (JSON)** 和 **ToolsService**。 + +## **1\. 主动提醒机制设计 (The Proactive Engine)** + +我们不需要写死一堆 cron 脚本,而是将“主动提醒”抽象为一种特殊的 **Skill**。 + +### **1.1 架构复用逻辑** + +* **触发源**:pg-boss 的定时任务 (Scheduler)。 +* **规则载体**:iit\_skills 表(新增 trigger\_type: 'cron')。 +* **执行者**:QcEngineService (复用现有的逻辑)。 + +### **1.2 数据库 Schema 扩展** + +在 iit\_skills 表中增加调度字段: + +model IitSkill { + // ... 原有字段 + triggerType String @default("webhook") // 'webhook' | 'cron' | 'event' + cronSchedule String? // 例如: "0 9 \* \* \*" (每天9点) + + // 核心配置 (JSON) + config Json +} + +### **1.3 Skill 配置示例:访视提醒** + +这是一个“每天早上9点检查即将到期访视”的技能配置。 + +{ + "name": "即将到期访视提醒", + "description": "扫描未来3天内需要访视的患者", + + // 核心:复用 Engine A 的逻辑来筛选数据 + "hard\_rules": \[ + // 这里我们稍微扩展一下 hard\_rules 的能力,支持 SQL-like 的筛选 + // 或者直接调用工具获取列表 + \], + + // 核心:复用 Engine B (LLM) 来生成人性化通知 + "soft\_instructions": \[ + { + "instruction": "请调用 \`get\_upcoming\_visits(days=3)\` 工具。如果有数据,请根据【用户偏好】生成一条提醒消息。如果没有数据,什么都不做。", + "tools": \["get\_upcoming\_visits", "send\_wechat\_msg"\] + } + \] +} + +### **1.4 代码实现 (SchedulerService.ts)** + +// SchedulerService.ts + +async initCronJobs() { + // 1\. 从数据库加载所有 triggerType \= 'cron' 的 Skill + const cronSkills \= await prisma.iitSkill.findMany({ + where: { triggerType: 'cron', isActive: true } + }); + + for (const skill of cronSkills) { + // 2\. 注册到 pg-boss + await this.boss.schedule( + \`run-skill-${skill.id}\`, + skill.cronSchedule, + { skillId: skill.id } + ); + } +} + +// Worker 处理逻辑 +async handleCronSkill(job) { + const { skillId } \= job.data; + const skill \= await prisma.iitSkill.findUnique({ where: { id: skillId } }); + + // 3\. 复用现有的引擎! + // 就像处理 Webhook 一样处理 Cron,架构极其统一 + await this.qcEngine.runSoftAgent( + skill.config.soft\_instructions\[0\].instruction, + { source: 'cron\_job' } // Context + ); +} + +**效果:** 你只需要在后台配一个 JSON,Agent 就会每天早上 9 点醒来,查数据,然后发微信:“张医生,P005 明天该来复查了。” + +## **2\. 用户画像增强 (User Profiling)** + +我们已经在 V2.8 中设计了 iit\_project\_memory (Hot Memory),现在只需要把“画像”结构化。 + +### **2.1 存储设计** + +在 iit\_user\_preferences 表中,我们将 preferences 字段细分为两类: + +1. **显性偏好 (Explicit)**:用户明确说的(“我要简报”)。 +2. **隐性画像 (Implicit)**:系统分析出来的(“他是 PI,关注宏观数据”)。 + +// preferences 字段内容示例 +{ + "communication\_style": "concise", // 简练 | 详尽 + "role\_tag": "PI", // PI | CRC | CRA + "focus\_areas": \["AE", "Enrollment"\], // 关注点 + "notification\_time": "08:30", // 接收时间 + "feedback\_history": { // 简单的反馈记录 + "thumbs\_up": 12, + "thumbs\_down": 1 + } +} + +### **2.2 自动画像更新 (The Profiler)** + +我们在 ChatService 结束一次对话后,异步触发一个微型任务:**“画像侧写”**。 + +// ProfilerService.ts + +async updateProfile(userId: string, lastConversation: any\[\]) { + const currentProfile \= await this.getProfile(userId); + + // 让 LLM 当心理分析师 + const prompt \= \` + 基于刚刚的对话,更新用户画像。 + 对话:${JSON.stringify(lastConversation)} + 旧画像:${JSON.stringify(currentProfile)} + + 提取规则: + 1\. 如果用户抱怨"太长了",将 style 设为 concise。 + 2\. 如果用户问了"安全性",将 AE 加入 focus\_areas。 + + 返回新的 JSON。 + \`; + + const newProfile \= await this.llm.chat(prompt); + await this.saveProfile(userId, newProfile); +} + +## **3\. 反馈循环 (Feedback Loop \- Lite)** + +不要做复杂的强化学习(RLHF),只做简单的\*\*“点赞/点踩”\*\*。 + +### **3.1 交互设计** + +在 Agent 回复的每条消息下面(如果是小程序/网页),加两个小按钮:👍 / 👎。 + +### **3.2 逻辑闭环** + +1. **用户点踩 (👎)**。 +2. **前端**:弹窗询问“哪里不好?”(选项:太啰嗦、不准确、没听懂)。 +3. **后端**: + * 将这条 Negative Feedback 写入 iit\_conversation\_history。 + * **关键动作**:触发一次 Hot Memory 更新。 + * **Prompt**:"用户对刚才的回答点了踩,原因是'太啰嗦'。请在 Hot Memory 中记下一条禁令:\[针对该用户,严禁输出超过 100 字的废话\]。" + +**结果**:Agent 下次遇到这个用户,真的会改。这就是最真实的“智能感”。 + +## **4\. 多意图与任务规划增强 (Multi-Intent Handling)** + +针对“先做 A,再做 B”的复杂指令(如:“查一下 P001 的状态,然后通知张医生”),我们不需要引入重型 Planner,而是通过 **ReAct \+ Prompt Engineering** 实现。 + +### **4.1 核心策略:ReAct 自然涌现** + +**结论**:ReAct 引擎本身就是一个轻量级 Planner。 + +不需要额外的 Planner 模块,只需要在 **System Prompt** 中显式教导 Agent 如何拆解任务。 + +### **4.2 Prompt 增强设计** + +在 backend/src/modules/iit-manager/engines/ReActEngine.ts 的系统提示词中加入以下**思维链(Chain of Thought)指令**: + +const REACT\_SYSTEM\_PROMPT \= \` +你是一个临床研究助手。你可以使用工具来回答问题。 + +核心原则: +1\. \*\*任务拆解\*\*:如果用户请求包含多个步骤(例如"先...再...","查完...发给..."),请务必分步执行。 + \- 不要试图在一个步骤里做完所有事。 + \- 比如:先调用查询工具,获得结果后,再调用发送工具。 +2\. \*\*依赖管理\*\*:如果步骤 B 依赖步骤 A 的结果,必须先执行 A,观察 A 的结果,再执行 B。 +3\. \*\*完成检查\*\*:在输出 Final Answer 之前,检查是否完成了用户的所有指令。 + +思考格式示例: +Thought: 用户想查 P001 并通知张医生。我有两个任务。首先,我需要查 P001 的状态。 +Action: 调用 read\_clinical\_data(id='P001')... +Observation: {"status": "Enrolled"} +Thought: 我已经查到了状态。现在我需要执行第二个任务:发消息给张医生。 +Action: 调用 send\_wechat\_msg(to='Dr. Zhang', content='P001 status is Enrolled')... +... +\`; + +### **4.3 为什么不用 Planner?** + +对于 2 人团队,显式 Planner(先生成计划列表,再循环执行)过于沉重且难以维护状态。ReAct 的 **“边走边想”** 模式完全能够覆盖 90% 的“线性多步任务”。 + +## **5\. 总结与建议** + +| 需求 | 解决方案 | 复杂度 | 推荐实施阶段 | +| :---- | :---- | :---- | :---- | +| **主动提醒** | **Cron Skill** (复用 pg-boss \+ QC Engine) | 低 | **Phase 4** (与周报一起做) | +| **用户画像** | **Profiler Job** (对话后异步分析) | 中 | **Phase 5** (智能化增强) | +| **反馈循环** | **点赞按钮 \+ 记忆写入** | 低 | **Phase 5** | +| **多意图处理** | **ReAct Prompt 优化** (无需新模块) | 极低 | **Phase 5** (开发 ReActEngine 时同步完成) | + +### **对你的建议** + +1. **全盘接受 P0 建议**:主动提醒和用户画像是成本低、收益大的功能。 +2. **技术上保持克制**: + * 别写专门的 Alert Engine,用 Cron Skill 复用现有的引擎。 + * 别搞复杂的推荐算法,用 JSON 存画像就够了。 + * 别搞复杂的 Planner,用 **Prompt** 教会 ReAct 拆解任务就够了。 + +这样,你的 V2.6+ 架构就真正补齐了“右脑”的情商短板,变成了一个 **“有记忆、会主动、懂人情、能办事”** 的完整 Agent。 \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/开发计划审核备忘录.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/开发计划审核备忘录.md new file mode 100644 index 00000000..4b669c83 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/开发计划审核备忘录.md @@ -0,0 +1,44 @@ + + +## **1\. 风险预警 (Risk Alert)** + +### **🔴 高风险:Phase 4 排期过饱和** + +* **问题**:Week 4 同时安排了"视觉识别"、"周报系统"、"高级伦理规则"开发,对于 2 人团队工作量过载。 +* **建议**:**战略性放弃视觉识别的自动录入功能**,将其降级为 MVP(仅识别展示)。将资源集中在"周报系统"上,因为周报对 PI 的价值感知更强。 + +### **🟠 中风险:SOP 状态丢失** + +* **问题**:SopEngine 在内存中运行 while 循环。若 Node.js 服务在长任务中途重启(如发布新版),当前流程会丢失。 +* **建议**:在 while 循环的每一步,增加 await updateTaskProgress(),将 currentNode 写入数据库,确保有据可查。 + +### **🟠 中风险:AI 自动写入合规性** + +* **问题**:Phase 4 提到"高置信度自动录入"。在 GCP 原则下,AI 直接修改源数据存在合规隐患。 +* **建议**:坚持 **Shadow State (影子状态)** 原则。VisionService 的结果应生成 PROPOSED 状态的记录,必须由 CRC 人工点击确认。 + +## **2\. 架构优化建议 (Architecture Tuning)** + +### **2.1 增加 "Dry Run" 机制** + +* **痛点**:JSON 配置写在数据库里,出错难排查。 +* **方案**:在 ToolsService 中增加 simulate() 方法,允许在不产生副作用的情况下测试 Skill 逻辑。 + +### **2.2 强化字段映射管理** + +* **痛点**:iit\_field\_mapping 表如果只能通过 SQL 修改,运营成本极高。 +* **方案**:在 Week 3 增加一个简单的 Admin API 或界面,用于管理字段别名。 + +## **3\. 调整后的推荐路线图 (Refined Roadmap)** + +* **Week 1 (基础)**:数据库 \+ HardRuleEngine \+ ToolsService (不变) +* **Week 2 (引擎)**:SoftRuleEngine \+ SopEngine (**增加状态持久化**) +* **Week 3 (联调)**:配置第一个 Skill \+ 端到端测试 (**增加 Dry Run 工具**) +* **Week 4 (扩展)**: + * **P0**: 定时任务 \+ 周报系统 (Scheduler \+ Report) + * **P1**: 高级质控工具 (Visit Window) + * **P2 (延后)**: 视觉识别 (VisionService) \-\> **建议移至 V2.6** + +## **4\. 结论** + +架构选型(Postgres-Only \+ Service-First)非常精准。只需在 Phase 4 做适当减法,并注意状态持久化细节,即可确保项目按时、高质量交付。 \ No newline at end of file diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/微信开发技术问题咨询.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/微信开发技术问题咨询.md new file mode 100644 index 00000000..59a1a5b2 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/微信开发技术问题咨询.md @@ -0,0 +1,169 @@ + + +**第二章 微信服务号集成与Error 200002根因深度剖析** + +用户提到的“配置消息推送失败: invalid args, 200002”且“后端无日志”,是微信开发中极其典型且令人抓狂的现象。这通常不是单一的代码错误,而是网络层、网关层与应用层叠加的系统性故障。 + +### **2.1 现象解码:“无日志”的恐怖** + +当微信后台提示配置失败,而您的服务器访问日志(Access Log)空空如也时,这意味着**请求根本没有到达您的应用服务器**。 + +#### **2.1.1 网络层的隐形墙:防火墙与安全组** + +微信服务器位于腾讯的公网IP池中。当它发起HTTP/HTTPS请求时,如果您的服务器部署在阿里云、腾讯云或AWS等云平台,\*\*安全组(Security Group)\*\*是第一道关卡。 + +* **诊断**:大多数云服务器默认只开放22端口(SSH)。 +* **排查**:检查云控制台的入站规则。必须允许0.0.0.0/0对TCP 80(HTTP)和443(HTTPS)的访问。 +* **误区**:不要试图将微信的IP加入白名单。微信的出口IP是不定期的海量IP池,必须全量开放。 + +#### **2.1.2 接入层的黑洞:Nginx配置错误** + +如果您使用了Nginx作为反向代理,请求可能在Nginx层被丢弃,导致后端Java/Python应用收不到日志。 + +* **Server Name匹配**:如果Nginx配置了server\_name www.example.com,而您在微信后台填写的是直接IP地址,Nginx可能因为找不到匹配的Host而直接拒绝连接。 +* **SSL握手失败**:如果配置的是HTTPS URL,但服务器的SSL证书不完整(如缺少中间证书)或协议版本过旧(TLS 1.0),微信服务器会终止握手,请求甚至不会进入Nginx日志。 + +### **2.2 Error 200002 “invalid args” 的代码级解剖** + +如果请求成功到达服务器(可以通过Nginx日志确认),但微信仍然报200002,则问题出在**握手协议的实现**上。这是最考验开发者基本功的环节。 + +#### **2.2.1 握手协议的三大铁律** + +微信在验证服务器有效性时,发送signature, timestamp, nonce, echostr四个参数。开发者必须完成以下步骤: + +1. **字典序排序(Lexicographical Sorting)**: + * **错误高发点**:很多开发者将数字类型的timestamp直接参与排序,或者使用了错误的排序算法。 + * **正确逻辑**:将\[token, timestamp, nonce\]放入一个字符串数组,调用标准的字符串排序方法(如Python的list.sort(),Java的Arrays.sort())。 +2. **SHA1哈希(Hashing)**: + * **错误高发点**:编码问题。在Python 3中,hashlib.sha1()接受的是字节流(bytes),必须先对拼接后的字符串进行.encode('utf-8')。 +3. **返回值的纯净性(Response Purity)**: + * **错误高发点**:这是导致200002最隐蔽的原因。微信要求**原样返回**echostr的明文。 + * **忌讳**: + * 不能返回JSON格式(如{"ret": "success", "echo": "..."})。 + * 不能包含引号(如"abc...")。 + * 不能包含换行符或空格。 + * **Content-Type**:虽然微信不强制,但最佳实践是设置Response Header为text/plain。 + +#### **2.2.2 深度调试代码示例(Python Flask版)** + +以下是一个通过了生产环境验证的标准校验代码,用于替换可能存在缺陷的逻辑: + +Python + +import hashlib +from flask import Flask, request, make\_response + +app \= Flask(\_\_name\_\_) + +@app.route('/wechat', methods=) +def check\_signature(): + \# 1\. 获取参数 + token \= "YOUR\_TOKEN" \# 必须与微信后台填写的完全一致 + signature \= request.args.get('signature') + timestamp \= request.args.get('timestamp') + nonce \= request.args.get('nonce') + echostr \= request.args.get('echostr') + + \# 2\. 空值校验(防御性编程) + if not all(\[signature, timestamp, nonce, token\]): + return "Invalid Request", 400 + + \# 3\. 字典序排序 + li \= \[token, timestamp, nonce\] + li.sort() + + \# 4\. 拼接与哈希 + temp\_str \= "".join(li) + hash\_str \= hashlib.sha1(temp\_str.encode('utf-8')).hexdigest() + + \# 5\. 对比与返回 + if hash\_str \== signature: + \# 关键:使用make\_response确保返回的是纯文本,不受框架序列化影响 + response \= make\_response(echostr) + response.headers\['content-type'\] \= 'text/plain' + return response + else: + \# 记录错误日志以便排查 + app.logger.error(f"Sig check failed. Calc: {hash\_str}\!= Req: {signature}") + return "Signature Failed", 403 + +if \_\_name\_\_ \== '\_\_main\_\_': + app.run(port=80) + +### **2.3 “我很痛苦”的终结方案:抓包与模拟** + +如果依然失败,请停止盲目修改代码,采用**证据驱动调试**。 + +1. **自测脚本**:编写一个Python脚本,模拟微信的逻辑,生成签名并发送GET请求给自己的服务器。如果自测通过但微信不通过,问题一定在网络层(防火墙/CDN/WAF)。 +2. **网络抓包**:在服务器上使用tcpdump \-i eth0 port 80 \-w wechat\_debug.pcap。在微信后台点击提交后,停止抓包并用Wireshark分析。 + * 如果抓不到包 \-\> 网络不通。 + * 如果抓到包但应用没日志 \-\> Nginx配置错误。 + * 如果应用返回了200但微信报错 \-\> 检查Response Body是否有隐藏字符(BOM头、换行符)。 + +## --- + +**第三章 全场景调试指南:工具链与最佳实践** + +对于“如何调试”、“本地还是远程”的疑问,行业内的最佳范式是\*\*“本地隧道调试”\*\*(Local Tunnel Debugging)。直接在远程服务器上修改代码并重启服务来调试微信接口,效率极低且风险极高。 + +### **3.1 本地调试的核心逻辑:内网穿透** + +微信服务器无法直接访问你笔记本电脑上的localhost:8080。因此,需要一个“隧道”,将公网请求转发到本地。 + +#### **3.1.1 工具选型与配置** + +针对中国网络环境,推荐以下工具链: + +| 工具名称 | 适用场景 | 优点 | 缺点 | 推荐指数 | +| :---- | :---- | :---- | :---- | :---- | +| **ngrok** | 快速验证 | 命令简单,全球通用 | 免费版域名随机变化,国内连接有时不稳定 | ⭐⭐⭐ | +| **frp** | 长期开发 | **国内标准**。需一台公网VPS。稳定,域名固定 | 配置稍繁琐,需维护服务端 | ⭐⭐⭐⭐⭐ | +| **cpolar/Natapp** | 无VPS用户 | 专为国内优化,速度快 | 免费版有限制,自定义域名需付费 | ⭐⭐⭐⭐ | + +#### **3.1.2 FRP实战配置SOP** + +假设您有一台腾讯云/阿里云服务器(IP: 1.2.3.4),这是最稳定的方案。 + +1. **服务端(VPS)配置 frps.ini**: + Ini, TOML + \[common\] + bind\_port \= 7000 \# 用于frp内部通信 + vhost\_http\_port \= 8080 \# 微信访问的端口 + + 启动:./frps \-c frps.ini +2. **客户端(本地电脑)配置 frpc.ini**: + Ini, TOML + \[common\] + server\_addr \= 1.2.3.4 + server\_port \= 7000 + + \[wechat-debug\] + type \= http + local\_port \= 5000 \# 本地Python/Java服务的端口 + custom\_domains \= wechat.your-startup.com \# 解析到1.2.3.4的域名 + + 启动:./frpc \-c frpc.ini +3. **调试闭环**: + * 在微信后台配置URL为:http://wechat.your-startup.com:8080/callback + * 当微信发送请求时,流量路径为:微信 \-\> VPS(7000) \-\> 隧道 \-\> 本地电脑(5000)。 + * **效果**:您可以在本地IDE(PyCharm/VSCode)中打断点,实时查看微信发来的XML数据包,单步调试每一行代码。这是解决“痛苦”的根本途径。 + +### **3.2 微信官方调试工具的使用** + +除了内网穿透,腾讯提供了**微信开发者工具**(主要用于小程序,但也包含公众号调试)和**在线接口调试工具**。 + +* **在线接口调试工具** 6: + * 地址:[https://developers.weixin.qq.com/apiExplorer](https://developers.weixin.qq.com/apiExplorer) + * 用途:当您的Token配置成功,但消息推送失败时,可以用它模拟微信服务器向您的URL发送POST请求。它会详细显示您的服务器返回了什么(HTTP Code, Body),帮助快速定位“回包格式错误”。 + +### **3.3 行业最佳实践范式** + +1. **TraceID全链路追踪**:在入口处生成一个UUID作为TraceID,贯穿Nginx日志、应用日志和数据库日志。当某个课题组反馈机器人不回话时,通过TraceID可秒级定位问题环节。 +2. **日志分级与脱敏**: + * **DEBUG级**:打印完整的XML/JSON请求体(注意:生产环境需对患者姓名、ID进行掩码脱敏)。 + * **INFO级**:记录核心链路节点(收到消息 \-\> 开始推理 \-\> 推送完成)。 + * **ERROR级**:记录所有的API调用非200状态码及异常堆栈。 +3. **容错与重试**:微信接口偶尔会出现网络抖动。在调用send接口时,务必封装重试逻辑(Exponential Backoff),遇到超时或5xx错误时自动重试3次。 + +## --- + diff --git a/docs/03-业务模块/IIT Manager Agent/05-测试文档/潜在的具体风险与问题.md b/docs/03-业务模块/IIT Manager Agent/05-测试文档/潜在的具体风险与问题.md new file mode 100644 index 00000000..63b8d563 --- /dev/null +++ b/docs/03-业务模块/IIT Manager Agent/05-测试文档/潜在的具体风险与问题.md @@ -0,0 +1,64 @@ +# **潜在的具体风险与问题** + +即使架构完美,细节仍可能翻车。以下是代码落地时可能遇到的“暗礁”以及针对性的应对策略。 + +## **🔴 风险一:意图路由的“延迟感”** + +* **位置**:IntentService (Day 20-21) +* **问题**:用户发一句话,系统先调一次 LLM (路由),再调一次 SOP/ReAct (执行)。 +* **后果**:响应延迟 \= LLM(路由) \+ LLM(执行) \+ 网络开销。可能导致用户发完消息后 **5-8 秒** 才有反应。在微信的即时通讯场景下,这个体感很差,用户容易认为系统卡死。 +* **对策**: + 1. **流式欺骗 (UX Trick)**:收到消息立刻回一个“正在思考...”或“正在查询...”的状态(企业微信 API 支持中间态更新或 typing 状态)。 + 2. **快速通道 (Fast Path)**:对于极短的、特征明显的指令(如“质控”、“报表”、“帮助”),先用 **正则 (Regex) / 关键词 (Keywords)** 进行拦截。只有正则拦截不住的复杂长句,再走 LLM 意图路由。 + * *原则*:**混合路由 \> 纯 LLM 路由**。 + +## **🔴 风险二:ReAct 的“多嘴”风险** + +* **位置**:ReActEngine (Day 22-23) +* **问题**:ReAct 的核心机制是“思考-行动-观察”循环。模型倾向于在思考过程中输出大量中间步骤,例如: + * *AI Thought*: "用户想查 P001,我需要先调用 search 工具,然后..." + * *AI Thought*: "哎呀,没查到数据,可能需要换个参数..." +* **后果**:如果将这些中间思考过程(Internal Monologue)全部实时推送给用户,用户会觉得“这个机器人话痨”、“不够干练”甚至“不自信”。 +* **对策**: + 1. **UI 静默处理**:只把 Final Answer 发送给用户。 + 2. **调试可见**:中间的 Trace (思考轨迹) 只存入后台日志,或者仅在 Admin 管理端的 Debug 模式下显示,用于排查问题。 + +## **🔴 风险三:SOP 状态机的“死锁”** + +* **位置**:SopEngine (Day 8-9) +* **问题**:SOP 流程中经常包含“人工介入”环节。例如,如果 SOP 走到了 review\_required(等待人工复核)节点,流程会暂停。如果 CRC 一直不点击确认,或者忘记处理,这个任务就会一直挂在内存或队列中。 +* **后果**:pg-boss 可能会认为任务超时(Timeout),触发反复重试机制,最终判定失败(Failed),导致流程异常终止。 +* **对策**: + 1. **区分状态**:在系统设计中严格区分 **“系统阻塞 (System Blocking)”** 和 **“业务等待 (Business Waiting)”**。 + 2. **挂起机制**:如果是“等人”,任务状态应设为 SUSPENDED(挂起)。此时应从活动队列中移除,不再占用 Worker 资源。 + 3. **唤醒机制**:当 CRC 在前端点击“确认”后,触发一个回调,创建一个新的 Job 来唤醒流程,继续执行后续步骤。 + +## **3\. 给 2 人团队的“瘦身版”执行建议** + +基于 V2.6 计划,为了确保在有限人力下 100% 交付核心价值,建议执行以下 **“瘦身”** 策略: + +### **✂️ 裁剪清单 (不做或延后)** + +1. **\[延后\] Phase 6 全阶段 (视觉能力)**:放到 V3.0 再说。目前的文本交互和结构化数据处理已经足够复杂,且 OCR 准确率调优是个无底洞。 +2. **\[裁剪\] Phase 5 的语义记忆 (pgvector)**:暂时只做简单的文本记录(如 JSON 或 Markdown)。跨度极大的语义召回在初期并非刚需。 +3. **\[简化\] 伦理合规检测 (T11.3)**:伦理规则极度复杂且非结构化。建议先写死几条最核心的规则(如“知情同意日期必须早于入组日期”),暂不做通用的伦理引擎。 + +### **✨ 聚焦清单 (必须做好)** + +1. **P0: ToolsService 的健壮性**:这是整个系统的地基。一定要加上 V2.3 文档中提到的 **“字段模糊映射”**,否则 LLM 根本调不通工具,体验会崩塌。 +2. **P0: 每日早报 (Morning Brief)**:这是用户(PI)感知最强的功能。一定要做得漂亮、准时,这是体现 Agent “主动性”的关键。 +3. **P0: 意图路由的混合模式**:正则 \+ LLM 双保险,这是保证响应速度和降低 Token 成本的关键。 + +## **4\. 最终评价** + +这份 **V2.6 计划** 是一个经过深思熟虑的、高水平的架构设计。 + +* **它懂业务**:SOP 状态机完美解决了医疗合规与流程锁定的痛点。 +* **它懂用户**:双脑架构(ReAct \+ SOP)解决了“机器人太死板”的交互体验痛点。 +* **它懂工程**:极简架构(Postgres-Only \+ Service Class)解决了小团队维护复杂微服务的痛点。 + +**结论**:只要你们能忍住不去做“视觉识别”和“复杂语义记忆”这些锦上添花的功能,按部就班地把前 5 个 Phase 做完,这将是一个在医疗垂直领域极具竞争力的产品。 + +**最后一步建议:** + +把这份文档发给团队,然后开一个 **Kick-off Meeting**,明确告诉大家:“我们要造的是**双脑 Agent**,但我们从\*\*左脑(SOP)\*\*开始造,视觉能力先放进冰箱。” \ No newline at end of file diff --git a/frontend-v2/nginx.conf b/frontend-v2/nginx.conf index adefb978..d4be55ea 100644 --- a/frontend-v2/nginx.conf +++ b/frontend-v2/nginx.conf @@ -102,6 +102,29 @@ http { # ==================== API 反向代理 ==================== + # 微信服务号回调代理(关键!微信验证需要) + location /wechat/ { + # 代理到后端服务 + proxy_pass http://backend; + + # 保留原始请求信息 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时配置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲配置 + proxy_buffering off; + + # HTTP 1.1 + proxy_http_version 1.1; + } + # 后端 API 代理(关键配置) location /api/ { # 文件上传大小限制(PKB 文档上传等场景) diff --git a/frontend-v2/src/modules/aia/styles/chat-workspace.css b/frontend-v2/src/modules/aia/styles/chat-workspace.css index 0a72fc17..711c7da4 100644 --- a/frontend-v2/src/modules/aia/styles/chat-workspace.css +++ b/frontend-v2/src/modules/aia/styles/chat-workspace.css @@ -1005,15 +1005,11 @@ .message-bubble .markdown-content ol { list-style-type: decimal; -} - -.message-bubble .markdown-content li { +}.message-bubble .markdown-content li { margin: 6px 0; line-height: 1.6; display: list-item; -} - -.message-bubble .markdown-content li::marker { +}.message-bubble .markdown-content li::marker { color: #4F6EF2; } @@ -1034,4 +1030,4 @@ border-radius: 4px; font-family: 'Monaco', 'Consolas', 'Courier New', monospace; font-size: 0.9em; -} +} \ No newline at end of file