# 模块架构设计文档 — 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
/ai/* + /healthz + /readyz] GRPC[grpc.aio Server
AiService 含 StreamChat] end subgraph Middleware["中间件层"] AUTH[AuthDepends
校验 x-user-id / x-user-roles] RATE[RateLimitDepends
Redis 令牌桶限流] TRACE[OTel + grpc interceptor] end subgraph Service["应用服务层"] S1[ChatService
聊天编排 + Prompt 模板渲染] S2[QuestionGenerationService
出题工作流编排] S3[ExpressionOptimizationService
表达优化] S4[LessonPreparationWorkflow
备课工作流 4 步编排] end subgraph Provider["LLM Provider 适配层"] P0[LLMProvider 抽象接口
chat / stream_chat] P1[OpenAIProvider
httpx 异步] P2[AnthropicProvider
httpx 异步] P3[BaichuanProvider
httpx 异步] P4[LocalOllamaProvider
httpx 异步] end subgraph Template["Prompt 模板管理"] T1[PromptTemplateRegistry
模板注册 + 渲染] T2[模板存储
YAML 文件 / DB] end subgraph Client["下游 gRPC client"] C1[ContentClient
查询知识点 / 题库] C2[DataAnaClient
查询学情 / 薄弱点] end subgraph Usage["用量计费"] U1[UsageRecorder
token 消耗统计] U2[KafkaProducer
发布 edu.insight.ai.usage] end subgraph External["外部 / 存储"] LLM[LLM Provider API
OpenAI/Anthropic/百川/Ollama] CONTENT[content:3005 gRPC] DATAANA[data-ana:3006 gRPC] KAFKA[(Kafka)] REDIS[(Redis
限流 + 缓存)] 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
/ai/v1/* + /healthz + /readyz + /metrics] GRPC[grpc.aio Server
AiService 含 StreamChat/StreamGenerateQuestion] end subgraph Middleware["中间件层"] AUTH[AuthDepends
JWT claims → UserContext] RATE[RateLimitDepends
Redis 多维度令牌桶
user/IP/school/token] TRACE[OTel FastAPIInstrumentor
+ grpc.aio ServerInterceptor
+ grpc.aio ClientInterceptor] REQID[RequestContextMiddleware
X-Request-Id 注入与透传] end subgraph Security["安全层 ai12 增补"] PII[PIIRedactor
学生姓名/手机号脱敏] SAN[InputSanitizer
Prompt 注入防御] MOD[OutputModerator
敏感词过滤 + 安全校验] end subgraph Service["应用服务层"] S1[ChatService
聊天编排 + Prompt 渲染] S2[QuestionGenerationService
出题 + 逐字流式生成] S3[ExpressionOptimizationService
表达优化] S4[LessonPreparationWorkflow
备课工作流 4 步编排] S5[PromptTemplateService
模板 CRUD + 版本化] S6[UsageService
用量查询 + 配额校验] end subgraph Provider["LLM Provider 适配层"] P0[LLMProvider 抽象接口
chat / stream_chat / embed?] P1[OpenAIProvider] P2[AnthropicProvider] P3[BaichuanProvider] P4[LocalOllamaProvider] PF[ProviderFailoverChain
多 Provider 故障切换] end subgraph Template["Prompt 模板管理"] T1[PromptTemplateRegistry
模板注册 + Jinja2 渲染] T2[模板存储
YAML 文件 P5 / DB P6+] T3[模板版本化 + A/B 路由 P6+] end subgraph Evaluation["评估层 ai12 增补"] E1[RuleValidator
题型/答案/解析规则校验] E2[LLMJudge
LLM-as-judge 质量评分] E3[QualityGate
质量门禁 不达标重新生成或降级] end subgraph Workflow["工作流持久化 ai12 增补"] W1[WorkflowStateStore
Redis 短期 24h TTL P5
Temporal P6+ 评估] W2[WorkflowEventLog
工作流步骤事件日志] end subgraph Cache["缓存层 ai12 增补"] CA1[PromptCache
hash 缓存 P5] CA2[SemanticCache
embedding 相似度 P6+] CA3[TemplateCache
模板缓存 1h TTL] end subgraph Client["下游 gRPC client"] C1[ContentClient
查询知识点/教材/题库] C2[DataAnaClient
查询学情/薄弱点/趋势] C3[IamClient
查询 DataScope P4+] end subgraph Usage["用量计费"] U1[UsageRecorder
token 消耗统计] U2[KafkaProducer
发布 edu.insight.ai.usage] U3[QuotaEnforcer
学校/年级/教师配额校验] end subgraph External["外部 / 存储"] LLM[LLM Provider API
OpenAI/Anthropic/百川/Ollama] CONTENT[content:3005 gRPC] DATAANA[data-ana:3006 gRPC] IAM[iam:3002 gRPC] KAFKA[(Kafka)] REDIS[(Redis
限流/缓存/工作流状态)] 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 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` | LLM 聊天 | | POST | `/ai/v1/chat/stream` | `AI_CHAT` | `ChatRequest` | SSE stream | 流式聊天 | | POST | `/ai/v1/generate/question` | `AI_QUESTION_GENERATE` | `GenerateQuestionRequest` | `ActionState` | 生成题目 | | POST | `/ai/v1/generate/question/stream` | `AI_QUESTION_GENERATE` | `GenerateQuestionRequest` | SSE stream(题目逐字生成) | **ai12 增补**:题目逐字流式 | | POST | `/ai/v1/optimize/expression` | `AI_EXPRESSION_OPTIMIZE` | `OptimizeExpressionRequest` | `ActionState` | 优化表达 | | POST | `/ai/v1/lesson/preparation` | `AI_LESSON_PREPARE` | `LessonPreparationRequest` | `ActionState` | **ai12 增补**:备课工作流启动 | | GET | `/ai/v1/lesson/preparation/{workflow_id}` | `AI_LESSON_PREPARE` | — | `ActionState` | **ai12 增补**:查询工作流状态 | | POST | `/ai/v1/lesson/preparation/{workflow_id}/confirm` | `AI_LESSON_PREPARE` | `ConfirmRequest` | `ActionState` | **ai12 增补**:教师确认入库 | | GET | `/ai/v1/prompts` | `AI_PROMPT_READ` | query: `?page&size` | `ActionState>` | **ai12 增补**:模板列表 | | POST | `/ai/v1/prompts` | `AI_PROMPT_CREATE` | `CreatePromptRequest` | `ActionState` | **ai12 增补**:创建模板 | | GET | `/ai/v1/prompts/{id}` | `AI_PROMPT_READ` | — | `ActionState` | **ai12 增补**:获取模板 | | PUT | `/ai/v1/prompts/{id}` | `AI_PROMPT_UPDATE` | `UpdatePromptRequest` | `ActionState` | **ai12 增补**:更新模板(版本化) | | GET | `/ai/v1/usage/me` | `AI_USAGE_READ` | query: `?start_date&end_date` | `ActionState` | **ai12 增补**:当前用户用量 | | GET | `/ai/v1/usage/school/{school_id}` | `AI_USAGE_READ_ALL` | query: `?start_date&end_date` | `ActionState` | **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 检测
正则 + NER 模型] Detect --> Redact[脱敏替换
学生姓名→学生A
手机号→***] Redact --> Log[脱敏日志
记录脱敏操作] 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 注入(`