# IIT Manager Agent - Phase 1.5 AI对话能力开发计? > **版本**: v3.0(极简?+ 上下文记?+ Dify知识库) > **创建日期**: 2026-01-03 > **最新更?*: 2026-01-04 > **状?*: ?**已完成(含Dify集成?* > **实际工作?*: ~2天(极简?+ Dify知识库) > **核心价?*: PI可在企业微信中自然对话查询REDCap真实数据 + 研究方案文档 > **核心成就**: ?REDCap数据集成 + ?上下文记?+ ?解决LLM幻觉 + ?**Dify知识库混合检?* --- ## 🚀 极简版快速启动(1天上线)?**通用能力层加速!** ### 🎉 重大发现:通用能力层已完善? **平台现状**?026-01-03调研结果): - ?**LLMFactory 完全就绪**?种模型(DeepSeek/Qwen/GPT-5/Claude),单例模式,零配置 - ?**ChatContainer 完全就绪**:Ant Design X组件,已在Tool C验证(~968行) - ?**环境变量已配?*:`DEEPSEEK_API_KEY`、`QWEN_API_KEY`?- ?**成熟实践**:ASL、DC模块已大量使用,稳定可靠 **核心优势**?```typescript // 后端LLM调用?行代码) import { LLMFactory } from '@/common/llm/adapters/LLMFactory.js'; const llm = LLMFactory.getAdapter('deepseek-v3'); const response = await llm.chat(messages, { temperature: 0.7 }); ``` ### 极简版功能范? ``` ?Day 1?-6小时? 实现基础对话 + 上下文记?+ "typing"反馈 - 复用LLMFactory?开发) - 创建ChatService.ts?小时? - 创建SessionMemory.ts?小时? - 修改WechatCallbackController?小时??Day 2?-6小时? Dify知识库集?+ 混合检索(2026-01-04完成? - 关联项目与Dify知识库(1小时? - 集成Dify检索到ChatService?小时? - 修复意图识别与数据注入bug?小时? - 端到端测试与文档记录?小时??暂不实现: 周报生成、复杂Tool Calling ``` ### 极简版架构(复用通用能力层) ``` PI提问 ?Node.js ?LLMFactory(deepseek-v3) ?生成回答 ?企业微信推? ? ? SessionMemory RedcapAdapter(可选) ``` **关键决策**?- 🚀 **复用LLMFactory**(已有,零开发,推荐`deepseek-v3`?- ?**只查REDCap数据**(已有RedcapAdapter,复用即可) - ?**不接Dify**(减少依赖,加快开发) - ?**上下文记?*(Node.js内存,存最?轮) - ?**正在输入反馈**(立即回"正在查询..."?- ?**单步路由**(不用ReAct循环? **预估工作量大幅降?*?-3??**1?*(因为LLM调用层已完善)⚡ --- ## 🎯 一、核心目标与价? ### 1.1 为什么需要AI对话能力? **当前状?*(Day 3已完成)?``` ?REDCap录入数据 ?Node.js捕获 ?企业微信推送通知 ``` **目标状?*(Phase 1.5): ``` ?PI在企业微信中提问 ?AI理解意图 ?查询数据/文档 ?智能回答 ``` **核心价?*?- 🚀 **主动查询**:PI不用等通知,随时问"现在入组多少人?" - 📊 **数据穿?*:实时查询REDCap数据(患者详情、质控状态) - 📚 **知识检?*:查询研究方案、CRF表格、入排标?- 💡 **智能理解**:自然语言提问,无需记忆命令 --- ## 📊 二、技术架构(基于本地Dify? ### 2.1 整体架构? ``` ┌─────────────────────────────────────────────────?? PI (企业微信) ?? "P001患者符合入排标准吗? ?└───────────┬─────────────────────────────────────? ? ?┌─────────────────────────────────────────────────?? Node.js 后端 (已有 WechatCallbackController) ?? ┌──────────────────────────────────────────? ?? ?1. 接收消息 (handleCallback) ? ?? ?2. 意图识别 (Intent Router) ? ?? ?3. 工具调用 (Tool Executor) ? ?? ?4. 企业微信推?(WechatService) ? ?? └──────────────────────────────────────────? ?└───────────┬─────────────────────────────────────? ? ┌────┴────────┬───────────────? ? ? ?┌──────────────?┌──────────────?┌──────────────?? Dify ?? REDCap ?? PostgreSQL ?? (本地Docker)?? API ?? 数据? ?? ?? ?? ??- 研究方案 ??- 患者数? ??- 项目配置 ??- CRF表格 ??- 质控状? ??- 审计日志 ??- 周报归档 ??- 不良反应 ??- 用户映射 ?└──────────────?└──────────────?└──────────────? <50ms <100ms <20ms 本地调用 本地调用 本地调用 ``` ### 2.2 关键技术决? | 决策?| 方案 | 原因 | |--------|------|------| | **AI推理引擎** | DeepSeek-V3 (API) | 性价比高,支持Function Calling | | **知识?* | Dify本地Docker | 已部署,无额外成本,延迟?| | **向量数据?* | Dify内置Weaviate | 免维护,开箱即?| | **路由策略** | 单步意图识别 | MVP阶段简化,不用ReAct循环 | | **数据查询** | RedcapAdapter | 已有,直接复?| --- ## ?二、极简版开发计划(2天) ### 🎯 Day 1:基础对话能力?小时? #### 核心目标 **让AI能回答用户问题(只查REDCap数据?* #### 任务1.1:创建SessionMemory?0分钟? **文件位置**:`backend/src/modules/iit-manager/agents/SessionMemory.ts` ```typescript /** * 会话记忆管理器(内存版) * 存储用户最?轮对话,用于上下文理? */ export class SessionMemory { // 内存存储:{ userId: ConversationHistory } private sessions: Map = new Map(); private readonly MAX_HISTORY = 3; // 只保留最?? /** * 添加对话记录 */ addMessage(userId: string, role: 'user' | 'assistant', content: string): void { if (!this.sessions.has(userId)) { this.sessions.set(userId, { userId, messages: [], createdAt: new Date(), updatedAt: new Date() }); } const session = this.sessions.get(userId)!; session.messages.push({ role, content, timestamp: new Date() }); // 只保留最?轮(6条消息) if (session.messages.length > this.MAX_HISTORY * 2) { session.messages = session.messages.slice(-this.MAX_HISTORY * 2); } session.updatedAt = new Date(); } /** * 获取用户对话历史 */ getHistory(userId: string): ConversationMessage[] { const session = this.sessions.get(userId); return session?.messages || []; } /** * 获取用户上下文(最近一轮) */ getContext(userId: string): string { const history = this.getHistory(userId); if (history.length === 0) return ''; // 只取最近一轮对? const recentMessages = history.slice(-2); return recentMessages .map(m => `${m.role}: ${m.content}`) .join('\n'); } /** * 清除用户会话 */ clearSession(userId: string): void { this.sessions.delete(userId); } /** * 清理过期会话(超?小时未使用) */ cleanupExpiredSessions(): void { const now = Date.now(); const ONE_HOUR = 3600000; for (const [userId, session] of this.sessions.entries()) { if (now - session.updatedAt.getTime() > ONE_HOUR) { this.sessions.delete(userId); } } } } interface ConversationHistory { userId: string; messages: ConversationMessage[]; createdAt: Date; updatedAt: Date; } interface ConversationMessage { role: 'user' | 'assistant'; content: string; timestamp: Date; } // 全局单例 export const sessionMemory = new SessionMemory(); // 定时清理过期会话(每小时?setInterval(() => { sessionMemory.cleanupExpiredSessions(); }, 3600000); ``` **验收标准**?- ?可存储对话历?- ?可获取上下文 - ?自动清理过期会话 --- #### 任务1.2:创建ChatService?小时)⚡ 复用LLMFactory **文件位置**:`backend/src/modules/iit-manager/services/ChatService.ts` ```typescript import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; import { Message } from '../../../common/llm/adapters/types.js'; import { logger } from '../../../common/logging/index.js'; import { sessionMemory } from '../agents/SessionMemory.js'; /** * AI对话服务(复用通用能力层LLMFactory? * 处理企业微信用户消息,支持上下文记忆 */ export class ChatService { private llm; constructor() { // ?复用通用能力层LLMFactory(零配置? this.llm = LLMFactory.getAdapter('deepseek-v3'); } /** * 识别用户意图(带上下文) */ async route( userMessage: string, userId: string, projectId: string ): Promise { try { // 1. 获取上下? const context = sessionMemory.getContext(userId); logger.info('[SimpleIntentRouter] Routing with context', { message: userMessage.substring(0, 50), hasContext: !!context, userId }); // 2. 构建Prompt(包含上下文? const systemPrompt = this.buildSystemPrompt(); const userPrompt = context ? `【上下文】\n${context}\n\n【当前问题】\n${userMessage}` : userMessage; // 3. 调用LLM const response = await this.llm.chat.completions.create({ model: 'deepseek-chat', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], tools: [this.getDataQueryTool()], tool_choice: 'auto', temperature: 0.1, max_tokens: 500 }); const message = response.choices[0].message; // 4. 如果LLM决定调用工具 if (message.tool_calls && message.tool_calls.length > 0) { const toolCall = message.tool_calls[0]; const toolArgs = JSON.parse(toolCall.function.arguments); // ?上下文解析:如果args中有代词,尝试从上下文中解析 if (context && this.hasPronouns(userMessage)) { toolArgs.patient_id = this.extractPatientIdFromContext(context, toolArgs); } return { needsToolCall: true, toolName: 'query_clinical_data', toolArgs, rawResponse: message }; } // 5. 直接回答 return { needsToolCall: false, directAnswer: message.content || '抱歉,我没有理解您的问题', rawResponse: message }; } catch (error: any) { logger.error('[SimpleIntentRouter] Routing failed', { error: error.message }); return { needsToolCall: false, directAnswer: '抱歉,我遇到了一些问题,请稍后再?, error: error.message }; } } /** * 构建System Prompt */ private buildSystemPrompt(): string { return `# 角色 你是临床研究项目助手,帮助PI查询项目数据? # 能力 你可以查询REDCap数据库,包括?1. 项目统计(入组人数、数据完整率?2. 患者详情(录入情况、基本信息) 3. 质控状态(数据问题? # 上下文理?- 如果用户???这个患?等代词,请根据【上下文】中提到的患者编?- 如果上下文中没有患者编号,请要求用户提? # 约束 - 严禁编造数?- 只能查询REDCap数据,不能查询文?- 回答要简洁专业`; } /** * 定义数据查询工具 */ private getDataQueryTool(): any { return { type: "function", function: { name: "query_clinical_data", description: `查询REDCap临床数据?适用场景?- 问项目统计:现在入组多少人?数据质量如何?- 问患者详情:P001患者录完了吗?有不良反应吗?- 问质控状态:有哪些质控问题?`, parameters: { type: "object", properties: { intent: { type: "string", enum: ["project_stats", "patient_detail", "qc_status"], description: `查询意图?- project_stats: 项目统计 - patient_detail: 患者详?- qc_status: 质控状态` }, patient_id: { type: "string", description: "患者编号(如P001),当intent=patient_detail时必? } }, required: ["intent"] } } }; } /** * 检查消息中是否有代? */ private hasPronouns(message: string): boolean { const pronouns = ['?, '?, '这个患?, '该患?, '这位', '那位']; return pronouns.some(p => message.includes(p)); } /** * 从上下文中提取患者ID */ private extractPatientIdFromContext(context: string, toolArgs: any): string { // 简单正则提取患者编? const match = context.match(/P\d{3,}/); return match ? match[0] : toolArgs.patient_id; } } export interface IntentRouteResult { needsToolCall: boolean; toolName?: string; toolArgs?: any; directAnswer?: string; error?: string; rawResponse?: any; } ``` **验收标准**?- ?可识别查询意?- ?支持上下文理解(代词解析?- ?错误处理完善 --- #### 任务1.3:简化ToolExecutor?.5小时? **文件位置**:`backend/src/modules/iit-manager/agents/SimpleToolExecutor.ts` ```typescript import { RedcapAdapter } from '../adapters/RedcapAdapter.js'; import { logger } from '../../../common/logging/index.js'; import { prisma } from '../../../config/database.js'; /** * 简化版工具执行? * 只执行REDCap数据查询 */ export class SimpleToolExecutor { /** * 执行查询临床数据 */ async execute( toolArgs: { intent: 'project_stats' | 'patient_detail' | 'qc_status'; patient_id?: string; }, context: { projectId: string; userId: string; } ): Promise { try { logger.info('[SimpleToolExecutor] Executing query', { intent: toolArgs.intent, patientId: toolArgs.patient_id, projectId: context.projectId }); // 1. 获取项目配置 const project = await prisma.iitProject.findUnique({ where: { id: context.projectId }, select: { name: true, redcapApiUrl: true, redcapApiToken: true } }); if (!project) { return { success: false, data: null, error: '项目不存? }; } // 2. 初始化RedcapAdapter const redcap = new RedcapAdapter( project.redcapApiUrl, project.redcapApiToken ); // 3. 根据intent执行查询 switch (toolArgs.intent) { case 'project_stats': return await this.getProjectStats(redcap, project.name); case 'patient_detail': if (!toolArgs.patient_id) { return { success: false, data: null, error: '请提供患者编号(如:P001? }; } return await this.getPatientDetail(redcap, toolArgs.patient_id); case 'qc_status': return await this.getQCStatus(context.projectId); default: return { success: false, data: null, error: '未知的查询类? }; } } catch (error: any) { logger.error('[SimpleToolExecutor] Execution failed', { error: error.message }); return { success: false, data: null, error: error.message }; } } /** * 获取项目统计 */ private async getProjectStats( redcap: RedcapAdapter, projectName: string ): Promise { const records = await redcap.exportRecords(); return { success: true, data: { type: 'project_stats', projectName, stats: { totalRecords: records.length, enrolled: records.length, completed: records.filter((r: any) => r.complete === '2').length, dataQuality: this.calculateDataQuality(records) } } }; } /** * 获取患者详? */ private async getPatientDetail( redcap: RedcapAdapter, patientId: string ): Promise { const records = await redcap.exportRecords([patientId]); if (records.length === 0) { return { success: false, data: null, error: `未找到患?${patientId}` }; } const record = records[0]; return { success: true, data: { type: 'patient_detail', patientId, details: { age: record.age, gender: record.gender, bmi: record.bmi, complete: record.complete === '2' ? '已完? : '进行?, lastUpdate: new Date().toISOString() } } }; } /** * 获取质控状? */ private async getQCStatus(projectId: string): Promise { const logs = await prisma.iitAuditLog.findMany({ where: { projectId, actionType: 'quality_issue' }, orderBy: { createdAt: 'desc' }, take: 10 }); return { success: true, data: { type: 'qc_status', issueCount: logs.length, recentIssues: logs.map(log => ({ recordId: log.entityId, issue: log.details, createdAt: log.createdAt })) } }; } /** * 计算数据质量(简单算法) */ private calculateDataQuality(records: any[]): string { if (records.length === 0) return '0%'; const completedCount = records.filter((r: any) => r.complete === '2').length; const quality = (completedCount / records.length) * 100; return `${quality.toFixed(1)}%`; } } export interface ToolExecutionResult { success: boolean; data: any; error?: string; } ``` **验收标准**?- ?可查询项目统?- ?可查询患者详?- ?可查询质控状? --- #### 任务1.4:简化AnswerGenerator?小时? **文件位置**:`backend/src/modules/iit-manager/agents/SimpleAnswerGenerator.ts` ```typescript import { ToolExecutionResult } from './SimpleToolExecutor.js'; import { logger } from '../../../common/logging/index.js'; /** * 简化版答案生成? * 使用模板生成回答,不调用LLM(节省成本) */ export class SimpleAnswerGenerator { /** * 生成回答 */ generate( userQuestion: string, toolResult: ToolExecutionResult ): string { try { logger.info('[SimpleAnswerGenerator] Generating answer', { success: toolResult.success, dataType: toolResult.data?.type }); // 如果工具执行失败 if (!toolResult.success) { return this.generateErrorMessage(toolResult.error); } // 根据数据类型生成回答 const dataType = toolResult.data.type; if (dataType === 'project_stats') { return this.generateProjectStatsAnswer(toolResult.data); } else if (dataType === 'patient_detail') { return this.generatePatientDetailAnswer(toolResult.data); } else if (dataType === 'qc_status') { return this.generateQCStatusAnswer(toolResult.data); } return '抱歉,我无法生成回答'; } catch (error: any) { logger.error('[SimpleAnswerGenerator] Generation failed', { error: error.message }); return '抱歉,回答生成失?; } } /** * 生成项目统计回答 */ private generateProjectStatsAnswer(data: any): string { const stats = data.stats; return `📊 **${data.projectName}项目统计** ?**入组人数**?{stats.enrolled}??**完成病例**?{stats.completed}??**数据质量**?{stats.dataQuality} 💡 更新时间?{new Date().toLocaleString('zh-CN')}`; } /** * 生成患者详情回? */ private generatePatientDetailAnswer(data: any): string { const details = data.details; return `👤 **患?${data.patientId} 详情** 📋 **基本信息**?- 年龄?{details.age || '未录?}?- 性别?{details.gender || '未录?} - BMI?{details.bmi || '未录?} 📊 **录入状?*?- ${details.complete} 💡 最后更新:${new Date().toLocaleString('zh-CN')}`; } /** * 生成质控状态回? */ private generateQCStatusAnswer(data: any): string { const issues = data.recentIssues.slice(0, 5); let answer = `🔍 **质控状?*\n\n`; answer += `⚠️ **质控问题?*?{data.issueCount}个\n\n`; if (issues.length > 0) { answer += `📋 **最近问?*:\n`; issues.forEach((issue: any, index: number) => { answer += `${index + 1}. 记录${issue.recordId}\n`; }); } else { answer += `?暂无质控问题`; } return answer; } /** * 生成错误提示 */ private generateErrorMessage(error?: string): string { return `?查询失败 原因?{error || '未知错误'} 💡 您可以: 1. 稍后重试 2. 换个问法 3. 联系管理员`; } } ``` **验收标准**?- ?回答格式友好 - ?支持Markdown - ?错误提示清晰 --- #### 任务1.5:集成到WechatCallbackController?小时? **修改文件**:`backend/src/modules/iit-manager/controllers/WechatCallbackController.ts` **在handleCallback方法中添?*? ```typescript import { sessionMemory } from '../agents/SessionMemory.js'; import { SimpleIntentRouter } from '../agents/SimpleIntentRouter.js'; import { SimpleToolExecutor } from '../agents/SimpleToolExecutor.js'; import { SimpleAnswerGenerator } from '../agents/SimpleAnswerGenerator.js'; class WechatCallbackController { private intentRouter: SimpleIntentRouter; private toolExecutor: SimpleToolExecutor; private answerGenerator: SimpleAnswerGenerator; constructor() { // ... 现有代码 ... this.intentRouter = new SimpleIntentRouter(); this.toolExecutor = new SimpleToolExecutor(); this.answerGenerator = new SimpleAnswerGenerator(); } async handleCallback(request: FastifyRequest, reply: FastifyReply): Promise { // ... 现有的验证、解密逻辑 ... // ?立即返回success reply.send('success'); // ?异步处理(新增AI对话? setImmediate(async () => { try { const userMessage = decryptedData.Content; const userId = decryptedData.FromUserName; logger.info('📥 收到用户消息', { userId, message: userMessage.substring(0, 50) }); // ?立即发?正在查询..."反馈 await wechatService.sendTextMessage( userId, '🫡 正在查询,请稍?..' ); // 1. 保存用户消息到会话记? sessionMemory.addMessage(userId, 'user', userMessage); // 2. 获取用户的项目信? const userMapping = await prisma.iitUserMapping.findFirst({ where: { wechatUserId: userId } }); if (!userMapping) { await wechatService.sendTextMessage( userId, '⚠️ 您还未绑定项目,请联系管理员配置' ); return; } // 3. 意图识别(带上下文) const routeResult = await this.intentRouter.route( userMessage, userId, userMapping.projectId ); // 4. 如果直接回答 if (!routeResult.needsToolCall) { const answer = routeResult.directAnswer!; await wechatService.sendTextMessage(userId, answer); sessionMemory.addMessage(userId, 'assistant', answer); return; } // 5. 执行工具 const toolResult = await this.toolExecutor.execute( routeResult.toolArgs, { projectId: userMapping.projectId, userId } ); // 6. 生成回答 const answer = this.answerGenerator.generate(userMessage, toolResult); // 7. 发送回? await wechatService.sendMarkdownMessage(userId, answer); // 8. 保存AI回答到会话记? sessionMemory.addMessage(userId, 'assistant', answer); // 9. 记录审计日志 await prisma.iitAuditLog.create({ data: { projectId: userMapping.projectId, actionType: 'wechat_user_query', operator: userId, entityId: userId, details: { question: userMessage, answer: answer.substring(0, 200), toolUsed: 'query_clinical_data', hasContext: !!sessionMemory.getContext(userId) } } }); logger.info('?回答发送成?, { userId }); } catch (error: any) { logger.error('?处理用户消息失败', { error: error.message }); await wechatService.sendTextMessage( userId, '抱歉,我遇到了一些问题,请稍后再? ); } }); } } ``` **验收标准**?- ?可接收用户消?- ?立即发?正在查询..." - ?正确识别意图 - ?正确执行工具 - ?正确发送回?- ?上下文记忆生? --- ### 🎯 Day 2:上下文优化 + 测试?小时? #### 任务2.1:上下文记忆优化?小时? **增强SessionMemory,支持患者ID提取**? ```typescript // 在SessionMemory中添?/** * 从历史记录中提取最近提到的患者ID */ getLastPatientId(userId: string): string | null { const history = this.getHistory(userId); // 从最近的对话中倒序查找患者ID for (let i = history.length - 1; i >= 0; i--) { const message = history[i]; const match = message.content.match(/P\d{3,}/); if (match) { return match[0]; } } return null; } ``` **在SimpleIntentRouter中使?*? ```typescript // 如果用户?他有不良反应吗?",自动填充patient_id if (context && this.hasPronouns(userMessage) && !toolArgs.patient_id) { const lastPatientId = sessionMemory.getLastPatientId(userId); if (lastPatientId) { toolArgs.patient_id = lastPatientId; logger.info('[SimpleIntentRouter] 自动填充患者ID', { patientId: lastPatientId }); } } ``` --- #### 任务2.2:完整测试(3小时? **测试场景**? ```typescript // 场景1:无上下文查?{ input: "现在入组多少人?", expectedIntent: "project_stats", expectedOutput: "📊 项目统计\n?入组人数:XX? } // 场景2:有上下文的多轮对话(关键!?{ conversation: [ { input: "帮我查一下P001的情?, expectedIntent: "patient_detail", expectedPatientId: "P001" }, { input: "他有不良反应吗?", // ?代词"? expectedIntent: "patient_detail", expectedPatientId: "P001", // ?自动填充 expectedOutput: "应该包含P001" } ] } // 场景3:正在输入反?{ input: "现在入组多少人?", expectedFirstReply: "🫡 正在查询,请稍?..", expectedSecondReply: "📊 项目统计..." } // 场景4:质控查?{ input: "有哪些质控问题?", expectedIntent: "qc_status", expectedOutput: "🔍 质控状? } // 场景5:闲?{ input: "你好", expectedOutput: "您好!我是临床研究助? } ``` **测试步骤**?1. 在企业微信中发送测试消?2. 验证是否收到"正在查询..." 3. 验证最终回复内?4. 检查审计日志中的上下文标记 5. 测试多轮对话的上下文理解 **验收标准**?- ?5个测试场景全部通过 - ?"正在输入"反馈生效 - ?上下文记忆生效(代词解析?- ?回复时间<3? --- ### 📊 极简版成功标? | 功能 | 验收标准 | 优先?| |------|---------|-------| | **基础对话** | 可查询REDCap数据 | 🔴 P0 | | **上下文记?* | 支持最?轮对?| 🔴 P0 | | **代词解析** | "?能自动识别患?| 🔴 P0 | | **正在输入反馈** | 立即?正在查询..." | 🔴 P0 | | **回复延迟** | <3?| 🔴 P0 | | **意图识别准确?* | >80% | 🔴 P0 | --- ### 🎉 极简版vs完整版对? | 功能 | 极简?(2? | 完整?(5? | |------|------------|------------| | REDCap查询 | ?| ?| | 上下文记?| ?(内存3? | ?(内存3? | | 正在输入反馈 | ?| ?| | Dify知识?| ?| ?| | 周报自动归档 | ?| ?| | 文档查询 | ?| ?| --- ## 🗓?三、完整版开发计划(5天,可选) ### Day 1:Dify环境配置与知识库创建?小时? #### 任务1.1:验证Dify本地环境?小时? **检查项**?```bash # 1. 检查Dify容器状?cd AIclinicalresearch/docker docker-compose ps | grep dify # 2. 访问Dify管理后台 # http://localhost/dify (或实际端? # 3. 获取API密钥 # Dify后台 ?设置 ?API Keys ?创建 ``` **验收标准**?- ?Dify容器运行正常 - ?可访问管理后?- ?获得API Key --- #### 任务1.2:创建IIT Manager知识库(2小时? **操作步骤**? 1. **创建知识?*(Dify后台操作? ``` 名称:IIT Manager - test0102项目 类型:通用知识? Embedding模型:text-embedding-3-small (OpenAI) 分块策略:智能分块(500字符/块,重叠50字符? ``` 2. **上传测试文档** - 上传1份CRF表格(PDF/Word? - 上传1份入排标准文档(Markdown/Text? - 上传1份研究方案摘要(PDF? 3. **测试检索效?* ``` 测试问题1?入组标准有哪些?" 测试问题2?CRF表格中有哪些字段? 测试问题3?研究终点是什么?" ``` **验收标准**?- ?知识库创建成?- ?3份文档上传成?- ?检索测试准确率>80% **产出**?- Dify知识库ID - API调用示例代码 --- #### 任务1.3:实现Dify API适配器(3小时? **文件位置**:`backend/src/modules/iit-manager/adapters/DifyAdapter.ts` **代码实现**? ```typescript import axios from 'axios'; import { logger } from '../../../common/logging/index.js'; /** * Dify API适配? * 用于与本地Dify Docker实例交互 */ export class DifyAdapter { private baseUrl: string; private apiKey: string; private knowledgeBaseId: string; constructor(projectId: string) { // 从环境变量或数据库读取配? this.baseUrl = process.env.DIFY_API_URL || 'http://localhost/v1'; this.apiKey = process.env.DIFY_API_KEY || ''; this.knowledgeBaseId = this.getKnowledgeBaseId(projectId); } /** * 搜索知识? * @param query 查询问题 * @param options 搜索选项 */ async searchKnowledge( query: string, options?: { doc_type?: 'protocol' | 'crf' | 'report'; top_k?: number; } ): Promise { try { logger.info('[DifyAdapter] Searching knowledge base', { query, options, knowledgeBaseId: this.knowledgeBaseId }); const response = await axios.post( `${this.baseUrl}/datasets/${this.knowledgeBaseId}/retrieve`, { query: query, retrieval_model: { search_method: 'semantic_search', top_k: options?.top_k || 3, score_threshold: 0.5 }, // 如果指定了doc_type,通过metadata过滤 ...(options?.doc_type && { retrieval_model: { filter: { doc_type: options.doc_type } } }) }, { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, timeout: 10000 // 10秒超? } ); logger.info('[DifyAdapter] Search completed', { recordCount: response.data.records?.length || 0 }); return { success: true, records: response.data.records || [], query: query }; } catch (error: any) { logger.error('[DifyAdapter] Search failed', { error: error.message, query }); return { success: false, records: [], query, error: error.message }; } } /** * 上传文档到知识库 * @param content 文档内容 * @param metadata 元数? */ async uploadDocument( content: string, metadata: { name: string; doc_type: 'protocol' | 'crf' | 'report'; date?: string; } ): Promise<{ success: boolean; documentId?: string }> { try { logger.info('[DifyAdapter] Uploading document', { name: metadata.name, type: metadata.doc_type }); const response = await axios.post( `${this.baseUrl}/datasets/${this.knowledgeBaseId}/document/create_by_text`, { name: metadata.name, text: content, indexing_technique: 'high_quality', process_rule: { mode: 'automatic', rules: { pre_processing_rules: [ { id: 'remove_extra_spaces', enabled: true }, { id: 'remove_urls_emails', enabled: false } ], segmentation: { separator: '\n', max_tokens: 500 } } }, doc_form: 'text_model', doc_language: 'Chinese', // 保存元数? metadata: { doc_type: metadata.doc_type, date: metadata.date || new Date().toISOString(), upload_time: new Date().toISOString() } }, { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' } } ); logger.info('[DifyAdapter] Document uploaded', { documentId: response.data.document.id }); return { success: true, documentId: response.data.document.id }; } catch (error: any) { logger.error('[DifyAdapter] Upload failed', { error: error.message, name: metadata.name }); return { success: false }; } } /** * 获取项目对应的知识库ID * @param projectId 项目ID */ private getKnowledgeBaseId(projectId: string): string { // TODO: 从数据库读取项目配置 // 临时方案:从环境变量读取 return process.env.DIFY_KNOWLEDGE_BASE_ID || ''; } } /** * Dify搜索结果 */ export interface DifySearchResult { success: boolean; records: Array<{ content: string; score: number; metadata?: { doc_type?: string; date?: string; }; }>; query: string; error?: string; } ``` **环境变量配置**(`.env`): ```bash # Dify配置 DIFY_API_URL=http://localhost/v1 DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxxx DIFY_KNOWLEDGE_BASE_ID=kb-xxxxxxxxxxxxxxxxxxxxxx ``` **验收标准**?- ?DifyAdapter类实现完?- ?可成功调用搜索API - ?可成功上传文?- ?错误处理完善 --- #### 任务1.4:编写单元测试(2小时? **文件位置**:`backend/src/modules/iit-manager/adapters/__tests__/DifyAdapter.test.ts` ```typescript import { DifyAdapter } from '../DifyAdapter'; describe('DifyAdapter', () => { let difyAdapter: DifyAdapter; beforeAll(() => { difyAdapter = new DifyAdapter('test-project-id'); }); describe('searchKnowledge', () => { it('应该成功搜索知识?, async () => { const result = await difyAdapter.searchKnowledge('入组标准有哪些?'); expect(result.success).toBe(true); expect(result.records.length).toBeGreaterThan(0); expect(result.records[0]).toHaveProperty('content'); expect(result.records[0]).toHaveProperty('score'); }); it('应该支持按文档类型过?, async () => { const result = await difyAdapter.searchKnowledge( '入组标准', { doc_type: 'protocol' } ); expect(result.success).toBe(true); expect(result.records.length).toBeGreaterThan(0); }); it('应该处理搜索失败情况', async () => { // Mock错误场景 const result = await difyAdapter.searchKnowledge(''); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); describe('uploadDocument', () => { it('应该成功上传文档', async () => { const result = await difyAdapter.uploadDocument( '这是一份测试文?, { name: '测试文档', doc_type: 'protocol' } ); expect(result.success).toBe(true); expect(result.documentId).toBeDefined(); }); }); }); ``` **验收标准**?- ?单元测试覆盖?80% - ?所有测试用例通过 --- ### Day 2:意图识别与路由逻辑?小时? #### 任务2.1:设计工具定义(Tool Schema)(2小时? **文件位置**:`backend/src/modules/iit-manager/agents/tools.ts` ```typescript /** * IIT Manager Agent工具定义 */ export const iitAgentTools = [ // 工具1:查询实时数? { type: "function", function: { name: "query_clinical_data", description: `【查REDCap实时数据】用于查询临床研究的实时数据状态?适用场景?- 问项目进度:现在入组多少人了?数据完整率如何?- 问患者详情:P001患者录完数据了吗?有没有不良反应? - 问质控状态:有哪些质控问题?数据质量怎么样?`, parameters: { type: "object", properties: { intent: { type: "string", enum: ["project_stats", "patient_detail", "qc_status"], description: `查询意图?- project_stats: 项目宏观统计(入组人数、数据完整率等) - patient_detail: 特定患者详情(录入情况、不良反应等?- qc_status: 质控状态(质疑列表、数据问题等)` }, patient_id: { type: "string", description: "患?受试者编号(?P001、P002),当intent=patient_detail时必? }, date_range: { type: "string", enum: ["today", "this_week", "this_month", "all"], description: "时间范围,默认为all" } }, required: ["intent"] } } }, // 工具2:搜索知识库 { type: "function", function: { name: "search_knowledge_base", description: `【查研究文档】用于搜索研究方案、规范文件、历史记录等静态资料?适用场景?- 问研究规范:入排标准是什么?研究终点怎么定义?- 问CRF表格:某个字段的定义是什么?填写规范是? - 问历史记录:上周的周报里提到了什么问题?`, parameters: { type: "object", properties: { query: { type: "string", description: "搜索关键词或问题" }, doc_category: { type: "string", enum: ["protocol", "crf", "report"], description: `文档类别?- protocol: 研究方案、伦理批件、知情同意书、入排标?- crf: CRF表格定义、填写说明、数据字?- report: 项目周报、进度总结、历史记录` } }, required: ["query"] } } } ]; ``` --- #### 任务2.2:实现意图路由器(Intent Router)(3小时? **文件位置**:`backend/src/modules/iit-manager/agents/IntentRouter.ts` ```typescript import OpenAI from 'openai'; import { logger } from '../../../common/logging/index.js'; import { iitAgentTools } from './tools.js'; /** * 意图路由? * 使用LLM的Function Calling能力识别用户意图 */ export class IntentRouter { private llm: OpenAI; private systemPrompt: string; constructor() { this.llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: process.env.OPENAI_BASE_URL || 'https://api.deepseek.com' }); this.systemPrompt = this.buildSystemPrompt(); } /** * 识别用户意图并返回工具调? */ async route(userMessage: string, context?: { projectId: string; userId: string; }): Promise { try { logger.info('[IntentRouter] Routing user message', { message: userMessage.substring(0, 100), projectId: context?.projectId }); const response = await this.llm.chat.completions.create({ model: 'deepseek-chat', messages: [ { role: 'system', content: this.systemPrompt }, { role: 'user', content: userMessage } ], tools: iitAgentTools, tool_choice: 'auto', temperature: 0.1, // 低温度,保证稳定? max_tokens: 500 }); const message = response.choices[0].message; // 如果LLM决定调用工具 if (message.tool_calls && message.tool_calls.length > 0) { const toolCall = message.tool_calls[0]; const toolName = toolCall.function.name; const toolArgs = JSON.parse(toolCall.function.arguments); logger.info('[IntentRouter] Tool selected', { toolName, toolArgs }); return { needsToolCall: true, toolName: toolName as 'query_clinical_data' | 'search_knowledge_base', toolArgs, rawResponse: message }; } // 如果LLM直接回答(不需要工具) logger.info('[IntentRouter] Direct answer', { answer: message.content?.substring(0, 100) }); return { needsToolCall: false, directAnswer: message.content || '抱歉,我没有理解您的问题', rawResponse: message }; } catch (error: any) { logger.error('[IntentRouter] Routing failed', { error: error.message, message: userMessage }); return { needsToolCall: false, directAnswer: '抱歉,我遇到了一些问题,请稍后再?, error: error.message }; } } /** * 构建System Prompt */ private buildSystemPrompt(): string { return `# 角色 你是由壹证循科技开发的"临床研究项目助手",服务于IIT(研究者发起试验)项目的PI(主要研究者)? # 能力 你拥有两个工具,请根据用户问题精准选择? 1. **query_clinical_data**(查实时数据? - 当用户问"现状"时使? - 例如?现在入组多少人?"?P001患者录完了吗?"?有没有不良反应?" - 这些问题需要查询REDCap数据库的实时数据 2. **search_knowledge_base**(查研究文档? - 当用户问"规定"?历史"时使? - 例如?入排标准是什么?"?上周的问题解决了吗?"?CRF里某字段怎么填?" - 这些问题需要查阅研究方案、周报等文档 # 路由原则 - 如果问题明确需要工具,必须调用工具,不要猜测或编造答?- 如果问题模糊,优先选择query_clinical_data(实时数据更重要?- 如果是闲聊或打招呼,可以直接回答,不调用工具 # 约束 - 严禁编造数?- 回答要简洁专?- 隐去患者真实姓名,只使用编号`; } } /** * 意图路由结果 */ export interface IntentRouteResult { needsToolCall: boolean; toolName?: 'query_clinical_data' | 'search_knowledge_base'; toolArgs?: any; directAnswer?: string; error?: string; rawResponse?: any; } ``` **验收标准**?- ?IntentRouter类实现完?- ?可正确识别查数据意图 - ?可正确识别查文档意图 - ?可处理闲聊场? --- #### 任务2.3:实现工具执行器(Tool Executor)(3小时? **文件位置**:`backend/src/modules/iit-manager/agents/ToolExecutor.ts` ```typescript import { DifyAdapter } from '../adapters/DifyAdapter.js'; import { RedcapAdapter } from '../adapters/RedcapAdapter.js'; import { logger } from '../../../common/logging/index.js'; import { prisma } from '../../../config/database.js'; /** * 工具执行? * 根据意图路由结果执行对应的工? */ export class ToolExecutor { /** * 执行工具 */ async execute( toolName: 'query_clinical_data' | 'search_knowledge_base', toolArgs: any, context: { projectId: string; userId: string; } ): Promise { try { logger.info('[ToolExecutor] Executing tool', { toolName, toolArgs, projectId: context.projectId }); if (toolName === 'query_clinical_data') { return await this.executeQueryClinicalData(toolArgs, context); } else if (toolName === 'search_knowledge_base') { return await this.executeSearchKnowledge(toolArgs, context); } return { success: false, data: null, error: `Unknown tool: ${toolName}` }; } catch (error: any) { logger.error('[ToolExecutor] Execution failed', { error: error.message, toolName }); return { success: false, data: null, error: error.message }; } } /** * 执行:查询临床数? */ private async executeQueryClinicalData( args: { intent: 'project_stats' | 'patient_detail' | 'qc_status'; patient_id?: string; date_range?: string; }, context: { projectId: string; userId: string } ): Promise { // 1. 获取项目配置 const project = await prisma.iitProject.findUnique({ where: { id: context.projectId }, select: { redcapApiUrl: true, redcapApiToken: true, redcapProjectId: true } }); if (!project) { return { success: false, data: null, error: '项目不存? }; } // 2. 初始化RedcapAdapter const redcap = new RedcapAdapter( project.redcapApiUrl, project.redcapApiToken ); // 3. 根据intent执行不同查询 switch (args.intent) { case 'project_stats': { // 查询项目统计 const records = await redcap.exportRecords(); return { success: true, data: { type: 'project_stats', totalRecords: records.length, stats: { enrolled: records.length, completed: records.filter((r: any) => r.complete === '2').length, dataQuality: '87.5%' // TODO: 实际计算 } } }; } case 'patient_detail': { // 查询特定患? if (!args.patient_id) { return { success: false, data: null, error: '缺少患者ID' }; } const records = await redcap.exportRecords([args.patient_id]); if (records.length === 0) { return { success: false, data: null, error: `未找到患?${args.patient_id}` }; } return { success: true, data: { type: 'patient_detail', patientId: args.patient_id, details: records[0] } }; } case 'qc_status': { // 查询质控状? const logs = await prisma.iitAuditLog.findMany({ where: { projectId: context.projectId, actionType: 'quality_issue' }, orderBy: { createdAt: 'desc' }, take: 10 }); return { success: true, data: { type: 'qc_status', issueCount: logs.length, recentIssues: logs.map(log => ({ recordId: log.entityId, issue: log.details, createdAt: log.createdAt })) } }; } default: return { success: false, data: null, error: `Unknown intent: ${args.intent}` }; } } /** * 执行:搜索知识库 */ private async executeSearchKnowledge( args: { query: string; doc_category?: 'protocol' | 'crf' | 'report'; }, context: { projectId: string; userId: string } ): Promise { // 1. 初始化DifyAdapter const dify = new DifyAdapter(context.projectId); // 2. 搜索知识? const result = await dify.searchKnowledge(args.query, { doc_type: args.doc_category, top_k: 3 }); if (!result.success) { return { success: false, data: null, error: result.error || '知识库搜索失? }; } // 3. 格式化结? return { success: true, data: { type: 'knowledge_search', query: args.query, category: args.doc_category, results: result.records.map(record => ({ content: record.content, score: record.score, metadata: record.metadata })) } }; } } /** * 工具执行结果 */ export interface ToolExecutionResult { success: boolean; data: any; error?: string; } ``` **验收标准**?- ?ToolExecutor类实现完?- ?query_clinical_data工具可执?- ?search_knowledge_base工具可执?- ?错误处理完善 --- ### Day 3:集成企业微信对话(8小时? #### 任务3.1:增强WechatCallbackController?小时? **修改文件**:`backend/src/modules/iit-manager/controllers/WechatCallbackController.ts` **在现有的`handleCallback`方法中增加AI对话逻辑**? ```typescript // 在WechatCallbackController类中添加 import { IntentRouter } from '../agents/IntentRouter.js'; import { ToolExecutor } from '../agents/ToolExecutor.js'; import { AnswerGenerator } from '../agents/AnswerGenerator.js'; class WechatCallbackController { private intentRouter: IntentRouter; private toolExecutor: ToolExecutor; private answerGenerator: AnswerGenerator; constructor() { // ... 现有代码 ... this.intentRouter = new IntentRouter(); this.toolExecutor = new ToolExecutor(); this.answerGenerator = new AnswerGenerator(); } /** * 处理企业微信回调消息(已有方法,增强? */ async handleCallback(request: FastifyRequest, reply: FastifyReply): Promise { // ... 现有的验证、解密逻辑 ... // ?立即返回success(避?秒超时) reply.send('success'); // ?异步处理用户消息(新增) setImmediate(async () => { try { const userMessage = decryptedData.Content; const userId = decryptedData.FromUserName; logger.info('📥 收到用户消息', { userId, message: userMessage.substring(0, 50) }); // 1. 获取用户的项目信? const userMapping = await prisma.iitUserMapping.findFirst({ where: { wechatUserId: userId } }); if (!userMapping) { await wechatService.sendTextMessage( userId, '⚠️ 您还未绑定项目,请联系管理员配置' ); return; } // 2. 意图识别 const routeResult = await this.intentRouter.route(userMessage, { projectId: userMapping.projectId, userId }); // 3. 如果直接回答(不需要工具) if (!routeResult.needsToolCall) { await wechatService.sendTextMessage(userId, routeResult.directAnswer!); return; } // 4. 执行工具 const toolResult = await this.toolExecutor.execute( routeResult.toolName!, routeResult.toolArgs, { projectId: userMapping.projectId, userId } ); // 5. 生成回答 const answer = await this.answerGenerator.generate( userMessage, toolResult, routeResult.toolName! ); // 6. 发送回? await wechatService.sendMarkdownMessage(userId, answer); // 7. 记录审计日志 await prisma.iitAuditLog.create({ data: { projectId: userMapping.projectId, actionType: 'wechat_user_query', operator: userId, entityId: userId, details: { question: userMessage, answer: answer.substring(0, 200), toolUsed: routeResult.toolName } } }); logger.info('?回答发送成?, { userId, toolUsed: routeResult.toolName }); } catch (error: any) { logger.error('?处理用户消息失败', { error: error.message }); // 发送错误提? await wechatService.sendTextMessage( userId, '抱歉,我遇到了一些问题,请稍后再试或联系管理? ); } }); } } ``` **验收标准**?- ?可接收用户消?- ?可调用意图路?- ?可执行工?- ?可生成回?- ?可发送回? --- #### 任务3.2:实现答案生成器(Answer Generator)(2小时? **文件位置**:`backend/src/modules/iit-manager/agents/AnswerGenerator.ts` ```typescript import OpenAI from 'openai'; import { logger } from '../../../common/logging/index.js'; import { ToolExecutionResult } from './ToolExecutor.js'; /** * 答案生成? * 将工具执行结果转换为用户友好的回? */ export class AnswerGenerator { private llm: OpenAI; constructor() { this.llm = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: process.env.OPENAI_BASE_URL || 'https://api.deepseek.com' }); } /** * 生成回答 */ async generate( userQuestion: string, toolResult: ToolExecutionResult, toolName: string ): Promise { try { logger.info('[AnswerGenerator] Generating answer', { question: userQuestion.substring(0, 50), toolName, success: toolResult.success }); // 如果工具执行失败 if (!toolResult.success) { return this.generateErrorMessage(toolResult.error); } // 根据不同工具类型,使用不同的回答模板 if (toolName === 'query_clinical_data') { return this.generateDataAnswer(userQuestion, toolResult.data); } else if (toolName === 'search_knowledge_base') { return await this.generateKnowledgeAnswer(userQuestion, toolResult.data); } return '抱歉,我无法生成回答'; } catch (error: any) { logger.error('[AnswerGenerator] Generation failed', { error: error.message }); return '抱歉,回答生成失败,请稍后再?; } } /** * 生成数据查询的回答(使用模板,不调用LLM? */ private generateDataAnswer(question: string, data: any): string { const type = data.type; if (type === 'project_stats') { return `📊 **项目统计数据** ?**入组人数**?{data.stats.enrolled}??**完成病例**?{data.stats.completed}??**数据质量**?{data.stats.dataQuality} 💡 数据更新时间?{new Date().toLocaleString('zh-CN')}`; } if (type === 'patient_detail') { const details = data.details; return `👤 **患?${data.patientId} 详情** 📋 **基本信息**?- 年龄?{details.age || '未录?}?- 性别?{details.gender || '未录?} - BMI?{details.bmi || '未录?} 📊 **录入状?*?- 数据完整度:${details.complete === '2' ? '?已完? : '?进行?} 💡 最后更新:${new Date().toLocaleString('zh-CN')}`; } if (type === 'qc_status') { const issues = data.recentIssues.slice(0, 5); let answer = `🔍 **质控状?*\n\n`; answer += `⚠️ **质控问题?*?{data.issueCount}个\n\n`; if (issues.length > 0) { answer += `📋 **最近问?*:\n`; issues.forEach((issue: any, index: number) => { answer += `${index + 1}. 记录${issue.recordId}?{JSON.stringify(issue.issue).substring(0, 50)}\n`; }); } else { answer += `?暂无质控问题`; } return answer; } return JSON.stringify(data, null, 2); } /** * 生成知识检索的回答(调用LLM综合? */ private async generateKnowledgeAnswer(question: string, data: any): Promise { const results = data.results; if (results.length === 0) { return `📚 **知识库搜?* ?未找到相关内? 💡 建议?1. 尝试换个关键?2. 查看研究方案原文 3. 联系项目协调员`; } // 将搜索结果拼接为上下? const context = results .map((r: any, index: number) => `[文档${index + 1}] ${r.content}`) .join('\n\n'); // 调用LLM综合回答 const response = await this.llm.chat.completions.create({ model: 'deepseek-chat', messages: [ { role: 'system', content: `你是临床研究助手。根据检索到的文档内容,回答用户问题?要求?1. 回答要准确、简?2. 引用文档时标注[文档X] 3. 如果文档中没有明确答案,诚实说明 4. 使用Markdown格式` }, { role: 'user', content: `用户问题?{question}\n\n检索到的文档内容:\n${context}` } ], temperature: 0.3, max_tokens: 800 }); const answer = response.choices[0].message.content || '无法生成回答'; return `📚 **知识库查询结?*\n\n${answer}`; } /** * 生成错误提示 */ private generateErrorMessage(error?: string): string { return `?查询失败 原因?{error || '未知错误'} 💡 您可以: 1. 稍后重试 2. 换个问法 3. 联系管理员`; } } ``` **验收标准**?- ?可生成数据查询回?- ?可生成知识检索回?- ?回答格式友好(Markdown?- ?错误提示清晰 --- #### 任务3.3:端到端测试?小时? **测试场景**? ```typescript // 测试场景1:查询项目统?{ input: "现在入组多少人了?, expectedTool: "query_clinical_data", expectedIntent: "project_stats", expectedOutput: "📊 项目统计数据\n?入组人数:XX? } // 测试场景2:查询特定患?{ input: "P001患者录完数据了吗?", expectedTool: "query_clinical_data", expectedIntent: "patient_detail", expectedOutput: "👤 患?P001 详情" } // 测试场景3:查询研究方?{ input: "入排标准是什么?", expectedTool: "search_knowledge_base", expectedCategory: "protocol", expectedOutput: "📚 知识库查询结? } // 测试场景4:查询CRF表格 { input: "BMI这个字段怎么填?", expectedTool: "search_knowledge_base", expectedCategory: "crf", expectedOutput: "📚 知识库查询结? } // 测试场景5:闲?{ input: "你好", expectedTool: null, expectedOutput: "您好!我是临床研究助? } ``` **测试步骤**?1. 在企业微信中发送测试消?2. 观察后端日志 3. 验证回复内容 4. 检查审计日? **验收标准**?- ?5个测试场景全部通过 - ?回复时间<3?- ?回复内容准确 - ?审计日志完整 --- ### Day 4:周报自动归档(6小时? #### 任务4.1:实现周报生成器?小时? **文件位置**:`backend/src/modules/iit-manager/services/WeeklyReportGenerator.ts` ```typescript import { prisma } from '../../../config/database.js'; import { RedcapAdapter } from '../adapters/RedcapAdapter.js'; import { DifyAdapter } from '../adapters/DifyAdapter.js'; import { logger } from '../../../common/logging/index.js'; import { getISOWeek, startOfWeek, endOfWeek } from 'date-fns'; /** * 周报生成? * 自动生成项目周报并上传到Dify知识? */ export class WeeklyReportGenerator { /** * 生成并上传周? */ async generateAndUpload(projectId: string): Promise<{ success: boolean; reportId?: string; error?: string; }> { try { const weekNumber = getISOWeek(new Date()); const year = new Date().getFullYear(); logger.info('[WeeklyReportGenerator] Starting generation', { projectId, year, weekNumber }); // 1. 检查是否已生成 const existing = await prisma.iitWeeklyReport.findFirst({ where: { projectId, year, weekNumber } }); if (existing) { logger.warn('[WeeklyReportGenerator] Report already exists', { reportId: existing.id }); return { success: false, error: '本周周报已生? }; } // 2. 收集数据 const reportData = await this.collectWeeklyData(projectId, year, weekNumber); // 3. 生成Markdown内容 const content = this.generateMarkdownContent(reportData, year, weekNumber); // 4. 保存到数据库 const report = await prisma.iitWeeklyReport.create({ data: { projectId, year, weekNumber, content, stats: reportData.stats, createdAt: new Date() } }); // 5. 上传到Dify知识? const dify = new DifyAdapter(projectId); const uploadResult = await dify.uploadDocument(content, { name: `周报-${year}年第${weekNumber}周`, doc_type: 'report', date: `${year}-W${weekNumber.toString().padStart(2, '0')}` }); if (!uploadResult.success) { logger.error('[WeeklyReportGenerator] Dify upload failed'); // 数据库已保存,Dify上传失败不影? } logger.info('[WeeklyReportGenerator] Generation completed', { reportId: report.id, difyUploaded: uploadResult.success }); return { success: true, reportId: report.id }; } catch (error: any) { logger.error('[WeeklyReportGenerator] Generation failed', { error: error.message, projectId }); return { success: false, error: error.message }; } } /** * 收集本周数据 */ private async collectWeeklyData( projectId: string, year: number, weekNumber: number ): Promise { const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); const weekEnd = endOfWeek(new Date(), { weekStartsOn: 1 }); // 1. 获取项目配置 const project = await prisma.iitProject.findUnique({ where: { id: projectId }, select: { name: true, redcapApiUrl: true, redcapApiToken: true } }); // 2. 从REDCap获取统计 const redcap = new RedcapAdapter( project!.redcapApiUrl, project!.redcapApiToken ); const allRecords = await redcap.exportRecords(); // 3. 从审计日志获取本周活? const weeklyLogs = await prisma.iitAuditLog.findMany({ where: { projectId, createdAt: { gte: weekStart, lte: weekEnd } }, orderBy: { createdAt: 'desc' } }); // 4. 统计数据 const stats = { totalRecords: allRecords.length, newRecordsThisWeek: weeklyLogs.filter( log => log.actionType === 'redcap_data_received' ).length, qualityIssues: weeklyLogs.filter( log => log.actionType === 'quality_issue' ).length, wechatNotifications: weeklyLogs.filter( log => log.actionType === 'wechat_notification_sent' ).length }; return { projectName: project!.name, year, weekNumber, weekStart: weekStart.toISOString(), weekEnd: weekEnd.toISOString(), stats, recentActivities: weeklyLogs.slice(0, 20).map(log => ({ actionType: log.actionType, entityId: log.entityId, createdAt: log.createdAt, details: log.details })) }; } /** * 生成Markdown内容 */ private generateMarkdownContent(data: any, year: number, weekNumber: number): string { return `# ${data.projectName} - ${year}年第${weekNumber}周周? ## 📊 统计数据 - **总记录数**?{data.stats.totalRecords}?- **本周新增**?{data.stats.newRecordsThisWeek}?- **质控问题**?{data.stats.qualityIssues}?- **企业微信通知**?{data.stats.wechatNotifications}? ## 📅 时间范围 - **开始时?*?{new Date(data.weekStart).toLocaleString('zh-CN')} - **结束时间**?{new Date(data.weekEnd).toLocaleString('zh-CN')} ## 📋 本周主要活动 ${data.recentActivities .map((activity: any, index: number) => { return `${index + 1}. **${activity.actionType}** - 记录${activity.entityId} (${new Date(activity.createdAt).toLocaleString('zh-CN')})`; }) .join('\n')} ## 💡 重点关注 ${data.stats.qualityIssues > 0 ? `⚠️ 本周发现${data.stats.qualityIssues}个质控问题,请及时处理` : '?本周无质控问?} --- *自动生成时间?{new Date().toLocaleString('zh-CN')}*`; } } // 数据库表定义(需要添加到Prisma Schema?/* model IitWeeklyReport { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid projectId String @db.Uuid year Int weekNumber Int content String @db.Text stats Json createdAt DateTime @default(now()) project IitProject @relation(fields: [projectId], references: [id]) @@unique([projectId, year, weekNumber]) @@map("weekly_reports") @@schema("iit_schema") } */ ``` **验收标准**?- ?可生成周报Markdown - ?可保存到数据?- ?可上传到Dify - ?防止重复生成 --- #### 任务4.2:配置定时任务(2小时? **文件位置**:`backend/src/modules/iit-manager/index.ts` ```typescript import cron from 'node-cron'; import { WeeklyReportGenerator } from './services/WeeklyReportGenerator.js'; /** * 初始化IIT Manager模块 */ export async function initIitManager() { // ... 现有的Worker注册代码 ... // ?新增:注册周报定时任? registerWeeklyReportCron(); logger.info('?IIT Manager initialized'); } /** * 注册周报定时任务 * 每周一 00:00 自动生成上周周报 */ function registerWeeklyReportCron() { const generator = new WeeklyReportGenerator(); // 每周一凌晨0点执? cron.schedule('0 0 * * 1', async () => { logger.info('?开始生成周?); try { // 获取所有活跃项? const projects = await prisma.iitProject.findMany({ where: { status: 'active' } }); for (const project of projects) { await generator.generateAndUpload(project.id); } logger.info('?周报生成完成', { projectCount: projects.length }); } catch (error: any) { logger.error('?周报生成失败', { error: error.message }); } }, { timezone: 'Asia/Shanghai' }); logger.info('?周报定时任务已注册(每周一 00:00?); } ``` **验收标准**?- ?定时任务注册成功 - ?可手动触发测?- ?日志记录完整 --- ### Day 5:文档编写与测试?小时? #### 任务5.1:用户使用手册(2小时? **文件位置**:`AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/05-使用手册/企业微信对话指南.md` **内容大纲**?1. 功能介绍 2. 支持的查询类?3. 常用问法示例 4. 注意事项 5. 常见问题FAQ --- #### 任务5.2:Phase 1.5开发记录(2小时? **文件位置**:`AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/06-开发记?Phase1.5-AI对话能力开发完成记?md` **内容大纲**?1. 开发目标与成果 2. 技术实现细?3. 测试验证结果 4. 已知限制与改进计? --- #### 任务5.3:完整测试(2小时? **测试矩阵**? | 场景 | 输入 | 预期工具 | 预期输出 | 状?| |------|------|---------|---------|------| | 项目统计 | "现在入组多少人?" | query_clinical_data | 包含入组人数 | ?| | 患者详?| "P001录完了吗? | query_clinical_data | 包含患者状?| ?| | 质控状?| "有哪些质控问题?" | query_clinical_data | 问题列表 | ?| | 研究方案 | "入排标准是什么?" | search_knowledge_base | 方案内容 | ?| | CRF查询 | "BMI怎么填?" | search_knowledge_base | CRF说明 | ?| | 周报查询 | "上周进展如何? | search_knowledge_base | 周报内容 | ?| | 闲聊 | "你好" | ?| 友好回复 | ?| --- ## 📊 四、成功标准与验收 ### 4.1 功能完整? | 功能 | 验收标准 | 优先?| |------|---------|-------| | **Dify集成** | 可成功调用本地Dify API | 🔴 P0 | | **意图识别** | 准确?80% | 🔴 P0 | | **数据查询** | 可查询REDCap实时数据 | 🔴 P0 | | **知识检?* | 可检索研究方案文?| 🔴 P0 | | **企业微信回复** | 回复时间<3?| 🔴 P0 | | **周报自动归档** | 每周一自动生成 | 🟠 P1 | | **审计日志** | 所有对话有日志 | 🟠 P1 | ### 4.2 性能指标 | 指标 | 目标 | 说明 | |------|------|------| | **回复延迟** | <3?| 用户??收到回复 | | **Dify查询延迟** | <500ms | 本地部署,应该很?| | **REDCap查询延迟** | <1?| 已有adapter,已验证 | | **意图识别准确?* | >80% | 通过测试矩阵验证 | | **知识检索准确率** | >70% | 依赖文档质量 | ### 4.3 代码质量 - ?TypeScript类型完整 - ?单元测试覆盖?70% - ?集成测试通过 - ?错误处理完善 - ?日志记录完整 - ?代码符合规范 --- ## 🚀 五、部署与上线 ### 5.1 环境变量配置 ```bash # Dify配置 DIFY_API_URL=http://localhost/v1 DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxxx DIFY_KNOWLEDGE_BASE_ID=kb-xxxxxxxxxxxxxxxxxxxxxx # LLM配置(DeepSeek?OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx OPENAI_BASE_URL=https://api.deepseek.com # 企业微信配置(已有) WECHAT_CORP_ID=ww01cb7b72ea2db83c WECHAT_CORP_SECRET=xxx WECHAT_AGENT_ID=1000002 ``` ### 5.2 数据库迁? ```bash # 添加周报?npx prisma db push npx prisma generate ``` ### 5.3 重启服务 ```bash cd AIclinicalresearch/backend npm run dev ``` --- ## 📚 六、技术债务与改进计? ### 6.1 当前限制(Phase 1.5? | 限制 | 影响 | 计划改进时间 | |------|------|------------| | **单项目支?* | 只能服务一个项?| Phase 2 | | **无多轮对?* | 每次问答独立 | Phase 2 | | **无上下文记忆** | 不记得之前的对话 | Phase 2 | | **硬编码知识库ID** | 不支持多项目 | Phase 2 | | **简单意图识?* | 不支持复杂推?| Phase 3 | ### 6.2 Phase 2 改进计划 1. **多项目支?* - 每个项目独立知识? - 用户权限管理 - 项目切换功能 2. **多轮对话** - 会话状态管? - 上下文记忆(Redis? - 澄清式提? 3. **混合推理(ReAct?* - 支持复杂查询 - 多工具组? - 自主推理循环 --- ## ?七、总结 ### 7.1 Phase 1.5核心价? **实现目标**?- ?PI可在企业微信中自然语言提问 - ?AI可理解意图并查询数据/文档 - ?回答准确、及时、友?- ?周报自动归档,可随时查询 **技术亮?*?- 🎯 基于本地Dify,无API成本 - 🧠 单步意图识别,简单高?- 🔧 复用现有RedcapAdapter - 📊 周报自动生成与归?- 🚀 端到端延?3? **开发效?*?- 📅 预估工作量:5?- 📝 新增代码:~2000?- 🧪 测试覆盖?70% - 📚 文档完整:用户手?开发记? --- **下一?*:Phase 2 - 多项目支持与高级对话能力 --- ## 🎯 八、极简版核心价值总结 ### 8.1 为什么极简版最重要? **用户反馈**?> "最重要的是先让AI能对话,其他都先放一? **现实情况**?- ?MVP闭环已打通(Day 3完成?- ?企业微信推送已验证?00%成功率) - ?RedcapAdapter已可用(直接复用?- ⚠️ **缺少**:PI无法主动查询数据 **极简版价?*?- 🚀 **2天上?*:最快实现AI对话 - 💰 **零成?*:只查REDCap,不用Dify - 🧠 **有记?*:支持多轮对话(3轮) - ?**有反?*?正在查询..."避免用户焦虑 - 🎯 **核心够用**:满?0%的查询需? --- ### 8.2 三大核心改进(基于用户建议) #### 改进1:上下文记忆 ? **问题**?``` PI: "帮我查一下P001的入组情? AI: "P001已入? PI: "他有不良反应吗?" AI: ?"请提供患者编?(失忆了?``` **解决**?```typescript // SessionMemory:存储最?轮对?sessionMemory.addMessage(userId, 'user', '帮我查P001'); sessionMemory.addMessage(userId, 'assistant', 'P001已入?); // 下次查询时,自动填充患者ID const lastPatientId = sessionMemory.getLastPatientId(userId); // P001 ``` **效果**?``` PI: "他有不良反应吗?" AI: ?"查询P001:无不良反应记录"(记得是P001?``` --- #### 改进2:正在输入反?? **问题**?- AI处理需?-8?- 用户发完消息后,手机没反?- 用户以为系统挂了 **解决**?```typescript // 立即发送临时反?await wechatService.sendTextMessage(userId, '🫡 正在查询,请稍?..'); // 再慢慢处?const answer = await processQuery(userMessage); await wechatService.sendMarkdownMessage(userId, answer); ``` **效果**?- ?用户立即看到反馈?1秒) - ?知道AI在工?- ?不会焦虑 --- #### 改进3:极简优先 ? **问题**?- 原计?天开发太?- Dify、周报等功能非必需 - 用户最想要:能对话 **解决**?- ?只做REDCap查询(复用现有adapter?- ?不接Dify(Phase 2再做?- ?不做周报(Phase 2再做?- ?2天上? **效果**?- ?快速验证价?- ?快速收集反?- ?快速迭? --- ### 8.3 前端架构演进路线 ``` Phase 1.5(当前): ├── 企业微信原生对话(省事) ├── 无自定义UI └── 上下文存在Node.js内存 ?(用户反?+ 需求增长) Phase 3(未来): ├── 自研H5/小程序(Taro 4.x?├── Ant Design X管理上下?├── 丰富的UI组件(输入提示、历史记录、知识卡片) └── 更好的用户体?``` **为什么分两阶段?** - ?Phase 1.5:验证核心价值(AI能回答问题) - ?Phase 3:优化用户体验(更美观、更智能?- ?避免过度设计(先有再好) --- ### 8.4 立即行动指南 #### Step 1:创建第一个文件(5分钟? ```bash cd AIclinicalresearch/backend/src/modules/iit-manager mkdir -p agents touch agents/SessionMemory.ts ``` 复制Day 1任务1.1的代码到 `SessionMemory.ts` #### Step 2:运行单元测试(可选) ```bash npm test agents/SessionMemory.test.ts ``` #### Step 3:继续Day 1其他任务 按照文档中的顺序?1. ?SessionMemory (30分钟) 2. ?SimpleIntentRouter (2小时) 3. ?SimpleToolExecutor (1.5小时) 4. ?SimpleAnswerGenerator (1小时) 5. ?集成到WechatCallbackController (1小时) #### Step 4:Day 1结束时测? 在企业微信中发送: ``` "现在入组多少人?" ``` 预期?1. 立即收到"🫡 正在查询,请稍?.." 2. 3秒内收到"📊 项目统计..." #### Step 5:Day 2测试多轮对话 ``` PI: "帮我查一下P001的情? AI: "👤 患?P001 详情..." PI: "他有不良反应吗?" ?测试上下?AI: "查询P001:无不良反应记录" ?应该自动识别 ``` --- ### 8.5 成功标准(极简版) | 检查项 | 标准 | 验证方式 | |-------|------|---------| | ?基础对话 | 能回?入组多少? | 企微测试 | | ?患者查?| 能回?P001情况" | 企微测试 | | ?上下文记?| "他有不良反应?能识别P001 | 企微测试 | | ?正在输入反馈 | <1秒收?正在查询..." | 企微测试 | | ?最终回?| <3秒收到完整答?| 后端日志 | | ?审计日志 | 记录上下文标?| 数据库检?| --- ### 8.6 与完整版的关? **极简版(2天)**?- 🎯 目标:最快验证AI对话价?- 📦 范围:REDCap查询 + 上下文记?- 💰 成本:无额外成本(复用现有) - 🚀 速度?天上? **完整版(5天)**?- 🎯 目标:全面的AI助手能力 - 📦 范围? Dify知识?+ 周报归档 + 文档查询 - 💰 成本:需配置Dify(已有Docker?- 🚀 速度?天上? **建议**??**先做极简?*?天),验证价? ?收集用户反馈 ?再决定是否做完整? **实际执行**??**极简版已完成**?026-01-03? ?**Dify知识库已集成**?026-01-04? ?**混合检索已实现**:REDCap实时数据 + Dify文档知识? --- ## 🎓 八、Dify知识库集成(2026-01-04完成? ### 8.7 集成背景 **完成时间**: 2026-01-04 **开发工作量**: 4-6小时 **集成目标**: 在REDCap实时数据查询基础上,增加研究方案文档查询能力 **核心价?*?- 📚 **文档查询**: 查询研究方案、CRF表格、伦理文?- 🔀 **混合检?*: 同时支持结构化数据(REDCap)和非结构化文档(Dify?- 🎯 **智能路由**: 根据用户问题自动选择数据? ### 8.8 技术方? #### 方案选择 | 维度 | 采用方案 | |------|---------| | **知识库架?* | 单项目单知识库(1个IIT项目 ?1个Dify Dataset?| | **文档上传** | Dify Web界面手动上传(MVP阶段?| | **项目关联** | 用户绑定默认项目(存储在`iit_schema.projects.dify_dataset_id`?| #### 核心实现 **1. 扩展意图识别** 在`ChatService.detectIntent()`中新增`query_protocol`意图? ```typescript // 识别文档查询(研究方案、伦理、知情同意、CRF等) if (/(研究方案|伦理|知情同意|CRF|病例报告表|纳入|入选|排除|标准|入组标准|治疗方案|试验设计|研究目的|研究流程|观察指标|诊断标准|疾病标准)/.test(message)) { return { intent: 'query_protocol' }; } ``` **2. 新增Dify查询方法** ```typescript private async queryDifyKnowledge(query: string): Promise { // 1. 获取项目的difyDatasetId const project = await prisma.iitProject.findFirst({ where: { status: 'active' }, select: { name: true, difyDatasetId: true } }); // 2. 调用Dify API检? const retrievalResult = await difyClient.retrieveKnowledge( project.difyDatasetId, query, { retrieval_model: { search_method: 'semantic_search', top_k: 5 } } ); // 3. 格式化检索结? // 修复bug:使用正确的字段路径 record.segment.document.name ?record.segment.content // ... } ``` **3. 更新对话流程** ```typescript async handleMessage(userId: string, userMessage: string): Promise { const { intent, params } = this.detectIntent(userMessage); // REDCap查询 let toolResult: any = null; if (intent === 'query_record') { toolResult = await this.queryRedcapRecord(params.recordId); } // Dify知识库查? let difyKnowledge: string = ''; if (intent === 'query_protocol') { difyKnowledge = await this.queryDifyKnowledge(userMessage); } // 构建LLM消息(同时注入REDCap数据和Dify知识? const messages = this.buildMessagesWithData( userMessage, context, toolResult, difyKnowledge, userId ); // 调用LLM生成回答 const response = await this.llm.chat(messages); // ... } ``` ### 8.9 问题排查与修? #### 问题1: AI不查询Dify,自己编造答? **现象**: 用户?纳入标准是什么?",AI编造了答案,Dify控制台无查询记录 **根因1**: 意图识别关键词不?- **缺少**: "入??诊断标准"?疾病标准" - **解决**: 扩充关键词列? **根因2**: Dify API返回字段路径错误 - **错误**: `record.document_name`、`record.content` ?返回`undefined` - **正确**: `record.segment.document.name`、`record.segment.content` - **解决**: 修正字段访问路径 **调试过程**: 1. 创建`debug-dify-injection.ts`追踪数据注入流程 2. 创建`inspect-dify-response.ts`查看Dify API实际返回结构 3. 发现并修复字段路径错? ### 8.10 测试验证 | 测试场景 | 问题 | 数据?| 结果 | |---------|------|--------|------| | **文档查询** | "这个研究的排除标准是什么?" | Dify | ?成功 | | **CRF查询** | "CRF表格中有哪些观察指标? | Dify | ?成功 | | **患者查?* | "ID 7的患者情? | REDCap | ?成功 | | **统计查询** | "目前入组了多少人? | REDCap | ?成功 | | **混合查询** | "这个研究的主要研究目的是什么?" | Dify | ?成功 | ### 8.11 集成成果 **技术架?*: ``` 用户提问 ?意图识别 ?┬→ [query_protocol] ?Dify API ?文档片段 ├→ [query_record] ?REDCap API ?患者数? └→ [count_records] ?REDCap API ?统计数据 ? 构建LLM Prompt(System + Data + Context? ? DeepSeek-V3 ? AI回答 ``` **核心能力**: 1. ?**混合检?*: 同时支持结构化数据和非结构化文档 2. ?**智能路由**: 根据意图自动选择数据?3. ?**防止幻觉**: 所有回答基于真实数?文档 4. ?**来源标注**: 清晰标注数据来自REDCap或Dify **详细记录**: 参见 [Dify知识库集成开发记录](../06-开发记?2026-01-04-Dify知识库集成开发记?md) --- ## ?九、总结 ### 核心成就(极简?+ Dify集成? 1. ?**2天上?*:最快实现AI对话能力(含Dify集成?2. ?**上下文记?*:支持多轮对话(3轮) 3. ?**正在输入反馈**:避免用户焦?4. ?**代词解析**??能自动识别患?5. ?**混合检?*:同时支持REDCap实时数据 + Dify文档知识?6. ?**防止幻觉**:所有回答基于真实数据,绝不编? ### 技术亮? - 🧠 **SessionMemory**:内存存储,无需Redis - 🎯 **单步路由**:不用复杂ReAct循环 - 🔧 **复用现有**:RedcapAdapter + WechatService - ?**性能保证**?3秒端到端延迟 - 📊 **审计完整**:记录所有对? ### 用户价? **Before(Day 3?*?- ?PI可以接收企业微信通知 - ?PI无法主动查询数据 - ?需要登录REDCap查看 **After(Phase 1.5 + Dify集成?*?- ?PI可以在企业微信中直接?入组多少?(REDCap?- ?PI可以?P001有不良反应吗"(REDCap?- ?PI可以?研究的纳入排除标准是什?(Dify?- ?PI可以?CRF表格中有哪些观察指标"(Dify?- ?AI记得上一轮对话,支持代词 - ?回复快速(<6秒),有反馈 - ?AI基于真实数据/文档回答,不编? --- ## 🎉 Phase 1.5 开发完成总结 (2026-01-03 & 2026-01-04) ### **实际完成情况** - ?**Day 1完成** (2026-01-03): SessionMemory + ChatService + REDCap集成 - ?**Day 2完成** (2026-01-04): Dify知识库集?+ 混合检?- ?**测试通过**: 企业微信对话 + 真实数据查询 + 文档查询 - ?**核心突破**: 解决LLM幻觉问题 + 混合检索架? ### **关键成果** 1. ?AI基于REDCap真实数据回答,不编?2. ?AI基于Dify知识库文档回答研究方案问?3. ?混合检索:同时支持结构化数据和非结构化文档 4. ?从数据库读取项目配置(test0102?5. ?意图识别 + 智能路由 + 数据查询 + LLM集成 6. ?上下文记忆(最?轮对话) 7. ?即时反馈?正在查询"? ### **测试验证** - **项目**: test0102 - REDCap PID: 16, 11条记? - Dify Dataset ID: `b49595b2-bf71-4e47-9988-4aa2816d3c6f` - 文档: 研究方案、CRF表格?个文件,已处理) - **场景1**: 查询ID 7患者信息(REDCap)→ ?完全匹配真实数据 - **场景2**: 查询研究排除标准(Dify)→ ?基于文档准确回答 - **场景3**: 查询CRF观察指标(Dify)→ ?基于文档准确回答 - **场景4**: 统计入组人数(REDCap)→ ?准确统计11?- **结果**: ?所有测试通过,无编? ### **详细记录** - [Phase 1.5开发完成记?(REDCap集成)](../06-开发记?Phase1.5-AI对话集成REDCap完成记录.md) - [Dify知识库集成开发记录](../06-开发记?2026-01-04-Dify知识库集成开发记?md) --- **维护?*:IIT Manager开发团? **最后更?*?026-01-03 **文档状?*:✅ Phase 1.5已完?