# AI Runtime 总体架构

## 1. 概述

M-API-AI-RUNTIME 里程碑新增 Rust Heavy Runtime 作为内部重任务执行器，完成 DeepSeek 调用、学习状态分析、题目/卡片候选生成。主 API 保持业务权威层地位不变。

### 架构图

```
iOS / Admin / Web
       ↓
  主 API（NestJS）
       ↓
  AiRuntimeJob / LearningAnalysisSnapshot
       ↓
  Rust Heavy Runtime（内部 HTTP）
       ↓
  DeepSeek API
       ↓
  Rust Heavy Runtime 输出结构化结果
       ↓
  主 API 二次校验 / 落库
       ↓
  iOS 展示 / Admin 诊断
```

## 2. 职责边界

### 2.1 API（本仓库）

| 负责 | 不负责 |
|------|--------|
| 用户权限与会员额度 | 直接调用 DeepSeek |
| 学习数据读取与聚合 | Prompt 渲染 |
| 用户 DeepSeek key 加密存储 | 模型输出校验 |
| AI Job 创建与调度 | 任务消费 |
| Snapshot 构建 | 结构化 JSON 解析 |
| Runtime 内部接口提供 | 题目/卡片生成 |
| Runtime 结果校验与落库 | |
| iOS / Admin 对外 API | |
| 平台预算与熔断 | |

### 2.2 Rust Heavy Runtime

| 负责 | 不负责 |
|------|--------|
| Job Worker 消费 | 用户登录与会员 |
| DeepSeek 调用 | 直接写业务主表 |
| Prompt 渲染 | 对公网暴露接口 |
| 结构化 JSON 校验 | 处理 readingTargetType |
| 题目/卡片候选生成 | 用户数据查询 |
| 调用日志回传 | |

### 2.3 Rust Document Runtime

| 负责 | 不负责 |
|------|--------|
| 文档解析与阅读状态 | readingTargetType |
| 阅读事件 V2 生成 | userId |
| EventBuffer | 上传 API |

### 2.4 iOS

| 负责 | 不负责 |
|------|--------|
| 阅读页 UI | 直接调 DeepSeek |
| heartbeat tick 控制 | 直接调 Rust Heavy Runtime |
| 补充 readingTargetType | |
| 本地上传队列 + ack | |

### 2.5 Admin

| 负责 | 不负责 |
|------|--------|
| 管理页面 | 直接调 Rust Runtime |
| 成本统计与诊断 | |

## 3. Docker 部署

```
┌─────────────────────── Docker internal network ───────────────────────┐
│                                                                        │
│  ┌─────────┐   ┌──────────────────┐   ┌──────────────────────┐       │
│  │  MySQL   │   │  api (NestJS)    │   │  heavy-runtime (Rust) │       │
│  │  :3306   │   │  :3000           │   │  :8080                │       │
│  │          │   │  → 公网暴露       │   │  → 不暴露公网          │       │
│  └─────────┘   └────────┬─────────┘   └──────────┬───────────┘       │
│                         │                        │                    │
│                         ├── 调 POST /internal/runtime/* ──→          │
│                         │                        │                    │
│                         │←── Runtime 调 internal API ──┤             │
│                                                                        │
└──────────────────────────────────────────────────────────────────────┘
```

### docker-compose 要点

```yaml
services:
  api:
    environment:
      RUNTIME_INTERNAL_BASE_URL: http://heavy-runtime:8080
      RUNTIME_SERVICE_TOKEN: ${RUNTIME_SERVICE_TOKEN}
    depends_on:
      - heavy-runtime
    networks:
      - zhixi-internal

  heavy-runtime:
    environment:
      API_INTERNAL_BASE_URL: http://api:3000
      RUNTIME_SERVICE_TOKEN: ${RUNTIME_SERVICE_TOKEN}
      DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
    expose:
      - "8080"
    networks:
      - zhixi-internal

networks:
  zhixi-internal:
    driver: bridge
```

关键约束：
- heavy-runtime 不映射公网端口
- 服务间通过 Docker 内部 DNS 发现（`http://heavy-runtime:8080`）
- service token 通过环境变量注入

## 4. 通信方式

### 4.1 主任务流：Job 异步

```
API 创建 AiRuntimeJob(status=pending)
  → Runtime poll /internal/runtime/jobs/poll
  → Runtime lock job
  → Runtime 获取 Snapshot + credential
  → Runtime 执行 DeepSeek 调用
  → Runtime 提交 result 到 /internal/runtime/jobs/{jobId}/result
  → API 校验并落库
```

### 4.2 内部接口

| 接口 | 调用方 | 说明 |
|------|--------|------|
| `POST /internal/runtime/jobs/poll` | Runtime | 拉取 pending job |
| `POST /internal/runtime/jobs/{jobId}/lock` | Runtime | 锁定 job |
| `POST /internal/runtime/jobs/{jobId}/heartbeat` | Runtime | 心跳续约 |
| `GET /internal/runtime/jobs/{jobId}/snapshot` | Runtime | 获取快照 |
| `POST /internal/runtime/model-credentials/resolve` | Runtime | 获取模型 key |
| `POST /internal/runtime/jobs/{jobId}/result` | Runtime | 提交成功结果 |
| `POST /internal/runtime/jobs/{jobId}/fail` | Runtime | 提交失败 |
| `POST /internal/runtime/invocation-logs` | Runtime | 提交调用日志 |
| `GET /internal/runtime/health` | API | Runtime 健康检查 |

### 4.3 鉴权

- 复用已有 `InternalAuthGuard`，使用 `x-internal-api-key` header
- 新增 `X-Runtime-Instance-Id` header 用于追踪
- 普通用户 JWT 不可访问 internal 接口
- service token 不可访问普通用户 API

## 5. AI Job 生命周期

### 5.1 状态机

```
pending ──→ locked ──→ running ──→ succeeded
  │          │           │
  │          │           └──→ failed (retryable) ──→ pending
  │          │           └──→ failed (non-retryable)
  │          │           └──→ expired
  │          │
  │          └──→ expired (lockUntil 超时)
  │
  └──→ cancelled
```

### 5.2 Job 类型

| 类型 | 说明 | 输出 |
|------|------|------|
| `learning_state_analysis` | 学习状态分析 | AiLearningAnalysis |
| `weak_point_analysis` | 薄弱点分析 | WeakPointCandidate[] |
| `next_action_planning` | 下一步建议 | NextActionRecommendation[] |
| `quiz_generation` | 题目生成 | QuizQuestion[] |
| `flashcard_generation` | 卡片生成 | Flashcard[] |

### 5.3 锁定与超时

```
lockUntil = now + 60s (推荐初始值)
heartbeat 延长 lockUntil
lockUntil 超时后其他 Runtime 可接管
maxRetryCount = 3（默认）
```

## 6. 用户 Key 与平台 Key

### 6.1 两种模式

| 模式 | Key 来源 | 消费方 |
|------|---------|--------|
| `platform_key` | 环境变量 `DEEPSEEK_API_KEY` 或 API 配置 | 平台预算 |
| `user_deepseek_key` | 用户在 iOS 填写，API 加密存储 | 用户自己 |

### 6.2 Key 流转

```
用户填 key → API 加密落库(UserModelCredential)
           → Runtime resolve → API 解密返回明文 → 仅内存使用 → 不写日志
           → ModelInvocationLog 只记录 credentialId，不记录 key
```

### 6.3 安全规则

- encryptedApiKey：AES-256-GCM 加密，密钥来自环境变量
- 明文 key 不落库、不进日志、不返回前端、Admin 不可查看
- maskedKey：如 `sk-****xxxx`

## 7. 失败与重试

### 7.1 retryable 错误

```
MODEL_TIMEOUT, MODEL_RATE_LIMIT, NETWORK_ERROR, TEMPORARY_PROVIDER_ERROR
→ retryable=true, job 回到 pending, retryCount+1
```

### 7.2 non-retryable 错误

```
INVALID_SNAPSHOT, INVALID_SCHEMA, INVALID_CREDENTIAL
→ retryable=false, job 直接 failed
```

### 7.3 熔断

连续失败 N 次 platform_key job → 平台熔断 open → 拒绝新的 platform_key job → half_open 后试探

### 7.4 取消

- pending job：用户/Admin 可直接取消
- running job：标记 cancelRequested，Runtime 下次 heartbeat 获知

## 8. 结果落库边界

### Runtime 提交 → API 校验 → 落业务表

```
Runtime submit result
  → API RuntimeOutputBusinessValidator 二次校验
  → 源数据归属校验 (sourceBlockIds, knowledgePointId)
  → 去重 (exact hash)
  → 写入对应业务表 (AiLearningAnalysis / QuizQuestion / Flashcard / ...）
  → 更新 job 状态 succeeded
```

Runtime 不直接写：AiLearningAnalysis, QuizQuestion, Flashcard, WeakPointCandidate, NextActionRecommendation, QuestionGenerationPlan, FlashcardGenerationPlan。这些只由 API 写。

## 9. 与现有 M1 RAG/Chat 的共存

1. 本批不迁移现有 M1 RAG。
2. 现有 Chat / RAG 问答链路（AiGatewayService → DeepSeek）继续运行。
3. 新 AI Runtime 只做：学习状态分析、题目/卡片候选生成。
4. 两者不共享 Job 队列。
5. 若 Chat 结果和 AI Analysis 结果同时存在，前端应区分展示：
   - 对话回答：来自 Chat/RAG，时效性高
   - 学习分析建议：来自 AI Runtime，基于 Snapshot 聚合
6. 后续 RAG 迁移需要独立里程碑。

## 10. 后续扩展预留

| 方向 | 本批 | 后续 |
|------|------|------|
| RAG 迁移 | ❌ | 独立里程碑 |
| 批量知识库分析 | ❌ (只做 user/material 级) | parentJob + childJob |
| 多模型供应商 | ❌ (只做 DeepSeek) | ModelProvider trait |
| A/B 测试 | ❌ | promptVersion 字段已预留 |
| Prompt 热更新 | ❌ (静态模板) | PromptRegistry |
| 用户自定义 baseUrl | ❌ | 需安全审计 |

## 11. 验收清单

- [ ] 文档明确 API 是业务权威层
- [ ] 文档明确 Runtime 是内部重任务执行器
- [ ] 文档明确 Runtime 不对公网暴露
- [ ] 文档明确 Docker 内部网络部署方式
- [ ] 文档明确本批不迁移 RAG
- [ ] 文档明确 AI Job 异步流程
- [ ] 文档明确 Runtime 结果最终由 API 校验和落库
- [ ] 文档明确后续 iOS / Admin 只访问 API，不访问 Runtime
- [ ] 文档明确后续 RAG 迁移只是预留
- [ ] 文档能指导 API-AI-001~072 的实现
