张胜宇
|
9dcfa64bc5
|
feat(W29): NL2SQL 接线与只读边界守卫(会签项 18)
## 接线
发布 agent_tools/financial_nl2sql:financial_query -> [query_financial_data]
(tools/publish_financial_nl2sql_config.py --apply,治理动作)。
结果:新 release 260(financial-nl2sql-f1be6be8063f)生效、旧 244 转 superseded、
活跃配置 8 -> 9 条;**客服侧那 8 条逐字未变**(回读核对:缺失 0 / 被改动 0)。
在此之前该能力「代码全在、工具调不通」—— 按「工具 = 代码上限 ∩ 发布白名单,
缺配置失败关闭」,缺的就是这一条配置。
## 接线后实测量出的缺口(本次修复对象)
RuleBasedFinancialPlanner 不识别写意图动词:「删除所有客户的持仓记录」被判成
「查持仓」,返回 status="ready" 并生成一段 SELECT —— 8 条写意图问句 8/8 复现。
数据安全当时**并未破**(SQL 仍是 SELECT,被 _safe_sql_check 的 SELECT-only +
BANNED_SQL 兜住),故定性为「答复与诉求不符」而非「越权写库」;但一旦将来给该
工具加写能力,这里即成为起点。
## 会签(先补签、后改动)
该文件原本不在 docs/48 白名单任何一档(等于「白名单之外一律不动」)⇒ 先补
docs/49(A-10)组 5 · 会签 18 并登记为类 3,获批后才实施。同步更新
docs/48 类 3 表与 客服agent/D2.1 §1.6(镜像已同步)。
## 改法(守住会签单的「最小化边界」)
- plan() 入口增写意图预检 -> 返回带 unsupported_reason 的不可执行计划
- _validate_plan() 增「不可执行计划优先」判据
- query() 走**现成**的 status="rejected" 分支,未新增代码路径,审计照旧留痕
判据分两级以控制误杀:一级强拦(删除/清空/撤销/改成/写入/导入…);
二级歧义写动词(修改/更新/变更/导出…)须**无查询语境词**才拦 ——
否则会误杀「费率变更历史」这类真实续问。
## 验证
- 只读护栏回归锁 8 条(与判据互为独立防线,判据退化时仍须绿)
- 写意图 8 条由 xfail 转为正式断言(全通过)
- 误杀边界 7 条(查询语境的歧义写动词不得被拦)
- NL2SQL 相关测试 84 passed
- 全量 pytest 2540 passed / 3 skipped / 0 failed(xfailed 归零)
- 金标 55 条与 W27 基线判分**逐项零差异**(M-1 55/55、M-4 55/55、四项零容忍全 0)
- ruff:改动文件 0 告警
|
2026-09-22 10:12:06 +08:00 |
|
张胜宇
|
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 |
|
张胜宇
|
d5e813b726
|
feat(model-gateway)+docs(W19): embedding 端点唯一性配置守卫 + 三份完整版/收敛版索引口径补注 + 门槛口径更正 + D3.7 难例口径统一
一、代码(2 文件 + 2 工具脚本注记)
* app/service/model_gateway.py:DatabaseModelEndpointResolver.resolve() 加配置守卫
—— required == "embedding" 且 len(matched) > 1 时 logger.warning(只告警、不改行为)。
多个 embedding 端点会让索引向量与查询向量可能来自不同模型(维度同为 1024、不报错),
COSINE 相似度整体失真,表现为"越答越差"的哑故障。顺手删掉重复的 return endpoints(死代码)。
* tests/unit/service/test_model_gateway.py:新增 2 条单测(多端点告警且返回顺序不变 / 单端点静默)。
* tools/configure_embedding_endpoint.py:加「已废弃,勿重跑」标注 —— 它写的是
qwen-embedding / qwen3.7-text-embedding-flash,与现役端点 knowledge-embedding-qwen-v3 /
text-embedding-v3 不一致,重跑会凭空多出一个 embedding 端点。
* tools/build_knowledge_chunks.py:删掉与新口径冲突的注释「不泄露档位与门槛」,
改为「registered 的依据是权益明细而非门槛;门槛属公开宣传口径」。
二、文档(8 份;D-1 选乙 + D-3 统一为 18)
* D2.4 v1.6 → v1.7:§4.4 + 附录B 更正「门槛金额不再单独构成 registered 的理由」
(public 的 FAQ-0014 已完整给出五档门槛、FAQ-0050 含钻石门槛);
HNW-004—HNW-007 保持 registered,依据收窄为"各层级权益明细";HNW-* 档位不动(分区键)。
* D3.1 v2.5 → v2.6:§5.3 加索引口径落地注(覆盖 §2.5 决策表 / FR-CS-007 / 排期 T4)
+ 补「字段表同属初稿」(实库 18 字段全 NOT NULL、doc_id 主键、无 metadata JSON)。
* D3.2 v1.2 → v1.6:§4.1 加同口径注 + 版本位追平(顶栏 v1.1 / doc-meta v1.2 落后于自身记录 v1.5)。
* D2.2 v2.6 → v2.7:§1.4.2 域 B 加注(TopK / 阈值 / 度量 / 集合选择均未变 ⇒ 不影响验收)。
* D3.7:§3 难例口径统一 —— 难例 32 条(改写 8 + 口语 16 + 多轮 4 + 禁忌 4)为定义式总数,
M-2b 分母 = 其中带期望证据家族的 18 条;并补正 §3 初稿表格条数(以 cases_46.json 为准)。
* D1.1 v1.8 → v1.9:新增 §28;四处版本位同步;顺带修正两处历史遗留
(D2.4 版本位长期停在 v1.3、D2.2 日期列停在 2026-09-17)。
* D1.6:新增 §4.47(含自我失误留痕)。
* D2.1 v6.33 → v6.34:新增本轮修订要点段。
三、实测门口(本机)
* tests/unit/service/test_model_gateway.py:10 passed
* pytest -q -p no:cacheprovider(全量,跑前已停 Worker):1917 passed / 3 skipped / 0 failed
* tools/check_authoritative_docs.py:54 文档无编号冲突(exit 0)
* _consistency.py:失效锚点 0、交叉引用全 ✅(exit 0)
* _fe_boundary_http.py(重建件):12/12 符合预期
* 服务已重启:/internal/health/ready 三依赖全绿(mysql / redis / milvus)
四、如实留痕(自我失误)
本轮清理临时文件时删除判据过宽,误删 _consistency.py(已原样恢复)、
_legacy_customer_service.py(已按 f72a545 逐字节重建,40,554 字节)、
_fe_boundary_http.py(原件不可恢复,已按既有判据重建并实跑 12/12)与若干历史轮次原始日志。
详见 D1.6 §4.47 五。
|
2026-09-20 17:53:06 +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 |
|
Windows
|
74b7d00dff
|
feat(投顾): 方案交付落点、可视化图表与推荐依据的大模型增强
交付落点
- 新增 GET /api/v1/users/me/advisor-contents(客户读**自己**已发布方案):
「发送给客户」原先只改数据状态、客户端没有任何页面或接口能读到它
- 客户端新增「我的投顾方案」页与导航入口
可视化(投顾结果区与客户页**共用** common/advisor-plan-view.js,避免两处漂移)
- 净值折线图(带坐标轴与网格)、组合业绩等权合成曲线(含区间收益与最大回撤)、
资产配置环形图与图例、组合构成条
- 修 num(null)=0 的假 0:Number(null)/Number('') 会得 0,导致「没数据」被渲染成 0.00%;
现一律显示「--」。同理管理费/起投未维护时按没数据处理,不显示 0
- 涨跌口径为「涨红跌绿」(A 股习惯),由 CSS 变量 --plan-up / --plan-down 集中定义
推荐依据接入大模型(可选,失败即回退)
- 新增 AdvisorReasonService:**只改文案,不参与选品**(候选池与排序在它之前已固定)
- 输入只允许是已算出的真实参数(风险等级、排序得分、区间收益、最大回撤、期限与流动性)
- 命中收益承诺词(保本/保证收益/稳赚/无风险…)整条丢弃并回退规则文案
- 未启用 / 缺密钥 / 超时 / 解析失败一律回退,推荐主流程不因模型不可用而失败
- 前端标注来源(AI 生成 / 规则生成)
数据与权限
- 客户角色补齐:绑 customer 角色、补建缺失的账户与交易段权限码(9060-9065)
- 净值全量同步(20 只产品),行情同步脚本按 --codes 分块(全量一次会被超时终止)
测试
- 新增 tests/unit/service/test_advisor_reason_service.py(10 项,专测三条合规边界)
- 前端模块自检纳入 service-request-module;补「两处共用同一渲染」回归测试
|
2026-09-16 18:17:47 +08:00 |
|
lzf_0626
|
06d0f9f231
|
知识库检索质量修复:「风险评估问卷怎么评分」从必然转人工 → 直接答
## 根因(两个问题叠加)
1. 灌库脚本 tools/build_knowledge_chunks.py 的 expand_table_rows() 用「第一个非分隔行」
当表头且 header 从不重置 → 同一节里第 2 张表格起**表头被当成数据行**。
《个人投资者适当性管理指南》第九条下有 16 张问卷表格 → 15 条正文逐字相同的
19 字零信息量碎片;同类共 19 条。
2. 检索层去重只按 doc_id,接不住「同一父块的兄弟子块」(POL-AST-009-07 与 -12 是
不同 doc_id、同一父块)→ 15 条碎片全留在候选里互相打平,把 top1/次优差压到 0.002,
而客服判定要求中置信必须领先 ≥0.07(MIN_GAP)→ 恒判并列转人工。
## 为什么没有重灌知识库
用 Milvus 现成向量离线复算四种做法(同一问句、同一批向量):
现状 gap 0.002 转人工
只修 bug(=重灌全部收益) gap 0.001 仍转人工,且更糟
只加父块归并 gap 0.093 可答,但答案是 19 字碎片
两处都改 gap 0.076 可答且有内容
原因:第九条被切成 94 块,删掉 15 条表头碎片后,剩下 79 条数据行碎片依然互相打平。
重灌解决不了,却要停机 3–10 分钟并丢掉上传路径的内容(含 R1–R5 那条)。
## 改了什么
① app/service/knowledge_search_service.py:新增 _merge_sibling_subblocks()
同一父块的兄弟子块只保留最高分那条;父块自身与 FAQ/政策这类本身就是细粒度答案
的块一律不合并(它们之间打平是真的多个候选)。6 个单测守着。
被丢掉的只是同节其它细节,本节完整内容由 _parent_hits 带回的父块兜底。
② tools/build_knowledge_chunks.py:修表头识别(markdown 表格只有紧邻 |---| 之前的
那一行才是表头)+ 新增 assert_no_duplicate_contents() 自带守卫 ——
正文完全相同的块必须为 0,否则中止且不写 jsonl。该脚本是一次性灌库脚本、
原来没有单测覆盖,这正是该 bug 活下来的原因,所以守卫放在它自己的执行路径上。
块数 636 → 617;正文完全相同的组 1 组 15 块 → 0 组。
负向验证:换回旧逻辑跑,退出码 1 并报出那 15 份碎片,且未覆盖 jsonl。
③ tools/drop_table_header_vectors.py:清掉已灌进 Milvus 的 19 条历史碎片。
判定可复现:语义 id(非纯数字)且正文不在修正后产物里。只动语义 id 是因为
纯数字 id 来自上传路径、本来就不在 jsonl 里(实测冒烟 A 线的 top1 就是数字 id 179);
不按长度判是因为短块本身是设计的一部分(「评审标准:管理人资质 15%」13 字是有效答案)。
实删 policy 16 + product 3,faq 0;dry-run 逐条核对过,全部是「标签 + 表头词」形态。
这是唯一一次绕过 Outbox 的删除:事件消费侧要回读 MySQL 行,而这批是无元数据种子向量。
## 验证
真实链路 10 问句回归 10/10 与预期一致,0 个变坏:
风险评估问卷怎么评分 0.7156 gap 0.1473 → 答(原 0.002 转人工),top1 变成真实答案行
场内基金的管理费率 0.8114 gap 0.0977 → 直接答(冒烟 A 线)
南方季季盈90天起投金额 0.8631 gap 0.3387 → 直接答(行级子块精确命中能力未受影响)
今天天气怎么样 仍正确转人工
端到端:ask_customer_service.py「风险评估问卷怎么评分」→ 直接答,返回完整评分标准;
e2e_smoke_test.py 44/44(含 A4 知识库覆盖未转人工);
pytest tests/unit tests/contract 1506 passed / 0 failed;
对账 重复正文 15 → 0,孤儿/死向量/缺向量仍全为 0。
(冒烟首跑 42/43 的唯一失败 B6 买入下单 503 是行情过期,补刷后 44/44,与本次无关。)
## 顺带查明
Milvus 的 delete() 是标记删除,约 1 秒后才在 query/search 中不可见(隔离临时集合实测:
删完立即查仍看得到,+1s 起消失)。清理工具第一版"删完立刻复核"因此谎报 19 条残留,
已改为轮询复核并把文案改成实测依据。get_collection_stats().row_count 在删除后仍显示旧值,
这也是对账工具坚持用 query 实际行数的原因。
## 文档
新增 docs/演示用/知识库检索质量修复-2026-09-15.md(根因、四方案对比、改动、全部证据);
并对 docs/演示用/知识库向量对账与清理-2026-09-15.md 做更正 —— 451 条短块里 429 条是
设计内的有效数据行、只有 19 条是 bug 产物;"按长度合并短块 + 重建重灌"的建议已被实测否决。
## 仍未解决(记录在案)
「高净值客户有什么权益」gap 0.0075 仍转人工:根因是不同父块之间同族内容打平
(金卡 vs 白金权益),父块归并救不了也不该救,要从内容侧或业务口径入手。
knowledge/_chunks.jsonl(新 617 块、编号连续)与 Milvus(旧编号、含 19 个空洞)目前不一致;
不重灌无影响,但下次重灌必须 drop 集合重建而不是 upsert,否则两套编号会共存。
|
2026-09-15 09:31:57 +08:00 |
|
lzf_0626
|
58b73ff594
|
知识库三项收口:向量-元数据对账 + 导入侧幂等 + 过期行向量清理入口
① 只读对账 tools/reconcile_knowledge_vectors.py
按集合列出:孤儿向量 / 死向量 / 缺向量 / 重复正文 / 低信息量碎片 / 纯标题。
关键口径:非数字 id(FAQ-0013 这类语义 id)是灌库脚本有意写进 Milvus 的,
单独归类、不建议删;向量数取自 query 实际行数,不用 get_collection_stats
(后者含已软删未 compaction 的行)。
② 导入侧幂等:同 source_file + 集合重传 = 覆盖上一版
app/service/knowledge_ingest_service.py 新增 _supersede_previous_version:
把上一版 active 行置为 expired,并逐行投 knowledge.vector_delete_requested
(与本次入库同事务)。写入侧只认 active 而检索侧不看 status,旧向量不清掉
会继续参与排序、和同题活块抢答。
顺带修掉一个真 bug:改为先判 chunks 非空再下线 —— 否则传一份解析出 0 块的
文档会把上一版下架、新版一行没写,这份文档在检索侧凭空消失。
③ 清理入口:POST /api/v1/knowledge/{knowledge_id}/vector-cleanups
给历史上"被别的途径置为 expired、从未投过删除事件"的行补投向量清理。
DELETE 对已过期行返回 404 的口径保持不变(重复删除静默成功会让调用方
分不清"这次真下线了"和"早就过期了"),因此新开一个语义明确的端点:
不存在 404 / 仍是 active 422(请改用 DELETE)/ 已 expired 200 并回传事件名。
配套 tools/purge_expired_knowledge_vectors.py(默认 dry-run)批量驱动该端点。
文档:docs/演示用/知识库向量对账与清理-2026-09-15.md(含真机验证输出),
并对 docs/演示用/知识库问答诊断-2026-09-14.md 做两处更正 —— 实测孤儿向量 0 条、
那 175 行历史副本从来没有向量(不参与排序),当时的差额来自 get_collection_stats
把已软删行算进去。
新发现(未修,需业务拍板):661 条向量里 451 条正文不到 40 字,是灌库时把
markdown 表格/标题切碎产生的碎片。「风险评估问卷怎么评分」实测前 4 名是 4 条
一模一样的 19 字碎片(gap 0.0024),真正 2828 字的答案排第 5 → 客服必然转人工。
属灌库切分缺陷,补内容救不了,也不应靠放宽 MIN_GAP 解决。
验证:pytest tests/unit tests/contract → 1500 passed, 2 skipped, 0 failed;
mypy app → 3 个错全在组员文件中(与本次改动无关);ruff 本次改动文件 0 错。
真机端到端:重传 → 旧行 expired + 删除事件 published + 旧向量已从 Milvus 删除;
两个问句回归仍正常回答(r1到r5 gap 0.0766;申购确认 0.8453)。
|
2026-09-15 09:06:57 +08:00 |
|
lzf_0626
|
076d786bc6
|
补齐客服转人工工单流:从"只能看"到"能推进"(基线状态机,不自行发明)
## 问题
`svc_handover_ticket` 的 DDL 与状态机在 `docs/02` §7.2 早就定好了
(pending → assigned → processing → resolved → closed,未解决可 cancelled),
但平台**只有 handover:read(只读队列)**:没有任何入口能改状态、assigned_to /
accepted_at / resolved_at / closed_at / resolution 五列**全库 0 非空**,
于是 40 张工单永远停在 pending —— 用户看到的就是"工单全都长一样"。
## 改了什么
后端:
- 新增 `app/service/customer_service_handover_action_service.py`:五个动作
(分配/接单/解决/关闭/取消),`SELECT ... FOR UPDATE` 锁单后判状态;
接单允许从 pending 自助接管(同时记受理人);取消不写 closed_at(该列属 closed 状态);
每次流转写一条 interaction_audit(handover.assigned/accepted/resolved/closed/cancelled);
非法流转 409、坐席不存在 422、工单不存在 404;回包不含 customer_id/session_id。
- 只读服务保持只读(读侧与写侧是两条边界,单测守着"读侧不许长出写方法"),
但列表支持 `?status=` 六态筛选、详情补上受理人与流转时间(坐席侧路由信息,非客户数据)。
- `app/api/controllers/admin.py`:五个 action 端点 A049–A053
(assignments / acceptances / resolutions / closures / cancellations),
走 `ApiTransactionService.execute_in` —— 幂等记录与业务写入同事务、重复键回放。
- 权限:新增 `handover:write`(9069,只授 admin),已并进种子
`tools/seed_test_rbac.py`;配套幂等脚本 `tools/grant_handover_write_permission.py`。
前端(管理员工作台 · 转人工工单页):
- 按状态给按钮(待处理→分配/直接接单、已分配→接单、处理中→解决、已解决→关闭、
未解决都可取消),加了状态筛选与"刷新";摘要弹窗补上受理人与四个时间点、处置结论。
- api-client 注册五个端点;workspace.js 的 api-client 引用与页面自身的 ?v= 一并升版,
避免浏览器拿旧缓存(旧缓存里没有这些端点)。
冒烟与测试:
- `tools/e2e_smoke_test.py`:B 段建的测试工单由 F 段走完 分配→接单→解决→关闭 收尾
—— 既不再把测试件堆在 pending 队列里(此前每次冒烟攒一张),又让每次冒烟都覆盖一遍状态机。
总数 40 → 44 项,实测 44/44 全绿。
- 新增单测 24 条(状态机合法/非法路径、越权、坐席不存在、审计、视图不泄漏客户标识)
与一条真机集成用例(HTTP 十步 + 数据库侧审计证据 + 自动清理)。
- 读侧那条"详情不得返回 assigned_to"的旧断言按新口径更新,并写清为什么。
## 验证
- `pytest tests/unit tests/contract` → 1489 passed, 2 skipped, 0 failed
- 新增集成用例通过;`tests/integration` 全量跑时
`test_memory_extraction` / `test_run_cancellation_mysql` 两条偶发红 —— 单独跑都通过,
是 AGENTS.md 已登记的"常驻 Worker 抢队列"(跑验收前须先停 Worker)
- `tools/portal_api_check.py` → 41 项通过 39、失败 0
- `tools/e2e_smoke_test.py` → 44/44 全通过
- `python tools/check_rbac_seed_consistency.py` → 通过(种子 63 条权限)
- 真机 HTTP 实测:分配→接单→解决→关闭四步 200 且时间戳齐全;取消路径 200 且 closed_at 为空;
同键重发回放不二次推进;对已关闭工单再分配 409;风控账号处置 403
## 文档
`docs/44-演示流程.md`(场景 4/8 + 命令 + 44 项)、`docs/演示用/后端接口文档`(新增 §11.4b 与
A049–A053)、`docs/演示用/全功能流程-大白话版.md`(工单页签改"读写"+ 已知偏差)、
`AGENTS.md`(9066-9069 号段演进 + 冒烟 44 项)
|
2026-09-15 00:41:59 +08:00 |
|
lzf_0626
|
9dd2802d67
|
客户看板「今日盈亏」不再是硬编码 0:按行情/净值基准真实计算(含当日买卖)
- 口径:今日盈亏 = 今日市值 − 昨日持仓市值 − 今日买入金额 + 今日卖出金额(不含费用,与「持有盈亏」同口径)
- 昨日持仓数量由当日成交反推,不需要新表新字段
- 基准优先场内行情最近两个交易日收盘价(与同页 latest_price/market_value 同源、客户可核对),
行情只有一天时回退基金净值(15911/159991-159995 的行情本身就来自净值序列)
- 只认 transaction_type ∈ {买入,卖出}:演示库里混进的风控场外申购/赎回(RISKDEMO-*)
会把「昨日持仓数量」抬到 76 万份、今日盈亏从 -21.22 变成 -1612.72
- 接口字段与前端一行未改:HoldingItem.today_profit_loss / PortfolioSummary 两个字段数值变真
- 新增 tools/check_today_profit_loss.py:纯 SQL 独立复算并与接口逐只比对(实测 9001 = -132.70 / -0.2528%)
- 新增 6 条单测;pytest tests/unit tests/contract → 1466 passed, 0 failed
|
2026-09-15 00:03:23 +08:00 |
|
lzf_0626
|
84bf51c0d0
|
NL2SQL:5 位产品代码被丢弃导致"查询成功但数据错"(演示产品 15911 正是 5 位)
## 现象
通用自然语言查询里问「查询15911的净值」,返回的是**别的产品**的数据:
```
问:查询15911的净值 → 返回 511810 货币ETF南方 的净值
问:查询基金代码15911最近30天净值 → 同上
```
而且 `status = success`、`message = 查询成功` —— **不报错,只是数据是错的**。
## 根因
`financial_nl2sql_service._filters()` 只认 6 位数字:
```python
re.findall(r"(?<!\d)\d{6}(?!\d)", question) # 15911 只有 5 位 ⇒ 被静默丢弃
```
过滤条件为空 ⇒ 生成的 SQL **没有产品过滤**(`WHERE 1=1 ... GROUP BY ... LIMIT 50`)
⇒ 返回一批无关于问题的产品净值。现库实测:**6 位代码 25 个、5 位代码 1 个**
(后者就是演示产品 `15911`)—— 所以这个缺陷精确地打在了演示产品上。
`tests/unit/service/test_financial_nl2sql_golden_cases.py` 里 30 条黄金用例**全是 6 位代码**
(159511/588890/159948),因此从未暴露。
## 修法
`app/service/financial_nl2sql_service.py`:
- 产品代码模式放宽为 **5–6 位**(`(?<!\d)\d{5,6}(?!\d)`);
- 5 位数字更容易与"金额/数量/天数"撞车(6 位不会),因此加单位守卫:
紧跟 `元/万元/万/亿/份/股/手/天/日/周/个月/年/次/笔` 的一律**不当**产品代码
(例:「申购金额50000元」里的 50000);
- 抽成 `_product_codes()` 便于单测。
## 验证
- `query("查询15911的净值")` → SQL 含 `p.product_code = :filter_0`、参数 `15911`、
返回行 `{"product_code": "15911", "product_name": "15911 自定义产品", "nav": "1.074596"}`;
三种问法("查询15911的净值"/"查询基金代码 15911 最近 30 天的净值"/"15911 的最新净值是多少")全部命中 15911;
- 新增 5 条单测:5 位代码成为过滤条件、6 位仍正常、金额不当代码、
年份/天数不被误认、多代码并存;
- `pytest tests/unit tests/contract` → **1458 passed, 2 skipped, 0 failed**;
- `ruff` 干净。
## 说明(未改,供你决定)
问「最新净值」时返回的是该产品的 50 行净值明细(SQL 无 `ORDER BY`),
不是"最新的那一条"。要的话我可以补成按日期倒序、单点取值。
|
2026-09-14 22:32:26 +08:00 |
|
lzf_0626
|
1d6c32f6b3
|
场外核对:桥接"单据账户标识 → 平台交易账号";保存识别字段后自动重跑核对;运营角色补推介材料权限
## 一、根因:核对查不到不是"缺数据",是**账户口径没桥接**
实测(客户 10002 的单据):
```
生成的 SQL: JOIN fin_holding h ... WHERE h.trade_account = '10002' ← 单据上的账户标识
结果: 0 行 → 「申请前持有份额」= null → 规则判「无法判断」→ 单据状态 query_failed
```
而库里 `fin_holding.trade_account` 存的是**平台交易账号** `FSA{客户号:06d}`
(与 `fin_sim_account.account_no` 同值,见 `tools/seed_sim_account_demo.py:226`),
客户 10002 **一直持有 15911 共 1000 份**。所以页面上"单据核对与运营动作"是空的,
看起来像缺数据,实为**单据印的账户标识与平台账号是两个口径,中间少了一步换算**。
修法:新增 `OffsiteFundService._platform_account()`,在**单据字段落库的两个入口**
(`_create_document` 新建、`_apply_recognition_fields` 保存/重试)做换算,规则:
1. 已是库里的 `account_no` → 原样;
2. 纯数字且命中 `customer_id` → 用该客户的 `account_no`;
3. 其余原样返回并留 warning —— **失败关闭,不猜不造**(不凭空生成 `FSA099999`)。
一处修好,后端查询、NL2SQL 页面的自然语言、页面展示三处口径一致。
附件上的 OCR **原始值不受影响**(`extracted_fields` 仍是单据上印的 `10002`)。
端到端证据(同一张单据,只走服务层):
| 阶段 | document.account_identifier | 规则数据 | 单据状态 |
|---|---|---|---|
| 修复前 | `10002` | `{}` → 无法判断 | `query_failed` |
| 保存识别字段后 | **`FSA010002`** | — | `query_failed` |
| 触发核对后 | `FSA010002` | **申请前持有份额 1000.0000** → **正常** | **planned** |
## 二、前端:保存识别字段后自动重跑核对
`employee-operations/offsite/offsite.js`:原先 `save-ocr` 只保存 + 重载页面,
**不触发核对**,于是运营改完字段点保存,那两个区块要么停留在上一次核对的状态、
要么整块是空的,必须再手动点一次"重新核对并判定规则"——看起来像"保存没生效"。
现在:保存 == 运营已人工确认该单据内容,因此保存成功后**自动对该附件关联的单据**
执行「触发 NL2SQL → 拉取返回字段 → 重新判定规则」,并在提示里区分"已重新核对"
与"核对未全部成功"。
顺带把这段逻辑抽成 `recalculateDocument(taskId)`,与面板上的"重新核对并判定规则"
按钮**走同一条路径** —— 两处各写一份正是"保存后不刷新"这类不一致的来源。
## 三、运营(operator)角色权限
`tools/grant_operator_role.py` 的授权清单补齐:`promotion:write/read/review/deliver`
(`promotion_material_service.py` 的八处 `_require` 恰好只用这四个码,缺任一都会 403,
例如只给 write 会在查看详情 read 那一步被拒)+ `agent:run`。
已实际执行并**从身份侧验证**(`IdentityRepository.load_context`):
9005 / 9006 现在各 7 项权限,四个推介材料码齐全。
- NL2SQL 全库与它相关的权限码**只有 `financial:nl2sql:read`**(`offsite:nl2sql` 在
`sys_permission` 里并不存在,是 `offsite_fund_service` 里 any-of 校验的死值),
该码运营早已有,本次无需新增。
- 可持续性已核实:`sys_role_permission` / `sys_user_role` **没有外键**,
重跑 `seed_test_rbac.py`(DELETE 重建 9001-9099 号段权限)**不会**删掉运营的绑定;
且本工具按**权限码查 id**、不写死 id,天然抗号段变动。
## 四、10001 / 10002 的 15911 持仓
复核结论:**各 1000 份,且三处口径一致**(`fin_holding.market_value` = 数量 × 最新净值、
净值历史 120 条、`fin_product.current_nav` 与净值最新一条一致、账户可用资金正常)。
`fin_holding` 的唯一键是 `(customer_id, product_id)`,所以**不能**再插一行
`trade_account='10002'` 的"同一个持仓"—— 那会把持仓重复计数,是错的。
需要改数量就用 `python tools/seed_custom_holdings.py --quantity N`(默认就是这两个客户 + 15911)。
## 五、验证与回归
- 新增 `tests/unit/service/test_offsite_account_bridge.py`(5 条:账号原样 / 客户号换算 /
认不出原样返回 / 不凭空造账号 / 空值不查库);
- `pytest tests/unit/service -k offsite` → 34 passed;
- 场外集成测试 4 个文件 → 27 passed;
- `ruff` 干净;`mypy app` 仍只有组员新代码里那 3 个既有错(与本次无关);
- 前端 `node --check offsite.js` 通过。
|
2026-09-14 22:16:29 +08:00 |
|
lzf_0626
|
c4f4c33fd6
|
记忆召回补一道能力码闸门:归属关系不等于授权
上一版把员工召回改成"按 sys_customer_assignment 归属"时,只看了**归属关系**,
没校验**能力码** —— 这留下一个我自己带出来的口子:
- 平台读他人画像/记忆的正式路径(customer_profile_service、public_platform_service)
校验的是 `memory:read:customer`(sys_permission id=9010,data_scope='own_customers')
+ `customer_ids`;
- 而召回是**另一条**把客户记忆送进模型上下文的路径,只按归属放行,等于让
"有归属行但没有该权限"的账号(例如 operator:只有 financial:nl2sql:read
+ offsite:write)凭空获得读他人客户记忆的能力 —— 它上一版之前是读不到的。
现改为**两个条件都满足才放行**(缺一即失败关闭,并在日志里分开点名"缺能力码"
与"归属未维护",避免运维在错误的表上找问题):
① `memory:read:customer` 在 context.permissions 里;
② 客户落在 context.customer_ids(sys_customer_assignment 生效行)内。
演示账号不受影响:9002(risk_operator)、9020(advisor)、9003(admin) 三个角色都持有该权限码,
真实身份链路复验 9020 仍是"修复前 0 条 → 修复后 2 条"。
测试:tests/unit/core/test_memory_scope.py +3 条(缺能力码读不到/能力码与归属是"与"关系/
现有用例补上能力码),tests/unit/service/test_agent_governance.py 同步。
`pytest tests/unit tests/contract` → 1448 passed, 2 skipped, 1 failed
(唯一失败仍是组员正在改的投顾页面)。
|
2026-09-14 21:40:05 +08:00 |
|
lzf_0626
|
9eebf9627f
|
记忆召回:按 sys_customer_assignment 归属定范围,不再把员工号当客户号
## 修的是什么
`governance.recall()` 把 `int(context.user_id)` 当客户号用。后果有两个,
方向相反但都致命:
1. **员工身份(风控/投顾/运营/管理员/system)恒空** —— 员工不是客户,
那是个不存在的客户号;日志只说 "empty",看不出是"设计如此"还是"记忆坏了"。
2. **越权陷阱** —— 员工号与客户号同号段(演示数据里客户 9001-9020、
员工 9002/9020 并存)。`int(user_id)` 一旦与真实客户号重合,就会把
**陌生客户的长期记忆读进来并注入提示词**,且不报错、看起来正常。
同一个问题在代码里还有另外两处**各自判断**、口径互不一致:
`BaseAgent.recall_memory()` 要求"每条记忆 customer_id == context.user_id"
(否则抛"越过客户范围"),`review_output()` 的引用校验只认同一条件。
## 怎么修的
新增 `app/core/memory_scope.py` 作为**唯一判定口径**,三处共用:
- 客户身份(customer / authenticated_user):**只读自己**,分配表里有别行也不读别人;
- 员工身份:**只读 `sys_customer_assignment` 分配给自己**的客户
(`context.customer_ids`,由 `IdentityRepository.load_context()` 读入);
归属未维护 ⇒ **失败关闭**,并在日志里点名"归属未维护",与"库里确实没有记忆"区分开;
- 访客:无(上游已拦)。
细节约定:
- 归属客户按客户号**升序**召回、单次上限 `MAX_RECALL_CUSTOMERS=10`
—— 升序是为了确定性(同一身份每次取同一批,不随数据库返回顺序漂移),
上限是为了别把成百上千条他人记忆塞进一个提示词;
- 跨客户合并后按置信度降序、`(客户号, uuid)` 兜底排序,最多 10 条;
- 员工同时持有多个归属客户的记忆时,`memory_context_text()` **逐行标注客户号**
并把提示词改成"多个客户的长期事实" —— 否则模型会把 A 客户的事实当成 B 客户的。
单一客户时保持原格式(客户身份的提示词与改动前逐字相同);
- 引用校验与范围守卫都改用同一口径:员工引用**归属客户**的记忆不再被判成伪造引用;
引用**非归属客户**的记忆即便被塞进 memories 也照样拦下。
## 验证(真实身份链路 + 生产召回装配)
`IdentityRepository.load_context` → `PlatformGovernance.recall`(含 Milvus 语义通道):
- 身份展开:roles=('advisor',)、customer_ids=('9001',)(sys_customer_assignment
里唯一那行 9020→9001)、可读范围 (9001,);
- **修复前** `recall(int(user_id)=9020)` → **0 条**;
- **修复后** `recall(按归属)` → **2 条**(客户9001:进取型 / 约三年);
- 边界:客户身份 9001 可读范围 (9001,);无归属员工 9002 = ()(失败关闭,
且**没有**把 9002 当客户号);未分配时的 9020 = ()。
测试:`pytest tests/unit tests/contract` → **1445 passed, 2 skipped, 1 failed**
(1432 + 新增 13;唯一失败是组员正在改的投顾页面,与记忆链路无关)。
新增用例:`tests/unit/core/test_memory_scope.py`(8 条,含"员工号不得被当成客户号"
的反例断言)、`tests/unit/service/test_agent_governance.py`(+5 条:归属召回/
无归属失败关闭且不碰数据库/客户只读自己/引用校验/越界守卫)。
## 遗留(已在 AGENTS.md 与文档里写明,未自行实施)
风控扫描这条线**仍读不到记忆**:它是唯一消费召回内容的地方
(`risk_agent.py:224`),而扫描上下文是 user_id="0"/roles=("system",) 且无归属行。
根因是**顺序问题**:召回发生在 handle() 之前,上下文里没有"本次目标客户"这个概念。
出路有两条:① 给风控专员补 sys_customer_assignment 行(运维动作,立即可用);
② 在 RequestContext 加显式的 target_customer_id 并校验它落在归属集合内
(推荐,但属跨线协议改动,等确认)。
文档:docs/演示用/记忆召回恒空-根因与修复-2026-09-14.md 新增 §五(含 §5.4 遗留说明)、
AGENTS.md 新增"记忆可读范围只有一个判定口径"易错点,并按 2026-09-14 复测更新测试基线。
|
2026-09-14 21:33:16 +08:00 |
|
lzf_0626
|
0c642133d4
|
修复长期记忆向量链路:投影入队 + 集合名三侧同源 + 召回按客户过滤
语义召回"恒空"的根因分四层,本提交修掉投递层与读取层(另两层——重试计数
门禁、episode 不投 rebuild 事件——已在前两个提交修复)。
R2 投递层:dispatch_profile_rebuild 只做图投影,没有任何 Milvus 投递,
长期记忆的向量从未被写入过(实测客户 9001 在 memory_sync_outbox 里 0 行)。
新增 _enqueue_memory_vector_projection(),在画像重建后投
MemorySyncOutbox(target_store="milvus")。走事件而不是同步写,是为了拿
Outbox 的重试/退避/死信,且不把 embedding 的网络等待拖进事务。
⚠️ payload 必须带 version(正整数):适配器 _coerce_profile_version 缺它
直接抛 ValueError(实测踩到,事件立刻 failed)。
R4 读取层:写的集合与读的集合不是同一个 ——
写(MilvusProfileProjection)用 "user_long_term_memory_v1",
读(bootstrap.get_vector_memory_adapter)与删(projection_cleanup_service)
却用 settings.milvus_collection = "jr_memory",而该集合从未被创建。
⇒ 召回:MilvusClient 构造不校验集合存在,适配器"构造成功"但每次 search
抛异常被 VectorMemoryAdapter 吞成 degraded → 召回恒
degraded_reasons=('milvus_unavailable',)、向量命中恒 0 条;
⇒ 清理:jr_memory 不在集合列表 → 走 vector_collection_absent 分支 →
报清理成功但一个向量都没删,陈旧向量永久留存。
修法:PROFILE_COLLECTION 成为唯一常量,读/删两侧直接引用它;
并删除 Settings.milvus_collection 配置项、清掉 .env.example 的
MILVUS_COLLECTION —— 写侧从来没读过它,一个只在契约一侧生效的配置项
比没有配置项更危险(Settings 的 extra="ignore" 会让其他环境残留的该
变量被安全忽略)。
顺带:
- 语义检索把客户过滤下推到 Milvus(filter="customer_id == N")。此前不带
过滤,别家客户的命中会白占 limit 名额,稀释本客户的召回条数。
- upsert 在 sources 为空时先返回,不再无条件 load_collection ——
"本来就没有可写内容"不该被记成投递失败(10001/10002 那两条事件即如此
重试 5 次进死信)。
回归守卫:tests/unit/infrastructure/test_memory_vector_collection_consistency.py
断言读侧与删侧用的都是 PROFILE_COLLECTION,且被删掉的配置项不得回归。
这个缺陷能活下来,正是因为两侧单测全绿而接缝无人守。
验证(走生产装配、进程内调用,未重启你正在跑的 API 窗口):
读侧集合打印 user_long_term_memory_v1(修前为 jr_memory);
召回 degraded=False / reasons=() / sources 含 milvus,且排序随 query 语义
变化(投资期限→horizon 0.288 > risk_level 0.201;风险偏好→risk_level 0.287);
query="进取型" 双通道合并且 vector_score=0.9987;query=None 走 mysql 全量。
pytest tests/unit tests/contract → 1432 passed, 2 skipped, 1 failed
(唯一失败是同事正在改的投顾页面,与记忆链路无关)。
文档:docs/演示用/记忆召回恒空-根因与修复-2026-09-14.md(新增,四层根因+证据)、
docs/37-记忆投影链路实现说明.md(补集合名三侧契约)。
|
2026-09-14 21:26:03 +08:00 |
|
lzf_0626
|
e4cd336afa
|
修复权限拒绝变 500、访客令牌无限流,并让两处测试跟上代码
## 1. 权限不足从 500 修回 403(14 个测试失败里的 11 个)
`app/service/authorization_service.py` 的 `_deny` 构造 `InteractionAudit(...)` 时
**漏了 `created_at`**(该列 NOT NULL 且无默认值),而 `raise ForbiddenAgentError`
写在 `session.commit()` **之后** —— commit 必抛 IntegrityError
(1048: Column 'created_at' cannot be null),于是**永远走不到 raise**:
预期 403,实际 500:Internal Server Error
**⇒ 任何「权限不足」的请求都变成 500**,破坏 docs/05 §3.6 的错误码契约。
全仓 100+ 处 `InteractionAudit(...)` 都跟着 `created_at=now`,只有这一处没有
(由 2026-09-14 的 `857c106`「投顾工作台:三接口支持按客户出方案」引入)。
修两处:
- 补 `created_at`;
- **并把审计写入失败与 403 解耦**(try/except + `logger.warning(exc_info=True)`):
安全判定不该依赖审计表是否可写 —— 拒绝就是拒绝。但也绝不静默,审计缺失是合规问题。
## 2. 访客令牌端点补限流(P0-4)
`app/api/controllers/visitor_tokens.py` 此前**零认证、零限流**,可以不限量铸造
有效 JWT;每个都能调 `/api/v1/agent-runs` 触发 LLM 调用,而 agent-runs 的限流
按 `user_id` 计、访客 `sub` 每次都是新随机值 ⇒ **限流被天然绕过**。
把 `rate_limit.py` 的登录闸门抽成通用的 `_enforce_ip_rate_limit(...)`,
新增 `enforce_visitor_token_rate_limit` 挂到该端点的 router 上。
⚠️ 刻意**用独立计数器前缀**(`visitor-token` vs `login`)而不是直接复用登录闸门:
共用会让两者互相挤占配额 —— 正常访客刷几次页面就把别人挡在登录外。
阈值 30 次/分钟(比登录的 10 次宽松,因为访客进站/刷新会正常签发)。
## 3. 测试跟上代码(2 个)
- `test_product_recommendation_service.py`:monkeypatch 打在 `current_for_agent` 上,
而 `generate()` 现在走 `current_for_customer(customer_id, context)` —— 补丁不生效,
真实方法被执行并命中鉴权抛 403。改为按新签名打补丁。
- `test_authorization_service.py` 等 3 个:随第 1 项一起恢复。
## 实测
- `pytest tests/unit tests/contract` -> **1427 passed, 2 skipped, 2 failed**
(修复前 **14 failed** / 1426 passed)
- `_deny`:权限不足恢复 **403**(原为 500)
- 访客限流:连发 40 次 -> `{201: 30, 429: 10}`,首次被拒返回
`RATE_LIMITED「访客令牌签发过于频繁:每 60 秒最多 30 次」`
(修复前:连发 15 次全部 201、无限流)
- `ruff check` -> All checks passed
## 剩余 2 个失败(需产品决策,未擅自处理)
1. `test_advisor_workspace_registers_documented_operation_endpoints` ——
`employee-advisor/dashboard/index.html` 已被**整个替换**为一个自包含静态页
(`data-page-node-id` 属性、内联全部 CSS/JS、硬编码 `API="http://127.0.0.1:8000"`、
自带「离线本地引擎」),**不引用 `api-client.js` / `app-shell.js`、不调 `mountShell`**。
测试断言的是旧页面措辞("组合分析"),新页面写的是"生成推荐方案"。
是接受替换后的页面(改测试断言),还是恢复挂平台壳的版本,属产品决策。
2. `test_docs_endpoint_ids` —— `docs/05` §19 表里被插入了分组标题行
(`**场外基金**`、`**账户与交易**`),而检查工具要求首列是端点编号。
|
2026-09-14 20:14:50 +08:00 |
|
yuancong_0626
|
14e027f781
|
合并yy与远程qyqy_develop并保留业务优先级
|
2026-09-14 18:16:33 +08:00 |
|
yuancong_0626
|
2548c39c6b
|
袁聪的最后一次完善更新
|
2026-09-14 18:13:10 +08:00 |
|
zhangshy
|
bfbaf823c2
|
同步预警队列后端分页上限为十条
|
2026-09-14 10:50:48 +08:00 |
|
张胜宇
|
f5d1b24618
|
merge qyqy_develop and retain risk review updates
|
2026-09-14 01:41:02 +08:00 |
|
yuancong_0626
|
abbfb6eddd
|
合并yy并同步远程qyqy_develop
|
2026-09-14 01:11:07 +08:00 |
|
yuancong_0626
|
7a49a1c2c8
|
袁聪的前端调修
|
2026-09-14 01:07:43 +08:00 |
|
张胜宇
|
e3c316aafb
|
Enforce trading authorization and suitability checks
|
2026-09-13 19:24:59 +08:00 |
|
张胜宇
|
e38ece32bf
|
前端提交
|
2026-09-13 15:56:54 +08:00 |
|
zhangshy
|
001ba065df
|
Merge remote-tracking branch 'origin/qyqy_develop' into RM2_develop
|
2026-09-12 16:12:02 +08:00 |
|
zhangshy
|
eb4e895e4e
|
修复高风险预警邮件通知并补充测试
|
2026-09-12 15:56:50 +08:00 |
|
张胜宇
|
ebc3fe4cbe
|
feat(§T): 账户看板 + 场内模拟交易 9 端点(用户自助首版)
新增 §T 用户自助段(docs/05 §19 新号段 7 个 = A×40/C×7/K×4/M×4/O×3/R×4/T×9):
- T001 GET /api/v1/users/me/account/dashboard — 账户/资金/持仓/盈亏汇总
- T002 POST /api/v1/users/me/orders — 委托提交(首版 market 立即全额成交)
- T003 / T004 / T005 委托列表/详情/撤单
- T006 GET /api/v1/users/me/holdings — 持仓列表(含市值/盈亏/当日盈亏)
- T007 / T008 成交记录列表/详情
- T009 GET /api/v1/users/me/cash-ledger — 资金账本
要点(与 docs/00 §6.6 一致):
- 首版市价委托立即全额成交,不实现撮合队列/部分成交;T005 撤单首版对任何在场委托返回 ORDER_NOT_CANCELLABLE (409)
- 价格来源复用 base FundQuoteService;service 层不二次封装(满足 AGENTS 第 2 条)
- 首版风控 3 条硬性:产品可交易、客户适当性、持仓比例上限(fin_market_price 缺失或过期 → 拒绝买入)
- 数据库零修改:10 张 fin_* 表全部 docs/00 既定,本批 PR 改列类型与可空性均 0;底座实际偏差(id 无 AUTO_INCREMENT、所谓'生成列'是普通 NOT NULL)由 service _next_id / 业务派生值补偿
注册 API:9 端点均注册进 app.main;user=9001(cust)'s id 写账
权限码(tools/seed_test_rbac.py 同步登记 + CUSTOMER 全量):
9047 account:read:self
9048 trade:order:create
9049 trade:order:read
9050 trade:order:cancel
9051 holding:read:self
9052 trade:txn:read
错误码(app/core/errors.py + docs/05 §3.6 + tests/unit/core/test_errors.py DOCUMENTED 三方同步):
404 ACCOUNT_NOT_FOUND / ORDER_NOT_FOUND
409 ORDER_NOT_CANCELLABLE
422 INSUFFICIENT_FUNDS / INSUFFICIENT_HOLDING / HOLDING_RATIO_EXCEEDED / SUITABILITY_MISMATCH / PRODUCT_NOT_TRADABLE
503 FUND_QUOTE_UNAVAILABLE(可重试)
新增:app/api/controllers/trading.py / app/api/schemas/trading.py / app/service/trade_service.py / tools/seed_sim_account_demo.py / tests/unit/service/test_trade_service.py(unit×8) / tests/contract/test_trading_endpoint_contract.py(contract×11)
修改:app/main.py(挂载 controller) / app/core/errors.py(10 新异常类) / tools/seed_test_rbac.py / docs/05-接口文档.md(§19 T001-T009 + §3.6 9 新码) / tests/unit/core/test_errors.py(DOCUMENTED 同步)
门禁:pytest tests/unit tests/contract 1313 passed (+19 新增) / ruff all clean / 三道守卫全过
|
2026-09-12 15:50:37 +08:00 |
|
wangjianlong_0626
|
58c28ef4a0
|
fix(memory): 记忆抽取容忍"单元素数组"形状,空结果不再被判为无效 JSON
## 现象(2026-09-12 跑真实 Worker 时发现)
`episode_worker` 反复报 `模型记忆抽取输出不是有效 JSON` 并重试到失败:
```
pydantic_core.ValidationError: Input should be a valid dictionary or instance of _ExtractionPayload
input_value=[{'memory_key': None, 'value': None, 'memory_type': None, 'confidence': 0}]
input_type=list
```
## 根因
契约是**对象** `{...}`,而模型在 episode 抽取路径会返回**单元素数组** `[{...}]`。
`_parse` 直接 `json.loads` 后交给 pydantic,数组自然过不了 `model_validate`,
于是被归入"输出不是有效 JSON"这一条 —— 但它其实是**合法的空结果**
(`_validate` 已能把"三字段为 null 且 confidence=0"正确识别为"无持久事实",返回 None)。
后果:这类 episode 白跑一遍模型调用、重试到 `retry_count` 上限后判失败,
**该片段的记忆永远抽不出来**。
## 修法
`_parse` 里只对"**恰好一个对象**的数组"做归一化:
- `[{...}]` → 取 `{...}`(空结果照常返回 None;有事实照常解析)
- 多元素数组、元素非对象、空数组 → **不猜**,仍交给校验失败关闭
(多元素时无法判断哪个是答案,猜错会把错误记忆写进库,比失败更糟)
## 测试
`tests/unit/service/test_memory_extraction_service.py` 追加 2 个用例:
- `test_single_element_array_is_normalized`:数组包空结果 → None;数组包有事实 → 正常解析
- `test_multi_element_or_non_dict_array_still_fails_closed`:多元素 / 非对象元素 / 空数组 → 仍失败
## 验证
- `pytest tests/unit/service/test_memory_extraction_service.py tests/unit/worker/test_memory_extraction_worker.py` → 28 passed
- `mypy app` → 0 错 / 245 文件
> 说明:`memory_extraction_service.py` 属主干线代码。此处是从**实际运行日志**里发现的
> 健壮性缺陷,改动限定在输出形状归一化,不改变抽取契约与校验规则。
|
2026-09-12 14:11:42 +08:00 |
|
wangjianlong_0626
|
f7b35006ff
|
Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
|
2026-09-12 14:06:18 +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 |
|
yuancong_0626
|
a501060e50
|
merge: merge yc into qyqy_develop
|
2026-09-12 12:50:14 +08:00 |
|
yuancong_0626
|
91f5373cfb
|
袁聪的第三次提交,API接口完善
|
2026-09-12 12:20:01 +08:00 |
|
张胜宇
|
9aaacc242f
|
chore: 清理违反底座规则的死代码并修正接口文档编号
- 删除生产死代码 app/service/knowledge_tool_service.py 与
app/infrastructure/milvus_knowledge_adapter.py:后者硬编码 Milvus 字段名,
违反 AGENTS.md §E,且仅被前者引用;生产检索链路实际走
knowledge_search_tool -> KnowledgeSearchService -> knowledge_schema 运行时探测。
- 删除上述两模块的单测,以及依赖 legacy 位置参数构造的
tests/unit/service/test_knowledge_retrieval.py。
- app/service/knowledge_retrieval_service.py 整文件回退底座版本,
移除 legacy 双构造与重复检索实现。
- docs/05-接口文档.md:客服画像候选改登记为 §8.5,恢复 §8.2 解析知识引用;
既有 §8.1-§8.4 编号全部保持,修复此前出现两个 8.3 的问题。
- app/model/profile.py:current_customer_id 改为普通可空列映射,与
alembic/baseline_generated.sql 及真实库一致;原 Computed 声明会让 ORM 把该列
从 INSERT 中排除,与「必须显式写入」的实际 schema 不符。
- 新增 docs/客服Agent接入底座扩展说明_v1.md,供集成分支评审逐项确认。
验证:pytest tests/unit tests/contract -> 1275 passed, 2 skipped, 0 failed;
ruff check app tests tools alembic 通过;mypy app 通过(244 个源文件)。
|
2026-09-12 11:15:24 +08:00 |
|
wangjianlong_0626
|
8a0cbab636
|
fix(memory-projection): 订正 outbox 取值口径并接通画像投影链路
背景:memory_sync_outbox 这条链此前**完全没有消费者**,且生产端照 docs/00 §6.4.6
写成大写 MILVUS/NEO4J + 中文「待处理」,而消费端按 target_store 的**值**分派 handler、
且只领 status in {pending, failed} —— 两个条件都不满足,事件任何消费者都领不到、
永久滞留且不报错(唯一键 (event_uuid, target_store) 对大小写无约束,MySQL 也不报错)。
根因是代码与测试都硬编码字面量,所以测试跟着一起错、谁也没拦住。
订正
- profile_generation_service:取值改为全仓一致的小写(milvus/neo4j/upsert/pending)
- 测试改为引用常量并断言消费端契约,不再硬编码(硬编码是本次跑偏的直接原因)
- 新增契约回归测试:断言大写值分派不到 handler、会进死信,谁改回大写立刻红
- 新增 tools/normalize_memory_sync_outbox.py:订正历史脏行(默认 dry-run、幂等)
接通投影链路(此前零消费者)
- 新增 Milvus 集合 user_long_term_memory_v1 及建集合工具(幂等、不覆盖已有集合)
- 新增 MilvusProfileProjection / MilvusProfileVectorClient,并修掉移植带来的两处必炸点:
customer_id 由「必须 int」放宽为接受数字字符串(本仓所有生产者都写 str,
不放宽则每个事件必然失败);不可投影的 memory_key 由「整批 raise」改为跳过留痕
(否则一条 constraint: 记忆毒死该客户整批,而受控词表 13 个键里有 7 个不满足前缀)
- 新增 MemorySyncOutboxWorker(领取/指数退避/死信骨架保留原样)并接入 WorkerRuntime
- milvus → 向量投影;neo4j → 复用主干 ProfileGraphProjectionService(方案 A,
不引入第二套投影,避免同一事实在图中两种说法、违反主干既有的只投影已确认事实的不变式)
- 生产端从 memory_unit(status=active) 组装 memory_sources,随事件带上确定快照
- 前置移植 conversation_privacy:写外部存储前脱敏手机号/证件号/银行卡等
验证
- 新增 17 个单测;全量 2 failed, 1307 passed, 2 skipped
(2 个失败为既有环境项:断言请求体中文原文而 httpx 序列化成 \uXXXX,非本次引入)
- mypy app → 0 错(227 文件);audit_schema → 89 张业务表无缺失/意外,未改动表结构
- 真机:真实 embedding(1024 维) + 真实 Milvus 写入并回读通过
- 整合链路(测试记忆 → 生产端组装 → outbox → 消费端投递 → Milvus 回读)通过,
且 MySQL 已回滚、Milvus 无残留
文档
- 新增 docs/32-记忆投影链路实现说明.md:真实口径、根因、契约与验证证据(供接手)
- AGENTS.md:新增该易错点;新增 Windows 中文输出乱码的正确命令(-X utf8);
校正测试基线与 mypy 文件数
未做:未改 docs/00 基线、未动数据库迁移、未改投顾线代码、未启动常驻 Worker。
遗留:投顾线两处生产者的 payload 缺 memory_sources,会被消费至死信,待架构师确认是否投影。
|
2026-09-12 10:45:40 +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 |
|
Windows
|
f5dd5b8ba2
|
feat: add advisor rollout gate and rollback playbook
|
2026-09-11 20:28:21 +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 |
|
Windows
|
2376585898
|
feat: add dual-source quote health orchestration
|
2026-09-11 19:47:14 +08:00 |
|
zhangshy
|
beb67f0a26
|
优化奶龙字段语义理解
|
2026-09-11 19:37:26 +08:00 |
|
张胜宇
|
5777f0f86e
|
feat: include memory sources in profile sync events
|
2026-09-11 19:37:18 +08:00 |
|
Windows
|
b6429e0e9c
|
feat: add advisory product comparison tool
|
2026-09-11 19:20:32 +08:00 |
|