# 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
{/* 顶部状态卡片 */}
{/* 快速诊断 */} {/* 诊断历史 */}
``` ### 5.3 核心交互流程 **快速诊断**: 1. 用户在下拉框选择 D-InSAR 结果 2. 选择诊断模式(快速/标准/详细) 3. 点击"开始诊断" 4. 前端 POST `/api/ai-diagnosis`,获得 `task_id` 5. 显示进度条,轮询 `/api/tasks/{task_id}` 6. 完成后自动打开详情 Modal **查看历史**: 1. 表格显示所有诊断记录(分页) 2. 点击行打开详情 Modal 3. Modal 显示完整 Markdown 报告(支持复制、导出) **模型管理**: 1. 顶部卡片显示当前选中模型 2. 点击"切换模型"打开下拉列表 3. 选择后保存到 localStorage(或用户配置) --- ## 6. 配置管理 ### 6.1 Prompt 模板文件 `backend/app/ai_prompts.py`: ```python PROMPT_TEMPLATES = { 'quick': { 'name_zh': '快速诊断', 'name_en': 'Quick Diagnosis', 'template': """ 你是 InSAR 专家。请用 200 字简述以下形变图的关键发现: - 是否存在明显形变区? - 与已知隐患点是否吻合? - 风险等级(低/中/高)? 背景:{context} """, }, 'standard': { 'name_zh': '标准诊断', 'name_en': 'Standard Diagnosis', 'template': """ 你是拥有 20 年经验的 InSAR 专家。请按以下步骤分析: 1. 形态学分析 2. 时空一致性 3. 风险演化预测 4. 综合风险评级 背景:{context} 隐患点:{hazards} """, }, 'detailed': { 'name_zh': '详细诊断', 'name_en': 'Detailed Diagnosis', 'template': """ 深度分析,包含: - 地形地貌分析 - 历史形变趋势 - 潜在触发因素 - 监测建议 背景:{context} """, }, } def build_prompt(template_name: str, context: dict) -> str: template = PROMPT_TEMPLATES.get(template_name, PROMPT_TEMPLATES['standard']) return template['template'].format(**context) ``` ### 6.2 环境变量 `.env` 新增: ```bash # Ollama 配置 OLLAMA_API_URL=http://127.0.0.1:11434/api/generate OLLAMA_TIMEOUT=600 OLLAMA_DEFAULT_MODEL=qwen3-vl:8b # AI 诊断配置 AI_DIAGNOSIS_AUTO_RETRY=true AI_DIAGNOSIS_MAX_RETRIES=2 AI_DIAGNOSIS_STREAM_OUTPUT=false # 未来支持流式 ``` --- ## 7. 成熟方案参考 ### 7.1 Prompt 管理 参考 **LangChain PromptTemplate**: - 支持变量插值 `{variable}` - 支持条件渲染 `{% if condition %}` - 支持模板继承 可引入 `Jinja2` 作为模板引擎: ```python from jinja2 import Template template = Template(PROMPT_TEMPLATES['standard']['template']) prompt = template.render(context=context, hazards=hazards) ``` ### 7.2 任务队列 当前使用自研的 `job_queue_service`,可考虑迁移到: - **Celery** + Redis:成熟的分布式任务队列 - **Dramatiq**:轻量级替代方案 - **ARQ**:基于 asyncio 的现代方案 ### 7.3 诊断报告导出 - **Markdown → PDF**:使用 `weasyprint` 或 `pdfkit` - **Markdown → Word**:使用 `python-docx` + `markdown` 解析 - **模板化报告**:使用 `Jinja2` + HTML 模板 ### 7.4 前端状态管理 如果 AI 功能复杂度增加,可考虑: - **Zustand**(已使用):继续用 `useAiStore` - **React Query**:管理服务端状态(诊断列表、模型列表) --- ## 8. 迁移计划 ### 8.1 数据迁移 **Step 1**:创建新表 ```bash alembic revision -m "add_ai_diagnosis_table" alembic upgrade head ``` **Step 2**:迁移历史数据(可选) 从 `system_tasks` 的 `message` 字段提取历史诊断,写入新表: ```python # 迁移脚本 async def migrate_old_diagnoses(): tasks = await db.execute( select(SystemTaskORM).where( SystemTaskORM.task_type == 'AI_ANALYZE', SystemTaskORM.status == 'COMPLETED' ) ) for task in tasks.scalars(): msg = json.loads(task.message) diagnosis = AiDiagnosisORM( result_id=msg['result_id'], diagnosis_markdown=msg['analysis'], created_at=task.completed_at, ... ) db.add(diagnosis) await db.commit() ``` ### 8.2 API 兼容性 **向后兼容**: - 保留旧接口 `/ai/analyze-result/{id}` 3 个月 - 内部重定向到新接口 - 响应格式保持一致 **废弃通知**: ```json { "message": "诊断任务已进入队列", "task_id": "...", "diagnosis_id": 42, "_deprecated": "此接口将在 2026-06-01 废弃,请使用 POST /api/ai-diagnosis" } ``` --- ## 9. 性能优化 ### 9.1 缓存策略 - **模型列表缓存**:`GET /api/ai/models` 结果缓存 5 分钟(Redis) - **Prompt 模板缓存**:启动时加载到内存 - **诊断列表分页**:使用游标分页(`cursor-based`)而非 offset ### 9.2 并发控制 - **Ollama 并发限制**:同时最多 2 个 VLM 任务(避免显存溢出) - **任务优先级**:用户手动触发 > 自动诊断 ### 9.3 超时与重试 ```python @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_exception_type(httpx.TimeoutException) ) async def call_ollama_with_retry(...): ... ``` --- ## 10. 安全考虑 ### 10.1 输入验证 - **Prompt 注入防护**:限制自定义 prompt 长度(8000 字符) - **模型名称白名单**:只允许已知模型 - **结果 ID 权限检查**:确保用户有权访问该结果 ### 10.2 输出过滤 - **敏感信息脱敏**:诊断报告中的坐标、地址模糊化 - **XSS 防护**:Markdown 渲染时使用 `DOMPurify` --- ## 11. 监控与日志 ### 11.1 关键指标 - **诊断成功率**:`completed / (completed + failed)` - **平均耗时**:按模型、模板统计 - **模型使用分布**:哪个模型最受欢迎 - **风险等级分布**:高/中/低风险占比 ### 11.2 日志记录 ```python logger.info( "AI diagnosis completed", extra={ "diagnosis_id": diag.id, "result_id": diag.result_id, "model": diag.model_name, "duration": diag.duration_seconds, "risk_level": diag.risk_level, } ) ``` --- ## 12. 未来扩展 ### 12.1 多模态输入 - 支持上传多张图片(时序对比) - 支持附加文本描述(用户观察) ### 12.2 对比分析 - 同一结果的多次诊断对比 - 不同模型的诊断结果对比 ### 12.3 自动化工作流 - 新结果自动触发诊断 - 高风险自动发送邮件/钉钉通知 ### 12.4 Fine-tuning - 收集用户反馈(诊断是否准确) - 定期 fine-tune 模型 --- ## 13. 总结 ### 13.1 核心改进 | 维度 | 当前 | 重构后 | |------|------|--------| | 架构 | 同步/异步混用 | 统一异步任务 | | Prompt | 硬编码 | 模板化 + 可自定义 | | 模型 | 自动选择 | 用户可选 + 偏好记忆 | | 存储 | task.message | 独立表 + 完整字段 | | 历史 | 无法查询 | 完整 CRUD + 导出 | ### 13.2 实施优先级 **P0(必须)**: - 创建 `ai_diagnosis` 表 - 改造 `_handle_ai_analyze` - 实现基础 CRUD API - 创建 `AiAnalysisPanel` 前端面板 **P1(重要)**: - Prompt 模板管理 - 模型选择器 - 诊断详情 Modal - 导出功能 **P2(可选)**: - 用户偏好保存 - 对比分析 - 自动化工作流 --- **文档结束**