Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day3开发计划.md
HaHafeng 91cab452d1 fix(dc/tool-c): Fix special character handling and improve UX
Major fixes:
- Fix pivot transformation with special characters in column names
- Fix compute column validation for Chinese punctuation
- Fix recode dialog to fetch unique values from full dataset via new API
- Add column mapping mechanism to handle special characters

Database migration:
- Add column_mapping field to dc_tool_c_sessions table
- Migration file: 20251208_add_column_mapping

UX improvements:
- Darken table grid lines for better visibility
- Reduce column width by 40% with tooltip support
- Insert new columns next to source columns
- Preserve original row order after operations
- Add notice about 50-row preview limit

Modified files:
- Backend: SessionService, SessionController, QuickActionService, routes
- Python: pivot.py, compute.py, recode.py, binning.py, conditional.py
- Frontend: DataGrid, RecodeDialog, index.tsx, ag-grid-custom.css
- Database: schema.prisma, migration SQL

Status: Code complete, database migrated, ready for testing
2025-12-08 23:20:55 +08:00

26 KiB
Raw Blame History

工具C Day 3 开发计划 - AI代码生成服务

文档版本: V1.0
创建日期: 2025-12-06
开发目标: AI代码生成 + Python执行 + 自我修正
预计工时: 5.5-6小时
依赖: Day 2已完成Session管理


📋 核心决策总结

决策1: 对话存储方式

方案选择: 创建独立表 dc_tool_c_ai_history

理由:

  • 未来模块可能独立销售或独立部署
  • 符合Schema隔离原则
  • Tool C有特殊字段需求code、executeResult

数据库Schema:

model DcToolCAiHistory {
  id                String    @id @default(uuid())
  sessionId         String    // 关联Tool C Session
  userId            String
  role              String    // user/assistant/system
  content           String    @db.Text
  
  // Tool C特有字段
  generatedCode     String?   @db.Text  // AI生成的代码
  codeExplanation   String?   @db.Text  // 代码解释
  executeStatus     String?   // success/failed/pending
  executeResult     Json?     // 执行结果
  executeError      String?   @db.Text  // 错误信息
  retryCount        Int       @default(0)
  
  model             String?   // deepseek-v3
  createdAt         DateTime  @default(now())
  
  @@index([sessionId])
  @@index([userId])
  @@map("dc_tool_c_ai_history")
  @@schema("dc_schema")
}

决策2: AI代码执行流程

方案选择: 用户确认后执行方案A

流程:

用户输入自然语言
    ↓
AI生成代码 + 解释
    ↓
前端展示代码(高亮)
    ↓
用户点击"执行"按钮 ← 用户确认
    ↓
Python服务执行代码
    ↓
返回结果 + 数据预览前50行

理由:

  • 用户可审查代码(安全可控)
  • 符合"AI辅助"而非"AI自动"的定位
  • 降低执行错误风险

决策3: System Prompt设计

方案选择: 完整版10个Few-shot示例

示例分布:

级别 数量 示例编号 场景
Level 1 2个 1-2 缺失值统一、数值清洗
Level 2 2个 3-4 编码、分箱
Level 3 3个 5-7 BMI、日期、筛选
Level 4 3个 8-10 简单填补、多重插补、去重

核心亮点:

  • 包含缺失值处理示例8
  • 包含多重插补MICE示例9 重点
  • 覆盖从基础到高级全梯度

文档位置: 工具C_AI_Few-shot示例库.md


决策4: 数据状态管理

方案选择: Python内存维护方案C

架构:

Session创建 → 数据加载到Python内存
    ↓
AI操作1 → 修改内存中的DataFrame
    ↓
AI操作2 → 继续修改DataFrame累积
    ↓
AI操作N → ...
    ↓
用户点击"导出" → 保存到OSS

技术债务:

  • 📝 Python重启会丢失状态
  • 📝 未来优化持久化到Redis或OSS
  • 📝 文档位置: 技术债务清单

决策5: AI自我修正机制

方案选择: 最多3次重试方案B

流程:

attempt = 0
while attempt < 3:
    # 生成代码
    code = generate_code(user_message + error_feedback)
    
    # 执行代码
    result = execute_code(code)
    
    if result.success:
        return result  # ✅ 成功
    
    error_feedback = result.error
    attempt += 1

# ❌ 3次仍失败返回友好错误
return "执行失败,请调整需求后重试"

决策6: LLM模型选择

优先使用: DeepSeek-V3

配置:

const llm = LLMFactory.createAdapter('deepseek-v3');
const response = await llm.chat(messages, {
  temperature: 0.1,    // 低温度,确保代码准确性
  maxTokens: 2000,     // 足够生成代码+解释
  topP: 0.9
});

备选方案:

  • Qwen3-72B: 中文理解更好
  • GPT-5-Pro: 代码质量最高(成本高)

决策7: 上下文传递

配置: 传递最近5轮对话

实现:

async getConversationHistory(sessionId: string, limit: number = 5) {
  return await prisma.dcToolCAiHistory.findMany({
    where: { sessionId },
    orderBy: { createdAt: 'desc' },
    take: limit * 2,  // user + assistant 成对
  });
}

// 构建消息上下文
const messages = [
  { role: 'system', content: systemPrompt },
  ...history.reverse(),  // 最近5轮
  { role: 'user', content: userMessage }
];

决策8: 执行结果展示

配置: 返回前50行预览

原因:

  • 50行足够查看数据变化
  • 不会过大影响性能
  • 符合医疗数据场景(通常几十到几百行)

决策9: Few-shot示例确认

最终10个示例:

  1. 统一缺失值标记
  2. 数值列清洗(检验值符号处理)
  3. 分类变量编码性别→1/0
  4. 连续变量分箱(年龄分组)
  5. BMI计算与分类
  6. 日期计算(住院天数)
  7. 条件筛选(入组标准)
  8. 简单缺失值填补(中位数)
  9. 多重插补MICE 核心
  10. 智能去重(按日期保留最新)

🏗️ 技术架构设计

整体架构

┌─────────────────────────────────────────────────┐
│           Frontend (React)                       │
│  - 对话界面Tool C专用                        │
│  - 代码展示(高亮)                              │
│  - 执行按钮                                      │
│  - 结果预览AG Grid                           │
└──────────────┬──────────────────────────────────┘
               │ REST API
┌──────────────▼──────────────────────────────────┐
│      Node.js Backend (Fastify)                   │
│  ┌─────────────────────────────────────────┐    │
│  │ AICodeService                           │    │
│  │ - generateCode()                        │    │
│  │ - executeCode()                         │    │
│  │ - generateAndExecute() (带重试)        │    │
│  │ - getHistory()                          │    │
│  └──────────┬───────────────┬──────────────┘    │
│             │               │                    │
│     ┌───────▼─────┐   ┌────▼──────────┐         │
│     │ LLMFactory  │   │ PythonExecutor│         │
│     │ (通用层复用)│   │ Service       │         │
│     └─────────────┘   └───────┬───────┘         │
└──────────────────────────────┼─────────────────┘
                                │ HTTP
                    ┌───────────▼──────────────────┐
                    │ Python Service (FastAPI)      │
                    │ - /api/dc/validate (AST检查) │
                    │ - /api/dc/execute (执行代码) │
                    │ - Session状态管理内存    │
                    └──────────────────────────────┘

核心服务设计

AICodeService (新建,~400行)

// backend/src/modules/dc/tool-c/services/AICodeService.ts
export class AICodeService {
  
  // ==================== 核心方法 ====================
  
  /**
   * 生成Pandas代码
   * @param sessionId - Tool C Session ID
   * @param userMessage - 用户自然语言需求
   * @returns { code, explanation, messageId }
   */
  async generateCode(
    sessionId: string,
    userMessage: string
  ): Promise<GenerateCodeResult> {
    // 1. 获取Session信息数据集元数据
    const session = await sessionService.getSession(sessionId);
    
    // 2. 构建System Prompt含10个Few-shot
    const systemPrompt = this.buildSystemPrompt(session);
    
    // 3. 获取对话历史最近5轮
    const history = await this.getHistory(sessionId, 5);
    
    // 4. 调用LLM复用LLMFactory
    const llm = LLMFactory.createAdapter('deepseek-v3');
    const response = await llm.chat([
      { role: 'system', content: systemPrompt },
      ...history,
      { role: 'user', content: userMessage }
    ], {
      temperature: 0.1,
      maxTokens: 2000
    });
    
    // 5. 解析AI回复提取code和explanation
    const parsed = this.parseAIResponse(response.content);
    
    // 6. 保存到数据库
    const messageId = await this.saveMessages(
      sessionId,
      userMessage,
      parsed.code,
      parsed.explanation
    );
    
    return {
      code: parsed.code,
      explanation: parsed.explanation,
      messageId
    };
  }
  
  /**
   * 执行Python代码
   * @param sessionId - Tool C Session ID
   * @param code - Python代码
   * @param messageId - 关联的消息ID
   * @returns { success, result, newDataPreview }
   */
  async executeCode(
    sessionId: string,
    code: string,
    messageId: string
  ): Promise<ExecuteCodeResult> {
    // 1. 调用Python服务执行
    const result = await pythonExecutorService.executeCode(code, {
      sessionId  // Python服务维护Session状态
    });
    
    // 2. 更新消息状态
    await prisma.dcToolCAiHistory.update({
      where: { id: messageId },
      data: {
        executeStatus: result.success ? 'success' : 'failed',
        executeResult: result.data,
        executeError: result.error
      }
    });
    
    // 3. 如果成功获取新数据预览前50行
    if (result.success) {
      const preview = result.data?.slice(0, 50);
      return {
        success: true,
        result: result.data,
        newDataPreview: preview
      };
    }
    
    return {
      success: false,
      error: result.error
    };
  }
  
  /**
   * 生成并执行(带自我修正)
   * @param sessionId - Tool C Session ID
   * @param userMessage - 用户需求
   * @param maxRetries - 最大重试次数默认3
   * @returns { code, explanation, executeResult, retryCount }
   */
  async generateAndExecute(
    sessionId: string,
    userMessage: string,
    maxRetries: number = 3
  ): Promise<ProcessResult> {
    let attempt = 0;
    let lastError: string | null = null;
    let generated: GenerateCodeResult | null = null;
    
    while (attempt < maxRetries) {
      try {
        // 构建带错误反馈的提示词
        const enhancedMessage = attempt === 0
          ? userMessage
          : `${userMessage}\n\n上次执行错误${lastError}\n请修正代码。`;
        
        // 生成代码
        generated = await this.generateCode(sessionId, enhancedMessage);
        
        // 执行代码
        const executeResult = await this.executeCode(
          sessionId,
          generated.code,
          generated.messageId
        );
        
        if (executeResult.success) {
          // ✅ 成功
          logger.info(`[AICodeService] 执行成功(尝试${attempt + 1}次)`);
          return {
            ...generated,
            executeResult,
            retryCount: attempt
          };
        }
        
        // ❌ 失败,准备重试
        lastError = executeResult.error || '未知错误';
        attempt++;
        
        logger.warn(`[AICodeService] 执行失败(尝试${attempt}/${maxRetries}: ${lastError}`);
        
      } catch (error: any) {
        logger.error(`[AICodeService] 异常: ${error.message}`);
        lastError = error.message;
        attempt++;
      }
    }
    
    // 3次仍失败
    throw new Error(
      `代码执行失败(已重试${maxRetries}次)。最后错误:${lastError}。` +
      `建议:请调整需求描述或手动修改代码。`
    );
  }
  
  /**
   * 获取对话历史
   */
  async getHistory(sessionId: string, limit: number = 5): Promise<Message[]> {
    const records = await prisma.dcToolCAiHistory.findMany({
      where: { sessionId },
      orderBy: { createdAt: 'desc' },
      take: limit * 2  // user + assistant 成对
    });
    
    return records.reverse().map(r => ({
      role: r.role,
      content: r.content
    }));
  }
  
  // ==================== 辅助方法 ====================
  
  /**
   * 构建System Prompt含10个Few-shot
   */
  private buildSystemPrompt(session: SessionData): string {
    return `你是医疗科研数据清洗专家负责生成Pandas代码来清洗整理数据。

## 当前数据集信息
- 文件名: ${session.fileName}
- 行数: ${session.totalRows}
- 列数: ${session.totalCols}
- 列名: ${session.columns.join(', ')}

## 安全规则(强制)
1. 只能操作df变量不能修改其他变量
2. 禁止导入os、sys、subprocess等危险模块
3. 禁止使用eval、exec、__import__等危险函数
4. 必须进行异常处理
5. 返回格式必须是JSON: {"code": "...", "explanation": "..."}

## Few-shot示例

### 示例1: 统一缺失值标记
用户: 把所有代表缺失的符号(-、不详、NA、N/A统一替换为标准空值
代码:
\`\`\`python
df = df.replace(['-', '不详', 'NA', 'N/A', '\\\\', '未查'], np.nan)
\`\`\`
说明: 将多种缺失值表示统一为NaN便于后续统计分析

### 示例2: 数值列清洗
用户: 把肌酐列里的非数字符号去掉,<0.1按0.05处理,转为数值类型
代码:
\`\`\`python
df['creatinine'] = df['creatinine'].astype(str).str.replace('>', '').str.replace('<', '')
df.loc[df['creatinine'] == '0.1', 'creatinine'] = '0.05'
df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce')
\`\`\`
说明: 检验科数据常含符号,需清理后才能计算

[... 示例3-8 ...]

### 示例9: 多重插补MICE⭐ 重点
用户: 使用多重插补法对BMI、年龄、肌酐列的缺失值进行填补
代码:
\`\`\`python
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

# 选择需要插补的数值列
cols = ['BMI', 'age', 'creatinine']

# 多重插补
imputer = IterativeImputer(max_iter=10, random_state=0)
df[cols] = imputer.fit_transform(df[cols])
\`\`\`
说明: 利用变量间相关性预测缺失值保持数据分布特征适用于MAR随机缺失

### 示例10: 智能去重
用户: 按患者ID去重保留检查日期最新的记录
代码:
\`\`\`python
df['check_date'] = pd.to_datetime(df['check_date'])
df = df.sort_values('check_date').drop_duplicates(subset=['patient_id'], keep='last')
\`\`\`
说明: 先按日期排序,再去重保留最后一条(最新)

## 用户当前请求
请根据以上示例和当前数据集信息,生成代码并解释。
`;
  }
  
  /**
   * 解析AI回复提取code和explanation
   */
  private parseAIResponse(content: string): { code: string; explanation: string } {
    try {
      // 方法1尝试解析JSON
      const json = JSON.parse(content);
      if (json.code && json.explanation) {
        return { code: json.code, explanation: json.explanation };
      }
    } catch {
      // 方法2正则提取代码块
      const codeMatch = content.match(/```python\n([\s\S]+?)\n```/);
      const code = codeMatch ? codeMatch[1] : '';
      
      // 提取解释(代码块之外的文本)
      const explanation = content.replace(/```python[\s\S]+?```/g, '').trim();
      
      if (code) {
        return { code, explanation };
      }
    }
    
    throw new Error('AI回复格式错误无法提取代码');
  }
  
  /**
   * 保存消息到数据库
   */
  private async saveMessages(
    sessionId: string,
    userMessage: string,
    code: string,
    explanation: string
  ): Promise<string> {
    // 保存用户消息
    await prisma.dcToolCAiHistory.create({
      data: {
        sessionId,
        userId: 'test-user',  // TODO: 从JWT获取
        role: 'user',
        content: userMessage
      }
    });
    
    // 保存AI回复
    const assistantMessage = await prisma.dcToolCAiHistory.create({
      data: {
        sessionId,
        userId: 'test-user',
        role: 'assistant',
        content: explanation,
        generatedCode: code,
        codeExplanation: explanation,
        executeStatus: 'pending',
        model: 'deepseek-v3'
      }
    });
    
    return assistantMessage.id;
  }
}

// 导出单例
export const aiCodeService = new AICodeService();

AIController (新建,~200行)

// backend/src/modules/dc/tool-c/controllers/AIController.ts
export class AIController {
  
  /**
   * POST /api/v1/dc/tool-c/ai/generate
   * 生成代码(不执行)
   */
  async generateCode(request: FastifyRequest, reply: FastifyReply) {
    try {
      const { sessionId, message } = request.body as any;
      
      // 参数验证
      if (!sessionId || !message) {
        return reply.code(400).send({
          success: false,
          error: '缺少必要参数sessionId、message'
        });
      }
      
      // 生成代码
      const result = await aiCodeService.generateCode(sessionId, message);
      
      return reply.code(200).send({
        success: true,
        data: result
      });
    } catch (error: any) {
      logger.error(`[AIController] generateCode失败: ${error.message}`);
      return reply.code(500).send({
        success: false,
        error: error.message
      });
    }
  }
  
  /**
   * POST /api/v1/dc/tool-c/ai/execute
   * 执行代码
   */
  async executeCode(request: FastifyRequest, reply: FastifyReply) {
    try {
      const { sessionId, code, messageId } = request.body as any;
      
      const result = await aiCodeService.executeCode(sessionId, code, messageId);
      
      return reply.code(200).send({
        success: true,
        data: result
      });
    } catch (error: any) {
      logger.error(`[AIController] executeCode失败: ${error.message}`);
      return reply.code(500).send({
        success: false,
        error: error.message
      });
    }
  }
  
  /**
   * POST /api/v1/dc/tool-c/ai/process
   * 生成并执行(一步到位,带重试)
   */
  async process(request: FastifyRequest, reply: FastifyReply) {
    try {
      const { sessionId, message, maxRetries = 3 } = request.body as any;
      
      const result = await aiCodeService.generateAndExecute(
        sessionId,
        message,
        maxRetries
      );
      
      return reply.code(200).send({
        success: true,
        data: result
      });
    } catch (error: any) {
      logger.error(`[AIController] process失败: ${error.message}`);
      return reply.code(500).send({
        success: false,
        error: error.message
      });
    }
  }
  
  /**
   * GET /api/v1/dc/tool-c/ai/history/:sessionId
   * 获取对话历史
   */
  async getHistory(
    request: FastifyRequest<{ Params: { sessionId: string } }>,
    reply: FastifyReply
  ) {
    try {
      const { sessionId } = request.params;
      
      const history = await aiCodeService.getHistory(sessionId, 10);
      
      return reply.code(200).send({
        success: true,
        data: { history }
      });
    } catch (error: any) {
      logger.error(`[AIController] getHistory失败: ${error.message}`);
      return reply.code(500).send({
        success: false,
        error: error.message
      });
    }
  }
}

export const aiController = new AIController();

📅 开发计划

阶段1: 数据库设计30分钟 09:00-09:30

任务清单:

  • 更新 backend/prisma/schema.prisma添加DcToolCAiHistory模型
  • 创建数据库迁移脚本 create-tool-c-ai-history-table.mjs
  • 执行迁移(创建表)
  • 验证表结构
  • 生成Prisma Client (npx prisma generate)

交付物:

  • dc_schema.dc_tool_c_ai_history 表创建成功
  • Prisma Client更新完成

阶段2: AICodeService实现2小时 09:30-11:30

任务清单:

  • 创建 AICodeService.ts 基础结构
  • 实现 buildSystemPrompt() - 10个Few-shot示例集成
  • 实现 generateCode() - AI生成代码
  • 实现 parseAIResponse() - 解析AI回复
  • 实现 executeCode() - 执行Python代码
  • 实现 generateAndExecute() - 生成+执行+重试
  • 实现 getHistory() - 获取对话历史
  • 实现 saveMessages() - 保存消息到数据库
  • 添加完整错误处理和日志

交付物:

  • AICodeService.ts 完整实现(~400行
  • 单元测试通过

阶段3: AIController实现1小时 11:30-12:30

任务清单:

  • 创建 AIController.ts
  • 实现 POST /ai/generate - 生成代码
  • 实现 POST /ai/execute - 执行代码
  • 实现 POST /ai/process - 一步到位
  • 实现 GET /ai/history/:sessionId - 对话历史
  • 添加参数验证
  • 添加错误处理

交付物:

  • AIController.ts 完整实现(~200行
  • 4个API端点就绪

午休 12:30-13:30


阶段4: 路由配置15分钟 13:30-13:45

任务清单:

  • 更新 routes/index.ts
  • 注册AI相关路由
  • 测试路由可访问性

交付物:

  • AI路由注册完成
  • Swagger文档更新如有

阶段5: 测试验收1.5小时) 13:45-15:15

5.1 基础测试30分钟

测试用例:

  1. 测试1: 统一缺失值标记
  2. 测试2: 数值列清洗
  3. 测试3: 性别编码
  4. 测试4: 年龄分组

验收标准:

  • AI能正确生成代码
  • 代码可执行
  • 结果符合预期

5.2 中级测试30分钟

测试用例: 5. [ ] 测试5: BMI计算 6. [ ] 测试6: 住院天数计算 7. [ ] 测试7: 条件筛选

5.3 高级测试30分钟

测试用例: 8. [ ] 测试8: 中位数填补 9. [ ] 测试9: 多重插补MICE 10. [ ] 测试10: 智能去重

5.4 特殊测试30分钟

测试用例: 11. [ ] 自我修正测试(故意错误,验证重试机制) 12. [ ] 边界测试(列不存在、全部缺失等) 13. [ ] 并发测试(多用户同时使用) 14. [ ] 端到端测试上传→AI处理→结果验证

交付物:

  • 测试脚本 test-tool-c-day3.mjs
  • 测试报告通过率≥90%

阶段6: 文档与优化30分钟 15:15-15:45

任务清单:

  • 创建技术债务清单 Tool-C技术债务清单.md
  • 更新模块状态文档 00-工具C当前状态与开发指南.md
  • 创建Day 3开发完成总结
  • 提交Git并推送

交付物:

  • 技术债务文档
  • Day 3开发记录
  • Git提交成功

🎯 验收标准

功能验收

功能 验收标准 状态
AI代码生成 10个示例场景100%可生成正确代码 ⏸️
代码执行 生成的代码可成功执行 ⏸️
自我修正 失败后能自动重试最多3次 ⏸️
对话历史 能获取最近5轮对话 ⏸️
数据预览 执行后返回前50行预览 ⏸️

技术验收

指标 目标 状态
代码质量 无TypeScript错误 ⏸️
云原生规范 100%符合 ⏸️
错误处理 所有异常都有处理 ⏸️
日志完整性 关键操作都有日志 ⏸️
测试覆盖率 ≥80% ⏸️

性能验收

指标 目标 状态
AI生成时间 <5秒 ⏸️
代码执行时间 <3秒简单操作 ⏸️
端到端时间 <10秒 ⏸️

📦 交付清单

代码文件6个

  1. backend/prisma/schema.prisma - 新增DcToolCAiHistory模型
  2. backend/scripts/create-tool-c-ai-history-table.mjs - 建表脚本
  3. backend/src/modules/dc/tool-c/services/AICodeService.ts - 400行
  4. backend/src/modules/dc/tool-c/controllers/AIController.ts - 200行
  5. backend/src/modules/dc/tool-c/routes/index.ts - 更新
  6. backend/test-tool-c-day3.mjs - 测试脚本

文档文件4个

  1. 工具C_AI_Few-shot示例库.md - 10个示例详解
  2. 工具C_Day3开发计划.md - 本文档
  3. Tool-C技术债务清单.md - 待优化项
  4. 2025-12-06_工具C_Day3开发完成总结.md - 总结报告

🔗 相关文档


🔄 风险管理

风险1: AI生成代码质量不稳定

应对措施:

  • 使用10个Few-shot示例提升质量
  • 降低temperature至0.1
  • 实施3次重试机制
  • 添加AST静态检查Python服务

风险2: LLM调用超时

应对措施:

  • 设置合理的timeout10秒
  • 前端显示加载状态
  • 添加重试机制

风险3: Python执行失败

应对措施:

  • AI自我修正最多3次
  • 友好错误提示
  • 建议用户调整需求

📊 预期成果

Day 3完成后:

  • Tool C用户可通过自然语言清洗数据
  • AI能生成90%场景的正确代码
  • 失败场景有自动重试机制
  • 完整的对话历史管理

整体进度:

  • Day 1: Python微服务
  • Day 2: Session管理
  • Day 3: AI代码生成 ⏸️
  • Day 4-5: 前端开发
  • Day 6: 端到端测试

📝 更新记录

日期 版本 更新内容 更新人
2025-12-06 V1.0 初始创建明确9大决策和开发计划 AI Assistant

文档状态: 已确认
下一步: 开始执行开发计划预计5.5-6小时

准备开始开发! 🚀