docs: add speckit planning artifacts for 001-ai-service-requirements

Generated plan.md, research.md, data-model.md, contracts/api.md,
quickstart.md, and CLAUDE.md agent context via /speckit-plan.
This commit is contained in:
wh
2026-04-10 14:58:13 +08:00
parent e0d080ceea
commit 092f9dbfc5
6 changed files with 835 additions and 0 deletions

30
CLAUDE.md Normal file
View File

@@ -0,0 +1,30 @@
# label_ai_service Development Guidelines
Auto-generated from all feature plans. Last updated: 2026-04-10
## Active Technologies
- Python 3.12.13conda `label` 环境) + FastAPI ≥0.111, uvicorn[standard] ≥0.29, pydantic ≥2.7, zhipuai ≥2.1, boto3 ≥1.34, pdfplumber ≥0.11, python-docx ≥1.1, opencv-python-headless ≥4.9, numpy ≥1.26, httpx ≥0.27, python-dotenv ≥1.0, pyyaml ≥6.0 (001-ai-service-requirements)
## Project Structure
```text
backend/
frontend/
tests/
```
## Commands
cd src; pytest; ruff check .
## Code Style
Python 3.12.13conda `label` 环境): Follow standard conventions
## Recent Changes
- 001-ai-service-requirements: Added Python 3.12.13conda `label` 环境) + FastAPI ≥0.111, uvicorn[standard] ≥0.29, pydantic ≥2.7, zhipuai ≥2.1, boto3 ≥1.34, pdfplumber ≥0.11, python-docx ≥1.1, opencv-python-headless ≥4.9, numpy ≥1.26, httpx ≥0.27, python-dotenv ≥1.0, pyyaml ≥6.0
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@@ -0,0 +1,333 @@
# API Contract: AI 服务接口定义
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
**Base URL**: `http://ai-service:8000`
**API Prefix**: `/api/v1`
**Swagger**: `/docs`FastAPI 自动生成)
---
## 通用约定
### 请求格式
- 所有请求体:`Content-Type: application/json`
- 无认证机制(内网服务,仅 Java 后端调用)
### 响应格式
- 成功HTTP 2xxJSON 响应体
- 错误HTTP 4xx/5xx统一错误格式
```json
{"code": "ERROR_CODE", "message": "具体描述"}
```
### 错误码
| HTTP 状态码 | code | 触发条件 |
|------------|------|---------|
| 400 | UNSUPPORTED_FILE_TYPE | 文件格式不支持(如 .xlsx |
| 400 | VIDEO_TOO_LARGE | 视频文件超过大小上限 |
| 502 | STORAGE_ERROR | RustFS 不可达或文件不存在 |
| 502 | LLM_PARSE_ERROR | GLM 返回非合法 JSON |
| 503 | LLM_CALL_ERROR | GLM API 限流 / 超时 |
| 500 | INTERNAL_ERROR | 未捕获异常 |
---
## 端点一览
| 端点 | 方法 | 功能 | 响应码 |
|------|------|------|--------|
| `/health` | GET | 健康检查 | 200 |
| `/api/v1/text/extract` | POST | 文档三元组提取 | 200 |
| `/api/v1/image/extract` | POST | 图像四元组提取 | 200 |
| `/api/v1/video/extract-frames` | POST | 视频帧提取(异步) | 202 |
| `/api/v1/video/to-text` | POST | 视频转文本(异步) | 202 |
| `/api/v1/qa/gen-text` | POST | 文本问答对生成 | 200 |
| `/api/v1/qa/gen-image` | POST | 图像问答对生成 | 200 |
| `/api/v1/finetune/start` | POST | 提交微调任务 | 200 |
| `/api/v1/finetune/status/{jobId}` | GET | 查询微调状态 | 200 |
---
## 端点详情
### GET /health
健康检查端点,无需认证,无请求体。
**响应200 OK**:
```json
{"status": "ok"}
```
---
### POST /api/v1/text/extract
从存储中指定路径的文档提取文本三元组。
**请求体**:
```json
{
"file_path": "text/202404/123.txt",
"file_name": "设备规范.txt",
"model": "glm-4-flash",
"prompt_template": "..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file_path | string | 是 | RustFS 中的文件路径 |
| file_name | string | 是 | 带扩展名的文件名(用于判断格式) |
| model | string | 否 | 模型名,默认使用 config 中的 default_text |
| prompt_template | string | 否 | 自定义提示词,不传使用内置模板 |
**支持格式**: `.txt`, `.pdf`, `.docx`
**响应200 OK**:
```json
{
"items": [
{
"subject": "变压器",
"predicate": "额定电压",
"object": "110kV",
"source_snippet": "该变压器额定电压为110kV",
"source_offset": {"start": 120, "end": 150}
}
]
}
```
---
### POST /api/v1/image/extract
从存储中指定路径的图片提取知识四元组,并自动裁剪 bbox 区域。
**请求体**:
```json
{
"file_path": "image/202404/456.jpg",
"task_id": 789,
"model": "glm-4v-flash",
"prompt_template": "..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file_path | string | 是 | RustFS 中的图片路径 |
| task_id | int | 是 | 标注任务 ID用于构造裁剪图存储路径 |
| model | string | 否 | 默认使用 config 中的 default_vision |
| prompt_template | string | 否 | 自定义提示词 |
**响应200 OK**:
```json
{
"items": [
{
"subject": "电缆接头",
"predicate": "位于",
"object": "配电箱左侧",
"qualifier": "2024年检修现场",
"bbox": {"x": 10, "y": 20, "w": 100, "h": 80},
"cropped_image_path": "crops/789/0.jpg"
}
]
}
```
---
### POST /api/v1/video/extract-frames
触发视频帧提取后台任务,立即返回。
**请求体**:
```json
{
"file_path": "video/202404/001.mp4",
"source_id": 10,
"job_id": 42,
"mode": "interval",
"frame_interval": 30
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file_path | string | 是 | RustFS 中的视频路径 |
| source_id | int | 是 | 原始资料 ID用于构造帧存储路径 |
| job_id | int | 是 | 由 Java 后端分配的任务 ID |
| mode | string | 否 | `interval`(默认)或 `keyframe` |
| frame_interval | int | 否 | interval 模式专用,按帧数步进,默认 30 |
**响应202 Accepted**:
```json
{"message": "任务已接受,后台处理中", "job_id": 42}
```
**完成后回调 Java 后端**POST `{BACKEND_CALLBACK_URL}`:
```json
{
"job_id": 42,
"status": "SUCCESS",
"frames": [
{"frame_index": 0, "time_sec": 0.0, "frame_path": "frames/10/0.jpg"}
],
"error_message": null
}
```
---
### POST /api/v1/video/to-text
触发视频片段转文字后台任务,立即返回。
**请求体**:
```json
{
"file_path": "video/202404/001.mp4",
"source_id": 10,
"job_id": 43,
"start_sec": 0,
"end_sec": 120,
"model": "glm-4v-flash",
"prompt_template": "..."
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file_path | string | 是 | RustFS 中的视频路径 |
| source_id | int | 是 | 原始资料 ID |
| job_id | int | 是 | 由 Java 后端分配的任务 ID |
| start_sec | float | 是 | 分析起始时间(秒) |
| end_sec | float | 是 | 分析结束时间(秒) |
| model | string | 否 | 默认使用 config 中的 default_vision |
| prompt_template | string | 否 | 自定义提示词 |
**响应202 Accepted**:
```json
{"message": "任务已接受,后台处理中", "job_id": 43}
```
**完成后回调 Java 后端**POST `{BACKEND_CALLBACK_URL}`:
```json
{
"job_id": 43,
"status": "SUCCESS",
"output_path": "video-text/10/1712800000.txt",
"error_message": null
}
```
---
### POST /api/v1/qa/gen-text
基于文本三元组批量生成候选问答对。
**请求体**:
```json
{
"items": [
{
"subject": "变压器",
"predicate": "额定电压",
"object": "110kV",
"source_snippet": "该变压器额定电压为110kV"
}
],
"model": "glm-4-flash",
"prompt_template": "..."
}
```
**响应200 OK**:
```json
{
"pairs": [
{"question": "变压器的额定电压是多少?", "answer": "该变压器额定电压为110kV。"}
]
}
```
---
### POST /api/v1/qa/gen-image
基于图像四元组生成候选图文问答对。图片由 AI 服务从存储自动获取,调用方只需提供路径。
**请求体**:
```json
{
"items": [
{
"subject": "电缆接头",
"predicate": "位于",
"object": "配电箱左侧",
"qualifier": "2024年检修现场",
"cropped_image_path": "crops/789/0.jpg"
}
],
"model": "glm-4v-flash",
"prompt_template": "..."
}
```
**响应200 OK**:
```json
{
"pairs": [
{
"question": "图中电缆接头位于何处?",
"answer": "图中电缆接头位于配电箱左侧。",
"image_path": "crops/789/0.jpg"
}
]
}
```
---
### POST /api/v1/finetune/start
向 ZhipuAI 提交微调任务。
**请求体**:
```json
{
"jsonl_url": "https://rustfs.example.com/finetune-export/export/xxx.jsonl",
"base_model": "glm-4-flash",
"hyperparams": {"learning_rate": 1e-4, "epochs": 3}
}
```
**响应200 OK**:
```json
{"job_id": "glm-ft-xxxxxx"}
```
---
### GET /api/v1/finetune/status/{jobId}
查询微调任务状态。
**路径参数**: `jobId` — 微调任务 ID由 `/finetune/start` 返回)
**响应200 OK**:
```json
{
"job_id": "glm-ft-xxxxxx",
"status": "RUNNING",
"progress": 45,
"error_message": null
}
```
`status` 取值: `RUNNING` | `SUCCESS` | `FAILED`

View File

@@ -0,0 +1,167 @@
# Data Model: AI 服务
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
---
## 实体定义
### TripleItem文本三元组
从文档中提取的一条知识关系。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| subject | string | 非空 | 主语实体 |
| predicate | string | 非空 | 谓语/关系 |
| object | string | 非空 | 宾语实体 |
| source_snippet | string | 非空 | 原文中的证据片段(直接引用) |
| source_offset.start | int | ≥0 | 证据片段在全文中的起始字符偏移 |
| source_offset.end | int | >start | 证据片段在全文中的结束字符偏移 |
**状态转换**: 无(只读输出)
---
### QuadrupleItem图像四元组
从图像中提取的一条知识关系,带图像位置信息。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| subject | string | 非空 | 主体实体 |
| predicate | string | 非空 | 关系/属性 |
| object | string | 非空 | 客体实体 |
| qualifier | string | 可为空 | 修饰信息(时间、条件、场景) |
| bbox.x | int | ≥0 | 边界框左上角 x 像素坐标 |
| bbox.y | int | ≥0 | 边界框左上角 y 像素坐标 |
| bbox.w | int | >0 | 边界框宽度(像素) |
| bbox.h | int | >0 | 边界框高度(像素) |
| cropped_image_path | string | 非空 | 裁剪图在 RustFS 中的存储路径 |
**派生规则**: `cropped_image_path = "crops/{task_id}/{item_index}.jpg"`,由 image_service 自动生成并上传
---
### QAPair文本问答对
由文本三元组生成的训练候选问答对。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| question | string | 非空 | 问题文本 |
| answer | string | 非空 | 答案文本 |
---
### ImageQAPair图像问答对
由图像四元组生成的训练候选图文问答对。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| question | string | 非空 | 问题文本 |
| answer | string | 非空 | 答案文本 |
| image_path | string | 非空 | 对应裁剪图的存储路径(来源于 QuadrupleItem.cropped_image_path |
---
### FrameInfo视频帧信息
视频帧提取任务中单帧的元数据。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| frame_index | int | ≥0 | 帧在视频中的原始帧序号 |
| time_sec | float | ≥0.0 | 帧对应的时间点(秒) |
| frame_path | string | 非空 | 帧图在 RustFS 中的存储路径 |
**派生规则**: `frame_path = "frames/{source_id}/{upload_index}.jpg"`
---
### VideoJobCallback视频任务回调
异步视频任务完成后发送给 Java 后端的通知载荷。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| job_id | int | 非空 | 由 Java 后端分配的任务 ID |
| status | string | SUCCESS \| FAILED | 任务最终状态 |
| frames | FrameInfo[] \| null | 仅帧提取时非 null | 提取的帧列表(可为空列表) |
| output_path | string \| null | 仅视频转文本时非 null | 输出文字描述的存储路径 |
| error_message | string \| null | 仅 FAILED 时非 null | 错误描述 |
---
### FinetuneJob微调任务
微调任务的状态快照。
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| job_id | string | 非空 | 由 ZhipuAI 平台分配的任务 ID如 "glm-ft-xxxxxx" |
| status | string | RUNNING \| SUCCESS \| FAILED | 当前状态 |
| progress | int \| null | 0-100 \| null | 完成百分比ZhipuAI 支持时) |
| error_message | string \| null | 仅 FAILED 时非 null | 错误描述 |
**状态映射**:
```
ZhipuAI "running" → RUNNING
ZhipuAI "succeeded" → SUCCESS
ZhipuAI "failed" → FAILED
其他 → RUNNING保守处理
```
---
## RustFS 存储路径规范
| 资源类型 | 存储桶 | 路径格式 |
|----------|--------|----------|
| 上传文本文件 | `source-data` | `text/{年月}/{source_id}.txt` |
| 上传图片 | `source-data` | `image/{年月}/{source_id}.jpg` |
| 上传视频 | `source-data` | `video/{年月}/{source_id}.mp4` |
| 视频帧图 | `source-data` | `frames/{source_id}/{upload_index}.jpg` |
| 视频转译文本 | `source-data` | `video-text/{source_id}/{timestamp}.txt` |
| 图像/帧 bbox 裁剪图 | `source-data` | `crops/{task_id}/{item_index}.jpg` |
| 导出 JSONL 文件 | `finetune-export` | `export/{batchUuid}.jsonl` |
---
## 配置模型
### config.yaml非敏感提交 git
```yaml
server:
port: 8000
log_level: INFO
storage:
buckets:
source_data: "source-data"
finetune_export: "finetune-export"
backend: {} # callback_url 由 .env 注入
video:
frame_sample_count: 8 # 视频转文本时均匀采样帧数
max_file_size_mb: 200 # 视频大小上限(可通过 MAX_VIDEO_SIZE_MB 覆盖)
models:
default_text: "glm-4-flash"
default_vision: "glm-4v-flash"
```
### 环境变量覆盖映射
| 环境变量 | YAML 路径 | 说明 |
|----------|-----------|------|
| ZHIPUAI_API_KEY | zhipuai.api_key | 必填 |
| STORAGE_ACCESS_KEY | storage.access_key | 必填 |
| STORAGE_SECRET_KEY | storage.secret_key | 必填 |
| STORAGE_ENDPOINT | storage.endpoint | RustFS 地址 |
| BACKEND_CALLBACK_URL | backend.callback_url | Java 后端回调接口 |
| LOG_LEVEL | server.log_level | 日志级别 |
| MAX_VIDEO_SIZE_MB | video.max_file_size_mb | 视频大小上限 |

View File

@@ -0,0 +1,120 @@
# Implementation Plan: AI 服务需求文档
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/001-ai-service-requirements/spec.md`
> **参考实现计划(主计划)**: `docs/superpowers/plans/2026-04-10-ai-service-impl.md`
> 本文件为 speckit 规划框架文档,详细 TDD 任务17 个步骤含完整代码)见上述主计划。
## Summary
实现一个独立部署的 Python FastAPI AI 服务,为知识图谱标注平台提供文本三元组提取、图像四元组提取、视频帧处理、问答对生成和 GLM 微调管理能力。服务通过 RustFS S3 API 读写文件,通过 ZhipuAI GLM API 调用大模型,通过回调接口通知 Java 后端异步任务结果。采用 ABC 适配层LLMClient / StorageClient保证可扩展性FastAPI BackgroundTasks 处理视频长任务,全量 TDD 开发。
## Technical Context
**Language/Version**: Python 3.12.13conda `label` 环境)
**Primary Dependencies**: FastAPI ≥0.111, uvicorn[standard] ≥0.29, pydantic ≥2.7, zhipuai ≥2.1, boto3 ≥1.34, pdfplumber ≥0.11, python-docx ≥1.1, opencv-python-headless ≥4.9, numpy ≥1.26, httpx ≥0.27, python-dotenv ≥1.0, pyyaml ≥6.0
**Storage**: RustFSS3 兼容协议boto3 访问)
**Testing**: pytest ≥8.0 + pytest-asyncio ≥0.23,所有 service 和 router 均有单元测试
**Target Platform**: Linux 容器Docker + Docker Compose
**Project Type**: web-service
**Performance Goals**: 文本提取 <60s图像提取 <30s视频任务接受 <1s健康检查 <1sQA 生成(≤10条<90s
**Constraints**: 视频文件大小上限默认 200MB可通过 MAX_VIDEO_SIZE_MB 环境变量配置不访问数据库GLM 为云端 API图片须以 base64 传输ZhipuAI SDK 同步阻塞须在线程池中执行
**Scale/Scope**: 低并发ADMIN 手动触发同时不超过 5 个视频任务
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
> 项目 constitution 为未填充的模板,无项目特定约束规则。以下采用通用工程原则进行评估。
| 原则 | 状态 | 说明 |
|------|------|------|
| 测试优先TDD | 通过 | 实现计划采用红绿重构循环所有模块先写测试 |
| 简单性YAGNI | 通过 | BackgroundTasks 而非 Celery无数据库适配层仅当前实现 |
| 可观测性 | 通过 | JSON 结构化日志含请求/GLM/视频任务维度 |
| 错误分类 | 通过 | 4 种异常类400/502/503/500结构化响应 |
| 可扩展性 | 通过 | LLMClient / StorageClient ABC 适配层 |
| 配置分层 | 通过 | config.yaml + .env + 环境变量覆盖 |
**GATE RESULT**: 无违规可进入 Phase 0
## Project Structure
### Documentation (this feature)
```text
specs/001-ai-service-requirements/
├── plan.md # 本文件 (/speckit.plan 输出)
├── research.md # Phase 0 输出
├── data-model.md # Phase 1 输出
├── quickstart.md # Phase 1 输出
├── contracts/ # Phase 1 输出
│ └── api.md
└── tasks.md # Phase 2 输出 (/speckit.tasks - 未由本命令创建)
```
### Source Code (repository root)
```text
label_ai_service/
├── app/
│ ├── main.py # FastAPI 应用入口lifespan/health 端点
│ ├── core/
│ │ ├── config.py # YAML + .env 分层配置lru_cache 单例
│ │ ├── logging.py # JSON 结构化日志,请求日志中间件
│ │ ├── exceptions.py # 自定义异常 + 全局处理器
│ │ ├── json_utils.py # GLM 响应 JSON 解析(兼容 Markdown 代码块)
│ │ └── dependencies.py # FastAPI Depends 工厂函数
│ ├── clients/
│ │ ├── llm/
│ │ │ ├── base.py # LLMClient ABCchat / chat_vision
│ │ │ └── zhipuai_client.py # ZhipuAI 实现(线程池包装同步 SDK
│ │ └── storage/
│ │ ├── base.py # StorageClient ABCdownload/upload/presigned/size
│ │ └── rustfs_client.py # RustFS S3 兼容实现
│ ├── services/
│ │ ├── text_service.py # TXT/PDF/DOCX 解析 + 三元组提取
│ │ ├── image_service.py # 四元组提取 + bbox 裁剪
│ │ ├── video_service.py # 帧提取 + 视频转文本BackgroundTask
│ │ ├── qa_service.py # 文本/图像问答对生成(图像用 base64
│ │ └── finetune_service.py # 微调任务提交与查询
│ ├── routers/
│ │ ├── text.py # POST /api/v1/text/extract
│ │ ├── image.py # POST /api/v1/image/extract
│ │ ├── video.py # POST /api/v1/video/extract-frames, /to-text
│ │ ├── qa.py # POST /api/v1/qa/gen-text, /gen-image
│ │ └── finetune.py # POST /api/v1/finetune/start, GET /status/{id}
│ └── models/
│ ├── text_models.py
│ ├── image_models.py
│ ├── video_models.py
│ ├── qa_models.py
│ └── finetune_models.py
├── tests/
│ ├── conftest.py # mock_llm, mock_storage fixtures
│ ├── test_config.py
│ ├── test_llm_client.py
│ ├── test_storage_client.py
│ ├── test_text_service.py
│ ├── test_text_router.py
│ ├── test_image_service.py
│ ├── test_image_router.py
│ ├── test_video_service.py
│ ├── test_video_router.py
│ ├── test_qa_service.py
│ ├── test_qa_router.py
│ ├── test_finetune_service.py
│ └── test_finetune_router.py
├── config.yaml
├── .env
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
```
**Structure Decision**: 单项目结构Option 1分层为 routers services clients测试与源码并列
## Complexity Tracking
> Constitution 无违规,此节无需填写。

View File

@@ -0,0 +1,109 @@
# Quickstart: AI 服务开发指南
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
---
## 环境准备
```bash
# 激活 conda 环境
conda activate label
# 安装依赖(在 label_ai_service 目录下)
pip install -r requirements.txt
```
---
## 本地开发启动
```bash
# 1. 复制并配置 .env已提交模板
# 编辑 .env 填写真实的 ZHIPUAI_API_KEY 和 STORAGE_ENDPOINT
# 2. 启动开发服务器
conda run -n label uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 3. 访问 Swagger 文档
# http://localhost:8000/docs
```
---
## 运行测试
```bash
# 运行全部测试
conda run -n label pytest tests/ -v
# 运行指定模块测试
conda run -n label pytest tests/test_text_service.py -v
# 运行带覆盖率报告
conda run -n label pytest tests/ --cov=app --cov-report=term-missing
```
---
## Docker 部署
```bash
# 构建镜像
docker build -t label-ai-service:dev .
# 使用 docker-compose 启动(含 RustFS
docker-compose up -d
# 查看日志
docker-compose logs -f ai-service
# 健康检查
curl http://localhost:8000/health
```
---
## 关键配置说明
### 视频大小上限调整
无需重建镜像,在 `.env` 中添加:
```ini
MAX_VIDEO_SIZE_MB=500
```
### 切换大模型
修改 `config.yaml`
```yaml
models:
default_text: "glm-4-flash" # 文本模型
default_vision: "glm-4v-flash" # 视觉模型
```
---
## 开发流程TDD
详细的 17 个任务步骤(含完整代码)见主实现计划:
`docs/superpowers/plans/2026-04-10-ai-service-impl.md`
每个任务的开发步骤:
1. 编写失败测试(`pytest ... -v` 验证失败)
2. 实现最小代码使测试通过(`pytest ... -v` 验证通过)
3. Commit
---
## 目录结构速查
```
app/
├── main.py # 入口,/health 端点,路由注册
├── core/ # 配置、日志、异常、工具
├── clients/ # LLM 和 Storage 适配层ABC + 实现)
├── services/ # 业务逻辑text/image/video/qa/finetune
├── routers/ # HTTP 路由处理
└── models/ # Pydantic 请求/响应 Schema
```

View File

@@ -0,0 +1,76 @@
# Research: AI 服务实现方案
**Branch**: `001-ai-service-requirements` | **Date**: 2026-04-10
**Status**: 完成(所有决策已在设计阶段确定,无待研究项)
---
## 决策记录
### D-001: 异步框架选型
**Decision**: FastAPI + uvicorn
**Rationale**: 原生 async/await 支持、Pydantic 自动校验、自动生成 Swagger 文档、Python 生态系中性能和开发效率的最优权衡。
**Alternatives considered**: Django过重、Flask无原生异步、aiohttp无自动文档和类型校验
---
### D-002: ZhipuAI SDK 调用方式
**Decision**: 同步 SDK 通过 `asyncio.get_event_loop().run_in_executor(None, ...)` 在线程池中调用
**Rationale**: ZhipuAI 官方 SDK 为同步阻塞设计,直接在 async 函数中调用会阻塞事件循环。`run_in_executor` 将阻塞调用卸载到线程池,保持 FastAPI 事件循环响应能力。
**Alternatives considered**: 使用 `asyncio.to_thread()`Python 3.9+ 语法糖,等效实现,选择 run_in_executor 保持向后兼容性);使用 httpx 直接调用 ZhipuAI HTTP API绕过 SDK 但增加维护负担)
---
### D-003: 图像 QA 生成的图片传输方式
**Decision**: base64 编码嵌入消息体(`data:image/jpeg;base64,...`
**Rationale**: RustFS 部署在 Docker 内网endpoint: `http://rustfs:9000`presigned URL 指向内网地址,云端 GLM-4V 无法访问。base64 编码将图片内容直接内联到 API 请求,不依赖网络可达性。
**Alternatives considered**: presigned URL不可行内网地址云端不可达公网 RustFS 暴露(增加安全风险)
---
### D-004: 视频长任务处理机制
**Decision**: FastAPI BackgroundTasks + HTTP 回调通知 Java 后端
**Rationale**: 视频处理耗时不可控几秒到几分钟同步等待会超时。BackgroundTasks 无需额外中间件Redis/Celery部署简单任务状态通过回调接口由 Java 后端管理符合整体架构风格。并发量有限≤5个同时任务BackgroundTasks 完全够用。
**Alternatives considered**: Celery需 Redis broker引入额外运维负担asyncio.create_task进程重启会丢失任务
---
### D-005: 分层配置方案
**Decision**: config.yaml稳定非敏感配置+ .env密钥和环境差异项环境变量优先级高于 YAML
**Rationale**: YAML 提供结构化可读性,适合 git 追踪非敏感配置变更;.env 格式为 Docker `env_file` 原生支持;环境变量覆盖机制使容器部署时无需重建镜像即可切换配置。
**Alternatives considered**: 纯 .env 文件(缺乏结构化,复杂配置难维护);数据库存储配置(过重)
---
### D-006: 视频大文件 OOM 防护
**Decision**: 在视频路由层(接受请求后、启动后台任务前)通过 `storage.get_object_size()` 查询文件大小,超限返回 HTTP 400
**Rationale**: 在下载前拒绝,避免实际 OOM大小限制通过 config.yaml + MAX_VIDEO_SIZE_MB 环境变量运行时可配置,无需重建镜像;实现简单,无需引入流式下载的新抽象。
**Alternatives considered**: 流式下载Completeness: 9/10但 YAGNI当前规模不需要不限制Completeness: 4/10有 OOM 风险)
---
### D-007: 视频关键帧检测算法
**Decision**: 帧差分frame difference近似检测计算当前帧与前帧灰度图的像素差均值差值超过阈值默认 30.0)判定为场景切换
**Rationale**: OpenCV 无原生 I 帧检测 API`CAP_PROP_POS_FRAMES` 是帧定位,非 I 帧标识)。帧差分简单有效,对场景切换检测准确,且无需视频解码器底层支持。
**Alternatives considered**: 基于编码信息的 I 帧检测(需 FFmpeg 支持,引入额外依赖);固定间隔(不够智能,不适合关键帧模式)
---
### D-008: 测试策略
**Decision**: pytest + pytest-asyncioService 层和 Router 层分别测试,使用 AsyncMock 模拟外部依赖
**Rationale**: Service 层测试业务逻辑,不依赖 HTTPRouter 层使用 TestClient 测试完整请求流程。视频 service 测试使用真实小视频文件OpenCV VideoWriter 生成),验证帧提取逻辑正确性。
**Alternatives considered**: 仅集成测试(需要真实 RustFS 和 ZhipuAICI 成本高);全部单元测试(无法覆盖路由和异常处理器集成)
---
## 无待解决项
所有 NEEDS CLARIFICATION 均已在设计阶段通过用户确认或合理默认值解决。本 research.md 仅作决策存档。