Files
Neo 70324d8542 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>
2026-04-06 00:02:37 +08:00

28 KiB
Raw Permalink Blame History

设计文档 — NS2AI Prompt 细化

概述

本设计将 P5-A 阶段交付的 6 个 AI 应用(应用 1/3/4/5/6/7build_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 │
└─────────────────────────────────────────────────┘

数据流

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.pyget_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_monthshire_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: 实体 IDcontextId
        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.pyassistant_data.py 的数据获取函数

2. 应用层 Prompt 拼接改造

2.1 应用 3app3_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 应用 4app4_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 应用 5app5_tactics.py)— 话术参考

复用应用 4 的数据获取逻辑,额外接收 context["app4_result"] 作为 task_suggestion

变更点:

  • build_prompt() 改为 async def
  • 数据获取逻辑与应用 4 一致
  • task_suggestioncontext["app4_result"] 获取,缺失时设为空对象

2.4 应用 6app6_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_notesbiz.notes 获取,单条截断 500 字符,最多 50 条
  • 空备注时 all_notes 设为空数组

2.5 应用 7app7_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 应用 1app1_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 参数映射到 contextTypepage_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_idmember_idfetch_member_consumption_data() 返回的字典必须包含所有必需键(consumption_recordsmember_cardscard_balance_totalstored_value_balance_totalexpected_visit_datedays_since_last_visitmember_nickname),且 consumption_records 中每条记录包含 settle_datesettle_typeitems_sumtable_charge_moneyassistant_pd_moneyassistant_cx_moneygoods_moneyroom_nameduration_minutesassistant_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_moneyassistant_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_timemember_nicknamemain_datareference 顶层键
  • App4 的 build_prompt 返回的 JSON 必须包含 current_timeassistant_infoservice_historycustomer_datareference 顶层键
  • App5 的 build_prompt 返回的 JSON 必须包含 App4 的所有键加 task_suggestion
  • App6 的 build_prompt 返回的 JSON 必须包含 current_timecurrent_notereference 顶层键
  • App7 的 build_prompt 返回的 JSON 必须包含 current_timemember_idmember_nicknameobjective_datasubjective_datareference 顶层键

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-detailcustomer-detailcoach-detailboard-financeboard-customerboard-coachperformancemy-profiletask-listcustomer-service-recordsbuild_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(字符串)、RoleNickname 三个键,且值与输入的用户信息一致。

Validates: Requirements 9.6, 13.2

Property 14: 错误降级产生合法 Prompt

对于任意 数据获取函数失败的组合0 到全部失败),build_prompt 仍必须返回可被 json.loads 解析的合法 JSON不包含 Python NoneJSON 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 处理 datetimeDecimal 等非标准类型

测试策略

属性测试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          # 边界/示例单元测试