- 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
19 KiB
19 KiB
AI 分析模块重构设计文档
版本: v2.0 日期: 2026-03-02 作者: System Architect
1. 背景与目标
1.1 当前问题
- 架构混乱: 同步/异步接口混用,用户体验不一致
- Prompt 硬编码: 无法灵活调整诊断策略
- 模型选择僵化: 用户无法指定模型
- 结果存储不规范: 诊断报告存在
task.message,无法检索和分析 - 缺少历史管理: 无法查看、对比、导出历史诊断
1.2 设计目标
- 统一异步架构: 所有 AI 任务进队列,前端轮询状态
- 配置化 Prompt: 模板化管理,支持自定义
- 灵活模型选择: 用户可选模型,记住偏好
- 规范化存储: 独立表管理诊断记录
- 完整历史管理: 查询、筛选、导出、对比
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 核心交互流程
快速诊断:
- 用户在下拉框选择 D-InSAR 结果
- 选择诊断模式(快速/标准/详细)
- 点击"开始诊断"
- 前端 POST
/api/ai-diagnosis,获得task_id - 显示进度条,轮询
/api/tasks/{task_id} - 完成后自动打开详情 Modal
查看历史:
- 表格显示所有诊断记录(分页)
- 点击行打开详情 Modal
- Modal 显示完整 Markdown 报告(支持复制、导出)
模型管理:
- 顶部卡片显示当前选中模型
- 点击"切换模型"打开下拉列表
- 选择后保存到 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:使用
weasyprint或pdfkit - 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_tasks 的 message 字段提取历史诊断,写入新表:
# 迁移脚本
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(可选):
- 用户偏好保存
- 对比分析
- 自动化工作流
文档结束