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

770 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<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`
```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(可选)**
- 用户偏好保存
- 对比分析
- 自动化工作流
---
**文档结束**