- .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>
28 KiB
设计文档 — NS2:AI Prompt 细化
概述
本设计将 P5-A 阶段交付的 6 个 AI 应用(应用 1/3/4/5/6/7)的 build_prompt() 占位骨架升级为完整实现。核心变更:
- 新建共享数据获取层
apps/backend/app/ai/data_fetchers/,封装 FDW 查询逻辑 - 完善应用 3/4/5/6/7 的
build_prompt()函数,从 TODO 占位升级为真实数据拼接 - 实现应用 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 │
└─────────────────────────────────────────────────┘
数据流
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 个月消费数据。
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 |
每条消费记录字段:
{
"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 共用。
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 专用)
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 结构:
{
"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 结构:
{
"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 结构:
{
"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 结构:
{
"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 获取客户备注,抽取为共享函数:
# 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 拼接层错误处理
# 各应用 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 次迭代。每个测试用注释标注对应的设计属性:
# 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 # 边界/示例单元测试