微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View File

@@ -0,0 +1 @@
{"specId": "cf5c24d6-ec72-4c49-8650-264ef414e10e", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,878 @@
# 设计文档P5 AI 集成层miniapp-ai-integration
## 概述
本设计文档描述 P5-A 阶段 AI 集成层的技术架构与实现方案。系统在现有 FastAPI 后端(`apps/backend/`)中新增 AI 模块,通过阿里云百炼 API通义千问为 8 个 AI 应用提供统一的调用能力。
核心交付物:
- 3 张新表(`biz.ai_conversations``biz.ai_messages``biz.ai_cache`
- 百炼 API 统一封装层(流式 + 非流式)
- 应用 1 SSE 流式对话端点
- 应用 2 财务洞察Prompt 完整)+ 应用 8 维客线索整理Prompt 完整)
- 应用 3/4/5/6/7 触发机制与调用骨架
- 事件调度与调用链编排
- AI 缓存读写 API
设计原则:
- **统一封装**:所有 AI 调用经 `BailianClient` 统一出口,便于重试、计量、日志
- **事件驱动**:复用现有 `trigger_scheduler.fire_event()` 机制,扩展支持串行调用链
- **骨架优先**P5-A 只实现管道和框架Prompt 细化留给 P5-B 阶段
- **site_id 隔离**:所有表和查询强制 site_id 过滤
## 架构
### 系统架构图
```mermaid
graph TB
subgraph 微信小程序
MP_CHAT[对话页面]
MP_PAGES[其他页面<br/>财务看板/任务详情/客户详情]
end
subgraph FastAPI 后端
subgraph AI 模块 - apps/backend/app/ai/
SSE[SSE 端点<br/>/api/ai/chat/stream]
CACHE_API[缓存 API<br/>/api/ai/cache]
HISTORY_API[历史对话 API<br/>/api/ai/conversations]
DISPATCHER[AI Event Dispatcher<br/>调用链编排]
BAILIAN[BailianClient<br/>百炼 API 封装]
end
subgraph 现有服务
NOTE_SVC[note_service<br/>备注服务]
TRIGGER[trigger_scheduler<br/>触发器调度]
TASK_GEN[task_generator<br/>任务生成]
end
end
subgraph 外部服务
BAILIAN_API[阿里云百炼 API<br/>通义千问]
end
subgraph PostgreSQL - zqyy_app
AI_CONV[biz.ai_conversations]
AI_MSG[biz.ai_messages]
AI_CACHE_T[biz.ai_cache]
CLUE_T[member_retention_clue]
end
MP_CHAT -->|SSE| SSE
MP_PAGES -->|REST| CACHE_API
MP_CHAT -->|REST| HISTORY_API
SSE --> BAILIAN
DISPATCHER --> BAILIAN
BAILIAN -->|HTTP/SSE| BAILIAN_API
NOTE_SVC -->|备注提交事件| DISPATCHER
TRIGGER -->|消费/任务分配事件| DISPATCHER
DISPATCHER -->|写入| AI_CONV
DISPATCHER -->|写入| AI_MSG
DISPATCHER -->|写入| AI_CACHE_T
DISPATCHER -->|全量替换 AI 线索| CLUE_T
SSE -->|写入| AI_CONV
SSE -->|写入| AI_MSG
```
### 事件调用链
```mermaid
sequenceDiagram
participant E as 业务事件
participant D as AI Dispatcher
participant A3 as App3 线索
participant A8 as App8 整理
participant A7 as App7 客户分析
participant A4 as App4 关系分析
participant A5 as App5 话术
Note over E,A5: 消费事件链(无助教)
E->>D: consumption_event(member_id, site_id)
D->>A3: 调用(串行)
A3-->>D: 线索结果 → ai_cache
D->>A8: 调用(串行)
A8-->>D: 整合线索 → ai_cache + member_retention_clue
D->>A7: 调用(串行)
A7-->>D: 客户分析 → ai_cache
Note over E,A5: 消费事件链(有助教)
E->>D: consumption_event(member_id, assistant_id, site_id)
D->>A3: 调用
A3-->>D: 线索结果
D->>A8: 调用
A8-->>D: 整合线索
D->>A7: 调用
D->>A4: 调用A8 完成后)
A4-->>D: 关系分析
D->>A5: 调用A4 完成后)
A5-->>D: 话术参考
Note over E,A5: 备注事件链
E->>D: note_event(member_id, note_id, site_id)
D->>+A6: 调用
Note right of A6: App6 备注分析
A6-->>-D: 线索 + 评分
D->>A8: 调用
A8-->>D: 整合线索
Note over E,A5: 任务分配事件链
E->>D: task_assign_event(assistant_id, member_id, site_id)
D->>A4: 调用(读已有 A8 缓存)
A4-->>D: 关系分析
D->>A5: 调用
A5-->>D: 话术参考
```
### 模块目录结构
```
apps/backend/app/ai/
├── __init__.py
├── bailian_client.py # 百炼 API 统一封装
├── dispatcher.py # AI 事件调度与调用链编排
├── cache_service.py # AI 缓存读写服务
├── conversation_service.py # 对话记录持久化服务
├── apps/
│ ├── __init__.py
│ ├── app1_chat.py # 应用 1 通用对话
│ ├── app2_finance.py # 应用 2 财务洞察
│ ├── app3_clue.py # 应用 3 客户数据维客线索
│ ├── app4_analysis.py # 应用 4 关系分析
│ ├── app5_tactics.py # 应用 5 话术参考
│ ├── app6_note.py # 应用 6 备注分析
│ ├── app7_customer.py # 应用 7 客户分析
│ └── app8_consolidation.py # 应用 8 维客线索整理
├── prompts/
│ ├── __init__.py
│ ├── app2_finance_prompt.py # 应用 2 完整 Prompt
│ └── app8_consolidation_prompt.py # 应用 8 完整 Prompt
└── schemas.py # Pydantic 模型
apps/backend/app/routers/
├── xcx_ai_chat.py # SSE 对话路由
└── xcx_ai_cache.py # 缓存查询路由
```
## 组件与接口
### 1. BailianClient百炼 API 统一封装)
文件:`apps/backend/app/ai/bailian_client.py`
技术方案(基于百炼官方文档):
- **流式调用**:使用 OpenAI 兼容接口(百炼支持 OpenAI SDK 协议),`stream=True` 返回 SSE 事件流
- **非流式调用**`stream=False`,返回完整 JSON 响应
- **JSON 输出模式**:通过 System Prompt 约束 + `response_format={"type": "json_object"}` 参数(百炼兼容 OpenAI 的 JSON mode
- **重试策略**:指数退避,最多 3 次,基础间隔 1s → 2s → 4s
- **SDK 选择**:使用 `openai` Python SDK百炼兼容 OpenAI 协议),`base_url` 指向百炼端点
```python
class BailianClient:
"""百炼 API 统一封装层。"""
def __init__(self, api_key: str, base_url: str, model: str):
"""
Args:
api_key: 百炼 API Key从 BAILIAN_API_KEY 环境变量读取)
base_url: 百炼 API 端点(从 BAILIAN_BASE_URL 环境变量读取)
model: 模型标识(如 qwen-plus
"""
async def chat_stream(
self,
messages: list[dict],
*,
temperature: float = 0.7,
max_tokens: int = 2000,
) -> AsyncGenerator[str, None]:
"""流式调用,逐 chunk 返回文本。用于应用 1 SSE。"""
async def chat_json(
self,
messages: list[dict],
*,
temperature: float = 0.3,
max_tokens: int = 4000,
) -> tuple[dict, int]:
"""非流式调用,返回解析后的 JSON dict 和 tokens_used。
用于应用 2-8。
Raises:
BailianJsonParseError: JSON 解析失败时抛出
BailianApiError: API 调用失败(重试耗尽后)
"""
def _inject_current_time(self, messages: list[dict]) -> list[dict]:
"""在首条消息的 content JSON 中注入 current_time 字段。"""
async def _call_with_retry(self, **kwargs) -> Any:
"""带指数退避的重试封装。"""
```
环境变量(新增到 `.env` / `.env.template`
```
BAILIAN_API_KEY=sk-xxx
BAILIAN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
BAILIAN_MODEL=qwen-plus
```
### 2. AI Event Dispatcher事件调度器
文件:`apps/backend/app/ai/dispatcher.py`
调度器负责根据业务事件编排 AI 应用调用链。与现有 `trigger_scheduler` 的关系:
- `trigger_scheduler.fire_event()` 触发业务事件 → 调用 `ai_dispatcher` 对应的 handler
- `ai_dispatcher` 内部管理串行调用链的执行顺序
```python
class AIDispatcher:
"""AI 应用调用链编排器。"""
async def handle_consumption_event(
self,
member_id: int,
site_id: int,
settle_id: int,
assistant_id: int | None = None,
) -> None:
"""消费事件链App3 → App8 → App7+ App4 → App5 如有助教)。"""
async def handle_note_event(
self,
member_id: int,
site_id: int,
note_id: int,
note_content: str,
noted_by_name: str,
) -> None:
"""备注事件链App6 → App8。"""
async def handle_task_assign_event(
self,
assistant_id: int,
member_id: int,
site_id: int,
task_type: str,
) -> None:
"""任务分配事件链App4 → App5读已有 App8 缓存)。"""
async def _run_chain(
self,
chain: list[Callable],
context: dict,
) -> None:
"""串行执行调用链,某步失败记录日志后继续。"""
```
容错策略:
- 调用链中某个应用失败 → 记录错误日志 + 写入 `ai_conversations`(标记失败)
- 后续应用使用已有缓存继续执行,不阻塞整条链
- 整条链在后台异步执行,不阻塞业务请求
### 3. AI Cache Service缓存读写服务
文件:`apps/backend/app/ai/cache_service.py`
```python
class AICacheService:
"""AI 缓存读写服务。"""
def get_latest(
self,
cache_type: str,
site_id: int,
target_id: str,
) -> dict | None:
"""查询最新缓存记录。"""
def get_history(
self,
cache_type: str,
site_id: int,
target_id: str,
limit: int = 2,
) -> list[dict]:
"""查询历史缓存记录(按 created_at DESC用于 Prompt reference。"""
def write_cache(
self,
cache_type: str,
site_id: int,
target_id: str,
result_json: dict,
triggered_by: str | None = None,
score: int | None = None,
expires_at: datetime | None = None,
) -> int:
"""写入缓存记录,返回 id。写入后异步清理超限记录。"""
def _cleanup_excess(
self,
cache_type: str,
site_id: int,
target_id: str,
max_count: int = 500,
) -> int:
"""清理超限记录,保留最近 max_count 条,返回删除数量。"""
```
### 4. Conversation Service对话记录持久化
文件:`apps/backend/app/ai/conversation_service.py`
```python
class ConversationService:
"""AI 对话记录持久化服务。"""
def create_conversation(
self,
user_id: int | str,
nickname: str,
app_id: str,
site_id: int,
source_page: str | None = None,
source_context: dict | None = None,
) -> int:
"""创建对话记录,返回 conversation_id。
系统自动调用时 user_id 为 'system'"""
def add_message(
self,
conversation_id: int,
role: str,
content: str,
tokens_used: int | None = None,
) -> int:
"""添加消息记录,返回 message_id。"""
def get_conversations(
self,
user_id: int,
site_id: int,
page: int = 1,
page_size: int = 20,
) -> list[dict]:
"""查询用户历史对话列表,按时间倒序,懒加载。"""
def get_messages(
self,
conversation_id: int,
) -> list[dict]:
"""查询对话的所有消息。"""
```
### 5. Clue Writer维客线索写入器
文件:集成在 `apps/backend/app/ai/apps/app8_consolidation.py`
```python
class ClueWriter:
"""维客线索全量替换写入器。"""
def replace_ai_clues(
self,
member_id: int,
site_id: int,
clues: list[dict],
) -> int:
"""全量替换该客户的 AI 来源线索。
1. DELETE FROM member_retention_clue
WHERE member_id = %s AND site_id = %s
AND source IN ('ai_consumption', 'ai_note')
2. INSERT 新线索(人工线索 source='manual' 不受影响)
字段映射:
- category → category
- emoji + summary → summary"📅 偏好周末下午时段消费"
- detail → detail
- providers → recorded_by_name
- source: 纯 App3 → ai_consumption纯 App6 → ai_note混合 → ai_consumption
- recorded_by_assistant_id: NULL系统触发
返回写入的线索数量。
"""
```
### 6. API 端点
#### 6.1 SSE 对话端点
路由文件:`apps/backend/app/routers/xcx_ai_chat.py`
```
POST /api/ai/chat/stream
Content-Type: application/json
Accept: text/event-stream
Request Body:
{
"message": "string",
"source_page": "string",
"page_context": {},
"screen_content": "string" // 页面可见内容文本化
}
SSE Events:
data: {"type": "chunk", "content": "..."}
data: {"type": "done", "conversation_id": 123, "tokens_used": 456}
data: {"type": "error", "message": "..."}
认证JWT Token从 xcx_auth 获取)
隔离:从 JWT 中提取 user_id、site_id、nickname、role
```
#### 6.2 历史对话 API
```
GET /api/ai/conversations?page=1&page_size=20
→ [{ id, app_id, source_page, created_at, first_message_preview }]
GET /api/ai/conversations/{conversation_id}/messages
→ [{ id, role, content, tokens_used, created_at }]
```
#### 6.3 缓存查询 API
路由文件:`apps/backend/app/routers/xcx_ai_cache.py`
```
GET /api/ai/cache/{cache_type}?target_id=xxx
→ { id, cache_type, target_id, result_json, score, created_at }
认证JWT Token
隔离site_id 从 JWT 提取,强制过滤
```
### 7. 各应用骨架接口
每个应用实现统一的调用接口:
```python
# 应用基类模式(非继承,约定接口)
async def run(
context: dict, # 包含 member_id, site_id, 及应用特定参数
bailian: BailianClient,
cache_svc: AICacheService,
conv_svc: ConversationService,
) -> dict:
"""
执行 AI 应用调用。
1. 构建 Promptbuild_prompt
2. 调用百炼 APIbailian.chat_json
3. 写入 ai_conversations + ai_messages
4. 写入 ai_cache
5. 返回结果 dict
"""
```
骨架应用App3/4/5/6/7`build_prompt` 函数留接口:
```python
def build_prompt(context: dict) -> list[dict]:
"""构建 Prompt 消息列表。
P5-A 阶段:返回占位 Prompt标注待细化字段。
P5-B 阶段:由对应页面 spec 补充完整 Prompt。
"""
# TODO: P5-B 细化
return [
{"role": "system", "content": json.dumps({
"task": "...",
"current_time": "", # BailianClient 自动注入
# 以下字段待细化
"data": {},
"reference": {},
})},
]
```
## 数据模型
### DDLbiz.ai_conversations
```sql
CREATE TABLE IF NOT EXISTS biz.ai_conversations (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(50) NOT NULL, -- 用户 ID 或 'system'
nickname VARCHAR(100) NOT NULL DEFAULT '',
app_id VARCHAR(30) NOT NULL, -- app1_chat / app2_finance / ... / app8_consolidation
site_id BIGINT NOT NULL,
source_page VARCHAR(100), -- 来源页面标识
source_context JSONB, -- 页面上下文 JSON
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.ai_conversations IS 'AI 对话记录:每次 AI 调用(用户主动或系统自动)创建一条';
COMMENT ON COLUMN biz.ai_conversations.app_id IS '应用标识app1_chat / app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note / app7_customer / app8_consolidation';
COMMENT ON COLUMN biz.ai_conversations.user_id IS '用户 ID系统自动调用时为 system';
CREATE INDEX IF NOT EXISTS idx_ai_conv_user_site ON biz.ai_conversations (user_id, site_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_conv_app_site ON biz.ai_conversations (app_id, site_id, created_at DESC);
```
### DDLbiz.ai_messages
```sql
CREATE TABLE IF NOT EXISTS biz.ai_messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES biz.ai_conversations(id) ON DELETE CASCADE,
role VARCHAR(10) NOT NULL
CONSTRAINT chk_ai_msg_role CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
tokens_used INTEGER, -- 本条消息消耗的 token 数
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE biz.ai_messages IS 'AI 消息记录:对话中的每条消息(输入/输出/系统)';
CREATE INDEX IF NOT EXISTS idx_ai_msg_conv ON biz.ai_messages (conversation_id, created_at);
```
### DDLbiz.ai_cache
```sql
CREATE TABLE IF NOT EXISTS biz.ai_cache (
id BIGSERIAL PRIMARY KEY,
cache_type VARCHAR(30) NOT NULL
CONSTRAINT chk_ai_cache_type CHECK (
cache_type IN (
'app2_finance', 'app3_clue', 'app4_analysis',
'app5_tactics', 'app6_note_analysis',
'app7_customer_analysis', 'app8_clue_consolidated'
)
),
site_id BIGINT NOT NULL,
target_id VARCHAR(100) NOT NULL, -- 含义因 cache_type 而异
result_json JSONB NOT NULL,
score INTEGER, -- 应用 6 专用评分
triggered_by VARCHAR(100), -- 触发来源标识
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ -- 可选过期时间
);
COMMENT ON TABLE biz.ai_cache IS 'AI 应用缓存:各应用的结构化输出结果';
COMMENT ON COLUMN biz.ai_cache.target_id IS '目标 IDApp2=时间维度编码 / App3,6,7,8=member_id / App4,5={assistant_id}_{member_id}';
COMMENT ON COLUMN biz.ai_cache.score IS '评分:仅应用 6 使用1-10 分)';
CREATE INDEX IF NOT EXISTS idx_ai_cache_lookup ON biz.ai_cache (cache_type, site_id, target_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_ai_cache_cleanup ON biz.ai_cache (cache_type, site_id, target_id, created_at);
```
### target_id 约定
| cache_type | target_id 格式 | 示例 |
|---|---|---|
| app2_finance | 时间维度编码 | `this_month`, `last_week` |
| app3_clue | member_id | `12345` |
| app4_analysis | `{assistant_id}_{member_id}` | `100_12345` |
| app5_tactics | `{assistant_id}_{member_id}` | `100_12345` |
| app6_note_analysis | member_id | `12345` |
| app7_customer_analysis | member_id | `12345` |
| app8_clue_consolidated | member_id | `12345` |
### 应用 2 时间维度编码
| 编码 | 含义 | 计算规则 |
|---|---|---|
| `this_month` | 本月 | 当前营业日所在月 |
| `last_month` | 上月 | 当前月 - 1 |
| `this_week` | 本周 | 当前营业日所在周(周一起) |
| `last_week` | 上周 | 当前周 - 1 |
| `last_3_months` | 前 3 月(不含本月) | 当前月 - 3 ~ 当前月 - 1 |
| `this_quarter` | 本季 | 当前营业日所在季度 |
| `last_quarter` | 上季 | 当前季度 - 1 |
| `last_6_months` | 近 6 月(不含本月) | 当前月 - 6 ~ 当前月 - 1 |
营业日分界点:每日 08:00`BUSINESS_DAY_START_HOUR` 环境变量)。
### 应用输出 JSON Schema
#### 应用 3/6 线索格式(写入 ai_cache.result_json
```json
{
"clues": [
{
"category": "消费习惯",
"summary": "偏好周末下午时段消费",
"detail": "近 3 个月 8 次消费中 6 次在周六/日 14:00-18:00",
"emoji": "📅"
}
]
}
```
应用 6 额外包含 `score` 字段1-10写入 `ai_cache.score`
#### 应用 8 整合线索格式
```json
{
"clues": [
{
"category": "消费习惯",
"summary": "偏好周末下午时段消费",
"detail": "近 3 个月 8 次消费中 6 次在周六/日 14:00-18:00",
"emoji": "📅",
"providers": "系统,张三"
}
]
}
```
#### 应用 4 关系分析格式
```json
{
"task_description": "...",
"action_suggestions": ["建议1", "建议2"],
"one_line_summary": "..."
}
```
#### 应用 5 话术参考格式
```json
{
"tactics": [
{ "scenario": "...", "script": "..." }
]
}
```
#### 应用 7 客户分析格式
```json
{
"strategies": [
{ "title": "...", "content": "..." }
],
"summary": "..."
}
```
#### 应用 2 财务洞察格式
```json
{
"insights": [
{ "seq": 1, "title": "...", "body": "..." }
]
}
```
### 与现有表的关系
- `member_retention_clue`:应用 8 的 `ClueWriter` 全量替换 `source IN ('ai_consumption', 'ai_note')` 的记录,`source='manual'` 的人工线索不受影响
- `biz.notes`:应用 6 触发点,`note_service.create_note()` 中的 `ai_analyze_note()` 占位函数将被替换为真实调用
- `biz.trigger_jobs`:新增 AI 相关的事件触发器配置(`consumption_settled``note_created``task_assigned`
- `biz.coach_tasks`:应用 4 触发条件之一(任务分配事件)
## 正确性属性
*正确性属性是系统在所有合法执行路径上都应保持为真的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1: BailianClient 双模式调用一致性
*For any* 合法的消息列表,`chat_stream` 应返回非空的 chunk 序列(拼接后为完整文本),`chat_json` 应返回可解析的 JSON dict 和正整数 tokens_used。两种模式对相同输入都应成功返回mock API 正常响应时)。
**Validates: Requirements 2.1, 2.3, 2.4**
### Property 2: 指数退避重试策略
*For any* 失败次数 n1 ≤ n ≤ max_retriesBailianClient 应在第 n 次失败后等待 base_interval × 2^(n-1) 秒后重试;当失败次数超过 max_retries 时,应抛出 BailianApiError。
**Validates: Requirements 2.2**
### Property 3: JSON 解析失败错误处理
*For any* 非法 JSON 字符串作为 API 响应,`chat_json` 应抛出 BailianJsonParseError 而非静默返回空值或崩溃。
**Validates: Requirements 2.5**
### Property 4: current_time 注入不变量
*For any* 消息列表,经 `_inject_current_time` 处理后,首条消息的 content解析为 JSON应包含 `current_time` 字段,且值为 ISO 格式的时间字符串(精确到秒),其余消息不受影响。
**Validates: Requirements 2.6**
### Property 5: AI 调用记录持久化 round-trip
*For any* AI 应用调用app1-app8调用完成后(a) `ai_conversations` 应包含一条匹配 app_id、site_id 的记录;(b) `ai_messages` 应包含至少一条 role='system' 或 role='user' 的输入消息和一条 role='assistant' 的输出消息;(c) 输出消息的 tokens_used 应为正整数。
**Validates: Requirements 3.2, 3.4, 3.5, 13.1, 13.2, 13.3**
### Property 6: 历史对话列表排序与分页
*For any* 用户和 site_id查询历史对话列表返回的记录应按 created_at 严格降序排列,且每页数量不超过 page_size默认 20
**Validates: Requirements 3.7**
### Property 7: 缓存写入 round-trip
*For any* AI 应用app2-app8的调用结果写入 `ai_cache` 后,按 (cache_type, site_id, target_id) 查询最新记录应返回与写入内容一致的 result_json。
**Validates: Requirements 4.7, 5.6, 6.6, 7.5, 8.6, 9.5, 10.10**
### Property 8: AI 应用输出 JSON 结构验证
*For any* AI 应用调用结果的 result_json
- App2: 应包含 `insights` 数组,每项含 `seq`(正整数)、`title`(非空字符串)、`body`(非空字符串)
- App3: 应包含 `clues` 数组,每条含 `category`(∈ {客户基础, 消费习惯, 玩法偏好})、`summary``detail``emoji`
- App4: 应包含 `task_description``action_suggestions`(数组)、`one_line_summary`
- App5: 应包含 `tactics` 数组
- App6: 应包含 `score`1-10 整数)和 `clues` 数组,每条 category ∈ 6 个枚举值
- App7: 应包含 `strategies` 数组(每项含 `title``content`)和 `summary`
- App8: 应包含 `clues` 数组,每条含 `category`(∈ 6 个枚举值)、`summary``detail``emoji``providers`
**Validates: Requirements 4.4, 5.2, 5.3, 5.4, 6.3, 7.3, 8.2, 8.3, 8.4, 9.2, 10.4, 10.5**
### Property 9: Prompt reference 历史注入
*For any* 应用 3/4/5/6/7/8 的 Prompt 构建reference 字段应包含相关应用的缓存结果(如有),且历史记录附带 `generated_at` 时间戳。当缓存不存在时reference 应为空对象。
**Validates: Requirements 5.8, 6.4, 6.5, 7.2, 7.4, 8.8, 9.7**
### Property 10: 事件调用链顺序正确性
*For any* 业务事件:
- 消费事件(无助教):调用顺序严格为 App3 → App8 → App7
- 消费事件(有助教):调用顺序严格为 App3 → App8 → {App7, App4 → App5}App7 和 App4 均在 App8 之后)
- 备注事件:调用顺序严格为 App6 → App8
- 任务分配事件:调用顺序严格为 App4 → App5
**Validates: Requirements 11.1, 11.2, 11.3, 11.4, 11.5, 11.6**
### Property 11: 调用链容错不变量
*For any* 调用链执行过程中某个应用调用失败,后续应用应继续执行(使用已有缓存),整条链不应因单点失败而中断。失败的应用应有错误日志记录。
**Validates: Requirements 11.7**
### Property 12: ClueWriter 全量替换不变量
*For any* member_id 和 site_id执行 `ClueWriter.replace_ai_clues(member_id, site_id, new_clues)` 后:
- (a) 该客户的 AI 来源线索source IN ('ai_consumption', 'ai_note'))应恰好等于 new_clues 的数量
- (b) 人工线索source='manual')的数量应与替换前完全一致
- (c) 写入的记录中 recorded_by_assistant_id 应为 NULL
- (d) summary 字段应为 emoji + 空格 + 原始 summary 的拼接格式
**Validates: Requirements 10.7, 10.8, 10.9**
### Property 13: 缓存查询 site_id 隔离
*For any* 两个不同的 site_idA 和 B写入 site_id=A 的缓存记录后,以 site_id=B 查询应返回空结果(即使 cache_type 和 target_id 相同)。
**Validates: Requirements 12.1, 12.5**
### Property 14: 缓存保留上限
*For any* (cache_type, site_id, target_id) 组合,无论写入多少条记录,清理后该组合的记录总数应 ≤ 500。
**Validates: Requirements 12.3**
## 错误处理
### 百炼 API 层
| 错误场景 | 处理策略 |
|---|---|
| API 超时 | 指数退避重试(最多 3 次),超时阈值 30s |
| API 返回 HTTP 4xx | 不重试,立即抛出 BailianApiError |
| API 返回 HTTP 5xx | 指数退避重试 |
| 响应非法 JSON | 抛出 BailianJsonParseError记录原始响应到日志 |
| API Key 无效 | 不重试,抛出 BailianAuthError记录告警日志 |
| 流式连接中断 | 已接收的 chunk 拼接为部分回复,标记 incomplete |
### 事件调度层
| 错误场景 | 处理策略 |
|---|---|
| 调用链中某应用失败 | 记录错误日志 + 写入失败 conversation 记录,后续应用使用已有缓存继续 |
| 数据库连接失败 | 整条链中止,记录错误日志 |
| 缓存查询失败 | 传空 reference 继续执行,不阻塞 |
### SSE 端点层
| 错误场景 | 处理策略 |
|---|---|
| 用户未认证 | 返回 HTTP 401 |
| 消息为空 | 返回 HTTP 422 |
| 流式过程中百炼 API 失败 | 发送 `{"type": "error", "message": "..."}` SSE 事件 |
| 客户端断开连接 | 取消百炼 API 调用,清理资源 |
### 缓存服务层
| 错误场景 | 处理策略 |
|---|---|
| 查询无结果 | 返回 null/None不抛异常 |
| 写入失败 | 抛出异常,由调用方处理 |
| 清理超限失败 | 记录警告日志,不影响写入操作 |
### ClueWriter 层
| 错误场景 | 处理策略 |
|---|---|
| 全量替换事务失败 | 回滚整个事务,保留原有线索不变 |
| 线索数据不符合 CHECK 约束 | 回滚事务记录错误日志category 枚举不匹配) |
## 测试策略
### 属性测试Property-Based Testing
- **测试库**hypothesisPython
- **最小迭代次数**:每个属性测试 100 次
- **测试文件位置**`tests/test_p5_ai_integration_properties.py`Monorepo 级)+ `apps/backend/tests/test_ai_*.py`(模块级)
- **标签格式**`# Feature: 05-miniapp-ai-integration, Property {N}: {property_text}`
每个正确性属性对应一个属性测试:
| Property | 测试策略 | 生成器 |
|---|---|---|
| P1: 双模式调用 | Mock 百炼 API验证两种模式返回格式 | 随机消息列表 |
| P2: 重试策略 | Mock 可控失败次数的 API | 随机失败次数 (0-5) |
| P3: JSON 解析失败 | Mock 返回非法 JSON | 随机非 JSON 字符串 |
| P4: current_time 注入 | 纯函数测试 | 随机消息列表 |
| P5: 记录持久化 | Mock 百炼 + 真实 DBtest_zqyy_app | 随机 app_id、消息内容 |
| P6: 历史列表排序 | 真实 DB | 随机对话记录(随机时间戳) |
| P7: 缓存 round-trip | 真实 DB | 随机 cache_type、target_id、result_json |
| P8: 输出 JSON 结构 | JSON Schema 验证 | 随机 AI 响应(符合各应用 schema |
| P9: reference 历史注入 | Mock 缓存数据 | 随机缓存记录(含/不含历史) |
| P10: 调用链顺序 | Mock 所有应用,记录调用序列 | 随机事件类型和参数 |
| P11: 调用链容错 | Mock 随机应用失败 | 随机失败位置 |
| P12: ClueWriter 替换 | 真实 DB | 随机线索列表 + 预置人工线索 |
| P13: site_id 隔离 | 真实 DB | 随机 site_id 对 |
| P14: 缓存上限 | 真实 DB | 批量写入(>500 条) |
### 单元测试
单元测试聚焦于具体示例和边界条件,与属性测试互补:
| 测试范围 | 测试内容 |
|---|---|
| 表结构验证 | 验证 3 张表的列、类型、约束、索引(需求 1.1-1.5 |
| App2 时间维度 | 验证 8 个时间维度编码的计算逻辑(营业日分界点 08:00 |
| App2 字段映射 | 验证 Prompt 使用 items_sum 口径而非 consume_money |
| SSE 协议 | 验证 Content-Type: text/event-stream 和事件格式 |
| ClueWriter 字段映射 | 验证 emoji+summary 拼接、source 判断逻辑 |
| 缓存 CHECK 约束 | 验证非法 cache_type 被拒绝 |
| App6 评分范围 | 验证 score 字段存储在 ai_cache.score |
### 集成测试
| 测试范围 | 测试内容 |
|---|---|
| 完整消费事件链 | Mock 百炼 API验证 App3→App8→App7 全链路 |
| 备注事件链 | Mock 百炼 API验证 App6→App8 全链路 |
| note_service 集成 | 验证 `ai_analyze_note` 占位函数被替换后的调用流程 |
| SSE 端到端 | 使用 httpx 的 SSE 客户端验证流式响应 |

View File

@@ -0,0 +1,257 @@
# 需求文档P5 AI 集成层miniapp-ai-integration
## 简介
本文档定义小程序 AI 集成层的需求规格,覆盖 P5-A 阶段(管道 + 骨架)。系统为台球门店助教和管理者提供 8 个 AI 应用,包括通用对话、财务洞察、客户数据分析、关系分析、话术参考、备注分析、客户分析和维客线索整理。技术栈为 FastAPI 后端 + 微信小程序前端 + PostgreSQLzqyy_app 业务库),通过阿里云百炼 API通义千问提供 AI 能力。
P5-A 阶段交付"管道":建表、百炼封装、缓存 API、SSE 框架,以及 Prompt 已完全确定的应用(应用 2、应用 8。应用 3/4/5/6/7 只实现触发机制和调用骨架Prompt 拼接函数留接口)。
> P5-B 阶段Prompt 细化)不在本 spec 范围,将分散到 P6task-detail和 P9customer-detail的对应任务中完成。
## 术语表
- **AI_Integration_System**P5 AI 集成层系统整体,包含后端 API、百炼封装、事件调度、缓存管理等
- **Bailian_Client**:百炼 API 统一封装层,负责与阿里云通义千问 API 的通信(流式/非流式)
- **SSE_Endpoint**Server-Sent Events 流式返回端点,用于应用 1 通用对话的逐字推送
- **AI_Cache_Service**AI 缓存读写服务,管理 `biz.ai_cache` 表的 CRUD 和保留策略
- **Event_Dispatcher**:事件调度器,负责根据业务事件(消费、备注、任务分配)触发对应 AI 应用调用链
- **Clue_Writer**:维客线索写入器,负责将应用 8 整合后的线索全量替换写入 `member_retention_clue`
- **App1_Chat**:应用 1 通用对话,用户主动发起的流式对话
- **App2_Finance**:应用 2 财务洞察,每日自动生成 8 个时间维度的财务分析
- **App3_Clue**:应用 3 客户数据维客线索分析,客户新增消费时自动触发
- **App4_Analysis**:应用 4 关系分析/任务建议,助教参与新结算或任务分配时触发
- **App5_Tactics**:应用 5 话术参考,联动应用 4 自动触发
- **App6_Note**:应用 6 备注分析,备注提交时自动触发
- **App7_Customer**:应用 7 客户分析,消费事件链中应用 8 完成后触发
- **App8_Consolidation**:应用 8 维客线索整理,应用 3 或应用 6 产出后触发
- **items_sum**:校准后的消费金额口径,= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money禁止使用 consume_money
- **ai_conversations**AI 对话表,记录每次对话的元信息
- **ai_messages**AI 消息表,记录对话中的每条消息
- **ai_cache**AI 缓存表,存储各应用的结构化输出结果
- **member_retention_clue**:维客线索表,存储整合后的客户维护线索
- **营业日分界点**:每日 08:00用于时间维度计算的日切点
## 需求
### 需求 1数据库表结构
**用户故事:** 作为系统,我需要持久化存储所有 AI 对话记录和缓存结果,以便支撑 8 个 AI 应用的数据读写需求。
#### 验收标准
1. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_conversations`包含字段id、user_id、nickname、app_id、site_id、source_page、source_contextJSON、created_at
2. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_messages`包含字段id、conversation_id外键关联 ai_conversations、roleuser/assistant/system、content、tokens_used、created_at
3. THE AI_Integration_System SHALL 在 `biz` schema 下创建 `ai_cache`包含字段id、cache_type枚举app2_finance / app3_clue / app4_analysis / app5_tactics / app6_note_analysis / app7_customer_analysis / app8_clue_consolidated、site_id、target_id、result_json、score应用 6 专用、triggered_bytrigger_job_id、created_at、expires_at
4. THE AI_Integration_System SHALL 对 ai_cache 表的 target_id 按应用约定存储:应用 2 存时间维度编码、应用 3/6/7/8 存 member_id、应用 4/5 存 `{assistant_id}_{member_id}` 格式
5. THE AI_Integration_System SHALL 对所有三张表启用 site_id 字段以支持多门店隔离
### 需求 2百炼 API 统一封装层
**用户故事:** 作为开发者,我需要一个统一的百炼 API 封装层,以便所有 AI 应用通过一致的接口调用阿里云通义千问,降低重复代码和维护成本。
#### 验收标准
1. THE Bailian_Client SHALL 支持流式调用模式(用于应用 1 SSE 推送)和非流式调用模式(用于应用 2-8 结构化输出)
2. THE Bailian_Client SHALL 在 API 调用失败时执行自动重试(含指数退避策略)
3. THE Bailian_Client SHALL 记录每次 API 调用的 tokens_used 统计信息
4. THE Bailian_Client SHALL 支持 JSON 输出模式,确保应用 2-8 返回的内容可解析为结构化 JSON
5. IF 百炼 API 返回非预期格式或解析失败THEN THE Bailian_Client SHALL 记录错误日志并返回明确的错误信息
6. THE Bailian_Client SHALL 在每次调用的首条 Prompt JSON 中统一注入 `current_time` 字段(精确到秒)
### 需求 3应用 1 通用对话SSE 流式)
**用户故事:** 作为助教,我可以在任意页面点击 AI 按钮,跳转到对话页面与 AI 交流AI 了解当前页面上下文。
#### 验收标准
1. THE SSE_Endpoint SHALL 以 Server-Sent Events 协议向前端推送 AI 回复,实现逐字展示效果
2. WHEN 用户从任意页面进入 chat 页面时THE App1_Chat SHALL 始终新建一条 ai_conversations 记录(不复用已有对话)
3. THE App1_Chat SHALL 在首条消息中注入页面上下文,包含 source_page来源页面标识、page_context页面上下文摘要、screen_content屏幕可见内容文本化描述
4. WHEN 用户发送消息时THE App1_Chat SHALL 立即将用户消息写入 ai_messagesrole=user
5. WHEN 流式返回完成后THE App1_Chat SHALL 将完整的 assistant 回复写入 ai_messagesrole=assistant包含 tokens_used
6. THE App1_Chat SHALL 通过 `biz_params.user_prompt_params` 传入 User_ID、Role助教/管理者、Nickname 实现信息隔离
7. THE App1_Chat SHALL 提供历史对话列表接口,按时间倒序展示,每页 20 条懒加载
8. THE App1_Chat SHALL 搭建上下文注入框架页面文本化工具留接口P5-B 阶段各页面逐步实现)
### 需求 4应用 2 财务洞察
**用户故事:** 作为管理者,我在财务看板能看到 AI 生成的财务洞察分析,覆盖多个时间维度。
#### 验收标准
1. THE App2_Finance SHALL 由 ETL 调度器在每日 08:00营业日分界点后的首次任务执行时触发
2. THE App2_Finance SHALL 在 DWS 日更数据更新完成后,依次对 8 个时间维度发起独立调用(共 8 次百炼 API 调用)
3. THE App2_Finance SHALL 覆盖 8 个时间维度本月this_month、上月last_month、本周this_week、上周last_week、前 3 月不含本月last_3_months、本季this_quarter、上季last_quarter、近 6 月不含本月last_6_months
4. THE App2_Finance SHALL 返回结构化 JSON格式为序号 + 标题 + 正文的数组
5. THE App2_Finance SHALL 在 Prompt 中包含当期和上期的收入结构table_fee、assistant_pd、assistant_cx、goods、recharge、储值资产、费用汇总、平台结算数据
6. THE App2_Finance SHALL 使用已校准的收入结构字段映射table_fee = table_charge_money、assistant_pd = assistant_pd_money、assistant_cx = assistant_cx_money、goods = goods_money、recharge = 充值 pay_amountsettle_type=5
7. THE App2_Finance SHALL 将每次调用结果写入 ai_cachecache_type=app2_financetarget_id=时间维度编码)
8. IF ETL 调度器中尚无应用 2 的调度逻辑THEN THE AI_Integration_System SHALL 在 P5-A 阶段补充该调度任务
### 需求 5应用 3 客户数据维客线索分析(骨架)
**用户故事:** 作为系统,客户新增消费时自动通过 AI 分析客户数据,提取维客线索。
#### 验收标准
1. WHEN 客户新增消费结账单出现THE Event_Dispatcher SHALL 触发 App3_Clue 调用
2. THE App3_Clue SHALL 返回 JSON 格式的线索数组,每条线索包含 category分类标签、summary摘要、detail详情、emoji
3. THE App3_Clue SHALL 将分类标签限定为 3 个枚举值:客户基础、消费习惯、玩法偏好
4. THE App3_Clue SHALL 将线索提供者统一标记为"系统"
5. THE App3_Clue SHALL 使用 items_sum 作为消费金额口径(= table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money禁止使用 consume_money
6. THE App3_Clue SHALL 将结果写入 ai_cachecache_type=app3_cluetarget_id=member_id
7. THE App3_Clue SHALL 实现触发机制和调用框架Prompt 拼接函数留接口consumption_records 等字段待 P9-T1 细化)
8. THE App3_Clue SHALL 在 Prompt 的 reference 中包含应用 6 的线索结果(如有)和最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 6应用 4 关系分析/任务建议(骨架)
**用户故事:** 作为系统,助教参与新结算或被分配召回任务时,自动生成关系分析和任务建议。
#### 验收标准
1. WHEN 助教参与新结算时THE Event_Dispatcher SHALL 在消费事件链中等待应用 3 → 应用 8 完成后触发 App4_Analysis
2. WHEN 优先召回任务分配或高优先召回任务分配时THE Event_Dispatcher SHALL 直接触发 App4_Analysis读取应用 8 已有缓存)
3. THE App4_Analysis SHALL 返回 JSON 格式,包含任务描述、行动建议数组、一句话总结
4. THE App4_Analysis SHALL 在 Prompt 的 reference 中包含应用 8 当前最新维客线索和最近 2 套历史信息(附 generated_at 时间)
5. IF 应用 8 缓存不存在如新客户首次结算THEN THE App4_Analysis SHALL 在 reference 中传空对象Prompt 中标注"暂无历史线索"
6. THE App4_Analysis SHALL 将结果写入 ai_cachecache_type=app4_analysistarget_id=`{assistant_id}_{member_id}`
7. THE App4_Analysis SHALL 实现触发机制和调用框架Prompt 拼接函数留接口service_history、assistant_info 等字段待 P6-T4 细化)
### 需求 7应用 5 话术参考(骨架)
**用户故事:** 作为系统,应用 4 生成任务建议后,自动联动生成沟通话术参考。
#### 验收标准
1. WHEN App4_Analysis 调用完成后THE Event_Dispatcher SHALL 自动触发 App5_Tactics
2. THE App5_Tactics SHALL 接收应用 4 的完整返回结果作为 Prompt 中的 task_suggestion 字段
3. THE App5_Tactics SHALL 返回 JSON 格式的话术内容数组
4. THE App5_Tactics SHALL 在 Prompt 的 reference 中包含最近 2 套应用 8 的历史信息(附 generated_at 时间)
5. THE App5_Tactics SHALL 将结果写入 ai_cachecache_type=app5_tacticstarget_id=`{assistant_id}_{member_id}`
6. THE App5_Tactics SHALL 实现联动框架Prompt 拼接函数留接口service_history、assistant_info 等字段随应用 4 同步在 P6-T4 细化)
### 需求 8应用 6 备注分析(骨架)
**用户故事:** 作为系统,助教提交备注后,自动通过 AI 分析备注内容,提取维客线索并评分。
#### 验收标准
1. WHEN 备注提交时THE Event_Dispatcher SHALL 触发 App6_Note 调用
2. THE App6_Note SHALL 返回 JSON 格式,包含 score评分 1-10和 clues线索数组每条含 category、summary、detail、emoji
3. THE App6_Note SHALL 将分类标签限定为 6 个枚举值:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈
4. THE App6_Note SHALL 将线索提供者标记为当前备注提供人
5. THE App6_Note SHALL 使用 6 分为标准分的评分规则:重复信息/低价值/时效性低酌情扣分,高价值信息酌情加分
6. THE App6_Note SHALL 将结果写入 ai_cachecache_type=app6_note_analysistarget_id=member_idscore 字段存储评分
7. THE App6_Note SHALL 实现触发机制和调用框架Prompt 拼接函数留接口consumption_data 等字段待 P9-T1 细化)
8. THE App6_Note SHALL 在 Prompt 的 reference 中包含应用 3 的线索结果(如有)和最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 9应用 7 客户分析(骨架)
**用户故事:** 作为系统,客户结账单出现后自动生成客户全量分析与运营建议。
#### 验收标准
1. WHEN 消费事件链中 App8_Consolidation 完成后THE Event_Dispatcher SHALL 串行触发 App7_Customer确保读到本次消费触发的最新线索
2. THE App7_Customer SHALL 返回 JSON 格式,包含 strategies 数组(每条含 title 和 content和 summary一句话总结
3. THE App7_Customer SHALL 使用 items_sum 作为消费金额口径,禁止使用 consume_money
4. THE App7_Customer SHALL 对主观信息来自备注标注【来源XXX请甄别信息真实性】
5. THE App7_Customer SHALL 将结果写入 ai_cachecache_type=app7_customer_analysistarget_id=member_id
6. THE App7_Customer SHALL 实现触发机制和调用框架Prompt 拼接函数留接口objective_data 等字段待 P9-T1 细化)
7. THE App7_Customer SHALL 在 Prompt 的 reference 中包含最新 + 最近 2 套应用 8 的历史信息(附 generated_at 时间)
### 需求 10应用 8 维客线索整理
**用户故事:** 作为系统,应用 3 或应用 6 产出新线索后,自动整合去重生成统一维客线索,并写入 member_retention_clue 表。
#### 验收标准
1. WHEN App3_Clue 产出新线索后THE Event_Dispatcher SHALL 立即触发 App8_Consolidation
2. WHEN App6_Note 产出新线索后THE Event_Dispatcher SHALL 立即触发 App8_Consolidation
3. THE App8_Consolidation SHALL 接收应用 3 和应用 6 的全部线索内容作为输入(附 generated_at 时间)
4. THE App8_Consolidation SHALL 返回 JSON 格式的整合后线索数组,每条含 category、summary、detail、emoji、providers
5. THE App8_Consolidation SHALL 将分类标签限定为 6 个枚举值:客户基础、消费习惯、玩法偏好、促销偏好、社交关系、重要反馈(与 member_retention_clue 表 CHECK 约束一致)
6. THE App8_Consolidation SHALL 合并相似线索(多提供者以逗号分隔),其余线索原文返回,遵循最小改动原则
7. THE App8_Consolidation SHALL 将整合后的线索全量替换该客户在 member_retention_clue 中的所有 AI 来源线索source IN ('ai_consumption', 'ai_note')人工线索source='manual')不受影响
8. THE Clue_Writer SHALL 按以下字段映射写入 member_retention_cluecategory → category、emoji + summary → summaryemoji 拼接在前,如"📅 偏好周末下午时段消费"、detail → detail、providers → recorded_by_name、source 根据线索来源判断(纯应用 3 → ai_consumption纯应用 6 → ai_note混合来源 → ai_consumption
9. THE Clue_Writer SHALL 对系统触发的线索将 recorded_by_assistant_id 填 NULL
10. THE App8_Consolidation SHALL 将结果同时写入 ai_cachecache_type=app8_clue_consolidatedtarget_id=member_id
### 需求 11事件调度与调用链编排
**用户故事:** 作为系统,我需要根据业务事件(消费、备注、任务分配)自动编排 AI 应用调用链,确保执行顺序和数据依赖正确。
#### 验收标准
1. WHEN 消费事件结账单发生时THE Event_Dispatcher SHALL 按严格串行顺序执行:应用 3 → 应用 8 → 应用 7
2. WHEN 消费事件中该结算单有助教参与时THE Event_Dispatcher SHALL 在应用 8 完成后额外执行:应用 4 → 应用 5
3. WHEN 备注提交事件发生时THE Event_Dispatcher SHALL 按串行顺序执行:应用 6 → 应用 8
4. WHEN 任务分配事件(优先召回/高优先召回发生时THE Event_Dispatcher SHALL 执行:应用 4 → 应用 5直接读取应用 8 已有缓存)
5. THE Event_Dispatcher SHALL 确保消费事件链中应用 7 等待应用 8 完成后再启动,保证读到本次消费触发的最新线索
6. THE Event_Dispatcher SHALL 确保消费事件链中应用 4 等待应用 3 → 应用 8 完成后再执行,确保读到本次消费的最新线索
7. IF 调用链中某个应用调用失败THEN THE Event_Dispatcher SHALL 记录错误日志,后续应用使用已有缓存继续执行(不阻塞整条链)
### 需求 12AI 缓存读写 API
**用户故事:** 作为前端,我需要通过 API 读取各 AI 应用的缓存结果,以便在对应页面展示 AI 分析内容。
#### 验收标准
1. THE AI_Cache_Service SHALL 提供按 cache_type + site_id + target_id 查询最新缓存结果的 API
2. THE AI_Cache_Service SHALL 支持以下前端消费场景:应用 2 结果展示在 board-finance 财务看板、应用 4/5 结果展示在 task-detail 任务详情页、应用 6 的 score 以打星方式展示在备注卡片、应用 7 结果展示在 customer-detail 客户详情页、应用 8 结果通过 member_retention_clue 表展示
3. THE AI_Cache_Service SHALL 对每个 (cache_type, site_id, target_id) 组合保留最近 500 条记录,超过时删除最旧的
4. WHEN 写入新缓存记录后THE AI_Cache_Service SHALL 异步检查并清理超限记录
5. THE AI_Cache_Service SHALL 对所有查询和写入操作执行 site_id 隔离
### 需求 13AI 调用记录持久化
**用户故事:** 作为系统,所有 AI 对话(含用户主动和系统自动调用)都要持久化记录,以便追溯和统计。
#### 验收标准
1. THE AI_Integration_System SHALL 对所有 8 个应用的每次 AI 调用创建 ai_conversations 记录,包含 conversation_id、app_id、user_id系统调用时为系统标识、nickname、site_id
2. THE AI_Integration_System SHALL 对每次 AI 调用的输入和输出分别写入 ai_messages包含 roleuser/assistant/system、content、tokens_used、created_at
3. THE AI_Integration_System SHALL 在 ai_conversations 中记录 source_page 和 source_contextJSON标识调用来源
### 需求 14百炼技术方案确认
**用户故事:** 作为开发者,我需要确认百炼 API 的流式返回技术方案和 JSON 输出最佳实践,以便正确实现封装层。
#### 验收标准
1. THE AI_Integration_System SHALL 查阅百炼官方文档确认流式返回的技术方案SSE vs WebSocket
2. THE AI_Integration_System SHALL 确认百炼 API 的 JSON 输出模式配置方式response_format 参数或 System Prompt 约束)
3. THE AI_Integration_System SHALL 基于确认结果输出技术方案文档,作为 Bailian_Client 实现的依据
---
## 范围说明
### P5-A 阶段(本 spec 覆盖)
| 任务 | 对应需求 | 说明 |
|------|---------|------|
| T1 | 需求 1 | 建表ai_conversations + ai_messages + ai_cache |
| T2 | 需求 2 | 百炼 API 统一封装层 |
| T3 | 需求 3 | 应用 1 通用对话 SSE |
| T5 | 需求 4 | 应用 2 财务洞察Prompt 已确定) |
| T6-骨架 | 需求 5 | 应用 3 触发机制 + 调用框架 |
| T7-骨架 | 需求 6 | 应用 4 触发机制 + 调用框架 |
| T8-骨架 | 需求 7 | 应用 5 联动框架 |
| T9-骨架 | 需求 8 | 应用 6 触发机制 + 调用框架 |
| T10-骨架 | 需求 9 | 应用 7 触发机制 + 调用框架 |
| T11 | 需求 10 | 应用 8 维客线索整理Prompt 已确定) |
| T12 | 需求 12 | AI 缓存读写 API |
| T13 | 需求 14 | 百炼技术方案确认 |
| — | 需求 11 | 事件调度与调用链编排(贯穿 T6-T11 |
| — | 需求 13 | AI 调用记录持久化(贯穿所有应用) |
### P5-B 阶段(不在本 spec 范围)
以下任务将分散到对应页面的开发 spec 中完成:
- T4页面内容文本化工具 → 随 P6-P9 各页面逐步实现
- T6-完整:应用 3 Prompt JSON 细化 → P9-T1customer-detail API
- T7-完整:应用 4 Prompt JSON 细化 → P6-T4task-detail API
- T8-完整:应用 5 Prompt JSON 细化 → P6-T4task-detail API
- T9-完整:应用 6 Prompt JSON 细化 → P9-T1customer-detail API
- T10-完整:应用 7 Prompt JSON 细化 → P9-T1customer-detail API

View File

@@ -0,0 +1,322 @@
# 实现计划P5 AI 集成层miniapp-ai-integration
## 概述
基于 P5-A 阶段设计,在 `apps/backend/app/ai/` 新建 AI 模块,实现百炼 API 封装、SSE 对话、事件调度、缓存服务、8 个 AI 应用(其中 App2/App8 含完整 PromptApp3/4/5/6/7 仅骨架)。每个任务增量构建,最终通过路由和事件调度器串联所有组件。
## 任务
- [ ] 1. 数据库表结构与基础模块搭建
- [x] 1.1 创建 DDL 迁移脚本,在 `biz` schema 下建表 `ai_conversations``ai_messages``ai_cache`
- 按设计文档中的 DDL 创建三张表包含所有字段、CHECK 约束、索引
- DDL 文件放置于 `db/zqyy_app/migrations/` 目录,日期前缀命名
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 1.2 创建 AI 模块目录结构和 Pydantic Schema
- 创建 `apps/backend/app/ai/` 目录及 `__init__.py`
- 创建 `apps/backend/app/ai/apps/` 子目录及 `__init__.py`
- 创建 `apps/backend/app/ai/prompts/` 子目录及 `__init__.py`
-`apps/backend/app/ai/schemas.py` 中定义所有 Pydantic 模型:
- `ChatStreamRequest`message, source_page, page_context, screen_content
- `SSEEvent`type, content, conversation_id, tokens_used, message
- `CacheTypeEnum`7 个枚举值)
- `ClueItem`category, summary, detail, emoji
- `ConsolidatedClueItem`(含 providers
- `App2InsightItem``App4Result``App5TacticsItem``App6Result``App7Result`
- `App2Result``App3Result``App8Result`
- _需求: 4.4, 5.2, 5.3, 6.3, 7.3, 8.2, 8.3, 9.2, 10.4, 10.5_
- [x] 1.3 编写属性测试AI 应用输出 JSON 结构验证
- **Property 8: AI 应用输出 JSON 结构验证**
- 使用 hypothesis 生成随机 JSON验证各应用 Pydantic 模型的解析和校验
- 验证 App3 category ∈ {客户基础, 消费习惯, 玩法偏好}App6/8 category ∈ 6 个枚举值
- 测试文件:`tests/test_p5_ai_integration_properties.py`
- **验证: 需求 4.4, 5.2, 5.3, 5.4, 6.3, 7.3, 8.2, 8.3, 8.4, 9.2, 10.4, 10.5**
- [ ] 2. 百炼 API 统一封装层BailianClient
- [x] 2.1 实现 BailianClient 核心逻辑
- 文件:`apps/backend/app/ai/bailian_client.py`
- 使用 `openai` Python SDK`base_url` 指向百炼端点
- 实现 `chat_stream`流式AsyncGenerator[str, None]
- 实现 `chat_json`(非流式,返回 tuple[dict, int]
- 实现 `_inject_current_time`(首条消息注入 current_time
- 实现 `_call_with_retry`(指数退避,最多 3 次1s→2s→4s
- 定义异常类:`BailianApiError``BailianJsonParseError``BailianAuthError`
- 环境变量:`BAILIAN_API_KEY``BAILIAN_BASE_URL``BAILIAN_MODEL`
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 2.2 编写属性测试:双模式调用一致性
- **Property 1: BailianClient 双模式调用一致性**
- Mock 百炼 API验证 `chat_stream` 返回非空 chunk 序列,`chat_json` 返回可解析 JSON + 正整数 tokens_used
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.1, 2.3, 2.4**
- [x] 2.3 编写属性测试:指数退避重试策略
- **Property 2: 指数退避重试策略**
- Mock 可控失败次数的 API验证重试间隔为 base_interval × 2^(n-1),超过 max_retries 抛出 BailianApiError
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.2**
- [x] 2.4 编写属性测试JSON 解析失败错误处理
- **Property 3: JSON 解析失败错误处理**
- Mock 返回非法 JSON 字符串,验证 `chat_json` 抛出 BailianJsonParseError
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.5**
- [x] 2.5 编写属性测试current_time 注入不变量
- **Property 4: current_time 注入不变量**
- 纯函数测试,随机消息列表,验证首条消息注入 current_timeISO 格式精确到秒),其余消息不变
- 测试文件:`apps/backend/tests/test_ai_bailian.py`
- **验证: 需求 2.6**
- [x] 3. 对话记录持久化服务ConversationService
- [x] 3.1 实现 ConversationService
- 文件:`apps/backend/app/ai/conversation_service.py`
- `create_conversation`:创建 ai_conversations 记录,系统调用时 user_id='system'
- `add_message`:写入 ai_messages 记录role, content, tokens_used
- `get_conversations`:按 user_id + site_id 查询created_at DESC分页page_size=20
- `get_messages`:按 conversation_id 查询所有消息
- _需求: 3.2, 3.4, 3.5, 3.7, 13.1, 13.2, 13.3_
- [x] 3.2 编写属性测试AI 调用记录持久化 round-trip
- **Property 5: AI 调用记录持久化 round-trip**
- 使用 test_zqyy_app 数据库,随机 app_id 和消息内容,验证写入后查询一致
- 测试文件:`apps/backend/tests/test_ai_conversation.py`
- **验证: 需求 3.2, 3.4, 3.5, 13.1, 13.2, 13.3**
- [x] 3.3 编写属性测试:历史对话列表排序与分页
- **Property 6: 历史对话列表排序与分页**
- 使用 test_zqyy_app 数据库,随机时间戳创建对话,验证返回严格降序且每页 ≤ page_size
- 测试文件:`apps/backend/tests/test_ai_conversation.py`
- **验证: 需求 3.7**
- [ ] 4. AI 缓存读写服务AICacheService
- [x] 4.1 实现 AICacheService
- 文件:`apps/backend/app/ai/cache_service.py`
- `get_latest`:按 (cache_type, site_id, target_id) 查询最新记录
- `get_history`查询历史记录created_at DESC默认 limit=2用于 Prompt reference
- `write_cache`:写入缓存记录,写入后异步清理超限记录
- `_cleanup_excess`:保留最近 500 条,删除最旧的
- _需求: 12.1, 12.2, 12.3, 12.4, 12.5_
- [~] 4.2 编写属性测试:缓存写入 round-trip
- **Property 7: 缓存写入 round-trip**
- 使用 test_zqyy_app 数据库,随机 cache_type、target_id、result_json验证写入后查询一致
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 4.7, 5.6, 6.6, 7.5, 8.6, 9.5, 10.10**
- [~] 4.3 编写属性测试:缓存查询 site_id 隔离
- **Property 13: 缓存查询 site_id 隔离**
- 使用 test_zqyy_app 数据库,写入 site_id=A 的记录,以 site_id=B 查询应返回空
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 12.1, 12.5**
- [~] 4.4 编写属性测试:缓存保留上限
- **Property 14: 缓存保留上限**
- 使用 test_zqyy_app 数据库,批量写入 >500 条记录,验证清理后 ≤ 500
- 测试文件:`apps/backend/tests/test_ai_cache.py`
- **验证: 需求 12.3**
- [ ] 5. 检查点 - 基础服务验证
- 确保所有测试通过ask the user if questions arise.
- 验证 BailianClient、ConversationService、AICacheService 三个核心服务可独立工作
- [ ] 6. 应用 1 通用对话 SSE 端点
- [~] 6.1 实现 App1 Chat 核心逻辑
- 文件:`apps/backend/app/ai/apps/app1_chat.py`
- 每次进入 chat 页面新建 ai_conversations 记录(不复用)
- 首条消息注入页面上下文source_page、page_context、screen_content
- 用户消息立即写入 ai_messagesrole=user
- 流式返回完成后写入完整 assistant 回复(含 tokens_used
- 通过 `biz_params.user_prompt_params` 传入 User_ID、Role、Nickname
- 上下文注入框架留接口(页面文本化工具 P5-B 实现)
- _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.8_
- [~] 6.2 实现 SSE 路由端点
- 文件:`apps/backend/app/routers/xcx_ai_chat.py`
- `POST /api/ai/chat/stream`SSE 协议推送Content-Type: text/event-stream
- SSE 事件格式chunk / done / error
- `GET /api/ai/conversations`:历史对话列表(分页,每页 20 条)
- `GET /api/ai/conversations/{conversation_id}/messages`:对话消息列表
- JWT 认证,从 token 提取 user_id、site_id、nickname、role
- 注册路由到 FastAPI app
- _需求: 3.1, 3.7_
- [~] 6.3 编写单元测试SSE 端点
- 验证 SSE Content-Type 和事件格式chunk/done/error
- 验证未认证返回 401、空消息返回 422
- 测试文件:`apps/backend/tests/test_ai_chat.py`
- _需求: 3.1_
- [ ] 7. 应用 2 财务洞察(完整 Prompt
- [~] 7.1 实现 App2 Finance Prompt 模板
- 文件:`apps/backend/app/ai/prompts/app2_finance_prompt.py`
- 完整 Prompt 包含当期和上期收入结构table_fee=table_charge_money、assistant_pd=assistant_pd_money、assistant_cx=assistant_cx_money、goods=goods_money、recharge=充值 pay_amount settle_type=5
- 包含储值资产、费用汇总、平台结算数据
- 使用 items_sum 口径,禁止 consume_money
- _需求: 4.5, 4.6_
- [~] 7.2 实现 App2 Finance 核心逻辑
- 文件:`apps/backend/app/ai/apps/app2_finance.py`
- 8 个时间维度独立调用this_month, last_month, this_week, last_week, last_3_months, this_quarter, last_quarter, last_6_months
- 营业日分界点 08:00`BUSINESS_DAY_START_HOUR` 环境变量)
- 每次调用结果写入 ai_cachecache_type=app2_financetarget_id=时间维度编码)
- 每次调用创建 ai_conversations + ai_messages 记录
- 返回结构化 JSONinsights 数组seq + title + body
- _需求: 4.1, 4.2, 4.3, 4.4, 4.7_
- [~] 7.3 编写单元测试App2 时间维度计算
- 验证 8 个时间维度编码的计算逻辑(营业日分界点 08:00
- 验证 Prompt 使用 items_sum 口径字段映射
- 测试文件:`apps/backend/tests/test_ai_app2.py`
- _需求: 4.3, 4.6_
- [ ] 8. 应用 3/4/5/6/7 骨架实现
- [~] 8.1 实现 App3 Clue 骨架
- 文件:`apps/backend/app/ai/apps/app3_clue.py`
- `run` 函数:构建 Prompt → 调用百炼 → 写入 conversation + cache
- `build_prompt`:留接口,返回占位 Prompt标注待细化字段consumption_records 等待 P9-T1
- 线索 category 限定 3 个枚举值providers 标记为"系统"
- 使用 items_sum 口径
- Prompt reference 包含 App6 线索 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app3_cluetarget_id=member_id
- _需求: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8_
- [~] 8.2 实现 App4 Analysis 骨架
- 文件:`apps/backend/app/ai/apps/app4_analysis.py`
- `build_prompt`留接口service_history、assistant_info 待 P6-T4
- Prompt reference 包含 App8 最新 + 最近 2 套历史(附 generated_at
- 缓存不存在时 reference 传空对象,标注"暂无历史线索"
- 结果写入 ai_cachecache_type=app4_analysistarget_id=`{assistant_id}_{member_id}`
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7_
- [~] 8.3 实现 App5 Tactics 骨架
- 文件:`apps/backend/app/ai/apps/app5_tactics.py`
- 接收 App4 完整返回结果作为 Prompt 中的 task_suggestion 字段
- `build_prompt`留接口service_history、assistant_info 随 App4 同步在 P6-T4
- Prompt reference 包含最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app5_tacticstarget_id=`{assistant_id}_{member_id}`
- _需求: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
- [~] 8.4 实现 App6 Note 骨架
- 文件:`apps/backend/app/ai/apps/app6_note.py`
- `build_prompt`留接口consumption_data 待 P9-T1
- 返回 score1-10+ clues 数组category 限定 6 个枚举值
- 线索提供者标记为当前备注提供人
- 评分规则6 分为标准分
- Prompt reference 包含 App3 线索 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app6_note_analysistarget_id=member_idscore 存入 ai_cache.score
- _需求: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8_
- [~] 8.5 实现 App7 Customer 骨架
- 文件:`apps/backend/app/ai/apps/app7_customer.py`
- `build_prompt`留接口objective_data 待 P9-T1
- 使用 items_sum 口径
- 对主观信息标注【来源XXX请甄别信息真实性】
- Prompt reference 包含最新 + 最近 2 套 App8 历史(附 generated_at
- 结果写入 ai_cachecache_type=app7_customer_analysistarget_id=member_id
- _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
- [~] 8.6 编写属性测试Prompt reference 历史注入
- **Property 9: Prompt reference 历史注入**
- Mock 缓存数据,验证各应用 build_prompt 的 reference 字段包含正确的缓存结果和 generated_at 时间戳
- 缓存不存在时 reference 为空对象
- 测试文件:`apps/backend/tests/test_ai_apps_prompt.py`
- **验证: 需求 5.8, 6.4, 6.5, 7.2, 7.4, 8.8, 9.7**
- [ ] 9. 应用 8 维客线索整理(完整 Prompt+ ClueWriter
- [~] 9.1 实现 App8 Consolidation Prompt 模板
- 文件:`apps/backend/app/ai/prompts/app8_consolidation_prompt.py`
- 完整 Prompt接收 App3 和 App6 全部线索(附 generated_at整合去重
- 分类标签限定 6 个枚举值(与 member_retention_clue CHECK 约束一致)
- 合并相似线索(多提供者逗号分隔),其余原文返回,最小改动原则
- _需求: 10.3, 10.4, 10.5, 10.6_
- [~] 9.2 实现 ClueWriter 全量替换逻辑
- 集成在 `apps/backend/app/ai/apps/app8_consolidation.py`
- DELETE source IN ('ai_consumption', 'ai_note') → INSERT 新线索(事务)
- 字段映射emoji+summary 拼接、providers→recorded_by_name、source 判断逻辑
- recorded_by_assistant_id 填 NULL
- 人工线索source='manual')不受影响
- _需求: 10.7, 10.8, 10.9_
- [~] 9.3 实现 App8 Consolidation 核心逻辑
- 文件:`apps/backend/app/ai/apps/app8_consolidation.py`
- `run` 函数:构建 Prompt → 调用百炼 → 写入 conversation + cache + member_retention_clue
- 结果同时写入 ai_cachecache_type=app8_clue_consolidatedtarget_id=member_id
- _需求: 10.1, 10.2, 10.10_
- [~] 9.4 编写属性测试ClueWriter 全量替换不变量
- **Property 12: ClueWriter 全量替换不变量**
- 使用 test_zqyy_app 数据库,随机线索列表 + 预置人工线索
- 验证AI 线索数量 = new_clues 数量、人工线索不变、recorded_by_assistant_id=NULL、summary=emoji+空格+原始 summary
- 测试文件:`apps/backend/tests/test_ai_clue_writer.py`
- **验证: 需求 10.7, 10.8, 10.9**
- [ ] 10. 检查点 - 应用层验证
- 确保所有测试通过ask the user if questions arise.
- 验证 8 个应用的 run 函数可独立调用Mock 百炼 API
- [ ] 11. 事件调度与调用链编排AIDispatcher
- [~] 11.1 实现 AIDispatcher 核心逻辑
- 文件:`apps/backend/app/ai/dispatcher.py`
- `handle_consumption_event`App3 → App8 → App7+ App4 → App5 如有助教)
- `handle_note_event`App6 → App8
- `handle_task_assign_event`App4 → App5读已有 App8 缓存)
- `_run_chain`:串行执行调用链,某步失败记录日志后继续
- 容错:失败应用记录错误日志 + 写入失败 conversation后续应用使用已有缓存
- 整条链后台异步执行,不阻塞业务请求
- _需求: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
- [~] 11.2 集成事件触发点
-`trigger_scheduler.fire_event()` 中注册 AI 事件处理器
- 消费事件consumption_settled`ai_dispatcher.handle_consumption_event`
- 备注事件note_created`ai_dispatcher.handle_note_event`
- 任务分配事件task_assigned`ai_dispatcher.handle_task_assign_event`
- _需求: 5.1, 6.1, 6.2, 7.1, 8.1, 9.1, 11.1, 11.2, 11.3, 11.4_
- [~] 11.3 编写属性测试:事件调用链顺序正确性
- **Property 10: 事件调用链顺序正确性**
- Mock 所有应用,记录调用序列,验证四种事件链的严格顺序
- 测试文件:`apps/backend/tests/test_ai_dispatcher.py`
- **验证: 需求 11.1, 11.2, 11.3, 11.4, 11.5, 11.6**
- [~] 11.4 编写属性测试:调用链容错不变量
- **Property 11: 调用链容错不变量**
- Mock 随机应用失败,验证后续应用继续执行且失败应用有错误日志
- 测试文件:`apps/backend/tests/test_ai_dispatcher.py`
- **验证: 需求 11.7**
- [ ] 12. 缓存查询路由与环境配置
- [~] 12.1 实现缓存查询路由
- 文件:`apps/backend/app/routers/xcx_ai_cache.py`
- `GET /api/ai/cache/{cache_type}?target_id=xxx`:查询最新缓存
- JWT 认证site_id 从 token 提取强制过滤
- 注册路由到 FastAPI app
- _需求: 12.1, 12.2, 12.5_
- [~] 12.2 新增环境变量配置
-`.env.template` 中添加 `BAILIAN_API_KEY``BAILIAN_BASE_URL``BAILIAN_MODEL``BUSINESS_DAY_START_HOUR`
- 在后端配置加载逻辑中读取这些变量,缺失时报错
- _需求: 2.1, 14.1, 14.2_
- [ ] 13. 百炼技术方案确认文档
- [ ] 13.1 输出百炼技术方案确认文档
- 文件:`docs/reports/bailian-technical-solution.md`
- 确认流式返回方案OpenAI 兼容 SSE
- 确认 JSON 输出模式response_format + System Prompt 约束)
- 确认 SDK 选择openai Python SDK + base_url 指向百炼)
- 作为 BailianClient 实现的依据
- _需求: 14.1, 14.2, 14.3_
- [ ] 14. 最终检查点 - 全量验证
- 确保所有测试通过ask the user if questions arise.
- 验证所有路由注册正确、事件触发点集成完毕、环境变量配置完整
## 备注
- 标记 `*` 的任务为可选,可跳过以加速 MVP 交付
- 每个任务引用具体需求编号以确保可追溯
- 属性测试验证设计文档中定义的 14 个正确性属性
- 使用 test_zqyy_app 测试库执行数据库相关测试,禁止连接正式库
- App3/4/5/6/7 的 Prompt 细化将在 P5-B 阶段P6/P9 对应任务)中完成