# 模块架构设计文档 — ai

> AI 标识：ai12（复核与长期演进接手）← ai06（初稿，2026-07-09）
> 负责模块：ai（P5）
> 阶段：架构设计外包 · 阶段 2（模块架构设计，ai06 初稿 + ai12 长期演进复核）
> 日期：2026-07-09（ai06 初稿）/ 2026-07-09（ai12 复核与补充）
> 关联文档：[阶段 1 理解确认书](./01-understanding.md)、[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[coord 交叉审查报告](../../../docs/architecture/coord-cross-review.md)
> 审查请求：本设计文档待 coord 按 ai-allocation.md §8 交叉审查（接口一致性 / 端口冲突 / Topic 重复 / 错误码重叠 / 黄金模板对齐）
>
> **ai12 接手说明**：本设计文档初稿由 ai06 在 ai-allocation v0.x 阶段撰写（涵盖 8 节标准模板）。ai12 在 ai06 初稿基础上执行：作者归属更正、长期架构演进补充（新增 §9-§17 共 9 节）、P5 交付物覆盖度核对、向前兼容性铺垫。**所有 ai12 新增内容用「ai12 增补」标注**，ai06 原文保留并标注「ai06 初稿」。

---

## 设计原则与全局约束

本设计遵循以下强制约束（来自 project_rules.md + coding-standards.md + 004）：

1. **契约先行**：proto 已定义（analytics.proto / ai.proto），实现前不修改 proto，如需修改走 coord 流程
2. **CQRS 读写分离**：ai 是无状态服务（无 DB 写），用量计费通过事件外发由 data-ana 落 ClickHouse
3. **事件驱动**：ai 不消费事件；仅发布 `AIUsageRecorded` 派生数据事件（004 §12.2 豁免 Outbox，允许直接 producer）
4. **gRPC 优先**：004 §4.1 + §4.2 明确 P5 ai 启用 gRPC server（含 StreamChat 流式 RPC），HTTP 保留作 Gateway 直连降级
5. **三支柱可观测**：structlog + prometheus-client + OpenTelemetry（已具备，需补业务指标 + gRPC interceptor）
6. **降级模式**：外部依赖（LLM / 下游 gRPC / Redis）不可用时返回骨架数据 + ActionState `details: {degraded: true}`
7. **Python 规范**：pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误
8. **响应信封**（ai12 增补）：004 §11.5 强制 ActionState，禁止 `{success, data, degraded}` 偏离结构
9. **端口规范**（ai12 增补）：HTTP 3008 + gRPC 50058（HTTP+10050 规则），提请 coord 在 004 §1.2 + `infra/port-allocation.md` 补登
10. **错误码前缀**（ai12 增补）：`AI_*`（004 §11.4 错误码前缀矩阵已确认 ai 前缀为 `AI_`）

---

## 1. 模块内部分层图

### 1.1 ai06 初稿：分层图

```mermaid
flowchart TB
    subgraph Entry["入口层"]
        HTTP[FastAPI HTTP Router<br/>/ai/* + /healthz + /readyz]
        GRPC[grpc.aio Server<br/>AiService 含 StreamChat]
    end

    subgraph Middleware["中间件层"]
        AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
        RATE[RateLimitDepends<br/>Redis 令牌桶限流]
        TRACE[OTel + grpc interceptor]
    end

    subgraph Service["应用服务层"]
        S1[ChatService<br/>聊天编排 + Prompt 模板渲染]
        S2[QuestionGenerationService<br/>出题工作流编排]
        S3[ExpressionOptimizationService<br/>表达优化]
        S4[LessonPreparationWorkflow<br/>备课工作流 4 步编排]
    end

    subgraph Provider["LLM Provider 适配层"]
        P0[LLMProvider 抽象接口<br/>chat / stream_chat]
        P1[OpenAIProvider<br/>httpx 异步]
        P2[AnthropicProvider<br/>httpx 异步]
        P3[BaichuanProvider<br/>httpx 异步]
        P4[LocalOllamaProvider<br/>httpx 异步]
    end

    subgraph Template["Prompt 模板管理"]
        T1[PromptTemplateRegistry<br/>模板注册 + 渲染]
        T2[模板存储<br/>YAML 文件 / DB]
    end

    subgraph Client["下游 gRPC client"]
        C1[ContentClient<br/>查询知识点 / 题库]
        C2[DataAnaClient<br/>查询学情 / 薄弱点]
    end

    subgraph Usage["用量计费"]
        U1[UsageRecorder<br/>token 消耗统计]
        U2[KafkaProducer<br/>发布 edu.insight.ai.usage]
    end

    subgraph External["外部 / 存储"]
        LLM[LLM Provider API<br/>OpenAI/Anthropic/百川/Ollama]
        CONTENT[content:3005 gRPC]
        DATAANA[data-ana:3006 gRPC]
        KAFKA[(Kafka)]
        REDIS[(Redis<br/>限流 + 缓存)]
    end

    HTTP --> AUTH --> RATE --> S1
    HTTP --> S2
    HTTP --> S3
    GRPC --> S1
    GRPC --> S2
    S2 --> S4
    S4 --> C1
    S4 --> C2
    S1 --> T1
    S2 --> T1
    S1 --> P0
    S2 --> P0
    P0 --> P1
    P0 --> P2
    P0 --> P3
    P0 --> P4
    P1 --> LLM
    P2 --> LLM
    P3 --> LLM
    P4 --> LLM
    S1 --> U1
    S2 --> U1
    U1 --> U2
    U2 --> KAFKA
    RATE --> REDIS
    C1 --> CONTENT
    C2 --> DATAANA
```

### 1.2 ai12 增补：完整分层图（含安全 / 评估 / 缓存 / 工作流持久化）

```mermaid
flowchart TB
    subgraph Entry["入口层"]
        HTTP[FastAPI HTTP Router<br/>/ai/v1/* + /healthz + /readyz + /metrics]
        GRPC[grpc.aio Server<br/>AiService 含 StreamChat/StreamGenerateQuestion]
    end

    subgraph Middleware["中间件层"]
        AUTH[AuthDepends<br/>JWT claims → UserContext]
        RATE[RateLimitDepends<br/>Redis 多维度令牌桶<br/>user/IP/school/token]
        TRACE[OTel FastAPIInstrumentor<br/>+ grpc.aio ServerInterceptor<br/>+ grpc.aio ClientInterceptor]
        REQID[RequestContextMiddleware<br/>X-Request-Id 注入与透传]
    end

    subgraph Security["安全层 ai12 增补"]
        PII[PIIRedactor<br/>学生姓名/手机号脱敏]
        SAN[InputSanitizer<br/>Prompt 注入防御]
        MOD[OutputModerator<br/>敏感词过滤 + 安全校验]
    end

    subgraph Service["应用服务层"]
        S1[ChatService<br/>聊天编排 + Prompt 渲染]
        S2[QuestionGenerationService<br/>出题 + 逐字流式生成]
        S3[ExpressionOptimizationService<br/>表达优化]
        S4[LessonPreparationWorkflow<br/>备课工作流 4 步编排]
        S5[PromptTemplateService<br/>模板 CRUD + 版本化]
        S6[UsageService<br/>用量查询 + 配额校验]
    end

    subgraph Provider["LLM Provider 适配层"]
        P0[LLMProvider 抽象接口<br/>chat / stream_chat / embed?]
        P1[OpenAIProvider]
        P2[AnthropicProvider]
        P3[BaichuanProvider]
        P4[LocalOllamaProvider]
        PF[ProviderFailoverChain<br/>多 Provider 故障切换]
    end

    subgraph Template["Prompt 模板管理"]
        T1[PromptTemplateRegistry<br/>模板注册 + Jinja2 渲染]
        T2[模板存储<br/>YAML 文件 P5 / DB P6+]
        T3[模板版本化 + A/B 路由 P6+]
    end

    subgraph Evaluation["评估层 ai12 增补"]
        E1[RuleValidator<br/>题型/答案/解析规则校验]
        E2[LLMJudge<br/>LLM-as-judge 质量评分]
        E3[QualityGate<br/>质量门禁 不达标重新生成或降级]
    end

    subgraph Workflow["工作流持久化 ai12 增补"]
        W1[WorkflowStateStore<br/>Redis 短期 24h TTL P5<br/>Temporal P6+ 评估]
        W2[WorkflowEventLog<br/>工作流步骤事件日志]
    end

    subgraph Cache["缓存层 ai12 增补"]
        CA1[PromptCache<br/>hash 缓存 P5]
        CA2[SemanticCache<br/>embedding 相似度 P6+]
        CA3[TemplateCache<br/>模板缓存 1h TTL]
    end

    subgraph Client["下游 gRPC client"]
        C1[ContentClient<br/>查询知识点/教材/题库]
        C2[DataAnaClient<br/>查询学情/薄弱点/趋势]
        C3[IamClient<br/>查询 DataScope P4+]
    end

    subgraph Usage["用量计费"]
        U1[UsageRecorder<br/>token 消耗统计]
        U2[KafkaProducer<br/>发布 edu.insight.ai.usage]
        U3[QuotaEnforcer<br/>学校/年级/教师配额校验]
    end

    subgraph External["外部 / 存储"]
        LLM[LLM Provider API<br/>OpenAI/Anthropic/百川/Ollama]
        CONTENT[content:3005 gRPC]
        DATAANA[data-ana:3006 gRPC]
        IAM[iam:3002 gRPC]
        KAFKA[(Kafka)]
        REDIS[(Redis<br/>限流/缓存/工作流状态)]
    end

    HTTP --> REQID --> AUTH --> RATE --> Security
    GRPC --> Security
    Security --> S1 & S2 & S3 & S4 & S5 & S6
    S1 & S2 --> T1
    S1 & S2 --> CA1
    S1 & S2 --> P0
    P0 --> PF
    PF --> P1 & P2 & P3 & P4
    P1 & P2 & P3 & P4 --> LLM
    S2 --> E1 --> E2 --> E3
    S2 --> CA1
    S4 --> C1 & C2
    S4 --> W1
    S4 --> W2
    S5 --> T2
    S5 --> CA3
    S6 --> U3
    S1 & S2 & S3 --> U1 --> U2 --> KAFKA
    RATE --> REDIS
    CA1 & CA3 & W1 --> REDIS
    C1 --> CONTENT
    C2 --> DATAANA
    C3 --> IAM
```

**ai12 增补：分层规则**

- **入口层**：HTTP（保留作 Gateway 直连降级 + 前端 SSE 流式）+ gRPC（主入口，BFF 调用，含 `StreamChat` / `StreamGenerateQuestion` 流式 RPC）
- **中间件层**：RequestContextMiddleware（X-Request-Id）→ AuthDepends（JWT claims 解析）→ RateLimitDepends（多维度限流）→ OTel 自动埋点
- **安全层**（ai12 增补）：PIIRedactor → InputSanitizer → OutputModerator，所有进出 LLM 的数据必须经过安全层
- **应用服务层**：6 个 Service，每个对应一类 AI 能力
- **Provider 适配层**：抽象 `LLMProvider` 接口 + 多适配器实现（策略模式）+ `ProviderFailoverChain` 故障切换（resilience 模式）
- **Prompt 模板**：模板注册 + Jinja2 渲染 + 版本化（P5 YAML，P6+ DB + A/B）
- **评估层**（ai12 增补）：规则校验 + LLM-as-judge + 质量门禁，构成生成质量保障三道防线
- **工作流持久化**（ai12 增补）：Redis 短期（P5）+ Temporal（P6+ 评估），保证长运行工作流状态不丢失
- **缓存层**（ai12 增补）：Prompt hash 缓存（P5）+ 语义缓存（P6+ 评估）+ 模板缓存
- **下游 client**：gRPC 调 content / data-ana / iam（DataScope），均带 client interceptor（trace + retry + circuit breaker）
- **用量计费**：token 消耗统计 + Kafka 事件外发 + 配额校验

## 2. 领域模型

ai 是**无状态服务**，不持有持久化聚合根。领域模型为**请求/响应模型 + 工作流编排**：

### 2.1 聚合根（请求型，无持久化）（ai06 初稿）

| 聚合根                       | 含义         | 生命周期                                                     |
| ---------------------------- | ------------ | ------------------------------------------------------------ |
| `ChatConversation`           | 单次聊天请求 | 单次请求                                                     |
| `QuestionGenerationTask`     | 出题任务     | 单次请求（备课工作流中多步）                                 |
| `ExpressionOptimizationTask` | 表达优化任务 | 单次请求                                                     |
| `LessonPreparationWorkflow`  | 备课工作流   | 跨多步（分析学情 → 推荐知识点 → 生成题目 → 教师审核 → 入库） |

### 2.2 ai12 增补：扩展聚合根与值对象

| 聚合根             | 含义             | 生命周期            | 持久化                                   |
| ------------------ | ---------------- | ------------------- | ---------------------------------------- |
| `PromptTemplate`   | Prompt 模板      | 持久化（YAML/DB）   | YAML 文件 P5 / DB P6+                    |
| `UsageRecord`      | 用量记录         | 单次 LLM 调用       | 不持久化，Kafka 事件外发                 |
| `EvaluationResult` | 生成结果质量评估 | 单次生成调用        | 不持久化（P6+ 评估落 ClickHouse 供分析） |
| `WorkflowState`    | 工作流状态       | 跨多步（数小时/天） | Redis 短期 P5 / Temporal P6+             |

### 2.3 值对象

- `ChatMessage`：role + content
- `Usage`：prompt_tokens + completion_tokens + total_tokens + latency_ms
- `GeneratedQuestion`：question + answer + explanation + question_type + difficulty + knowledge_point_ids
- `PromptTemplate`：name + system_prompt + user_template + variables + version
- **ai12 增补**：`UserContext`：user_id + roles + data_scope + school_id + request_id
- **ai12 增补**：`ProviderConfig`：provider_name + model + api_key + base_url + timeout + max_retries
- **ai12 增补**：`EvaluationReport`：rule_score + judge_score + overall_score + reasons + pass
- **ai12 增补**：`WorkflowStep`：step_name + status + started_at + completed_at + result + error
- **ai12 增补**：`QuotaContext`：school_id + grade_id + teacher_id + budget + used + remaining

### 2.4 ai12 增补：备课工作流状态机

备课工作流是 ai 服务最复杂的工作流，明确状态机避免实现混乱：

```mermaid
stateDiagram-v2
    [*] --> Pending: 教师发起备课请求
    Pending --> Analyzing: 工作流启动
    Analyzing --> Analyzed: 查询 data-ana 学情成功
    Analyzing --> AnalyzeFailed: data-ana 不可达 + 降级跳过
    AnalyzeFailed --> Recommending: 降级（仅基于 prompt 生成）
    Analyzed --> Recommending: 查询 content 知识点
    Recommending --> Recommended: 知识点推荐完成
    Recommended --> Generating: 启动 LLM 出题
    Generating --> Generated: LLM 生成完成 + 评估通过
    Generating --> Regenerating: 评估未通过 + 重试 < 3 次
    Regenerating --> Generated: 重试生成通过
    Regenerating --> GenerationFailed: 重试 ≥ 3 次仍失败
    GenerationFailed --> [*]: 返回错误 + 工作流终止
    Generated --> PendingReview: 暂存题目 + 通知教师审核
    PendingReview --> Confirmed: 教师确认入库
    PendingReview --> Modified: 教师修改后确认
    PendingReview --> Rejected: 教师拒绝
    PendingReview --> Expired: 24h 未审核
    Confirmed --> Persisting: 调 content.CreateQuestions
    Modified --> Persisting: 调 content.CreateQuestions
    Persisting --> Persisted: 入库成功
    Persisting --> PersistFailed: 入库失败
    PersistFailed --> Persisting: 重试（最多 3 次）
    PersistFailed --> [*]: 重试失败 + 告警
    Persisted --> [*]: 工作流完成
    Rejected --> [*]: 工作流终止
    Expired --> [*]: 工作流过期
```

**ai12 增补：状态持久化策略**

| 状态                               | P5 持久化                                 | P6+ 持久化                          |
| ---------------------------------- | ----------------------------------------- | ----------------------------------- |
| Pending-Analyzing-Generated        | Redis（key: `ai:workflow:{id}`，TTL 24h） | Temporal workflow history           |
| PendingReview                      | Redis + 通知 BFF（前端轮询/SSE）          | Temporal signal（教师审核事件）     |
| 终态（Persisted/Rejected/Expired） | Redis 标记 + 事件日志                     | Temporal workflow 完成 + ClickHouse |

**ai12 增补：工作流事件日志**（供审计 + RLHF 数据收集）

```json
{
  "workflow_id": "uuid",
  "user_id": "user-xxx",
  "school_id": "school-xxx",
  "step": "Generating",
  "status": "completed",
  "duration_ms": 1200,
  "llm_provider": "openai",
  "llm_model": "gpt-4o-mini",
  "tokens_used": 230,
  "evaluation_score": 0.85,
  "occurred_at": "2026-07-09T12:00:00Z"
}
```

### 2.5 ai12 增补：备课工作流序列图（细化 004 §9.3）

```mermaid
sequenceDiagram
    participant T as 教师
    participant BFF as teacher-bff
    participant AI as ai 服务
    participant IAM as iam
    participant DA as data-ana
    participant Content as content
    participant LLM as LLM Provider
    participant Redis as Redis（工作流状态）
    participant Kafka as Kafka

    T->>BFF: POST /lesson/preparation {class_id, subject_id, topic}
    BFF->>AI: gRPC GenerateLessonPlan
    AI->>IAM: gRPC GetEffectiveDataScope(user_id)
    IAM-->>AI: DataScope
    AI->>Redis: 创建工作流（状态=Pending）
    AI->>AI: 工作流启动（状态=Analyzing）
    par 并行查询
        AI->>DA: gRPC GetClassPerformance(class_id, subject_id)
        DA-->>AI: 班级学情
    and
        AI->>DA: gRPC GetStudentWeakness(student_id, subject_id)
        DA-->>AI: 薄弱知识点
    end
    AI->>Content: gRPC GetPrerequisites(knowledge_point_id)
    Content-->>AI: 前置依赖知识点
    AI->>Redis: 更新工作流（状态=Recommended）
    AI->>AI: 渲染 Prompt 模板
    AI->>LLM: stream_chat（题目逐字生成）
    loop 流式 chunks
        LLM-->>AI: chunk
        AI-->>BFF: stream chunk（透传给前端）
    end
    AI->>AI: 规则校验 + LLM-as-judge
    alt 评估通过
        AI->>Redis: 更新工作流（状态=PendingReview）
        AI-->>BFF: 题目列表 + workflow_id
        BFF-->>T: 题目供审核
    else 评估未通过 + 重试 < 3
        AI->>AI: 调整 Prompt + 重新生成
    else 重试 ≥ 3 仍失败
        AI-->>BFF: 错误响应
    end
    AI->>Kafka: 发布 AIUsageRecorded 事件
    T->>BFF: POST /lesson/preparation/{id}/confirm
    BFF->>AI: gRPC ConfirmLessonPlan(workflow_id)
    AI->>Redis: 更新工作流（状态=Persisting）
    AI->>Content: gRPC CreateQuestions(questions)
    Content-->>AI: 入库成功
    AI->>Redis: 更新工作流（状态=Persisted）
    AI-->>BFF: 入库结果
    BFF-->>T: 入库成功
```

## 3. 数据模型

### 3.1 ai06 初稿：无 DB 设计

ai 服务**无独占数据库**，无 MySQL schema。所有数据通过 Kafka 事件外发（用量计费）或 gRPC 查询下游。

### 3.2 ai12 增补：Redis schema 详细设计

虽然 ai 无 DB，但 Redis 承载多种数据结构，需明确 schema：

| Key 模式                              | 数据类型 | TTL      | 用途                             |
| ------------------------------------- | -------- | -------- | -------------------------------- |
| `ai:ratelimit:user:{user_id}`         | Hash     | 滑动窗口 | 用户级令牌桶（每分钟 10 次）     |
| `ai:ratelimit:ip:{ip}`                | Hash     | 滑动窗口 | IP 级令牌桶（每分钟 30 次）      |
| `ai:ratelimit:school:{school_id}`     | Hash     | 滑动窗口 | 学校级令牌桶（每分钟 100 次）    |
| `ai:quota:school:{school_id}`         | Hash     | 月度重置 | 学校月度 token 预算 + 已用       |
| `ai:quota:teacher:{user_id}`          | Hash     | 月度重置 | 教师月度 token 预算 + 已用       |
| `ai:cache:prompt:{hash}`              | String   | 30 分钟  | LLM 响应缓存（hash 缓存）        |
| `ai:cache:template:{template_name}`   | String   | 1 小时   | Prompt 模板缓存                  |
| `ai:cache:datascope:{user_id}`        | String   | 5 分钟   | DataScope 缓存（避免重复查 iam） |
| `ai:workflow:{workflow_id}`           | Hash     | 24 小时  | 备课工作流状态                   |
| `ai:workflow:user:{user_id}`          | Set      | 24 小时  | 用户的活跃工作流 ID 集合         |
| `ai:dedup:event:{event_id}`           | String   | 7 天     | 事件去重（SETNX）                |
| `ai:circuit:provider:{provider_name}` | String   | 60 秒    | Provider 熔断状态                |

### 3.3 ai12 增补：用量计费 Kafka 事件 payload（建议 events.proto 新增 `AIUsageEvent`）

```protobuf
// 建议 coord 在 events.proto 新增（ai12 提请，待仲裁）
message AIUsageEvent {
  string event_id = 1;            // UUID，幂等去重
  string aggregate_id = 2;        // workflow_id 或 request_id
  string event_type = 3;          // 固定 "AIUsageRecorded"
  int64 occurred_at = 4;          // Unix 毫秒时间戳
  string user_id = 5;
  string school_id = 6;           // 用于多租户配额
  string request_id = 7;          // 链路追踪 ID
  string provider = 8;            // openai/anthropic/baichuan/local
  string model = 9;               // gpt-4o-mini/claude-3-haiku/...
  string operation = 10;          // chat/generate_question/optimize_expression/lesson_preparation
  uint32 prompt_tokens = 11;
  uint32 completion_tokens = 12;
  uint32 total_tokens = 13;
  uint32 latency_ms = 14;
  bool success = 15;
  bool degraded = 16;             // 是否降级（LLM 不可用时）
  map<string, string> metadata = 17;  // 额外上下文（subject/grade/difficulty 等）
}
```

### 3.4 ai12 增补：缓存策略

| 数据                         | 存储                     | TTL      | 失效策略                                     |
| ---------------------------- | ------------------------ | -------- | -------------------------------------------- |
| Prompt 模板                  | Redis（template cache）  | 1 小时   | 文件/DB 变更时主动失效（pub/sub 通知）       |
| LLM 响应（相同 prompt hash） | Redis（prompt cache）    | 30 分钟  | 短 TTL，避免陈旧；用户反馈差时主动失效       |
| DataScope（避免重复查 iam）  | Redis（datascope cache） | 5 分钟   | 短 TTL，权限变更时由 iam 主动失效（pub/sub） |
| 限流计数                     | Redis（ratelimit hash）  | 滑动窗口 | 自动过期                                     |
| 配额计数                     | Redis（quota hash）      | 月度重置 | 月初定时任务重置                             |
| 工作流状态                   | Redis（workflow hash）   | 24 小时  | 工作流终态后保留 1h 再过期（供审计）         |
| **P6+ 语义缓存**             | Redis + 向量索引         | 30 分钟  | embedding 相似度 > 0.95 命中                 |

## 4. API 设计

### 4.1 HTTP 端点（保留作 Gateway 直连降级，ai06 初稿 + ai12 增补）

> **ai12 增补**：所有路径加 `/v1` 版本前缀（P5 起强制），便于未来破坏性变更；响应统一为 ActionState

| method | path                                              | 权限                     | 请求体                        | 响应                                   | 说明                              |
| ------ | ------------------------------------------------- | ------------------------ | ----------------------------- | -------------------------------------- | --------------------------------- |
| GET    | `/healthz`                                        | —                        | —                             | `{status, service}`                    | liveness                          |
| GET    | `/readyz`                                         | —                        | —                             | `{status, llm_configured, ...}`        | readiness                         |
| GET    | `/metrics`                                        | —                        | —                             | Prometheus                             | 指标                              |
| POST   | `/ai/v1/chat`                                     | `AI_CHAT`                | `ChatRequest`                 | `ActionState<ChatData>`                | LLM 聊天                          |
| POST   | `/ai/v1/chat/stream`                              | `AI_CHAT`                | `ChatRequest`                 | SSE stream                             | 流式聊天                          |
| POST   | `/ai/v1/generate/question`                        | `AI_QUESTION_GENERATE`   | `GenerateQuestionRequest`     | `ActionState<GeneratedQuestionData>`   | 生成题目                          |
| POST   | `/ai/v1/generate/question/stream`                 | `AI_QUESTION_GENERATE`   | `GenerateQuestionRequest`     | SSE stream（题目逐字生成）             | **ai12 增补**：题目逐字流式       |
| POST   | `/ai/v1/optimize/expression`                      | `AI_EXPRESSION_OPTIMIZE` | `OptimizeExpressionRequest`   | `ActionState<OptimizedExpressionData>` | 优化表达                          |
| POST   | `/ai/v1/lesson/preparation`                       | `AI_LESSON_PREPARE`      | `LessonPreparationRequest`    | `ActionState<LessonPreparationData>`   | **ai12 增补**：备课工作流启动     |
| GET    | `/ai/v1/lesson/preparation/{workflow_id}`         | `AI_LESSON_PREPARE`      | —                             | `ActionState<WorkflowState>`           | **ai12 增补**：查询工作流状态     |
| POST   | `/ai/v1/lesson/preparation/{workflow_id}/confirm` | `AI_LESSON_PREPARE`      | `ConfirmRequest`              | `ActionState<PersistResult>`           | **ai12 增补**：教师确认入库       |
| GET    | `/ai/v1/prompts`                                  | `AI_PROMPT_READ`         | query: `?page&size`           | `ActionState<Page<TemplateSummary>>`   | **ai12 增补**：模板列表           |
| POST   | `/ai/v1/prompts`                                  | `AI_PROMPT_CREATE`       | `CreatePromptRequest`         | `ActionState<PromptTemplate>`          | **ai12 增补**：创建模板           |
| GET    | `/ai/v1/prompts/{id}`                             | `AI_PROMPT_READ`         | —                             | `ActionState<PromptTemplate>`          | **ai12 增补**：获取模板           |
| PUT    | `/ai/v1/prompts/{id}`                             | `AI_PROMPT_UPDATE`       | `UpdatePromptRequest`         | `ActionState<PromptTemplate>`          | **ai12 增补**：更新模板（版本化） |
| GET    | `/ai/v1/usage/me`                                 | `AI_USAGE_READ`          | query: `?start_date&end_date` | `ActionState<UsageSummary>`            | **ai12 增补**：当前用户用量       |
| GET    | `/ai/v1/usage/school/{school_id}`                 | `AI_USAGE_READ_ALL`      | query: `?start_date&end_date` | `ActionState<UsageSummary>`            | **ai12 增补**：学校用量（管理员） |

### 4.2 gRPC 契约（ai.proto，待实现 server，ai06 初稿 + ai12 增补）

| RPC                      | 请求                                                                                                         | 响应                                                        | 权限                     | 说明                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | ------------------------ | ------------------------------- |
| `Chat`                   | `ChatRequest{messages, model, temperature, user_id?, session_id?, data_scope?}`                              | `ChatResponse{content, model, usage}`                       | `AI_CHAT`                | 非流式聊天                      |
| `StreamChat`             | `ChatRequest`                                                                                                | `stream ChatChunk`                                          | `AI_CHAT`                | 流式聊天（SSE over gRPC）       |
| `GenerateQuestion`       | `GenerateQuestionRequest{prompt, subject, difficulty, grade?, knowledge_point_ids?, question_type?, count?}` | `GeneratedQuestion`                                         | `AI_QUESTION_GENERATE`   | 生成题目                        |
| `StreamGenerateQuestion` | `GenerateQuestionRequest`                                                                                    | `stream GeneratedQuestionChunk`                             | `AI_QUESTION_GENERATE`   | **ai12 增补**：题目逐字流式生成 |
| `OptimizeExpression`     | `OptimizeExpressionRequest{text, context}`                                                                   | `OptimizedExpression`                                       | `AI_EXPRESSION_OPTIMIZE` | 优化表达                        |
| `GenerateLessonPlan`     | `GenerateLessonPlanRequest{class_id, subject_id, topic, user_id, data_scope}`                                | `LessonPlanResponse{workflow_id, status, questions?}`       | `AI_LESSON_PREPARE`      | **ai12 增补**：备课工作流启动   |
| `GetLessonPlanStatus`    | `GetLessonPlanStatusRequest{workflow_id}`                                                                    | `LessonPlanStatus{workflow_id, status, questions?, error?}` | `AI_LESSON_PREPARE`      | **ai12 增补**：查询工作流状态   |
| `ConfirmLessonPlan`      | `ConfirmLessonPlanRequest{workflow_id, modifications?}`                                                      | `ConfirmResult{success, persisted_question_ids?}`           | `AI_LESSON_PREPARE`      | **ai12 增补**：教师确认入库     |

### 4.3 ai12 增补：Pydantic 请求/响应模型（ActionState 对齐）

```python
from typing import Any, Literal
from pydantic import BaseModel, Field


# === ActionState 信封（004 §11.5 强制约束） ===
class ErrorDetail(BaseModel):
    code: str
    message: str
    details: dict[str, Any] | None = None
    trace_id: str | None = None


class ActionState(BaseModel):
    """统一响应信封，所有响应必须使用此结构."""
    success: bool
    data: Any | None = None
    error: ErrorDetail | None = None


# === 聊天 ===
class ChatMessage(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str = Field(..., min_length=1, max_length=8000)


class ChatRequest(BaseModel):
    messages: list[ChatMessage] = Field(..., min_length=1, max_length=50)
    model: str = "gpt-4o-mini"
    temperature: float = Field(0.7, ge=0.0, le=2.0)
    stream: bool = False


class Usage(BaseModel):
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int
    latency_ms: int


class ChatData(BaseModel):
    content: str
    model: str
    usage: Usage


# === 题目生成 ===
class GenerateQuestionRequest(BaseModel):
    prompt: str = Field(..., min_length=1, max_length=2000)
    subject: str = Field(..., max_length=50)
    difficulty: Literal["easy", "medium", "hard"]
    grade: str | None = Field(None, max_length=20)
    knowledge_point_ids: list[str] = Field(default_factory=list, max_length=20)
    question_type: Literal["single_choice", "multi_choice", "fill_blank", "short_answer", "essay"] = "short_answer"
    count: int = Field(1, ge=1, le=10)


class GeneratedQuestionData(BaseModel):
    question: str
    answer: str
    explanation: str
    question_type: str
    difficulty: str
    knowledge_point_ids: list[str] = []
    evaluation_score: float | None = None  # ai12 增补：质量评分


# === 备课工作流 ===
class LessonPreparationRequest(BaseModel):
    class_id: str
    subject_id: str
    topic: str = Field(..., max_length=500)
    target_difficulty: Literal["easy", "medium", "hard"] = "medium"
    question_count: int = Field(5, ge=1, le=20)


class LessonPreparationData(BaseModel):
    workflow_id: str
    status: Literal["pending", "analyzing", "generating", "pending_review", "persisted", "failed"]
    estimated_completion_seconds: int


class ConfirmRequest(BaseModel):
    modifications: dict[str, str] | None = None  # question_id → 修改后内容
```

### 4.4 ai12 增补：Prompt 模板 YAML 格式

```yaml
# services/ai/src/ai/prompts/generate_question.yaml
name: generate_question
version: "1.0.0"
description: 通用出题 Prompt 模板
variables:
  - name: subject
    description: 学科
    required: true
  - name: difficulty
    description: 难度
    required: true
    allowed: ["easy", "medium", "hard"]
  - name: grade
    description: 年级
    required: false
  - name: knowledge_points
    description: 知识点列表
    required: false
  - name: question_type
    description: 题型
    required: true
  - name: count
    description: 题目数量
    required: true
system_prompt: |
  你是一名经验丰富的{subject}教师，擅长根据学情出题。
  出题要求：
  1. 难度：{difficulty}
  2. 题型：{question_type}
  3. 数量：{count} 道
  {%- if grade %}
  4. 年级：{grade}
  {%- endif %}
  {%- if knowledge_points %}
  5. 必须覆盖以下知识点：{knowledge_points}
  {%- endif %}
  输出格式：JSON 数组，每道题包含 question/answer/explanation 三个字段。
user_template: |
  请根据以上要求出题。
```

## 5. 事件设计

### 5.1 消费的事件

ai 服务**不消费任何事件**（无状态，纯请求-响应）。

**ai12 增补：长期可考虑消费的事件**（P6+ 评估）

- `edu.content.question.published`：题库新增题目事件，可用于"题目查重"——避免 AI 生成与已有题目重复
- `edu.identity.user.role_changed`：用户角色变更，主动失效 DataScope 缓存（替代当前定时 5min TTL 失效）

### 5.2 发布的事件

| 事件              | Topic                                     | 触发时机          | 消费者                                 | Payload                                                                                                                                                           |
| ----------------- | ----------------------------------------- | ----------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AIUsageRecorded` | `edu.insight.ai.usage`（004 §7.2 已确认） | 每次 LLM 调用完成 | data-ana（落 ClickHouse ai_usage_log） | `{event_id, user_id, school_id, request_id, provider, model, operation, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, degraded, metadata}` |

**ai12 增补：长期可发布事件**（P6+ 评估，待 coord 仲裁）

| 事件                 | Topic                      | 触发时机           | 消费者         | 用途                     |
| -------------------- | -------------------------- | ------------------ | -------------- | ------------------------ |
| `AIContentGenerated` | `edu.insight.ai.generated` | 生成内容审计       | data-ana       | 落 ClickHouse 供质量分析 |
| `AIFeedbackRecorded` | `edu.insight.ai.feedback`  | 教师对生成结果反馈 | data-ana       | RLHF 数据集构建          |
| `AIWorkflowEvent`    | `edu.insight.ai.workflow`  | 工作流步骤状态变更 | data-ana + msg | 工作流监控 + 失败告警    |

**发布实现**：

- 每次 LLM 调用后异步发布（不阻塞响应）
- `aiokafka.AIOKafkaProducer` + `acks=all` + `idempotent=true` + `transactional_id`
- 失败重试 3 次（指数退避：1s/2s/4s），仍失败记录日志（不影响主流程，用量计费为派生数据可丢失）
- **幂等性**：`event_id` UUID + Redis SETNX 去重（TTL 7 天），防止 producer 重试导致重复投递

## 6. 横切关注点对齐清单

### 6.1 权限装饰器等价物（ai06 初稿 + ai12 增补）

```python
class Permissions:
    AI_CHAT = "ai:chat"
    AI_QUESTION_GENERATE = "ai:question:generate"
    AI_EXPRESSION_OPTIMIZE = "ai:expression:optimize"
    AI_LESSON_PREPARE = "ai:lesson:prepare"
    AI_PROMPT_READ = "ai:prompt:read"
    AI_PROMPT_CREATE = "ai:prompt:create"
    AI_PROMPT_UPDATE = "ai:prompt:update"
    AI_USAGE_READ = "ai:usage:read"           # ai12 增补：查询自身用量
    AI_USAGE_READ_ALL = "ai:usage:read_all"   # ai12 增补：查询学校用量（管理员）


async def require_permission(permission: str) -> UserContext:
    """FastAPI Depends 权限校验.

    从 x-user-id / x-user-roles 头提取身份，校验角色是否含 permission。
    DataScope 通过 iam.GetEffectiveDataScope 查询，Redis 缓存 5min。
    """
    ...
```

### 6.2 错误码清单（前缀 `AI_*`，ai06 初稿 + ai12 增补）

| 错误码                           | 触发条件                                              | HTTP                | gRPC status         |
| -------------------------------- | ----------------------------------------------------- | ------------------- | ------------------- |
| `AI_UNAUTHORIZED`                | 缺失 x-user-id 或 token 无效                          | 401                 | UNAUTHENTICATED     |
| `AI_FORBIDDEN`                   | 角色无对应权限                                        | 403                 | PERMISSION_DENIED   |
| `AI_RATE_LIMITED`                | 触发限流（user/IP/school 维度）                       | 429                 | RESOURCE_EXHAUSTED  |
| `AI_QUOTA_EXCEEDED`              | 学校/教师月度 token 配额耗尽                          | 429                 | RESOURCE_EXHAUSTED  |
| `AI_LLM_UNAVAILABLE`             | LLM Provider 不可达（降级模式仍返回骨架）             | 200 + degraded:true | OK + degraded flag  |
| `AI_LLM_TIMEOUT`                 | LLM 调用超时（30s）                                   | 504                 | DEADLINE_EXCEEDED   |
| `AI_LLM_ALL_PROVIDERS_FAILED`    | 所有 Provider 故障切换链均失败                        | 503                 | UNAVAILABLE         |
| `AI_INVALID_MODEL`               | model 名不支持                                        | 400                 | INVALID_ARGUMENT    |
| `AI_INVALID_DIFFICULTY`          | difficulty 不在 easy/medium/hard                      | 400                 | INVALID_ARGUMENT    |
| `AI_INVALID_QUESTION_TYPE`       | question_type 不在枚举内                              | 400                 | INVALID_ARGUMENT    |
| `AI_DOWNSTREAM_UNAVAILABLE`      | content / data-ana gRPC 不可达                        | 502                 | UNAVAILABLE         |
| `AI_PROMPT_RENDER_FAILED`        | Prompt 模板渲染失败（变量缺失）                       | 500                 | INTERNAL            |
| `AI_PROMPT_TEMPLATE_NOT_FOUND`   | Prompt 模板不存在                                     | 404                 | NOT_FOUND           |
| `AI_WORKFLOW_NOT_FOUND`          | 备课工作流 ID 不存在                                  | 404                 | NOT_FOUND           |
| `AI_WORKFLOW_EXPIRED`            | 工作流已过期（24h 未审核）                            | 410                 | FAILED_PRECONDITION |
| `AI_WORKFLOW_STATE_INVALID`      | 工作流状态不允许此操作（如已 Persisted 再次 Confirm） | 409                 | FAILED_PRECONDITION |
| `AI_EVALUATION_FAILED`           | 生成质量评估未通过且重试耗尽                          | 503                 | UNAVAILABLE         |
| `AI_PII_DETECTED`                | 输入包含未脱敏 PII（脱敏失败）                        | 400                 | INVALID_ARGUMENT    |
| `AI_PROMPT_INJECTION_DETECTED`   | 输入疑似 Prompt 注入攻击                              | 400                 | INVALID_ARGUMENT    |
| `AI_CONTENT_MODERATION_REJECTED` | 输出内容审核不通过（敏感词）                          | 503                 | UNAVAILABLE         |
| `AI_INTERNAL_ERROR`              | 未捕获异常                                            | 500                 | INTERNAL            |

### 6.3 Logger 初始化

- 位置：`main.py`（已具备）
- 配置：`structlog.make_filtering_bound_logger(level)` + `TimeStamper(fmt="iso")` + `ConsoleRenderer`
- **改进**：生产环境改用 `structlog.processors.JSONRenderer()`（当前 ConsoleRenderer 适合开发）
- **ai12 增补**：日志必须包含 `request_id`（从 RequestContextMiddleware 注入）、`user_id`、`workflow_id`（如适用），便于全链路追踪

### 6.4 Metrics 指标清单（ai06 初稿 + ai12 增补）

| 指标名                             | 类型      | 标签                                      | 描述                               |
| ---------------------------------- | --------- | ----------------------------------------- | ---------------------------------- |
| `ai_http_requests_total`           | Counter   | method, path, status                      | HTTP 请求总数                      |
| `ai_http_request_duration_seconds` | Histogram | method, path                              | HTTP 请求延迟                      |
| `ai_grpc_requests_total`           | Counter   | method, status                            | gRPC 请求总数（ai12 增补）         |
| `ai_grpc_request_duration_seconds` | Histogram | method                                    | gRPC 请求延迟（ai12 增补）         |
| `ai_llm_calls_total`               | Counter   | provider, model, operation, status        | LLM 调用总数                       |
| `ai_llm_call_duration_seconds`     | Histogram | provider, model, operation                | LLM 调用延迟                       |
| `ai_llm_tokens_total`              | Counter   | provider, model, type (prompt/completion) | token 消耗总数                     |
| `ai_llm_stream_chunks_total`       | Counter   | provider, model                           | 流式 chunk 总数                    |
| `ai_llm_first_token_seconds`       | Histogram | provider, model                           | 流式首字延迟（ai12 增补，SLO）     |
| `ai_provider_failover_total`       | Counter   | from_provider, to_provider, reason        | Provider 故障切换次数（ai12 增补） |
| `ai_grpc_downstream_calls_total`   | Counter   | downstream, method, status                | 下游 gRPC 调用总数                 |
| `ai_rate_limit_hits_total`         | Counter   | dimension (user/ip/school)                | 限流命中次数                       |
| `ai_quota_used_ratio`              | Gauge     | school_id, dimension (school/teacher)     | 配额使用率（ai12 增补）            |
| `ai_usage_events_published_total`  | Counter   | status                                    | 用量事件发布数                     |
| `ai_prompt_template_renders_total` | Counter   | template_name, status                     | 模板渲染次数                       |
| `ai_prompt_cache_hits_total`       | Counter   | —                                         | Prompt 缓存命中（ai12 增补）       |
| `ai_workflow_active_count`         | Gauge     | status                                    | 活跃工作流数（ai12 增补）          |
| `ai_workflow_duration_seconds`     | Histogram | status                                    | 工作流总耗时（ai12 增补）          |
| `ai_evaluation_pass_rate`          | Gauge     | operation                                 | 评估通过率（ai12 增补）            |
| `ai_circuit_breaker_state`         | Gauge     | provider, state (closed/open/half_open)   | 熔断器状态（ai12 增补）            |

### 6.5 Tracer 初始化

- 位置：`main.py` `init_tracer()`（已具备，dev_mode 跳过）
- **改进**：
  - gRPC server 注册 `grpc.aio.ServerInterceptor` 透传 W3C trace context（从 metadata 提取 `traceparent`）
  - 下游 gRPC client 注册 `grpc.aio.ClientInterceptor` 注入 `traceparent` 到 metadata
  - LLM 调用创建子 span（`ai_llm_call`），记录 provider/model/tokens/latency
  - 备课工作流每步创建子 span（`ai_workflow_step`），记录 step_name/status/duration

### 6.6 /healthz 检查逻辑

- liveness：仅进程存活（已具备）

### 6.7 /readyz 检查逻辑（ai06 初稿 + ai12 增补）

```python
async def readyz() -> dict:
    return {
        "status": "ok" if all_ready else "degraded",
        "ready": all_ready,
        "service": "ai",
        "llm_configured": settings.llm_available,
        "degraded": not all_ready,
        "providers": {
            "openai": bool(settings.openai_api_key),
            "anthropic": bool(settings.anthropic_api_key),
            "baichuan": bool(settings.baichuan_api_key),
            "local_ollama": bool(settings.ollama_base_url),
        },
        "downstream_grpc": {
            "content": "ok" | "unreachable" | "not_configured",
            "data_ana": "ok" | "unreachable" | "not_configured",
            "iam": "ok" | "unreachable" | "not_configured",  # ai12 增补
        },
        "redis": "ok" | "unreachable",  # 限流 + 缓存 + 工作流状态依赖
        "kafka": "ok" | "unreachable",  # ai12 增补：用量事件发布依赖
        "timestamp": datetime.now(UTC).isoformat(),
    }
```

### 6.8 优雅关闭顺序（ai06 初稿 + ai12 增补）

1. HTTP server stop accepting new requests（uvicorn graceful shutdown）
2. **ai12 增补**：拒绝新工作流启动（返回 `AI_SERVICE_SHUTTING_DOWN` 503）
3. gRPC server graceful stop（**关键**：等待在途 `StreamChat` / `StreamGenerateQuestion` 流式 RPC 完成，60s 超时，避免截断用户响应）
4. LLM 流式请求 drain（等待 httpx stream 完成）
5. **ai12 增补**：在途工作流状态保存（Redis 已持久化，无需额外操作；Temporal 模式下需 complete workflow）
6. Kafka producer flush + close（确保用量事件已投递）
7. 下游 gRPC channels close（content / data-ana / iam）
8. Redis connection close

**信号处理**：注册 `signal.SIGTERM` handler，触发上述顺序，总超时 90s（gRPC 60s + 其他 30s）。

## 7. 与其他模块的交互点（契约清单）

| 方向   | 对方服务     | 协议  | 接口/事件                                                    | 用途                            |
| ------ | ------------ | ----- | ------------------------------------------------------------ | ------------------------------- |
| 被调用 | api-gateway  | HTTP  | `/ai/*`                                                      | Gateway 代理                    |
| 被调用 | teacher-bff  | gRPC  | `AiService.*`                                                | BFF 聚合 AI 能力                |
| 调用   | content      | gRPC  | `KnowledgeGraphService.GetPrerequisites / GetLearningPath`   | 出题上下文查询                  |
| 调用   | content      | gRPC  | `TextbookService.ListTextbooks`                              | 出题教材关联（ai12 增补）       |
| 调用   | content      | gRPC  | `QuestionService.CreateQuestions`（**待 coord 补全 proto**） | 备课工作流入库（ai12 增补）     |
| 调用   | data-ana     | gRPC  | `AnalyticsService.GetStudentWeakness / GetLearningTrend`     | 个性化出题学情查询              |
| 调用   | data-ana     | gRPC  | `AnalyticsService.GetClassPerformance`                       | 备课工作流班级学情（ai12 增补） |
| 调用   | iam          | gRPC  | `IamService.GetEffectiveDataScope`（**待 coord P4 补全**）   | DataScope 解析（ai12 增补）     |
| 调用   | LLM Provider | HTTP  | OpenAI 兼容 REST `/chat/completions`                         | LLM 推理                        |
| 发布   | —            | Kafka | `edu.insight.ai.usage`（004 §7.2 已确认）                    | 用量计费外发                    |
| 发布   | —            | Kafka | `edu.insight.ai.generated`（**P6+ 待 coord 仲裁**）          | 生成内容审计（ai12 增补）       |
| 发布   | —            | Kafka | `edu.insight.ai.feedback`（**P6+ 待 coord 仲裁**）           | RLHF 反馈数据（ai12 增补）      |

## 8. 风险与假设

### 8.1 假设

- **假设 1**：content 服务实现了 `KnowledgeGraphService` gRPC server。当前 content.proto 已定义但未实现 gRPC server（ai05 阶段 2 设计中）。ai 调用前需确认 content gRPC 可用
- **假设 2**：data-ana 实现了 `AnalyticsService` gRPC server（ai11 设计文档已设计）。ai 调用前需确认 data-ana gRPC 可用
- **假设 3**：coord 同意新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message。**004 §7.2 已确认 topic**，events.proto message 待 coord 补全
- **假设 4**：Redis 可用（限流 + 缓存 + 工作流状态依赖）。若 Redis 不可用：
  - 限流降级为"无限制"（风险：LLM 成本失控），或降级为内存令牌桶（单实例有效，多实例不一致）
  - 缓存失效，所有 LLM 调用直达 Provider（成本增加）
  - 工作流状态丢失，在途工作流失效（影响教师审核流程）
- **假设 5**（ai12 增补）：iam 提供 `GetEffectiveDataScope` gRPC API。coord 已仲裁 P4 补全（§15.3 #5）
- **假设 6**（ai12 增补）：LLM Provider API 稳定性符合 SLA（OpenAI 99.5%+）。多 Provider 故障切换可覆盖单 Provider 故障
- **假设 7**（ai12 增补）：教师审核备课结果在 24h 内完成。超过 24h 工作流过期，需重新发起

### 8.2 技术风险

| 风险                   | 影响                   | 缓解措施                                                                                             |
| ---------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------- |
| LLM 调用延迟高（>30s） | 用户体验差             | 超时 30s + 降级骨架响应 + 流式优先（用户感知首字延迟）                                               |
| LLM 成本失控           | 财务风险               | Redis 令牌桶限流（user/IP/school 三维度）+ 月度配额 + 用量计费监控 + 告警阈值                        |
| LLM Provider 单点故障  | 服务不可用             | 多 Provider 适配器 + ProviderFailoverChain 自动 failover（OpenAI 失败切 Anthropic，再切本地 Ollama） |
| 流式 RPC 中断          | 用户响应截断           | gRPC server graceful shutdown 60s drain + 客户端重连机制 + 工作流状态持久化支持重试                  |
| Prompt 注入攻击        | LLM 输出恶意内容       | InputSanitizer 过滤 + system prompt 加安全约束 + OutputModerator 过滤                                |
| 下游 gRPC 不可达       | 备课工作流失败         | 降级：跳过学情查询，仅基于 prompt 生成题目 + `degraded: true`                                        |
| PII 数据泄露给 LLM     | 合规风险               | PIIRedactor 在写入 prompt 前脱敏（学生姓名→"学生A"，手机号→"***"）                                   |
| 生成质量低             | 教师审核拒绝率高       | RuleValidator + LLMJudge 双重校验 + 不达标重试（最多 3 次）                                          |
| 工作流状态丢失         | 教师审核流程中断       | Redis 持久化 + 24h TTL + 事件日志（P6+ Temporal）                                                    |
| Redis 单点故障         | 限流/缓存/工作流全失效 | Redis 哨兵/集群（P6 硬化）+ 降级策略（限流无限制/缓存失效/工作流拒绝启动）                           |

### 8.3 未决设计决策（需 coord 仲裁）

1. **备课工作流是否引入 Temporal**：004 §2.3 列出 Temporal 用于 AI 编排，但简单 4 步工作流可用 FastAPI BackgroundTasks + Redis 状态。建议：P5 用 BackgroundTasks + Redis（24h TTL），P6 评估迁移 Temporal（支持长运行 + 复杂状态机）
2. **`edu.insight.ai.usage` topic 已确认，`AIUsageEvent` proto message 待补全**：需 coord 在 shared-proto events.proto 新增（ai12 已给出建议 schema §3.3）
3. **LLM Provider 配置化路由**：是否在 shared-py 建立通用 `LLMProvider` 抽象（供未来其他 Python 服务复用）。建议 P5 阶段在 ai 服务内部实现，P6 评估是否提取到 shared-py
4. **Prompt 模板存储位置**：YAML 文件（简单，无 DB）vs DB（动态更新）。建议 P5 用 YAML 文件（`services/ai/src/ai/prompts/*.yaml`），P6 评估迁移 DB
5. **ai12 新增 10 项提请**（详见 01-understanding.md §"ai12 新增提请项"）：ai gRPC 端口 50058、ai.proto 待补全、events.proto 待补 AIUsageEvent、iam GetEffectiveDataScope、响应信封整改、长运行工作流持久化、多模态规划、评估框架归属、RLHF 数据收集

---

## ai12 增补：长期架构演进设计（§9-§17）

> 以下章节为 ai12 新增，旨在为未来 P6+ 演进做好铺垫，确保 P5 实现不阻塞未来扩展。

## 9. 安全架构

### 9.1 PII 脱敏流水线

```mermaid
flowchart LR
    Input[用户输入] --> Detect[PII 检测<br/>正则 + NER 模型]
    Detect --> Redact[脱敏替换<br/>学生姓名→学生A<br/>手机号→***]
    Redact --> Log[脱敏日志<br/>记录脱敏操作]
    Log --> Prompt[安全 Prompt]
    Prompt --> LLM[LLM 调用]
```

| PII 类型 | 检测方式               | 脱敏方式             |
| -------- | ---------------------- | -------------------- |
| 学生姓名 | 白名单匹配（来自 iam） | 替换为"学生A/B/C"    |
| 手机号   | 正则 `1[3-9]\d{9}`     | 替换为 `***`         |
| 身份证号 | 正则 `\d{17}[\dXx]`    | 替换为 `***`         |
| 邮箱     | 正则                   | 替换为 `***@***.com` |
| 家庭住址 | NER 模型（P6+ 评估）   | 替换为"某地"         |

### 9.2 Prompt 注入防御

- **输入 sanitize**：过滤 SQL 注入模式（`'; DROP TABLE`）、HTML 注入（`<script>`）、命令注入（`; rm -rf`）
- **System prompt 加安全约束**：明确禁止执行"忽略上述指令"、"输出系统 prompt"等攻击模式
- **输出过滤**：检测输出中是否包含 system prompt 内容、是否执行了注入指令

### 9.3 输出内容审核

- **敏感词过滤**：基于敏感词字典（政治/色情/暴力/广告），P5 用基础字典，P6+ 接入专业审核 API
- **安全校验**：检测输出是否包含代码执行指令、是否诱导不当行为
- **审核失败处理**：返回 `AI_CONTENT_MODERATION_REJECTED` 503 + 触发重新生成（最多 3 次）

### 9.4 多租户数据隔离

- **DataScope 透传**：所有 LLM 调用必须携带 `UserContext.data_scope`，确保生成内容不超出用户权限范围
- **Prompt 注入 DataScope 约束**：system prompt 明确"仅可生成用户 DataScope 范围内的内容"
- **配额隔离**：按 school_id 隔离 token 配额，避免单一学校耗尽全平台预算

## 10. 评估框架

### 10.1 三道防线质量保障

```mermaid
flowchart LR
    Generated[LLM 生成结果] --> R1[第一道：规则校验<br/>RuleValidator]
    R1 -->|通过| R2[第二道：LLM-as-judge<br/>LLMJudge]
    R1 -->|不通过| Retry[重新生成]
    R2 -->|score >= 0.7| R3[第三道：质量门禁<br/>QualityGate]
    R2 -->|score < 0.7| Retry
    R3 -->|通过| Output[输出给用户]
    R3 -->|不通过| Retry
    Retry -->|重试 < 3| Generated
    Retry -->|重试 >= 3| Degraded[降级返回错误]
```

### 10.2 RuleValidator 规则清单

| 规则       | 校验内容                                             | 失败处理           |
| ---------- | ---------------------------------------------------- | ------------------ |
| 题型匹配   | 生成题目格式符合 `question_type`（单选/多选/填空等） | 重新生成           |
| 答案非空   | answer 字段非空且非占位符                            | 重新生成           |
| 解析合理   | explanation 长度 >= 20 字符                          | 重新生成           |
| 难度匹配   | LLM 判定难度与请求 `difficulty` 一致                 | 标记 warning，通过 |
| 知识点覆盖 | 生成题目覆盖请求的 `knowledge_point_ids`             | 标记 warning，通过 |
| 字符长度   | question <= 1000 字符                                | 截断 + warning     |
| 格式合规   | 输出可解析为 JSON（如要求 JSON 格式）                | 重新生成           |

### 10.3 LLMJudge 评估维度

| 维度         | 评分标准                   | 权重 |
| ------------ | -------------------------- | ---- |
| 准确性       | 题目本身正确性、答案正确性 | 0.4  |
| 清晰度       | 题目表述清晰、无歧义       | 0.2  |
| 难度匹配     | 实际难度与请求难度匹配     | 0.2  |
| 知识点相关性 | 题目与知识点相关           | 0.1  |
| 解析质量     | 解析能帮助学生理解         | 0.1  |

**评分阈值**：

- `>= 0.85`：优秀，直接通过
- `0.7-0.85`：合格，通过
- `< 0.7`：不合格，重新生成

### 10.4 评估数据收集（RLHF 基础）

每次生成结果记录：

- 生成内容（题目/答案/解析）
- 评估报告（rule_score / judge_score / overall_score）
- 教师反馈（采纳/拒绝/修改，由 BFF 调用 `ConfirmLessonPlan` 时携带 `modifications`）

P6+ 评估发布 `AIFeedbackRecorded` 事件，data-ana 落 ClickHouse 供未来 fine-tune。

## 11. RAG 检索架构演进

### 11.1 P5：复用 content 服务（无独立向量库）

```mermaid
flowchart LR
    Prompt[出题 Prompt] --> Content[content.KnowledgeGraphService<br/>GetPrerequisites]
    Content --> Context[知识点上下文]
    Context --> LLM[LLM 生成]
```

### 11.2 P6+：引入向量库语义检索（评估）

```mermaid
flowchart LR
    Prompt[出题 Prompt] --> Embed[Embedding Provider<br/>text-embedding-3-small]
    Embed --> Vector[(向量库<br/>pgvector/Chroma)]
    Vector --> Retrieve[Top-K 相似题目]
    Retrieve --> Rerank[Reranker<br/>cohere-rerank]
    Rerank --> Context[精排后上下文]
    Context --> LLM[LLM 生成]
```

**演进路径**：

- P5：仅基于 content 知识点查询
- P6+：评估引入向量库（pgvector 复用 MySQL / Chroma 独立部署）+ Embedding Provider 抽象
- P7+：评估 Reranker 提升检索精度

**proto 预留**（ai12 增补，建议 coord 在 ai.proto v2 预留）：

```protobuf
message EmbeddingRequest {
  string text = 1;
  string model = 2;  // text-embedding-3-small / bge-large-zh
}

message EmbeddingResponse {
  repeated float embedding = 1;
  int32 tokens = 2;
}
```

## 12. 多模态支持规划

### 12.1 P5：仅文本

- 出题：仅支持文本题目
- 优化：仅支持文本表达优化
- 聊天：仅支持文本对话

### 12.2 P6+：多模态评估

| 场景       | 模态      | 技术方案                            |
| ---------- | --------- | ----------------------------------- |
| 数学几何题 | 文本+图片 | GPT-4V / Claude 3 Vision 多模态 LLM |
| 英语听力题 | 文本+音频 | TTS 生成音频 + 文本题目             |
| 物理实验题 | 文本+图片 | 多模态 LLM + 实验图理解             |
| 化学反应题 | 文本+图片 | 多模态 LLM + 化学式图片识别         |

### 12.3 proto 预留（ai12 增补）

```protobuf
message MultimodalContent {
  string text = 1;
  optional string image_url = 2;       // 图片 URL
  optional string audio_url = 3;       // 音频 URL
  optional string image_base64 = 4;    // 图片 base64（小图）
}

message ChatRequest {
  repeated ChatMessage messages = 1;
  // ... 其他字段
  optional MultimodalContent multimodal = 10;  // v2 预留
}
```

## 13. LLM Provider 适配器详细设计

### 13.1 LLMProvider 抽象接口

```python
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator


class LLMProvider(ABC):
    """LLM Provider 抽象接口，所有适配器必须实现."""

    @property
    @abstractmethod
    def name(self) -> str:
        """Provider 名称（openai/anthropic/baichuan/local_ollama）."""

    @abstractmethod
    async def chat(self, messages: list[dict], model: str, temperature: float) -> dict:
        """非流式调用，返回 OpenAI 兼容响应 dict."""

    @abstractmethod
    async def stream_chat(self, messages: list[dict], model: str, temperature: float) -> AsyncGenerator[str, None]:
        """流式调用，yield SSE 格式 chunk."""

    @abstractmethod
    async def embed(self, text: str, model: str) -> list[float]:
        """Embedding 调用（P6+ 评估，P5 可 NotImplementedError）."""
```

### 13.2 适配器实现清单

| Provider              | API 协议         | 认证方式         | 备注                       |
| --------------------- | ---------------- | ---------------- | -------------------------- |
| `OpenAIProvider`      | OpenAI REST      | Bearer token     | 默认 Provider，gpt-4o-mini |
| `AnthropicProvider`   | Anthropic REST   | x-api-key header | 内部转换为 OpenAI 兼容格式 |
| `BaichuanProvider`    | OpenAI 兼容 REST | Bearer token     | 百川大模型，国内访问稳定   |
| `LocalOllamaProvider` | Ollama REST      | 无认证           | 本地部署，私有化场景       |

### 13.3 ProviderFailoverChain 故障切换

```python
class ProviderFailoverChain:
    """多 Provider 故障切换链.

    按优先级顺序尝试 Provider，失败自动切换到下一个。
    熔断器：单 Provider 连续 3 次失败触发熔断（60s 内不再尝试）。
    """

    def __init__(self, providers: list[LLMProvider], circuit_breaker: CircuitBreaker):
        self._providers = providers
        self._circuit_breaker = circuit_breaker

    async def chat(self, messages, model, temperature) -> dict:
        for provider in self._providers:
            if not self._circuit_breaker.is_closed(provider.name):
                continue  # 熔断中，跳过
            try:
                result = await provider.chat(messages, model, temperature)
                self._circuit_breaker.record_success(provider.name)
                return result
            except Exception as exc:
                self._circuit_breaker.record_failure(provider.name)
                logger.warning("provider_failover", from_=provider.name, error=str(exc))
                continue
        raise AllProvidersFailedError()
```

### 13.4 配置化路由

```python
# config.py
class Settings(BaseSettings):
    # Provider 优先级（按顺序 failover）
    llm_provider_priority: list[str] = ["openai", "anthropic", "baichuan", "local_ollama"]
    # 各 Provider 配置
    openai_api_key: str = ""
    openai_base_url: str = "https://api.openai.com/v1"
    anthropic_api_key: str = ""
    anthropic_base_url: str = "https://api.anthropic.com"
    baichuan_api_key: str = ""
    baichuan_base_url: str = "https://api.baichuan-ai.com/v1"
    ollama_base_url: str = ""  # 本地 Ollama，如 http://localhost:11434
    # 默认模型路由（model 名 → Provider）
    llm_model_routing: dict[str, str] = {
        "gpt-4o-mini": "openai",
        "gpt-4o": "openai",
        "claude-3-haiku": "anthropic",
        "baichuan2-13b": "baichuan",
        "qwen2.5-7b": "local_ollama",
    }
```

## 14. 性能与容量规划

### 14.1 SLO 指标

| 指标            | SLO          | 测量方式                       |
| --------------- | ------------ | ------------------------------ |
| 流式首字延迟    | < 2s（P95）  | `ai_llm_first_token_seconds`   |
| 非流式响应延迟  | < 10s（P95） | `ai_llm_call_duration_seconds` |
| 流式 chunk 间隔 | < 100ms      | 客户端测量                     |
| 工作流总耗时    | < 60s（P95） | `ai_workflow_duration_seconds` |
| 评估通过率      | > 80%        | `ai_evaluation_pass_rate`      |
| 服务可用性      | 99.5%        | 30 天滚动窗口                  |

### 14.2 容量规划

| 资源            | 单实例容量          | P5 预估需求         | 扩展策略           |
| --------------- | ------------------- | ------------------- | ------------------ |
| 并发 LLM 调用   | 50（受 httpx 限制） | 200（500 教师）     | 水平扩展（4 实例） |
| 并发流式 RPC    | 30                  | 100                 | 水平扩展（4 实例） |
| Redis 内存      | 100MB               | 500MB（工作流状态） | Redis 集群（P6+）  |
| Kafka 吞吐      | 1000 events/s       | 100 events/s        | 单分区足够         |
| 月度 token 消耗 | —                   | 10M tokens          | 按学校配额控制     |

### 14.3 流式背压处理

- **gRPC 流式 RPC**：默认支持背压（client 慢消费时 server 自动暂停 yield）
- **SSE**：FastAPI StreamingResponse 不支持背压，需手动检测 client 断开（`await request.is_disconnected()`）
- **超时**：流式 RPC 60s 无 chunk 自动断开；非流式 30s 超时

## 15. 测试策略

### 15.1 测试金字塔

| 层级     | 覆盖率目标 | 工具                            | 重点                                                        |
| -------- | ---------- | ------------------------------- | ----------------------------------------------------------- |
| 单元测试 | ≥ 80%      | pytest + pytest-asyncio         | LLMProvider 适配器 / RuleValidator / PIIRedactor / 模板渲染 |
| 集成测试 | 关键路径   | pytest + httpx + testcontainers | HTTP/gRPC 端点 / Redis / Kafka                              |
| 契约测试 | proto 一致 | pact-python                     | ai.proto 与 BFF 契约一致性                                  |
| 评估测试 | 生成质量   | 自研 + LLM-as-judge             | 题目质量评分（人工 + 自动）                                 |

### 15.2 LLM Mock 策略

- 测试环境不调真实 LLM（成本高 + 不稳定）
- `MockLLMProvider` 实现固定响应（按 prompt 模式匹配返回预设响应）
- 评估测试用真实 LLM（独立 CI job，低频运行）

## 16. 实施里程碑（ai12 增补）

### 16.1 M14（第 1 月）：基础架构

**目标**：建立 LLMProvider 抽象 + gRPC server 基础

| 交付物                                                          | 验收标准                 |
| --------------------------------------------------------------- | ------------------------ |
| `LLMProvider` 抽象接口 + OpenAIProvider 实现                    | 单元测试覆盖 ≥ 80%       |
| AnthropicProvider + BaichuanProvider + LocalOllamaProvider 实现 | 4 Provider 切换可用      |
| `ProviderFailoverChain` + CircuitBreaker                        | 故障切换测试通过         |
| gRPC server（Chat + StreamChat）                                | BFF 可调通 gRPC 流式 RPC |
| Redis 多维度限流（user/IP/school）                              | 限流命中准确             |
| 响应信封整改为 ActionState                                      | 004 §11.5 合规           |
| Dockerfile 多阶段构建                                           | 镜像 < 200MB             |

### 16.2 M15（第 2 月）：出题核心

**目标**：Prompt 模板管理 + 出题工作流 + 用量计费

| 交付物                                         | 验收标准              |
| ---------------------------------------------- | --------------------- |
| PromptTemplateService + Jinja2 渲染            | 5+ 模板可渲染         |
| Prompt 模板 CRUD API                           | HTTP/gRPC 均可用      |
| GenerateQuestion + StreamGenerateQuestion      | 题目逐字流式生成      |
| RuleValidator + LLMJudge + QualityGate         | 评估通过率 > 80%      |
| UsageRecorder + KafkaProducer                  | 用量事件落 ClickHouse |
| QuotaEnforcer（学校/教师配额）                 | 配额超限返回 429      |
| PIIRedactor + InputSanitizer + OutputModerator | 安全测试通过          |

### 16.3 M16（第 3 月）：备课工作流

**目标**：备课工作流 4 步编排端到端

| 交付物                                  | 验收标准             |
| --------------------------------------- | -------------------- |
| LessonPreparationWorkflow 4 步编排      | 端到端跑通           |
| WorkflowStateStore（Redis 持久化）      | 24h 内可恢复         |
| 教师审核流程（BFF 编排）                | 教师可审核/修改/拒绝 |
| content.CreateQuestions 入库            | 题目入库成功         |
| gRPC client（content / data-ana / iam） | 下游调用稳定         |
| 集成测试 + 契约测试                     | 关键路径覆盖         |
| 文档完善（README + known-issues）       | 文档同步更新         |

## 17. 与架构标准的对齐清单

### 17.1 通用架构原则对齐

| 原则                   | 对齐情况 | 说明                                                 |
| ---------------------- | -------- | ---------------------------------------------------- |
| DDD 限界上下文         | ✅       | D6.b 生成子域，边界清晰                              |
| CQRS                   | N/A      | ai 无 DB，无读写分离                                 |
| 事件驱动               | ✅       | AIUsageRecorded 事件外发，豁免 Outbox（004 §12.2）   |
| 12-factor              | ✅       | 配置通过环境变量 + pydantic-settings                 |
| SOLID                  | ✅       | LLMProvider 抽象（OCP/ISP）+ Service 单一职责（SRP） |
| Hexagonal architecture | ✅       | LLMProvider / ContentClient / DataAnaClient 均为端口 |

### 17.2 Resilience 模式对齐

| 模式                 | 实现                                   |
| -------------------- | -------------------------------------- |
| Circuit Breaker      | ProviderFailoverChain + CircuitBreaker |
| Retry with backoff   | LLM 调用 3 次重试（1s/2s/4s 指数退避） |
| Timeout              | LLM 30s / gRPC 60s / Redis 1s          |
| Bulkhead             | 单实例 50 并发 LLM 调用上限            |
| Failover             | 多 Provider 故障切换链                 |
| Graceful degradation | LLM 不可用返回骨架 + degraded:true     |

### 17.3 可观测性对齐

| 支柱    | 实现                                  | 与 004 §10 对齐 |
| ------- | ------------------------------------- | --------------- |
| Logs    | structlog + JSONRenderer + request_id | ✅              |
| Metrics | prometheus-client + 20+ 业务指标      | ✅              |
| Traces  | OpenTelemetry + gRPC interceptor      | ✅              |
| Health  | /healthz + /readyz（多维度检查）      | ✅              |

---

## 待 coord 交叉审查项（ai 相关）

| #        | 议题                                                                                 | 涉及文档                      |
| -------- | ------------------------------------------------------------------------------------ | ----------------------------- |
| 2        | 新增 `edu.insight.ai.usage` topic + `AIUsageEvent` proto message                     | 004 §7.2 + events.proto       |
| 4        | ai 实现 gRPC server 决策（含 StreamChat 流式 RPC）                                   | 004 §4.1                      |
| 6        | ai 备课工作流是否引入 Temporal                                                       | 004 §2.3                      |
| 7        | 端口冲突检查：ai 3008 HTTP + 50058 gRPC（ai12 增补：gRPC 端口待补登）                | full-stack-runbook + 004 §1.2 |
| 8        | 错误码前缀检查：`AI_*`（与其他服务不重叠）                                           | —                             |
| 9        | 黄金模板对齐：Python 服务无 NestJS 装饰器，权限校验用 FastAPI Depends 等价物是否认可 | —                             |
| ai12-A1  | ai gRPC 端口 50058 补登 004 §1.2 + infra/port-allocation.md                          | 004 §1.2                      |
| ai12-A2  | ai.proto 待补全（GenerateLessonPlan / StreamGenerateQuestion / 字段扩展）            | ai.proto                      |
| ai12-A3  | events.proto 待补 `AIUsageEvent` message（建议 schema 见 §3.3）                      | events.proto                  |
| ai12-A4  | iam GetEffectiveDataScope proto P4 补全（004 §15.3 #5 已仲裁）                       | iam.proto                     |
| ai12-A5  | 响应信封偏离 ActionState 强制整改（004 §11.5 已约束）                                | known-issues §2.9             |
| ai12-A6  | Prompt 模板存储位置（P5 YAML / P6+ DB 评估）                                         | —                             |
| ai12-A7  | 长运行工作流持久化（P5 Redis 24h / P6+ Temporal 评估）                               | 004 §2.3                      |
| ai12-A8  | 多模态支持规划（P5 文本 / P6+ 多模态评估），proto v2 预留字段                        | ai.proto v2                   |
| ai12-A9  | 评估框架归属（P5/P6 ai 内部 / P7+ 独立评估）                                         | —                             |
| ai12-A10 | RLHF 数据收集 `AIFeedbackRecorded` topic（P6+ 评估）                                 | 004 §7.2                      |

下一步：等待 coord 交叉审查通过后，进入阶段 3（按图实施）。

---

**AI Agent**: ai12 (ai) ← ai06（初稿）
**Branch**: 单仓库并行模式（直接提交 main）
**Coordinator**: coord-ai
