chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+769
View File
@@ -0,0 +1,769 @@
# 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(可选)**
- 用户偏好保存
- 对比分析
- 自动化工作流
---
**文档结束**