# 需求文档 — P14：AI 模块改造 — DashScope 迁移 + 调度器完善

## 简介

当前 AI 模块使用 `openai` SDK 的通用模型 API（`chat.completions.create`），但项目的 8 个 App 均为百炼控制台创建的智能体应用（各有独立 `app_id`）。通用模型 API 不接受 `app_id`，等于绕过了百炼控制台配置的 System Prompt、MCP 工具等全部能力。

本 spec 将 SDK 从 `openai` 切换到 `dashscope`（Application API），一步到位完成迁移，同时修复调度器 asyncio 嵌套问题、打通事件触发链、新增熔断/限流/Token 预算控制，并完成相关数据库变更。

### 依赖

- P5（AI 集成层）— 现有 AI 模块基础架构、8 个 App 实现、缓存/对话服务
- RNS1.4（CHAT 对齐）— CHAT 路径迁移、SSE 端点、对话复用规则

### 来源文档

- `docs/prd/specs/P14-ai-dashscope-migration.md` — PRD 主文档
- `docs/reports/2026-03-21__ai_module_issues.md` — 18 个问题清单（4 P0 / 6 P1 / 5 P2 / 3 P3）

### 不在本 spec 范围

- admin-web AI 监控后台（P15）
- 全链路测试重建与历史回填（P15）
- 多门店支持（BACKLOG，当前写死 `2790685415443269`）
- 消息队列替代 HTTP 触发（单独 PRD）
- Prompt 版本管理（BACKLOG）
- 前端刷新机制（前端改动，不在本 spec）

## 术语表

- **Backend**：FastAPI 后端应用，位于 `apps/backend/`
- **ETL**：飞球 Connector ETL 管道，位于 `apps/etl/connectors/feiqiu/`
- **DashScope_Client**：新的 DashScope Application API 客户端，替代现有 `BailianClient`
- **Application_API**：`dashscope.Application.call()` 方法，百炼智能体应用的原生调用接口
- **App1**：通用对话应用（流式，支持多轮 session_id）
- **App2**：财务洞察应用（单轮，DWS 完成后预生成）
- **App3**：维客线索应用（单轮，消费事件触发）
- **App4**：关系分析应用（单轮，消费/任务事件触发）
- **App5**：话术参考应用（单轮，依赖 App4 结果）
- **App6**：备注分析应用（单轮，备注事件触发）
- **App7**：客户分析应用（单轮，消费事件触发）
- **App8**：维客线索整理应用（单轮，整合 App3/App6 线索）
- **session_id**：百炼云端对话管理标识，格式 `conv_{conversation_id}_{created_timestamp}`
- **Circuit_Breaker**：熔断器，按 app_id 独立计数，连续失败后暂停请求
- **Rate_Limiter**：限流器，按用户/门店维度控制请求频率
- **Budget_Tracker**：Token 预算追踪器，按日/月聚合 token 消耗
- **Internal_AI_API**：内部触发接口 `POST /api/internal/ai/trigger`，ETL 通过此接口触发 AI 调用链
- **Dispatcher**：AI 事件调度与调用链编排器，位于 `apps/backend/app/ai/dispatcher.py`
- **ai_run_logs**：AI 运行记录表，记录每次 Application API 调用的输入/输出/耗时/token
- **ai_trigger_jobs**：调度运行记录表，记录事件触发的调用链执行状态

## 需求

### 需求 1：SDK 替换 — openai 切换到 dashscope Application API

**用户故事：** 作为后端开发者，我希望将 AI 客户端从 `openai` SDK 切换到 `dashscope` Application API，以便 8 个百炼智能体应用能通过各自的 `app_id` 调用，使用百炼控制台配置的 System Prompt 和 MCP 工具。

#### 验收标准

1. THE DashScope_Client SHALL 使用 `dashscope.Application.call()` 替代 `openai.AsyncOpenAI.chat.completions.create()`，所有 8 个 App 通过各自的 `app_id` 参数调用 Application API
2. THE DashScope_Client SHALL 使用 `asyncio.to_thread()` 包装同步的 `Application.call()` 方法，避免阻塞 FastAPI 事件循环
3. WHEN App1 进行流式调用时，THE DashScope_Client SHALL 在独立线程中消费 `Application.call(stream=True)` 返回的同步迭代器，通过 `asyncio.Queue` 桥接到 async generator，逐 chunk 输出文本
4. WHEN App2~8 进行单轮调用时，THE DashScope_Client SHALL 通过 `prompt` 参数传入后端拼好的完整数据 JSON，不使用 `messages` 数组
5. THE DashScope_Client SHALL 保留指数退避重试机制：最多 3 次重试，间隔 1s → 2s → 4s；HTTP 4xx 不重试，5xx/超时/连接错误重试
6. WHEN Application API 返回非合法 JSON 时，THE DashScope_Client SHALL 纯重试（最大 3 次），不做本地解析修复
7. THE Backend SHALL 在 `pyproject.toml` 中移除 `openai` 依赖，新增 `dashscope` 依赖

### 需求 2：环境变量统一 — BAILIAN_* 迁移到 DASHSCOPE_*

**用户故事：** 作为运维人员，我希望环境变量从 `BAILIAN_*` 统一迁移到 `DASHSCOPE_*` 前缀，以便与 DashScope SDK 的命名规范保持一致。

#### 验收标准

1. THE Backend SHALL 废弃并删除以下环境变量：`BAILIAN_API_KEY`、`BAILIAN_BASE_URL`、`BAILIAN_MODEL`
2. THE Backend SHALL 新增以下环境变量：`DASHSCOPE_API_KEY`（DashScope API Key）、`DASHSCOPE_WORKSPACE_ID`（百炼工作空间 ID，可选）
3. THE Backend SHALL 将 8 个 App ID 环境变量从 `BAILIAN_APP_ID_*` 前缀重命名为 `DASHSCOPE_APP_ID_*` 前缀（`DASHSCOPE_APP_ID_1_CHAT` 至 `DASHSCOPE_APP_ID_8_CONSOLIDATE`）
4. THE Backend SHALL 更新 `.env` 和 `.env.template` 文件，反映所有环境变量变更
5. THE Backend SHALL 在启动时校验必需环境变量（`DASHSCOPE_API_KEY` 和 8 个 `DASHSCOPE_APP_ID_*`），缺失时立即报错，禁止静默回退空字符串

### 需求 3：App1 对话管理 — session_id 云端 + 本地双轨

**用户故事：** 作为助教用户，我希望与 AI 助手的多轮对话能通过百炼 session_id 保持上下文连贯，同时本地持久化消息记录，以便在 session 过期后仍能恢复对话。

#### 验收标准

1. WHEN App1 创建新对话时，THE Backend SHALL 生成 session_id（格式 `conv_{conversation_id}_{created_timestamp}`），存储到 `ai_conversations.session_id` 字段
2. WHEN App1 发送消息时，THE DashScope_Client SHALL 携带 `session_id` 参数调用 Application API，由百炼云端管理对话上下文
3. THE DashScope_Client SHALL 通过 `biz_params.user_prompt_params` 传递用户信息：`User_ID`（用户 ID）、`Role`（角色）、`Nickname`（昵称）
4. THE Backend SHALL 同时将每条消息写入本地 `ai_messages` 表，实现云端 + 本地双轨持久化
5. IF session_id 过期（百炼返回 session 无效错误），THEN THE Backend SHALL 从本地 `ai_messages` 加载最近 20 条历史消息，用 `messages` 数组（不带 session_id）重新调用百炼，并将百炼返回的新 session_id 更新到本地
6. THE Backend SHALL 保持现有对话复用规则不变：task 入口无时限复用、customer/coach 入口 3 天时限、general 入口始终新建

### 需求 4：App2~8 单轮 Prompt 调用

**用户故事：** 作为后端开发者，我希望 App2~8 统一使用单轮 `prompt` 调用模式，以便简化调用逻辑并充分利用百炼控制台配置的 System Prompt。

#### 验收标准

1. THE Backend SHALL 为 App2~8 的每次调用使用 `build_prompt()` 函数拼好完整数据 JSON，通过 `Application.call(app_id=..., prompt=data_json)` 传入
2. THE Backend SHALL 不再为 App2~8 在代码中维护 System Prompt，以百炼控制台配置为准
3. THE Backend SHALL 不再为 App2~8 使用 `messages` 数组或 `response_format` 参数
4. WHEN Application API 返回结果时，THE Backend SHALL 解析 `response.output.text` 字段获取 JSON 内容，解析失败时按需求 1 第 6 条重试
5. THE Backend SHALL 为每次 App2~8 调用记录 `ai_run_logs`（详见需求 10）

### 需求 5：熔断器

**用户故事：** 作为系统管理员，我希望 AI 调用具备熔断保护，以便在百炼服务异常时快速降级，避免无效请求堆积。

#### 验收标准

1. THE Circuit_Breaker SHALL 按 `app_id` 独立计数，App1 熔断不影响 App2~8，反之亦然
2. WHEN 某个 app_id 连续 5 次调用失败时，THE Circuit_Breaker SHALL 进入熔断状态，持续 60 秒内所有该 app_id 的请求直接返回降级响应
3. WHEN 熔断 60 秒后，THE Circuit_Breaker SHALL 进入半开状态，放行 1 个请求进行探测
4. WHEN 半开状态的探测请求成功时，THE Circuit_Breaker SHALL 关闭熔断，恢复正常调用
5. WHEN 半开状态的探测请求失败时，THE Circuit_Breaker SHALL 重新进入熔断状态，再等待 60 秒
6. WHILE Circuit_Breaker 处于熔断状态，THE Backend SHALL 对 App1 请求返回友好提示"AI 服务暂时不可用，请稍后重试"，对 App2~8 后台任务记录 `circuit_open` 状态并跳过执行

### 需求 6：限流

**用户故事：** 作为系统管理员，我希望 AI 调用具备限流保护，以便防止单个用户或门店过度消耗 AI 资源。

#### 验收标准

1. THE Rate_Limiter SHALL 对 App1 实施每用户每分钟 10 次的请求频率限制
2. THE Rate_Limiter SHALL 对 App2~8 实施每门店每小时 100 次（合计）的请求频率限制
3. WHEN 请求超过限流阈值时，THE Rate_Limiter SHALL 对 App1 返回友好提示"请求过于频繁，请稍后再试"，对 App2~8 后台任务记录 `rate_limited` 状态并跳过执行
4. THE Rate_Limiter SHALL 使用内存计数器实现（单实例部署），不依赖外部 Redis

### 需求 7：Token 预算控制

**用户故事：** 作为系统管理员，我希望 AI 调用具备 Token 预算控制，以便防止 API 费用失控。

#### 验收标准

1. THE Budget_Tracker SHALL 从 `ai_run_logs.tokens_used` 按日/月聚合计算已消耗 token 数
2. THE Budget_Tracker SHALL 支持日预算上限（默认 100,000 tokens）和月预算上限（默认 2,000,000 tokens）
3. WHEN 日预算或月预算超限时，THE Backend SHALL 对 App1 用户对话返回友好提示"AI 服务今日额度已用完，请明天再试"
4. WHEN 日预算或月预算超限时，THE Backend SHALL 对 App2~8 后台任务跳过执行，记录 `budget_exceeded` 状态到 `ai_run_logs`
5. THE Budget_Tracker SHALL 在每次 AI 调用前检查预算，调用后更新 token 消耗记录

### 需求 8：调度器 asyncio 修复

**用户故事：** 作为后端开发者，我希望修复 dispatcher.py 中的 asyncio 嵌套问题，以便事件处理器在 FastAPI 事件循环中正常工作，不再因 `asyncio.run()` 嵌套而报错。

#### 验收标准

1. THE Dispatcher SHALL 移除所有 `asyncio.run()` 和 `asyncio.new_event_loop()` 调用
2. THE Dispatcher SHALL 将所有事件处理器入口改为 `async def`，使用 `asyncio.create_task()` 发起后台异步任务
3. THE Dispatcher SHALL 使用 `asyncio.wait_for()` 实现超时控制，替代同步超时机制
4. THE Dispatcher SHALL 确保事件处理器在 FastAPI lifespan 中注册后，能在已有事件循环中正常执行调用链

### 需求 9：事件触发链打通

**用户故事：** 作为系统管理员，我希望消费事件、备注事件、任务分配事件能正确触发对应的 AI 调用链，以便 AI 分析结果能自动生成，无需人工干预。

#### 验收标准

1. WHEN ETL DWS 任务完成后发送消费事件时，THE Dispatcher SHALL 执行调用链：App3 → App8 → App7（无助教时），或 App3 → App8 → App7 + App4 → App5（有助教时）
2. WHEN 小程序助教提交备注时，THE Dispatcher SHALL 执行调用链：App6 → App8
3. WHEN task_manager 自动分配任务时，THE Dispatcher SHALL 执行调用链：App4 → App5
4. THE Dispatcher SHALL 在调用链中某步失败时记录错误日志，后续应用使用已有缓存继续执行，不中断整条链
5. THE Dispatcher SHALL 将每次事件触发记录到 `ai_trigger_jobs` 表，包含事件类型、执行链、状态、耗时

### 需求 10：ETL → 后端内部触发 API

**用户故事：** 作为 ETL 开发者，我希望 DWS 任务完成后能通过 HTTP 接口触发后端 AI 调用链，以便实现 ETL 与 AI 模块的自动联动。

#### 验收标准

1. THE Backend SHALL 实现 `POST /api/internal/ai/trigger` 端点，接受 JSON 请求体包含：`event_type`（事件类型）、`connector_type`（连接器类型，默认 `feiqiu`）、`site_id`（门店 ID）、`member_id`（会员 ID，可选）、`payload`（事件附加数据）
2. THE Internal_AI_API SHALL 使用独立的 `INTERNAL_API_TOKEN` 环境变量进行认证，通过 HTTP Header `Authorization: Internal-Token {token}` 传递，不走 JWT
3. IF 认证 token 缺失或不匹配，THEN THE Internal_AI_API SHALL 返回 HTTP 401
4. THE Internal_AI_API SHALL 将事件写入 `ai_trigger_jobs` 表后立即返回 `{ trigger_job_id, status: "pending" }`，调用链在后台异步执行
5. THE Internal_AI_API SHALL 设计为连接器无关接口，`connector_type` 字段标识来源，为多平台扩展预留
6. THE ETL SHALL 在 DWS 任务完成后通过 HTTP POST 调用 Internal_AI_API，传递消费事件或 DWS 完成事件

### 需求 11：App2 预生成

**用户故事：** 作为门店管理者，我希望财务洞察（App2）在每日 DWS 数据刷新后自动预生成，以便打开页面时能立即看到最新分析结果，无需等待。

#### 验收标准

1. WHEN ETL 通过 Internal_AI_API 发送 `event_type: "dws_completed"` 事件时，THE Dispatcher SHALL 触发 App2 预生成任务
2. THE Dispatcher SHALL 为当前门店（`site_id: 2790685415443269`）生成 8 个时间维度的财务洞察：今日、昨日、本周、上周、本月、上月、本季、上季
3. THE Dispatcher SHALL 将 App2 预生成结果写入 `ai_cache`（cache_type: `app2_finance`），过期时间为当日 23:59:59
4. THE Dispatcher SHALL 确保 App2 预生成每日调用量为 1 门店 × 8 维度 = 8 次

### 需求 12：幂等与去重

**用户故事：** 作为系统管理员，我希望 AI 事件触发具备幂等去重能力，以便重复事件不会导致重复执行和资源浪费。

#### 验收标准

1. THE Dispatcher SHALL 对自动触发的事件按 `(event_type, member_id, site_id, date)` 进行去重，重复事件跳过执行并记录 `skipped_duplicate` 状态
2. WHEN 手动重跑时（`is_forced = true`），THE Dispatcher SHALL 允许强制执行，跳过去重检查，在 `ai_trigger_jobs` 中明确标记 `is_forced`
3. THE Backend SHALL 对 App8 写入 `member_retention_clue` 业务表时实施强幂等：在事务中先 DELETE 再 INSERT，同一 member 同一天只执行一次
4. IF App8 幂等写入事务失败，THEN THE Backend SHALL 自动回滚，记录错误到 `ai_trigger_jobs.error_message`

### 需求 13：缓存策略

**用户故事：** 作为后端开发者，我希望 AI 缓存按 App 类型设置不同的过期时间和状态管理，以便缓存数据的新鲜度与业务需求匹配。

#### 验收标准

1. THE Backend SHALL 为每个 App 设置独立的缓存过期策略：App2 当日 23:59:59、App3/App4/App5/App7/App8 为 7 天、App6 为 30 天
2. THE Backend SHALL 在 `ai_cache` 表新增 `status` 字段，支持四种状态：`valid`（有效）、`expired`（已过期）、`invalidated`（手动失效）、`generating`（生成中）
3. WHEN 写入新缓存前，THE Backend SHALL 将 `status` 设为 `generating`，写入完成后更新为 `valid`，防止并发读取到不完整数据
4. THE Backend SHALL 在查询缓存时仅返回 `status = 'valid'` 且未过期（`expires_at > now()` 或 `expires_at IS NULL`）的记录
5. THE Backend SHALL 对 App2~8 每个 App 保留最新 20,000 条 `ai_cache` 记录，超限时清理最旧记录；App1 对话记录不自动删除

### 需求 14：数据库变更

**用户故事：** 作为后端开发者，我希望新增必要的数据库表和字段，以便支持 AI 运行日志、事件调度记录、session_id 管理和缓存状态管理。

#### 验收标准

1. THE Backend SHALL 在 `biz` schema 新增 `ai_run_logs` 表，包含字段：`id`（BIGSERIAL PK）、`site_id`（BIGINT NOT NULL）、`app_type`（VARCHAR(30)）、`trigger_type`（VARCHAR(20)）、`member_id`（BIGINT 可空）、`request_prompt`（TEXT，截断前 2000 字符）、`response_text`（TEXT）、`tokens_used`（INTEGER）、`latency_ms`（INTEGER）、`status`（VARCHAR(20)）、`error_message`（TEXT）、`session_id`（VARCHAR(100)）、`created_at`（TIMESTAMPTZ）、`finished_at`（TIMESTAMPTZ）
2. THE Backend SHALL 在 `biz` schema 新增 `ai_trigger_jobs` 表，包含字段：`id`（BIGSERIAL PK）、`site_id`（BIGINT NOT NULL）、`event_type`（VARCHAR(30)）、`connector_type`（VARCHAR(30) 默认 `feiqiu`）、`member_id`（BIGINT 可空）、`payload`（JSONB）、`status`（VARCHAR(20)）、`is_forced`（BOOLEAN 默认 false）、`app_chain`（VARCHAR(100)）、`started_at`（TIMESTAMPTZ）、`finished_at`（TIMESTAMPTZ）、`error_message`（TEXT）、`created_at`（TIMESTAMPTZ）
3. THE Backend SHALL 在 `ai_conversations` 表新增 `session_id` 字段（VARCHAR(100)），用于存储百炼 session_id
4. THE Backend SHALL 在 `ai_cache` 表新增 `status` 字段（VARCHAR(20) 默认 `valid`），CHECK 约束限制为 `valid`/`expired`/`invalidated`/`generating`
5. THE Backend SHALL 为 `ai_run_logs` 创建索引：`(site_id, app_type)`、`(created_at)`、`(status)`
6. THE Backend SHALL 为 `ai_trigger_jobs` 创建索引：`(site_id, event_type)`、去重索引 `(event_type, member_id, site_id, created_at::date)` WHERE status NOT IN ('skipped_duplicate')、`(status)`
7. THE Backend SHALL 编写迁移脚本 `db/zqyy_app/migrations/YYYYMMDD_p14_ai_module.sql`，包含所有 DDL 变更，并编写对应的回滚脚本

### 需求 15：SSE 端点适配

**用户故事：** 作为助教用户，我希望 AI 对话的 SSE 流式端点能适配 DashScope Application API 的流式输出，以便继续获得逐字显示的对话体验。

#### 验收标准

1. THE Backend SHALL 适配 SSE 端点（`POST /api/xcx/chat/stream`），将 DashScope_Client 的 async generator 输出转换为 SSE 事件流
2. THE Backend SHALL 保持现有 SSE 事件格式不变：`event: message`（逐 token）、`event: done`（流结束）、`event: error`（错误）
3. WHEN DashScope_Client 流式调用过程中发生错误时，THE Backend SHALL 发送 `event: error` 事件并关闭连接，不导致客户端挂起
4. THE Backend SHALL 在流式调用完成后记录 `ai_run_logs`，包含 token 消耗和响应耗时

### 需求 16：AI 运行日志记录

**用户故事：** 作为系统管理员，我希望每次 AI 调用都有完整的运行日志，以便追踪调用状态、排查问题和统计 token 消耗。

#### 验收标准

1. THE Backend SHALL 在每次 Application API 调用前创建 `ai_run_logs` 记录（status: `pending`），调用开始时更新为 `running`
2. WHEN 调用成功时，THE Backend SHALL 更新 `ai_run_logs` 状态为 `success`，记录 `response_text`、`tokens_used`、`latency_ms`、`finished_at`
3. WHEN 调用失败时，THE Backend SHALL 更新 `ai_run_logs` 状态为 `failed`，记录 `error_message`、`latency_ms`、`finished_at`
4. WHEN 调用超时时，THE Backend SHALL 更新 `ai_run_logs` 状态为 `timeout`
5. WHEN 预算超限跳过执行时，THE Backend SHALL 创建 `ai_run_logs` 记录，状态为 `budget_exceeded`
6. THE Backend SHALL 将 `request_prompt` 截断为前 2000 字符后存储，避免大 prompt 占用过多存储空间
