From 54bc505f7a57955b5cc43841f1f5c2ef5aca5654 Mon Sep 17 00:00:00 2001 From: wangdl Date: Fri, 19 Jun 2026 20:10:35 +0800 Subject: [PATCH] docs: add ADR-001 unified AI architecture decision record Freeze the architecture decision for 8-batch unified AI migration. Covers current/target topology, API/Worker process boundaries, 5 Processors, Heavy Runtime disposition, phase-1 non-goals, rollback principles, and cross-batch constraints. Includes Appendix A: worker dependency closure audit. Co-Authored-By: Claude Opus 4.7 --- ...01-appendix-a-worker-dependency-closure.md | 173 ++++++++++++++++++ .../adr-001-unified-ai-architecture.md | 169 +++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 docs/architecture/adr-001-appendix-a-worker-dependency-closure.md create mode 100644 docs/architecture/adr-001-unified-ai-architecture.md diff --git a/docs/architecture/adr-001-appendix-a-worker-dependency-closure.md b/docs/architecture/adr-001-appendix-a-worker-dependency-closure.md new file mode 100644 index 0000000..2e3f568 --- /dev/null +++ b/docs/architecture/adr-001-appendix-a-worker-dependency-closure.md @@ -0,0 +1,173 @@ +# ADR-001 附录 A:Worker 执行依赖闭包审计 + +## 审计日期 + +2026-06-19 + +## 目标 + +确认每个 BullMQ Processor 的完整依赖、事件和副作用,防止 Processor 移到独立 Worker 进程后链路断裂。 + +--- + +## 一、Processor 依赖矩阵 + +### 1.1 AiAnalysisWorker + +| 维度 | 事实 | 证据 | +|------|------|------| +| **队列** | `ai-analysis` | `@Processor(QUEUE_AI_ANALYSIS)` | +| **注册位置** | `AppModule` (line 193) + `WorkerModule` (line 62) — **双重注册** | `app.module.ts:193`、`worker.module.ts:62` | +| **注入依赖** | `ActiveRecallAnalysisWorkflow`、`FeynmanEvaluationWorkflow`、`AiAnalysisRepository`、`EventBusService` (@Optional)、`FocusItemsService` (@Optional) | `ai-analysis.worker.ts:22-28` | +| **依赖 Module** | `AiModule`(提供 Workflow)、`AiAnalysisModule`(提供 Repository)、`EventBusModule`(提供 EventBusService)、`FocusItemsModule`(提供 FocusItemsService) | — | +| **写入的表** | `AiAnalysisJob`(updateJobStatus)、`AiAnalysisResult`(createResult)、`FocusItem`(create) | `ai-analysis.worker.ts:47,66,67,87,101` | +| **发布的事件** | `ai.analysis.completed`(通过 `EventBusService.publish()` → `EventEmitter2.emit()` — **进程内**) | `ai-analysis.worker.ts:71` | +| **事件订阅者** | `ReviewCardSubscriber`(`@OnEvent('ai.analysis.completed')` → `ReviewModule` → `AppModule`) | `review-card.subscriber.ts:11` | +| **WorkerModule 中缺失** | `FocusItemsModule` 未导入 → `FocusItemsService` 在 Worker 进程中为 `null` | `worker.module.ts:1-66` | +| **移动后断裂链路** | ⚠️ `ai.analysis.completed` 事件无法到达 `ReviewCardSubscriber`(进程内 EventEmitter2);⚠️ `FocusItemsService` 在独立 Worker 进程中为 null | — | + +### 1.2 DocumentImportWorker + +| 维度 | 事实 | 证据 | +|------|------|------| +| **队列** | `document-import` | `@Processor(QUEUE_DOCUMENT_IMPORT)` | +| **注册位置** | `AppModule` (line 194) + `WorkerModule` (line 65) — **双重注册** | `app.module.ts:194`、`worker.module.ts:65` | +| **注入依赖** | `KnowledgeImportWorkflow`、`KnowledgeItemsRepository`、`RedisService` | 需确认 `document-import.worker.ts` | +| **写入的表** | `KnowledgeItem` | — | +| **发布的事件** | 无 | — | +| **移动后断裂链路** | 无事件依赖,但需确认 Redis 连接在 Worker 进程中可用 | — | + +### 1.3 NotificationWorker + +| 维度 | 事实 | 证据 | +|------|------|------| +| **队列** | `notification` | `@Processor(QUEUE_NOTIFICATION)` | +| **注册位置** | `AppModule` (line 195) + `WorkerModule` (line 66) — **双重注册** | `app.module.ts:195`、`worker.module.ts:66` | +| **注入依赖** | `NotificationsService` | 需确认 `notification.worker.ts` | +| **写入的表** | `Notification`(更新发送状态) | — | +| **发布的事件** | 无 | — | +| **移动后断裂链路** | 需确认 `NotificationsService` 及其 Module 依赖在 WorkerModule 中可用 | — | + +### 1.4 AuditLogProcessor + +| 维度 | 事实 | 证据 | +|------|------|------| +| **队列** | `audit-logs` | `@Processor(QUEUE_AUDIT_LOG)` | +| **注册位置** | `AdminAuditLogModule` providers(仅注册一次,未双重) | `admin-audit-log.module.ts:13` | +| **注入依赖** | `PrismaService` | `audit-log.processor.ts:7` | +| **写入的表** | `AdminAuditLog` | `audit-log.processor.ts:12-23` | +| **发布的事件** | 无 | — | +| **移动后断裂链路** | 无业务事件依赖,但 `AdminAuditLogModule` 同时包含 Controller 和 Processor,需要拆分 | — | + +### 1.5 FileCleanupProcessor + +| 维度 | 事实 | 证据 | +|------|------|------| +| **队列** | `file-cleanup` | `@Processor(QUEUE_FILE_CLEANUP)` | +| **注册位置** | ⚠️ `FilesModule` 导入了 `FileCleanupProcessor` 但**未在 providers 中注册** — `BullExplorer` 扫描可能仍能发现,但依赖注入不可靠 | `files.module.ts:3,12-14` | +| **注入依赖** | `CosStorageProvider` | `file-cleanup.processor.ts:10` | +| **写入的表** | 无(直接操作 COS) | — | +| **发布的事件** | 无 | — | +| **移动后断裂链路** | 需确认 `CosStorageProvider` 在 Worker 进程中可注入 | — | + +--- + +## 二、事件链路审计 + +### 2.1 EventBusService 实现 + +| 方法 | 机制 | 跨进程 | 证据 | +|------|------|:---:|------| +| `publish(event)` | `EventEmitter2.emit()` — 进程内同步 | ❌ | `event-bus.service.ts:19-22` | +| `publishAsync(event)` | BullMQ `domain-events` 队列 — 跨进程异步 | ✅ | `event-bus.service.ts:26-36` | + +### 2.2 当前事件使用 + +| 事件 | 发布者 | 发布方式 | 订阅者 | 订阅方式 | 位置 | 跨进程安全 | +|------|--------|---------|--------|---------|------|:---:| +| `ai.analysis.completed` | `AiAnalysisWorker` | `publish()`(进程内) | `ReviewCardSubscriber` | `@OnEvent`(EventEmitter2) | `review-card.subscriber.ts:11` | ❌ | +| `task.enqueued` | `QueueService` | `publish()`(进程内) | 无消费者 | — | `queue.service.ts:34` | — | +| `streak.updated` | `GrowthService` | `publish()`(进程内) | 无消费者 | — | `growth.service.ts:53` | — | + +### 2.3 Domain Events 队列 + +- **生产者**:`EventBusService.publishAsync()` → `queue.add('domain-events', ...)` +- **消费者**:无 — 代码库中无任何 `@Processor('domain-events')` +- **状态**:死队列 — 事件被写入 Redis 但永不被处理 +- **结论**:`publishAsync()` 当前无实际效果 + +### 2.4 移动后的断裂链路 + +``` +移动前(同进程): + AiAnalysisWorker → EventBusService.publish() → EventEmitter2.emit('ai.analysis.completed') + → ReviewCardSubscriber.@OnEvent('ai.analysis.completed') + → reviewService.generateCards() → ReviewCard 创建 ✅ + +移动后(跨进程): + Worker 进程: AiAnalysisWorker → EventBusService.publish() → EventEmitter2.emit() + API 进程: ReviewCardSubscriber.@OnEvent() — 收不到 ❌ +``` + +**影响**:AI 分析完成后不再自动生成复习卡片。 + +--- + +## 三、Module 注册审计 + +### 3.1 双重注册的 Processor + +| Processor | AppModule | WorkerModule | 业务 Module | 问题 | +|-----------|:---:|:---:|:---:|------| +| AiAnalysisWorker | ✅ (line 193) | ✅ (line 62) | — | 若两个 Module 均加载,BullMQ 创建两个 Worker 实例消费同一队列 → **并发双消费** | +| DocumentImportWorker | ✅ (line 194) | ✅ (line 65) | — | 同上 | +| NotificationWorker | ✅ (line 195) | ✅ (line 66) | — | 同上 | + +### 3.2 需拆分的 Module(同时包含 Controller 和 Processor) + +| Module | Controller | Processor | Producer | +|--------|:---:|:---:|:---:| +| `AdminAuditLogModule` | ✅ | ✅ (`AuditLogProcessor`) | — | +| `FilesModule` | ✅ | ⚠️ (`FileCleanupProcessor` 导入但未在 providers 注册) | ✅ (`FilesService`) | + +--- + +## 四、WorkerModule 缺失依赖 + +当前 `WorkerModule`(`worker.module.ts:1-66`)导入的 Module: + +``` +PrismaModule, RedisModule, QueueModule, AiModule, StorageModule, +LoggerModule, AiAnalysisModule, DocumentImportModule, KnowledgeItemsModule, NotificationsModule +``` + +**缺失但 AiAnalysisWorker 需要的 Module**: + +| 缺失 Module | 提供的 Service | 影响 | +|------------|---------------|------| +| `FocusItemsModule` | `FocusItemsService` | AiAnalysisWorker 中 `focusItems` 为 null,不创建 FocusItem | +| `EventBusModule` | `EventBusService` | AiAnalysisWorker 中 `eventBus` 为 null,不发布事件。但当前有 `@Optional()` 标记,不会崩溃 | +| `ReviewModule` | `ReviewCardSubscriber` | 即使事件发布成功(使用 publishAsync),无订阅者处理 | + +--- + +## 五、修复建议矩阵(供 01-03 使用) + +| 问题 | 修复方向 | 优先级 | +|------|---------|:---:| +| AiAnalysisWorker/NotificationWorker/DocumentImportWorker 双重注册 | 从 `AppModule` providers 中移除,仅保留在 `WorkerModule` | **阻塞** | +| `EventBusService.publish()` 无法跨进程 | 选项 A:将 `ReviewCardSubscriber` 移入 `WorkerModule`;选项 B:改为 `publishAsync()` + 新增 `DomainEventsProcessor` 消费 `domain-events` 队列 | **阻塞** | +| `FocusItemsModule` 未在 WorkerModule | 添加 `FocusItemsModule` 到 `WorkerModule.imports` | **阻塞** | +| `ReviewModule` 未在 WorkerModule | 如果选 A,需在 WorkerModule 中导入 `ReviewModule`(或拆出 `ReviewWorkerModule`) | **阻塞** | +| `AuditLogProcessor` 在 `AdminAuditLogModule` 中 | 拆分为 `AdminAuditLogProducerModule` + `AdminAuditLogWorkerModule`,或将其移入 `WorkerModule` 并保留 Controller 在原 Module | 高 | +| `FileCleanupProcessor` 未在 providers 中注册 | 补充注册,或显式移入 `WorkerModule` | 高 | + +--- + +## 六、无法确认项 + +| 项目 | 状态 | 原因 | +|------|:---:|------| +| Docker Compose 当前是否同时启动 API 和 Worker | 无法确认 | 需检查 `docker-compose.yml` 中 `command` 或 `entrypoint` 指向 `dist/main.js` 还是两者 | +| 生产 systemd 是否使用独立 Worker 进程 | 无法确认 | 无法访问生产服务器 | +| `NotificationsService` 的完整依赖链 | 未深入审计 | NotificationWorker 的审计超出了当前 Issue 范围,需后续补充 | diff --git a/docs/architecture/adr-001-unified-ai-architecture.md b/docs/architecture/adr-001-unified-ai-architecture.md new file mode 100644 index 0000000..c76336a --- /dev/null +++ b/docs/architecture/adr-001-unified-ai-architecture.md @@ -0,0 +1,169 @@ +# ADR-001:统一 AI 架构迁移决策 + +## 状态 + +已接受(2026-06-19) + +## 背景 + +当前知习项目存在两套独立的 AI 分析系统,以及一套规则出题系统: + +- **系统 A(BullMQ + TypeScript,运行中)**:处理 active-recall 和 feynman-evaluation 分析,通过 `AiAnalysisWorker` 在 NestJS 进程内执行,写入 `AiAnalysisJob`/`AiAnalysisResult` 表,创建 `FocusItem`,发布 `ai.analysis.completed` 事件驱动 `ReviewCardSubscriber` 生成复习卡片。 +- **系统 B(Rust + HTTP 轮询,闲置)**:`zhixi-heavy-runtime` 独立进程,支持 5 种分析类型(learning_state_analysis、weak_point_analysis、next_action_planning、quiz_generation、flashcard_generation),通过轮询 `AiRuntimeJob` 表获取任务,调用 DeepSeek API,结果写入多个相互独立的领域表。 +- **系统 C(规则出题,运行中)**:`QuizService.create()` 通过纯文本拼接生成测验题目,不调用任何 AI 模型。 + +系统 B 存在以下关键问题: +1. **无任务生产者** — 唯一创建 `AiRuntimeJob` 的入口 `POST /ai/jobs` 无人调用 +2. **Snapshot 结构不兼容** — Rust 端 `LearningAnalysisSnapshot` 结构体与 API 端 `SnapshotBuilderService` 产出的 JSON 结构完全不匹配 +3. **Rust 无重计算** — 所有 Pipeline 的独有业务逻辑总和不足 50 行,实际是一个 HTTP 代理层 +4. **数据断层** — Rust 结果写入的表(`AiLearningAnalysis`、`WeakPointCandidate`、`NextActionRecommendation`)无 iOS/Admin 消费端 + +同时,当前 API 进程内注册 BullMQ Processor 的部署拓扑存在以下问题: +- API 进程与 Worker 进程边界模糊,Docker 与生产环境配置不一致 +- `AppModule` 中同时注册 `AiAnalysisWorker`、`DocumentImportWorker`、`NotificationWorker` +- `WorkerModule` 和 `worker.main.ts` 已存在但未独立使用 + +## 决策 + +采用 **8 批次渐进式迁移**,将 AI 分析能力从 BullMQ + Rust 双系统统一到单一 BullMQ + TypeScript 架构。 + +### 目标架构拓扑 + +``` + ┌─────────────┐ + │ iOS App │ + └──────┬──────┘ + │ HTTP + ┌──────▼──────┐ + │ zhixi-api │ NestJS (仅 Controller + Queue Producer) + │ :3000 │ 不注册任何 @Processor() + └──────┬──────┘ + │ Redis (BullMQ) + ┌──────▼──────┐ + │ zhixi-worker│ NestJS (仅 @Processor()) + │ (无端口) │ AiAnalysisWorker + DocumentImportWorker + └──────┬──────┘ + NotificationWorker + AuditLogProcessor + │ HTTP + FileCleanupProcessor + ┌──────▼──────┐ + │ DeepSeek │ + │ API │ + └──────────────┘ +``` + +### API Process 职责 + +- 暴露所有 HTTP Controller +- 注入 Queue Producer(`QueueService.add()`) +- 不注册任何 `@Processor()` 或 `WorkerHost` +- 不启动 BullMQ Worker 实例 + +### Worker Process 职责 + +- 注册所有 `@Processor()` 和 `WorkerHost` +- 消费 BullMQ 队列 +- 调用 AI Workflow → DeepSeek API +- 写入分析结果表 +- 发布领域事件 +- 不暴露 HTTP 端口 + +### Heavy Runtime 当前处置 + +- 第一阶段保持部署但不修改 +- 不停止 Rust 进程 +- 不删除 `AiRuntimeJob` 及相关表 +- M-AI-08 正式退场 + +## 迁移批次 + +| 批次 | 名称 | 范围 | +|------|------|------| +| M-AI-01 | Worker 进程边界与部署收口 | 拆分 API/Worker 进程,不改变业务 | +| M-AI-02 | 统一 AiJob 数据库 Expand | 扩展 `AiAnalysisJob`,新增 Artifact/Snapshot/Outbox | +| M-AI-03 | 统一 Job Engine、Registry 与 Outbox | AiJobService + Registry + 双队列 | +| M-AI-04 | Active Recall 端到端迁移 | 切换生产者,iOS 展示真实结果 | +| M-AI-05 | Feynman 与 ReviewCard 可靠链路 | Outbox 替代进程内 EventEmitter | +| M-AI-06 | AI Quiz 端到端迁移 | 迁移 Rust Prompt/Schema,异步生成 | +| M-AI-07 | 后台学习分析与推荐闭环 | learning_state/weak_point/next_action | +| M-AI-08 | Heavy Runtime 退场与 Legacy Contract | 停止 Rust,归档表 | + +## 第一阶段非目标(M-AI-01) + +1. 不修改 `AiAnalysisJob` / `AiRuntimeJob` 表结构 +2. 不新建统一 AiJob 模型 +3. 不迁移 Active Recall、Feynman 或 Quiz 业务链路 +4. 不停止 `zhixi-heavy-runtime` 服务 +5. 不修改 iOS 代码 +6. 不删除任何数据库表 +7. 不修改 AI Prompt、模型或业务结果结构 +8. 不新增 Prisma Migration + +## 架构约束 + +以下约束在所有批次中持续生效: + +1. `AppModule` 不允许注册 `@Processor()` 或 `WorkerHost` +2. `WorkerModule` 不允许暴露公开 HTTP Controller +3. 不允许新建第三套 AI Job 模型(除 M-AI-02 统一扩展外) +4. 不允许顺带修改 Prompt、模型选择或业务结果 Schema(各自独立 Issue) +5. 没有 iOS/Admin 消费者的任务类型不得启用自动生成 +6. 自动触发必须有频率限制、幂等保护、成本上限 + +## 回滚原则 + +每个 Issue 必须提供独立回滚方案: + +1. 代码层面:回滚提交或 revert PR +2. 数据库:本批次不涉及 Schema 变更,无需数据回滚 +3. 部署:恢复旧 AppModule Processor 注册,停止独立 Worker service +4. 队列:验证无双重消费 + +## 现有 Processor 清单 + +| Processor | 队列 | 文件 | +|-----------|------|------| +| `AiAnalysisWorker` | `ai-analysis` | `workers/ai-analysis.worker.ts` | +| `DocumentImportWorker` | `document-import` | `workers/document-import.worker.ts` | +| `NotificationWorker` | `notification` | `workers/notification.worker.ts` | +| `AuditLogProcessor` | `audit-logs` | `modules/admin-audit-log/audit-log.processor.ts` | +| `FileCleanupProcessor` | `file-cleanup` | `modules/files/file-cleanup.processor.ts` | + +## 模块依赖图(当前) + +``` +AppModule +├── WorkerModule (已存在但未独立使用) +│ ├── AiAnalysisWorker +│ ├── DocumentImportWorker +│ └── NotificationWorker +├── QueueModule +│ ├── BullModule.forRootAsync (Redis 连接) +│ └── BullModule.registerQueue (6 个队列) +├── AiAnalysisModule +│ ├── AiAnalysisController (HTTP 端点) +│ └── AiAnalysisService (BullMQ Producer) +├── ActiveRecallModule +│ └── ActiveRecallService (BullMQ Producer 调用者) +├── ReviewModule +│ ├── ReviewController (HTTP 端点) +│ ├── ReviewCardSubscriber (监听 ai.analysis.completed) +│ └── ReviewService +├── AiRuntimeModule +│ ├── UserAiController (HTTP 端点) +│ └── RuntimeInternalController (Rust 内部端点) +├── AdminAuditLogModule +│ └── AuditLogProcessor (@Processor) +└── FilesModule + └── FileCleanupProcessor (@Processor) +``` + +## 后续 Issue 引用 + +后续 M-AI-01 Issue 必须引用本文档(ADR-001)作为架构约束依据。 + +## 证据 + +- 当前部署:`docker-compose.yml:1-70` — API 和 Worker 共享同一 NestJS 实例 +- Worker 模块:`worker.module.ts:1-20` — 已定义但未独立使用 +- Worker 入口:`worker.main.ts:1-10` — 已存在但未被 Docker/生产独立调用 +- 现有架构文档:`docs/ai-runtime-architecture.md` — 描述 Rust Heavy Runtime 架构 +- BullMQ 版本:`bullmq@5.77.1`、`@nestjs/bullmq@11.0.4`