Files
Edu/docs/architecture/ai06-phase2-design.md
SpecialX 0a71b02e04
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped
fix: code compliance audit and fix across all services
NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
2026-07-09 17:28:27 +08:00

48 KiB
Raw Blame History

ai06 阶段 2 交付物:模块架构设计文档

AI 标识ai06 负责模块data-anaP4、aiP5 阶段:架构设计外包 · 阶段 2模块架构设计 日期2026-07-09 关联文档:ai06 阶段 1 确认书ai-allocation.md004 架构影响地图pending-features.md 审查请求:本设计文档待 coord 按 ai-allocation.md §8 交叉审查(接口一致性 / 端口冲突 / Topic 重复 / 错误码重叠 / 黄金模板对齐)


设计原则与全局约束

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

  1. 契约先行proto 已定义analytics.proto / ai.proto实现前不修改 proto如需修改走 coord 流程
  2. CQRS 读写分离data-ana 是纯读模型服务(无 MySQL 写ClickHouse 宽表由 CDC 投影构建
  3. 事件驱动data-ana 消费 CDC + 领域事件ai 不参与事件流(无状态)
  4. gRPC 优先004 §4.1 明确 BFF → 业务服务走 gRPC两服务需实现 gRPC serverHTTP 保留作 Gateway 直连降级)
  5. DataScope 过滤004 §5.3 DataScope 6 级在查询层注入 WHERE
  6. 三支柱可观测structlog + prometheus-client + OpenTelemetry已具备需补业务指标
  7. 降级模式外部依赖ClickHouse / LLM / 下游 gRPC不可用时返回骨架数据 + degraded: true
  8. Python 规范pydantic-settings 配置 / Pydantic 模型校验 / async 优先 / 类型注解强制 / ruff 零错误

模块架构设计文档 — data-ana

1. 模块内部分层图

flowchart TB
    subgraph Entry["入口层"]
        HTTP[FastAPI HTTP Router<br/>/analytics/* + /healthz + /readyz]
        GRPC[grpc.aio Server<br/>AnalyticsService]
    end

    subgraph Middleware["中间件层"]
        AUTH[AuthDepends<br/>校验 x-user-id / x-user-roles]
        SCOPE[DataScopeDepends<br/>注入 data_scope 元数据]
        TRACE[OTel FastAPIInstrumentor<br/>+ grpc.aio server interceptor]
    end

    subgraph Service["应用服务层 Application Service"]
        S1[AnalyticsService<br/>班级/学生/趋势查询编排]
        S2[MasteryService<br/>掌握度计算 + 事件发布]
        S3[ErrorBookService<br/>错题本查询]
    end

    subgraph Repo["数据访问层 Repository"]
        R1[ClickHouseRepository<br/>宽表查询 + DataScope WHERE 注入]
        R2[KafkaProducer<br/>mastery.updated 事件发布]
        R3[IamClient<br/>gRPC 调 iam.getEffectiveDataScope]
    end

    subgraph Consumer["CDC 消费者(后台任务)"]
        C1[CdcConsumer<br/>aiokafka AIOKafkaConsumer]
        C2[ExamCache<br/>exam_id→class_id 内存映射]
        C3[EventHandler<br/>grades/exams/homework/classes 路由]
    end

    subgraph Storage["存储 / 总线"]
        CH[(ClickHouse<br/>edu_analytics 库)]
        KAFKA[(Kafka<br/>edu-cdc.* + edu.insight.mastery.updated)]
        IAM[iam:3002 gRPC]
    end

    HTTP --> AUTH --> SCOPE --> S1
    HTTP --> S3
    GRPC --> S1
    S1 --> R1
    S3 --> R1
    S2 --> R1
    S2 --> R2
    SCOPE --> R3
    R1 --> CH
    R2 --> KAFKA
    R3 --> IAM

    C1 --> C3
    C3 --> C2
    C3 --> R1
    C1 --> KAFKA

分层规则

  • 入口层HTTP保留作 Gateway 直连降级)+ gRPC主入口BFF 调用)。两入口共享同一 Application Service
  • 中间件层FastAPI Depends 链(AuthDependsDataScopeDependsgRPC 用 server interceptor 注入身份元数据
  • 应用服务层:编排查询 / 计算掌握度 / 发布事件,不直接访问存储
  • 数据访问层ClickHouse 查询封装 + Kafka producer + gRPC client调 iam
  • CDC 消费者独立后台任务lifespan 启动),与 HTTP/gRPC 入口解耦

2. 领域模型

data-ana 是纯读模型服务,不持有写聚合根。领域模型为视图聚合ClickHouse 物化):

聚合根(视图型)

聚合根 含义 物化载体 不变式
StudentDashboard 学生学情宽表 ClickHouse student_dashboard_view 同一 (student_id, exam_id, knowledge_point_id) 仅保留最新版本
ClassPerformance 班级成绩聚合 ClickHouse 即时聚合(不物化) 聚合维度为 class_id + 时间窗
StudentErrorBook 学生错题本 ClickHouse student_errors 同一 (student_id, question_id) 累计 error_count
MasterySnapshot 知识点掌握度快照 ClickHouse mastery_snapshot新增 同一 (student_id, knowledge_point_id) 保留历史版本

值对象

  • WeakPointknowledge_point_id + title + mastery_level
  • TrendPointdate + score
  • DataScopelevel (SELF/CLASS/GRADE/SCHOOL/DISTRICT/ALL) + scope_ids具体可见的 class_id / grade_id 列表)

聚合间通信

  • 同服务内直接函数调用Application Service → Repository
  • 跨服务:仅通过 Kafka 事件(发布 mastery.updated+ gRPC调 iam 查 DataScope

3. 数据模型ClickHouse DDL

DDL 文件由 coord 统一管理在 infra/clickhouse/ddl/(待 coord 建立data-ana 提供内容。

3.1 宽表 student_dashboard_view

-- 学生学情宽表:每次成绩写入产生一行,按 ORDER BY 去重保留最新版本
CREATE TABLE IF NOT EXISTS student_dashboard_view
(
    student_id        String,
    class_id          String,
    exam_id           String,
    subject_id        String,
    score             Float64,
    rank_in_class     UInt32,
    knowledge_point_id String,
    mastery_level     Float32,       -- 0.0-1.0
    error_count       UInt32,
    last_updated      DateTime64(3, 'UTC')
)
ENGINE = ReplacingMergeTree(last_updated)
PARTITION BY toYYYYMM(last_updated)
ORDER BY (student_id, exam_id, knowledge_point_id)
SETTINGS index_granularity = 8192;

索引策略

  • ORDER BY (student_id, exam_id, knowledge_point_id):主键索引,支持按学生查学情、按考试查成绩、按知识点查掌握度
  • PARTITION BY toYYYYMM(last_updated):按月分区,支持历史数据归档
  • ReplacingMergeTree(last_updated):同 ORDER BY 自动去重,保留 last_updated 最大版本(幂等消费保证)

3.2 错题本 student_errors

CREATE TABLE IF NOT EXISTS student_errors
(
    student_id          String,
    question_id         String,
    knowledge_point_id  String,
    error_count         UInt32,
    last_error_time     DateTime64(3, 'UTC'),
    content             String
)
ENGINE = ReplacingMergeTree(last_error_time)
PARTITION BY toYYYYMM(last_error_time)
ORDER BY (student_id, question_id);

3.3 掌握度快照 mastery_snapshot新增

-- 知识点掌握度历史快照:每次掌握度计算产生新版本,支持趋势查询
CREATE TABLE IF NOT EXISTS mastery_snapshot
(
    student_id          String,
    knowledge_point_id  String,
    subject_id          String,
    mastery_level       Float32,
    calculated_at       DateTime64(3, 'UTC'),
    calculation_method  LowCardinality(String)  -- 'weighted_moving_avg' / 'simple_avg'
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(calculated_at)
ORDER BY (student_id, knowledge_point_id, calculated_at);

3.4 用量计费 ai_usage_log新增,供 ai 服务写入

-- AI 用量记录ai 服务通过 Kafka 事件投递data-ana 消费落库
CREATE TABLE IF NOT EXISTS ai_usage_log
(
    request_id          String,
    user_id             String,
    provider            LowCardinality(String),  -- 'openai' / 'anthropic' / 'baichuan' / 'local'
    model               LowCardinality(String),
    prompt_tokens       UInt32,
    completion_tokens   UInt32,
    total_tokens        UInt32,
    latency_ms          UInt32,
    success             Boolean,
    occurred_at         DateTime64(3, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (user_id, occurred_at);

3.5 读写分离策略

操作 路径 说明
学情查询 ClickHouse 宽表 实时聚合,亚秒级响应
错题本查询 ClickHouse student_errors 实时查询
掌握度趋势 ClickHouse mastery_snapshot 历史快照
掌握度计算 CDC 触发 → 内存计算 → 写 ClickHouse 派生数据,非事务写
DataScope 解析 gRPC 调 iam 实时查询,结果 Redis 缓存 5min004 §6.3

4. API 设计

4.1 HTTP 端点(保留作 Gateway 直连降级)

method path 权限 请求 响应 说明
GET /healthz {status, service} liveness
GET /readyz {status, ready, degraded, clickhouse, cdc_consumer} readiness
GET /metrics Prometheus 格式 指标
GET /analytics/class/{class_id}/performance ANALYTICS_CLASS_READ query: subject_id?, start_date?, end_date? ClassPerformanceResponse 班级成绩分析
GET /analytics/student/{student_id}/weakness ANALYTICS_STUDENT_READ query: subject_id? StudentWeaknessResponse 学生薄弱知识点DataScope 过滤)
GET /analytics/student/{student_id}/errorbook ANALYTICS_STUDENT_READ query: page?, size? StudentErrorBookResponse 学生错题本DataScope 过滤)
GET /analytics/student/{student_id}/trend ANALYTICS_STUDENT_READ query: start_date, end_date, subject_id? LearningTrendResponse 学习趋势(新增
GET /analytics/dashboard/teacher/{user_id} ANALYTICS_TEACHER_DASHBOARD query: class_id? TeacherDashboardResponse 教师仪表盘聚合(新增

4.2 gRPC 契约analytics.proto待实现 server

RPC 请求 响应 权限
GetClassPerformance GetClassPerformanceRequest{class_id, subject_id, start_date, end_date} ClassPerformance ANALYTICS_CLASS_READ
GetStudentWeakness GetStudentWeaknessRequest{student_id, subject_id} StudentWeakness ANALYTICS_STUDENT_READ
GetLearningTrend GetLearningTrendRequest{student_id, start_date, end_date} LearningTrend ANALYTICS_STUDENT_READ

权限校验gRPC server interceptor 从 metadata 提取 x-user-id / x-user-roles / x-data-scope,调用 AuthDepends 等价逻辑。

4.3 Pydantic 请求/响应模型

# 示例:班级成绩分析响应
class ClassPerformanceResponse(BaseModel):
    success: bool
    data: ClassPerformanceData
    degraded: bool = False

class ClassPerformanceData(BaseModel):
    class_id: str
    average_score: float
    pass_rate: float
    total_students: int
    scores: list[StudentScore] = []  # 详细成绩列表(受 DataScope 过滤)

class StudentScore(BaseModel):
    student_id: str
    score: float
    grade: str

5. 事件设计

5.1 消费的事件

Topic 来源 消息格式 消费动作
edu-cdc.next_edu_cloud.core_edu_grades Debezium CDCcore-edu MySQL Debezium JSONbefore/after/source/op/ts_ms 解析 → 查 ExamCache 填 class_id → 计算掌握度 → upsert student_dashboard_view
edu-cdc.next_edu_cloud.core_edu_exams Debezium CDC Debezium JSON upsert ExamCacheexam_id → class_id, subject_id
edu-cdc.next_edu_cloud.core_edu_homework_submissions Debezium CDC新增订阅 Debezium JSON 记录作业提交行为 → 更新 student_dashboard_view
edu-cdc.next_edu_cloud.classes Debezium CDC Debezium JSON 同步班级维度head_teacher_id用于教师 DataScope
edu-cdc.next_edu_cloud.iam_users Debezium CDC新增订阅 Debezium JSON 同步用户 dataScope 用于查询过滤(避免每次查 iam
edu.insight.ai.usage ai 服务 Kafka producer待 coord 新增 topic JSONUsageRecord 落 ai_usage_log 表

幂等性

  • CDC 事件:依赖 ClickHouse ReplacingMergeTree(last_updated) 引擎按 ORDER BY 去重
  • 领域事件(若消费):基于 event_id 去重Redis SETNXTTL 7 天)

5.2 发布的事件

事件 Topic 触发时机 消费者 Payload
MasteryUpdated edu.insight.mastery.updated 掌握度计算完成CDC grades 事件触发后异步计算) core-edu推荐个性化练习、msg掌握度预警 {event_id, student_id, knowledge_point_id, mastery_level, calculated_at}

发布实现Python 无 Outbox 模式):

  • 掌握度计算是派生数据(非业务事务写),不需要 Outbox 保证事务一致
  • 直接用 aiokafka.AIOKafkaProducer 发布,idempotent=true + 事务性 producer
  • 失败重试 3 次,仍失败记录日志 + 落 mastery_publish_failed 本地表(待 P6 评估是否引入 Outbox

6. 横切关注点对齐清单

6.1 权限装饰器等价物FastAPI Depends

# 权限点常量(与 iam 权限点对齐)
class Permissions:
    ANALYTICS_CLASS_READ = "analytics:class:read"
    ANALYTICS_STUDENT_READ = "analytics:student:read"
    ANALYTICS_TEACHER_DASHBOARD = "analytics:teacher:dashboard"

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

    从 x-user-id / x-user-roles 头提取身份,校验角色是否含 permission。
    """
    ...

async def inject_data_scope(ctx: UserContext = Depends(require_permission(...)))-> DataScope:
    """注入 DataScope从 iam.getEffectiveDataScope 查询Redis 缓存 5min."""
    ...

6.2 错误码清单(前缀 DATA_ANA_*

错误码 触发条件 HTTP gRPC status
DATA_ANA_UNAUTHORIZED 缺失 x-user-id 头或 token 无效 401 UNAUTHENTICATED
DATA_ANA_FORBIDDEN 角色无对应权限 403 PERMISSION_DENIED
DATA_ANA_DATASCOPE_VIOLATION 查询目标超出 DataScope 范围 403 PERMISSION_DENIED
DATA_ANA_CLICKHOUSE_UNAVAILABLE ClickHouse 不可达(降级模式仍返回骨架) 200 + degraded:true OK + degraded flag
DATA_ANA_INVALID_DATE_RANGE start_date > end_date 400 INVALID_ARGUMENT
DATA_ANA_STUDENT_NOT_FOUND student_id 不存在 404 NOT_FOUND
DATA_ANA_CLASS_NOT_FOUND class_id 不存在 404 NOT_FOUND
DATA_ANA_INTERNAL_ERROR 未捕获异常 500 INTERNAL

6.3 Logger 初始化

  • 位置:main.py init_logger()(已具备)
  • 配置:structlog.make_filtering_bound_logger(level) + TimeStamper(fmt="iso") + ConsoleRenderer
  • 改进:生产环境改用 structlog.processors.JSONRenderer()(当前 ConsoleRenderer 适合开发)

6.4 Metrics 指标清单

指标名 类型 标签 描述
data_ana_http_requests_total Counter method, path, status HTTP 请求总数
data_ana_http_request_duration_seconds Histogram method, path HTTP 请求延迟
data_ana_clickhouse_query_duration_seconds Histogram query_type ClickHouse 查询延迟
data_ana_clickhouse_query_total Counter query_type, status ClickHouse 查询总数
data_ana_cdc_events_consumed_total Counter table, op CDC 事件消费总数
data_ana_cdc_event_process_duration_seconds Histogram table CDC 事件处理延迟
data_ana_cdc_consumer_lag Gauge topic, partition CDC 消费者 lag
data_ana_mastery_calculated_total Counter 掌握度计算次数
data_ana_mastery_published_total Counter status mastery.updated 事件发布数
data_ana_datascope_cache_hits_total Counter DataScope 缓存命中

6.5 Tracer 初始化

  • 位置:main.py init_tracer()(已具备)
  • endpointsettings.otel_endpoint + /v1/traces
  • 改进gRPC server 注册 grpc.aio.ServerInterceptor 透传 W3C trace context

6.6 /healthz 检查逻辑

  • liveness仅进程存活已具备

6.7 /readyz 检查逻辑

async def readyz() -> dict:
    return {
        "status": "ok" if all_ready else "degraded",
        "ready": all_ready,
        "degraded": not all_ready,
        "clickhouse": "ok" | "unreachable" | "not_configured",
        "cdc_consumer": "running" | "disabled" | "failed",
        "kafka_brokers": settings.kafka_brokers or None,
        "iam_grpc": "ok" | "unreachable",  # 新增iam gRPC 连通性
        "timestamp": datetime.now(UTC).isoformat(),
    }

6.8 优雅关闭顺序

  1. HTTP server stop accepting new requestsuvicorn graceful shutdown
  2. gRPC server graceful stop等待在途 RPC 完成30s 超时)
  3. CDC consumer stop等待在途消息处理完成commit offset
  4. Kafka producer flush + close确保 mastery.updated 事件已投递)
  5. ClickHouse client close
  6. iam gRPC channel close

信号处理:注册 signal.SIGTERM handler触发上述顺序。

7. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途
被调用 api-gateway HTTP /analytics/* Gateway 代理
被调用 teacher-bff / student-bff gRPC AnalyticsService.* BFF 聚合查询
被调用 ai gRPC AnalyticsService.GetStudentWeakness / GetLearningTrend AI 个性化出题上下文
调用 iam gRPC IamService.GetEffectiveDataScope待 proto 新增 DataScope 解析
消费 core-eduCDC Kafka edu-cdc.next_edu_cloud.core_edu_grades/exams/homework_submissions 学情数据投递
消费 core-eduCDC Kafka edu-cdc.next_edu_cloud.classes 班级维度同步
消费 iamCDC Kafka edu-cdc.next_edu_cloud.iam_users 用户 dataScope 同步
消费 ai Kafka edu.insight.ai.usage待 coord 新增 AI 用量落库
发布 Kafka edu.insight.mastery.updated 掌握度更新通知 core-edu / msg

8. 风险与假设

8.1 假设

  • 假设 1iam 提供 GetEffectiveDataScope(userId) → DataScope gRPC API。若 iam 未提供fallback 为:从 x-user-roles 头推导admin=ALL, teacher=CLASS_TAUGHT, student=SELF但无法支持细粒度年级/学校范围
  • 假设 2core-edu 的 core_edu_homework_submissions 表存在 binlog。若不存在作业相关学情无法通过 CDC 获取,需 core-edu 补表或走 Outbox 事件
  • 假设 3ClickHouse ReplacingMergeTree 在查询时需 FINAL 关键字确保去重生效。当前查询未加 FINAL,可能读到重复版本。修复:所有查询加 FINAL 或使用 argMax 聚合
  • 假设 4coord 同意新增 edu.insight.ai.usage topic。若不同意ai 服务的用量计费需自建记录(破坏 ai 无状态原则)

8.2 技术风险

风险 影响 缓解措施
ClickHouse 查询延迟超 5s 违反 P4 退出标准 宽表索引优化 + 物化视图预聚合 + 查询超时 3s 降级
CDC 消费者 lag 过大 学情数据延迟 > 5s 监控 lag + 告警 + 水平扩展消费者(分区数提升)
ExamCache 内存泄漏 长期运行 OOM LRU 淘汰策略max 10000 条)+ 定期清理过期 exam
mastery.updated 事件丢失 下游 core-edu/msg 收不到通知 Kafka producer acks=all + 本地失败表重试
iam gRPC 不可达 DataScope 无法解析 → 查询降级为 SELF Redis 缓存 5min + fallback SELF 范围(最保守)
ClickHouse FINAL 查询性能 查询变慢 使用 argMax 替代 FINAL,或在写入时去重

8.3 未决设计决策(需 coord 仲裁)

  1. mastery.updated 发布是否需要 OutboxPython 无 Outbox 模式,建议直接 producer但 004 §12.2 明确"事件发布Outbox 模式 / 禁止直接 Kafka producer"。冲突data-ana 是 Python 服务无 MySQL 写事务Outbox 不适用。建议 coord 裁定:派生数据事件(非业务事务)允许直接 producer
  2. iam GetEffectiveDataScope proto 新增:当前 iam.proto 仅有 GetUserInfo,无 DataScope 解析 API。需 coord 在 shared-proto 新增 GetEffectiveDataScope RPC
  3. edu.insight.ai.usage topic 新增004 §7.2 未列出,需 coord 确认是否新增

模块架构设计文档 — ai

1. 模块内部分层图

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

分层规则

  • 入口层HTTP保留作 Gateway 直连)+ gRPC主入口StreamChat 流式 RPC
  • 中间件层Auth + RateLimitRedis 令牌桶,按 user_id 限流)
  • 应用服务层4 个 Service每个对应一类 AI 能力
  • Provider 适配层:抽象 LLMProvider 接口,多适配器实现(策略模式)
  • Prompt 模板:模板注册 + 渲染模板存储可配置YAML 文件 or DB
  • 下游 clientgRPC 调 content / data-ana
  • 用量计费token 消耗统计 + Kafka 事件外发

2. 领域模型

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

聚合根(请求型,无持久化)

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

值对象

  • ChatMessagerole + content
  • Usageprompt_tokens + completion_tokens + total_tokens
  • GeneratedQuestionquestion + answer + explanation
  • PromptTemplatename + system_prompt + user_template + variables

工作流编排(备课)

sequenceDiagram
    participant T as 教师
    participant BFF as teacher-bff
    participant AI as ai 服务
    participant Content as content
    participant DA as data-ana

    T->>BFF: 请求备课class_id, subject_id
    BFF->>AI: gRPC GenerateLessonPlan
    AI->>DA: gRPC GetStudentWeakness(class_id)
    DA-->>AI: 薄弱知识点列表
    AI->>Content: gRPC GetPrerequisites(knowledge_point_id)
    Content-->>AI: 前置依赖知识点
    AI->>AI: LLM 生成题目(基于学情 + 知识点)
    AI-->>BFF: 题目列表 + 推荐理由
    BFF-->>T: 题目供审核
    T->>BFF: 确认入库
    BFF->>Content: gRPC CreateQuestions
    Content-->>BFF: 入库成功

工作流实现

  • 简单场景4 步内FastAPI BackgroundTasks + asyncio.gather 并行查询
  • 复杂场景(含教师审核等待):待 coord 仲裁是否引入 Temporal004 §2.3 列出 Temporal 用于 AI 编排)

3. 数据模型

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

用量计费Kafka 事件 → data-ana 落 ClickHouse

{
  "event_id": "uuid",
  "user_id": "user-xxx",
  "request_id": "req-xxx",
  "provider": "openai",
  "model": "gpt-4o-mini",
  "prompt_tokens": 150,
  "completion_tokens": 80,
  "total_tokens": 230,
  "latency_ms": 1200,
  "success": true,
  "occurred_at": "2026-07-09T12:00:00Z"
}

Topicedu.insight.ai.usage待 coord 新增

缓存策略

数据 存储 TTL 失效策略
Prompt 模板 Redis模板变更事件驱动失效 1 小时 文件/DB 变更时主动失效
LLM 响应(相同 prompt Redishash 缓存) 30 分钟 短 TTL避免陈旧
DataScopeai 不需要,仅 data-ana 用)
限流计数 Redis 令牌桶 滑动窗口 自动过期

4. API 设计

4.1 HTTP 端点(保留作 Gateway 直连降级)

method path 权限 请求体 响应 说明
GET /healthz {status, service} liveness
GET /readyz {status, llm_configured, degraded} readiness
GET /metrics Prometheus 指标
POST /ai/chat AI_CHAT ChatRequest ChatResponse LLM 聊天
POST /ai/chat/stream AI_CHAT ChatRequest SSE stream 流式聊天
POST /ai/generate/question AI_QUESTION_GENERATE GenerateQuestionRequest GeneratedQuestionResponse 生成题目
POST /ai/optimize/expression AI_EXPRESSION_OPTIMIZE OptimizeExpressionRequest OptimizedExpressionResponse 优化表达
POST /ai/lesson/preparation AI_LESSON_PREPARE LessonPreparationRequest LessonPreparationResponse 备课工作流(新增

4.2 gRPC 契约ai.proto待实现 server

RPC 请求 响应 权限 说明
Chat ChatRequest{messages, model, temperature} ChatResponse{content, model, usage} AI_CHAT 非流式聊天
StreamChat ChatRequest stream ChatChunk AI_CHAT 流式聊天SSE over gRPC
GenerateQuestion GenerateQuestionRequest{prompt, subject, difficulty} GeneratedQuestion AI_QUESTION_GENERATE 生成题目
OptimizeExpression OptimizeExpressionRequest{text, context} OptimizedExpression AI_EXPRESSION_OPTIMIZE 优化表达

4.3 Pydantic 请求/响应模型

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

class ChatMessage(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str

class ChatResponse(BaseModel):
    success: bool
    data: ChatData
    degraded: bool = False

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

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

class GenerateQuestionRequest(BaseModel):
    prompt: str = Field(..., min_length=1, max_length=2000)
    subject: str
    difficulty: Literal["easy", "medium", "hard"]
    knowledge_point_ids: list[str] = []  # 可选:靶向知识点

class GeneratedQuestionResponse(BaseModel):
    success: bool
    data: GeneratedQuestionData
    degraded: bool = False

5. 事件设计

5.1 消费的事件

ai 服务不消费任何事件(无状态,纯请求-响应)。

5.2 发布的事件

事件 Topic 触发时机 消费者 Payload
AIUsageRecorded edu.insight.ai.usage待 coord 新增 每次 LLM 调用完成 data-ana落 ClickHouse ai_usage_log {event_id, user_id, request_id, provider, model, prompt_tokens, completion_tokens, total_tokens, latency_ms, success, occurred_at}

发布实现

  • 每次 LLM 调用后异步发布(不阻塞响应)
  • aiokafka.AIOKafkaProducer + acks=all
  • 失败重试 3 次,仍失败记录日志(不影响主流程)

6. 横切关注点对齐清单

6.1 权限装饰器等价物

class Permissions:
    AI_CHAT = "ai:chat"
    AI_QUESTION_GENERATE = "ai:question:generate"
    AI_EXPRESSION_OPTIMIZE = "ai:expression:optimize"
    AI_LESSON_PREPARE = "ai:lesson:prepare"

async def require_permission(permission: str) -> UserContext:
    """从 x-user-id / x-user-roles 校验权限."""
    ...

6.2 错误码清单(前缀 AI_*

错误码 触发条件 HTTP gRPC status
AI_UNAUTHORIZED 缺失 x-user-id 或 token 无效 401 UNAUTHENTICATED
AI_FORBIDDEN 角色无对应权限 403 PERMISSION_DENIED
AI_RATE_LIMITED 触发限流 429 RESOURCE_EXHAUSTED
AI_LLM_UNAVAILABLE LLM Provider 不可达(降级模式仍返回骨架) 200 + degraded:true OK + degraded flag
AI_LLM_TIMEOUT LLM 调用超时30s 504 DEADLINE_EXCEEDED
AI_INVALID_MODEL model 名不支持 400 INVALID_ARGUMENT
AI_INVALID_DIFFICULTY difficulty 不在 easy/medium/hard 400 INVALID_ARGUMENT
AI_DOWNSTREAM_UNAVAILABLE content / data-ana gRPC 不可达 502 UNAVAILABLE
AI_PROMPT_RENDER_FAILED Prompt 模板渲染失败(变量缺失) 500 INTERNAL
AI_INTERNAL_ERROR 未捕获异常 500 INTERNAL

6.3 Logger 初始化

  • 位置:main.py(已具备)
  • 改进:生产环境改用 JSONRenderer

6.4 Metrics 指标清单

指标名 类型 标签 描述
ai_http_requests_total Counter method, path, status HTTP 请求总数
ai_http_request_duration_seconds Histogram method, path HTTP 请求延迟
ai_llm_calls_total Counter provider, model, status LLM 调用总数
ai_llm_call_duration_seconds Histogram provider, model LLM 调用延迟
ai_llm_tokens_total Counter provider, model, type (prompt/completion) token 消耗总数
ai_llm_stream_chunks_total Counter provider, model 流式 chunk 总数
ai_grpc_calls_total Counter downstream, method, status 下游 gRPC 调用总数
ai_rate_limit_hits_total Counter user_id 限流命中次数
ai_usage_events_published_total Counter status 用量事件发布数
ai_prompt_template_renders_total Counter template_name, status 模板渲染次数

6.5 Tracer 初始化

  • 位置:main.py init_tracer()已具备dev_mode 跳过)
  • 改进gRPC server interceptor + 下游 gRPC client interceptor 透传 trace context

6.6 /healthz 检查逻辑

  • liveness仅进程存活已具备

6.7 /readyz 检查逻辑

async def readyz() -> dict:
    return {
        "status": "ok",
        "service": "ai",
        "llm_configured": settings.llm_available,
        "degraded": not settings.llm_available,
        "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",  # 新增
            "data_ana": "ok" | "unreachable",  # 新增
        },
        "redis": "ok" | "unreachable",  # 新增(限流依赖)
    }

6.8 优雅关闭顺序

  1. HTTP server stop accepting new requests
  2. gRPC server graceful stop关键:等待在途 StreamChat 流式 RPC 完成60s 超时,避免截断用户响应)
  3. LLM 流式请求 drain等待 httpx stream 完成)
  4. Kafka producer flush + close确保用量事件已投递
  5. 下游 gRPC channels closecontent / data-ana
  6. Redis connection close

7. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途
被调用 api-gateway HTTP /ai/* Gateway 代理
被调用 teacher-bff gRPC AiService.* BFF 聚合 AI 能力
调用 content gRPC KnowledgeGraphService.GetPrerequisites / GetLearningPath 出题上下文查询
调用 content gRPC TextbookService.*(若需教材上下文) 出题教材关联
调用 data-ana gRPC AnalyticsService.GetStudentWeakness / GetLearningTrend 个性化出题学情查询
调用 LLM Provider HTTP OpenAI 兼容 REST /chat/completions LLM 推理
发布 Kafka edu.insight.ai.usage待 coord 新增 用量计费外发

8. 风险与假设

8.1 假设

  • 假设 1content 服务实现了 KnowledgeGraphService gRPC server。当前 content.proto 已定义但未实现 gRPC serverai05 阶段 2 设计中。ai 调用前需确认 content gRPC 可用
  • 假设 2data-ana 实现了 AnalyticsService gRPC server本设计文档已设计。ai 调用前需确认 data-ana gRPC 可用
  • 假设 3coord 同意新增 edu.insight.ai.usage topic。若不同意用量计费降级为 ai 本地日志(不落 ClickHouse影响成本分析
  • 假设 4Redis 可用(限流依赖)。若 Redis 不可用,限流降级为"无限制"风险LLM 成本失控),或降级为内存令牌桶(单实例有效,多实例不一致)

8.2 技术风险

风险 影响 缓解措施
LLM 调用延迟高(>30s 用户体验差 超时 30s + 降级骨架响应 + 流式优先(用户感知首字延迟)
LLM 成本失控 财务风险 Redis 令牌桶限流(每用户每分钟 10 次)+ 用量计费监控 + 告警阈值
LLM Provider 单点故障 服务不可用 多 Provider 适配器 + 自动 failoverOpenAI 失败切 Anthropic
流式 RPC 中断 用户响应截断 gRPC server graceful shutdown 60s drain + 客户端重连机制
Prompt 注入攻击 LLM 输出恶意内容 输入 sanitize + system prompt 加安全约束 + 输出过滤
下游 gRPC 不可达 备课工作流失败 降级:跳过学情查询,仅基于 prompt 生成题目 + degraded: true

8.3 未决设计决策(需 coord 仲裁)

  1. 备课工作流是否引入 Temporal004 §2.3 列出 Temporal 用于 AI 编排,但简单 4 步工作流可用 FastAPI BackgroundTasks。建议M14 用 BackgroundTasksM15 评估是否迁移 Temporal
  2. edu.insight.ai.usage topic 新增:需 coord 在 shared-proto events.proto 新增 AIUsageEvent message + 004 §7.2 新增 topic
  3. LLM Provider 配置化路由:是否在 shared-py 建立通用 LLMProvider 抽象(供未来其他 Python 服务复用)。建议 P5 阶段在 ai 服务内部实现P6 评估是否提取到 shared-py
  4. Prompt 模板存储位置YAML 文件(简单,无 DBvs DB动态更新。建议 P5 用 YAML 文件(services/ai/src/ai/prompts/*.yamlP6 评估迁移 DB

阶段 2 总结

ai06 已完成阶段 2 模块架构设计,产出 data-ana 与 ai 两份设计文档。核心设计决策:

data-ana 设计要点

  1. 分层HTTP + gRPC 双入口共享 Application ServiceCDC 消费者独立后台任务
  2. 数据模型4 张 ClickHouse 宽表student_dashboard_view / student_errors / mastery_snapshot / ai_usage_logReplacingMergeTree 引擎保证幂等
  3. 权限FastAPI Depends 链require_permission + inject_data_scopeDataScope WHERE 注入 ClickHouse 查询
  4. 事件:消费 6 个 CDC topic + 发布 edu.insight.mastery.updated(直接 producer非 Outbox因派生数据非事务写
  5. gRPC:实现 AnalyticsService serveranalytics.proto 已定义)
  6. 降级ClickHouse 不可达返回骨架 + degraded:trueiam gRPC 不可达降级为 SELF DataScope

ai 设计要点

  1. 分层HTTP + gRPC 双入口LLM Provider 适配层策略模式4 适配器)
  2. 无状态:无 DB用量计费通过 Kafka 事件外发
  3. 权限FastAPI Depends + Redis 令牌桶限流(每用户每分钟 10 次)
  4. 工作流:备课 4 步编排(学情查询 → 知识点推荐 → 题目生成 → 教师审核入库),简单场景用 BackgroundTasks
  5. gRPC:实现 AiService serverStreamChat 流式 RPCgRPC client 调 content / data-ana
  6. 降级LLM 不可达返回骨架 + degraded:true下游 gRPC 不可达降级跳过

待 coord 交叉审查项(汇总)

# 议题 涉及文档
1 data-ana 发布 edu.insight.mastery.updated 用直接 producer非 Outbox是否合规 004 §12.2
2 新增 edu.insight.ai.usage topic + AIUsageEvent proto message 004 §7.2 + events.proto
3 iam 新增 GetEffectiveDataScope gRPC RPC iam.proto
4 data-ana / ai 实现 gRPC server 决策 004 §4.1
5 ClickHouse DDL 管理位置(建议 infra/clickhouse/ddl/ infra/
6 ai 备课工作流是否引入 Temporal 004 §2.3
7 端口冲突检查data-ana 3006 / ai 3008无冲突 full-stack-runbook
8 错误码前缀检查:DATA_ANA_* / AI_*(与其他服务不重叠)
9 黄金模板对齐Python 服务无 NestJS 装饰器,权限校验用 FastAPI Depends 等价物是否认可

下一步:等待 coord 交叉审查通过后,进入阶段 3按图实施