Files
group_fqcd_jr/docs/evidence/20260919-t7c-profile-tier.json
T
张胜宇 5d0becb67d 客服 Agent 重构收口:五出口决策链 + 知识库档位隔离 + 前端入参边界(答辩演示版本)
一、客服 Agent 智能增强(正面回应"不智能、动不动就转人工")
- 决策链由 2 个出口扩到 5 个:E1 澄清 / E2 计算型 / E3 知识直返 / E4 证据约束生成 / E5 分级回退
- 转人工从"默认动作"降为最后一档 E5c,只保留 4 类白名单:
  P0 反诈 / P1 账户与个人数据 / P2 写操作与争议 / 用户明确要求人工
- 46 条金标实测(修复前 → 修复后):
  转人工率 43.5% → 10.9%;出口准确率 45.7% → 100%;事实正确率 69.6% → 100%
  禁忌违反 1 → 0;档位越权 / 无出处数字 / 误拒 四项零容忍全 0
- 安全不变量 INV-1~INV-5;零容忍规则未删,改的是挂载点
  (输出侧字面黑名单 → 检索层档位隔离 + 判定层合规词表 + 输出守护)

二、知识库:档位单点化与物理隔离
- 新增 app/core/knowledge_tier.py 作为档位规则唯一落点(G-03),
  knowledge_contracts.py 原定义块改为显式再导出(X as X,非副本)
- 档位过滤由 bool 默认值(fail-open)改为 tiers 必填集合(缺参即 TypeError)
- Milvus 侧四集合按 visibility 分区键物理隔离;双 schema 收敛为一套
- 新增 app/core/actor.py:访客三元组与匿名判定的唯一构造/判定点(G-01/G-01b)
- 新增 app/core/fund_fee_rules.py:费率计算纯函数

三、前端入参边界对齐(本轮 W11 新修,4 处"校验宽于存储")
- message 加 max_length=8000(与浮窗 widget.js 的 maxlength 一致)
- session_id 加 1—64;idempotency_key 上限 128 → 64(对齐列宽 String(64))
- feedback_type 加 max_length=32(对齐列宽 String(32))
- 8 条路径参数补 min_length=1 + max_length=64 + 字符集正则
  ({session_id} / {run_id} / {handover_id})
- 改前超限值会落到 MySQL 才失败(500);改后一律 422 AGENT_INPUT_INVALID + 字段级定位
- 新增 tests/unit/api/test_frontend_boundaries.py(33 例),含"端点表 ↔ OpenAPI 全量对照"

四、投顾模块整体清除(D4.4 / D4.5)
- 删除投顾相关 controller / schema / model / repository / service 及门户页面
- tools/portal_api_check.py 同步作废 AD003/AD005/AD011/A047 四条用例与 advisor_t 登录
  (端点与账号均已不存在,此前稳定报 3 条假红)

五、验证(提交前实测)
- pytest -q:1856 passed / 2 skipped / 0 failed
- ruff check app tools tests:19(= 基线);mypy app:2(= 基线)
- 前端接口契约体检 portal_api_check.py:38 项,通过 34,失败 0,跳过 4
- 全链路冒烟 e2e_smoke_test.py --read-only:31/31
- HTTP 全链路探针 http_probe.py:11/11 succeeded
- 跨文档一致性 _consistency.py:GATE PASS
- 真机边界复验 12 条:12/12 符合预期

六、纪律与文档
- 可改文件白名单 A-09(docs/46)与底座会签申请单 A-10(docs/47,组 1—组 4 全部受理)
- 零 DDL:未新增/修改任何表结构,89 张业务表与基线一致
- 证据留痕:docs/evidence/**(含 46 条金标 score、快照、清除与重建记录)
- 未提交(刻意排除,见提交说明):仓库内 客服agent/ 与 开发文档/ 是 2026-09-16 前的
  过期副本(Todolist 440 行 vs 权威 D2.1 1167 行),权威正本在仓库外;
  _chunks_report.txt 是 tools/build_knowledge_chunks.py 生成的本地产物
2026-09-20 14:33:30 +08:00

93 lines
5.7 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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 自述项"
]
}