一、客服 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 生成的本地产物
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 自述项"
|
||
]
|
||
} |