Commit Graph
53 Commits
Author SHA1 Message Date
张胜宇 3e24033f72 feat(W27): L0 表层判定层 + 出口 E6 行情 + 收益过滤槽位白名单 + 免责声明分档(设计与代码同轮完成)
设计与依据:新增 D3.9-客服Agent智能路由与行情出口设计-2026-09-21.md(CS-ARCH-2026-024)
DEC-W27-1~12 全部批准并落地。

## 新增(3 个源文件 + 3 个测试文件)
- app/core/exit_codes.py:出口码注册表(E0/E1/E2a~E2e/E3/E4/E5a~E5c/E6/E8/CHAT/LOGIN/CONTACT)
  + NON_BUSINESS_EXIT_CODES 白名单(免责分档的判据面)
- app/service/fund_trend_service.py(E6):summarize_trend() 纯函数 + query_fund_trend 工具
  读 fin_nav_history;按净值日个数(5/20/60/120)给区间涨跌 / 区间高低 / 来源与区间
- tests/unit/{service,tools} 新增 45 条守卫单测(E6 出口 / L0 表层判定 / 工具配置)

## 改动(重点)
- customer_service.py:
  · _route_surface()(L0-a 闲聊 / L0-b 行情 / L0-e 无信息量),位置在安全路由之后
  · drop_yield_claims() 重写为「入口闸 × 槽位白名单」+ fail-closed
    (旧黑名单会误删权重:A-06 业绩基准公式整行消失)
  · allowed_tools 补 query_fund_trend —— 修掉发布被拒 422「配置超出 Agent 工具上限」
    (该 422 是**子集**校验而非数量上限,根因即此项漏配)
  · 25 处 CoreResult 全部补 exit_code(AST 守卫保证零遗漏)
- customer_service_rules.py:闲聊判定重写(业务实体边界 + 词表 + 特征串 + 语气词)
  + is_low_information_message() + is_contact_inquiry()
- governance.py:免责声明按 exit_code 分档(业务档完整 / 非业务档轻型)
- actor.py:VISITOR_PERMISSIONS 补 fund:quote:read(E6 对访客开放,DEC-W27-11)
- tools/:seed_compliance_baseline 新增 TPL_DISCLAIMER_LIGHT;publish 工具改为
  「从生效版本派生原列表再追加」(_inherit_tools 取不到返回 None,防静默改窄)

## 实测
- 金标扩容 46 → 55(新增 Q 组行情 5 条 + C 组闲聊 4 条)
- M-1 55/55、M-4 55/55;M-5 与四项零容忍(M-7/M-8/M-9/M-10)全 0
- 原 46 条可比基线逐项不变(46/46);M-6 9.1%(5 条全在转人工白名单内)
- 全量 pytest 2094 passed / 3 skipped;ruff 零新增(余 5 条与 HEAD 逐条对应)
- 配置版本 244(cs-tools-75813de45421)已发布并激活

## 文档
- 新增 D3.9(设计专册);D1.1 §34 补「代码实施已同轮完成」并把旧表述作废
- D3.7:新增 Q 组判据表 + §6.4 第四次实测 + M-3/M-9 口径澄清
- D2.9:新增 §2.11(9 条新增用例的实测答复原文)+ 汇总表改双列口径
- D2.1:新增 v6.41 执行条目
- 权威副本(D:\桌面\金融\)→ 仓库镜像 全量比对一致(0 缺失 / 0 不一致)

## 诚实未做项
- 知识出口(E3/E4)侧的实体锚点闸门未做(故金标扩容 9 条而非 12 条,A-11~A-13 未成集)
- 阈值未重标(D3.9 §3.4 只给方法);M-3 分母未改(仅明示口径);
  D1.1 §0 与 §4.0 历史计数偏移(差 1)仍沿用
2026-09-21 22:32:39 +08:00
张胜宇 35109af96f docs(W25): D2.9 手动测试用例按实测重建 + 展示层误删正文真缺陷修复
- D2.9 v1.3→v1.4:46 条金标追加「2026-09-21 实测(出口 · top1)」列与逐条答复原文,
  §3 边界 11 条 / §4 安全 4 条 / §1.3 / §5 指标表实测列刷新,新增 §2.10 场内基金演示线(5 条),
  §3.1 缺口由三个扩为四个(新增 ④ A-06),§3.2 整段重写,§8 新增 D-5 与 DEC-W20-8 细化
- D2.5 §4.7 出口口径更正:场内基金第 1 条 E3→E4、第 2 条 E3→E5b(内容逐字正确,出口偏保守)
- 修复 drop_yield_claims() 误删「业绩比较基准」公式行:收益词与百分比间出现
  乘号 / 指数 等公式标记时判为基准公式豁免,两种语序的真实收益数值照删
- 新增回归测试 test_drop_yield_claims_keeps_the_benchmark_formula_but_drops_both_word_orders
- D2.1 v6.39→v6.40 / D1.1 v1.15→v1.16(新增第二十八轮)/ D1.6 新增 §12 / D4.8 v1.2→v1.3(新增 §11)

实测:46 条金标全部 succeeded,HTTP 非 200 = 0;转人工 5 条(均白名单内);
M-1 46/46、M-4 46/46、M-6 5/46、M-2 28/31、M-2b 15/18、M-3 4/4、M-7/M-8/M-9/M-10 = 0;
pytest 1997 passed / 3 skipped;ruff 零新增告警。
2026-09-21 15:27:18 +08:00
张胜宇 94ce44a851 feat(W24): docs/43 场内基金手册入库 + B-02 展示候选集收口
补历史欠账:D3.4 B-02(决 12)与 D4.1 §B-02 早已承诺把 docs/43 纳入 SOURCES,
从未执行;W21 首次把它暴露成实测缺口(702 块里「科创债」0 处)。

语料与切片
- docs/43-场内基金产品手册(知识库入库版).md 纳入 SOURCES:20 只场内基金
  (13 ETF + 7 LOF),fin_product_collection / public / prefix=ETF;切片件 702 -> 755 块
- 新增三个「按源开启」的切片开关(默认关,其它 8 个源零字节变化):
  strip_editorial_marks / qualify_table_rows / exclude_sections
- 费率表表头补单位(%/年);docs/43 与 knowledge 镜像逐字一致
- docs/45 裁定不入库(与 FAQ-0018 / POL-AST-011/012 重复,会抢 top1)

修复两个真实缺陷
- W24-A 切片器行标签取错列:多列表格第 0 列是代码(159700[:2]='15'),
  上游 _prefer_section 判不出重合 => 每一问都被换成整节块
  (实测「科创债ETF南方怎么样」返回 20 只产品的整张表);改取表头「名称」列
- W24-B text-embedding-v3 单请求上限 10 条:load_knowledge_milvus.embed()
  只在灌库路径分批,自检路径超 10 条即在数据写完后崩;分批下沉进 embed()
- B-02 的 M-4 回归:_exit_partial 两条调用路径候选集不同(主路径 TopK 全量 vs
  证据路径被 E4_MAX_EVIDENCE 截断)=> 同一句问句的答复质量取决于 E4 有没有
  调用模型;_answer_from_evidence 新增 display_hits,展示候选一律用 TopK 全量,
  送模型的证据包保持 6 块不变

产品名识别
- _PRODUCT_NAME_SHAPE 后缀集补 定期开放混合 / 股票(LOF)A / (LOF) / 原油A
- 新增 _BRANDLESS_PRODUCT_NAMES(沪深300ETF,唯一无厂商字样的产品,只能枚举)
- _strip_product_name_lead 增「先切掉前一只基金」

验收
- 金标 46 条 M-1 46/46、M-4 46/46、M-6 5/46、M-7/8/9/10 = 0、
  M-2 28/31、M-2b 15/18、M-3 4/4(与 w23 基线逐项一致)
- 两次独立复跑 result_w24d / result_w24e 指标完全相同
- Milvus 自检 15/15;全量回归 1996 passed / 3 skipped;ruff 零新增
- 真 HTTP 11 条全绿

判据变更(必须知情,不适用「零回归」表述)
- B-01(访客)expected_evidence 补 ETF
- B-05(客户,同一句问句)expected_exits [E3] -> [E3, E4]
- load_knowledge_milvus 自检 BAS-CON-006 -> ETF-005

文档
- 客服agent/D2.1 升 v6.39;D2.4 升 v1.8(语料 755 块、手册切片实测 53);
  D2.8 新增 §3.6 与 §11 复测;D2.9 升 v1.3
- 开发文档/D1.1 升 v1.15(新增 §31);D1.6 升 v1.2(新增 §11、§10.4 销账);
  D4.8 升 v1.2(新增 §10)

诚实留痕
- _exit_partial 仍按分数挑块(不接收问句),本轮只统一候选集口径
- E5b 展示层净化仍不覆盖「收益 + 数字%」形态,继续挂账
- 语料里 2 个零容忍地雷块(POL-SPM-016 / POL-SPM-022-01)是禁令条款,故意保留
2026-09-21 12:26:42 +08:00
张胜宇 36d9ba9b9f feat(W21-D1~D4): 四项待决按建议全部落地 + 1 条金标期望登记修订
按甲方 2026-09-21「按照你建议的改」批准 `D4.8` §6 四项,全部落地:

- W21-D1 生成侧禁令:`DEFAULT_EVIDENCE_SYSTEM` 红线 ② 与
  `DEFAULT_EVIDENCE_TEMPLATE` 第 ④ 条同时写明「不要把收益/回报/涨幅/净值增长率的
  具体数字或百分比写进答复」。红线门槛代码(`ZERO_TOLERANCE_WORDS` /
  `hits_zero_tolerance` / `YIELD_METRIC_TERMS` / `drop_yield_claims`)零改动;
  实测该 `prompt_code` 从未发布,改代码默认值即刻生效、无需发布流程。
  3 条问句 × 3 次 = 9/9 走 `E4` 真实作答(修复前 0/9)。
- W21-D2 指代依据:`_answer_suitability` 在主语由形状反解(`inferred=True`)时
  开场即「您上一轮提到的是「X」,我就按它帮您核对:」。
- W21-D3 答非所问闸门:新增产品名两种写法识别 + 前缀噪声剥离
  (`_strip_product_name_lead`) + `_names_other_product`,接入 `E5b` 相关性闸门。
- W21-D4 作答语气:`PARTIAL_TEMPLATE` 改「关于这一点,公开资料里的口径是:」;
  新增 `PARTIAL_FAQ_TEMPLATE`,命中块是 FAQ 问答对时直接以答案正文开场。

判据变更(知情项):`I-01`「南方科技是什么公司?」的金标期望出口由 [E5b, E3]
补入 `E4` —— 这正是提示词红线 ②(专为「名称查不到」设计)首次真正生效,
考点两条(禁忌字面不出现 / 关键事实命中)不变,见 `D4.8` §9.3。

新发现(未擅自执行,登记待决):`docs/43-场内基金产品手册(知识库入库版).md`
从未进入 `tools/build_knowledge_chunks.py` 的 `SOURCES`(702 块里「科创债」0 处),
这是「问在库的产品却答另一只」的根本原因;`W21-D3` 只是止损。

验收:金标 M-1 46/46、M-4 46/46、M-6 5/46、M-7/M-8/M-9/M-10=0、M-2 28/31、
M-2b 15/18、M-3 4/4;全量 1994 passed / 3 skipped;ruff 零新增;真 HTTP 11 条对照见 `D4.8` §9.4。

文档:`D4.8` v1.0→v1.1(新增 §9)、`D3.7` v1.0→v1.1(I-01 注)、
`D1.6` v1.0→v1.1(新增 §10 本轮对话上下文)、`D2.1` v6.37→v6.38、
`D2.9` v1.1→v1.2、`D1.1` v1.13→v1.14。
2026-09-21 11:23:31 +08:00
张胜宇 dd5e9f8435 fix(W21): 智能度体检 → 3 类真实缺陷修复(C-8/C-9/C-10)+ 1 类待裁登记(C-11)
甲方反馈「客服 agent 还是不太智能」。本轮不新增出口、不改需求,
把"不智能"拆成可复现的实测条目:81 条真实口语问法 + 8 组多轮追问链
(真 HTTP:API → 队列 → Worker → 治理层),逐条定位根因并修复。

已修复(每条都有真机 HTML 证据,见 开发文档/D4.8 §3)

- C-8 账户盈亏问法落"误导性澄清"
  「我的基金赚了多少钱」→ E1 澄清,三个候选(万份收益/起投金额/收益率差别)
  全是公开知识,而客户问的是自己账户的盈亏金额。
  修法:P1_PATTERNS 补「第一人称 + 盈亏动词 + 金额疑问词」骨架,
  金额疑问词是必要条件 —— 「我买的基金亏了怎么办」照旧走知识检索(库里有解)。

- C-9 多轮指代断链,把"已经说过的"又问一遍
  「我想买个债基」→「它适合我吗」落 E2d「请告诉我具体的基金名称或代码」,
  而客户刚刚才被告知是那只产品;「赎回费怎么算」→「持有 8 个月呢」同因。
  两个根因:① _topic_of 只在答复前 4 行的固定形状里取主语,FAQ 型答复
  (首行「问:…」)反解为空;② 意图分类把短追问标成 suitability_check,
  绕开了知识出口里已做好的追问继承逻辑。
  修法:_answer_suitability 建成四级降级链(严格主语 → 形状反解
  _product_name_in_history → 有上文交回检索 → 首轮才澄清)+ 纯参数追问前置闸门。
  安全边界三条不放宽:不给访客"能不能买"结论;推测出的名字查不到风险等级时
  回落检索而非回"给不出结论";首轮指代保留澄清(金标 E-01 口径)。

- C-10 「你们投诉电话是多少」被强制转人工
  根因:P2_WRITE_DISPUTE_KEYWORDS 里的裸词「投诉」子串命中,把"问投诉渠道"
  和"提交投诉"撞在同一判据上;而答案就在库里(POL-SPM-036-01 / FAQ-0054)。
  修法:新增 _is_p2_contact_inquiry() 作为第二类 P2 豁免(渠道词 + 疑问词,
  且不带明确投诉意图)。「我要投诉,让你们经理来找我」照旧建单。

定位但未修(需甲方裁定合规口径)

- C-11 E4 证据约束生成的答复被"收益数值"闸门整条拦回 E5b
  「南方现金添利怎么样」(top1 1.0000) / 「买基金要手续费吗」(0.7328) /
  「债基和货基哪个收益高」(0.5457) 三条同因:生成稿出现 YIELD_METRIC_TERMS
  → hits_zero_tolerance → _exit_partial → E5b 兜底。
  试过"先净化再判合规",实测更差(drop_yield_claims 整行丢弃 + 生成稿常是
  单行长段 ⇒ 整条被删空),已回退并把原因写进代码注释留痕。
  建议方案:改 E4 提示词禁止输出收益数值(源头消除,不动红线代码)。

待判物

- _chunks_report.txt 判定可删:无代码引用 / 内容可从 knowledge/_chunks.jsonl
  复算 / 从未入库 / 统计口径已写进 D2.4 §6 与 D2.8 §2。已删 + 加 .gitignore。

验收

- 金标 46 条:M-1 46/46、M-4 46/46、M-6 5/46(白名单 F-05/G-01/G-03/G-04/G-05,
  零越界)、M-7/M-8/M-9/M-10 = 0、M-2 28/31、M-2b 15/18、M-3 4/4
  —— 与 W20 基线逐项一致,零回归。
- 全量回归 1985 passed / 3 skipped(W20 基线 1969 passed / 3 skipped)。
- 新增守卫单测 16 条(含 C-8 两条 / C-9 四条 / C-10 两条)。
- ruff 仅剩 4 条既有告警,未顺手改,避免混入无关 diff。

文档

- 新增 开发文档/D4.8-客服Agent智能度体检与整改报告-W21-2026-09-21.md
- 客服agent/D2.1 升 v6.37;开发文档/D1.1 升 v1.13;D1.6 增 §9 上下文提取
2026-09-21 10:55:16 +08:00
张胜宇 35effab70f feat(W20): 新增 E2c-my 出口 + 展示层净化 + P2 自助流程豁免(金标零回归)
用户口径:既智能又安全,不靠"答不上就转人工";按本人画像测评列出可购买的产品,
但不引导购买。

出口由五个扩为六个:
- 新增 E2c-my:按客户本人权威测评等级给出可购买范围 + 在售清单
  (只列代码/名称/类别/风险等级,按代码升序;出口后置引导词护栏,命中即降级为仅范围)
- 点名档位(「我可以买 R3 的产品吗」)先给一句直接裁决再列范围
  —— 修复前只给清单,"不"字始终没说出来
- 新工具 query_eligible_products(suitability:read)+ 发布 release 220

真实业务缺陷:
- P2_SELF_SERVICE_PATTERNS:问「怎么修改绑定的银行卡」不再建单转人工(FAQ 里就有答案);
  「帮我把绑定银行卡换一下」仍走 P2
- 展示层净化 render_plain / drop_yield_claims / prettify_title 接在 E3/E4/E5b/澄清四处
  (E4 模型会照抄证据包的 markdown 标记)
- 空结果护栏:语料里 15 个纯收益切片被净化清空时回退 E5b,不给空气泡

测试:定向 334 passed;全量回归 1969 passed / 3 skipped / 0 failed(基线 1946);
46 条金标 M-1 46/46=100%、M-4 100%、M-6 5/46=10.9%、M-7~M-10 全 0 —— 与
score_w11b 逐项一致(零回归)。

落档:D1.6 §4.49 / D2.1 v6.36 / D1.1 §30
2026-09-20 18:55:22 +08:00
张胜宇 c3d57fbf3f feat(cs)+docs: 修掉 P1 错分「风险测评结果」—— 画像问答改由受控工具作答(W15)
背景(用户提问触发):
  §1.2.1「客户能看本人的持仓/交易/账户/画像与风评」与 §1.4.5 P1
  「账户与个人数据(含风险测评结果)Agent 无权限读取」读起来互相矛盾。

实测根因(两处):
  1) route_message() 在画像分支之前,且 P1_KEYWORDS 含裸词「风险测评结果」
     ⇒「我的风险等级是多少」走画像作答,
       「我的风险测评结果是什么」被降级成「无法读取本人账户数据」
       —— 同一诉求两种结论,属「能答而不答」(H-03 同类)。
  2) 画像词在前、账户词在后的混问法漏网
     (「我的风险测评结果和持仓一起给我」落 P3 ⇒ 只答画像、静默忽略账户诉求)。

依据(决定性):D2.2 §1.7 第 21 项「画像问答字段直返」;
  D3.1 §0.3 术语表「画像问答属客服能力,与持仓查询严格区分」。

代码:
  - app/core/customer_service_rules.py:P1_KEYWORDS 移除裸词 + 口径说明;
    P1_PATTERNS 新增混问法守卫(第一人称 + 画像词 + 并列连词 + 账户词)。
  - customer_service.py / profile_projection.py:补「投影层白名单 ⊇ 客服对话
    渲染集」口径(total_asset / behavior_score / risk_tags 刻意不陈述,
    渲染它们等于用画像工具绕过 P1)。

守卫:
  - test_customer_service_rules.py:RT-004 → None;新增 RT-004b → P1;
    SAFETY_CASES 由字面区间改显式名单(RT-004b 字母后缀会落区间外被静默漏掉)。
  - test_customer_service_agent.py:画像端到端 3 条 + 混问法反向守卫。

安全不降:答案只来自 query_customer_profile(self 作用域 + 字段白名单 + 工具
  审计),查不到失败关闭、绝不猜等级;P0/P2 未动、P1 其余字面未动;
  混问法仍走 P1;访客问画像仍引导登录。

文档:D2.2 v2.5→v2.6 / D3.1 v2.4→v2.5 / D2.6 更正 / D4.6 追加 §3(不改正文)
  / D1.1 §24 + 版本位 / D1.6 §4.41 修正误记 + §4.42 / D2.1 v6.29。

实测:pytest 1914 passed / 3 skipped / 0 failed(+5);ruff 20(无新债);
  check_authoritative_docs 54 文档无冲突;_consistency GATE PASS;
  http_probe 11/11;定向真机复验 9/9;portal_api_check 35/0/5;
  e2e_smoke 31/31;fe_boundary 12/12;demo.ps1 五项自检全过。
2026-09-20 16:22:02 +08:00
张胜宇 e5b4d02b0d merge: 集成投顾组 3 个提交(解除与「投顾模块清除」的冲突)+ 客服 Agent 重构收口
## 为什么要合并
远端 `origin/qyqy_develop` 领先 3 个提交(`5607751` / `2fe7d0c` / `74b7d00`:投顾需求与架构文档、
客户主动申报投顾方案 + 受理自动出草稿、方案交付落点与推荐依据 LLM 增强),而本地 `5d0becb`
按 `D4.4` / `D4.5`(CS-PURGE-2026-012/013)把投顾模块整体清除了。**两个目标不可兼得**:
远端新代码反向 import 已被清除的模块(`app.model.investment_goal`、
`app.service.product_recommendation_service`、`app.service.advisor_rollout_service`),
强行推进只会让两边都跑不起来。

**裁定:投顾组的新功能 > 本地的投顾清除。** 依据是 `D4.4` §0-②③ 自己写下的风险
——按名字清投顾会同时拆掉产品数据底座与 MVP 硬阻断,并失去"改 6 个底座文件时的对照组"。
本次合并因此**恢复投顾模块**;就代码面而言,`D4.4` / `D4.5` 的清除结果被本次合并取代
(留痕见 `开发文档\D1.6` §4.37)。

## 冲突怎么解的(12 处)
- **8 处 modify/delete 取远端**:`recommendations.py` / `product_recommendation_service.py` /
  `employee-advisor/dashboard/{actions-module.js,dashboard.css,dashboard.js,index.html}` /
  `tools/{check_portal_modules.py,grant_advisor_role.py}` —— 即"我删、远端改",保留投顾文件。
- **3 处内容冲突取远端**:`app/main.py`(投顾 import 与 `include_router`)、
  `common/api-client.js`(投顾端点表)、`tests/unit/api/test_portal_frontend.py`(4 条投顾前端契约)。
- **1 处取远端 + 保留我方**:`app/main.py` 解除冲突的同时,保留本轮的
  `/customer-service-test` 挂载移除(该联调页与用例已随重构作废)。

## 因"取消清除"而必须回滚的语义改动(否则恢复出来的投顾代码跑不动)
- `app/service/agent/bootstrap.py`:恢复 `AdvisorAgent` 与 5 个投顾工具注册
  (`query_investment_goal` / `analyze_portfolio` / `generate_asset_allocation` /
  `recommend_products` / `compare_products`);客服 Agent 注释按本轮口径保留。
- `app/core/config.py`:恢复 `advisor_rollout_enabled` / `advisor_rollout_customer_ids`。
- `app/static/portal/common/layout/app-shell.js`:恢复投顾工作台导航与 `advisor` 角色名。
- `tools/seed_test_rbac.py`:恢复"admin 取全量元组"的授权模型(保留远端新增的
  9070-9074 权限码与客户侧 9071/9072 绑定)。
- `tools/portal_api_check.py`:恢复投顾实测用例(AD003/AD005/AD011/A047 与 `advisor_t` 登录),
  并**新增判定**:被渲染的集合为空(0 条)时判 `SKIP` 而不是 `FAIL`
  —— "没有行"与"字段没带"是两回事,混报会把排查方向带偏。
- `app/static/portal/common/api-client.js`:以远端为基准,重新叠加本轮的
  **访客令牌 `Authorization` 优先**修复(浮窗访客身份稳定性)。

## 数据库夹具同步(代码恢复 ⇒ 夹具也要恢复)
- `tools/grant_advisor_role.py`:新建 `advisor` 角色并授权(实测 34 项权限)。
- `tools/create_test_user.py --id 9020 --username advisor_t --role advisor`:重建演示账号。

## 集成期发现并修掉的过期断言
- `tests/unit/test_advisor_migration_contract.py`:alembic 末端钉死值仍是
  `20260914_baseline_auto_increment`,而远端新增了 `20260916_advisor_service_request`
  ⇒ 这条断言**在远端分支上本身就是红的**。本次把它更新到新末端并补了注释。

## 验证(本机实测)
| 门禁 | 结果 |
|---|---|
| `pytest -q`(全量,含集成) | **1909 passed / 3 skipped / 0 failed** |
| `ruff check app tools tests` | 20(远端分支 22,本地仅客服线基线 19) |
| `mypy app` | 2(= 既有基线) |
| `tools/portal_api_check.py` | 40 项:通过 35 / 失败 0 / 跳过 5 |
| `tools/e2e_smoke_test.py --read-only` | 31/31 |
| `_eval_harness/http_probe.py` | 11/11 succeeded |
| `_consistency.py` | GATE PASS |
| `_fe_boundary_http.py`(前端入参边界真机) | 全部符合预期 |

## 未做(如实登记)
- **投顾演示数据未灌**:`AD011` / `A047` 需要 `advisor_product_suitability_reference`
  这类带 `source_url` + `document_sha256` 的证据行,而披露文件不在仓库里;
  `tools/seed_advisor_demo.py` 明确"不编证据"(fail closed),故这两条按空集 SKIP。
- **客服线文档目录仍未入库**:`客服agent/`、`开发文档/`(权威副本在本机)与
  `_chunks_report.txt`(本地构建产物)依旧排除在提交之外。
2026-09-20 14:52:35 +08:00
张胜宇 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
lzf_0626 c0e5c80929 记忆系统:recall 结果接入 prompt + 可观测性(既有改动,代为提交)
## 说明

**这批改动不是本次会话写的**,它们在会话开始前就已在工作区里、一直未提交。
我做的是**验证**它确实成立,然后按你的指示代为提交。

出处:`docs/演示用/记忆系统排查报告-2026-09-14.md` 与同目录
`记忆系统修复文档-2026-09-14.md`(两份都在本次一并入库)。
排查报告的结论是「记忆系统没有坏」——库里有真实数据、170 条抽取事件全部消费成功;
真正的问题是「观测不到」+「召回结果没人消费」。

## 改动内容(按两份文档的编号)

- **F1 `RecalledMemory.content` 断头路**:`base.py` 新增 `memory_context_text()`,
  `risk_agent._agent_system_prompt` 接收并注入记忆段。无记忆时返回空串,
  因此 prompt 逐字不变 —— 这也是它能安全接线的理由。
- **F3 `governance.recall` 员工身份恒空**:补一条明确的语义日志。
  员工身份下召回的是"该用户自身作为客户"的记忆,恒为空属预期,
  但此前没有任何提示,运维看到 `count=0` 只会以为记忆坏了。
- **F4 可观测性**:`GET /api/v1/users/me/memories`(`stored` / `recalled` /
  `downstream` / `pending_events` 四段)+ 抽取与召回的 6 处日志 +
  三个只读探针 `tools/probe_memory_state.py`、`probe_memory_detail.py`、
  `probe_agent_types.py`。

**未实施**(文档明确留作待决,我也不代为决定):F2 `known` 引用校验永不触发
(需架构确认 memory 类 `source_references` 由业务填还是底座统一附加)、
F5 客服是否读写长期记忆(涉脱敏与复核,需产品+合规)。

## 我做的验证(会话内实测,非照录文档)

- 新接口 `GET /users/me/memories` 以 `cust_t` 调用 -> **HTTP 200**:

      stored:    total=2, by_status={'active': 2}
      recalled:  count=2, degraded=False
      两条记忆:preference:horizon='约三年'(0.95)、preference:risk_level='稳健型'(0.98)

  与排查报告 §〇 列出的那两条**完全吻合**。
- `pytest tests/unit tests/contract` 全绿(这批改动没有破坏既有测试)。

## 未验证的部分

`memory_context_text()` 接进 prompt 后的**端到端效果没有实测** —— 文档自己说明了
原因:当前 `risk` Agent 的召回恒空(员工身份不是客户),所以接线后行为不变,
要用测试替身才能验证注入。我没有为此编造证据。
2026-09-14 20:35:46 +08:00
yuancong_0626 2548c39c6b 袁聪的最后一次完善更新 2026-09-14 18:13:10 +08:00
zhangshy 20773453bd 修复通用风险问答会话历史丢失 2026-09-14 10:08:05 +08:00
lzf_0626 f72a545c39 refactor: 品牌统一为「南方财富」(项目方定)
背景:品牌名此前三处不一致 —— 后端 Agent 自称「奶龙基金」(customer_service_rules
的防诈骗/转人工话术、risk_agent 与 risk_analysis_service 的系统提示词)、前端全站
「南方财富」、闲聊提示词与知识素材「南方科技」。docs/36 已把它登记为"上报项目方后
待定",现按项目方决定统一为「南方财富」。

改动:
- app/core/customer_service_rules.py:P0 防诈骗话术与 P2 转人工话术里的品牌名
- app/service/agent/implementations/customer_service.py:COMPANY 常量,
  以及那处引用实测样本的注释(改为不绑定具体品牌名,免得下次改名又过时)
- app/service/agent/implementations/risk_agent.py:docstring、自我介绍、system prompt
- app/service/risk_analysis_service.py:SYSTEM_PROMPT
- app/worker/risk_scan_scheduler.py:--help 描述
- app/static/index.html(旧联调页 4 处)、portal/employee-risk/dashboard/index.html
- tests/unit/api/test_customer_service_test_page.py:同步断言(它断言的正是页面里的品牌名)
- tools/publish_chitchat_prompt.py:SYSTEM_PROMPT 改品牌;并修掉"存在即跳过"的检查
  —— 原来只判当前版本有没有这一行,于是改了文案也发不出去(脚本打印"无需发布"直接
  退出),没有任何提示。改为比对 system_prompt/user_prompt_template 内容。

闲聊提示词已重发为 release 308 / v5,生效内容为"你是南方财富的智能客服助手…"。

刻意未动:
- fin_product.fund_manager = "南方基金" —— 它被 market_quote_sync_service 与
  product_history_sync_service 当过滤条件使用,改名会让同步链路查不到产品
- knowledge_search_service.py 注释里引用的知识库实际标题「南方科技有限公司…」
- docs/客服docs 下的历史素材与 docs/ 下的过程记录(属历史留痕)

⚠️ Milvus 里的知识条目仍写「奶龙基金」(RAG-*/NF-*)与「南方科技」(PROD-*),
属知识数据,需重灌才能统一;本轮不动。

同时:
- docs/40 把品牌条目标为已处理,并补上知识库缺口的现状
- 新增 docs/42-场内基金知识条目草稿.md:按 fin_product 的 20 只产品生成,
  含通用交易规则与产品清单;费率等缺失字段一律标"以交易页面为准",未编造数字。
  **该文件是草稿,未入库**,待审核后走 POST /api/v1/knowledge/documents 灌库。
2026-09-13 19:17:56 +08:00
张胜宇 e740a58e9d 前端2 2026-09-13 18:31:38 +08:00
wangjianlong_0626 57677f6554 merge: 合并主干 qyqy_develop(PR #7 之后)并对齐两套投影实现
共同祖先 bbf623a;主干 54 个提交、118 个文件;本线 25 个文件;9 个冲突文件。
主干这次把 **ZSY 的整条投影实现合进来了(PR #7)**,而本线此前的提交正是
移植并修正同一套代码 —— 因此冲突的本质是"同一功能两份实现并存",取舍错了会把
已修好的缺陷又带回来。逐项取舍与理由见 `docs/39-主干合并对策记录.md`。

## 取舍(9 个冲突)

取本线:
- `app/infrastructure/milvus_profile_projection.py` —— 主干是 ZSY 原版,含两处必炸点:
  ① `customer_id` 要求 int 而本仓所有生产者都写 `str` ⇒ 每个事件必然失败;
  ② 不可投影的 `memory_key` 直接 raise ⇒ 一条 `constraint:` 记忆毒死整客户整批。
  本线版已放宽为「接受纯数字字符串」与「跳过并留痕」。
- `memory_sync_outbox_worker.py` / `conversation_privacy.py` / `risk_questionnaire.py`
  —— 代码逐行一致,仅注释与说明文字详略不同(`risk_questionnaire.py` 两边**独立做了
  完全相同的修复**,都改成 re-export `app.model.profile`)。
- 两个投影测试文件 —— 本线是他那份的**超集**(4→10、4→5 例,包含他全部用例)。

两边合并:
- `app/worker/runtime.py`:`__init__` 两边各加一个参数,都要。
- `app/service/agent/implementations/customer_service.py`:import 取并集;
  `COMPANY` 取主干的「奶龙基金责任有限公司」("奶龙"是本项目实际品牌名,主干多处出现),
  `HOTLINE`/`SERVICE_HOURS` **取本线的修复**(主干仍是占位符 `400-XXX-XXXX`,
  本线已改为引用 `customer_service_rules` 的唯一来源 —— 这是 A1 缺陷修复,
  否则同一客服给客户两个不同号码)。
- `AGENTS.md`:表数/Agent 清单取主干(90/89、7 个 Agent),本线的
  `-X utf8` 与两条 outbox 易错点保留,测试基线按合并后实测重算。

## 消费端只保留一套(本次最重要的一处)

合并后曾出现**两套消费者读同一个 `memory_sync_outbox`**:`__main__.py`(PR #7)
与 `runtime.consume_profile_projections()`(本线),而**两者的 neo4j handler 不同**
—— 前者用 ZSY 的 `Neo4jProfileProjection`(按客户各建私有节点),
后者用主干 `ProfileGraphProjectionService`(共享 tag 节点、只投影已确认事实)。
同一事件被谁领到结果不定,等于"同一事实在图里有两种说法",正是**方案 A 要避免的状态**。

现只保留 runtime 那一套(带 `memory_sources` 兜底、neo4j 复用主干服务),
删除 `__main__.py` 的重复接线;装配入口职责仍在该文件(注入 `relationships` /
`projection_cleaner`),Milvus 客户端由 `bootstrap` 工厂惰性构造、缺配置时显式降级。

副作用:`app/infrastructure/neo4j_profile_projection.py` 不再被生产代码引用,成为
**死代码**(本线未删,属架构师线,其单测仍在)—— 待架构师决定删或明确分工。

## 顺带修掉的 3 个继承缺陷(主干同样存在,PR #7 后未整套复跑故未发现)

1. `tools/seed_test_rbac.py` **少建 `review_t`(9004)账号** —— 两个集成测试都依赖它
   ("账号存在但无权限应返回 200 空集而非 404"、`PLACEHOLDER_ACCOUNTS`)。
   同时把用户↔角色绑定从 `zip(..., strict=True)` 改为**显式配对表**:原写法隐含
   "USERS 与 ROLES 一一对应",一加不绑角色的账号就 ValueError、整个种子跑不完
   (commit 在最后,外部表现是"什么都没发生")。
2. `CustomerProfileCandidateService._write_profile_snapshot` **漏写 `current_customer_id`**
   —— 该列不是生成列而是普通可空列 + 唯一键 `uk_profile_snapshot_current`,
   不写则唯一键形同虚设(多个 NULL 不冲突),且旧当前版本也没清该列、补写就会撞键。
   现旧值置 None、新值显式写入(与 `ProfileGenerationService._clear_current` 一致)。
3. 集成测试前置未记录 —— 13 个登录/RBAC 用例因 401 而红,实为"测试账号不存在",
   跑 `seed_test_rbac.py` + `set_user_password.py` 后转绿;已在 `AGENTS.md` 记明,
   避免被误判成代码缺陷。

## 文档

- 新增 `docs/39-主干合并对策记录.md`(逐文件取舍 + 理由 + 遗留)
- `docs/37` 订正一处过时说法:曾写 `current_customer_id` 无人使用且故意不映射,
  实际 `app/model/profile.py` 已映射且有人使用(详见该文档 §6.2 的订正块)
- 文档编号:主干已占 29–36,本线两份文档让号至 `docs/37`、`docs/38`

## 验证(合并后实测)

- `pytest tests`(全量)→ `2 failed, 1396 passed, 2 skipped`
- `pytest tests/integration` → `102 passed, 1 skipped`(修上述 1、2 后从 15 failed 归零)
- `mypy app` → `Success: no issues found in 245 source files`
- `tools/audit_schema.py` → 89 张业务表无缺失/意外(未改动任何表结构)
- `tools/check_authoritative_docs.py` → 52 份文档无编号冲突
- `tools/check_rbac_seed_consistency.py` → 通过

那 2 个失败是既有环境项(`test_offsite_document_recognition_adapter.py` 断言请求体
中文原文而 httpx 序列化成 `\uXXXX`),与本次合并无关。
2026-09-12 13:14:57 +08:00
张胜宇 ef701c844c merge: integrate ZSY customer service and profile capabilities 2026-09-11 22:31:51 +08:00
wangjianlong_0626 cc6ae87f26 Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague 2026-09-11 21:18:30 +08:00
qyqy a7e2d3eac0 fix(customer-service): 统一客服热线来源;补 transfer_required 出参(docs/05 §6.3 未兑现的一半)
两个都是"代码里存在但没接对"的缺陷,都不是新功能。

## A1 客服热线在代码里有两个值(一个出口给假号码)

- `app/core/customer_service_rules.py:35` `CONTACT_PHONE = "15936583816"` ← 真号码,安全路由 6 处在用
- `app/service/agent/implementations/customer_service.py:155` `HOTLINE = "400-XXX-XXXX"` ← 占位符,兜底出口在用

后果:**同一个客服给客户两个不同的电话号码**。问"风险等级怎么划分"被安全路由处理时给真号码;
问一个知识库答不了的问题走兜底时给 `400-XXX-XXXX` —— 客户按这个号码永远打不通。

修法:`HOTLINE` / `SERVICE_HOURS` 改为**转发** `customer_service_rules` 的两个常量
(不是"改成相同的值",而是引用同一对象,避免日后再次漂移);工作时间也随之从
"每日 7:00-22:00" 统一为 "工作日 09:00-18:00"(与安全路由出口一致)。
新增守卫测试用 `is` 断言对象同一性 —— 值相等挡不住"两边各写一份恰好相同"的漂移。

## A2 `transfer_required` 既没落库也没出参

`docs/05` §6.3 一直规定 `GET /agent-runs/{run_id}` 的 `result` 里有
`transfer_required` / `transfer_reason`,但实现里两个都没有:前端判断"这轮要不要转人工"
只能靠**猜正文里有没有兜底话术的开头**(`docs/24` 自己把这称为权宜之计)。

- 写入侧:`conversation_message` **没有** `transfer_required` 列,加列要迁移且规则 4 禁止改既有
  字段定义 ⇒ 放进 `tool_calls` 这个现成 JSON 列,作为 `calls` 的兄弟键
  (`{"calls": [...], "transfer_required": bool, "transfer_reason": str|None}`)
- 读取侧:`RunQueryService.get` 取出来放进 `result`;**兼容历史行**(`calls` 裸列表 / None →
  按 False/None 处理,不抛异常、也不凭正文猜)

刻意**没做**的一半:`docs/05` §6.3 的 `result` 里还有 `degraded` / `degradation_reason`,
但 `CoreResult` 里根本没有这两个字段(降级信息目前只在工具出参里)—— 补它要改
`CoreResult` 并让各 Agent 传递降级状态,属另一个改动范围。**已在交付说明里注明这一半仍缺。**

## 真机验证

| 问题 | transfer_required | transfer_reason | 正文电话 |
|---|---|---|---|
「请介绍一下量子纠缠在基金估值中的应用」 | **True** | 置信度不足:score=0.571 gap=0.004 | 15936583816 ✅ |
「请帮我计算一下三体问题的数值解」 | **True** | 置信度不足:score=0.499 gap=0.011 | 15936583816 ✅ |
「基金申购后多久确认」(正常知识直返) | False | — | 无(正确) |
「你们公司明天会下雪吗」(闲聊出口) | False | — | 无(正确) |

(第一次我用"下雪"当兜底用例,结果它被闲聊出口正确接住了 —— 是我的期望值写错,不是代码问题。)

门禁:测试 1223 passed(新增 4 个用例)/ 3 failed(均为已知非代码缺陷)/ mypy 0 错。
2026-09-11 21:17:47 +08:00
Windows bbf623a464 merge: integrate advisor capabilities on latest qyqy base 2026-09-11 21:03:58 +08:00
lzf_0626 f09ea9e988 Merge origin/NL_develop:客服画像出口、知识管理三端点、合规语境与知识向量链路
NL 线(含其并入的袁聪场外/推广域)。唯一冲突是 .gitignore —— 双方都往同一区域加了
.workdir/,取对方版本(他的更完整,含 .tmp/ 与说明),顺带修掉我之前用
Add-Content -Encoding utf8 造成的编码混合(read 工具当时报 invalid UTF-8)。

合并后修的问题 —— 都不是"改别人业务逻辑",是让门禁能绿:

1. 缺运行依赖 python-docx。document_parser.py 解析 .docx 用它,但 requirements.txt 与
   pyproject.toml 都没声明 —— 别人环境跑知识入库会直接
   ModuleNotFoundError: No module named 'docx'。已补声明。
2. ruff 7 项:其中 tests/conftest.py 的 F821 Undefined name 'Path'(他的 tmp_path 修复
   写了字符串注解 "Path" 却漏 import,运行时不求值所以没炸,但 mypy/ruff 会抓)、
   tools/publish_customer_service_config.py 的 F841 inherited_keys 死变量(他改同 key
   覆盖、换成 inherited_only 后忘删旧的)、3 处 E501,另 2 项 ruff --fix 自动修复。
3. 合规基线种子未跑:integration 的 test_compliance_seed_mysql 4 个用例要求
   agent_negative_word 有 7 条 active 且已复核、agent_reply_template 覆盖 6 场景。
   跑 tools/seed_compliance_baseline.py(11 条 active 规则 / 6 个场景模板)后 80 passed。

验证:ruff 干净 / mypy 180 文件 0 错 / unit+contract 1140 passed /
integration 80 passed / 表数 68(alembic 已在 20260911_merge_risk_heads)。

唯一失败 tests/unit/repository/test_fund_readonly_contract.py 是双方一致的既有缺陷:
它断言 Base.metadata 里的 fin_* 表集合,而实测为空集 —— 即该测试依赖别的测试先导入模型的
副作用,单独跑必失败。NL 方也明确"不修不报",此处照办,仅记录。
2026-09-11 20:22:59 +08:00
zhangshy beb67f0a26 优化奶龙字段语义理解 2026-09-11 19:37:26 +08:00
Windows b6429e0e9c feat: add advisory product comparison tool 2026-09-11 19:20:32 +08:00
qyqy 7677aeaee1 merge: 并入同事的场外申购/推广/行情/NL2SQL 线(11 提交、334 文件)
冲突仅 3 个文件,全部取并集(双方都没有需要丢弃的改动):
- app/main.py:import 双方路由(我方 knowledge_management + 同事的 offsite_fund/
  promotion_material);include_router 段本已自动合并
- app/service/agent/bootstrap.py:import 与工具注册均取并集
  (query_customer_profile + query_financial_data 都注册)
- tests/integration/test_config_release_mysql.py:outbox 清理同时保留
  架构师的 event_type 限定(防误删其它域 outbox 行)与同事新增的 peer_release_id

同事这轮带入:11 个 alembic 迁移(建 offsite_* / promotion_* 等表)、
场外申购与推广素材 Agent、financial NL2SQL 工具。
注意:本库尚无 offsite_*/promotion_* 表,跑相关测试前需要执行 alembic upgrade。

边界核对:同事的场外代码未写入场内交易表(fin_sim_order/fin_capital_flow/fin_cash_ledger),
符合 AGENTS.md 规则 8。
2026-09-11 18:56:26 +08:00
lzf_0626 fcd3eaadd6 Merge origin/qyqy_develop:风控时间口径补齐 + Agent 截断提示
组员的 4 个提交(相对 c11e56c):

- 4b0c148 把我这边的风控 P3 收尾与适当性豁免额度合并进 qyqy_develop(PR #4)
- 2ebe438 docs: 增加风控 Agent 工具白名单与意图配置
- 5102eaa Merge origin/qyqy_develop into RM2_develop
- d2cdbba 修复风控时间口径与 Agent 截断提示

自动合并零冲突,两侧的改动是互补而非重叠:

- 时区:我在 list_alerts 做了 from_local,他补的是 list_evidence 与通知列表;
- 截断:我加了 data_truncated / evidence_truncated,他在 risk_agent 的 system prompt
  里加了第 10 条 —— 这些标记出现时不得按全量证据下结论。方向是一致的。

验证:ruff 干净 / mypy 138 文件 / 703 unit+contract(+6,组员新增的用例)/
33 integration,均在合并后的工作树上跑过。
2026-09-11 16:33:55 +08:00
Windows 4088c634de feat: complete advisor conversation loop 2026-09-11 16:12:38 +08:00
qyqy cbd6de2721 Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-latest
# Conflicts:
#	app/service/model_gateway.py
2026-09-11 15:38:11 +08:00
Windows e8ec0af398 feat: add advisor recommendation review flow 2026-09-11 15:33:27 +08:00
qyqy 395bad25b7 fix(knowledge): 检索服务适配现库集合 schema、发布对齐的工具白名单、免责声明只由治理层注入
合并暴露的三个真机问题(单测全绿但线上必挂):

1. 检索服务字段名与现库集合不符 -> 静默零召回
   架构师那套按 load_knowledge_milvus.py 的 schema 读 doc_id/content/chapter/section/
   visibility,而现库三集合的真实字段是 knowledge_id/title/snippet/tags/version/intent。
   Milvus 对不存在的字段直接报错 -> 三集合全失败 -> degraded -> 客服一律转人工。
   改法:只改读取侧,用 _FIELD_ALIASES 映射;KnowledgeHit 对外形状不变(下游与测试不动)。
   代价已注明:没有 visibility 字段 -> 检索层内部资料硬隔离失效(现库 356 行均为对外知识)。

2. 发布白名单与代码上限不匹配 -> AGENT_PERMISSION_DENIED
   active 版本白名单是 query_knowledge,而合并后代码上限是 search_knowledge/check_suitability/
   query_customer_profile -> 交集为空 -> 所有知识问题 failed。
   改法:publish_customer_service_config.py 补 PROFILE_TOOL 进 faq 白名单,并让同 key 的
   继承项被本次定义覆盖(旧值原样继承会被子集校验 422 拒掉整次发布)。已激活版本 216。

3. 免责声明重复出现
   Agent 自己拼一句 + 治理层追加权威话术 -> 客户看到两条。改为只由治理层注入
   (话术属发布配置,改文案不该改代码)。风控等内部 Agent 不注入(结构化输出不被污染)。

测试:934 passed / 1 failed(test_fund_readonly_contract 既有空集缺陷,与本线无关)
真机:知识问答两问 succeeded 且只带一条声明;画像问答 succeeded;知识库三端点全绿
2026-09-11 15:27:16 +08:00
zhangshy d2cdbbac01 修复风控时间口径与Agent截断提示 2026-09-11 15:25:45 +08:00
qyqy 57c4add7d8 merge: 客服Agent+RAG+画像 与 架构师最新 qyqy_develop 合并
- customer_service.py 以架构师实现为骨架(三档置信/适当性/会话记忆/话题矩阵),嫁接本人画像出口
- 知识检索契约合并两条链路:架构师 search_knowledge(KnowledgeSearchInput) + 本线
  query_knowledge 链路所需常量(ALLOWED_CONSTANTS/VECTOR_DIM/intent_for_qa_id)
- bootstrap 保留架构师 6 工具/3 Agent,补回 query_customer_profile 与 get_milvus_knowledge_writer
- model_gateway 能力映射修正 intent_classification→chat,保留空集回退兜底
- governance 免责声明限定面向客户 Agent(agent_type 由定义透传),风控结构化输出不再被追加
- 修 JWT 密钥路径(config/jwt/dev)、文档 21 号撞号→25
- 测试基线 934 passed / 1 failed(既有空集缺陷)
2026-09-11 15:06:44 +08:00
lzf_0626 575b4c2baa 客服适当性改按第十二条匹配矩阵(更正我上一轮的复核结论)
你说得对:C ≥ R 是风控的口径,客服要走政策原文的矩阵。

上一轮我复核后说"两边一致、无需改动"是错的 —— 我只对比了政策文本与
SuitabilityService,漏了第三个东西:客服自己的知识库。POL-AST-012 就是第十二条矩阵
原文,而客服为 C1-C5 补的「能买什么产品」问答答案直接取自该矩阵(docs/24 第二节)。
于是同一个客服 Agent 对同一个问题给出相反答案:

  问"C1 能买什么产品"   → 知识检索答 R1、R2 可买(矩阵口径)
  问"C1 能买这只 R2 吗" → 适当性出口答不能购买(严格 C ≥ R)

政策冲突的精确位置也不是"矩阵 vs 硬匹配",而是第十四条**第 1 款与它自己的第 2、3 款**
矛盾:第 2 款说"低于一个等级以上"才拒绝(C1→R2 只低 1 级,够不上"以上"),第 3 款禁止
C1 买 R3+、C2 买 R4+(R2 不在禁止列表)。矩阵与第 2、3 款三方一致,孤立的是第 1 款。

改动:

- suitability_service.py 新增 MATRIX_ALLOWED / MATRIX_NEEDS_DISCLOSURE,_decide 由
  "C < R 即拒绝"改为按矩阵:C1→R2、C2→R3 直接可买;C3→R4、C4→R5 走第十五条豁免档
  (reason_code=SUITABLE_WITH_DISCLOSURE,强制揭示 + 确认 + 录音);低两级及以上仍拒绝。
- 客服话术分档:越级档不再说"在您的风险承受能力范围内" —— 那句只对 C ≥ R 成立,
  用在 C1 买 R2 上会让客户以为自己的测评本来就覆盖这只产品。
- 风控侧不动,保留 C ≥ R:它要发现的是"越级成交且留痕不全",这个差异是有意保留的。

测试:新增逐格对照政策原文的 25 格矩阵用例、豁免档用例、话术分档用例(+29)。
docs/25 第七节 #1 与 docs/24 第七节同步更正,包括写明我上一轮那个结论错在哪。

门禁:ruff 干净 / mypy 137 文件 / 693 unit+contract / 33 integration。
2026-09-11 14:47:08 +08:00
wangjianlong_0626 e4c4099aaa wip: 客服Agent + RAG + 画像收尾(基于 6516ccb) 2026-09-11 14:46:40 +08:00
Windows ff71a1a724 feat: add dynamic advisor asset allocation 2026-09-11 14:44:29 +08:00
Windows 281e76e4b1 feat: add advisor portfolio analysis 2026-09-11 13:20:09 +08:00
lzf_0626 4e2e42c896 test(base): 验证最后一种工具拒绝(角色不符),四种分支全部实测通过
分支 4 是最难构造的一种,两个前提缺一不可:

1. **工具的角色集合必须比 Agent 的更窄**。Agent 层的 validate_access(base.py:101)会先按
   AgentDefinition.allowed_roles 拦截,两者一致时永远进不到工具层的角色校验。所以把
   probe_alt 收窄为 ("risk_operator",),而 Agent 仍允许 admin。
2. **调用者必须有工具要求的权限**,否则会先命中权限分支。所以脚本临时给 admin 授
   probe:read,验证后撤销。

过程中又修掉一处自己写错的地方:探针的 handle 原先硬编码调用 PROBE_TOOL,导致分支 4
(需要调 probe_alt)与分支 2(需要调白名单之外的那一个)互相干扰——第一次跑出来的结果
是"工具不在当前意图白名单"。改为按消息里的 "alt" 选择要调的工具。

四种分支的实测结果,message 各自独立、指向不同处置动作:
- 白名单为空        → 该意图未配置工具白名单
- 工具不在白名单    → 工具不在当前意图白名单
- 缺少工具权限      → 缺少工具权限
- 角色不符          → 角色不能使用工具

目标的另一半也验证了:审计里是完整细节(reason = "角色 ['admin'] 与工具允许的角色
['risk_operator'] 无交集",并带 tool_name / intent / trace_id),而异常 message 只有
"角色不能使用工具"、不含角色集合。**内部配置只进审计,不进客户可见响应。**

环境复原:生效配置 9 条(与起点一致);sys_permission / sys_role_permission 中
probe:read 的行数为 0。
2026-09-11 13:12:56 +08:00
Windows 60c4a49b9b feat: migrate advisor investment goals 2026-09-11 13:11:58 +08:00
lzf_0626 1eb8946552 test(base): 端到端验证工具拒绝的分支 2 与 3,探针加第二个工具
接上一轮(分支 1 已验证)。本轮用只读探针触发另外两种拒绝:

- 分支 2「工具不在当前意图白名单」:发布白名单 ["probe_alt"] 但 Agent 调 probe_echo。
  这一步能做,正是因为给探针加了第二个工具 —— governance.resolve 取的是
  「发布白名单 ∩ 代码声明的 allowed_tools」,配置**只能缩小不能放大**,所以单个工具的
  Agent 永远构造不出"有白名单但不含该工具"的场景。这是做端到端时才撞到的结构性约束,
  platform_probe.py 里已注明。

- 分支 3「缺少工具权限」:发布白名单 ["probe_echo"],工具可用了,但 admin 角色并没有
  probe:read 这条权限,天然命中权限分支,不需要动 RBAC。

实测:两种拒绝的 stderr 分别为「工具不在当前意图白名单」与「缺少工具权限」,
运行状态均为 failed / AGENT_PERMISSION_DENIED;生效配置已恢复(终点 9 条,与起点一致)。

顺带发现分支 4 的结构性障碍(下一步处理):Agent 层的 validate_access 会先按
AgentDefinition.allowed_roles 拦截,所以要在**工具层**触发"角色不能使用工具",
必须让工具的角色集合比 Agent 的更窄 —— 探针目前两者的角色集合相同,触发不到。
2026-09-11 13:11:30 +08:00
lzf_0626 edc0c43245 test(base): 造只读探针端到端验证工具拒绝,并修掉它暴露的一个死分支
**为什么造探针**:ToolExecutor 的四种拒绝在真实链路上很难安全触发——要么改客服、风控的
生效配置,要么动 RBAC,两条路都会影响正在工作的 Agent。platform_probe 是个只读、无副作用
的探针:它只声明 probe 一个意图(所以意图分类只可能返回它)、没有发布工具白名单
(天然处于"未配置"状态)、工具只回显参数不碰业务数据。

**它立刻查出一个死分支**:探针报的是「工具不在当前意图白名单」,而不是我新加的
「该意图未配置工具白名单」。原因是 governance.resolve 会为每个 supported_intents
**预填条目**(governance.py:55-61),未配置时得到的是**空元组**——所以
intent not in configured_tools 在运行期**永远不成立**,那个分支是死代码。

单元测试没能发现它,因为我在测试里手工构造了 configured={},而真实链路不产生这个形状。
**这正是端到端测试的价值**:单元测试验证的是我设想的形状,端到端验证的是真实形状。

修法:改判"白名单为空"而非"缺键",文案改为"该意图的工具白名单为空",并注明经过 governance
装配后"完全没配"与"配了空列表"无法区分、也不假装能区分(两者运维动作相同)。新增一条按
**真实形状**({"faq": ()})构造的用例把它锁住。

实测:探针调用 → failed / AGENT_PERMISSION_DENIED,stderr 为
ForbiddenAgentError: 该意图未配置工具白名单(tool_executor.py:108)。

ruff / mypy(136 文件) / 611 unit+contract 全绿。
2026-09-11 13:10:04 +08:00
Windows 5e8eecc288 feat: adapt advisor agent to qyqy base 2026-09-11 11:49:11 +08:00
lzf_0626 478b64e4d7 Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop_1
# Conflicts:
#	app/service/agent/bootstrap.py
2026-09-11 10:43:08 +08:00
lzf_0626 c369a919c6 test(customer-service): 用格式矩阵把 _topic_of 的第四、五口咬痕也堵上
方案 A:把每种出口的**真实产出**都过一遍 _topic_of。谁再改回答格式或加出口,测试立刻红,
而不是等客户实测先撞上——它已经咬了三次,三次都是客户先发现的。

写这个矩阵当场又抓出两个真 bug:

1. 整节块的主语带着章节号("2.1 南方季季盈90天")。下游 _product_risk_level 要求产品名
   是该块标题或正文的**子串**,带编号就永远匹配不上——适当性查询会静默失败、退化成转人工。
   这是真故障,不是洁癖。
2. 政策条款的主语是"第十二条 投资者与产品匹配矩阵";整节块去掉标题行后剩下的表格行
   ("| C1保守型 | ✅ 可购买 |")也会被当成主语。

修法:剥掉开头的章节号;排除以"|"开头的表格行;排除"第X条"开头的条款标题。

矩阵覆盖全部四个出口:知识直返(行级块/整节块/FAQ/政策四种形状)、适当性裁决(四种话术,
真调 _suitability_text 而非手写样例)、引导人工(兜底话术取不出主语是对的)、
闲聊(无产品实体,取不出也是对的)。
2026-09-10 22:53:29 +08:00
lzf_0626 8b1ed70916 fix(customer-service): 自述等级的说明行不能挡住产品名
实测连问「季季盈90天起投多少」→「C3 客户能买它吗」→「那我能买它吗」,第三问转人工。
原因是上一步刚加的客户自述等级说明("您提到自己是 C3。…以您在公司留存的测评结果为准")
占了回答首行,而 _topic_of 只看首行,产品名被挤到第二行,追问于是丢掉了指代对象。

这是 _topic_of 第三次因为"只认固定格式"而咬人(前两次:FAQ 的"问"字、适当性回答没有
冒号)。改成遍历回答前四行、逐行尝试解析,并把单行解析抽成 _topic_in。

同时补上自述等级时的依据说明:客户说"我是 C3"而系统答"您没有测评结果",在他眼里是
矛盾的,必须点明判断以档案为准、不以自述为准。自述等级只用于这一句说明,**绝不**参与
裁决。拒绝后的指引也从"联系客户经理或拨打热线"改成"请先完成风险测评"。

新增三条单测:自述等级识别(含小写)、自述说明行占首行时仍能取到主语、
"为 R2"开头说明前面没有产品名时返回空。
2026-09-10 22:48:56 +08:00
lzf_0626 c564494f84 fix(customer-service): 追问适当性结论时不能丢掉产品名
实测三连问:「季季盈90天起投多少」→「C1 客户能买它吗」→「那我能买它吗」,
第三问转人工。原因是 _topic_of 不认识适当性回答的格式——它那一行是
"南方季季盈90天为 R2(中低风险),与您当前的风险测评结果不匹配…",
既没有冒号也不是标题行,于是被判成"取不出主语",第三问就失去了指代对象。

补上这种格式:产品名在"为 R{1-5}"之前,且必须在冒号判断**之前**匹配(那一行没有冒号)。
以"为 R2"开头的行说明前面没有产品名,返回空串,而不是把等级本身当成主语。

修好后第三问给出与第二问一致的结论,这是对的:裁决是确定性的,同一个客户问同一只
产品,结论就该一样。新增两条单测钉住这两种情形。
2026-09-10 22:46:31 +08:00
lzf_0626 33fbb0eb01 feat(customer-service): 接入适当性裁决,回答"我这个等级能不能买它"
客户实测反馈:「c1客户能买它吗」答的是 C1 的通用规则,没回答"能不能买季季盈90天"。
根因是这个问题需要**组合两个事实**——产品的风险等级(R2)与客户档案等级能不能匹配——
而检索只能给出"最像的那段原文",给不出结论。基座早就有了 check_suitability 裁决工具,
接口留好了但客服没接线(这个项目里第三次遇到同一类情况)。

改动三处:
1. 新增 suitability_check 意图,意图码三处对齐(AgentDefinition.supported_intents、
   agent_intent_config 的 active 行、发布版 agent_tools 白名单)。
2. 新出口 _answer_suitability:产品风险等级**从知识库查出来**(不猜、也不采信问句里
   出现的"R2"字样),产品名只取上一轮回答里的主语(来自知识块字段,可信),客户等级
   交给 check_suitability 按档案解析——**不采信客户自称**。任何一步拿不到确定值就转人工:
   这个出口会给出"能不能买"的结论,宁可答不了也不能答错。
3. 发布脚本加意图注册与工具白名单,并做成幂等(重跑不会因为"已经审过了"而 409)。

实测:意图正确路由到新出口,裁决链路走通。9001 因为在 fin_risk_assessment 里没有测评
记录,系统给出"暂时无法购买 + 您目前没有在有效期内的风险测评结果"——这正是适当性管理
要求的行为,不是故障:不能卖给一个没有有效测评结果的客户。措辞也据此改过,不写
"您的等级为未记录"这种客户看不懂的句子。

顺带修了前端一处误导标记:它用"是否含客服热线"判断"已引导人工",而正常的适当性回答
里也会建议拨打客服热线,于是"已经给出结论"被误报成"已引导人工"。
2026-09-10 22:42:17 +08:00
lzf_0626 ee9de1b520 fix(customer-service): FAQ 型回答不能把"问"字当成追问的主语
给 C1-C5 补 FAQ 之后暴露的连带问题:FAQ 型回答的首行是"问:C1 客户能买什么产品?",
_topic_of 取冒号前的内容会拿到一个孤零零的"问"字,客户追问时检索词被污染成
"问 那它能买基金吗"。

改为"问"/"答"这类纯标签直接判为取不出主语,退化成只查当前这一句。

另附一条实测结论(**没有改代码**):在 FAQ 型回答之后追问「那它能买基金吗」仍会转人工,
这是安全且合理的——上一轮回答里没有可指代的产品实体,而"C1 能不能买基金"本身取决于
那只基金的风险等级,知识库没有、也不该有这种一概而论的答案。按金融场景的原则,
"答不了"好过"答错"。
2026-09-10 22:35:17 +08:00
lzf_0626 4c2b147793 feat(knowledge): 产品知识拆到表格行级,并按问句选粒度
问题(客户实测反馈):同一会话里问「季季盈90天起投多少」和「那它风险高吗」,两次回答
**一模一样**——都是整个产品小节的表格。客户问的是风险,收到的是整张说明书,看起来像
客服没听懂问题。

根因是切分粒度:原来"一个叶子标题 = 一块",产品手册里就是整个产品小节(表格 + 说明)
成一块。这既让两个不同的问题命中同一块,也让整节几百字的向量成了"整节的混合语义",
与"起投多少"这种具体小问题相似度天然偏低(实测该问句向量 top1 仅 0.6291,够不到 0.75
硬门槛,只能靠与次优的差值勉强通过)。

改动三处:
1. 切分:Markdown 表格的每一行额外生成一个**自解释**的小块("南方季季盈90天:起投金额
   1万元"),挂在父块 doc_id 下(PROD-007-04),父块照旧保留。知识块 160 → 631。
   效果:该问句的命中分从 0.6291 升到 0.869,命中的正是"起投金额"那一行。
2. 检索:命中行级子块时把它的整节父块一并带回(分数按 0.9 折算),供调用方按问句选粒度。
   整节块保底占最后一个名额,且不参与 top1/top2 判定——实测它挤到第 2 位会把 gap 从
   0.090 压到 0.076,几乎跌破 0.07 的转人工门槛。
3. 客服:命中的是行级子块时,看问句与子块标签是否真的对得上——「起投多少」对「起投金额」
   对得上,用那一行;「介绍一下」对不上,换成整节。

过程中两次判据写错并已修正(都固化进了测试):用"含连字符"认子块时,整节块自己的编号
PROD-901 被误判成子块;用"不含两位数字后缀"认整节块时,FAQ 块全被误判成整节块排到后面,
把正确答案挤出 top1、害得「基金赎回几天到账」转人工。

验证:起投/管理费等字段问法给出聚焦的单行答案;"介绍一下"给出整节;FAQ 与政策问法不受
影响(换话题、指代追问等此前修好的场景复测通过);
ruff / mypy(113 文件) / 468 unit+contract / 29 integration 全绿。
2026-09-10 22:29:23 +08:00
lzf_0626 a6c09fa3c4 fix(customer-service): 检索问句只在客户这一句说不清楚时才带上文
实测的答非所问:同一会话先问「季季盈90天的起投金额是多少」,再问「基金赎回几天到账」,
第二问答出的是季季盈的产品介绍。原因是 _search_query 无条件把上一轮客户问题拼进检索词,
客户换话题时旧话题的检索结果被带了回来。在金融场景里这比"引导转人工"糟得多:客户问 A
得到 B 的答案会直接失去对客服的信任,而"答不了"至少是诚实的。

改为只在两种真正需要上文的情况下拼接:句子里有明确指代词("这个产品""该基金"),
或短到不构成完整意图("那它风险高吗"只有 6 个字)。指代词刻意不收单字"它/他"——中文里
"其他产品"会被误判,而这类短句已经由长度规则覆盖。

验证:换话题场景第二问回到 FAQ-0016 的到账时间,指代追问仍命中季季盈产品块;
ruff / mypy(113 文件) / 457 unit+contract 全绿。
2026-09-10 22:17:34 +08:00
lzf_0626 dbe7285c1c feat: 短期会话记忆(多轮指代可解析)
一、此前的缺口
方案 §2.2 要求会话短期记忆,但底座**没有任何加载历史消息的代码**:conversation_message
存了全部消息、svc_conversation_session 只在计数,而 run 执行时只拿到当前这一条消息。
后果是客户问"那它风险高吗"时"它"无从对应,向量检索落到无关内容、整条回答走兜底——
多轮对话事实上不可用。

二、实现
1. 契约:AgentRequest 新增 `history: tuple[ConversationTurn, ...] = ()`(默认空元组,
   既有构造点无需改动)。ConversationTurn 只保留 role 与正文,不把意图/置信度等内部字段
   喂给模型——既减少噪声,也收窄"模型看到不该看的东西"的面。
2. 加载:WorkerRuntime._execute_claimed 构造 AgentRequest 时加载本会话此前的对话
   (上限 10 轮,按 id 正序)。`before_message_id` 排除本轮请求消息本身,否则模型会在
   上下文里看到自己的问题被重复一遍。
3. 使用:客服 Agent 构造检索查询时,把最近两轮**客户**消息与当前问题拼接。只取客户的
   话、不取 Agent 自己的回答——把后者拼进来会让检索偏向自己上一轮的说法,而客户的真实
   意图可能已经在下一句里被修正。

三、两个刻意的取舍
· **不引入 Redis 双写**:方案 §2.2 设想用 Redis 列表,但消息在受理时已落库,再同步一份
  只会带来不一致与 TTL 管理成本,换来的仅是一次索引查询的节省。这里取等价语义
  (同样"最近若干轮、超出即截断")而不复制存储。
· **按条数截断而非 token**:没有与模型一致的分词器,按 token 截断只能估算、边界会随实现
  漂移;按条数是确定性的,宁可少给几轮,也不给一个不稳定的边界。

四、实测(同一会话两轮)
· 第 1 轮"南方季季盈90天的起投金额是多少" → 正确返回该产品表格(R2、起投 1 万元等);
· 第 2 轮只说"那它风险高吗"(不含任何产品名)→ 仍正确检索到同一产品并答出风险等级 R2、
  业绩比较基准与投资范围;此前这类提问必然走兜底;
· ruff 通过、mypy 113 文件无错、unit+contract 447 passed。
2026-09-10 22:01:43 +08:00
zhangshy a94d5c754d feat: 迁移奶龙风控业务模块与演示文档 2026-09-10 21:03:44 +08:00
lzf_0626 1fa5fc7d03 refactor: 客服回答正文只保留固定免责声明
业务方要求客户侧只看到一句固定话术(不构成投资建议),因此从回答正文移除:
1. 中置信的「(以上信息可能不完整,具体以产品说明书与公司制度为准)」提示;
2. 「(依据:…)」出处行——原先它显示的是知识块标题,FAQ 的标题就是问题本身,
   展示为「(依据:公司什么时候成立的?)」并没有可读价值。

可追溯性不受影响:本次命中哪个知识块仍由审计(agent.tool_executed 的工具调用记录)
与消息表留痕,source_references(tool 类型)也照常返回,只是不再面向客户展示。
取舍已在代码注释中标明:中置信回答此后不再向客户标注不确定性。

若将来要把出处展示给客户,应当走 source_references 的 knowledge 类型
(前提是让 ToolExecutor 把工具返回的 doc_id 登记为本次可引用来源),
而不是继续往正文里拼字符串。

同时移除因此不再使用的 INCOMPLETE_NOTICE 常量、_source_note 方法,
以及提问工具里那句"正文没有依据行"的提示。

验证:ruff 通过、mypy 107 文件无错;customer_service_check 9/9 通过;
ask_customer_service 实测回答正文为「答案 + 免责声明」两行。
2026-09-10 20:33:13 +08:00