chore: 文档与 IDE 配置整理
- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro) - CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/ - 新增 /spec-close、/pre-change 两个工作流命令 - DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表) - BD_Manual → BD_manual 命名统一(48 个文件) - 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数) - 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表) - 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档) - docs/database/README.md 索引更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
753
docs/specs/P14-ai-dashscope-migration/design.md
Normal file
753
docs/specs/P14-ai-dashscope-migration/design.md
Normal file
@@ -0,0 +1,753 @@
|
||||
# 设计文档 — P14:AI 模块改造 — DashScope 迁移 + 调度器完善
|
||||
|
||||
## 概述
|
||||
|
||||
本设计将 AI 模块从 `openai` SDK(通用模型 API)迁移到 `dashscope` SDK(Application API),使 8 个百炼智能体应用能通过各自的 `app_id` 调用,充分利用百炼控制台配置的 System Prompt 和 MCP 工具。同时修复调度器 asyncio 嵌套问题、打通事件触发链、新增熔断/限流/Token 预算控制,并完成相关数据库变更。
|
||||
|
||||
### 设计决策与理由
|
||||
|
||||
| 决策 | 理由 |
|
||||
|------|------|
|
||||
| `asyncio.to_thread()` 包装同步 SDK | `dashscope.Application.call()` 是同步方法,FastAPI 全 async,`to_thread` 是标准库方案,无需引入第三方异步包装 |
|
||||
| 流式调用用 `asyncio.Queue` 桥接 | `Application.call(stream=True)` 返回同步迭代器,需在线程中消费后桥接到 async generator,保持 SSE 端点的 async 特性 |
|
||||
| 熔断器内存实现(非 Redis) | 单实例部署,无需分布式状态;按 `app_id` 独立计数,结构简单 |
|
||||
| 限流器内存计数器 | 同上,单实例部署;滑动窗口计数器足够 |
|
||||
| Token 预算从 `ai_run_logs` 聚合 | 避免引入额外计数表,利用已有日志数据;查询频率低(每次调用前一次) |
|
||||
| session_id 云端 + 本地双轨 | 百炼云端管理上下文减少 token 消耗,本地持久化保证 session 过期后可恢复 |
|
||||
| 内部 API 用 `INTERNAL_API_TOKEN` 而非 JWT | ETL 进程是内部服务,不需要用户身份;简单 token 认证足够且配置简单 |
|
||||
| App2~8 纯重试不做本地 JSON 修复 | 百炼控制台已配置 System Prompt 要求 JSON 输出,本地修复容易引入错误数据 |
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "外部触发"
|
||||
ETL["ETL DWS 任务"]
|
||||
XCX["小程序助教"]
|
||||
TM["task_manager"]
|
||||
end
|
||||
|
||||
subgraph "入口层"
|
||||
SSE["SSE 端点<br/>POST /api/xcx/chat/stream"]
|
||||
IAPI["内部触发 API<br/>POST /api/internal/ai/trigger"]
|
||||
EVT["事件发射<br/>fire_event()"]
|
||||
end
|
||||
|
||||
subgraph "防护层"
|
||||
CB["CircuitBreaker<br/>按 app_id 独立"]
|
||||
RL["RateLimiter<br/>用户/门店维度"]
|
||||
BT["BudgetTracker<br/>日/月 token 预算"]
|
||||
end
|
||||
|
||||
subgraph "核心层"
|
||||
DSC["DashScopeClient<br/>Application.call() 包装"]
|
||||
DISP["Dispatcher<br/>事件调度 + 调用链编排"]
|
||||
end
|
||||
|
||||
subgraph "服务层"
|
||||
CONV["ConversationService<br/>session_id 双轨"]
|
||||
CACHE["AICacheService<br/>status 字段 + 过期策略"]
|
||||
end
|
||||
|
||||
subgraph "存储层"
|
||||
DB_CONV["ai_conversations<br/>+session_id"]
|
||||
DB_MSG["ai_messages"]
|
||||
DB_CACHE["ai_cache<br/>+status"]
|
||||
DB_LOG["ai_run_logs<br/>新表"]
|
||||
DB_JOB["ai_trigger_jobs<br/>新表"]
|
||||
DB_CLUE["member_retention_clue"]
|
||||
end
|
||||
|
||||
ETL -->|HTTP POST| IAPI
|
||||
XCX -->|API 路由| EVT
|
||||
TM -->|fire_event| EVT
|
||||
|
||||
SSE --> CB --> RL --> BT --> DSC
|
||||
IAPI --> DISP
|
||||
EVT --> DISP
|
||||
|
||||
DISP --> CB
|
||||
DSC -->|App1 流式| SSE
|
||||
DSC -->|App2~8 单轮| DISP
|
||||
|
||||
DISP --> CONV
|
||||
DISP --> CACHE
|
||||
DSC --> DB_LOG
|
||||
|
||||
CONV --> DB_CONV
|
||||
CONV --> DB_MSG
|
||||
CACHE --> DB_CACHE
|
||||
DISP --> DB_JOB
|
||||
DISP --> DB_CLUE
|
||||
```
|
||||
|
||||
### 调用链流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ETL as ETL DWS
|
||||
participant API as Internal AI API
|
||||
participant DISP as Dispatcher
|
||||
participant CB as CircuitBreaker
|
||||
participant RL as RateLimiter
|
||||
participant BT as BudgetTracker
|
||||
participant DSC as DashScopeClient
|
||||
participant DB as PostgreSQL
|
||||
|
||||
ETL->>API: POST /api/internal/ai/trigger
|
||||
API->>DB: INSERT ai_trigger_jobs (pending)
|
||||
API-->>ETL: {trigger_job_id, status: "pending"}
|
||||
|
||||
API->>DISP: asyncio.create_task(handle_event)
|
||||
DISP->>DISP: 去重检查 (event_type, member_id, site_id, date)
|
||||
|
||||
loop 调用链每一步 (App3→App8→App7)
|
||||
DISP->>CB: 检查 app_id 熔断状态
|
||||
CB-->>DISP: closed/open/half_open
|
||||
DISP->>RL: 检查门店限流
|
||||
RL-->>DISP: allowed/rejected
|
||||
DISP->>BT: 检查 token 预算
|
||||
BT-->>DISP: within_budget/exceeded
|
||||
|
||||
DISP->>DB: INSERT ai_run_logs (pending→running)
|
||||
DISP->>DSC: Application.call(app_id, prompt)
|
||||
DSC-->>DISP: response
|
||||
DISP->>DB: UPDATE ai_run_logs (success/failed)
|
||||
DISP->>DB: UPSERT ai_cache
|
||||
end
|
||||
|
||||
DISP->>DB: UPDATE ai_trigger_jobs (completed)
|
||||
```
|
||||
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 1. DashScopeClient(替代 BailianClient)
|
||||
|
||||
文件:`apps/backend/app/ai/dashscope_client.py`(完全重写 `bailian_client.py`)
|
||||
|
||||
```python
|
||||
class DashScopeClient:
|
||||
"""DashScope Application API 统一封装层。"""
|
||||
|
||||
MAX_RETRIES = 3
|
||||
BASE_INTERVAL = 1 # 秒
|
||||
|
||||
def __init__(self, api_key: str, workspace_id: str | None = None):
|
||||
"""初始化。dashscope 通过全局 dashscope.api_key 设置密钥。"""
|
||||
|
||||
async def call_app_stream(
|
||||
self,
|
||||
app_id: str,
|
||||
prompt: str,
|
||||
session_id: str | None = None,
|
||||
biz_params: dict | None = None,
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""App1 流式调用。
|
||||
在线程中消费同步迭代器,通过 asyncio.Queue 桥接到 async generator。
|
||||
"""
|
||||
|
||||
async def call_app(
|
||||
self,
|
||||
app_id: str,
|
||||
prompt: str,
|
||||
session_id: str | None = None,
|
||||
biz_params: dict | None = None,
|
||||
) -> tuple[dict, int, str | None]:
|
||||
"""App2~8 单轮调用。
|
||||
返回 (parsed_json, tokens_used, new_session_id)。
|
||||
"""
|
||||
|
||||
async def _call_with_retry(self, func: Callable, **kwargs) -> Any:
|
||||
"""指数退避重试:1s→2s→4s,HTTP 4xx 不重试,5xx/超时/连接错误重试。"""
|
||||
```
|
||||
|
||||
流式桥接实现要点:
|
||||
|
||||
```python
|
||||
async def call_app_stream(self, app_id, prompt, session_id=None, biz_params=None):
|
||||
queue: asyncio.Queue[str | None] = asyncio.Queue()
|
||||
|
||||
def _consume_in_thread():
|
||||
"""在线程中消费同步迭代器,逐 chunk 放入 queue。"""
|
||||
response = Application.call(
|
||||
app_id=app_id, prompt=prompt, session_id=session_id,
|
||||
biz_params=biz_params, stream=True, incremental_output=True,
|
||||
)
|
||||
for chunk in response:
|
||||
if chunk.status_code == 200:
|
||||
text = chunk.output.get("text", "")
|
||||
if text:
|
||||
# 线程安全地放入 queue
|
||||
asyncio.run_coroutine_threadsafe(queue.put(text), loop)
|
||||
else:
|
||||
raise DashScopeApiError(chunk.message, chunk.status_code)
|
||||
asyncio.run_coroutine_threadsafe(queue.put(None), loop) # 结束信号
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.run_in_executor(None, _consume_in_thread)
|
||||
|
||||
while True:
|
||||
item = await queue.get()
|
||||
if item is None:
|
||||
break
|
||||
yield item
|
||||
```
|
||||
|
||||
### 2. CircuitBreaker(熔断器)
|
||||
|
||||
文件:`apps/backend/app/ai/circuit_breaker.py`(新增)
|
||||
|
||||
```python
|
||||
class CircuitState(enum.Enum):
|
||||
CLOSED = "closed" # 正常
|
||||
OPEN = "open" # 熔断中
|
||||
HALF_OPEN = "half_open" # 探测中
|
||||
|
||||
class CircuitBreaker:
|
||||
"""按 app_id 独立的熔断器。"""
|
||||
|
||||
def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
|
||||
self._breakers: dict[str, _BreakerState] = {}
|
||||
|
||||
def check(self, app_id: str) -> CircuitState:
|
||||
"""检查当前状态,OPEN 时直接拒绝。"""
|
||||
|
||||
def record_success(self, app_id: str) -> None:
|
||||
"""记录成功,HALF_OPEN→CLOSED。"""
|
||||
|
||||
def record_failure(self, app_id: str) -> None:
|
||||
"""记录失败,连续达阈值→OPEN;HALF_OPEN 失败→重新 OPEN。"""
|
||||
|
||||
class _BreakerState:
|
||||
state: CircuitState
|
||||
failure_count: int
|
||||
last_failure_time: float
|
||||
last_state_change: float
|
||||
```
|
||||
|
||||
### 3. RateLimiter(限流器)
|
||||
|
||||
文件:`apps/backend/app/ai/rate_limiter.py`(新增)
|
||||
|
||||
```python
|
||||
class RateLimiter:
|
||||
"""滑动窗口内存限流器。"""
|
||||
|
||||
def __init__(self):
|
||||
self._user_windows: dict[str, deque[float]] = {} # App1: user_id → 时间戳队列
|
||||
self._store_windows: dict[str, deque[float]] = {} # App2~8: site_id → 时间戳队列
|
||||
|
||||
def check_user_rate(self, user_id: str, limit: int = 10, window_seconds: int = 60) -> bool:
|
||||
"""App1 每用户每分钟限流。返回 True 表示允许。"""
|
||||
|
||||
def check_store_rate(self, site_id: int, limit: int = 100, window_seconds: int = 3600) -> bool:
|
||||
"""App2~8 每门店每小时限流。返回 True 表示允许。"""
|
||||
```
|
||||
|
||||
### 4. BudgetTracker(Token 预算追踪)
|
||||
|
||||
文件:`apps/backend/app/ai/budget_tracker.py`(新增)
|
||||
|
||||
```python
|
||||
class BudgetTracker:
|
||||
"""Token 预算追踪器,从 ai_run_logs 聚合。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
daily_limit: int = 100_000,
|
||||
monthly_limit: int = 2_000_000,
|
||||
):
|
||||
self.daily_limit = daily_limit
|
||||
self.monthly_limit = monthly_limit
|
||||
|
||||
def check_budget(self) -> BudgetStatus:
|
||||
"""检查当前预算状态。返回 BudgetStatus(allowed, daily_used, monthly_used)。"""
|
||||
|
||||
def _get_daily_usage(self) -> int:
|
||||
"""从 ai_run_logs 聚合今日 token 消耗。"""
|
||||
|
||||
def _get_monthly_usage(self) -> int:
|
||||
"""从 ai_run_logs 聚合本月 token 消耗。"""
|
||||
|
||||
@dataclass
|
||||
class BudgetStatus:
|
||||
allowed: bool
|
||||
daily_used: int
|
||||
monthly_used: int
|
||||
reason: str | None = None # "daily_exceeded" / "monthly_exceeded" / None
|
||||
```
|
||||
|
||||
### 5. Dispatcher(调度器重写)
|
||||
|
||||
文件:`apps/backend/app/ai/dispatcher.py`(重写)
|
||||
|
||||
关键变更:
|
||||
- 移除所有 `asyncio.run()` 和 `asyncio.new_event_loop()`
|
||||
- 所有入口改为 `async def`,用 `asyncio.create_task()` 发起后台任务
|
||||
- 超时用 `asyncio.wait_for()`
|
||||
- 集成 CircuitBreaker、RateLimiter、BudgetTracker
|
||||
- 新增 `ai_trigger_jobs` 记录
|
||||
- 新增去重逻辑
|
||||
|
||||
```python
|
||||
class AIDispatcher:
|
||||
"""AI 事件调度与调用链编排器。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: DashScopeClient,
|
||||
cache_svc: AICacheService,
|
||||
conv_svc: ConversationService,
|
||||
circuit_breaker: CircuitBreaker,
|
||||
rate_limiter: RateLimiter,
|
||||
budget_tracker: BudgetTracker,
|
||||
): ...
|
||||
|
||||
async def handle_trigger(self, event: TriggerEvent) -> int:
|
||||
"""统一事件入口。写 ai_trigger_jobs 后异步执行调用链。返回 trigger_job_id。"""
|
||||
|
||||
async def _execute_chain(self, job_id: int, event: TriggerEvent) -> None:
|
||||
"""执行调用链,根据 event_type 分发。"""
|
||||
|
||||
async def _check_dedup(self, event: TriggerEvent) -> bool:
|
||||
"""去重检查:(event_type, member_id, site_id, date)。"""
|
||||
|
||||
async def _run_step(self, app_name: str, app_id: str, prompt: str, context: dict) -> dict | None:
|
||||
"""执行单步:熔断检查→限流检查→预算检查→调用→记录日志。"""
|
||||
|
||||
# 事件处理器
|
||||
async def _handle_consumption(self, event: TriggerEvent) -> None: ...
|
||||
async def _handle_note(self, event: TriggerEvent) -> None: ...
|
||||
async def _handle_task_assigned(self, event: TriggerEvent) -> None: ...
|
||||
async def _handle_dws_completed(self, event: TriggerEvent) -> None: ...
|
||||
```
|
||||
|
||||
### 6. AI Config(环境变量)
|
||||
|
||||
文件:`apps/backend/app/ai/config.py`(重写)
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class AIConfig:
|
||||
"""AI 模块配置,从环境变量加载。"""
|
||||
api_key: str # DASHSCOPE_API_KEY
|
||||
workspace_id: str | None # DASHSCOPE_WORKSPACE_ID(可选)
|
||||
app_id_1_chat: str # DASHSCOPE_APP_ID_1_CHAT
|
||||
app_id_2_finance: str # DASHSCOPE_APP_ID_2_FINANCE
|
||||
app_id_3_clue: str # ...
|
||||
app_id_4_analysis: str
|
||||
app_id_5_tactics: str
|
||||
app_id_6_note: str
|
||||
app_id_7_customer: str
|
||||
app_id_8_consolidate: str
|
||||
internal_api_token: str # INTERNAL_API_TOKEN
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "AIConfig":
|
||||
"""从环境变量加载,缺失必需变量时立即报错。"""
|
||||
```
|
||||
|
||||
### 7. Internal AI API(内部触发接口)
|
||||
|
||||
文件:`apps/backend/app/routers/internal_ai.py`(新增)
|
||||
|
||||
```python
|
||||
router = APIRouter(prefix="/api/internal/ai", tags=["internal-ai"])
|
||||
|
||||
@router.post("/trigger")
|
||||
async def trigger_ai_event(body: TriggerRequest, token: str = Depends(verify_internal_token)):
|
||||
"""接收 ETL/内部事件,写 ai_trigger_jobs 后异步执行。"""
|
||||
|
||||
class TriggerRequest(BaseModel):
|
||||
event_type: str # consumption / dws_completed / note_created / task_assigned
|
||||
connector_type: str = "feiqiu"
|
||||
site_id: int
|
||||
member_id: int | None = None
|
||||
payload: dict | None = None
|
||||
|
||||
def verify_internal_token(authorization: str = Header(...)) -> str:
|
||||
"""校验 Internal-Token {token}。"""
|
||||
```
|
||||
|
||||
### 8. SSE 端点适配
|
||||
|
||||
文件:`apps/backend/app/routers/xcx_chat.py`(修改)
|
||||
|
||||
变更要点:
|
||||
- `_get_bailian_client()` → `_get_dashscope_client()`
|
||||
- `bailian.chat_stream(messages)` → `client.call_app_stream(app_id, prompt, session_id, biz_params)`
|
||||
- `_build_ai_messages()` 改为构建 `prompt` + `biz_params`
|
||||
- 流结束后记录 `ai_run_logs`
|
||||
- 保持 SSE 事件格式不变:`event: message` / `event: done` / `event: error`
|
||||
|
||||
### 9. ETL 触发集成
|
||||
|
||||
文件:`apps/etl/connectors/feiqiu/tasks/` 相关 DWS 任务(修改)
|
||||
|
||||
变更要点:
|
||||
- DWS 任务完成后,通过 `httpx` 发送 `POST /api/internal/ai/trigger`
|
||||
- 携带 `Authorization: Internal-Token {INTERNAL_API_TOKEN}` Header
|
||||
- 事件类型:`dws_completed`(触发 App2 预生成)或 `consumption`(触发消费事件链)
|
||||
|
||||
### 10. AI 运行日志服务
|
||||
|
||||
文件:`apps/backend/app/ai/run_log_service.py`(新增)
|
||||
|
||||
```python
|
||||
class AIRunLogService:
|
||||
"""AI 运行日志 CRUD。"""
|
||||
|
||||
def create_log(self, site_id: int, app_type: str, trigger_type: str, **kwargs) -> int:
|
||||
"""创建日志记录(status: pending),返回 log_id。"""
|
||||
|
||||
def update_running(self, log_id: int) -> None:
|
||||
"""更新为 running。"""
|
||||
|
||||
def update_success(self, log_id: int, response_text: str, tokens_used: int, latency_ms: int) -> None:
|
||||
"""更新为 success。"""
|
||||
|
||||
def update_failed(self, log_id: int, error_message: str, latency_ms: int) -> None:
|
||||
"""更新为 failed。"""
|
||||
|
||||
def update_timeout(self, log_id: int, latency_ms: int) -> None:
|
||||
"""更新为 timeout。"""
|
||||
|
||||
def get_daily_token_usage(self) -> int:
|
||||
"""聚合今日 token 消耗。"""
|
||||
|
||||
def get_monthly_token_usage(self) -> int:
|
||||
"""聚合本月 token 消耗。"""
|
||||
```
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 新增表
|
||||
|
||||
#### biz.ai_run_logs(AI 运行记录)
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz.ai_run_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
site_id BIGINT NOT NULL,
|
||||
app_type VARCHAR(30) NOT NULL, -- app1_chat / app2_finance / ...
|
||||
trigger_type VARCHAR(20) NOT NULL, -- user / scheduled / event / forced
|
||||
member_id BIGINT,
|
||||
request_prompt TEXT, -- 截断前 2000 字符
|
||||
response_text TEXT,
|
||||
tokens_used INTEGER DEFAULT 0,
|
||||
latency_ms INTEGER,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
-- pending / running / success / failed / timeout / budget_exceeded
|
||||
error_message TEXT,
|
||||
session_id VARCHAR(100),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
finished_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ai_run_logs_site_app ON biz.ai_run_logs(site_id, app_type);
|
||||
CREATE INDEX idx_ai_run_logs_created ON biz.ai_run_logs(created_at);
|
||||
CREATE INDEX idx_ai_run_logs_status ON biz.ai_run_logs(status);
|
||||
```
|
||||
|
||||
#### biz.ai_trigger_jobs(调度运行记录)
|
||||
|
||||
```sql
|
||||
CREATE TABLE biz.ai_trigger_jobs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
site_id BIGINT NOT NULL,
|
||||
event_type VARCHAR(30) NOT NULL,
|
||||
connector_type VARCHAR(30) DEFAULT 'feiqiu',
|
||||
member_id BIGINT,
|
||||
payload JSONB,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
-- pending / running / completed / failed / skipped_duplicate / budget_exceeded
|
||||
is_forced BOOLEAN DEFAULT false,
|
||||
app_chain VARCHAR(100), -- 如 "app3→app8→app7"
|
||||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_ai_trigger_jobs_site ON biz.ai_trigger_jobs(site_id, event_type);
|
||||
CREATE INDEX idx_ai_trigger_jobs_dedup ON biz.ai_trigger_jobs(event_type, member_id, site_id, (created_at::date))
|
||||
WHERE status NOT IN ('skipped_duplicate');
|
||||
CREATE INDEX idx_ai_trigger_jobs_status ON biz.ai_trigger_jobs(status);
|
||||
```
|
||||
|
||||
### 已有表变更
|
||||
|
||||
#### biz.ai_conversations — 新增字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE biz.ai_conversations ADD COLUMN session_id VARCHAR(100);
|
||||
```
|
||||
|
||||
- 存储百炼 session_id,格式 `conv_{conversation_id}_{created_timestamp}`
|
||||
- 仅 App1 使用
|
||||
|
||||
#### biz.ai_cache — 新增字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE biz.ai_cache ADD COLUMN status VARCHAR(20) DEFAULT 'valid'
|
||||
CHECK (status IN ('valid', 'expired', 'invalidated', 'generating'));
|
||||
```
|
||||
|
||||
- `valid`:有效缓存
|
||||
- `expired`:已过期(定时任务标记)
|
||||
- `invalidated`:手动失效(admin-web 操作)
|
||||
- `generating`:正在生成中(防并发读取不完整数据)
|
||||
|
||||
### 缓存过期策略
|
||||
|
||||
| App | cache_type | expires_at |
|
||||
|-----|-----------|------------|
|
||||
| App2 | app2_finance | 当日 23:59:59 |
|
||||
| App3 | app3_clue | 7 天 |
|
||||
| App4 | app4_analysis | 7 天 |
|
||||
| App5 | app5_tactics | 7 天 |
|
||||
| App6 | app6_note_analysis | 30 天 |
|
||||
| App7 | app7_customer_analysis | 7 天 |
|
||||
| App8 | app8_clue_consolidated | 7 天 |
|
||||
|
||||
### 环境变量映射
|
||||
|
||||
| 旧变量 | 新变量 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `BAILIAN_API_KEY` | `DASHSCOPE_API_KEY` | API Key |
|
||||
| `BAILIAN_BASE_URL` | _(删除)_ | Application API 不需要 base_url |
|
||||
| `BAILIAN_MODEL` | _(删除)_ | 通过 app_id 指定应用 |
|
||||
| _(新增)_ | `DASHSCOPE_WORKSPACE_ID` | 工作空间 ID(可选) |
|
||||
| `BAILIAN_APP_ID_1_CHAT` | `DASHSCOPE_APP_ID_1_CHAT` | App1 |
|
||||
| `BAILIAN_APP_ID_2_FINANCE` | `DASHSCOPE_APP_ID_2_FINANCE` | App2 |
|
||||
| `BAILIAN_APP_ID_3_CLUE` | `DASHSCOPE_APP_ID_3_CLUE` | App3 |
|
||||
| `BAILIAN_APP_ID_4_ANALYSIS` | `DASHSCOPE_APP_ID_4_ANALYSIS` | App4 |
|
||||
| `BAILIAN_APP_ID_5_TACTICS` | `DASHSCOPE_APP_ID_5_TACTICS` | App5 |
|
||||
| `BAILIAN_APP_ID_6_NOTE` | `DASHSCOPE_APP_ID_6_NOTE` | App6 |
|
||||
| `BAILIAN_APP_ID_7_CUSTOMER` | `DASHSCOPE_APP_ID_7_CUSTOMER` | App7 |
|
||||
| `BAILIAN_APP_ID_8_CONSOLIDATE` | `DASHSCOPE_APP_ID_8_CONSOLIDATE` | App8 |
|
||||
| _(新增)_ | `INTERNAL_API_TOKEN` | 内部 API 认证 token |
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是在系统所有合法执行路径上都应成立的特征或行为——本质上是对"系统应该做什么"的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 重试策略正确性
|
||||
|
||||
*对于任意* DashScopeClient 调用和任意错误类型序列,HTTP 4xx 错误应立即抛出不重试,HTTP 5xx/超时/连接错误应最多重试 3 次(间隔 1s→2s→4s),非合法 JSON 响应应触发重试而非本地修复。
|
||||
|
||||
**Validates: Requirements 1.5, 1.6**
|
||||
|
||||
### Property 2: 环境变量校验完整性
|
||||
|
||||
*对于任意* 必需环境变量(`DASHSCOPE_API_KEY` 和 8 个 `DASHSCOPE_APP_ID_*`)的子集缺失,`AIConfig.from_env()` 应抛出异常,不返回包含空字符串的配置对象。
|
||||
|
||||
**Validates: Requirements 2.5**
|
||||
|
||||
### Property 3: session_id 格式不变量
|
||||
|
||||
*对于任意* conversation_id(正整数)和 created_timestamp,生成的 session_id 应匹配正则 `^conv_\d+_\d+$`,且从 session_id 可以反解出原始 conversation_id。
|
||||
|
||||
**Validates: Requirements 3.1**
|
||||
|
||||
### Property 4: 对话复用规则
|
||||
|
||||
*对于任意* 入口类型和时间差组合,对话复用决策应满足:task 入口始终复用已有对话、customer/coach 入口在 3 天内复用、general 入口始终新建。
|
||||
|
||||
**Validates: Requirements 3.6**
|
||||
|
||||
### Property 5: 熔断器 app_id 隔离
|
||||
|
||||
*对于任意* 两个不同的 app_id,对其中一个记录任意次数的失败,不应改变另一个 app_id 的熔断状态。
|
||||
|
||||
**Validates: Requirements 5.1**
|
||||
|
||||
### Property 6: 熔断器状态机转换
|
||||
|
||||
*对于任意* app_id 和任意成功/失败事件序列,熔断器状态转换应满足:连续 5 次失败→OPEN;OPEN 经过 60 秒→HALF_OPEN;HALF_OPEN + 成功→CLOSED;HALF_OPEN + 失败→OPEN。且 CLOSED 状态下任何成功事件应重置失败计数。
|
||||
|
||||
**Validates: Requirements 5.2, 5.3, 5.4, 5.5**
|
||||
|
||||
### Property 7: 限流器窗口控制
|
||||
|
||||
*对于任意* 用户 ID 或门店 ID,在滑动窗口内(App1: 60 秒/10 次,App2~8: 3600 秒/100 次)请求次数未超过阈值时应允许,超过阈值时应拒绝;窗口外的历史请求不影响当前判断。
|
||||
|
||||
**Validates: Requirements 6.1, 6.2**
|
||||
|
||||
### Property 8: Token 预算检查正确性
|
||||
|
||||
*对于任意* 一组 `ai_run_logs` 记录(含 `tokens_used` 和 `created_at`),日聚合应等于当日所有 `status='success'` 记录的 `tokens_used` 之和,月聚合应等于当月之和;当日聚合 ≥ 100,000 或月聚合 ≥ 2,000,000 时,`check_budget()` 应返回 `allowed=false`。
|
||||
|
||||
**Validates: Requirements 7.1, 7.3**
|
||||
|
||||
### Property 9: 事件类型到调用链映射
|
||||
|
||||
*对于任意* 事件类型,Dispatcher 应执行正确的调用链:`consumption`(无助教)→ App3→App8→App7;`consumption`(有助教)→ App3→App8→App7 + App4→App5;`note_created` → App6→App8;`task_assigned` → App4→App5;`dws_completed` → App2(8 个时间维度)。
|
||||
|
||||
**Validates: Requirements 9.1, 9.2, 9.3, 11.1**
|
||||
|
||||
### Property 10: 调用链容错不中断
|
||||
|
||||
*对于任意* 调用链和任意失败步骤位置,该步骤失败后链中后续步骤仍应继续执行(使用已有缓存),不中断整条链。
|
||||
|
||||
**Validates: Requirements 9.4**
|
||||
|
||||
### Property 11: 内部 API 认证
|
||||
|
||||
*对于任意* HTTP 请求到 `/api/internal/ai/trigger`,当 `Authorization` Header 缺失或 token 不匹配 `INTERNAL_API_TOKEN` 时应返回 HTTP 401;token 匹配时应正常处理请求。
|
||||
|
||||
**Validates: Requirements 10.2, 10.3**
|
||||
|
||||
### Property 12: 事件去重与强制执行
|
||||
|
||||
*对于任意* 两个具有相同 `(event_type, member_id, site_id, date)` 的自动触发事件,第二个应被跳过(`skipped_duplicate`);但当 `is_forced=true` 时,即使存在重复也应正常执行。
|
||||
|
||||
**Validates: Requirements 12.1, 12.2**
|
||||
|
||||
### Property 13: App8 幂等写入
|
||||
|
||||
*对于任意* member_id 和日期,多次执行 App8 写入 `member_retention_clue` 后,该 member 该天的记录数应恒为 1(DELETE + INSERT 事务保证)。
|
||||
|
||||
**Validates: Requirements 12.3**
|
||||
|
||||
### Property 14: 缓存过期策略正确性
|
||||
|
||||
*对于任意* App 类型和写入时间,缓存记录的 `expires_at` 应匹配该 App 的过期策略:App2 为当日 23:59:59、App3/4/5/7/8 为写入时间 + 7 天、App6 为写入时间 + 30 天。
|
||||
|
||||
**Validates: Requirements 13.1, 11.3**
|
||||
|
||||
### Property 15: 缓存查询过滤
|
||||
|
||||
*对于任意* 缓存数据集(包含各种 `status` 和 `expires_at` 值),查询结果应仅包含 `status='valid'` 且 `expires_at > now()` 或 `expires_at IS NULL` 的记录;`generating`、`expired`、`invalidated` 状态的记录不应出现在查询结果中。
|
||||
|
||||
**Validates: Requirements 13.4**
|
||||
|
||||
### Property 16: 缓存保留上限
|
||||
|
||||
*对于任意* App 类型(App2~8)和任意数量的缓存写入,清理后每个 App 的 `ai_cache` 记录数不超过 20,000 条;App1 对话记录不受此限制。
|
||||
|
||||
**Validates: Requirements 13.5**
|
||||
|
||||
### Property 17: SSE 事件流格式
|
||||
|
||||
*对于任意* 流式对话输出序列(包括正常完成和中途错误),SSE 事件流应由零或多个 `event: message` 事件组成,最终以恰好一个 `event: done`(正常)或 `event: error`(异常)事件结束。
|
||||
|
||||
**Validates: Requirements 15.2**
|
||||
|
||||
### Property 18: AI 运行日志状态机
|
||||
|
||||
*对于任意* AI 调用(成功/失败/超时/预算超限),`ai_run_logs` 记录的状态转换应满足:正常调用经历 `pending→running→success/failed/timeout`;预算超限直接创建 `budget_exceeded` 状态。且 `success` 状态的记录必须包含 `response_text`、`tokens_used`、`latency_ms`、`finished_at`;`failed` 状态必须包含 `error_message`。
|
||||
|
||||
**Validates: Requirements 16.1, 16.2, 16.3, 16.4, 16.5**
|
||||
|
||||
### Property 19: Prompt 截断不变量
|
||||
|
||||
*对于任意* 长度的 prompt 字符串,存储到 `ai_run_logs.request_prompt` 后的长度不超过 2000 字符;长度 ≤ 2000 的 prompt 应完整保留。
|
||||
|
||||
**Validates: Requirements 16.6**
|
||||
|
||||
### Property 20: Application API 响应解析
|
||||
|
||||
*对于任意* 合法的 `Application.call()` 响应对象(`response.output.text` 为 JSON 字符串),解析应正确提取并返回等价的 Python dict;`response.output.text` 为非法 JSON 时应触发重试。
|
||||
|
||||
**Validates: Requirements 4.4**
|
||||
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 异常层级
|
||||
|
||||
```
|
||||
DashScopeError (基类)
|
||||
├── DashScopeApiError — Application API 调用失败(重试耗尽)
|
||||
│ ├── DashScopeAuthError — API Key 无效(HTTP 401)
|
||||
│ └── DashScopeTimeoutError — 调用超时
|
||||
├── DashScopeJsonParseError — 响应 JSON 解析失败(重试耗尽)
|
||||
├── CircuitOpenError — 熔断器处于 OPEN 状态
|
||||
├── RateLimitExceededError — 限流阈值超限
|
||||
└── BudgetExceededError — Token 预算超限
|
||||
```
|
||||
|
||||
### 错误处理策略
|
||||
|
||||
| 错误类型 | App1(用户对话) | App2~8(后台任务) |
|
||||
|---------|-----------------|-------------------|
|
||||
| `DashScopeApiError` (5xx) | SSE `event: error` + 友好提示 | 记录 `failed` 到 `ai_run_logs`,链继续 |
|
||||
| `DashScopeAuthError` (401) | SSE `event: error` + "AI 服务配置异常" | 记录 `failed`,链中断(全局配置问题) |
|
||||
| `DashScopeTimeoutError` | SSE `event: error` + "AI 响应超时" | 记录 `timeout`,链继续 |
|
||||
| `DashScopeJsonParseError` | 不适用(App1 不解析 JSON) | 重试 3 次后记录 `failed`,链继续 |
|
||||
| `CircuitOpenError` | SSE `event: error` + "AI 服务暂时不可用" | 记录 `circuit_open`,跳过该步骤 |
|
||||
| `RateLimitExceededError` | HTTP 429 + "请求过于频繁" | 记录 `rate_limited`,跳过执行 |
|
||||
| `BudgetExceededError` | SSE `event: error` + "AI 额度已用完" | 记录 `budget_exceeded`,跳过执行 |
|
||||
| session_id 过期 | 从本地加载历史重建,对用户透明 | 不适用(App2~8 无 session) |
|
||||
| App8 幂等写入失败 | 不适用 | 事务回滚,记录错误到 `ai_trigger_jobs` |
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 熔断状态下:App1 返回友好提示;App2~8 跳过执行,前端展示已有缓存
|
||||
- 预算超限:App1 返回"额度已用完";App2~8 跳过,记录 `budget_exceeded`
|
||||
- 限流超限:App1 返回"请求过于频繁";App2~8 跳过,记录 `rate_limited`
|
||||
- 调用链某步失败:记录错误,后续步骤使用已有缓存继续
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 属性测试(Property-Based Testing)
|
||||
|
||||
使用 `hypothesis` 库(项目已有依赖),每个属性测试最少运行 100 次迭代。
|
||||
|
||||
测试文件位于 `tests/` 目录(Monorepo 级属性测试),按组件分文件:
|
||||
|
||||
| 测试文件 | 覆盖属性 |
|
||||
|---------|---------|
|
||||
| `tests/test_circuit_breaker_props.py` | Property 5(隔离)、Property 6(状态机) |
|
||||
| `tests/test_rate_limiter_props.py` | Property 7(窗口控制) |
|
||||
| `tests/test_budget_tracker_props.py` | Property 8(预算检查) |
|
||||
| `tests/test_dispatcher_props.py` | Property 9(调用链映射)、Property 10(容错)、Property 12(去重) |
|
||||
| `tests/test_cache_service_props.py` | Property 14(过期策略)、Property 15(查询过滤)、Property 16(保留上限) |
|
||||
| `tests/test_dashscope_client_props.py` | Property 1(重试)、Property 20(响应解析) |
|
||||
| `tests/test_ai_config_props.py` | Property 2(环境变量校验) |
|
||||
| `tests/test_session_props.py` | Property 3(session_id 格式)、Property 4(对话复用) |
|
||||
| `tests/test_run_log_props.py` | Property 18(日志状态机)、Property 19(Prompt 截断) |
|
||||
| `tests/test_sse_props.py` | Property 17(SSE 格式) |
|
||||
|
||||
每个属性测试必须包含注释标签:
|
||||
```python
|
||||
# Feature: P14-ai-dashscope-migration, Property 6: 熔断器状态机转换
|
||||
@given(st.lists(st.sampled_from(["success", "failure"]), min_size=1, max_size=50))
|
||||
def test_circuit_breaker_state_machine(events):
|
||||
...
|
||||
```
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试覆盖属性测试不适合的场景:
|
||||
|
||||
| 测试文件 | 覆盖内容 |
|
||||
|---------|---------|
|
||||
| `tests/test_internal_ai_api.py` | Property 11(认证)、API 端点集成 |
|
||||
| `tests/test_app8_idempotent.py` | Property 13(幂等写入)、事务回滚 |
|
||||
| `tests/test_sse_endpoint.py` | SSE 端点集成、错误事件 |
|
||||
| `tests/test_session_recovery.py` | session_id 过期恢复流程(Requirements 3.5) |
|
||||
| `tests/test_app2_pregenerate.py` | App2 预生成 8 个时间维度(Requirements 11.2) |
|
||||
|
||||
### 测试配置
|
||||
|
||||
```python
|
||||
# conftest.py 中的 hypothesis 配置
|
||||
from hypothesis import settings
|
||||
settings.register_profile("ci", max_examples=200, deadline=10000)
|
||||
settings.register_profile("dev", max_examples=100, deadline=5000)
|
||||
```
|
||||
|
||||
### 测试依赖
|
||||
|
||||
- 属性测试:`hypothesis`(已有)
|
||||
- Mock:`unittest.mock`(标准库)
|
||||
- DashScope 调用:mock `Application.call()`,不依赖真实 API
|
||||
- 数据库:使用 `test_zqyy_app` 测试库(遵循 `testing-env.md` 规范)
|
||||
|
||||
246
docs/specs/P14-ai-dashscope-migration/requirements.md
Normal file
246
docs/specs/P14-ai-dashscope-migration/requirements.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 需求文档 — 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 占用过多存储空间
|
||||
252
docs/specs/P14-ai-dashscope-migration/tasks.md
Normal file
252
docs/specs/P14-ai-dashscope-migration/tasks.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# 实施计划: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(与设计文档一致)
|
||||
Reference in New Issue
Block a user