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:
669
docs/specs/ai-prompt-refinement/design.md
Normal file
669
docs/specs/ai-prompt-refinement/design.md
Normal file
@@ -0,0 +1,669 @@
|
||||
# 设计文档 — NS2:AI Prompt 细化
|
||||
|
||||
## 概述
|
||||
|
||||
本设计将 P5-A 阶段交付的 6 个 AI 应用(应用 1/3/4/5/6/7)的 `build_prompt()` 占位骨架升级为完整实现。核心变更:
|
||||
|
||||
1. 新建共享数据获取层 `apps/backend/app/ai/data_fetchers/`,封装 FDW 查询逻辑
|
||||
2. 完善应用 3/4/5/6/7 的 `build_prompt()` 函数,从 TODO 占位升级为真实数据拼接
|
||||
3. 实现应用 1 的页面上下文文本化,根据 `contextType` 自动获取并格式化页面数据
|
||||
|
||||
设计原则:
|
||||
- 数据获取与 Prompt 拼接分离,data_fetchers 可被多个应用复用
|
||||
- 所有 FDW 查询遵循 RLS 隔离(`SET LOCAL app.current_site_id`)
|
||||
- 金额口径统一使用 `items_sum`,禁止 `consume_money`
|
||||
- 部分数据获取失败不阻断 Prompt 生成(错误降级)
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体分层
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ AI 应用层(apps/app*.py) │
|
||||
│ build_prompt() / run() │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 数据获取层(data_fetchers/) 🆕 新建 │
|
||||
│ member_data / assistant_data / page_context │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ 基础设施层(已有) │
|
||||
│ database.py / cache_service.py / bailian_client │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 事件触发
|
||||
E1[消费结算事件]
|
||||
E2[备注提交事件]
|
||||
E3[任务分配事件]
|
||||
E4[用户进入 chat]
|
||||
end
|
||||
|
||||
subgraph data_fetchers
|
||||
MF[member_data.py]
|
||||
AF[assistant_data.py]
|
||||
PC[page_context.py]
|
||||
end
|
||||
|
||||
subgraph FDW 视图
|
||||
DWD[v_dwd_settlement_head<br/>v_dwd_table_fee_log<br/>v_dwd_store_goods_sale<br/>v_dwd_assistant_service_log]
|
||||
DWS[v_dws_member_consumption_summary<br/>v_dws_member_visit_detail<br/>v_dws_assistant_salary_calc]
|
||||
DIM[v_dim_member<br/>v_dim_assistant<br/>v_dim_member_card_account]
|
||||
end
|
||||
|
||||
subgraph 业务库
|
||||
BIZ[biz.notes<br/>biz.coach_tasks<br/>biz.ai_cache]
|
||||
end
|
||||
|
||||
E1 --> App3 --> MF
|
||||
E1 --> App4 --> AF
|
||||
E1 --> App4 --> MF
|
||||
E2 --> App6 --> MF
|
||||
E3 --> App4
|
||||
E4 --> App1 --> PC
|
||||
|
||||
MF --> DWD
|
||||
MF --> DWS
|
||||
MF --> DIM
|
||||
AF --> DWD
|
||||
AF --> DWS
|
||||
AF --> DIM
|
||||
PC --> BIZ
|
||||
PC --> MF
|
||||
PC --> AF
|
||||
```
|
||||
|
||||
### 调用链编排(已有 dispatcher.py)
|
||||
|
||||
消费事件链:`App3 → App8 → App7 + App4 → App5`
|
||||
备注事件链:`App6 → App8`
|
||||
任务分配链:`App4 → App5`
|
||||
对话流:`App1.chat_stream()` → `build_page_text()` → SSE 流式
|
||||
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### 1. 数据获取层(`apps/backend/app/ai/data_fetchers/`)
|
||||
|
||||
#### 1.1 `member_data.py` — 客户消费数据获取
|
||||
|
||||
应用 3/6/7 共用。从 FDW 视图获取客户近 N 个月消费数据。
|
||||
|
||||
```python
|
||||
async def fetch_member_consumption_data(
|
||||
site_id: int, member_id: int, months: int = 3
|
||||
) -> dict:
|
||||
"""获取客户近 N 个月消费数据。
|
||||
|
||||
返回:
|
||||
{
|
||||
"consumption_records": list[dict], # 消费记录(最多 100 条,settle_date DESC)
|
||||
"member_cards": list[dict], # 会员卡明细
|
||||
"card_balance_total": Decimal, # 储值卡余额合计
|
||||
"stored_value_balance_total": Decimal, # 储值余额合计
|
||||
"expected_visit_date": str | None, # 预计到店日期
|
||||
"days_since_last_visit": int | None, # 距上次到店天数
|
||||
"member_nickname": str, # 会员昵称
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
数据源与查询策略:
|
||||
|
||||
| 数据 | FDW 视图 | 连接方式 | 筛选条件 |
|
||||
|------|---------|---------|---------|
|
||||
| 台桌结账 | `v_dwd_settlement_head` + `v_dwd_table_fee_log` | `get_etl_readonly_connection` | `settle_type IN (1,3)`, `settle_date >= NOW() - N months` |
|
||||
| 商城订单 | `v_dwd_store_goods_sale` | 同上 | `sale_date >= NOW() - N months` |
|
||||
| 会员卡 | `v_dim_member_card_account` | 同上 | `member_id` 匹配 |
|
||||
| 消费汇总 | `v_dws_member_consumption_summary` | 同上 | `member_id` 匹配 |
|
||||
| 到店明细 | `v_dws_member_visit_detail` | 同上 | `member_id` 匹配 |
|
||||
| 会员信息 | `v_dim_member` | 同上 | `member_id` 匹配, `scd2_is_current=1` |
|
||||
|
||||
每条消费记录字段:
|
||||
```python
|
||||
{
|
||||
"settle_date": "2026-03-05",
|
||||
"settle_type": 1,
|
||||
"items_sum": 280.00, # 强制口径
|
||||
"table_charge_money": 180.00,
|
||||
"assistant_pd_money": 80.00, # 陪打费
|
||||
"assistant_cx_money": 0, # 超休费
|
||||
"goods_money": 20.00,
|
||||
"room_name": "VIP-3",
|
||||
"duration_minutes": 120,
|
||||
"assistant_names": ["张助教"],
|
||||
}
|
||||
```
|
||||
|
||||
实现要点:
|
||||
- 使用 `_fdw_context` 模式(参考 `fdw_queries.py`):`get_etl_readonly_connection(site_id)` + `SET LOCAL app.current_site_id`
|
||||
- 多个 FDW 查询可在同一连接上串行执行(共享 RLS 设置)
|
||||
- 消费记录限制 100 条,超出时在返回 dict 中附加 `"truncated": True, "total_count": N`
|
||||
- 5 秒查询超时,超时抛出 `TimeoutError`
|
||||
- 会员昵称通过 `member_id JOIN v_dim_member (scd2_is_current=1)` 获取
|
||||
|
||||
#### 1.2 `assistant_data.py` — 助教数据获取
|
||||
|
||||
应用 4/5 共用。
|
||||
|
||||
```python
|
||||
async def fetch_assistant_info(
|
||||
site_id: int, assistant_id: int
|
||||
) -> dict:
|
||||
"""获取助教基本信息。
|
||||
|
||||
返回:
|
||||
{
|
||||
"nickname": str,
|
||||
"level": str,
|
||||
"hire_date": str,
|
||||
"tenure_months": int,
|
||||
"monthly_customers": int,
|
||||
"performance_tier": str,
|
||||
}
|
||||
"""
|
||||
|
||||
async def fetch_service_history(
|
||||
site_id: int, assistant_id: int, member_id: int, months: int = 3
|
||||
) -> list[dict]:
|
||||
"""获取助教服务该客户的历史记录。
|
||||
|
||||
返回:
|
||||
[
|
||||
{
|
||||
"service_date": str,
|
||||
"duration_minutes": int,
|
||||
"items_sum": Decimal,
|
||||
"room_name": str,
|
||||
"is_pd": bool, # 是否陪打
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
```
|
||||
|
||||
数据源:
|
||||
|
||||
| 数据 | FDW 视图 | 说明 |
|
||||
|------|---------|------|
|
||||
| 助教基本信息 | `v_dim_assistant` | 花名、级别、入职日期 |
|
||||
| 绩效数据 | `v_dws_assistant_salary_calc` | 本月客户数、绩效档位 |
|
||||
| 服务记录 | `v_dwd_assistant_service_log` | 按 assistant_id + member_id 筛选 |
|
||||
| 关系指数 | `v_dws_member_assistant_relation_index` | 助教-客户关系指数 |
|
||||
| 亲密度 | `v_dws_member_assistant_intimacy` | 亲密度数据 |
|
||||
|
||||
实现要点:
|
||||
- 使用 `is_trash` 字段排除废单(`WHERE is_trash = false`),禁止使用已废弃的 `dwd_assistant_trash_event` 表
|
||||
- 服务记录按 `service_date DESC` 排序
|
||||
- `tenure_months` 从 `hire_date` 到当前日期计算
|
||||
|
||||
#### 1.3 `page_context.py` — 页面上下文文本化(应用 1 专用)
|
||||
|
||||
```python
|
||||
async def build_page_text(
|
||||
source_page: str,
|
||||
context_id: int | str | None,
|
||||
site_id: int,
|
||||
filters: dict | None = None,
|
||||
) -> str:
|
||||
"""将页面数据转换为 AI 可读的结构化中文文本。
|
||||
|
||||
Args:
|
||||
source_page: 页面类型(contextType)
|
||||
context_id: 实体 ID(contextId)
|
||||
site_id: 门店 ID
|
||||
filters: 看板类页面的筛选参数
|
||||
|
||||
Returns:
|
||||
结构化中文文本(≤ 2000 字符)
|
||||
"""
|
||||
```
|
||||
|
||||
支持的 10 种页面类型:
|
||||
|
||||
| source_page | context_id | filters | 数据获取 |
|
||||
|-------------|-----------|---------|---------|
|
||||
| `task-detail` | taskId | — | `biz.coach_tasks` + 会员信息 + 备注 + `ai_cache` |
|
||||
| `customer-detail` | memberId | — | 会员信息 + 消费记录 + 维客线索 |
|
||||
| `coach-detail` | assistantId | — | 助教信息 + 任务统计 + 备注 |
|
||||
| `board-finance` | — | `timeDimension`, `areaFilter` | 财务 DWS 汇总 |
|
||||
| `board-customer` | — | `dimension`, `typeFilter` | 客户排名 top 列表 |
|
||||
| `board-coach` | — | `dimension`, `projectFilter`, `timeDimension` | 助教排名 |
|
||||
| `performance` | — | `timeDimension` | `v_dws_assistant_salary_calc` |
|
||||
| `my-profile` | — | — | 用户信息 + 助教绑定 |
|
||||
| `task-list` | taskId | — | 任务摘要 + 客户-助教关系 |
|
||||
| `customer-service-records` | memberId | — | 服务记录列表 |
|
||||
|
||||
实现要点:
|
||||
- 每个页面类型对应一个内部函数(如 `_text_task_detail()`、`_text_customer_detail()`)
|
||||
- 输出为结构化中文描述(分段标题 + 缩进),非 JSON
|
||||
- 输出限制 2000 字符,超出截断并标注
|
||||
- 看板类页面未传筛选参数时使用默认值(`board-finance` 默认"本月")
|
||||
- 数据获取失败返回 `"页面上下文获取失败,请直接描述您的问题"`
|
||||
- 不传入 `member_phone` 等断档敏感字段
|
||||
- 复用 `member_data.py` 和 `assistant_data.py` 的数据获取函数
|
||||
|
||||
### 2. 应用层 Prompt 拼接改造
|
||||
|
||||
#### 2.1 应用 3(`app3_clue.py`)— 客户数据维客线索分析
|
||||
|
||||
改造 `build_prompt()` 为 `async def`,调用 `fetch_member_consumption_data()` 获取真实数据。
|
||||
|
||||
Prompt JSON 结构:
|
||||
```json
|
||||
{
|
||||
"current_time": "2026-03-08 14:30:25",
|
||||
"member_nickname": "客户昵称",
|
||||
"main_data": {
|
||||
"consumption_records": [...],
|
||||
"member_cards": [...],
|
||||
"card_balance_total": 1700.00,
|
||||
"stored_value_balance_total": 1700.00,
|
||||
"expected_visit_date": "2026-03-10",
|
||||
"days_since_last_visit": 15
|
||||
},
|
||||
"reference": {
|
||||
"app6_clues": [...],
|
||||
"app8_history": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
变更点:
|
||||
- `build_prompt()` 签名改为 `async def`,新增 `site_id` 参数用于 FDW 查询
|
||||
- `system_content["data"]` 替换为 `fetch_member_consumption_data()` 返回的真实数据
|
||||
- `run()` 中调用 `build_prompt()` 需 `await`
|
||||
- 空数据时标注"该客户暂无消费记录"
|
||||
|
||||
#### 2.2 应用 4(`app4_analysis.py`)— 关系分析/任务建议
|
||||
|
||||
改造 `build_prompt()` 为 `async def`,调用三个数据获取函数。
|
||||
|
||||
Prompt JSON 结构:
|
||||
```json
|
||||
{
|
||||
"current_time": "2026-03-08 14:30:25",
|
||||
"assistant_info": { "nickname": "...", "level": "...", ... },
|
||||
"service_history": [...],
|
||||
"task_assignment_basis": "优先召回",
|
||||
"customer_data": {
|
||||
"system_data": { /* 同应用 3 的 main_data */ },
|
||||
"notes": [...]
|
||||
},
|
||||
"reference": { "app8_current": {...}, "app8_history": [...] }
|
||||
}
|
||||
```
|
||||
|
||||
变更点:
|
||||
- 调用 `fetch_assistant_info()` + `fetch_service_history()` + `fetch_member_consumption_data()`
|
||||
- 备注从 `biz.notes` 获取,单条截断 500 字符
|
||||
- 使用 `asyncio.gather` 并发获取三类数据
|
||||
|
||||
#### 2.3 应用 5(`app5_tactics.py`)— 话术参考
|
||||
|
||||
复用应用 4 的数据获取逻辑,额外接收 `context["app4_result"]` 作为 `task_suggestion`。
|
||||
|
||||
变更点:
|
||||
- `build_prompt()` 改为 `async def`
|
||||
- 数据获取逻辑与应用 4 一致
|
||||
- `task_suggestion` 从 `context["app4_result"]` 获取,缺失时设为空对象
|
||||
|
||||
#### 2.4 应用 6(`app6_note.py`)— 备注分析
|
||||
|
||||
改造 `build_prompt()` 为 `async def`,调用 `fetch_member_consumption_data()` 获取客户消费数据。
|
||||
|
||||
Prompt JSON 结构:
|
||||
```json
|
||||
{
|
||||
"current_time": "2026-03-08 14:30:25",
|
||||
"current_note": { "content": "...", "recorded_by": "...", "created_at": "..." },
|
||||
"reference": {
|
||||
"member_nickname": "王先生",
|
||||
"consumption_data": { /* 同应用 3 的 main_data */ },
|
||||
"all_notes": [...],
|
||||
"app3_clues": [...],
|
||||
"app8_history": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
变更点:
|
||||
- 调用 `fetch_member_consumption_data()` 获取消费数据
|
||||
- `all_notes` 从 `biz.notes` 获取,单条截断 500 字符,最多 50 条
|
||||
- 空备注时 `all_notes` 设为空数组
|
||||
|
||||
#### 2.5 应用 7(`app7_customer.py`)— 客户分析
|
||||
|
||||
改造 `build_prompt()` 为 `async def`,调用 `fetch_member_consumption_data()` 获取客观数据。
|
||||
|
||||
Prompt JSON 结构:
|
||||
```json
|
||||
{
|
||||
"current_time": "2026-03-08 14:30:25",
|
||||
"member_id": 12345,
|
||||
"member_nickname": "王先生",
|
||||
"objective_data": { /* 同应用 3 的 main_data */ },
|
||||
"subjective_data": {
|
||||
"notes": [{ "recorded_by": "...", "content": "...", "created_at": "..." }]
|
||||
},
|
||||
"reference": { "app8_current": {...}, "app8_history": [...] }
|
||||
}
|
||||
```
|
||||
|
||||
变更点:
|
||||
- 调用 `fetch_member_consumption_data()` 获取客观数据
|
||||
- 备注从 `biz.notes` 获取,标注"【来源:{recorded_by},请甄别信息真实性】"
|
||||
- 空备注时标注"该客户暂无主观备注信息"
|
||||
|
||||
#### 2.6 应用 1(`app1_chat.py`)— 页面上下文集成
|
||||
|
||||
改造 `_build_page_context()` 为 `async def`,调用 `build_page_text()` 获取页面上下文。
|
||||
|
||||
> **P5 对齐说明**:P5 定义首条 Prompt 包含 `page_context` + `screen_content` 两个独立字段。NS2 通过 `build_page_text()` 生成合并文本,等效覆盖两者的信息需求(详见 NS2 PRD 3.7 设计决策)。
|
||||
|
||||
变更点:
|
||||
- `_build_page_context()` 改为 `async def`,内部调用 `page_context.build_page_text()`
|
||||
- `source_page` 参数映射到 `contextType`,`page_context` 中的 `contextId` 传入 `build_page_text()`
|
||||
- 看板类页面从 `page_context` 提取筛选参数传入 `filters`
|
||||
- `contextType` 为空或未识别时跳过页面上下文注入
|
||||
- `biz_params.user_prompt_params` 保持不变
|
||||
- system prompt 总字符数控制在 4000 以内
|
||||
|
||||
### 3. 备注查询辅助函数
|
||||
|
||||
多个应用(4/5/6/7)需要从 `biz.notes` 获取客户备注,抽取为共享函数:
|
||||
|
||||
```python
|
||||
# data_fetchers/member_data.py 或独立 notes 模块
|
||||
async def fetch_member_notes(
|
||||
site_id: int, member_id: int, limit: int = 50
|
||||
) -> list[dict]:
|
||||
"""获取客户的全部备注(按 created_at DESC,最多 limit 条)。
|
||||
|
||||
返回:
|
||||
[{"recorded_by": str, "content": str, "created_at": str}, ...]
|
||||
"""
|
||||
```
|
||||
|
||||
- 使用 `get_connection()` 查询业务库 `biz.notes`
|
||||
- 单条备注内容截断 500 字符,超出附加"…(已截断)"
|
||||
- 按 `created_at DESC` 排序
|
||||
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 无新建数据库表
|
||||
|
||||
本设计不需要新建数据库表。所有数据获取基于已有的 FDW 视图和业务表。
|
||||
|
||||
### 数据获取涉及的表
|
||||
|
||||
| 数据获取函数 | 涉及表 | 连接方式 |
|
||||
|-------------|--------|---------|
|
||||
| `fetch_member_consumption_data` | `v_dwd_settlement_head`, `v_dwd_table_fee_log`, `v_dwd_store_goods_sale`, `v_dim_member_card_account`, `v_dws_member_consumption_summary`, `v_dws_member_visit_detail`, `v_dim_member` | ETL 只读连接 |
|
||||
| `fetch_assistant_info` | `v_dim_assistant`, `v_dws_assistant_salary_calc` | ETL 只读连接 |
|
||||
| `fetch_service_history` | `v_dwd_assistant_service_log`, `v_dws_member_assistant_relation_index`, `v_dws_member_assistant_intimacy` | ETL 只读连接 |
|
||||
| `fetch_member_notes` | `biz.notes` | 业务库连接 |
|
||||
| `build_page_text` | 以上全部 + `biz.coach_tasks`, `biz.ai_cache`, `public.member_retention_clue` | 混合 |
|
||||
|
||||
### Prompt JSON 结构汇总
|
||||
|
||||
各应用 Prompt 的 `system_content` 顶层字段:
|
||||
|
||||
| 应用 | 顶层字段 | 数据来源 |
|
||||
|------|---------|---------|
|
||||
| App3 | `current_time`, `member_nickname`, `main_data`, `reference` | `fetch_member_consumption_data` + `ai_cache` |
|
||||
| App4 | `current_time`, `assistant_info`, `service_history`, `task_assignment_basis`, `customer_data`, `reference` | `fetch_assistant_info` + `fetch_service_history` + `fetch_member_consumption_data` + `biz.notes` + `ai_cache` |
|
||||
| App5 | 同 App4 + `task_suggestion` | 同 App4 + `context["app4_result"]` |
|
||||
| App6 | `current_time`, `current_note`, `reference`(含 `consumption_data`, `all_notes`) | `fetch_member_consumption_data` + `biz.notes` + `ai_cache` |
|
||||
| App7 | `current_time`, `member_id`, `member_nickname`, `objective_data`, `subjective_data`, `reference` | `fetch_member_consumption_data` + `biz.notes` + `ai_cache` |
|
||||
| App1 | `task`, `biz_params`, `page_context` | `build_page_text()` |
|
||||
|
||||
### Token 预算约束
|
||||
|
||||
| 应用 | 限制 | 说明 |
|
||||
|------|------|------|
|
||||
| App 3/4/5/6/7 | Prompt JSON ≤ 8000 字符 | 约 4000 token |
|
||||
| App 1 | system prompt ≤ 4000 字符 | 含 `page_context`(≤ 2000 字符) |
|
||||
|
||||
### 金额口径强制规则
|
||||
|
||||
```
|
||||
items_sum = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money + electricity_money
|
||||
```
|
||||
|
||||
- 禁止使用 `consume_money`
|
||||
- 助教费用必须拆分:`assistant_pd_money`(陪打)+ `assistant_cx_money`(超休)
|
||||
- 会员信息通过 `member_id JOIN v_dim_member (scd2_is_current=1)` 获取,禁止使用结算单冗余字段
|
||||
|
||||
|
||||
## 正确性属性
|
||||
|
||||
*属性(Property)是系统在所有有效执行中都应保持为真的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 数据获取函数返回结构完整性
|
||||
|
||||
*对于任意* 有效的 `site_id` 和 `member_id`,`fetch_member_consumption_data()` 返回的字典必须包含所有必需键(`consumption_records`、`member_cards`、`card_balance_total`、`stored_value_balance_total`、`expected_visit_date`、`days_since_last_visit`、`member_nickname`),且 `consumption_records` 中每条记录包含 `settle_date`、`settle_type`、`items_sum`、`table_charge_money`、`assistant_pd_money`、`assistant_cx_money`、`goods_money`、`room_name`、`duration_minutes`、`assistant_names` 字段。同理,`fetch_assistant_info()` 和 `fetch_service_history()` 返回的数据也必须包含各自的必需键。
|
||||
|
||||
**Validates: Requirements 1.1, 1.3, 1.5, 2.1, 2.2, 2.4**
|
||||
|
||||
### Property 2: 消费记录仅包含正向交易
|
||||
|
||||
*对于任意* `fetch_member_consumption_data()` 返回的消费记录列表,其中每条记录的 `settle_type` 必须属于 `{1, 3}`(正向交易),不得包含退款或其他类型。
|
||||
|
||||
**Validates: Requirements 1.2**
|
||||
|
||||
### Property 3: 金额口径使用 items_sum
|
||||
|
||||
*对于任意* 数据获取函数返回的包含金额的记录,必须包含 `items_sum` 字段且不包含 `consume_money` 字段;助教费用必须拆分为 `assistant_pd_money` 和 `assistant_cx_money` 两个独立字段。
|
||||
|
||||
**Validates: Requirements 1.4, 3.5, 11.3, 11.4**
|
||||
|
||||
### Property 4: 消费记录数量限制与排序
|
||||
|
||||
*对于任意* `fetch_member_consumption_data()` 返回的 `consumption_records`,列表长度必须 ≤ 100,且按 `settle_date` 降序排列(即 `records[i].settle_date >= records[i+1].settle_date`)。
|
||||
|
||||
**Validates: Requirements 1.9, 14.3**
|
||||
|
||||
### Property 5: 废单记录排除
|
||||
|
||||
*对于任意* `fetch_service_history()` 返回的服务记录列表,不得包含 `is_trash=true` 的记录。
|
||||
|
||||
**Validates: Requirements 2.3**
|
||||
|
||||
### Property 6: 备注截断不变量
|
||||
|
||||
*对于任意* 长度的备注内容字符串,经截断处理后长度必须 ≤ 500 字符;若原始内容超过 500 字符,截断后必须以"…(已截断)"结尾。
|
||||
|
||||
**Validates: Requirements 4.4, 6.4**
|
||||
|
||||
### Property 7: 各应用 build_prompt 返回结构完整性
|
||||
|
||||
*对于任意* 有效的 context 输入和数据获取结果:
|
||||
- App3 的 `build_prompt` 返回的 JSON 必须包含 `current_time`、`member_nickname`、`main_data`、`reference` 顶层键
|
||||
- App4 的 `build_prompt` 返回的 JSON 必须包含 `current_time`、`assistant_info`、`service_history`、`customer_data`、`reference` 顶层键
|
||||
- App5 的 `build_prompt` 返回的 JSON 必须包含 App4 的所有键加 `task_suggestion`
|
||||
- App6 的 `build_prompt` 返回的 JSON 必须包含 `current_time`、`current_note`、`reference` 顶层键
|
||||
- App7 的 `build_prompt` 返回的 JSON 必须包含 `current_time`、`member_id`、`member_nickname`、`objective_data`、`subjective_data`、`reference` 顶层键
|
||||
|
||||
**Validates: Requirements 3.1, 3.2, 4.1, 4.2, 5.2, 6.1, 6.2, 7.1, 7.2**
|
||||
|
||||
### Property 8: App5 task_suggestion 传递
|
||||
|
||||
*对于任意* `context["app4_result"]` 值(包括非空字典和空/缺失),App5 的 `build_prompt` 返回的 Prompt JSON 中 `task_suggestion` 字段必须等于 `context["app4_result"]`(非空时)或空对象(缺失时)。
|
||||
|
||||
**Validates: Requirements 5.3, 5.4**
|
||||
|
||||
### Property 9: App7 主观信息来源标注
|
||||
|
||||
*对于任意* 包含备注的 App7 Prompt,每条备注在 Prompt 文本中必须附带"【来源:{recorded_by},请甄别信息真实性】"格式的标注。
|
||||
|
||||
**Validates: Requirements 7.4**
|
||||
|
||||
### Property 10: 页面上下文输出长度约束
|
||||
|
||||
*对于任意* 页面类型和任意数据量,`build_page_text()` 返回的文本长度必须 ≤ 2000 字符。
|
||||
|
||||
**Validates: Requirements 8.8**
|
||||
|
||||
### Property 11: 页面上下文覆盖所有页面类型
|
||||
|
||||
*对于任意* 10 种支持的页面类型(`task-detail`、`customer-detail`、`coach-detail`、`board-finance`、`board-customer`、`board-coach`、`performance`、`my-profile`、`task-list`、`customer-service-records`),`build_page_text()` 必须能处理而不抛出未识别类型异常,且返回非空字符串。
|
||||
|
||||
**Validates: Requirements 8.1, 8.2**
|
||||
|
||||
### Property 12: 页面上下文不泄露敏感字段
|
||||
|
||||
*对于任意* `build_page_text()` 的输出文本,不得包含 `member_phone` 等断档敏感字段的值。
|
||||
|
||||
**Validates: Requirements 8.12**
|
||||
|
||||
### Property 13: biz_params 注入后不变量
|
||||
|
||||
*对于任意* 页面上下文注入操作,App1 的 system prompt 中 `biz_params.user_prompt_params` 必须包含 `User_ID`(字符串)、`Role`、`Nickname` 三个键,且值与输入的用户信息一致。
|
||||
|
||||
**Validates: Requirements 9.6, 13.2**
|
||||
|
||||
### Property 14: 错误降级产生合法 Prompt
|
||||
|
||||
*对于任意* 数据获取函数失败的组合(0 到全部失败),`build_prompt` 仍必须返回可被 `json.loads` 解析的合法 JSON,不包含 Python `None`(JSON `null`)值,且失败部分使用空数组/空对象替代并附带提示文本。
|
||||
|
||||
**Validates: Requirements 12.1, 12.2, 12.4**
|
||||
|
||||
### Property 15: 应用 3-7 Prompt Token 预算
|
||||
|
||||
*对于任意* 有效输入数据,应用 3/4/5/6/7 的 `build_prompt` 返回的消息列表中,system message 的 `content` 字符串长度必须 ≤ 8000 字符。
|
||||
|
||||
**Validates: Requirements 14.1**
|
||||
|
||||
### Property 16: 应用 1 System Prompt Token 预算
|
||||
|
||||
*对于任意* 页面上下文,App1 的 `_build_system_prompt()` 返回的 JSON 序列化后字符串长度必须 ≤ 4000 字符。
|
||||
|
||||
**Validates: Requirements 14.2**
|
||||
|
||||
### Property 17: 备注数量限制
|
||||
|
||||
*对于任意* 数量的客户备注,传入 Prompt 的备注列表长度必须 ≤ 50 条,且按 `created_at` 降序排列。
|
||||
|
||||
**Validates: Requirements 14.4**
|
||||
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 数据获取层错误处理
|
||||
|
||||
| 错误场景 | 处理策略 | 调用方行为 |
|
||||
|---------|---------|-----------|
|
||||
| FDW 连接失败 | 抛出 `ConnectionError`,包含视图名称 | `build_prompt` 捕获,该部分数据设为默认空值 |
|
||||
| FDW 查询超时(>5s) | 抛出 `TimeoutError`,包含视图名称和耗时 | 同上 |
|
||||
| 会员不存在(`v_dim_member` 无匹配) | 返回 `member_nickname=""` | `build_prompt` 使用空昵称 |
|
||||
| 助教不存在(`v_dim_assistant` 无匹配) | 抛出 `ValueError("assistant not found")` | `build_prompt` 捕获,`assistant_info` 设为空对象 |
|
||||
| 业务库连接失败(`biz.notes`) | 抛出 `ConnectionError` | `build_prompt` 捕获,备注设为空数组 |
|
||||
|
||||
### Prompt 拼接层错误处理
|
||||
|
||||
```python
|
||||
# 各应用 build_prompt 中的错误降级模式
|
||||
async def build_prompt(context, cache_svc=None):
|
||||
# 并发获取数据,部分失败不阻断
|
||||
member_data, assistant_data, notes = await asyncio.gather(
|
||||
fetch_member_consumption_data(site_id, member_id),
|
||||
fetch_assistant_info(site_id, assistant_id),
|
||||
fetch_member_notes(site_id, member_id),
|
||||
return_exceptions=True, # 关键:失败返回异常对象而非抛出
|
||||
)
|
||||
|
||||
# 检查每个结果,失败时使用默认值
|
||||
if isinstance(member_data, Exception):
|
||||
logger.warning("客户数据获取失败: %s", member_data)
|
||||
member_data = _default_member_data() # 空数组/零值
|
||||
# Prompt 中标注"该部分数据获取失败"
|
||||
```
|
||||
|
||||
### 页面上下文错误处理
|
||||
|
||||
- `build_page_text()` 内部捕获所有异常
|
||||
- 任何数据获取失败返回 `"页面上下文获取失败,请直接描述您的问题"`
|
||||
- 不阻断 App1 对话流程
|
||||
|
||||
### Prompt JSON 合法性保证
|
||||
|
||||
- 所有 `None` 值替换为空字符串、空数组或空对象
|
||||
- `json.dumps` 前进行最终校验
|
||||
- 使用 `default=str` 处理 `datetime`、`Decimal` 等非标准类型
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 属性测试(Property-Based Testing)
|
||||
|
||||
使用 **Hypothesis** 库(项目已有 `.hypothesis/` 目录)。
|
||||
|
||||
每个属性测试最少运行 100 次迭代。每个测试用注释标注对应的设计属性:
|
||||
|
||||
```python
|
||||
# Feature: ai-prompt-refinement, Property 1: 数据获取函数返回结构完整性
|
||||
@given(st.integers(min_value=1), st.integers(min_value=1))
|
||||
@settings(max_examples=100)
|
||||
def test_member_data_structure_completeness(site_id, member_id):
|
||||
...
|
||||
```
|
||||
|
||||
属性测试覆盖的属性(Property 1-17):
|
||||
|
||||
| 属性 | 测试策略 | 生成器 |
|
||||
|------|---------|--------|
|
||||
| P1: 返回结构完整性 | Mock 数据库返回随机行,验证函数输出包含所有必需键 | `st.fixed_dictionaries` 生成随机数据库行 |
|
||||
| P2: 正向交易过滤 | 生成混合 settle_type 的记录,验证输出仅含 1/3 | `st.sampled_from([1,2,3,4])` |
|
||||
| P3: items_sum 口径 | 验证输出记录包含 items_sum 且不含 consume_money | 复用 P1 生成器 |
|
||||
| P4: 记录数量与排序 | 生成 0-200 条记录,验证输出 ≤100 且降序 | `st.lists(record_strategy, max_size=200)` |
|
||||
| P5: 废单排除 | 生成含 is_trash 标记的记录,验证输出不含废单 | `st.booleans()` 控制 is_trash |
|
||||
| P6: 备注截断 | 生成 0-2000 字符的字符串,验证截断后 ≤500 | `st.text(min_size=0, max_size=2000)` |
|
||||
| P7: Prompt 结构完整性 | Mock 数据获取返回随机数据,验证 Prompt JSON 键 | 组合生成器 |
|
||||
| P8: task_suggestion 传递 | 生成随机 app4_result(含空),验证传递正确 | `st.one_of(st.none(), st.dictionaries(...))` |
|
||||
| P9: 主观信息标注 | 生成随机备注,验证标注格式 | `st.text()` 生成 recorded_by |
|
||||
| P10: 页面上下文长度 | 生成大量数据,验证输出 ≤2000 | 各页面类型的数据生成器 |
|
||||
| P11: 页面类型覆盖 | 枚举 10 种类型,验证不抛异常 | `st.sampled_from(PAGE_TYPES)` |
|
||||
| P12: 敏感字段排除 | 生成含手机号的数据,验证输出不含 | `st.from_regex(r'1[3-9]\d{9}')` |
|
||||
| P13: biz_params 不变量 | 生成随机用户信息,验证注入后保持 | `st.text()` 生成 user_id/role/nickname |
|
||||
| P14: 错误降级 | 随机让 0-N 个数据获取函数失败,验证 Prompt 合法 | `st.booleans()` 控制每个函数是否失败 |
|
||||
| P15: App3-7 Token 预算 | 生成大量数据,验证 Prompt ≤8000 字符 | 大数据量生成器 |
|
||||
| P16: App1 Token 预算 | 生成大量页面上下文,验证 ≤4000 字符 | 大数据量生成器 |
|
||||
| P17: 备注数量限制 | 生成 0-100 条备注,验证输出 ≤50 且降序 | `st.lists(note_strategy, max_size=100)` |
|
||||
|
||||
### 单元测试
|
||||
|
||||
单元测试聚焦于具体示例、边界情况和集成点:
|
||||
|
||||
| 测试类别 | 测试内容 |
|
||||
|---------|---------|
|
||||
| 边界:空数据 | 客户无消费记录、无备注、无服务历史时的 Prompt 输出 |
|
||||
| 边界:新客户 | ai_cache 无历史数据时 reference 为空对象 |
|
||||
| 边界:超时 | FDW 查询超时时的异常类型和消息 |
|
||||
| 边界:未识别 contextType | App1 收到未知 contextType 时跳过页面上下文 |
|
||||
| 边界:app4_result 缺失 | App5 的 task_suggestion 设为空对象 |
|
||||
| 边界:看板无筛选参数 | 使用默认值(本月/消费金额排序) |
|
||||
| 示例:App3 完整流程 | Mock 数据 → build_prompt → 验证 JSON 结构和内容 |
|
||||
| 示例:App1 页面上下文 | task-detail 页面 → build_page_text → 验证输出包含任务信息 |
|
||||
| 示例:RLS 隔离 | 验证 get_etl_readonly_connection 被调用且传入正确 site_id |
|
||||
| 示例:看板筛选参数传递 | board-finance + timeDimension → 验证传入 build_page_text |
|
||||
| 集成:调用链 | dispatcher 触发消费事件 → App3 → App8 → App7 完整链路 |
|
||||
|
||||
### 测试文件组织
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_data_fetchers/
|
||||
│ ├── test_member_data_props.py # P1-P4 属性测试
|
||||
│ ├── test_assistant_data_props.py # P5 属性测试
|
||||
│ ├── test_page_context_props.py # P10-P12 属性测试
|
||||
│ └── test_data_fetchers_unit.py # 边界/示例单元测试
|
||||
├── test_ai_apps/
|
||||
│ ├── test_build_prompt_props.py # P6-P9, P14-P17 属性测试
|
||||
│ ├── test_app1_props.py # P13, P16 属性测试
|
||||
│ └── test_ai_apps_unit.py # 边界/示例单元测试
|
||||
```
|
||||
|
||||
221
docs/specs/ai-prompt-refinement/requirements.md
Normal file
221
docs/specs/ai-prompt-refinement/requirements.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 需求文档 — NS2:AI Prompt 细化
|
||||
|
||||
## 简介
|
||||
|
||||
NS2 将 P5-A 阶段交付的 6 个 AI 应用(应用 1/3/4/5/6/7)的 `build_prompt()` 占位骨架升级为完整实现。核心工作包括:创建共享数据获取层(`data_fetchers/`)从 FDW 视图获取真实业务数据、完善 6 个应用的 Prompt 拼接函数使 AI 能基于真实数据生成分析、以及实现应用 1 的页面上下文文本化(根据 `contextType` 自动获取并格式化页面数据)。
|
||||
|
||||
### 依赖
|
||||
|
||||
- P5-A(AI 集成管道)— 百炼封装、缓存 API、SSE 框架、应用 2/8 完整实现、应用 1/3/4/5/6/7 触发机制和调用骨架
|
||||
- NS1(小程序后端 API)— 后端数据结构确定、FDW 映射建立
|
||||
- RNS1.4(CHAT 对齐与联调收尾)— chat 模块路径迁移、`contextType`/`contextId` 参数机制
|
||||
|
||||
### 来源文档
|
||||
|
||||
- `docs/prd/Neo_Specs/NS2-ai-prompt-refinement.md` — NS2 完整 PRD
|
||||
- `docs/prd/specs/P5-miniapp-ai-integration.md` — P5 AI 集成 spec(Prompt JSON 结构定义)
|
||||
- `docs/miniprogram-dev/API-contract.md` — 接口契约(数据结构参考)
|
||||
- `docs/prd/Neo_Specs/NS1-xcx-backend-api.md` — NS1 后端 API spec
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Data_Fetcher**:共享数据获取模块,位于 `apps/backend/app/ai/data_fetchers/`,负责从数据库查询并格式化 AI 所需数据
|
||||
- **Build_Prompt**:各 AI 应用中的 Prompt 拼接函数,将数据获取层返回的数据组装为百炼 API 所需的消息列表
|
||||
- **FDW_View**:PostgreSQL Foreign Data Wrapper 视图,前缀 `fdw_etl.v_*`,用于从业务库只读访问 ETL 库数据
|
||||
- **Page_Context_Builder**:页面上下文文本化模块,根据 `contextType` 从数据库获取页面数据并格式化为 AI 可读的结构化中文文本
|
||||
- **Bailian_Client**:百炼 API 统一封装层,位于 `apps/backend/app/ai/bailian_client.py`
|
||||
- **AI_Cache**:AI 缓存表 `biz.ai_cache`,存储各应用的 AI 分析结果供前端读取和跨应用引用
|
||||
- **items_sum**:DWD-DOC 强制使用的消费金额口径,= `table_charge_money` + `goods_money` + `assistant_pd_money` + `assistant_cx_money` + `electricity_money`
|
||||
- **RLS**:Row Level Security,通过 `SET LOCAL app.current_site_id` 实现多门店数据隔离
|
||||
- **contextType**:前端跳转 chat 页面时传入的页面类型标识(如 `task-detail`、`board-finance`),后端据此路由到对应的文本化函数
|
||||
- **contextId**:前端跳转 chat 页面时传入的页面实体 ID(如 taskId、memberId)
|
||||
- **biz_params**:应用 1 system prompt 中注入的用户身份参数,百炼平台侧据此执行数据查询隔离
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:客户消费数据获取模块
|
||||
|
||||
**用户故事:** 作为 AI 应用(应用 3/6/7),我需要获取客户近期消费数据的完整结构化信息,以便基于真实数据生成维客线索分析、备注分析和运营策略。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Data_Fetcher SHALL 提供 `fetch_member_consumption_data(site_id, member_id, months)` 异步函数,从 FDW_View 获取客户近 N 个月(默认 3 个月)的消费数据,返回包含以下字段的字典:`consumption_records`(消费记录列表)、`member_cards`(会员卡明细列表)、`card_balance_total`(储值卡余额合计)、`stored_value_balance_total`(储值余额合计)、`expected_visit_date`(预计到店日期)、`days_since_last_visit`(距上次到店天数)
|
||||
2. THE Data_Fetcher SHALL 从 `fdw_etl.v_dwd_settlement_head` 和 `fdw_etl.v_dwd_table_fee_log` 获取台桌结账记录,从 `fdw_etl.v_dwd_store_goods_sale` 获取商城订单记录,仅包含正向交易(`settle_type IN (1, 3)`)
|
||||
3. THE Data_Fetcher SHALL 为每条消费记录返回以下金额拆分字段:`settle_date`、`settle_type`、`items_sum`、`table_charge_money`、`assistant_pd_money`、`assistant_cx_money`、`goods_money`、`room_name`、`duration_minutes`、`assistant_names`(服务助教列表)
|
||||
4. THE Data_Fetcher SHALL 使用 `items_sum` 作为消费金额口径,禁止使用 `consume_money`
|
||||
5. THE Data_Fetcher SHALL 从 `fdw_etl.v_dim_member_card_account` 获取会员卡明细,每张卡包含 `card_type`(卡类型)、`balance`(余额)、`gift_balance`(赠送余额)
|
||||
6. THE Data_Fetcher SHALL 从 `fdw_etl.v_dws_member_visit_detail` 的到店间隔数据推算 `expected_visit_date`,并计算 `days_since_last_visit`
|
||||
7. THE Data_Fetcher SHALL 通过 `member_id` JOIN `fdw_etl.v_dim_member`(`scd2_is_current=1`)获取会员昵称和手机号,禁止使用结算单冗余的 `member_phone`/`member_name` 字段
|
||||
8. THE Data_Fetcher SHALL 在执行 FDW 查询前通过 `get_etl_readonly_connection(site_id)` 获取连接,并执行 `SET LOCAL app.current_site_id` 设置 RLS 隔离
|
||||
9. THE Data_Fetcher SHALL 将单次查询返回的消费记录数限制为最多 100 条,按 `settle_date` 倒序排列
|
||||
10. IF FDW 查询超时(超过 5 秒)或连接失败,THEN THE Data_Fetcher SHALL 抛出明确的异常,包含失败的视图名称和错误类型,由调用方决定降级策略
|
||||
|
||||
### 需求 2:助教数据获取模块
|
||||
|
||||
**用户故事:** 作为 AI 应用(应用 4/5),我需要获取助教基本信息和助教-客户服务历史,以便基于真实数据生成关系分析和话术参考。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Data_Fetcher SHALL 提供 `fetch_assistant_info(site_id, assistant_id)` 异步函数,从 `fdw_etl.v_dim_assistant` 和 `fdw_etl.v_dws_assistant_salary_calc` 获取助教基本信息,返回包含以下字段的字典:`nickname`(花名)、`level`(级别)、`hire_date`(入职日期)、`tenure_months`(工龄月数)、`monthly_customers`(本月服务客户数)、`performance_tier`(绩效档位)
|
||||
2. THE Data_Fetcher SHALL 提供 `fetch_service_history(site_id, assistant_id, member_id, months)` 异步函数,从 `fdw_etl.v_dwd_assistant_service_log` 获取助教服务该客户的历史记录(默认近 3 个月),每条记录包含:`service_date`、`duration_minutes`、`items_sum`、`room_name`、`is_pd`(是否陪打)
|
||||
3. THE Data_Fetcher SHALL 使用 `dwd_assistant_service_log_ex.is_trash` 排除废单记录,禁止使用已废弃的 `dwd_assistant_trash_event` 表
|
||||
4. THE Data_Fetcher SHALL 从 `fdw_etl.v_dws_member_assistant_relation_index` 获取助教-客户关系指数,从 `fdw_etl.v_dws_member_assistant_intimacy` 获取亲密度数据
|
||||
5. THE Data_Fetcher SHALL 在执行 FDW 查询前通过 `get_etl_readonly_connection(site_id)` 获取连接,并执行 `SET LOCAL app.current_site_id` 设置 RLS 隔离
|
||||
6. IF FDW 查询超时或连接失败,THEN THE Data_Fetcher SHALL 抛出明确的异常,包含失败的视图名称和错误类型
|
||||
|
||||
### 需求 3:应用 3 Prompt 拼接完善(客户数据维客线索分析)
|
||||
|
||||
**用户故事:** 作为系统,我需要在客户新增消费时,将真实的消费数据、会员卡信息和历史线索拼接为完整的 Prompt JSON,以便 AI 能基于真实数据提取维客线索。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 应用 3 的 `build_prompt()` 被调用时,THE Build_Prompt SHALL 调用 `fetch_member_consumption_data()` 获取客户近 3 个月消费数据,替换当前的 TODO 占位符
|
||||
2. THE Build_Prompt SHALL 将获取到的数据组装为 P5 spec 定义的 Prompt JSON 结构,包含 `current_time`(精确到秒)、`member_nickname`、`main_data`(含 `consumption_records`、`member_cards`、`card_balance_total`、`stored_value_balance_total`、`expected_visit_date`、`days_since_last_visit`)、`reference`(含 `app6_clues` 和 `app8_history`)
|
||||
3. THE Build_Prompt SHALL 在 `reference.app8_history` 中包含最近 2 套应用 8 历史结果,每套附带 `generated_at` 时间戳
|
||||
4. IF 客户无消费记录,THEN THE Build_Prompt SHALL 将 `consumption_records` 设为空数组,其余字段使用默认值(余额为 0、`days_since_last_visit` 为 null),Prompt 中标注"该客户暂无消费记录"
|
||||
5. THE Build_Prompt SHALL 将每条消费记录中的金额字段逐项拆分(`table_charge_money`、`assistant_pd_money`、`assistant_cx_money`、`goods_money`),禁止使用 `consume_money`
|
||||
|
||||
### 需求 4:应用 4 Prompt 拼接完善(关系分析/任务建议)
|
||||
|
||||
**用户故事:** 作为系统,我需要在助教参与新结算或被分配召回任务时,将助教信息、服务历史和客户数据拼接为完整的 Prompt JSON,以便 AI 能生成关系分析和任务建议。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 应用 4 的 `build_prompt()` 被调用时,THE Build_Prompt SHALL 调用 `fetch_assistant_info()` 获取助教基本信息,调用 `fetch_service_history()` 获取助教-客户服务历史,调用 `fetch_member_consumption_data()` 获取客户消费数据,替换当前的 TODO 占位符
|
||||
2. THE Build_Prompt SHALL 将获取到的数据组装为 P5 spec 定义的 Prompt JSON 结构,包含 `current_time`、`assistant_info`、`service_history`、`task_assignment_basis`、`customer_data`(含 `system_data` 和 `notes`)、`reference`(含 `app8_current` 和 `app8_history`)
|
||||
3. THE Build_Prompt SHALL 从 `biz.notes` 获取所有助教对该客户的全部备注,每条备注包含 `recorded_by`(创建者)、`content`(内容)、`created_at`(创建时间)
|
||||
4. THE Build_Prompt SHALL 对备注内容进行截断处理,单条备注最大 500 字符,超出部分截断并附加"…(已截断)"标记
|
||||
5. IF 助教无服务该客户的历史记录,THEN THE Build_Prompt SHALL 将 `service_history` 设为空数组,Prompt 中标注"该助教暂无服务该客户的记录"
|
||||
6. THE Build_Prompt SHALL 在 `reference` 中包含应用 8 最新结果(`app8_current`)和最近 2 套历史(`app8_history`),每套附带 `generated_at` 时间戳
|
||||
|
||||
### 需求 5:应用 5 Prompt 拼接完善(话术参考)
|
||||
|
||||
**用户故事:** 作为系统,我需要在应用 4 完成后,将应用 4 的分析结果连同助教信息和服务历史拼接为完整的 Prompt JSON,以便 AI 能生成针对性的沟通话术。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 应用 5 的 `build_prompt()` 被调用时,THE Build_Prompt SHALL 复用应用 4 的数据获取逻辑(`fetch_assistant_info()`、`fetch_service_history()`、`fetch_member_consumption_data()`),替换当前的 TODO 占位符
|
||||
2. THE Build_Prompt SHALL 将获取到的数据组装为 P5 spec 定义的 Prompt JSON 结构,包含 `current_time`、`assistant_info`、`service_history`、`task_assignment_basis`、`customer_data`(含 `system_data` 和 `notes`)、`task_suggestion`(应用 4 的完整返回结果)、`reference`(含 `app8_history`)
|
||||
3. THE Build_Prompt SHALL 从 `context["app4_result"]` 获取应用 4 的返回结果,作为 `task_suggestion` 字段传入 Prompt
|
||||
4. IF `context["app4_result"]` 为空或缺失,THEN THE Build_Prompt SHALL 将 `task_suggestion` 设为空对象,Prompt 中标注"暂无任务建议"
|
||||
|
||||
### 需求 6:应用 6 Prompt 拼接完善(备注分析)
|
||||
|
||||
**用户故事:** 作为系统,我需要在助教提交备注后,将备注内容、客户消费数据和历史备注拼接为完整的 Prompt JSON,以便 AI 能分析备注价值并提取维客线索。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 应用 6 的 `build_prompt()` 被调用时,THE Build_Prompt SHALL 调用 `fetch_member_consumption_data()` 获取客户消费数据,替换当前的 TODO 占位符
|
||||
2. THE Build_Prompt SHALL 将获取到的数据组装为 P5 spec 定义的 Prompt JSON 结构,包含 `current_time`、`current_note`(含 `content`、`recorded_by`、`created_at`)、`reference`(含 `member_nickname`、`consumption_data`、`all_notes`、`app3_clues`、`app8_history`)
|
||||
3. THE Build_Prompt SHALL 从 `biz.notes` 获取所有助教对该客户的全部备注作为 `all_notes`,每条备注包含 `recorded_by`、`content`、`created_at`
|
||||
4. THE Build_Prompt SHALL 对 `all_notes` 中每条备注内容进行截断处理,单条备注最大 500 字符
|
||||
5. THE Build_Prompt SHALL 在 `reference` 中包含应用 3 最新线索(`app3_clues`)和最近 2 套应用 8 历史(`app8_history`),每套附带 `generated_at` 时间戳
|
||||
6. IF 客户无历史备注,THEN THE Build_Prompt SHALL 将 `all_notes` 设为空数组
|
||||
|
||||
### 需求 7:应用 7 Prompt 拼接完善(客户分析)
|
||||
|
||||
**用户故事:** 作为系统,我需要在消费事件链中应用 8 完成后,将客户全量客观数据和主观备注拼接为完整的 Prompt JSON,以便 AI 能生成全局运营策略。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 应用 7 的 `build_prompt()` 被调用时,THE Build_Prompt SHALL 调用 `fetch_member_consumption_data()` 获取客户消费数据,替换当前的 TODO 占位符
|
||||
2. THE Build_Prompt SHALL 将获取到的数据组装为 P5 spec 定义的 Prompt JSON 结构,包含 `current_time`、`member_id`、`member_nickname`、`objective_data`(同应用 3 的 `main_data` 结构)、`subjective_data`(含 `notes`)、`reference`(含 `app8_current` 和 `app8_history`)
|
||||
3. THE Build_Prompt SHALL 从 `biz.notes` 获取该客户的全部备注作为 `subjective_data.notes`,每条备注包含 `recorded_by`(创建者)、`content`、`created_at`
|
||||
4. THE Build_Prompt SHALL 对来自备注的主观信息在 Prompt 中标注"【来源:{recorded_by},请甄别信息真实性】"
|
||||
5. THE Build_Prompt SHALL 在 `reference` 中包含应用 8 最新结果(`app8_current`)和最近 2 套历史(`app8_history`),每套附带 `generated_at` 时间戳
|
||||
6. IF 客户无备注记录,THEN THE Build_Prompt SHALL 将 `subjective_data.notes` 设为空数组,Prompt 中标注"该客户暂无主观备注信息"
|
||||
|
||||
### 需求 8:页面上下文文本化框架
|
||||
|
||||
**用户故事:** 作为助教,我希望从任意页面进入 AI 对话时,AI 能自动了解当前页面的上下文信息(客户数据、任务信息、看板数据等),以便提供与当前场景相关的回答。
|
||||
|
||||
> **P5 对齐说明**:P5 spec 定义应用 1 首条 Prompt 包含 `page_context`(页面上下文摘要)和 `screen_content`(用户当前屏幕可见内容)两个独立字段。NS2 采用后端自动查询方案,通过 `build_page_text()` 生成合并的页面上下文文本,等效覆盖两个字段的信息需求。详见 NS2 PRD 3.7 设计决策。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Page_Context_Builder SHALL 提供 `build_page_text(source_page, context_id, site_id, filters)` 异步函数,根据 `source_page`(即 `contextType`)从数据库获取对应页面数据并格式化为结构化中文文本
|
||||
2. THE Page_Context_Builder SHALL 支持以下 10 种页面类型的文本化:`task-detail`、`customer-detail`、`coach-detail`、`board-finance`、`board-customer`、`board-coach`、`performance`、`my-profile`、`task-list`、`customer-service-records`
|
||||
3. WHEN `source_page` 为 `task-detail` 时,THE Page_Context_Builder SHALL 从 `biz.coach_tasks`、会员信息、备注和 `ai_cache` 获取数据,输出包含任务信息、客户信息、备注摘要和 AI 分析的结构化文本
|
||||
4. WHEN `source_page` 为 `customer-detail` 时,THE Page_Context_Builder SHALL 从会员信息、消费记录和维客线索获取数据,输出包含客户全信息、消费记录摘要和维客线索的结构化文本
|
||||
5. WHEN `source_page` 为 `coach-detail` 时,THE Page_Context_Builder SHALL 从助教信息、任务统计和备注获取数据,输出包含助教信息、任务统计和备注摘要的结构化文本
|
||||
6. WHEN `source_page` 为看板类页面(`board-finance`、`board-customer`、`board-coach`)时,THE Page_Context_Builder SHALL 接受可选的筛选参数(`timeDimension`、`dimension`、`areaFilter` 等),从对应 DWS 汇总视图获取数据,输出数据摘要文本
|
||||
7. WHEN `source_page` 为 `performance` 时,THE Page_Context_Builder SHALL 从 `fdw_etl.v_dws_assistant_salary_calc` 获取绩效数据,输出绩效数据摘要文本
|
||||
8. THE Page_Context_Builder SHALL 将每个页面上下文的输出文本控制在 2000 字符以内,避免 token 浪费
|
||||
9. THE Page_Context_Builder SHALL 输出结构化中文描述文本(非 JSON),使用分段标题和缩进格式便于 AI 理解
|
||||
10. IF 看板类页面未传入筛选参数,THEN THE Page_Context_Builder SHALL 使用默认值(如 `board-finance` 默认"本月"、`board-customer` 默认按消费金额排序)
|
||||
11. IF 数据获取失败(FDW 超时、连接失败等),THEN THE Page_Context_Builder SHALL 返回"页面上下文获取失败,请直接描述您的问题"文本,不阻断对话流程
|
||||
12. THE Page_Context_Builder SHALL 不传入 `member_phone` 等已断档的敏感字段,会员信息通过 `member_id` JOIN `v_dim_member` 获取
|
||||
|
||||
### 需求 9:应用 1 页面上下文集成
|
||||
|
||||
**用户故事:** 作为助教,我希望从不同页面入口进入 AI 对话时,AI 的首条回复能体现对当前页面上下文的理解,而不是一个通用的空白对话。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户从任意页面进入 chat 页面时,THE Build_Prompt SHALL 在 `app1_chat.py` 的 `_build_page_context()` 中调用 `Page_Context_Builder.build_page_text()`,根据 `contextType` 和 `contextId` 获取页面上下文文本
|
||||
2. THE Build_Prompt SHALL 将 `build_page_text()` 返回的文本作为 system prompt 中的 `page_context` 字段注入,替换当前的空透传实现
|
||||
3. WHEN `contextType` 为详情类页面(`task-detail`、`customer-detail`、`coach-detail`)时,THE Build_Prompt SHALL 使用 `contextId` 作为实体 ID 传入 `build_page_text()`
|
||||
4. WHEN `contextType` 为看板类页面(`board-finance`、`board-customer`、`board-coach`)时,THE Build_Prompt SHALL 将前端传入的筛选参数(`timeDimension`、`dimension` 等)作为 `filters` 传入 `build_page_text()`
|
||||
5. IF `contextType` 为空或未识别的类型,THEN THE Build_Prompt SHALL 跳过页面上下文注入,AI 以通用对话模式响应
|
||||
6. THE Build_Prompt SHALL 确保 `biz_params.user_prompt_params`(`User_ID`、`Role`、`Nickname`)在页面上下文注入后仍正确存在于 system prompt 中
|
||||
|
||||
### 需求 10:前端看板筛选参数传递
|
||||
|
||||
**用户故事:** 作为助教或管理者,我希望从看板页面进入 AI 对话时,AI 能了解当前看板的筛选条件(时间维度、排序维度等),以便基于当前视图提供分析。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户从 `board-finance` 页面跳转到 chat 页面时,THE Miniprogram SHALL 将当前的 `timeDimension`(时间维度)和 `areaFilter`(区域筛选)作为额外参数传入 chat 页面
|
||||
2. WHEN 用户从 `board-customer` 页面跳转到 chat 页面时,THE Miniprogram SHALL 将当前的 `dimension`(排序维度)和 `typeFilter`(类型筛选)作为额外参数传入 chat 页面
|
||||
3. WHEN 用户从 `board-coach` 页面跳转到 chat 页面时,THE Miniprogram SHALL 将当前的 `dimension`、`projectFilter`(技能筛选)和 `timeDimension` 作为额外参数传入 chat 页面
|
||||
4. THE Miniprogram SHALL 将看板筛选参数编码为 chat 页面 URL 的查询参数,后端从请求中提取并传入 `build_page_text()` 的 `filters` 参数
|
||||
|
||||
### 需求 11:数据获取层全局约束
|
||||
|
||||
**用户故事:** 作为后端开发者,我需要确保所有数据获取函数遵循统一的数据库连接、金额口径和安全规范,以保证数据一致性和多门店隔离。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Data_Fetcher SHALL 对所有 FDW 查询使用 `get_etl_readonly_connection(site_id)` 获取只读连接,对业务库查询使用 `get_connection()` 获取连接
|
||||
2. THE Data_Fetcher SHALL 在每次 FDW 查询前执行 `SET LOCAL app.current_site_id = {site_id}`,确保 RLS 多门店数据隔离生效
|
||||
3. THE Data_Fetcher SHALL 在所有涉及金额的查询和计算中使用 `items_sum` 口径,禁止使用 `consume_money`
|
||||
4. THE Data_Fetcher SHALL 在所有涉及助教费用的查询中将费用拆分为 `assistant_pd_money`(陪打)和 `assistant_cx_money`(超休),禁止使用合并的 `service_fee`
|
||||
5. THE Data_Fetcher SHALL 在所有涉及会员信息的查询中通过 `member_id` JOIN `v_dim_member`(`scd2_is_current=1`)获取昵称和手机号,禁止使用结算单冗余字段
|
||||
6. THE Data_Fetcher SHALL 支持多个数据获取函数并发执行(通过 `asyncio.gather`),以减少 Prompt 构建的总耗时
|
||||
7. THE Data_Fetcher SHALL 为所有 FDW 查询设置 5 秒超时,超时后抛出 `TimeoutError` 并记录慢查询日志
|
||||
|
||||
### 需求 12:空数据与错误降级处理
|
||||
|
||||
**用户故事:** 作为系统,我需要在数据获取部分失败或数据为空时,仍能生成有效的 Prompt 并调用 AI,以保证 AI 功能的可用性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. IF 某个数据获取函数失败(FDW 超时、连接失败等),THEN THE Build_Prompt SHALL 将该部分数据设为默认空值(空数组或空对象),在 Prompt 中标注"该部分数据获取失败",继续生成 Prompt 并调用百炼 API
|
||||
2. IF 客户无消费记录、无备注、无服务历史,THEN THE Build_Prompt SHALL 在 Prompt 中使用明确的空状态提示词(如"该客户暂无消费记录,请基于已有信息分析"),而非传入空数据不做说明
|
||||
3. IF AI_Cache 中无 reference 历史数据(如新客户首次结算),THEN THE Build_Prompt SHALL 将 `reference` 设为空对象,在 Prompt 中标注"暂无历史线索"
|
||||
4. THE Build_Prompt SHALL 确保在任何数据获取失败的情况下,Prompt 的整体 JSON 结构仍然合法,不出现 null 值导致百炼 API 解析失败
|
||||
5. IF 应用 1 的页面上下文获取失败,THEN THE Build_Prompt SHALL 使用"页面上下文获取失败,请直接描述您的问题"作为 `page_context`,不阻断对话
|
||||
|
||||
### 需求 13:biz_params 端到端正确性
|
||||
|
||||
**用户故事:** 作为系统管理员,我需要确保应用 1 的用户身份信息从前端到百炼平台的完整传递链路正确无误,以保证数据查询隔离生效。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Backend SHALL 从 JWT token 中提取 `user_id`、`role`、`nickname`,传入 `app1_chat.py` 的 `_build_system_prompt()`,确保用户身份信息不可由前端伪造
|
||||
2. THE Build_Prompt SHALL 在 system prompt 的 `biz_params.user_prompt_params` 中注入 `User_ID`(字符串)、`Role`("助教"或"管理者")、`Nickname`(昵称),供百炼平台侧执行数据查询隔离
|
||||
3. WHILE 用户身份为助教时,THE Bailian_Client SHALL 确保百炼平台仅允许查询与该助教相关的数据,禁止查询其他助教业绩、工资、客户关系等敏感数据,禁止查询门店级财务数据
|
||||
4. WHILE 用户身份为管理者时,THE Bailian_Client SHALL 确保百炼平台允许查询该门店下所有数据
|
||||
5. THE Backend SHALL 确保所有传入百炼平台的查询均包含 `site_id` 过滤,防止跨门店数据泄露
|
||||
|
||||
### 需求 14:Prompt Token 预算控制
|
||||
|
||||
**用户故事:** 作为系统,我需要控制每个应用首条 Prompt 的数据量,避免超出百炼 API 的 token 限制或产生不必要的 token 消耗。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Build_Prompt SHALL 将应用 3/4/5/6/7 的首条 Prompt JSON 总字符数控制在 8000 字符以内(约 4000 token)
|
||||
2. THE Build_Prompt SHALL 将应用 1 的 system prompt(含页面上下文)总字符数控制在 4000 字符以内(约 2000 token)
|
||||
3. WHEN 消费记录数量超过 100 条时,THE Data_Fetcher SHALL 仅返回最近 100 条记录,并在 Prompt 中标注"仅展示最近 100 条,共 {total} 条"
|
||||
4. WHEN 备注总数超过 50 条时,THE Build_Prompt SHALL 仅传入最近 50 条备注(按 `created_at` 倒序),并在 Prompt 中标注"仅展示最近 50 条备注"
|
||||
5. THE Build_Prompt SHALL 对所有传入 Prompt 的文本字段进行长度检查,单个文本字段不超过 1000 字符
|
||||
294
docs/specs/ai-prompt-refinement/tasks.md
Normal file
294
docs/specs/ai-prompt-refinement/tasks.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 实施计划:NS2 AI Prompt 细化
|
||||
|
||||
## 概述
|
||||
|
||||
按照设计文档,将实施拆分为:共享数据获取层 → 应用 3/4/5/6/7 Prompt 拼接 → 应用 1 页面上下文 → 前端筛选参数 → 集成联调。每个任务增量构建,确保无孤立代码。属性测试(Hypothesis)和单元测试作为可选子任务紧跟实现步骤。
|
||||
|
||||
后端使用 Python(FastAPI + asyncio),测试使用 Hypothesis + pytest。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 创建共享数据获取层基础结构
|
||||
- [x] 1.1 创建 `apps/backend/app/ai/data_fetchers/` 模块骨架
|
||||
- 创建 `__init__.py`,导出 `fetch_member_consumption_data`、`fetch_assistant_info`、`fetch_service_history`、`fetch_member_notes`、`build_page_text`
|
||||
- 创建 `member_data.py`、`assistant_data.py`、`page_context.py` 空文件
|
||||
- _需求: 1.1, 2.1, 8.1_
|
||||
|
||||
- [x] 2. 实现客户消费数据获取(member_data.py)
|
||||
- [x] 2.1 实现 `fetch_member_consumption_data(site_id, member_id, months=3)`
|
||||
- 使用 `get_etl_readonly_connection(site_id)` + `SET LOCAL app.current_site_id` 获取 FDW 连接
|
||||
- 从 `v_dwd_settlement_head` + `v_dwd_table_fee_log` 获取台桌结账(`settle_type IN (1,3)`)
|
||||
- 从 `v_dwd_store_goods_sale` 获取商城订单
|
||||
- 从 `v_dim_member_card_account` 获取会员卡明细
|
||||
- 从 `v_dws_member_consumption_summary` + `v_dws_member_visit_detail` 获取汇总和到店数据
|
||||
- 从 `v_dim_member`(`scd2_is_current=1`)获取会员昵称
|
||||
- 消费记录限制 100 条,按 `settle_date DESC` 排序,超出标注 `truncated`
|
||||
- 金额使用 `items_sum` 口径,拆分 `assistant_pd_money` / `assistant_cx_money`
|
||||
- 5 秒查询超时,超时抛出 `TimeoutError`
|
||||
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 11.1, 11.2, 11.3, 11.4, 11.5, 11.7_
|
||||
|
||||
- [x] 2.2 实现 `fetch_member_notes(site_id, member_id, limit=50)`
|
||||
- 使用 `get_connection()` 查询 `biz.notes`
|
||||
- 按 `created_at DESC` 排序,最多 `limit` 条
|
||||
- 单条备注内容截断 500 字符,超出附加"…(已截断)"
|
||||
- _需求: 4.3, 4.4, 6.3, 6.4, 14.4_
|
||||
|
||||
- [x] 2.3 编写属性测试:数据获取返回结构完整性(P1)
|
||||
- **Property 1: 数据获取函数返回结构完整性**
|
||||
- Mock 数据库返回随机行,验证 `fetch_member_consumption_data()` 返回字典包含所有必需键
|
||||
- 验证每条消费记录包含 `settle_date`、`settle_type`、`items_sum` 等 10 个字段
|
||||
- 测试文件: `tests/test_data_fetchers/test_member_data_props.py`
|
||||
- **验证: 需求 1.1, 1.3, 1.5, 2.1, 2.2, 2.4**
|
||||
|
||||
- [x] 2.4 编写属性测试:正向交易过滤(P2)+ 金额口径(P3)+ 记录限制与排序(P4)
|
||||
- **Property 2: 消费记录仅包含正向交易** — 生成混合 `settle_type` 记录,验证输出仅含 1/3
|
||||
- **Property 3: 金额口径使用 items_sum** — 验证输出包含 `items_sum` 且不含 `consume_money`
|
||||
- **Property 4: 消费记录数量限制与排序** — 生成 0-200 条记录,验证输出 ≤100 且 `settle_date` 降序
|
||||
- 测试文件: `tests/test_data_fetchers/test_member_data_props.py`
|
||||
- **验证: 需求 1.2, 1.4, 1.9, 3.5, 11.3, 11.4, 14.3**
|
||||
|
||||
- [x] 2.5 编写属性测试:备注截断(P6)+ 备注数量限制(P17)
|
||||
- **Property 6: 备注截断不变量** — 生成 0-2000 字符字符串,验证截断后 ≤500,超长以"…(已截断)"结尾
|
||||
- **Property 17: 备注数量限制** — 生成 0-100 条备注,验证输出 ≤50 且 `created_at` 降序
|
||||
- 测试文件: `tests/test_data_fetchers/test_member_data_props.py`
|
||||
- **验证: 需求 4.4, 6.4, 14.4**
|
||||
|
||||
- [x] 2.6 编写单元测试:member_data 边界情况
|
||||
- 客户无消费记录时返回空数组和默认值
|
||||
- 客户无备注时返回空数组
|
||||
- FDW 查询超时时抛出 `TimeoutError`
|
||||
- 会员不存在时 `member_nickname` 为空字符串
|
||||
- 测试文件: `tests/test_data_fetchers/test_data_fetchers_unit.py`
|
||||
- _需求: 1.10, 3.4, 6.6, 12.1, 12.2_
|
||||
|
||||
- [x] 3. 实现助教数据获取(assistant_data.py)
|
||||
- [x] 3.1 实现 `fetch_assistant_info(site_id, assistant_id)`
|
||||
- 从 `v_dim_assistant` 获取花名、级别、入职日期
|
||||
- 从 `v_dws_assistant_salary_calc` 获取本月客户数、绩效档位
|
||||
- 计算 `tenure_months`(入职日期到当前日期)
|
||||
- 助教不存在时抛出 `ValueError("assistant not found")`
|
||||
- _需求: 2.1, 2.5, 2.6, 11.1, 11.2_
|
||||
|
||||
- [x] 3.2 实现 `fetch_service_history(site_id, assistant_id, member_id, months=3)`
|
||||
- 从 `v_dwd_assistant_service_log` 获取服务记录,`WHERE is_trash = false`
|
||||
- 从 `v_dws_member_assistant_relation_index` 获取关系指数
|
||||
- 从 `v_dws_member_assistant_intimacy` 获取亲密度
|
||||
- 按 `service_date DESC` 排序
|
||||
- _需求: 2.2, 2.3, 2.4, 11.1, 11.2_
|
||||
|
||||
- [x] 3.3 编写属性测试:废单排除(P5)
|
||||
- **Property 5: 废单记录排除**
|
||||
- 生成含 `is_trash` 标记的服务记录,验证输出不含 `is_trash=true` 的记录
|
||||
- 测试文件: `tests/test_data_fetchers/test_assistant_data_props.py`
|
||||
- **验证: 需求 2.3**
|
||||
|
||||
- [x] 3.4 编写单元测试:assistant_data 边界情况
|
||||
- 助教不存在时抛出 `ValueError`
|
||||
- 无服务历史时返回空列表
|
||||
- FDW 查询超时时抛出 `TimeoutError`
|
||||
- 测试文件: `tests/test_data_fetchers/test_data_fetchers_unit.py`
|
||||
- _需求: 2.6, 4.5, 12.1_
|
||||
|
||||
- [x] 4. 检查点 — 数据获取层完成
|
||||
- 确保所有测试通过,ask the user if questions arise.
|
||||
|
||||
|
||||
- [x] 5. 实现应用 3 Prompt 拼接(app3_clue.py)
|
||||
- [x] 5.1 改造 `app3_clue.py` 的 `build_prompt()` 为 `async def`
|
||||
- 新增 `site_id` 参数,调用 `fetch_member_consumption_data()` 获取真实消费数据
|
||||
- 组装 Prompt JSON:`current_time`、`member_nickname`、`main_data`、`reference`
|
||||
- `reference` 从 `ai_cache` 获取 `app6_clues` + 最近 2 套 `app8_history`
|
||||
- 空数据时标注"该客户暂无消费记录"
|
||||
- `run()` 中调用 `build_prompt()` 需 `await`
|
||||
- _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 12.1, 12.2, 12.3_
|
||||
|
||||
- [x] 5.2 编写属性测试:App3 Prompt 结构完整性(P7 局部)
|
||||
- **Property 7(App3 部分): build_prompt 返回结构完整性**
|
||||
- Mock 数据获取返回随机数据,验证 Prompt JSON 包含 `current_time`、`member_nickname`、`main_data`、`reference` 顶层键
|
||||
- 测试文件: `tests/test_ai_apps/test_build_prompt_props.py`
|
||||
- **验证: 需求 3.1, 3.2**
|
||||
|
||||
- [x] 6. 实现应用 4 Prompt 拼接(app4_analysis.py)
|
||||
- [x] 6.1 改造 `app4_analysis.py` 的 `build_prompt()` 为 `async def`
|
||||
- 使用 `asyncio.gather` 并发调用 `fetch_assistant_info()` + `fetch_service_history()` + `fetch_member_consumption_data()`
|
||||
- 调用 `fetch_member_notes()` 获取客户备注
|
||||
- 组装 Prompt JSON:`current_time`、`assistant_info`、`service_history`、`task_assignment_basis`、`customer_data`(含 `system_data` + `notes`)、`reference`
|
||||
- `reference` 从 `ai_cache` 获取 `app8_current` + 最近 2 套 `app8_history`
|
||||
- 部分数据获取失败时使用 `return_exceptions=True` 降级处理
|
||||
- _需求: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 11.6, 12.1, 12.4_
|
||||
|
||||
- [x] 6.2 编写属性测试:App4 Prompt 结构完整性(P7 局部)
|
||||
- **Property 7(App4 部分): build_prompt 返回结构完整性**
|
||||
- Mock 数据获取返回随机数据,验证 Prompt JSON 包含 `current_time`、`assistant_info`、`service_history`、`customer_data`、`reference` 顶层键
|
||||
- 测试文件: `tests/test_ai_apps/test_build_prompt_props.py`
|
||||
- **验证: 需求 4.1, 4.2**
|
||||
|
||||
- [x] 7. 实现应用 5 Prompt 拼接(app5_tactics.py)
|
||||
- [x] 7.1 改造 `app5_tactics.py` 的 `build_prompt()` 为 `async def`
|
||||
- 复用应用 4 的数据获取逻辑(`fetch_assistant_info` + `fetch_service_history` + `fetch_member_consumption_data`)
|
||||
- 从 `context["app4_result"]` 获取 `task_suggestion`,缺失时设为空对象
|
||||
- 组装 Prompt JSON:同 App4 结构 + `task_suggestion`
|
||||
- _需求: 5.1, 5.2, 5.3, 5.4_
|
||||
|
||||
- [x] 7.2 编写属性测试:App5 task_suggestion 传递(P8)
|
||||
- **Property 8: App5 task_suggestion 传递**
|
||||
- 生成随机 `app4_result`(含空/缺失),验证 `task_suggestion` 字段正确传递或设为空对象
|
||||
- 测试文件: `tests/test_ai_apps/test_build_prompt_props.py`
|
||||
- **验证: 需求 5.3, 5.4**
|
||||
|
||||
- [x] 8. 实现应用 6 Prompt 拼接(app6_note.py)
|
||||
- [x] 8.1 改造 `app6_note.py` 的 `build_prompt()` 为 `async def`
|
||||
- 调用 `fetch_member_consumption_data()` 获取消费数据
|
||||
- 调用 `fetch_member_notes()` 获取全部备注作为 `all_notes`
|
||||
- 组装 Prompt JSON:`current_time`、`current_note`、`reference`(含 `member_nickname`、`consumption_data`、`all_notes`、`app3_clues`、`app8_history`)
|
||||
- 空备注时 `all_notes` 设为空数组
|
||||
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 12.1, 12.2_
|
||||
|
||||
- [x] 9. 实现应用 7 Prompt 拼接(app7_customer.py)
|
||||
- [x] 9.1 改造 `app7_customer.py` 的 `build_prompt()` 为 `async def`
|
||||
- 调用 `fetch_member_consumption_data()` 获取客观数据
|
||||
- 调用 `fetch_member_notes()` 获取备注作为 `subjective_data.notes`
|
||||
- 每条备注标注"【来源:{recorded_by},请甄别信息真实性】"
|
||||
- 组装 Prompt JSON:`current_time`、`member_id`、`member_nickname`、`objective_data`、`subjective_data`、`reference`
|
||||
- 空备注时标注"该客户暂无主观备注信息"
|
||||
- _需求: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 12.1, 12.2_
|
||||
|
||||
- [x] 9.2 编写属性测试:App7 主观信息标注(P9)
|
||||
- **Property 9: App7 主观信息来源标注**
|
||||
- 生成随机备注(含 `recorded_by`),验证 Prompt 中每条备注附带"【来源:{recorded_by},请甄别信息真实性】"
|
||||
- 测试文件: `tests/test_ai_apps/test_build_prompt_props.py`
|
||||
- **验证: 需求 7.4**
|
||||
|
||||
- [x] 10. 检查点 — 应用 3-7 Prompt 拼接完成
|
||||
- 确保所有测试通过,ask the user if questions arise.
|
||||
|
||||
- [x] 11. 实现错误降级与 Token 预算控制
|
||||
- [x] 11.1 在各应用 `build_prompt()` 中实现统一错误降级模式
|
||||
- 使用 `asyncio.gather(return_exceptions=True)` 捕获部分失败
|
||||
- 失败部分使用默认空值(空数组/空对象),附带提示文本
|
||||
- 确保 Prompt JSON 不含 `None`(Python)/ `null`(JSON),使用 `default=str` 处理 `datetime`/`Decimal`
|
||||
- _需求: 12.1, 12.2, 12.3, 12.4_
|
||||
|
||||
- [x] 11.2 实现 Prompt 字符数限制
|
||||
- 应用 3/4/5/6/7 的 system message `content` ≤ 8000 字符
|
||||
- 消费记录超 100 条时截断并标注"仅展示最近 100 条,共 {total} 条"
|
||||
- 备注超 50 条时截断并标注"仅展示最近 50 条备注"
|
||||
- 单个文本字段不超过 1000 字符
|
||||
- _需求: 14.1, 14.3, 14.4, 14.5_
|
||||
|
||||
- [x] 11.3 编写属性测试:错误降级(P14)+ Token 预算(P15)
|
||||
- **Property 14: 错误降级产生合法 Prompt** — 随机让 0-N 个数据获取函数失败,验证 `build_prompt` 返回合法 JSON,不含 `null`
|
||||
- **Property 15: 应用 3-7 Prompt Token 预算** — 生成大量数据,验证 system message ≤ 8000 字符
|
||||
- 测试文件: `tests/test_ai_apps/test_build_prompt_props.py`
|
||||
- **验证: 需求 12.1, 12.2, 12.4, 14.1**
|
||||
|
||||
|
||||
- [x] 12. 实现页面上下文文本化(page_context.py)
|
||||
- [x] 12.1 实现 `build_page_text(source_page, context_id, site_id, filters)` 框架
|
||||
- 根据 `source_page` 路由到对应内部函数(`_text_task_detail()`、`_text_customer_detail()` 等)
|
||||
- 输出结构化中文描述文本(分段标题 + 缩进),非 JSON
|
||||
- 输出限制 2000 字符,超出截断并标注
|
||||
- 未识别的 `source_page` 返回空字符串
|
||||
- 数据获取失败返回"页面上下文获取失败,请直接描述您的问题"
|
||||
- _需求: 8.1, 8.2, 8.8, 8.9, 8.11_
|
||||
|
||||
- [x] 12.2 实现详情类页面文本化函数
|
||||
- `_text_task_detail(task_id, site_id)` — 从 `biz.coach_tasks` + 会员信息 + 备注 + `ai_cache` 获取数据
|
||||
- `_text_customer_detail(member_id, site_id)` — 复用 `fetch_member_consumption_data()` + 维客线索
|
||||
- `_text_coach_detail(assistant_id, site_id)` — 复用 `fetch_assistant_info()` + 任务统计 + 备注
|
||||
- `_text_customer_service_records(member_id, site_id)` — 服务记录列表
|
||||
- `_text_task_list(task_id, site_id)` — 任务摘要 + 客户-助教关系
|
||||
- 不传入 `member_phone` 等敏感字段
|
||||
- _需求: 8.3, 8.4, 8.5, 8.12_
|
||||
|
||||
- [x] 12.3 实现看板类与其他页面文本化函数
|
||||
- `_text_board_finance(site_id, filters)` — 财务 DWS 汇总,默认"本月"
|
||||
- `_text_board_customer(site_id, filters)` — 客户排名 top 列表
|
||||
- `_text_board_coach(site_id, filters)` — 助教排名
|
||||
- `_text_performance(site_id, filters)` — `v_dws_assistant_salary_calc` 绩效数据
|
||||
- `_text_my_profile(site_id)` — 用户信息 + 助教绑定
|
||||
- 看板未传筛选参数时使用默认值
|
||||
- _需求: 8.6, 8.7, 8.10_
|
||||
|
||||
- [x] 12.4 编写属性测试:页面上下文长度(P10)+ 类型覆盖(P11)+ 敏感字段排除(P12)
|
||||
- **Property 10: 页面上下文输出长度约束** — 生成大量数据,验证 `build_page_text()` 输出 ≤ 2000 字符
|
||||
- **Property 11: 页面上下文覆盖所有页面类型** — 枚举 10 种类型,验证不抛异常且返回非空字符串
|
||||
- **Property 12: 页面上下文不泄露敏感字段** — 生成含手机号的数据,验证输出不含 `member_phone`
|
||||
- 测试文件: `tests/test_data_fetchers/test_page_context_props.py`
|
||||
- **验证: 需求 8.8, 8.1, 8.2, 8.12**
|
||||
|
||||
- [x] 12.5 编写单元测试:page_context 边界情况
|
||||
- 未识别 `contextType` 时返回空字符串
|
||||
- 看板无筛选参数时使用默认值
|
||||
- 数据获取失败时返回降级文本
|
||||
- `task-detail` 页面完整流程验证
|
||||
- 测试文件: `tests/test_data_fetchers/test_data_fetchers_unit.py`
|
||||
- _需求: 8.10, 8.11, 9.5_
|
||||
|
||||
- [x] 13. 实现应用 1 页面上下文集成(app1_chat.py)
|
||||
- [x] 13.1 改造 `app1_chat.py` 的 `_build_page_context()` 为 `async def`
|
||||
- 调用 `page_context.build_page_text(source_page, context_id, site_id, filters)` 获取页面上下文文本
|
||||
- `source_page` 映射到 `contextType`,`contextId` 传入 `context_id`
|
||||
- 看板类页面从请求参数提取筛选参数传入 `filters`
|
||||
- `contextType` 为空或未识别时跳过页面上下文注入
|
||||
- 将返回文本作为 system prompt 中的 `page_context` 字段注入
|
||||
- 确保 `biz_params.user_prompt_params`(`User_ID`、`Role`、`Nickname`)注入后仍正确存在
|
||||
- system prompt 总字符数控制在 4000 以内
|
||||
- _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 14.2_
|
||||
|
||||
- [x] 13.2 编写属性测试:biz_params 不变量(P13)+ App1 Token 预算(P16)
|
||||
- **Property 13: biz_params 注入后不变量** — 生成随机用户信息,验证 `user_prompt_params` 包含 `User_ID`、`Role`、`Nickname` 且值一致
|
||||
- **Property 16: 应用 1 System Prompt Token 预算** — 生成大量页面上下文,验证 system prompt ≤ 4000 字符
|
||||
- 测试文件: `tests/test_ai_apps/test_app1_props.py`
|
||||
- **验证: 需求 9.6, 13.2, 14.2**
|
||||
|
||||
- [x] 13.3 编写单元测试:App1 页面上下文集成
|
||||
- `contextType` 为空时跳过页面上下文
|
||||
- `task-detail` 页面上下文注入后 system prompt 结构正确
|
||||
- 看板筛选参数正确传递到 `build_page_text()`
|
||||
- 页面上下文获取失败时使用降级文本
|
||||
- 测试文件: `tests/test_ai_apps/test_ai_apps_unit.py`
|
||||
- _需求: 9.1, 9.4, 9.5, 12.5_
|
||||
|
||||
- [x] 14. 检查点 — 页面上下文与应用 1 集成完成
|
||||
- 确保所有测试通过,ask the user if questions arise.
|
||||
|
||||
- [x] 15. 前端看板筛选参数传递(小程序端)
|
||||
- [x] 15.1 修改看板页面跳转 chat 的参数传递
|
||||
- `board-finance` 页面跳转时传入 `timeDimension` + `areaFilter`
|
||||
- `board-customer` 页面跳转时传入 `dimension` + `typeFilter`
|
||||
- `board-coach` 页面跳转时传入 `dimension` + `projectFilter` + `timeDimension`
|
||||
- 将筛选参数编码为 chat 页面 URL 查询参数
|
||||
- _需求: 10.1, 10.2, 10.3, 10.4_
|
||||
|
||||
- [x] 15.2 后端从请求中提取看板筛选参数
|
||||
- 在 chat 消息接口中解析 URL 查询参数中的筛选参数
|
||||
- 将筛选参数传入 `build_page_text()` 的 `filters` 参数
|
||||
- _需求: 10.4, 9.4_
|
||||
|
||||
- [x] 16. 集成连线与最终验证
|
||||
- [x] 16.1 确保 dispatcher 调用链中 `build_prompt()` 的 `await` 正确
|
||||
- 消费事件链:App3 → App8 → App7 + App4 → App5,所有 `build_prompt` 调用加 `await`
|
||||
- 备注事件链:App6 → App8,`build_prompt` 调用加 `await`
|
||||
- 验证 `run()` 方法中 `build_prompt()` 调用已改为 `await`
|
||||
- _需求: 3.1, 4.1, 5.1, 6.1, 7.1, 11.6_
|
||||
|
||||
- [x] 16.2 编写单元测试:各应用完整流程
|
||||
- App3 完整流程:Mock 数据 → `build_prompt` → 验证 JSON 结构和内容
|
||||
- App4 并发数据获取:验证 `asyncio.gather` 调用
|
||||
- App5 `task_suggestion` 传递:验证 `context["app4_result"]` 正确传入
|
||||
- App7 主观信息标注:验证备注标注格式
|
||||
- RLS 隔离:验证 `get_etl_readonly_connection` 被调用且传入正确 `site_id`
|
||||
- 测试文件: `tests/test_ai_apps/test_ai_apps_unit.py`
|
||||
- _需求: 3.1, 4.1, 5.3, 7.4, 11.2, 13.1_
|
||||
|
||||
- [x] 17. 最终检查点 — 全部完成
|
||||
- 确保所有测试通过,ask the user if questions arise.
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的任务为可选,可跳过以加速 MVP
|
||||
- 每个任务引用具体需求编号,确保可追溯
|
||||
- 属性测试验证设计文档中的 17 个正确性属性(P1-P17)
|
||||
- 检查点确保增量验证
|
||||
- 测试文件组织:`tests/test_data_fetchers/`(数据获取层)、`tests/test_ai_apps/`(应用层)
|
||||
Reference in New Issue
Block a user