# AI 分析模块重构设计文档 **版本**: v2.0 **日期**: 2026-03-02 **作者**: System Architect --- ## 1. 背景与目标 ### 1.1 当前问题 - **架构混乱**: 同步/异步接口混用,用户体验不一致 - **Prompt 硬编码**: 无法灵活调整诊断策略 - **模型选择僵化**: 用户无法指定模型 - **结果存储不规范**: 诊断报告存在 `task.message`,无法检索和分析 - **缺少历史管理**: 无法查看、对比、导出历史诊断 ### 1.2 设计目标 1. **统一异步架构**: 所有 AI 任务进队列,前端轮询状态 2. **配置化 Prompt**: 模板化管理,支持自定义 3. **灵活模型选择**: 用户可选模型,记住偏好 4. **规范化存储**: 独立表管理诊断记录 5. **完整历史管理**: 查询、筛选、导出、对比 --- ## 2. 数据库设计 ### 2.1 核心表:`ai_diagnosis` 存储所有 AI 诊断记录。 ```sql CREATE TABLE ai_diagnosis ( id SERIAL PRIMARY KEY, -- 关联信息 result_id INTEGER NOT NULL REFERENCES dinsar_results(id) ON DELETE CASCADE, task_id VARCHAR(50), -- 关联 system_tasks,用于追踪任务状态 -- 模型与配置 model_name VARCHAR(100) NOT NULL, -- 如 'qwen3-vl:8b' prompt_template VARCHAR(50) NOT NULL, -- 'quick'/'standard'/'detailed'/'custom' prompt_text TEXT, -- 实际使用的完整 prompt(用于审计) -- 诊断结果 diagnosis_markdown TEXT NOT NULL, -- Markdown 格式的诊断报告 risk_level VARCHAR(20), -- 'LOW'/'MEDIUM'/'HIGH'/NULL confidence_score FLOAT, -- 0.0-1.0,模型自评置信度(可选) -- 上下文信息(快照,避免关联查询) result_name VARCHAR(255), date_range VARCHAR(100), -- 如 '20240101-20240115' quality_score FLOAT, -- 当时的 ai_score hazards_found INTEGER DEFAULT 0, -- 覆盖范围内的隐患点数量 hazards_snapshot JSONB, -- 隐患点详情快照 [{name, type, location}] -- 元数据 created_at TIMESTAMP DEFAULT NOW(), duration_seconds FLOAT, -- 诊断耗时 error_message TEXT, -- 如果失败,记录错误 -- 索引 INDEX idx_result_id (result_id), INDEX idx_created_at (created_at DESC), INDEX idx_risk_level (risk_level) ); ``` **设计要点**: - `prompt_text` 存储实际 prompt,便于审计和复现 - `hazards_snapshot` 用 JSONB 存快照,避免隐患点被删除后无法回溯 - `duration_seconds` 用于性能分析 - `error_message` 支持失败记录(不删除,便于调试) ### 2.2 配置表:`ai_prompt_templates`(可选) 如果需要 UI 管理 Prompt 模板,可建此表。否则用配置文件即可。 ```sql CREATE TABLE ai_prompt_templates ( id SERIAL PRIMARY KEY, name VARCHAR(50) UNIQUE NOT NULL, -- 'quick'/'standard'/'detailed' display_name_zh VARCHAR(100), display_name_en VARCHAR(100), template_text TEXT NOT NULL, is_system BOOLEAN DEFAULT FALSE, -- 系统内置模板不可删除 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` **初始数据**: ```sql INSERT INTO ai_prompt_templates (name, display_name_zh, display_name_en, template_text, is_system) VALUES ('quick', '快速诊断', 'Quick Diagnosis', '你是 InSAR 专家,用 200 字简述...', TRUE), ('standard', '标准诊断', 'Standard Diagnosis', '你是资深专家,按 4 步分析...', TRUE), ('detailed', '详细诊断', 'Detailed Diagnosis', '深度分析,包含地形、历史对比...', TRUE); ``` ### 2.3 用户偏好表:`user_ai_preferences`(可选) 存储用户的 AI 偏好设置。 ```sql CREATE TABLE user_ai_preferences ( user_id INTEGER PRIMARY KEY REFERENCES users(id), preferred_model VARCHAR(100), -- 如 'qwen3-vl:8b' default_template VARCHAR(50), -- 'standard' auto_diagnose BOOLEAN DEFAULT FALSE, -- 新结果自动诊断 updated_at TIMESTAMP DEFAULT NOW() ); ``` --- ## 3. 后端 API 设计 ### 3.1 路由结构 新建 `backend/app/routers/ai_diagnosis.py`,遵循 RESTful 规范。 #### 3.1.1 诊断记录管理 **列表查询** ```http GET /api/ai-diagnosis?page=1&limit=20&result_id=123&risk_level=HIGH&sort=-created_at ``` **Query 参数**: - `page`, `limit`: 分页 - `result_id`: 筛选特定结果的诊断 - `risk_level`: 筛选风险等级(`LOW`/`MEDIUM`/`HIGH`) - `model_name`: 筛选模型 - `date_from`, `date_to`: 时间范围 - `sort`: 排序字段(`-created_at` 表示倒序) **响应**: ```json { "total": 156, "page": 1, "limit": 20, "items": [ { "id": 42, "result_id": 123, "result_name": "Task_001_20240101_20240115", "model_name": "qwen3-vl:8b", "prompt_template": "standard", "risk_level": "HIGH", "hazards_found": 3, "created_at": "2026-03-02T10:30:00Z", "duration_seconds": 45.2 } ] } ``` **详情查询** ```http GET /api/ai-diagnosis/{id} ``` **响��**: ```json { "id": 42, "result_id": 123, "result_name": "Task_001_20240101_20240115", "model_name": "qwen3-vl:8b", "prompt_template": "standard", "prompt_text": "你是一位拥有 20 年经验...", "diagnosis_markdown": "## 形态学分析\n\n观察到...", "risk_level": "HIGH", "confidence_score": 0.85, "hazards_found": 3, "hazards_snapshot": [ {"name": "XX滑坡", "type": "滑坡", "location": "XX县"} ], "quality_score": 0.72, "created_at": "2026-03-02T10:30:00Z", "duration_seconds": 45.2 } ``` **创建诊断任务** ```http POST /api/ai-diagnosis Content-Type: application/json { "result_id": 123, "model_name": "qwen3-vl:8b", // 可选,不传则自动选择 "prompt_template": "standard" // 'quick'/'standard'/'detailed'/'custom' "custom_prompt": "..." // 仅当 template='custom' 时需要 } ``` **响应**: ```json { "diagnosis_id": 42, // 预创建的记录 ID(状态为 PENDING) "task_id": "task_abc123", "message": "诊断任务已进入队列" } ``` **删除记录** ```http DELETE /api/ai-diagnosis/{id} ``` **批量导出** ```http POST /api/ai-diagnosis/export Content-Type: application/json { "ids": [42, 43, 44], "format": "markdown" // 'markdown'/'pdf'/'json' } ``` **响应**:返回文件流或下载链接。 #### 3.1.2 模型管理 **获取可用模型** ```http GET /api/ai/models ``` **响应**: ```json { "ollama_online": true, "models": [ { "name": "qwen3-vl:8b", "size": "8.5 GB", "modified_at": "2026-03-01T12:00:00Z", "capabilities": ["vision", "text"], "recommended": true }, { "name": "qwen2-vl:72b", "size": "72 GB", "capabilities": ["vision", "text"] } ], "default_model": "qwen3-vl:8b" } ``` **预热模型** ```http POST /api/ai/models/{model_name}/warmup ``` #### 3.1.3 Prompt 模板管理 **列表** ```http GET /api/ai/prompt-templates ``` **响应**: ```json { "templates": [ { "name": "quick", "display_name": "快速诊断", "is_system": true }, { "name": "custom_001", "display_name": "我的自定义模板", "is_system": false } ] } ``` **详情** ```http GET /api/ai/prompt-templates/{name} ``` **创建/更新**(仅非系统模板) ```http POST /api/ai/prompt-templates PUT /api/ai/prompt-templates/{name} { "name": "custom_001", "display_name": "我的模板", "template_text": "..." } ``` --- ## 4. 任务处理流程 ### 4.1 异步任务架构 **流程图**: ``` 用户点击"诊断" ↓ 前端 POST /api/ai-diagnosis ↓ 后端创建 diagnosis 记录(状态 PENDING)+ system_task + system_job ↓ 返回 diagnosis_id + task_id ↓ 前端轮询 GET /api/tasks/{task_id} ↓ job_worker 执行 _handle_ai_analyze ↓ 调用 Ollama VLM ↓ 更新 diagnosis 记录(状态 COMPLETED,写入 markdown) ↓ 更新 system_task(状态 COMPLETED) ↓ 前端检测到完成,跳转到诊断详情页 ``` ### 4.2 Handler 改造 `backend/app/services/job_handlers.py` 中的 `_handle_ai_analyze` 改为: ```python async def _handle_ai_analyze(job: SystemJobORM) -> None: payload = job.payload or {} diagnosis_id = payload.get("diagnosis_id") if not diagnosis_id: raise ValueError("AI_ANALYZE requires diagnosis_id") async with AsyncSessionLocal() as db: # 1. 查询 diagnosis 记录 diag = await db.get(AiDiagnosisORM, diagnosis_id) if not diag: raise ValueError(f"Diagnosis {diagnosis_id} not found") # 2. 查询关联的 result result = await db.get(DinsarResultORM, diag.result_id) # 3. 查询隐患点(PostGIS) hazards = await db.execute( select(HazardPointORM).where(ST_Covers(result.geom, HazardPointORM.geom)) ) hazards = hazards.scalars().all() # 4. 准备图片 img_path = data_service.get_dinsar_cache_path(result.id, result.name) img_base64 = _load_image_as_base64(img_path) # 5. 构建 prompt prompt = _build_prompt(diag.prompt_template, diag.prompt_text, result, hazards) # 6. 调用 VLM start_time = time.time() try: analysis = await analyze_map_with_vlm( images_base64=[img_base64], prompt=prompt, model_name=diag.model_name ) duration = time.time() - start_time # 7. 解析风险等级(从 Markdown 中提取) risk_level = _extract_risk_level(analysis) # 8. 更新 diagnosis 记录 diag.diagnosis_markdown = analysis diag.risk_level = risk_level diag.hazards_found = len(hazards) diag.hazards_snapshot = [ {"name": h.hazard_name, "type": h.hazard_type, "location": f"{h.city}{h.county}"} for h in hazards ] diag.duration_seconds = duration await db.commit() # 9. 更新 task 状态 await task_service.update_task( job.task_id, status="COMPLETED", message=f"诊断完成,风险等级: {risk_level}", progress=100 ) except Exception as e: diag.error_message = str(e) await db.commit() await task_service.update_task( job.task_id, status="FAILED", message=f"诊断失败: {str(e)}" ) ``` --- ## 5. 前端设计 ### 5.1 组件结构 ``` src/ ├── panels/ │ └── AiAnalysisPanel.jsx # 新增:AI 分析主面板 ├── components/ │ ├── AiDiagnosisTable.jsx # 诊断历史表格 │ ├── AiDiagnosisDetailModal.jsx # 诊断详情 Modal │ ├── AiModelSelector.jsx # 模型选择器 │ ├── AiPromptTemplateEditor.jsx # Prompt 模板编辑器 │ └── AiQuickDiagnoseCard.jsx # 快速诊断卡片 ├── api/ │ └── aiDiagnosis.js # API 封装 └── hooks/ └── useAiDiagnosis.js # 自定义 Hook ``` ### 5.2 AiAnalysisPanel 布局 ```jsx