Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/Phase1.5-AI对话能力开发计划.md
HaHafeng b47079b387 feat(iit): Phase 1.5 AI对话集成REDCap真实数据完成
- feat: ChatService集成DeepSeek-V3实现AI对话(390行)
- feat: SessionMemory实现上下文记忆(最近3轮对话,170行)
- feat: 意图识别支持REDCap数据查询(关键词匹配)
- feat: REDCap数据注入LLM(queryRedcapRecord, countRedcapRecords, getProjectInfo)
- feat: 解决LLM幻觉问题(基于真实数据回答,明确system prompt)
- feat: 即时反馈(正在查询...提示)
- test: REDCap查询测试通过(test0102项目,10条记录,ID 7患者详情)
- docs: 创建Phase1.5开发完成记录(313行)
- docs: 更新Phase1.5开发计划(标记完成)
- docs: 更新MVP开发任务清单(Phase 1.5完成)
- docs: 更新模块当前状态(60%完成度)
- docs: 更新系统总体设计文档(v2.6)
- chore: 删除测试脚本(test-redcap-query-for-ai.ts, check-env-config.ts)
- chore: 移除REDCap测试环境变量(REDCAP_TEST_*)

技术亮点:
- AI基于REDCap真实数据对话,不编造信息
- 从数据库读取项目配置,不使用环境变量
- 企业微信端测试通过,用户体验良好

测试通过:
-  查询项目记录总数(10条)
-  查询特定患者详情(ID 7)
-  项目信息查询
-  上下文记忆(3轮对话)
-  即时反馈提示

影响范围:IIT Manager Agent模块
2026-01-03 22:48:10 +08:00

78 KiB
Raw Blame History

IIT Manager Agent - Phase 1.5 AI对话能力开发计划

版本: v2.0(极简版 + 上下文记忆)
创建日期: 2026-01-03
完成日期: 2026-01-03
状态: 已完成
实际工作量: ~1天极简版
核心价值: PI可在企业微信中自然对话查询REDCap真实数据
核心成就: REDCap数据集成 + 上下文记忆 + 解决LLM幻觉


🚀 极简版快速启动1天上线 通用能力层加速!

🎉 重大发现:通用能力层已完善!

平台现状2026-01-03调研结果

  • LLMFactory 完全就绪5种模型DeepSeek/Qwen/GPT-5/Claude单例模式零配置
  • ChatContainer 完全就绪Ant Design X组件已在Tool C验证~968行
  • 环境变量已配置DEEPSEEK_API_KEYQWEN_API_KEY
  • 成熟实践ASL、DC模块已大量使用稳定可靠

核心优势

// 后端LLM调用3行代码
import { LLMFactory } from '@/common/llm/adapters/LLMFactory.js';
const llm = LLMFactory.getAdapter('deepseek-v3');
const response = await llm.chat(messages, { temperature: 0.7 });

极简版功能范围

✅ Day 14-6小时: 实现基础对话 + 上下文记忆 + "typing"反馈
   - 复用LLMFactory0开发
   - 创建ChatService.ts2小时
   - 创建SessionMemory.ts2小时
   - 修改WechatCallbackController2小时
❌ 暂不实现: Dify知识库、周报生成、复杂Tool Calling

极简版架构(复用通用能力层)

PI提问 → Node.js → LLMFactory(deepseek-v3) → 生成回答 → 企业微信推送
                      ↓                ↑
                 SessionMemory    RedcapAdapter可选

关键决策

  • 🚀 复用LLMFactory(已有,零开发,推荐deepseek-v3
  • 只查REDCap数据已有RedcapAdapter复用即可
  • 不接Dify(减少依赖,加快开发)
  • 上下文记忆Node.js内存存最近3轮
  • 正在输入反馈(立即回"正在查询..."
  • 单步路由不用ReAct循环

预估工作量大幅降低2-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基础对话能力6小时

核心目标

让AI能回答用户问题只查REDCap数据

任务1.1创建SessionMemory30分钟

文件位置backend/src/modules/iit-manager/agents/SessionMemory.ts

/**
 * 会话记忆管理器(内存版)
 * 存储用户最近3轮对话用于上下文理解
 */
export class SessionMemory {
  // 内存存储:{ userId: ConversationHistory }
  private sessions: Map<string, ConversationHistory> = new Map();
  private readonly MAX_HISTORY = 3; // 只保留最近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()
    });

    // 只保留最近3轮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);
  }

  /**
   * 清理过期会话超过1小时未使用
   */
  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创建ChatService2小时 复用LLMFactory

文件位置backend/src/modules/iit-manager/services/ChatService.ts

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<IntentRouteResult> {
    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简化ToolExecutor1.5小时)

文件位置backend/src/modules/iit-manager/agents/SimpleToolExecutor.ts

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<ToolExecutionResult> {
    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<ToolExecutionResult> {
    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<ToolExecutionResult> {
    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<ToolExecutionResult> {
    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简化AnswerGenerator1小时

文件位置backend/src/modules/iit-manager/agents/SimpleAnswerGenerator.ts

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集成到WechatCallbackController1小时

修改文件backend/src/modules/iit-manager/controllers/WechatCallbackController.ts

在handleCallback方法中添加

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<void> {
    // ... 现有的验证、解密逻辑 ...

    // ✅ 立即返回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上下文优化 + 测试4小时

任务2.1上下文记忆优化1小时

增强SessionMemory支持患者ID提取

// 在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中使用

// 如果用户说"他有不良反应吗?"自动填充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小时

测试场景

// 场景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
上下文记忆 支持最近3轮对话 🔴 P0
代词解析 "他"能自动识别患者 🔴 P0
正在输入反馈 立即回"正在查询..." 🔴 P0
回复延迟 <3秒 🔴 P0
意图识别准确率 >80% 🔴 P0

🎉 极简版vs完整版对比

功能 极简版 (2天) 完整版 (5天)
REDCap查询
上下文记忆 (内存3轮) (内存3轮)
正在输入反馈
Dify知识库
周报自动归档
文档查询

🗓️ 三、完整版开发计划5天可选

Day 1Dify环境配置与知识库创建8小时

任务1.1验证Dify本地环境1小时

检查项

# 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

代码实现

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<DifySearchResult> {
    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

# 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

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意图识别与路由逻辑8小时

任务2.1设计工具定义Tool Schema2小时

文件位置backend/src/modules/iit-manager/agents/tools.ts

/**
 * 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 Router3小时

文件位置backend/src/modules/iit-manager/agents/IntentRouter.ts

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<IntentRouteResult> {
    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 Executor3小时

文件位置backend/src/modules/iit-manager/agents/ToolExecutor.ts

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<ToolExecutionResult> {
    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<ToolExecutionResult> {
    // 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<ToolExecutionResult> {
    // 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增强WechatCallbackController3小时

修改文件backend/src/modules/iit-manager/controllers/WechatCallbackController.ts

在现有的handleCallback方法中增加AI对话逻辑

// 在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<void> {
    // ... 现有的验证、解密逻辑 ...

    // ✅ 立即返回success避免5秒超时
    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 Generator2小时

文件位置backend/src/modules/iit-manager/agents/AnswerGenerator.ts

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<string> {
    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<string> {
    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端到端测试3小时

测试场景

// 测试场景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实现周报生成器3小时

文件位置backend/src/modules/iit-manager/services/WeeklyReportGenerator.ts

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<any> {
    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

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文档编写与测试6小时

任务5.1用户使用手册2小时

文件位置AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/05-使用手册/企业微信对话指南.md

内容大纲

  1. 功能介绍
  2. 支持的查询类型
  3. 常用问法示例
  4. 注意事项
  5. 常见问题FAQ

任务5.2Phase 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 环境变量配置

# 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 数据库迁移

# 添加周报表
npx prisma db push
npx prisma generate

5.3 重启服务

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完成
  • 企业微信推送已验证100%成功率)
  • RedcapAdapter已可用直接复用
  • ⚠️ 缺少PI无法主动查询数据

极简版价值

  • 🚀 2天上线最快实现AI对话
  • 💰 零成本只查REDCap不用Dify
  • 🧠 有记忆支持多轮对话3轮
  • 有反馈"正在查询..."避免用户焦虑
  • 🎯 核心够用满足80%的查询需求

8.2 三大核心改进(基于用户建议)

改进1上下文记忆

问题

PI: "帮我查一下P001的入组情况"
AI: "P001已入组"
PI: "他有不良反应吗?"
AI: ❌ "请提供患者编号"(失忆了)

解决

// SessionMemory存储最近3轮对话
sessionMemory.addMessage(userId, 'user', '帮我查P001');
sessionMemory.addMessage(userId, 'assistant', 'P001已入组');

// 下次查询时自动填充患者ID
const lastPatientId = sessionMemory.getLastPatientId(userId); // P001

效果

PI: "他有不良反应吗?"
AI: ✅ "查询P001无不良反应记录"记得是P001

改进2正在输入反馈

问题

  • AI处理需要5-8秒
  • 用户发完消息后,手机没反应
  • 用户以为系统挂了

解决

// 立即发送临时反馈
await wechatService.sendTextMessage(userId, '🫡 正在查询,请稍候...');

// 再慢慢处理
const answer = await processQuery(userMessage);
await wechatService.sendMarkdownMessage(userId, answer);

效果

  • 用户立即看到反馈(<1秒
  • 知道AI在工作
  • 不会焦虑

改进3极简优先

问题

  • 原计划5天开发太长
  • Dify、周报等功能非必需
  • 用户最想要:能对话

解决

  • 只做REDCap查询复用现有adapter
  • 不接DifyPhase 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分钟

cd AIclinicalresearch/backend/src/modules/iit-manager
mkdir -p agents
touch agents/SessionMemory.ts

复制Day 1任务1.1的代码到 SessionMemory.ts

Step 2运行单元测试可选

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 4Day 1结束时测试

在企业微信中发送:

"现在入组多少人?"

预期:

  1. 立即收到"🫡 正在查询,请稍候..."
  2. 3秒内收到"📊 项目统计..."

Step 5Day 2测试多轮对话

PI: "帮我查一下P001的情况"
AI: "👤 患者 P001 详情..."

PI: "他有不良反应吗?" ← 测试上下文
AI: "查询P001无不良反应记录" ← 应该自动识别

8.5 成功标准(极简版)

检查项 标准 验证方式
基础对话 能回答"入组多少人" 企微测试
患者查询 能回答"P001情况" 企微测试
上下文记忆 "他有不良反应吗"能识别P001 企微测试
正在输入反馈 <1秒收到"正在查询..." 企微测试
最终回复 <3秒收到完整答案 后端日志
审计日志 记录上下文标记 数据库检查

8.6 与完整版的关系

极简版2天

  • 🎯 目标最快验证AI对话价值
  • 📦 范围REDCap查询 + 上下文记忆
  • 💰 成本:无额外成本(复用现有)
  • 🚀 速度2天上线

完整版5天

  • 🎯 目标全面的AI助手能力
  • 📦 范围:+ Dify知识库 + 周报归档 + 文档查询
  • 💰 成本需配置Dify已有Docker
  • 🚀 速度5天上线

建议 先做极简版2天验证价值
收集用户反馈
再决定是否做完整版


九、总结

核心成就(极简版)

  1. 2天上线最快实现AI对话能力
  2. 上下文记忆支持多轮对话3轮
  3. 正在输入反馈:避免用户焦虑
  4. 代词解析"他"能自动识别患者
  5. 零成本只查REDCap不用额外服务

技术亮点

  • 🧠 SessionMemory内存存储无需Redis
  • 🎯 单步路由不用复杂ReAct循环
  • 🔧 复用现有RedcapAdapter + WechatService
  • 性能保证<3秒端到端延迟
  • 📊 审计完整:记录所有对话

用户价值

BeforeDay 3

  • PI可以接收企业微信通知
  • PI无法主动查询数据
  • 需要登录REDCap查看

AfterPhase 1.5极简版)

  • PI可以在企业微信中直接问"入组多少人"
  • PI可以问"P001有不良反应吗"
  • AI记得上一轮对话支持代词
  • 回复快速(<3秒有反馈

🎉 Phase 1.5 开发完成总结 (2026-01-03)

实际完成情况

  • Day 1完成: SessionMemory + ChatService + REDCap集成
  • 测试通过: 企业微信对话 + 真实数据查询
  • 核心突破: 解决LLM幻觉问题

关键成果

  1. AI基于REDCap真实数据回答不编造
  2. 从数据库读取项目配置test0102
  3. 意图识别 + 数据查询 + LLM集成
  4. 上下文记忆最近3轮对话
  5. 即时反馈("正在查询"

测试验证

  • 项目: test0102 (REDCap PID: 16, 10条记录)
  • 场景: 查询ID 7患者信息
  • 结果: 完全匹配真实数据,无编造

详细记录

参见:Phase 1.5开发完成记录


维护者IIT Manager开发团队
最后更新2026-01-03
文档状态 Phase 1.5已完成