Summary: - Update Tool C MVP Development Plan (V1.3) * Clarify Python execution as core feature * Add 15 real medical data cleaning scenarios (basic/medium/advanced) * Enhance System Prompt with 10 Few-shot examples * Discover existing Python service (extraction_service) * Update to extend existing service instead of rebuilding - Create Tool C MVP Development TODO List * 3-week plan with 30 tasks (Day 1-15) * 4 core milestones with clear acceptance criteria * Daily checklist and risk management * Detailed task breakdown for each day Key Changes: - Python service: Extend existing extraction_service instead of new setup - Test scenarios: 15 scenarios (5 basic + 5 medium + 5 advanced) - Success criteria: Basic >90%, Medium >80%, Advanced >60%, Total >80% - Development time: Reduced from 3 weeks to 2 weeks (reuse infrastructure) Status: Planning complete, ready to start Day 1 development
2062 lines
68 KiB
Markdown
2062 lines
68 KiB
Markdown
# 工具C - 科研数据编辑器 MVP开发计划
|
||
|
||
> **文档版本:** V1.0 (务实快速验证版)
|
||
> **创建日期:** 2025-12-06
|
||
> **计划周期:** 3周(15个工作日)
|
||
> **核心策略:** 用最小成本验证核心假设,快速失败优于完美计划
|
||
> **状态:** 待启动
|
||
|
||
---
|
||
|
||
## ⚠️ 开发前必读:严格遵守现有架构与规范
|
||
|
||
### 🔴 不要重复造轮子!复用平台能力
|
||
|
||
**本项目已有完整的3层架构体系和平台基础设施,所有代码必须复用现有能力:**
|
||
|
||
#### 平台基础层(✅ 已完成,直接使用)
|
||
|
||
| 服务 | 导入方式 | 用途 | 文档 |
|
||
|------|---------|------|------|
|
||
| **存储服务** | `import { storage } from '@/common/storage'` | 文件上传下载 | ✅ 必须使用 |
|
||
| **日志系统** | `import { logger } from '@/common/logging'` | 标准化日志 | ✅ 必须使用 |
|
||
| **缓存服务** | `import { cache } from '@/common/cache'` | 分布式缓存 | ✅ 必须使用 |
|
||
| **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务 | ✅ 必须使用 |
|
||
| **数据库** | `import { prisma } from '@/config/database'` | 数据库操作 | ✅ 必须使用 |
|
||
| **LLM能力** | `import { LLMFactory } from '@/common/llm'` | LLM调用 | ✅ 必须使用 |
|
||
|
||
#### 云原生开发规范(✅ 强制执行)
|
||
|
||
**详细文档**:`docs/04-开发规范/08-云原生开发规范.md`
|
||
|
||
**核心要求:**
|
||
- ✅ **文件存储**:使用`storage.upload()`,不要用`fs.writeFile()`
|
||
- ✅ **Session管理**:存数据库,不要用`Map<sessionId, data>`
|
||
- ✅ **日志输出**:使用`logger.info()`,不要用`console.log()`
|
||
- ✅ **数据库连接**:使用全局`prisma`实例,不要`new PrismaClient()`
|
||
- ✅ **LLM调用**:使用`LLMFactory.getLLM()`,不要自己集成
|
||
- ❌ **禁止本地文件存储**:Excel直接从内存解析
|
||
- ❌ **禁止内存缓存Map**:用数据库或cache服务
|
||
- ❌ **禁止硬编码配置**:使用环境变量
|
||
|
||
#### 代码文件夹结构(✅ 参考tool-b)
|
||
|
||
**后端**:
|
||
```
|
||
backend/src/modules/dc/tool-c/ ← 已存在
|
||
├── services/ ← 业务逻辑
|
||
├── controllers/ ← HTTP控制器
|
||
├── routes/ ← 路由定义
|
||
└── utils/ ← 工具函数
|
||
```
|
||
|
||
**前端**:
|
||
```
|
||
frontend-v2/src/modules/dc/pages/tool-c/ ← 已存在
|
||
```
|
||
|
||
#### 常见错误示例(❌ 严禁)
|
||
|
||
```typescript
|
||
// ❌ 错误1:自己实现Session管理
|
||
const sessions = new Map<string, any>(); // 违反规范!
|
||
|
||
// ❌ 错误2:本地文件存储
|
||
fs.writeFileSync('./uploads/file.xlsx', buffer); // 违反规范!
|
||
|
||
// ❌ 错误3:不用logger
|
||
console.log('User uploaded file'); // 违反规范!
|
||
|
||
// ❌ 错误4:新建Prisma实例
|
||
const prisma = new PrismaClient(); // 违反规范!
|
||
|
||
// ❌ 错误5:自己集成LLM
|
||
import Anthropic from '@anthropic-ai/sdk'; // 违反规范!
|
||
```
|
||
|
||
**正确示例(✅ 必须)**:参考本文档Day 1-5的代码示例
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
- [开发前必读](#开发前必读严格遵守现有架构与规范)
|
||
- [一、MVP核心目标](#一mvp核心目标)
|
||
- [二、技术架构方案(务实版)](#二技术架构方案务实版)
|
||
- [三、功能优先级矩阵](#三功能优先级矩阵)
|
||
- [四、3周详细开发计划](#四3周详细开发计划)
|
||
- [五、风险应对策略](#五风险应对策略)
|
||
- [六、验收标准](#六验收标准)
|
||
- [七、快速失败机制](#七快速失败机制)
|
||
|
||
---
|
||
|
||
## 一、MVP核心目标
|
||
|
||
### 1.1 核心假设验证
|
||
|
||
**我们需要验证的3个核心假设:**
|
||
|
||
| 假设 | 验证方式 | 成功标准 | 失败后果 |
|
||
|------|---------|---------|---------|
|
||
| **H1: AI能生成高质量Pandas代码并成功执行** | 15个真实场景测试 | 总体成功率 > 80% | MVP失败,改用代码模板库 |
|
||
| **H2: Python代码执行环境稳定可靠** | 复杂场景测试 | 高级场景成功率 > 60% | 简化为批处理模式 |
|
||
| **H3: 左表格+右AI的交互模式好用** | 用户体验测试 | 用户能独立完成任务 | 重新设计UI交互 |
|
||
| **H4: 性能可接受(含Python执行)** | 性能测试 | 端到端 < 20秒 | 优化Python执行或改批处理 |
|
||
|
||
**⚠️ 核心价值主张:**
|
||
- ✅ **AI生成代码 + 真实执行 + 表格刷新**是工具C的差异化价值
|
||
- ✅ 如果只生成代码不执行,用户还不如直接用ChatGPT
|
||
- ✅ Python执行环境是MVP的**技术核心**,不是可选项
|
||
|
||
### 1.2 MVP功能范围
|
||
|
||
**✅ MVP必须有的(P0):**
|
||
1. 文件上传(10MB限制)
|
||
2. 表格展示(AG Grid,100行预览)
|
||
3. AI对话界面(右侧侧边栏)
|
||
4. AI代码生成(DeepSeek-V3)
|
||
5. 代码执行(Python沙箱)
|
||
6. UI锁定机制(AI处理时表格只读)
|
||
7. AST安全检查
|
||
8. 表格自动刷新
|
||
9. 导出Excel
|
||
|
||
**❌ MVP明确不做的:**
|
||
- 手动编辑单元格(专注AI能力)
|
||
- 撤销/回滚(节省内存)
|
||
- Apache Arrow(先用JSON)
|
||
- Redis会话(用进程内存)
|
||
- 样式保留(直接覆盖)
|
||
- 操作审计日志
|
||
- 协同编辑
|
||
|
||
### 1.3 MVP交付物
|
||
|
||
**最终演示场景:**
|
||
```
|
||
用户上传 "lung_cancer_patients.xlsx" (5000行 × 20列)
|
||
↓
|
||
左侧显示数据表格,右侧AI助手准备就绪
|
||
↓
|
||
用户对AI说:"把年龄大于60的患者标记为老年组"
|
||
↓
|
||
AI生成代码 → 展示预操作卡片 → 用户确认 → 执行成功
|
||
↓
|
||
表格自动刷新,新增"age_group"列
|
||
↓
|
||
用户继续说:"删除所有缺失患者ID的行"
|
||
↓
|
||
AI执行 → 表格刷新,显示删除了23行
|
||
↓
|
||
用户点击"导出",下载处理后的Excel
|
||
```
|
||
|
||
**成功标准:**
|
||
- ✅ 整个流程 < 2分钟完成
|
||
- ✅ AI代码一次性执行成功
|
||
- ✅ 用户无需看文档就能操作
|
||
|
||
---
|
||
|
||
## 二、技术架构方案(务实版)
|
||
|
||
### 2.1 总体架构(简化版)- ⚠️ 重要:复用平台能力
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 前端 (React) │
|
||
│ frontend-v2/src/modules/dc/pages/tool-c/ │
|
||
│ ┌──────────────────────┐ ┌────────────────────────┐ │
|
||
│ │ AG Grid (70%) │ │ AI Chat Sidebar (30%) │ │
|
||
│ │ • 展示100行数据 │ │ • 自然语言输入 │ │
|
||
│ │ • 只读模式切换 │ │ • 预操作卡片 │ │
|
||
│ │ • 锁定遮罩 │ │ • 消息历史 │ │
|
||
│ └──────────────────────┘ └────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
↕ REST API (JSON)
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Node.js后端 (backend/src/modules/dc/tool-c/) │
|
||
│ • 文件上传(复用storage服务)✅ │
|
||
│ • 会话管理(存数据库,不用内存Map)✅ │
|
||
│ • LLM集成(复用LLMFactory)✅ │
|
||
│ • 日志记录(复用logger)✅ │
|
||
│ • 异步任务(复用jobQueue)✅ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
↕ HTTP(如需Python执行)
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Python微服务(可选,扩展文档处理引擎) │
|
||
│ • DataFrame管理(存数据库,不用内存) │
|
||
│ • exec()代码执行 │
|
||
│ • AST静态安全检查 │
|
||
│ • JSON序列化返回 │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**⚠️ 云原生架构强制要求:**
|
||
- ✅ **复用storage服务**:`import { storage } from '@/common/storage'`
|
||
- ✅ **复用logger服务**:`import { logger } from '@/common/logging'`
|
||
- ✅ **复用cache服务**:`import { cache } from '@/common/cache'`
|
||
- ✅ **复用LLMFactory**:`import { LLMFactory } from '@/common/llm'`
|
||
- ✅ **复用prisma实例**:`import { prisma } from '@/config/database'`
|
||
- ✅ **Session存数据库**:不用内存Map(违反规范)
|
||
- ❌ **禁止本地文件存储**:Excel直接从内存解析
|
||
- ❌ **禁止新建Prisma实例**:使用全局实例
|
||
|
||
**关键决策:**
|
||
- ✅ **不用Apache Arrow**:JSON够快(100行 ≈ 20KB)
|
||
- ✅ **Session存数据库**:符合云原生规范,支持多实例
|
||
- ✅ **不做撤销**:节省开发时间,用户重新上传即可
|
||
- ✅ **不保留样式**:直接生成新Excel
|
||
|
||
### 2.2 技术栈选型(✅ 复用现有技术栈)
|
||
|
||
#### 前端(frontend-v2)
|
||
```json
|
||
{
|
||
"framework": "React 19 + TypeScript 5 (已有)",
|
||
"table": "AG Grid Community (免费版)",
|
||
"ui": "Ant Design 5 (已有)",
|
||
"state": "React Query v5 (已有)",
|
||
"http": "axios (已有)",
|
||
"routing": "React Router DOM v6 (已有)"
|
||
}
|
||
```
|
||
|
||
#### Node.js后端(backend)
|
||
```json
|
||
{
|
||
"framework": "Fastify v4 (已有)",
|
||
"llm": "✅ 复用 LLMFactory (平台通用能力层)",
|
||
"storage": "✅ 复用 storage服务 (平台基础层)",
|
||
"logging": "✅ 复用 logger服务 (平台基础层)",
|
||
"cache": "✅ 复用 cache服务 (平台基础层)",
|
||
"database": "✅ 复用 prisma全局实例 (平台基础层)",
|
||
"session": "✅ PostgreSQL (dc_tool_c_sessions表)",
|
||
"excel": "xlsx 库(内存解析)",
|
||
"validation": "Joi"
|
||
}
|
||
```
|
||
|
||
#### Python微服务(⚠️ 核心功能,扩展现有服务)⭐⭐⭐⭐⭐
|
||
**决策**:✅ **系统已有Python微服务(FastAPI),需扩展代码执行功能**
|
||
|
||
**📦 现有Python服务(已完成):**
|
||
- ✅ **extraction_service**:FastAPI + PyMuPDF + Pandas + openpyxl
|
||
- ✅ **端口**:8000(已运行)
|
||
- ✅ **功能**:PDF/Docx/Txt文档提取、语言检测
|
||
- ✅ **集成**:Node.js通过ExtractionClient调用
|
||
- ✅ **依赖**:Pandas、openpyxl、chardet、langdetect(已安装)
|
||
|
||
**🔧 工具C需要的新功能:**
|
||
| 功能 | 现有服务 | 需求 | 方案 |
|
||
|------|---------|------|------|
|
||
| **Pandas代码执行** | ❌ 不支持 | ✅ 核心 | **新增API端点** `/api/dc/execute` |
|
||
| **Excel上传** | ❌ 不支持 | ✅ 需要 | 复用MultiPart上传 |
|
||
| **DataFrame管理** | ❌ 不支持 | ✅ 会话 | 新增Session管理 |
|
||
| **AST代码检查** | ❌ 不支持 | ✅ 必须 | 新增AST模块 |
|
||
| **Excel导出** | ❌ 不支持 | ✅ 需要 | 使用openpyxl(已安装) |
|
||
|
||
**⚠️ 不需要重复开发:**
|
||
- ❌ 不需要新建Python项目
|
||
- ❌ 不需要重新安装Pandas/openpyxl(已安装)
|
||
- ❌ 不需要重新配置FastAPI(已运行)
|
||
- ❌ 不需要重新写Node.js调用逻辑(ExtractionClient已存在)
|
||
|
||
**✅ MVP扩展方案:**
|
||
```python
|
||
# extraction_service/services/dc_executor.py(新增)
|
||
import pandas as pd
|
||
import ast
|
||
from typing import Dict, Any
|
||
|
||
def validate_code(code: str) -> Dict[str, Any]:
|
||
"""AST静态检查(安全验证)"""
|
||
try:
|
||
tree = ast.parse(code)
|
||
# 禁止import os、sys、subprocess等
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
if alias.name in ['os', 'sys', 'subprocess', 'socket']:
|
||
return {'valid': False, 'error': f'禁止导入{alias.name}'}
|
||
return {'valid': True}
|
||
except Exception as e:
|
||
return {'valid': False, 'error': str(e)}
|
||
|
||
def execute_pandas_code(data: list, code: str) -> Dict[str, Any]:
|
||
"""执行Pandas代码(⭐核心功能)"""
|
||
try:
|
||
# 1. 安全检查
|
||
validation = validate_code(code)
|
||
if not validation['valid']:
|
||
return {'success': False, 'error': validation['error']}
|
||
|
||
# 2. 加载数据
|
||
df = pd.DataFrame(data)
|
||
|
||
# 3. 执行代码(注入df和pd)
|
||
exec(code, {'df': df, 'pd': pd})
|
||
|
||
# 4. 返回结果(前100行)
|
||
return {
|
||
'success': True,
|
||
'data': df.head(100).to_dict('records'),
|
||
'totalRows': len(df),
|
||
'totalCols': len(df.columns),
|
||
'columns': df.columns.tolist()
|
||
}
|
||
except Exception as e:
|
||
return {'success': False, 'error': str(e)}
|
||
```
|
||
|
||
```python
|
||
# extraction_service/main.py(扩展)
|
||
from services.dc_executor import execute_pandas_code, validate_code
|
||
|
||
@app.post("/api/dc/execute")
|
||
async def execute_code(
|
||
data: List[Dict],
|
||
code: str
|
||
):
|
||
"""工具C:执行Pandas代码"""
|
||
result = execute_pandas_code(data, code)
|
||
return JSONResponse(result)
|
||
|
||
@app.post("/api/dc/validate")
|
||
async def validate_code_endpoint(code: str):
|
||
"""工具C:AST代码检查"""
|
||
result = validate_code(code)
|
||
return JSONResponse(result)
|
||
```
|
||
|
||
**✅ Node.js调用(复用ExtractionClient模式):**
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/services/PythonExecutorService.ts
|
||
import axios from 'axios';
|
||
|
||
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000';
|
||
|
||
export class PythonExecutorService {
|
||
async executeCode(data: any[], code: string) {
|
||
const response = await axios.post(`${EXTRACTION_SERVICE_URL}/api/dc/execute`, {
|
||
data,
|
||
code
|
||
});
|
||
return response.data;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 数据流设计
|
||
|
||
**会话生命周期:**
|
||
```python
|
||
# 1. 用户上传文件
|
||
POST /api/tool-c/session/init
|
||
{
|
||
file: File,
|
||
userId: string
|
||
}
|
||
|
||
# Node.js: 转发到Python
|
||
# Python:
|
||
# - 读取Excel → DataFrame
|
||
# - 存入内存:sessions[sessionId] = df
|
||
# - 返回:sessionId + 数据概览
|
||
|
||
# 2. 用户发送AI指令
|
||
POST /api/tool-c/ai/execute
|
||
{
|
||
sessionId: string,
|
||
prompt: string
|
||
}
|
||
|
||
# Node.js:
|
||
# - 构建System Prompt(包含数据上下文)
|
||
# - 调用LLM生成代码
|
||
# - 转发代码到Python
|
||
|
||
# Python:
|
||
# - AST检查
|
||
# - exec(code)修改df
|
||
# - 返回前100行JSON
|
||
|
||
# 3. 用户导出
|
||
GET /api/tool-c/session/export/{sessionId}
|
||
|
||
# Python:
|
||
# - df.to_excel(buffer)
|
||
# - 返回二进制流
|
||
```
|
||
|
||
---
|
||
|
||
## 三、功能优先级矩阵
|
||
|
||
### 3.1 P0级:必须开发(核心闭环)⭐⭐⭐⭐⭐
|
||
|
||
| ID | 功能 | 描述 | 验证目标 | 工时 | 负责人 |
|
||
|----|------|------|---------|------|--------|
|
||
| **P0-001** | 文件上传 | 前端Upload组件 + 10MB限制 | 能否解析Excel | 0.5天 | 前端 |
|
||
| **P0-002** | Session初始化 | 后端接收文件,Python加载DataFrame | 中文列名、GBK编码 | 1天 | 后端 |
|
||
| **P0-003** | 表格展示 | AG Grid展示100行数据 | 列类型识别、空值高亮 | 1天 | 前端 |
|
||
| **P0-004** | AI对话UI | 右侧侧边栏,消息列表 | 聊天交互流畅 | 0.5天 | 前端 |
|
||
| **P0-005** | System Prompt构建 | 包含数据上下文的Prompt | AI能理解数据结构 | 1天 | 后端 |
|
||
| **P0-006** | AI代码生成 | 调用DeepSeek-V3生成Pandas代码 | **成功率>80%** | 2天 | 后端 |
|
||
| **P0-007** | AST安全检查 | 拦截危险代码 | `import os`被拦截 | 1天 | Python |
|
||
| **P0-008** | 代码执行 | exec()在沙箱中运行代码 | 修改DataFrame成功 | 1天 | Python |
|
||
| **P0-009** | 预操作卡片 | 展示代码,用户确认后执行 | 用户能看懂代码意图 | 0.5天 | 前端 |
|
||
| **P0-010** | 表格刷新 | 执行后自动拉取新数据 | 前端实时更新 | 0.5天 | 前端 |
|
||
| **P0-011** | UI锁定机制 | AI处理时表格变灰+遮罩 | 物理禁止并发操作 | 0.5天 | 前端 |
|
||
| **P0-012** | 导出Excel | 下载处理后的文件 | 文件完整性 | 1天 | Python |
|
||
|
||
**P0小计:11.5天**
|
||
|
||
### 3.2 P1级:必须验证,可简化实现 ⭐⭐⭐⭐
|
||
|
||
| ID | 功能 | MVP简化方案 | 完整版 | 工时 |
|
||
|----|------|------------|--------|------|
|
||
| **P1-001** | 会话管理 | 进程内存Map(单实例) | Redis分布式 | 0.5天 |
|
||
| **P1-002** | 心跳保活 | 固定10分钟过期,不续期 | 前端心跳续期 | 0.5天 |
|
||
| **P1-003** | 编码检测 | chardet自动检测+友好报错 | 自动转换 | 1天 |
|
||
| **P1-004** | AI自我修复 | 失败后重试1次 | 多次重试+学习 | 1天 |
|
||
| **P1-005** | 快捷模板 | 3个常用模板(年龄分组等) | 10+模板库 | 0.5天 |
|
||
|
||
**P1小计:3.5天**
|
||
|
||
### 3.3 P2级:延后或不做 ⭐⭐
|
||
|
||
| 功能 | 延后原因 | 何时做 |
|
||
|------|---------|--------|
|
||
| 手动编辑单元格 | MVP专注AI | P2阶段 |
|
||
| 撤销/回滚 | 节省内存 | P2阶段 |
|
||
| Apache Arrow | JSON够用 | 性能不达标时 |
|
||
| Redis分布式 | 单实例够用 | 横向扩展时 |
|
||
| 样式保留 | 复杂度高 | P3阶段 |
|
||
| 操作审计 | 非核心 | P3阶段 |
|
||
| 多Sheet支持 | 简化逻辑 | P3阶段 |
|
||
|
||
---
|
||
|
||
## 四、3周详细开发计划
|
||
|
||
### Week 1:基础架构搭建(5天)
|
||
|
||
#### Day 1:环境搭建 + 代码结构(⚠️ 参考tool-b结构 + Python环境)
|
||
|
||
**任务清单:**
|
||
- [ ] 创建项目目录结构(**参考tool-b**)
|
||
```
|
||
backend/src/modules/dc/tool-c/ ← 主目录
|
||
├── services/ ← 业务逻辑层
|
||
│ ├── SessionService.ts ← Session管理(存数据库)
|
||
│ ├── AICodeService.ts ← AI代码生成
|
||
│ ├── PythonExecutorService.ts ← ⭐ Python代码执行(核心)
|
||
│ └── DataProcessService.ts ← 数据处理逻辑
|
||
├── controllers/ ← 控制器层
|
||
│ └── ToolCController.ts ← HTTP请求处理
|
||
├── routes/ ← 路由层
|
||
│ └── index.ts ← 路由定义
|
||
└── utils/ ← 工具函数
|
||
├── codeValidator.ts ← AST代码检查
|
||
└── pythonScripts/ ← ⭐ Python执行脚本
|
||
└── executor.py ← Pandas代码执行器
|
||
|
||
frontend-v2/src/modules/dc/pages/tool-c/ ← 前端(已存在)
|
||
```
|
||
|
||
- [ ] **⭐ 扩展现有Python服务(核心功能)**
|
||
```bash
|
||
# 系统已有Python微服务(extraction_service),只需扩展功能
|
||
cd extraction_service
|
||
|
||
# 1. 创建DC执行器模块
|
||
cat > services/dc_executor.py << 'EOF'
|
||
import pandas as pd
|
||
import ast
|
||
from typing import Dict, Any, List
|
||
|
||
def validate_code(code: str) -> Dict[str, Any]:
|
||
"""AST静态检查(安全验证)"""
|
||
try:
|
||
tree = ast.parse(code)
|
||
# 禁止危险导入
|
||
forbidden_modules = ['os', 'sys', 'subprocess', 'socket', 'shutil', 'requests']
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
if alias.name in forbidden_modules:
|
||
return {'valid': False, 'error': f'禁止导入{alias.name}模块'}
|
||
elif isinstance(node, ast.ImportFrom):
|
||
if node.module in forbidden_modules:
|
||
return {'valid': False, 'error': f'禁止导入{node.module}模块'}
|
||
return {'valid': True}
|
||
except Exception as e:
|
||
return {'valid': False, 'error': f'代码语法错误: {str(e)}'}
|
||
|
||
def execute_pandas_code(data: List[Dict], code: str) -> Dict[str, Any]:
|
||
"""执行Pandas代码(⭐核心功能)"""
|
||
try:
|
||
# 1. 安全检查
|
||
validation = validate_code(code)
|
||
if not validation['valid']:
|
||
return {'success': False, 'error': validation['error']}
|
||
|
||
# 2. 加载数据到DataFrame
|
||
df = pd.DataFrame(data)
|
||
|
||
# 3. 执行代码(注入df和pd,隔离环境)
|
||
local_env = {'df': df, 'pd': pd}
|
||
exec(code, {'__builtins__': {}}, local_env)
|
||
|
||
# 4. 获取执行后的df
|
||
df_result = local_env.get('df', df)
|
||
|
||
# 5. 返回结果(前100行,避免数据量过大)
|
||
return {
|
||
'success': True,
|
||
'data': df_result.head(100).to_dict('records'),
|
||
'totalRows': len(df_result),
|
||
'totalCols': len(df_result.columns),
|
||
'columns': df_result.columns.tolist()
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': f'代码执行失败: {str(e)}'
|
||
}
|
||
EOF
|
||
|
||
# 2. 扩展main.py,添加DC端点
|
||
# 在main.py末尾添加:
|
||
cat >> main.py << 'EOF'
|
||
|
||
# ==================== DC工具C端点 ====================
|
||
from services.dc_executor import execute_pandas_code, validate_code
|
||
from pydantic import BaseModel
|
||
|
||
class ExecuteRequest(BaseModel):
|
||
data: List[Dict]
|
||
code: str
|
||
|
||
class ValidateRequest(BaseModel):
|
||
code: str
|
||
|
||
@app.post("/api/dc/execute")
|
||
async def dc_execute_code(request: ExecuteRequest):
|
||
"""工具C:执行Pandas代码"""
|
||
logger.info(f"DC Execute: code length={len(request.code)}, data rows={len(request.data)}")
|
||
result = execute_pandas_code(request.data, request.code)
|
||
return JSONResponse(result)
|
||
|
||
@app.post("/api/dc/validate")
|
||
async def dc_validate_code(request: ValidateRequest):
|
||
"""工具C:AST代码检查"""
|
||
logger.info(f"DC Validate: code length={len(request.code)}")
|
||
result = validate_code(request.code)
|
||
return JSONResponse(result)
|
||
EOF
|
||
```
|
||
|
||
- [ ] **Node.js调用Python服务(PythonExecutorService)**
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/services/PythonExecutorService.ts
|
||
import axios from 'axios';
|
||
import { logger } from '@/common/logging';
|
||
|
||
const EXTRACTION_SERVICE_URL = process.env.EXTRACTION_SERVICE_URL || 'http://localhost:8000';
|
||
|
||
export interface ExecuteResult {
|
||
success: boolean;
|
||
data?: any[];
|
||
totalRows?: number;
|
||
totalCols?: number;
|
||
columns?: string[];
|
||
error?: string;
|
||
}
|
||
|
||
export class PythonExecutorService {
|
||
private baseUrl: string;
|
||
|
||
constructor() {
|
||
this.baseUrl = EXTRACTION_SERVICE_URL;
|
||
}
|
||
|
||
/**
|
||
* 执行Pandas代码(⭐ 核心功能)
|
||
* 复用现有Python微服务,添加新端点
|
||
*/
|
||
async executeCode(data: any[], code: string): Promise<ExecuteResult> {
|
||
try {
|
||
logger.info('Calling Python executor', {
|
||
dataRows: data.length,
|
||
codeLength: code.length
|
||
});
|
||
|
||
const response = await axios.post<ExecuteResult>(
|
||
`${this.baseUrl}/api/dc/execute`,
|
||
{ data, code },
|
||
{ timeout: 30000 } // 30秒超时
|
||
);
|
||
|
||
logger.info('Python execution success', {
|
||
totalRows: response.data.totalRows
|
||
});
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
logger.error('Python execution failed', { error });
|
||
|
||
if (axios.isAxiosError(error) && error.response) {
|
||
throw new Error(error.response.data.error || 'Python execution failed');
|
||
}
|
||
|
||
throw new Error('无法连接到Python执行服务');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AST代码检查(执行前验证)
|
||
*/
|
||
async validateCode(code: string): Promise<{ valid: boolean; error?: string }> {
|
||
try {
|
||
const response = await axios.post(
|
||
`${this.baseUrl}/api/dc/validate`,
|
||
{ code },
|
||
{ timeout: 5000 }
|
||
);
|
||
|
||
return response.data;
|
||
} catch (error) {
|
||
logger.error('Code validation failed', { error });
|
||
throw new Error('代码验证失败');
|
||
}
|
||
}
|
||
}
|
||
|
||
export const pythonExecutorService = new PythonExecutorService();
|
||
```
|
||
|
||
- [ ] **强制检查**:确保复用平台服务
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/services/SessionService.ts
|
||
|
||
// ✅ 必须导入这些平台服务
|
||
import { storage } from '@/common/storage';
|
||
import { logger } from '@/common/logging';
|
||
import { cache } from '@/common/cache';
|
||
import { prisma } from '@/config/database';
|
||
|
||
// ❌ 禁止自己实现
|
||
// const sessions = new Map() ← 违反规范!
|
||
```
|
||
|
||
- [ ] 数据库Schema设计(dc_schema)
|
||
```prisma
|
||
// prisma/schema.prisma(添加到dc_schema)
|
||
model DcToolCSession {
|
||
id String @id @default(uuid())
|
||
userId String
|
||
sessionId String @unique
|
||
fileName String
|
||
dataSnapshot Json // 存储100行预览数据
|
||
totalRows Int
|
||
totalCols Int
|
||
columns Json // 列信息
|
||
expiresAt DateTime // 10分钟过期
|
||
createdAt DateTime @default(now())
|
||
|
||
@@schema("dc_schema")
|
||
@@map("dc_tool_c_sessions")
|
||
}
|
||
```
|
||
|
||
**验收标准:**
|
||
- ✅ 文件夹结构与tool-b一致
|
||
- ✅ 导入了所有必须的平台服务
|
||
- ✅ 没有内存Map、没有本地文件存储
|
||
|
||
**负责人:** 后端开发
|
||
|
||
---
|
||
|
||
#### Day 2:Session管理 + 数据加载(⚠️ 存数据库,不用内存)
|
||
|
||
**任务清单:**
|
||
- [ ] 实现SessionService(**存数据库,符合云原生规范**)
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/services/SessionService.ts
|
||
import { storage } from '@/common/storage';
|
||
import { logger } from '@/common/logging';
|
||
import { prisma } from '@/config/database';
|
||
import * as xlsx from 'xlsx';
|
||
import chardet from 'chardet';
|
||
|
||
export class SessionService {
|
||
/**
|
||
* 创建Session(✅ 存数据库,不用内存Map)
|
||
*/
|
||
async createSession(userId: string, fileBuffer: Buffer, fileName: string): Promise<string> {
|
||
// 1. 检测编码
|
||
const detected = chardet.detect(fileBuffer);
|
||
if (detected.encoding.toLowerCase() !== 'utf-8') {
|
||
throw new Error(`文件编码为${detected.encoding},请转换为UTF-8`);
|
||
}
|
||
|
||
// 2. 内存解析Excel(✅ 云原生:不落盘)
|
||
const workbook = xlsx.read(fileBuffer, { type: 'buffer' });
|
||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||
const data = xlsx.utils.sheet_to_json(sheet);
|
||
|
||
// 3. 提取列信息
|
||
const columns = Object.keys(data[0] || {}).map(col => ({
|
||
name: col,
|
||
type: typeof data[0][col]
|
||
}));
|
||
|
||
// 4. Session存数据库(✅ 符合云原生规范)
|
||
const sessionId = `session_${Date.now()}_${Math.random().toString(36)}`;
|
||
await prisma.dcToolCSession.create({
|
||
data: {
|
||
sessionId,
|
||
userId,
|
||
fileName,
|
||
dataSnapshot: data.slice(0, 100), // 只存前100行预览
|
||
totalRows: data.length,
|
||
totalCols: columns.length,
|
||
columns,
|
||
expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10分钟过期
|
||
}
|
||
});
|
||
|
||
// 5. 完整数据上传到OSS(✅ 云原生:持久化存储)
|
||
const dataKey = `dc/tool-c/${sessionId}/full-data.json`;
|
||
await storage.uploadBuffer(dataKey, Buffer.from(JSON.stringify(data)));
|
||
|
||
logger.info('Session created', { sessionId, totalRows: data.length });
|
||
|
||
return sessionId;
|
||
}
|
||
|
||
/**
|
||
* 获取Session(从数据库读取)
|
||
*/
|
||
async getSession(sessionId: string) {
|
||
const session = await prisma.dcToolCSession.findUnique({
|
||
where: { sessionId }
|
||
});
|
||
|
||
if (!session || new Date() > session.expiresAt) {
|
||
throw new Error('Session已过期');
|
||
}
|
||
|
||
return session;
|
||
}
|
||
}
|
||
```
|
||
|
||
**测试用例:**
|
||
- [ ] 上传UTF-8编码的Excel,正常解析
|
||
- [ ] 上传GBK编码的Excel,被拦截并提示
|
||
- [ ] 上传中文列名的Excel,无乱码
|
||
- [ ] Session存入数据库,可查询
|
||
- [ ] 10分钟后Session自动过期
|
||
|
||
**验收标准:**
|
||
- ✅ Session存储在数据库(不是内存Map)
|
||
- ✅ 完整数据存储在OSS
|
||
- ✅ 符合云原生开发规范
|
||
|
||
**负责人:** Node.js后端
|
||
|
||
---
|
||
|
||
#### Day 3:Node.js BFF + 文件上传
|
||
|
||
**任务清单:**
|
||
- [ ] 创建Fastify路由
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/routes/index.ts
|
||
import { FastifyInstance } from 'fastify';
|
||
|
||
export async function registerToolCRoutes(fastify: FastifyInstance) {
|
||
// 文件上传
|
||
fastify.post('/api/v1/dc/tool-c/session/init', async (req, reply) => {
|
||
const file = await req.file();
|
||
|
||
// 1. 文件大小检查
|
||
if (file.file.bytesRead > 10 * 1024 * 1024) {
|
||
return reply.code(413).send({
|
||
success: false,
|
||
error: '文件过大,请压缩后重试(限10MB)'
|
||
});
|
||
}
|
||
|
||
// 2. 转发到Python服务
|
||
const formData = new FormData();
|
||
formData.append('file', file.file, file.filename);
|
||
|
||
const response = await axios.post('http://localhost:8001/api/python/init', formData);
|
||
|
||
return {
|
||
success: true,
|
||
sessionId: response.data.sessionId,
|
||
data: response.data
|
||
};
|
||
});
|
||
}
|
||
```
|
||
- [ ] 集成到主应用
|
||
```typescript
|
||
// backend/src/modules/dc/index.ts
|
||
import { registerToolCRoutes } from './tool-c/routes';
|
||
|
||
export async function registerDCRoutes(fastify: FastifyInstance) {
|
||
await registerToolBRoutes(fastify);
|
||
await registerToolCRoutes(fastify); // 新增
|
||
}
|
||
```
|
||
|
||
**测试用例:**
|
||
- [ ] 上传9MB文件,成功
|
||
- [ ] 上传11MB文件,被拦截
|
||
- [ ] 并发上传2个文件,sessionId不冲突
|
||
|
||
**验收标准:**
|
||
- ✅ Node.js能正确转发文件到Python
|
||
- ✅ 文件大小限制生效
|
||
|
||
**负责人:** Node.js后端
|
||
|
||
---
|
||
|
||
#### Day 4:前端框架搭建
|
||
|
||
**任务清单:**
|
||
- [ ] 创建Tool C页面
|
||
```typescript
|
||
// frontend-v2/src/modules/dc/pages/tool-c/index.tsx
|
||
import { AgGridReact } from 'ag-grid-react';
|
||
import { Upload, Spin } from 'antd';
|
||
|
||
export default function ToolCEditor() {
|
||
const [sessionId, setSessionId] = useState<string>();
|
||
const [data, setData] = useState<any[]>([]);
|
||
const [columns, setColumns] = useState<any[]>([]);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
const handleUpload = async (file: File) => {
|
||
setIsLoading(true);
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
const response = await api.post('/api/v1/dc/tool-c/session/init', formData);
|
||
|
||
setSessionId(response.data.sessionId);
|
||
setData(response.data.data.preview);
|
||
setColumns(response.data.data.columns);
|
||
setIsLoading(false);
|
||
};
|
||
|
||
return (
|
||
<div className="h-screen flex">
|
||
{/* 左侧:表格区域 */}
|
||
<div className="flex-1">
|
||
{!sessionId ? (
|
||
<Upload beforeUpload={handleUpload}>
|
||
<Button>上传Excel文件(限10MB)</Button>
|
||
</Upload>
|
||
) : (
|
||
<AgGridReact
|
||
rowData={data}
|
||
columnDefs={columns.map(col => ({
|
||
field: col.name,
|
||
headerName: col.name
|
||
}))}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* 右侧:AI侧边栏(占位) */}
|
||
<div className="w-96 border-l bg-white">
|
||
<h3>AI助手</h3>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
- [ ] 集成AG Grid
|
||
```bash
|
||
cd frontend-v2
|
||
npm install ag-grid-react ag-grid-community
|
||
```
|
||
- [ ] 路由配置
|
||
```typescript
|
||
// frontend-v2/src/modules/dc/routes.tsx
|
||
{
|
||
path: 'tool-c',
|
||
element: <ToolCEditor />
|
||
}
|
||
```
|
||
|
||
**测试用例:**
|
||
- [ ] 上传文件,表格正确显示
|
||
- [ ] 中文列名显示正常
|
||
- [ ] 空值单元格有视觉提示
|
||
|
||
**验收标准:**
|
||
- ✅ 用户能看到左右分栏界面
|
||
- ✅ 表格能展示100行数据
|
||
|
||
**负责人:** 前端开发
|
||
|
||
---
|
||
|
||
#### Day 5:System Prompt构建
|
||
|
||
**任务清单:**
|
||
- [ ] 创建Prompt模板
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/prompts/system-prompt.ts
|
||
export function buildSystemPrompt(dataContext: DataContext): string {
|
||
return `# AI医疗数据清洗助手 - 系统角色
|
||
|
||
## 当前数据结构
|
||
- 总行数:${dataContext.totalRows} 行
|
||
- 总列数:${dataContext.totalCols} 列
|
||
- 列名:${dataContext.columns.map(c => c.name).join(', ')}
|
||
|
||
### 前5行数据示例
|
||
${JSON.stringify(dataContext.headData, null, 2)}
|
||
|
||
## 严格规则
|
||
1. 只能使用pandas操作(已预导入为pd)
|
||
2. 变量名必须是df(不要用其他名字)
|
||
3. 就地修改:df['new'] = ... 或 df.drop(...)
|
||
4. 不要print()、display()等输出
|
||
5. 禁止import os、sys、requests等
|
||
|
||
## Few-shot示例(分层难度:基础→中等→高级)
|
||
|
||
### 🟢 基础场景(单步骤操作)
|
||
#### 场景1:年龄分组
|
||
用户:"把年龄大于60的标记为老年组"
|
||
代码:
|
||
df['age_group'] = df['age'].apply(lambda x: '老年组' if pd.notna(x) and x > 60 else '非老年组')
|
||
|
||
#### 场景2:删除缺失
|
||
用户:"删除没有患者ID的行"
|
||
代码:
|
||
df.dropna(subset=['patient_id'], inplace=True)
|
||
|
||
#### 场景3:性别编码
|
||
用户:"把性别转为数字"
|
||
代码:
|
||
df['gender_code'] = df['gender'].map({'男': 1, '女': 0})
|
||
|
||
### 🟡 中等场景(多步骤或跨列逻辑)
|
||
#### 场景4:计算NLR并分组
|
||
用户:"计算中性粒细胞淋巴细胞比值NLR,并按2.5分为高低两组"
|
||
代码:
|
||
df['NLR'] = df.apply(lambda row: row['neutrophil'] / row['lymphocyte'] if pd.notna(row['neutrophil']) and pd.notna(row['lymphocyte']) and row['lymphocyte'] > 0 else None, axis=1)
|
||
df['NLR_group'] = df['NLR'].apply(lambda x: 'High' if pd.notna(x) and x > 2.5 else 'Low')
|
||
|
||
#### 场景5:字符串拆分(血压)
|
||
用户:"把血压列的'120/80'格式拆分成收缩压和舒张压,并判断是否高血压"
|
||
代码:
|
||
df[['systolic_bp', 'diastolic_bp']] = df['blood_pressure'].str.split('/', expand=True)
|
||
df['systolic_bp'] = pd.to_numeric(df['systolic_bp'], errors='coerce')
|
||
df['diastolic_bp'] = pd.to_numeric(df['diastolic_bp'], errors='coerce')
|
||
df['is_hypertension'] = ((df['systolic_bp'] > 140) | (df['diastolic_bp'] > 90)).astype(int)
|
||
|
||
#### 场景6:时间差计算(逻辑验证)
|
||
用户:"计算住院天数,如果出院日期早于入院日期则标记为异常"
|
||
代码:
|
||
df['admission_date'] = pd.to_datetime(df['admission_date'], errors='coerce')
|
||
df['discharge_date'] = pd.to_datetime(df['discharge_date'], errors='coerce')
|
||
df['hospital_days'] = (df['discharge_date'] - df['admission_date']).dt.days
|
||
df['date_error'] = df['hospital_days'] < 0
|
||
df.loc[df['date_error'], 'hospital_days'] = None
|
||
|
||
### 🔴 高级场景(分组聚合、时间序列、医学规则)
|
||
#### 场景7:生存时间计算(复杂条件逻辑)
|
||
用户:"生成生存状态和生存时间,如果死亡日期存在则状态为1,时间为死亡日期减诊断日期,否则状态为0,时间为随访截止日期减诊断日期"
|
||
代码:
|
||
df['diagnosis_date'] = pd.to_datetime(df['diagnosis_date'], errors='coerce')
|
||
df['death_date'] = pd.to_datetime(df['death_date'], errors='coerce')
|
||
df['followup_end_date'] = pd.to_datetime(df['followup_end_date'], errors='coerce')
|
||
df['vital_status'] = df['death_date'].notna().astype(int)
|
||
df['survival_days'] = df.apply(lambda row: (row['death_date'] - row['diagnosis_date']).days if pd.notna(row['death_date']) else (row['followup_end_date'] - row['diagnosis_date']).days, axis=1)
|
||
df['survival_months'] = (df['survival_days'] / 30.44).round(1)
|
||
|
||
#### 场景8:分组聚合(首末记录)
|
||
用户:"对每个患者找出第一次化疗日期和最后一次化疗日期,计算持续时间"
|
||
代码:
|
||
df['chemo_date'] = pd.to_datetime(df['chemo_date'], errors='coerce')
|
||
patient_chemo = df.groupby('patient_id')['chemo_date'].agg(['min', 'max']).reset_index()
|
||
patient_chemo.columns = ['patient_id', 'first_chemo', 'last_chemo']
|
||
patient_chemo['chemo_duration_days'] = (patient_chemo['last_chemo'] - patient_chemo['first_chemo']).dt.days
|
||
df = df.merge(patient_chemo[['patient_id', 'first_chemo', 'last_chemo', 'chemo_duration_days']], on='patient_id', how='left')
|
||
|
||
#### 场景9:时间序列变化率
|
||
用户:"按患者ID分组,计算每次随访相比上次的肿瘤大小变化率"
|
||
代码:
|
||
df = df.sort_values(['patient_id', 'followup_date'])
|
||
df['prev_tumor_size'] = df.groupby('patient_id')['tumor_size'].shift(1)
|
||
df['tumor_change_rate'] = ((df['tumor_size'] - df['prev_tumor_size']) / df['prev_tumor_size'] * 100).round(2)
|
||
df['tumor_change_rate'] = df['tumor_change_rate'].fillna(0)
|
||
|
||
#### 场景10:医学规则引擎(肝功能分级)
|
||
用户:"根据ALT、AST、ALP、TBIL判断肝功能分级"
|
||
代码:
|
||
def classify_liver_function(row):
|
||
abnormal_count = 0
|
||
if pd.notna(row.get('ALT')) and row['ALT'] > 40: abnormal_count += 1
|
||
if pd.notna(row.get('AST')) and row['AST'] > 40: abnormal_count += 1
|
||
if pd.notna(row.get('ALP')) and row['ALP'] > 125: abnormal_count += 1
|
||
if pd.notna(row.get('TBIL')) and row['TBIL'] > 20: abnormal_count += 1
|
||
if abnormal_count == 0: return '正常'
|
||
elif abnormal_count == 1: return '轻度异常'
|
||
elif abnormal_count == 2: return '中度异常'
|
||
else: return '重度异常'
|
||
df['liver_function_grade'] = df.apply(classify_liver_function, axis=1)
|
||
|
||
现在,请准备好接收用户的指令。记住:代码要安全、可靠、易懂,能处理从基础到高级的复杂医疗场景。
|
||
`;
|
||
}
|
||
```
|
||
- [ ] 集成LLMFactory(✅ 复用平台通用能力层)
|
||
```typescript
|
||
// backend/src/modules/dc/tool-c/services/AICodeService.ts
|
||
import { LLMFactory } from '@/common/llm'; // ✅ 复用平台LLM能力
|
||
import { logger } from '@/common/logging'; // ✅ 复用日志
|
||
|
||
export class AICodeService {
|
||
/**
|
||
* 生成Pandas代码(✅ 复用LLMFactory)
|
||
*/
|
||
async generateCode(prompt: string, dataContext: DataContext): Promise<string> {
|
||
logger.info('Generating Pandas code', { prompt, dataContext });
|
||
|
||
// ✅ 复用平台LLM服务
|
||
const llm = LLMFactory.getLLM('deepseek-v3');
|
||
|
||
const systemPrompt = buildSystemPrompt(dataContext);
|
||
|
||
const response = await llm.chat([
|
||
{ role: 'system', content: systemPrompt },
|
||
{ role: 'user', content: prompt }
|
||
]);
|
||
|
||
// 提取纯代码(去除Markdown格式)
|
||
const code = this.extractCode(response.content);
|
||
|
||
logger.info('Code generated successfully', { codeLength: code.length });
|
||
|
||
return code;
|
||
}
|
||
|
||
/**
|
||
* AI自我修复(失败后重试1次)
|
||
*/
|
||
async fixCode(originalCode: string, errorMsg: string, dataContext: DataContext): Promise<string> {
|
||
logger.warn('Code execution failed, attempting self-repair', { errorMsg });
|
||
|
||
const llm = LLMFactory.getLLM('deepseek-v3');
|
||
|
||
const fixPrompt = `以下代码执行失败,请修复:
|
||
|
||
错误信息:
|
||
${errorMsg}
|
||
|
||
原始代码:
|
||
${originalCode}
|
||
|
||
数据结构:
|
||
${JSON.stringify(dataContext)}
|
||
|
||
请生成修复后的代码(不要有Markdown格式):`;
|
||
|
||
const response = await llm.chat([{ role: 'user', content: fixPrompt }]);
|
||
|
||
return this.extractCode(response.content);
|
||
}
|
||
|
||
private extractCode(text: string): string {
|
||
// 去除```python```或```标记
|
||
const match = text.match(/```(?:python)?\n([\s\S]*?)\n```/);
|
||
return match ? match[1] : text;
|
||
}
|
||
}
|
||
```
|
||
|
||
**测试用例:**
|
||
- [ ] 输入"年龄分组",生成正确代码
|
||
- [ ] 输入"删除空行",生成正确代码
|
||
- [ ] 检查代码中是否包含危险语句
|
||
|
||
**验收标准:**
|
||
- ✅ AI能生成可执行的Pandas代码
|
||
- ✅ 代码符合规范(无print、无import)
|
||
|
||
**负责人:** Node.js后端
|
||
|
||
---
|
||
|
||
### Week 2:核心功能开发(5天)
|
||
|
||
#### Day 6-7:AI代码生成 + AST检查
|
||
|
||
**Day 6任务:**
|
||
- [ ] 实现AST静态检查
|
||
```python
|
||
# python-service/code_validator.py
|
||
import ast
|
||
|
||
DANGEROUS_IMPORTS = {'os', 'sys', 'subprocess', 'requests', 'urllib'}
|
||
DANGEROUS_BUILTINS = {'eval', 'exec', 'compile', 'open', '__import__'}
|
||
|
||
def validate_code_safety(code: str) -> Tuple[bool, str]:
|
||
try:
|
||
tree = ast.parse(code)
|
||
|
||
for node in ast.walk(tree):
|
||
# 检查import
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
if alias.name in DANGEROUS_IMPORTS:
|
||
return False, f"禁止导入:{alias.name}"
|
||
|
||
# 检查函数调用
|
||
if isinstance(node, ast.Call):
|
||
if isinstance(node.func, ast.Name):
|
||
if node.func.id in DANGEROUS_BUILTINS:
|
||
return False, f"禁止使用:{node.func.id}"
|
||
|
||
return True, "验证通过"
|
||
except SyntaxError as e:
|
||
return False, f"语法错误:{str(e)}"
|
||
```
|
||
|
||
**Day 7任务:**
|
||
- [ ] 实现AI代码生成API
|
||
```typescript
|
||
// Node.js
|
||
fastify.post('/api/v1/dc/tool-c/ai/generate', async (req, reply) => {
|
||
const { sessionId, prompt } = req.body;
|
||
|
||
// 1. 获取数据上下文
|
||
const context = await pythonService.getDataContext(sessionId);
|
||
|
||
// 2. 调用AI生成代码
|
||
const code = await aiService.generateCode(prompt, context);
|
||
|
||
return {
|
||
success: true,
|
||
code,
|
||
summary: `将执行以下操作:${extractSummary(code)}`
|
||
};
|
||
});
|
||
```
|
||
|
||
**测试场景(15个真实医疗数据清洗场景):**
|
||
|
||
**🟢 基础场景(5个)- 单步骤操作:**
|
||
1. [ ] "把年龄大于60的标记为老年组" - 简单条件判断
|
||
2. [ ] "删除所有患者ID为空的行" - 数据完整性清洗
|
||
3. [ ] "把性别转为数字,男1女0" - 分类变量编码
|
||
4. [ ] "计算BMI = 体重 / (身高/100)^2" - 简单公式计算
|
||
5. [ ] "删除缺失率超过50%的列" - 列级数据质量控制
|
||
|
||
**🟡 中等场景(5个)- 多步骤或跨列逻辑:**
|
||
6. [ ] "把诊断日期和出院日期计算天数差,如果出院日期早于诊断日期则标记为异常" - 逻辑验证
|
||
7. [ ] "根据白细胞、中性粒细胞、淋巴细胞三个指标,计算NLR(中性粒细胞/淋巴细胞),并按2.5分为高低两组" - 多步骤计算
|
||
8. [ ] "从病理报告列中提取TNM分期,生成新列,如果没有提取到则标记为'未分期'" - 文本提取(可用正则)
|
||
9. [ ] "把血压列中的'120/80'格式拆分成收缩压和舒张压两列,并判断是否高血压(收缩压>140 or 舒张压>90)" - 字符串处理+逻辑判断
|
||
10. [ ] "删除重复的患者ID,保留最新的一条记录(根据就诊日期)" - 去重+排序
|
||
|
||
**🔴 高级场景(5个)- 复杂分组、时间序列、医学规则:**
|
||
11. [ ] "对于每个患者,找出第一次化疗日期和最后一次化疗日期,计算化疗持续时间" - 分组聚合
|
||
12. [ ] "生成生存状态变量:如果死亡日期存在则为1,否则为0;生成生存时间:如果死亡则为(死亡日期-诊断日期),否则为(随访截止日期-诊断日期)" - 复杂条件逻辑
|
||
13. [ ] "根据多个实验室指标(ALT、AST、ALP、TBIL)判断肝功能分级(正常、轻度异常、中度异常、重度异常)" - 医学规则引擎
|
||
14. [ ] "按患者ID分组,对每个患者的多次随访记录,计算相邻两次之间的指标变化率(如肿瘤大小变化率)" - 时间序列分析
|
||
15. [ ] "根据入院时间,计算患者的季节变量(春夏秋冬),然后统计不同季节的发病人数" - 时间特征提取+统计分析
|
||
|
||
**验收标准(分层要求):**
|
||
- ✅ **基础场景成功率 > 90%**(5/5或4/5成功)
|
||
- ✅ **中等场景成功率 > 80%**(4/5成功)
|
||
- ✅ **高级场景成功率 > 60%**(3/5成功)
|
||
- ✅ **总体成功率 > 80%**(12/15场景成功)
|
||
- ✅ AST能拦截`import os`等危险代码
|
||
- ❌ 如果总体成功率 < 60%(9/15失败),**MVP失败,需要Pivot到模板库模式**
|
||
|
||
**负责人:** Node.js后端 + Python开发
|
||
|
||
---
|
||
|
||
#### Day 8:代码执行 + 表格刷新
|
||
|
||
**任务清单:**
|
||
- [ ] 实现代码执行API
|
||
```python
|
||
# Python
|
||
@app.post("/api/python/execute")
|
||
async def execute_code(request: ExecuteRequest):
|
||
# 1. 获取Session
|
||
df = session_manager.get(request.sessionId)
|
||
|
||
# 2. AST检查
|
||
is_safe, error_msg = validate_code_safety(request.code)
|
||
if not is_safe:
|
||
return {"success": False, "error": error_msg}
|
||
|
||
# 3. 执行代码
|
||
try:
|
||
exec(request.code, {'df': df, 'pd': pd, 'np': np})
|
||
|
||
# 4. 返回预览数据
|
||
preview = df.head(100).to_dict('records')
|
||
|
||
return {
|
||
"success": True,
|
||
"preview": preview,
|
||
"stats": {
|
||
"totalRows": len(df),
|
||
"totalCols": len(df.columns)
|
||
}
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"error": str(e),
|
||
"traceback": traceback.format_exc()
|
||
}
|
||
```
|
||
- [ ] 前端执行流程
|
||
```typescript
|
||
async function executeAICode(code: string) {
|
||
// 1. 展示预操作卡片
|
||
const confirmed = await showActionCard(code);
|
||
if (!confirmed) return;
|
||
|
||
// 2. 锁定表格
|
||
setIsLocked(true);
|
||
|
||
try {
|
||
// 3. 执行代码
|
||
const response = await api.post('/api/v1/dc/tool-c/ai/execute', {
|
||
sessionId,
|
||
code
|
||
});
|
||
|
||
if (response.data.success) {
|
||
// 4. 刷新表格
|
||
setData(response.data.preview);
|
||
message.success('执行成功!');
|
||
} else {
|
||
message.error(`执行失败:${response.data.error}`);
|
||
}
|
||
} finally {
|
||
// 5. 解锁表格
|
||
setIsLocked(false);
|
||
}
|
||
}
|
||
```
|
||
|
||
**测试用例:**
|
||
- [ ] 执行成功,表格正确刷新
|
||
- [ ] 执行失败,显示错误信息
|
||
- [ ] 执行期间,表格处于锁定状态
|
||
|
||
**验收标准:**
|
||
- ✅ 代码能正确修改DataFrame
|
||
- ✅ 前端能实时看到变化
|
||
|
||
**负责人:** Python开发 + 前端
|
||
|
||
---
|
||
|
||
#### Day 9:UI锁定 + 预操作卡片
|
||
|
||
**任务清单:**
|
||
- [ ] 实现AI对话UI
|
||
```typescript
|
||
function AIChatPanel({ sessionId }: Props) {
|
||
const [messages, setMessages] = useState<Message[]>([]);
|
||
const [input, setInput] = useState('');
|
||
const [isProcessing, setIsProcessing] = useState(false);
|
||
|
||
const handleSend = async () => {
|
||
if (!input.trim()) return;
|
||
|
||
// 1. 添加用户消息
|
||
setMessages(prev => [...prev, {
|
||
role: 'user',
|
||
content: input
|
||
}]);
|
||
|
||
setIsProcessing(true);
|
||
|
||
// 2. 调用AI生成代码
|
||
const response = await api.post('/api/v1/dc/tool-c/ai/generate', {
|
||
sessionId,
|
||
prompt: input
|
||
});
|
||
|
||
// 3. 添加AI消息(包含代码)
|
||
setMessages(prev => [...prev, {
|
||
role: 'assistant',
|
||
content: response.data.summary,
|
||
code: response.data.code
|
||
}]);
|
||
|
||
setIsProcessing(false);
|
||
setInput('');
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* 消息列表 */}
|
||
<div className="flex-1 overflow-y-auto p-4">
|
||
{messages.map((msg, idx) => (
|
||
<MessageBubble key={idx} message={msg} />
|
||
))}
|
||
</div>
|
||
|
||
{/* 输入框 */}
|
||
<div className="p-4 border-t">
|
||
<Input.TextArea
|
||
value={input}
|
||
onChange={e => setInput(e.target.value)}
|
||
placeholder="输入指令,例如:把年龄大于60的标记为老年组"
|
||
disabled={isProcessing}
|
||
/>
|
||
<Button onClick={handleSend} disabled={isProcessing}>
|
||
发送
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
- [ ] 实现预操作卡片
|
||
```typescript
|
||
function ActionCard({ code, onConfirm, onCancel }: Props) {
|
||
return (
|
||
<Card className="mt-2 bg-slate-900 text-white">
|
||
<div className="mb-2 text-xs text-slate-400">
|
||
AI生成的代码 - 请确认后执行
|
||
</div>
|
||
<pre className="text-sm font-mono text-blue-300">
|
||
{code}
|
||
</pre>
|
||
<div className="mt-3 flex gap-2">
|
||
<Button type="primary" onClick={onConfirm}>
|
||
运行代码
|
||
</Button>
|
||
<Button onClick={onCancel}>
|
||
取消
|
||
</Button>
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
- [ ] 实现UI锁定
|
||
```typescript
|
||
<AgGridReact
|
||
rowData={data}
|
||
suppressClickEdit={isLocked}
|
||
readOnlyEdit={isLocked}
|
||
/>
|
||
|
||
{isLocked && (
|
||
<div className="absolute inset-0 bg-black/20 flex items-center justify-center">
|
||
<Spin tip="AI正在处理中,请稍候..." />
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
**验收标准:**
|
||
- ✅ 用户能发送消息
|
||
- ✅ AI返回代码时展示预操作卡片
|
||
- ✅ 执行期间表格锁定
|
||
|
||
**负责人:** 前端
|
||
|
||
---
|
||
|
||
#### Day 10:导出 + 端到端测试
|
||
|
||
**任务清单:**
|
||
- [ ] 实现导出API
|
||
```python
|
||
@app.get("/api/python/export/{session_id}")
|
||
async def export_excel(session_id: str):
|
||
df = session_manager.get(session_id)
|
||
|
||
# 生成Excel
|
||
output = io.BytesIO()
|
||
df.to_excel(output, index=False, engine='openpyxl')
|
||
output.seek(0)
|
||
|
||
return StreamingResponse(
|
||
output,
|
||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
headers={
|
||
'Content-Disposition': f'attachment; filename=cleaned_data_{session_id}.xlsx'
|
||
}
|
||
)
|
||
```
|
||
- [ ] 前端导出按钮
|
||
```typescript
|
||
const handleExport = async () => {
|
||
const response = await api.get(`/api/v1/dc/tool-c/session/export/${sessionId}`, {
|
||
responseType: 'blob'
|
||
});
|
||
|
||
const url = window.URL.createObjectURL(response.data);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'cleaned_data.xlsx';
|
||
a.click();
|
||
};
|
||
```
|
||
- [ ] **端到端测试(完整流程)**
|
||
```
|
||
1. 上传 test_data.xlsx (5000行 × 20列)
|
||
2. AI指令:"把年龄大于60的标记为老年组"
|
||
3. 确认 → 执行 → 表格刷新(验证新增列)
|
||
4. AI指令:"删除患者ID为空的行"
|
||
5. 确认 → 执行 → 表格刷新(验证行数减少)
|
||
6. 点击"导出" → 下载文件
|
||
7. 打开下载的文件,验证数据正确
|
||
```
|
||
|
||
**验收标准:**
|
||
- ✅ 整个流程 < 2分钟
|
||
- ✅ 导出的Excel数据正确
|
||
- ✅ AI代码执行成功
|
||
|
||
**负责人:** 全员
|
||
|
||
---
|
||
|
||
### Week 3:优化与测试(5天)
|
||
|
||
#### Day 11-12:错误处理 + AI自我修复
|
||
|
||
**任务清单:**
|
||
- [ ] 实现AI自我修复
|
||
```typescript
|
||
async function executeWithRetry(code: string): Promise<ExecuteResult> {
|
||
// 第一次执行
|
||
let result = await pythonService.execute(sessionId, code);
|
||
|
||
if (!result.success) {
|
||
// 失败,让AI修复
|
||
const fixedCode = await aiService.fixCode(code, result.error);
|
||
|
||
// 重试1次
|
||
result = await pythonService.execute(sessionId, fixedCode);
|
||
|
||
if (!result.success) {
|
||
// 彻底失败
|
||
return {
|
||
success: false,
|
||
error: '代码执行失败,请尝试更详细地描述您的需求',
|
||
originalError: result.error
|
||
};
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
- [ ] 优化错误提示
|
||
```typescript
|
||
function showErrorDetail(error: string) {
|
||
const errorMap = {
|
||
"KeyError: 'age'": "列名'age'不存在,请检查列名是否正确",
|
||
"ValueError": "数据类型不匹配,请检查数据格式",
|
||
"MemoryError": "数据量过大,请减少数据行数"
|
||
};
|
||
|
||
const friendlyMsg = errorMap[error] || error;
|
||
|
||
Modal.error({
|
||
title: '执行失败',
|
||
content: friendlyMsg
|
||
});
|
||
}
|
||
```
|
||
|
||
**测试场景(边界情况):**
|
||
- [ ] 列名不存在:`df['nonexistent']`
|
||
- [ ] 语法错误:`df[df['age'] > 60 and df['age'] < 80]`
|
||
- [ ] 数据类型错误:`df['age'] + '字符串'`
|
||
- [ ] 内存溢出:处理超大数据
|
||
|
||
**验收标准:**
|
||
- ✅ 常见错误能自我修复
|
||
- ✅ 无法修复时有友好提示
|
||
|
||
**负责人:** Node.js后端
|
||
|
||
---
|
||
|
||
#### Day 13:快捷模板 + 编码检测
|
||
|
||
**任务清单:**
|
||
- [ ] 实现快捷模板
|
||
```typescript
|
||
const QUICK_TEMPLATES = {
|
||
'年龄分组': {
|
||
label: '年龄分组(60岁分界)',
|
||
code: `df['age_group'] = pd.cut(df['age'], bins=[0,60,150], labels=['非老年','老年'])`
|
||
},
|
||
'删除空行': {
|
||
label: '删除空行',
|
||
code: `df.dropna(how='all', inplace=True)`
|
||
},
|
||
'性别编码': {
|
||
label: '性别编码(男1女0)',
|
||
code: `df['gender_code'] = df['gender'].map({'男':1, '女':0})`
|
||
}
|
||
};
|
||
|
||
function QuickActions() {
|
||
return (
|
||
<div className="p-4 border-t">
|
||
<div className="text-xs text-slate-500 mb-2">快捷操作</div>
|
||
{Object.entries(QUICK_TEMPLATES).map(([key, template]) => (
|
||
<Button
|
||
key={key}
|
||
size="small"
|
||
onClick={() => executeTemplate(template.code)}
|
||
>
|
||
{template.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
- [ ] 实现编码检测
|
||
```python
|
||
import chardet
|
||
|
||
def detect_encoding(file_bytes: bytes) -> str:
|
||
result = chardet.detect(file_bytes)
|
||
encoding = result['encoding']
|
||
confidence = result['confidence']
|
||
|
||
if confidence < 0.8:
|
||
raise ValueError(f"无法确定文件编码(置信度{confidence:.0%})")
|
||
|
||
return encoding
|
||
|
||
@app.post("/api/python/init")
|
||
async def init_session(file: UploadFile):
|
||
content = await file.read()
|
||
|
||
# 检测编码
|
||
encoding = detect_encoding(content)
|
||
|
||
if encoding.lower() not in ['utf-8', 'utf-8-sig']:
|
||
return {
|
||
"success": False,
|
||
"error": f"文件编码为{encoding},请转换为UTF-8后重新上传",
|
||
"suggestion": "在Excel中另存为CSV UTF-8格式"
|
||
}
|
||
|
||
# 正常加载
|
||
# ...
|
||
```
|
||
|
||
**验收标准:**
|
||
- ✅ 快捷模板一键执行
|
||
- ✅ GBK文件被友好拦截
|
||
|
||
**负责人:** 前端 + Python开发
|
||
|
||
---
|
||
|
||
#### Day 14:性能测试 + 压力测试
|
||
|
||
**测试场景:**
|
||
- [ ] **性能测试**
|
||
```
|
||
测试数据:5000行 × 20列(约5MB)
|
||
|
||
场景1:上传文件
|
||
- 目标:< 5秒
|
||
- 实测:____秒
|
||
|
||
场景2:AI生成代码
|
||
- 目标:< 5秒
|
||
- 实测:____秒
|
||
|
||
场景3:代码执行+刷新
|
||
- 目标:< 3秒
|
||
- 实测:____秒
|
||
|
||
场景4:导出Excel
|
||
- 目标:< 3秒
|
||
- 实测:____秒
|
||
|
||
总计:< 16秒
|
||
```
|
||
- [ ] **压力测试**
|
||
```
|
||
场景1:并发用户
|
||
- 5个用户同时上传文件
|
||
- 验证:Session隔离,互不影响
|
||
|
||
场景2:大数据量
|
||
- 上传50000行 × 50列(约10MB)
|
||
- 验证:不崩溃,响应时间可接受
|
||
|
||
场景3:连续操作
|
||
- 连续执行10次AI指令
|
||
- 验证:内存不泄漏
|
||
```
|
||
|
||
**性能优化(如果不达标):**
|
||
- [ ] 前端:AG Grid虚拟滚动配置
|
||
- [ ] 后端:DataFrame操作改为惰性计算
|
||
- [ ] Python:使用Polars替代Pandas(如果需要)
|
||
|
||
**验收标准:**
|
||
- ✅ 单次操作 < 16秒
|
||
- ✅ 5个并发用户正常工作
|
||
- ✅ 无内存泄漏
|
||
|
||
**负责人:** 全员
|
||
|
||
---
|
||
|
||
#### Day 15:文档 + 演示准备
|
||
|
||
**任务清单:**
|
||
- [ ] 编写用户文档
|
||
```markdown
|
||
# 工具C使用指南
|
||
|
||
## 快速开始
|
||
1. 点击"上传文件",选择Excel(限10MB)
|
||
2. 等待3-5秒,表格加载完成
|
||
3. 在右侧AI助手输入指令,例如:"把年龄大于60的标记为老年组"
|
||
4. 点击"运行代码"确认
|
||
5. 查看表格自动刷新
|
||
6. 完成所有操作后,点击"导出"下载结果
|
||
|
||
## 常见问题
|
||
Q: 文件上传失败,提示文件过大?
|
||
A: 请将Excel文件压缩到10MB以内,或删除不需要的列。
|
||
|
||
Q: 列名显示乱码?
|
||
A: 请在Excel中另存为"CSV UTF-8"格式后重新上传。
|
||
```
|
||
- [ ] 准备演示数据
|
||
```
|
||
创建 demo_data.xlsx:
|
||
- 100行患者数据
|
||
- 包含:patient_id, name, age, gender, admission_date, bmi
|
||
- 故意制造一些问题:空值、异常值、需要清洗的数据
|
||
```
|
||
- [ ] 准备演示脚本
|
||
```
|
||
演示流程(5分钟):
|
||
1. 上传demo_data.xlsx
|
||
2. AI指令:"把年龄大于60的标记为老年组"
|
||
3. AI指令:"删除患者ID为空的行"
|
||
4. AI指令:"把性别转为数字,男1女0"
|
||
5. 导出结果
|
||
6. 展示导出的Excel文件
|
||
```
|
||
|
||
**验收标准:**
|
||
- ✅ 文档清晰易懂
|
||
- ✅ 演示流畅无卡顿
|
||
- ✅ 演示数据能体现核心功能
|
||
|
||
**负责人:** 全员
|
||
|
||
---
|
||
|
||
## 五、风险应对策略
|
||
|
||
### 5.1 风险清单与应对
|
||
|
||
| 风险 | 概率 | 影响 | 应对策略 | 降级方案 |
|
||
|------|------|------|---------|---------|
|
||
| **AI代码质量低(成功率<60%)** | 中 | 致命 | 1. 优化Prompt工程<br>2. 增加Few-shot示例<br>3. 实现自我修复 | **Pivot:改用代码模板库** |
|
||
| **Apache Arrow集成困难** | 高 | 高 | 1. MVP不用Arrow<br>2. 直接用JSON | **已降级:MVP用JSON** |
|
||
| **Redis内存成本高** | 中 | 中 | 1. MVP用进程内存<br>2. 激进的Session过期 | **已降级:用Map缓存** |
|
||
| **Python内存泄漏** | 中 | 高 | 1. 不做历史快照<br>2. 定期重启进程 | **SAE自动重启** |
|
||
| **中文Excel乱码** | 高 | 中 | 1. chardet自动检测<br>2. 友好报错提示 | **文档说明转UTF-8** |
|
||
| **SAE冷启动慢** | 高 | 中 | 1. 最小实例数=1<br>2. 异步初始化 | **Loading优化** |
|
||
| **前端AG Grid性能差** | 低 | 中 | 1. 只展示100行<br>2. 虚拟滚动 | **已优化** |
|
||
|
||
### 5.2 快速失败触发器
|
||
|
||
**立即停止开发的条件:**
|
||
|
||
1. **Day 7结束,AI代码成功率 < 60%**
|
||
- 决策:放弃AI Code Interpreter路线
|
||
- Pivot:改为"代码模板库 + 参数化配置"
|
||
|
||
2. **Day 10结束,端到端时间 > 30秒**
|
||
- 决策:性能无法接受
|
||
- Pivot:改为批处理模式(上传 → 后台处理 → 下载)
|
||
|
||
3. **Day 14结束,3个用户测试,都无法独立完成任务**
|
||
- 决策:交互设计失败
|
||
- Pivot:重新设计UI,增加新手引导
|
||
|
||
---
|
||
|
||
## 六、验收标准
|
||
|
||
### 6.1 功能验收清单
|
||
|
||
| 编号 | 验收项 | 验收标准 | 验收方式 |
|
||
|------|--------|---------|---------|
|
||
| **F-001** | 文件上传 | 能上传10MB以内的Excel | 手动测试 |
|
||
| **F-002** | 编码检测 | GBK文件被友好拦截 | 手动测试 |
|
||
| **F-003** | 表格展示 | 100行数据正确显示,中文无乱码 | 手动测试 |
|
||
| **F-004** | AI对话 | 能发送消息,接收回复 | 手动测试 |
|
||
| **F-005** | 代码生成 | **10个场景,8个成功(80%)** | 自动化测试 |
|
||
| **F-006** | AST检查 | 拦截`import os`等危险代码 | 单元测试 |
|
||
| **F-007** | 代码执行 | 能正确修改DataFrame | 集成测试 |
|
||
| **F-008** | 表格刷新 | 执行后自动更新 | 手动测试 |
|
||
| **F-009** | UI锁定 | AI处理时表格只读+遮罩 | 手动测试 |
|
||
| **F-010** | 导出Excel | 下载的文件数据正确 | 手动测试 |
|
||
|
||
### 6.2 性能验收标准
|
||
|
||
| 指标 | 目标值 | 验收方式 |
|
||
|------|--------|---------|
|
||
| 文件上传到表格显示 | < 5秒 | 性能测试 |
|
||
| AI生成代码 | < 5秒 | 性能测试 |
|
||
| 代码执行+刷新 | < 3秒 | 性能测试 |
|
||
| 导出Excel | < 3秒 | 性能测试 |
|
||
| **端到端总时间** | **< 16秒** | 性能测试 |
|
||
| 并发用户数 | ≥ 5人 | 压力测试 |
|
||
| 内存占用 | < 2GB(5用户) | 压力测试 |
|
||
|
||
### 6.3 安全验收标准
|
||
|
||
| 指标 | 验收标准 | 验收方式 |
|
||
|------|---------|---------|
|
||
| 代码沙箱 | 拦截所有危险代码 | 渗透测试 |
|
||
| Session隔离 | 用户A看不到用户B数据 | 并发测试 |
|
||
| Session过期 | 10分钟后自动清理 | 时间测试 |
|
||
|
||
---
|
||
|
||
## 七、快速失败机制
|
||
|
||
### 7.1 关键检查点
|
||
|
||
```
|
||
Checkpoint 1 (Day 5结束):
|
||
├─ System Prompt能否让AI理解数据?
|
||
├─ AI能生成Pandas代码吗?
|
||
└─ 决策:继续 or 调整Prompt策略
|
||
|
||
Checkpoint 2 (Day 7结束):
|
||
├─ AI代码成功率 ≥ 80%?
|
||
├─ 如果 < 60%:立即停止,改用模板库
|
||
└─ 如果 60-80%:继续优化Prompt
|
||
|
||
Checkpoint 3 (Day 10结束):
|
||
├─ 端到端流程能跑通?
|
||
├─ 性能 < 16秒?
|
||
└─ 决策:继续 or 性能优化
|
||
|
||
Checkpoint 4 (Day 14结束):
|
||
├─ 用户测试通过?
|
||
├─ 无重大Bug?
|
||
└─ 决策:发布 or 修复Bug
|
||
```
|
||
|
||
### 7.2 Pivot决策树
|
||
|
||
```
|
||
如果 AI代码成功率 < 60%:
|
||
├─ Pivot 1:改用"代码模板库 + 参数化配置"
|
||
│ └─ 用户选择模板 → 填写参数 → 生成代码
|
||
│
|
||
├─ Pivot 2:AI变成"辅助建议"角色
|
||
│ └─ 用户手动写代码,AI提供建议
|
||
│
|
||
└─ Pivot 3:简化为"批处理模式"
|
||
└─ 上传 → 选择预设操作 → 后台处理 → 下载
|
||
|
||
如果 性能无法接受(> 30秒):
|
||
├─ 方案1:引入Apache Arrow
|
||
├─ 方案2:使用Polars替代Pandas
|
||
└─ 方案3:改为批处理+异步通知
|
||
|
||
如果 用户无法独立完成任务:
|
||
├─ 方案1:增加交互式新手引导
|
||
├─ 方案2:提供示例数据集
|
||
└─ 方案3:简化UI,减少选项
|
||
```
|
||
|
||
---
|
||
|
||
## 八、附录
|
||
|
||
### 8.1 测试数据准备
|
||
|
||
**创建标准测试数据集:**
|
||
```python
|
||
# create_test_data.py
|
||
import pandas as pd
|
||
import numpy as np
|
||
|
||
# 生成5000行测试数据
|
||
data = {
|
||
'patient_id': [f'P{i:04d}' for i in range(1, 5001)],
|
||
'name': [f'患者{i}' for i in range(1, 5001)],
|
||
'age': np.random.randint(18, 90, 5000),
|
||
'gender': np.random.choice(['男', '女'], 5000),
|
||
'admission_date': pd.date_range('2023-01-01', periods=5000, freq='H'),
|
||
'weight': np.random.uniform(45, 95, 5000).round(1),
|
||
'height': np.random.uniform(150, 190, 5000).round(0),
|
||
'diagnosis': np.random.choice(['肺癌', '糖尿病', '高血压'], 5000)
|
||
}
|
||
|
||
df = pd.DataFrame(data)
|
||
|
||
# 故意制造问题
|
||
df.loc[np.random.choice(5000, 100, replace=False), 'age'] = None # 100个缺失
|
||
df.loc[np.random.choice(5000, 10, replace=False), 'age'] = 150 # 10个异常值
|
||
df.loc[np.random.choice(5000, 50, replace=False), 'patient_id'] = None # 50个空ID
|
||
|
||
df.to_excel('test_data.xlsx', index=False)
|
||
```
|
||
|
||
### 8.2 10个典型场景测试用例
|
||
|
||
```python
|
||
# test_ai_scenarios.py
|
||
test_cases = [
|
||
{
|
||
"id": "TC-001",
|
||
"prompt": "把年龄大于60的患者标记为老年组",
|
||
"expected_code": "df['age_group'] = df['age'].apply(lambda x: '老年组' if pd.notna(x) and x > 60 else '非老年组')",
|
||
"verify": lambda df: 'age_group' in df.columns
|
||
},
|
||
{
|
||
"id": "TC-002",
|
||
"prompt": "删除所有患者ID为空的行",
|
||
"expected_code": "df.dropna(subset=['patient_id'], inplace=True)",
|
||
"verify": lambda df: df['patient_id'].isnull().sum() == 0
|
||
},
|
||
# ... 其余8个场景
|
||
]
|
||
```
|
||
|
||
### 8.3 环境配置清单
|
||
|
||
**Node.js依赖:**
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"fastify": "^4.0.0",
|
||
"axios": "^1.6.0",
|
||
"joi": "^17.11.0",
|
||
"@fastify/multipart": "^8.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Python依赖:**
|
||
```txt
|
||
fastapi==0.115.0
|
||
uvicorn==0.30.0
|
||
pandas==2.2.0
|
||
openpyxl==3.1.0
|
||
chardet==5.2.0
|
||
python-multipart==0.0.9
|
||
```
|
||
|
||
---
|
||
|
||
## 九、项目管理
|
||
|
||
### 9.1 团队配置
|
||
|
||
| 角色 | 人数 | 职责 |
|
||
|------|------|------|
|
||
| 前端开发 | 1人 | React + AG Grid + UI |
|
||
| Node.js后端 | 1人 | BFF + LLM集成 + Prompt工程 |
|
||
| Python开发 | 1人 | FastAPI + DataFrame + AST检查 |
|
||
| **总计** | **3人** | **3周(15天)** |
|
||
|
||
### 9.2 沟通机制
|
||
|
||
- **每日站会**:15分钟,同步进度和问题
|
||
- **关键检查点**:Day 5、7、10、14
|
||
- **问题升级**:阻塞问题1小时内升级
|
||
- **文档更新**:每日更新开发记录
|
||
|
||
### 9.3 代码管理
|
||
|
||
**分支策略:**
|
||
```
|
||
main (稳定版)
|
||
└── develop (开发版)
|
||
├── feature/tool-c-frontend
|
||
├── feature/tool-c-backend
|
||
└── feature/tool-c-python
|
||
```
|
||
|
||
**提交规范:**
|
||
```
|
||
feat: 新增AI代码生成功能
|
||
fix: 修复表格刷新Bug
|
||
test: 增加AST检查测试用例
|
||
docs: 更新开发文档
|
||
```
|
||
|
||
---
|
||
|
||
## 十、总结
|
||
|
||
### 核心成功要素
|
||
1. ✅ **AI代码质量**:成功率 > 80%(核心假设)
|
||
2. ✅ **性能可接受**:端到端 < 16秒
|
||
3. ✅ **交互简单**:用户无需文档就能用
|
||
|
||
### 风险控制
|
||
1. ✅ **快速失败**:7天验证AI能力,不行立即Pivot
|
||
2. ✅ **务实技术**:JSON不用Arrow,内存不用Redis
|
||
3. ✅ **降级方案**:准备好模板库、批处理等Plan B
|
||
|
||
### 时间节点
|
||
- **Week 1**:基础架构(5天)
|
||
- **Week 2**:核心功能(5天)
|
||
- **Week 3**:优化测试(5天)
|
||
- **总计**:15个工作日
|
||
|
||
---
|
||
|
||
## 📝 文档修订记录
|
||
|
||
### V1.3(2025-12-06)- 发现现有Python服务 ⭐ 重大发现
|
||
|
||
**修正内容**:检查系统发现已有Python微服务,不需要重复开发:
|
||
|
||
#### 修正1:复用现有Python服务,不重复造轮 ⭐⭐⭐⭐⭐
|
||
- **重大发现**:系统已有 `extraction_service/`(FastAPI + Pandas + openpyxl)
|
||
- **现有功能**:
|
||
- ✅ PDF/Docx/Txt文档提取(PyMuPDF + Mammoth)
|
||
- ✅ Pandas、openpyxl、chardet、langdetect已安装
|
||
- ✅ FastAPI框架已运行(端口8000)
|
||
- ✅ Node.js已有ExtractionClient集成
|
||
- **新增方案**:
|
||
- ✅ 扩展现有服务,添加 `/api/dc/execute` 端点
|
||
- ✅ 新增 `dc_executor.py` 模块(AST检查 + 代码执行)
|
||
- ✅ 复用ExtractionClient模式调用
|
||
- **避免重复**:
|
||
- ❌ 不需要新建Python项目
|
||
- ❌ 不需要重新安装依赖
|
||
- ❌ 不需要重新写HTTP调用逻辑
|
||
|
||
---
|
||
|
||
### V1.2(2025-12-06)- Python执行环境 + 复杂场景 ⚠️ 核心功能
|
||
|
||
**修正内容**:根据用户反馈,明确Python代码执行是核心功能:
|
||
|
||
#### 修正1:Python执行是核心,不是可选 ⭐⭐⭐⭐⭐
|
||
- **原错误**:说"MVP阶段可能不需要Python微服务"
|
||
- **已修正**:**Python代码执行是工具C的核心价值,必须实现**
|
||
- **新增内容**:
|
||
- Day 1增加Python环境搭建
|
||
- 新增PythonExecutorService(Node.js ↔ Python通信)
|
||
- 新增executor.py(Pandas代码执行器)
|
||
- 技术方案:Node.js child_process调用Python脚本
|
||
|
||
#### 修正2:测试场景从简单到复杂 ✅
|
||
- **原问题**:10个测试场景过于简单
|
||
- **已修正**:15个真实医疗数据清洗场景
|
||
- 🟢 基础场景5个:单步骤操作
|
||
- 🟡 中等场景5个:多步骤、跨列逻辑
|
||
- 🔴 高级场景5个:分组聚合、时间序列、医学规则
|
||
- **新增场景**:
|
||
- 复杂条件逻辑(生存时间计算)
|
||
- 医学规则引擎(肝功能分级)
|
||
- 时间序列分析(指标变化率)
|
||
- 文本提取(TNM分期)
|
||
- 字符串处理(血压拆分)
|
||
|
||
#### 修正3:验收标准分层 ✅
|
||
- **原标准**:总体成功率 > 80%
|
||
- **新标准**:
|
||
- 基础场景成功率 > 90%
|
||
- 中等场景成功率 > 80%
|
||
- 高级场景成功率 > 60%
|
||
- 总体成功率 > 80%(12/15场景)
|
||
|
||
#### 修正4:核心假设验证 ✅
|
||
- **新增H2**:Python代码执行环境稳定可靠
|
||
- **修改H1**:从"生成代码"改为"生成代码并成功执行"
|
||
- **强调**:AI生成代码 + 真实执行 + 表格刷新是差异化价值
|
||
|
||
---
|
||
|
||
### V1.1(2025-12-06)- 架构合规性修正 ⚠️ 重要
|
||
|
||
**修正内容**:根据用户严肃提醒,修正以下违反规范的问题:
|
||
|
||
#### 修正1:强制复用平台能力 ✅
|
||
- **原错误**:建议自己实现Session管理、存储、日志
|
||
- **已修正**:强制使用`storage`, `logger`, `cache`, `prisma`, `LLMFactory`
|
||
- **影响章节**:所有Day 1-15的代码示例
|
||
|
||
#### 修正2:Session存储方式 ✅
|
||
- **原错误**:建议用`Map<sessionId, data>`内存缓存
|
||
- **已修正**:Session存数据库(`dc_tool_c_sessions`表)
|
||
- **原因**:违反云原生规范第2条(禁止内存缓存)
|
||
|
||
#### 修正3:文件夹结构 ✅
|
||
- **原错误**:建议创建`python-service/`、`node-service/`子文件夹
|
||
- **已修正**:遵循tool-b结构(`services/`, `controllers/`, `routes/`)
|
||
- **参考**:`backend/src/modules/dc/tool-b/`
|
||
|
||
#### 修正4:技术栈说明 ✅
|
||
- **原错误**:未强调复用现有能力
|
||
- **已修正**:明确标注"已有"、"复用平台能力"
|
||
- **新增**:云原生开发规范检查清单
|
||
|
||
#### 修正5:代码示例 ✅
|
||
- **原错误**:Day 1-5的代码示例未体现平台服务
|
||
- **已修正**:所有代码示例都使用`import { storage } from '@/common/storage'`等
|
||
- **新增**:常见错误示例(❌ 严禁)
|
||
|
||
#### 新增章节 ✅
|
||
- **开发前必读**:强调不要重复造轮子
|
||
- **云原生规范强制要求**:列出6条核心规范
|
||
- **常见错误示例**:展示5个严禁的错误写法
|
||
|
||
---
|
||
|
||
**文档状态:** ✅ 已完成(V1.1 架构合规性修正版)
|
||
**下一步:** 团队Review → 严格按规范开发
|
||
**负责人:** 项目经理
|
||
**创建日期:** 2025-12-06
|
||
**修订日期:** 2025-12-06(架构合规性修正)
|
||
|
||
**⚠️ 重要提醒**:
|
||
1. 开发前必须阅读:`docs/04-开发规范/08-云原生开发规范.md`
|
||
2. 参考现有实现:`backend/src/modules/dc/tool-b/`
|
||
3. 禁止违反规范:内存缓存、本地文件存储、重复实现平台能力
|
||
|