93 lines
5.7 KiB
JSON
93 lines
5.7 KiB
JSON
{
|
||||
|
|
"evidence_id": "20260919-t7c-profile-tier",
|
|||
|
|
"generated_at": "2026-09-19T17:29:52+08:00",
|
|||
|
|
"session": "D1.6 §4.32 / D2.1 v6.19",
|
|||
|
|
"what": "D-11 落地:把「客户分层」接通(sys_user.customer_tier -> 画像快照 -> 客服作答),过程中定位并修掉真正的根因 —— 画像快照被残片覆盖 + JSON 列二次编码",
|
|||
|
|
"how": "连库取证 profile_snapshots 全版本 -> 对读两个快照写者的产出形状 -> 修 -> pytest / 冒烟 / 真 HTTP 复验",
|
|||
|
|
"corrected_diagnosis": {
|
|||
|
|
"previous": "D-11 原判:sys_user.customer_tier 全库 NULL(从未种过)+ 画像快照按设计排除该字段",
|
|||
|
|
"actual": "主因是两个写者互相覆盖:ProfileAssemblyService.rebuild_profile() 就地拼一个只含 PROFILE_OWNED_FIELDS + generated_at 的四字段残片,同样置 is_current=1,把权威快照里的 customer_tier / total_asset / behavior_score / assessment_* 全部抹掉。实测客户 9001 当前快照被覆盖到只剩 {risk_tags, generated_at, investor_type, investment_horizon, preferred_asset_class}(版本 2..16 全是这种形状)。「字段没种」与「写侧排除」两条也都成立,但都是次因。",
|
|||
|
|
"evidence_query": "SELECT version, is_current, snapshot FROM profile_snapshots WHERE customer_id=9001"
|
|||
|
|
},
|
|||
|
|
"fixes": [
|
|||
|
|
{
|
|||
|
|
"id": "D-12a",
|
|||
|
|
"file": "app/repository/profile_repository.py",
|
|||
|
|
"change": "_SQL_PROFILE 增加 LEFT JOIN sys_user 取 customer_tier(fin_customer_profile 无该列;用 LEFT JOIN 使缺账号行时画像仍能生成)"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "D-12b",
|
|||
|
|
"file": "app/service/profile_generation_service.py",
|
|||
|
|
"change": "build_snapshot 纳入 customer_tier;REQUIRED_SNAPSHOT_FIELDS 取消 customer_tier 例外(写侧与读取侧白名单必须等集)"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "D-12c",
|
|||
|
|
"file": "app/service/profile_assembly_service.py",
|
|||
|
|
"change": "rebuild_profile 改走 build_snapshot()(唯一实现),不再自拼残片;字段所有权约束的是「写 fin_customer_profile 表」,不是快照字段集合"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "D-13",
|
|||
|
|
"file": "app/service/profile_assembly_service.py",
|
|||
|
|
"change": "新增 PROFILE_JSON_FIELDS:preferred_asset_class / risk_tags 是 JSON 列,不再套 _as_text(会把列表序列化成字符串再被 JSON 列二次编码);risk_tags 改存数组"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "D-13b",
|
|||
|
|
"file": "app/service/profile_generation_service.py",
|
|||
|
|
"change": "_as_list 兜底 JSON 字面量 \"null\"(当作空),并保留 json.loads 已脱引号的值"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "D-14",
|
|||
|
|
"file": "tools/seed_profile_demo.py",
|
|||
|
|
"change": "① 客户分层写进权威列 sys_user.customer_tier;② 新增 user_facts 种子(preference:horizon / preference:asset_class)—— 这两个字段由重建服务独占、无事实即清空,写主表列会被下一次重建抹掉;③ 快照改由 ProfileGenerationService.generate() 生成,不再手拼"
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"before_after": {
|
|||
|
|
"snapshot_9001_before": {
|
|||
|
|
"investor_type": "C1",
|
|||
|
|
"risk_tags": null,
|
|||
|
|
"generated_at": "...",
|
|||
|
|
"investment_horizon": null,
|
|||
|
|
"preferred_asset_class": null
|
|||
|
|
},
|
|||
|
|
"snapshot_9001_after": {
|
|||
|
|
"risk_tags": [],
|
|||
|
|
"total_asset": "60000.00",
|
|||
|
|
"customer_tier": "gold",
|
|||
|
|
"investor_type": "C1",
|
|||
|
|
"behavior_score": 22,
|
|||
|
|
"last_active_at": "2026-09-16T09:27:34+00:00",
|
|||
|
|
"trading_frequency": "low",
|
|||
|
|
"assessment_expired": false,
|
|||
|
|
"investment_horizon": "short_term",
|
|||
|
|
"preferred_asset_class": [
|
|||
|
|
"money_fund"
|
|||
|
|
],
|
|||
|
|
"assessment_valid_until": "2027-07-16T09:27:34+00:00"
|
|||
|
|
},
|
|||
|
|
"answer_before": "您的风险测评等级是 保守型(C1)。",
|
|||
|
|
"answer_after": "您的风险测评等级是 保守型(C1)。 / 投资期限偏好:短期(1 年以内)。 / 交易频率:较低。 / 偏好资产类别:货币基金。 / 客户分层:金卡。"
|
|||
|
|
},
|
|||
|
|
"authority_layout": {
|
|||
|
|
"customer_tier": "sys_user.customer_tier(权威列,本次补种 6 行)",
|
|||
|
|
"user_facts": "preference:horizon / preference:asset_class(12 行 = 6 客户 x 2)",
|
|||
|
|
"fin_customer_profile": "investor_type / investment_horizon / preferred_asset_class / trading_frequency / total_asset / behavior_score / risk_tags",
|
|||
|
|
"note": "investment_horizon / preferred_asset_class 由 ProfileAssemblyService 独占,无 user_facts 即清空 —— 所以种子的权威来源是 user_facts,不是主表列"
|
|||
|
|
},
|
|||
|
|
"gates": {
|
|||
|
|
"pytest": "1746 passed / 0 failed / 2 skipped(较上一轮 +4 条新用例)",
|
|||
|
|
"ruff": "22(= 基线)",
|
|||
|
|
"mypy": "3(= 基线)",
|
|||
|
|
"smoke": "tools/e2e_smoke_test.py --read-only -> 31/31 全通过",
|
|||
|
|
"http_probe": "_eval_harness/http_probe.py -> 11/11 全 succeeded"
|
|||
|
|
},
|
|||
|
|
"tests_added": [
|
|||
|
|
"tests/unit/service/test_profile_generation_service.py::test_snapshot_carries_customer_tier_from_its_authority",
|
|||
|
|
"tests/unit/service/test_profile_generation_service.py::test_json_null_literal_is_not_a_preference",
|
|||
|
|
"tests/unit/service/test_profile_generation_service.py::test_json_string_value_loses_its_serialization_quotes",
|
|||
|
|
"tests/integration/test_profile_snapshot_completeness_mysql.py::test_rebuild_writes_a_complete_snapshot_not_a_partial_fragment(含 JSON 列二次编码断言)"
|
|||
|
|
],
|
|||
|
|
"open_items": [
|
|||
|
|
"分层档位与 total_asset 不一致:6 行里 5 行的 tier 与公开门槛(金卡 50 万+/白金 200 万+/钻石 600 万+/尊享 1000 万+)对不上;但 agent 只读 sys_user.customer_tier、不用资产推,因此回答本身不自相矛盾 —— 口径待定",
|
|||
|
|
"risk_tags 是「自述事实重算」字段:种子写进列的标签在首次记忆重建后会被清空(9001 实测由 [conservative] 变 []);要演示标签需种 user_facts 自述项"
|
|||
|
|
]
|
|||
|
|
}
|