Files
insar-management-system-v2/docs/archive/AI_ANALYSIS_REDESIGN.md
T
Harmon d108b33f80 docs: govern and archive superseded notes
- add documentation governance and cleanup audit documents

- update README and docs index to distinguish current vs historical sources

- move superseded planning, SBAS, Gamma, and experiment notes into docs/archive

- fix remaining archive text garbling in two documents
2026-04-27 08:06:09 +08:00

19 KiB
Raw Blame History

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 诊断记录。

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 模板,可建此表。否则用配置文件即可。

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()
);

初始数据

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 偏好设置。

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 诊断记录管理

列表查询

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 表示倒序)

响应

{
  "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
    }
  ]
}

详情查询

GET /api/ai-diagnosis/{id}

响应

{
  "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
}

创建诊断任务

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' 时需要
}

响应

{
  "diagnosis_id": 42,  // 预创建的记录 ID(状态为 PENDING)
  "task_id": "task_abc123",
  "message": "诊断任务已进入队列"
}

删除记录

DELETE /api/ai-diagnosis/{id}

批量导出

POST /api/ai-diagnosis/export
Content-Type: application/json

{
  "ids": [42, 43, 44],
  "format": "markdown"  // 'markdown'/'pdf'/'json'
}

响应:返回文件流或下载链接。

3.1.2 模型管理

获取可用模型

GET /api/ai/models

响应

{
  "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"
}

预热模型

POST /api/ai/models/{model_name}/warmup

3.1.3 Prompt 模板管理

列表

GET /api/ai/prompt-templates

响应

{
  "templates": [
    {
      "name": "quick",
      "display_name": "快速诊断",
      "is_system": true
    },
    {
      "name": "custom_001",
      "display_name": "我的自定义模板",
      "is_system": false
    }
  ]
}

详情

GET /api/ai/prompt-templates/{name}

创建/更新(仅非系统模板)

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 改为:

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 布局

<div className="ai-analysis-panel">
  {/* 顶部状态卡片 */}
  <div className="ai-status-cards">
    <OllamaStatusCard />
    <ModelSelectorCard />
  </div>

  {/* 快速诊断 */}
  <AiQuickDiagnoseCard />

  {/* 诊断历史 */}
  <div className="ai-diagnosis-history">
    <div className="history-toolbar">
      <input placeholder="搜索结果名称..." />
      <select>
        <option>全部风险等级</option>
        <option>高风险</option>
        <option>中风险</option>
        <option>低风险</option>
      </select>
      <button>导出选中</button>
    </div>
    <AiDiagnosisTable />
  </div>
</div>

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

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 新增:

# 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 作为模板引擎:

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:使用 weasyprintpdfkit
  • Markdown → Word:使用 python-docx + markdown 解析
  • 模板化报告:使用 Jinja2 + HTML 模板

7.4 前端状态管理

如果 AI 功能复杂度增加,可考虑:

  • Zustand(已使用):继续用 useAiStore
  • React Query:管理服务端状态(诊断列表、模型列表)

8. 迁移计划

8.1 数据迁移

Step 1:创建新表

alembic revision -m "add_ai_diagnosis_table"
alembic upgrade head

Step 2:迁移历史数据(可选) 从 system_tasksmessage 字段提取历史诊断,写入新表:

# 迁移脚本
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 个月
  • 内部重定向到新接口
  • 响应格式保持一致

废弃通知

{
  "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 超时与重试

@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 日志记录

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(可选)

  • 用户偏好保存
  • 对比分析
  • 自动化工作流

文档结束