# 实施计划:P14 — AI 模块改造 — DashScope 迁移 + 调度器完善 ## 概述 按依赖关系从底层到顶层逐步实施:环境变量/配置 → DashScope 客户端 → 防护层(熔断/限流/预算) → 运行日志 → 调度器 → API 路由 → 服务适配 → ETL 触发 → 数据库迁移 → 收尾。每个任务构建在前序任务之上,确保无孤立代码。 ## 任务 - [x] 1. 环境变量与配置基础 - [x] 1.1 重写 `apps/backend/app/ai/config.py`,实现 `AIConfig` dataclass - 定义所有 `DASHSCOPE_*` 环境变量字段和 `INTERNAL_API_TOKEN` - 实现 `from_env()` 类方法,缺失必需变量时立即抛出异常 - 删除所有 `BAILIAN_*` 引用 - _需求: 2.1, 2.2, 2.3, 2.5_ - [x] 1.2 编写属性测试:环境变量校验完整性 - **Property 2: 环境变量校验完整性** - 对任意必需变量子集缺失,`from_env()` 应抛出异常,不返回含空字符串的配置 - **验证: 需求 2.5** - [x] 1.3 更新 `.env`、`.env.template`、`pyproject.toml` - `.env` / `.env.template`:`BAILIAN_*` → `DASHSCOPE_*`,新增 `DASHSCOPE_WORKSPACE_ID`、`INTERNAL_API_TOKEN` - `pyproject.toml`:移除 `openai` 依赖,新增 `dashscope` 依赖 - _需求: 2.4, 1.7_ - [x] 2. DashScopeClient 核心客户端 - [x] 2.1 创建 `apps/backend/app/ai/dashscope_client.py`,实现 `DashScopeClient` 类 - `call_app()` — App2~8 单轮调用,`asyncio.to_thread()` 包装,返回 `(parsed_json, tokens_used, new_session_id)` - `call_app_stream()` — App1 流式调用,线程消费同步迭代器 + `asyncio.Queue` 桥接 async generator - `_call_with_retry()` — 指数退避重试(1s→2s→4s),HTTP 4xx 不重试,5xx/超时/连接错误重试 - 非合法 JSON 响应纯重试(最大 3 次),不做本地修复 - _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_ - [x] 2.2 定义异常层级 - `DashScopeError` 基类及子类:`DashScopeApiError`、`DashScopeAuthError`、`DashScopeTimeoutError`、`DashScopeJsonParseError`、`CircuitOpenError`、`RateLimitExceededError`、`BudgetExceededError` - _需求: 1.5, 5.6, 6.3, 7.3_ - [x] 2.3 编写属性测试:重试策略正确性 - **Property 1: 重试策略正确性** - 4xx 立即抛出不重试,5xx/超时/连接错误最多重试 3 次,非法 JSON 触发重试 - **验证: 需求 1.5, 1.6** - [x] 2.4 编写属性测试:Application API 响应解析 - **Property 20: Application API 响应解析** - 合法 JSON 正确解析为 dict,非法 JSON 触发重试 - **验证: 需求 4.4** - [x] 3. 检查点 — 确保所有测试通过 - 确保所有测试通过,ask the user if questions arise. - [x] 4. 防护层:熔断器、限流器、Token 预算 - [x] 4.1 创建 `apps/backend/app/ai/circuit_breaker.py`,实现 `CircuitBreaker` 类 - 按 `app_id` 独立计数,`_BreakerState` 内部状态 - `check()` / `record_success()` / `record_failure()` 方法 - 状态机:CLOSED → OPEN(连续 5 次失败)→ HALF_OPEN(60 秒后)→ CLOSED/OPEN - _需求: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6_ - [x] 4.2 编写属性测试:熔断器 app_id 隔离 + 状态机转换 - **Property 5: 熔断器 app_id 隔离** - **Property 6: 熔断器状态机转换** - 不同 app_id 互不影响;状态转换符合 CLOSED→OPEN→HALF_OPEN→CLOSED/OPEN 规则 - **验证: 需求 5.1, 5.2, 5.3, 5.4, 5.5** - [x] 4.3 创建 `apps/backend/app/ai/rate_limiter.py`,实现 `RateLimiter` 类 - 滑动窗口内存计数器 - `check_user_rate()` — App1 每用户每分钟 10 次 - `check_store_rate()` — App2~8 每门店每小时 100 次 - _需求: 6.1, 6.2, 6.3, 6.4_ - [x] 4.4 编写属性测试:限流器窗口控制 - **Property 7: 限流器窗口控制** - 窗口内未超阈值允许,超阈值拒绝;窗口外历史不影响当前判断 - **验证: 需求 6.1, 6.2** - [x] 4.5 创建 `apps/backend/app/ai/budget_tracker.py`,实现 `BudgetTracker` 类 - `check_budget()` 从 `ai_run_logs` 聚合日/月 token 消耗 - 日预算 100,000 / 月预算 2,000,000 - 返回 `BudgetStatus(allowed, daily_used, monthly_used, reason)` - _需求: 7.1, 7.2, 7.3, 7.4, 7.5_ - [x] 4.6 编写属性测试:Token 预算检查正确性 - **Property 8: Token 预算检查正确性** - 日聚合 = 当日 success 记录 tokens_used 之和;超限时 allowed=false - **验证: 需求 7.1, 7.3** - [x] 5. AI 运行日志服务 - [x] 5.1 创建 `apps/backend/app/ai/run_log_service.py`,实现 `AIRunLogService` 类 - `create_log()` — 创建 pending 记录 - `update_running()` / `update_success()` / `update_failed()` / `update_timeout()` — 状态转换 - `get_daily_token_usage()` / `get_monthly_token_usage()` — 聚合查询 - `request_prompt` 截断为前 2000 字符 - _需求: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6_ - [x] 5.2 编写属性测试:AI 运行日志状态机 + Prompt 截断 - **Property 18: AI 运行日志状态机** - **Property 19: Prompt 截断不变量** - 状态转换正确(pending→running→success/failed/timeout);prompt ≤ 2000 字符 - **验证: 需求 16.1, 16.2, 16.3, 16.4, 16.5, 16.6** - [x] 6. 检查点 — 确保所有测试通过 - 确保所有测试通过,ask the user if questions arise. - [x] 7. Dispatcher 调度器重写 - [x] 7.1 重写 `apps/backend/app/ai/dispatcher.py`,实现 `AIDispatcher` 类 - 移除所有 `asyncio.run()` 和 `asyncio.new_event_loop()` 调用 - 所有入口改为 `async def`,用 `asyncio.create_task()` 发起后台任务 - 超时用 `asyncio.wait_for()` - 集成 CircuitBreaker、RateLimiter、BudgetTracker - `_run_step()` — 单步执行:熔断检查→限流检查→预算检查→调用→记录日志 - _需求: 8.1, 8.2, 8.3, 8.4_ - [x] 7.2 实现事件触发链编排 - `_handle_consumption()` — 消费事件:App3→App8→App7(无助教)/ +App4→App5(有助教) - `_handle_note()` — 备注事件:App6→App8 - `_handle_task_assigned()` — 任务分配:App4→App5 - `_handle_dws_completed()` — DWS 完成:App2 预生成(8 个时间维度) - 调用链某步失败时记录错误,后续步骤使用已有缓存继续 - _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 11.1, 11.2, 11.3, 11.4_ - [x] 7.3 实现幂等去重逻辑 - `_check_dedup()` — 按 `(event_type, member_id, site_id, date)` 去重 - `is_forced=true` 时跳过去重检查 - 写入 `ai_trigger_jobs` 记录 - _需求: 12.1, 12.2_ - [x] 7.4 编写属性测试:事件类型到调用链映射 - **Property 9: 事件类型到调用链映射** - 各事件类型映射到正确的 App 调用链 - **验证: 需求 9.1, 9.2, 9.3, 11.1** - [x] 7.5 编写属性测试:调用链容错不中断 - **Property 10: 调用链容错不中断** - 任意步骤失败后后续步骤仍继续执行 - **验证: 需求 9.4** - [x] 7.6 编写属性测试:事件去重与强制执行 - **Property 12: 事件去重与强制执行** - 重复自动事件跳过;`is_forced=true` 正常执行 - **验证: 需求 12.1, 12.2** - [x] 8. 检查点 — 确保所有测试通过 ✅ 76 passed - 确保所有测试通过,ask the user if questions arise. - [x] 9. API 路由与端点 - [x] 9.1 创建 `apps/backend/app/routers/internal_ai.py`,实现内部触发 API - `POST /api/internal/ai/trigger` 端点 - `TriggerRequest` Pydantic 模型(event_type, connector_type, site_id, member_id, payload) - `verify_internal_token()` 依赖注入,校验 `Authorization: Internal-Token {token}` - 写入 `ai_trigger_jobs` 后立即返回 `{trigger_job_id, status: "pending"}`,后台异步执行 - 在 FastAPI app 中注册路由 - _需求: 10.1, 10.2, 10.3, 10.4, 10.5_ - [x] 9.2 编写属性测试:内部 API 认证 - **Property 11: 内部 API 认证** - token 缺失/不匹配返回 401;匹配时正常处理 - **验证: 需求 10.2, 10.3** - [x] 9.3 适配 `apps/backend/app/routers/xcx_chat.py` SSE 端点 - 替换 `_get_bailian_client()` → `_get_dashscope_client()` - 调用 `client.call_app_stream(app_id, prompt, session_id, biz_params)` - 构建 `prompt` + `biz_params`(User_ID, Role, Nickname) - 保持 SSE 事件格式:`event: message` / `event: done` / `event: error` - 流结束后记录 `ai_run_logs` - _需求: 15.1, 15.2, 15.3, 15.4, 3.3_ - [x] 9.4 编写属性测试:SSE 事件流格式 - **Property 17: SSE 事件流格式** - 零或多个 `event: message`,最终以恰好一个 `done` 或 `error` 结束 - **验证: 需求 15.2** - [x] 10. 服务层适配 - [x] 10.1 适配 `apps/backend/app/services/ai/chat_service.py` — session_id 双轨逻辑 - 新对话生成 session_id(格式 `conv_{conversation_id}_{created_timestamp}`) - 每条消息同时写入本地 `ai_messages` - session_id 过期时从本地加载最近 20 条历史重建 - 保持对话复用规则:task 无时限、customer/coach 3 天、general 新建 - _需求: 3.1, 3.2, 3.4, 3.5, 3.6_ - [x] 10.2 编写属性测试:session_id 格式 + 对话复用规则 - **Property 3: session_id 格式不变量** - **Property 4: 对话复用规则** - session_id 匹配 `^conv_\d+_\d+$`;复用规则按入口类型正确判断 - **验证: 需求 3.1, 3.6** - [x] 10.3 适配 `apps/backend/app/services/ai/cache_service.py` — 缓存状态与过期策略 - 新增 `status` 字段处理(valid/expired/invalidated/generating) - 写入前设 `generating`,完成后设 `valid` - 查询仅返回 `status='valid'` 且未过期的记录 - 按 App 类型设置过期时间(App2 当日 23:59:59、App3~5/7/8 七天、App6 三十天) - App2~8 每 App 保留最新 20,000 条,超限清理最旧记录 - _需求: 13.1, 13.2, 13.3, 13.4, 13.5_ - [x] 10.4 编写属性测试:缓存过期策略 + 查询过滤 + 保留上限 - **Property 14: 缓存过期策略正确性** - **Property 15: 缓存查询过滤** - **Property 16: 缓存保留上限** - 过期时间匹配策略;查询仅返回 valid 且未过期;每 App ≤ 20,000 条 - **验证: 需求 13.1, 13.4, 13.5** - [x] 10.5 实现 App8 幂等写入 `member_retention_clue` - 事务中 DELETE + INSERT,同一 member 同一天只执行一次 - 事务失败自动回滚,记录错误到 `ai_trigger_jobs.error_message` - _需求: 12.3, 12.4_ - [x] 10.6 编写属性测试:App8 幂等写入 - **Property 13: App8 幂等写入** - 多次执行后同一 member 同一天记录数恒为 1 - **验证: 需求 12.3** - [x] 11. 检查点 — 确保所有测试通过 ✅ 106 passed - 确保所有测试通过,ask the user if questions arise. - [x] 12. ETL 触发集成 - [x] 12.1 修改 `apps/etl/connectors/feiqiu/tasks/` 相关 DWS 任务 - DWS 任务完成后通过 `requests` 发送 `POST /api/internal/ai/trigger` - 携带 `Authorization: Internal-Token {INTERNAL_API_TOKEN}` Header - 事件类型:`dws_completed`(App2 预生成)/ `consumption`(消费事件链) - 新增 `utils/ai_trigger.py` 工具函数 + `BaseDwsTask.AI_TRIGGER_EVENT` 类属性 - `FinanceDailyTask` → `dws_completed`,`MemberConsumptionTask` → `consumption` - _需求: 10.6_ - [x] 13. 数据库迁移 - [x] 13.1 编写迁移脚本 `db/zqyy_app/migrations/2026-03-22__p14_ai_module.sql` - CREATE TABLE `biz.ai_run_logs`(含 3 个索引) - CREATE TABLE `biz.ai_trigger_jobs`(含 3 个索引,含去重部分索引) - ALTER TABLE `biz.ai_conversations` ADD COLUMN `session_id` - ALTER TABLE `biz.ai_cache` ADD COLUMN `status` + CHECK 约束 - 编写对应回滚脚本(逆序 DROP/ALTER)+ 验证 SQL(7 条) - _需求: 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7_ - [x] 14. 收尾:文档与集成验证 - [x] 14.1 更新 BD 手册 - 更新 `docs/database/BD_Manual_ai_tables.md`:新增 `ai_run_logs`、`ai_trigger_jobs` 表结构 - 更新 `ai_cache.status` 和 `ai_conversations.session_id` 字段说明 - _需求: 14.1, 14.2, 14.3, 14.4_ - [x] 14.2 文档同步 - 更新 `docs/prd/ai-app-prompts.md`:环境变量映射 BAILIAN_* → DASHSCOPE_* - 更新 `apps/backend/README.md`:AI 模块架构说明 - 更新 `docs/DOCUMENTATION-MAP.md`:新增文档条目 - _需求: 2.4_ - [x] 15. 最终检查点 — 确保所有测试通过 ✅ 505 passed(P14 全部 15 个测试文件通过,80 个预存失败均非 P14 相关) - 确保所有测试通过,ask the user if questions arise. ## 说明 - 标记 `*` 的子任务为可选(属性测试),可跳过以加速 MVP - 每个任务关联了具体的需求编号,确保可追溯 - 属性测试任务标注了对应的 Property 编号和验证的需求条款 - 检查点确保增量验证,避免问题累积 - 实现语言:Python(与设计文档一致)