# 需求文档 — 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 占用过多存储空间