张胜宇
|
2762b05203
|
docs(W24): 交付件口径纠错 —— D2.4 自相矛盾 / D2.5 过期块数 / D4.8 墓碑行误标 / D2.6 补 W24 状态
复核时发现四处「交付件里的事实错误」,逐条修:
- D2.4 §AC-04/AC-05 段:正文写「本仓库 _chunks.jsonl 当前 617 块全部为 public」,
与本文档自己的 v1.6/v1.8 版本行(public 730 / registered 25)自相矛盾。
改为标「设计当时」+ 补 v1.8 现状(755 块),并写明 AC-04/AC-05 现已可证伪、
不再需要「造一条测试块」(双向验证已于 2026-09-18 实测通过)。
- D2.5 演示脚本:「族判定(family_id 628 块全覆盖)」→ 755 块(W24 复测)。
这是答辩当天要照着念的稿子,块数必须与实库一致。
- D4.8 §9.2:把「新集合条数 basic 105 / product 398 / faq 300 / policy 576」
当成真实条数引用 —— 那组数字是 upsert 留下的墓碑行被计入
get_collection_stats().row_count 的结果(同 D2.1 v6.9:298/576/382 = 存活行
149/288/191 的两倍)。已补口径更正段,写明本轮复测的存活行数
251/154/288/62 = 755,并定下「引用块数一律用 _chunks.jsonl,不用 row_count」。
- D2.6 答辩报告:补一条 2026-09-21 W24 状态更新 —— ① docs/43 场内基金手册已入库
(20 只场内基金,702 → 755 块),「问在库产品却答另一只」的根因已根除;
② 出口经 E2c-my 细分后共六个(E1/E2/E2c-my/E3/E4/E5),转人工只在 E5c。
已复核:Milvus 四集合存活行数与 knowledge/_chunks.jsonl 逐集合一致(288/251/154/62 = 755),
无墓碑行残留;recalls_customer_memory 仍为 False,与 D2.7 记载一致。
D2.4 HTML 结构自检通过(table/tr/td 标签配平)。
|
2026-09-21 14:36:08 +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 |
|
张胜宇
|
eb1822d802
|
docs(W18): D2.4 索引口径更正为 AUTOINDEX(v1.6)+ D2.9 手动对话测试用例成文 + 空白消息 500 修复
- D2.4 v1.3 -> v1.6:索引统一 AUTOINDEX(设计初稿 HNSW/IVF_FLAT 未落地,实库直查为据)
更正 9 处 + 新增 §4.1 索引口径落地注;语料 628 -> 675 块并补 fin_basic_collection 说明
(三集合仍是唯一默认检索面,basic 并入会使 M-1 100% -> 91.3%)
- 新增 客服agent/D2.9-客服Agent手动对话测试用例-2026-09-20.md:
46 条金标逐条可问 + 11 条边界 Z 组 + 判分四问 + M-1~M-10 手动汇总 + 真 HTTP 核验配方
实测登记 3 项缺口:空白消息 500(已修)/ 语料档位口径矛盾(待裁定)/ 重复问句漂移(待裁定)
- 修修复:message 纯空白 500 -> 422 AGENT_INPUT_INVALID(入口补判据 + 回归测试)
- tools/chat_console.py 提示语去掉已下线产品名(季季盈)
- 落档:D1.1 §27(60 -> 61 份 / 客服agent 8 -> 9 份 / 注入校验 56 -> 57)、D2.1 v6.33、D1.6 §4.46
- 门禁:check_authoritative_docs 通过、_consistency.py GATE PASS、pytest 1915 passed / 0 failed
|
2026-09-20 17:31:40 +08:00 |
|
张胜宇
|
c91bbcbdc1
|
feat(ops)+docs: 密钥轮换工具 + 两份文档目录审计收口(D1.1 §23 / D2.1 v6.26)
一、密钥轮换(新增工具 + 操作手册)
- 新增 tools/rotate_api_keys.py:--check 体检 + 交互式轮换;getpass 不回显、
自动备份 .env.bak-<时间戳>(已被 ignore 命中)、校验不过整体不写入、
三个 Qwen 变量写同一值 / 两个 DeepSeek 变量写同一值。
实测 --check:Qwen 三变量同值且非空、DeepSeek 两变量同值且非空。
- 新增 开发文档/D3.8-模型密钥轮换与凭据安全操作手册-2026-09-20.md(CS-OPS-2026-023):
.env 5 个变量与读取方取证、五步流程、3 个坑、复核清单、回退方式、能力边界。
- 口径确认:model_endpoint_config.secret_ref 存变量名 ⇒ 轮换只改 .env,不动 DB;
但必须重启 API + Worker。
二、门禁修复:docs/ 编号撞车
- tools/check_authoritative_docs.py(D3.4 N-14 登记的验收命令集之一)实测 FAIL:
我方 docs/46 docs/47(2026-09-20 建)与投顾组 docs/46-投顾Agent需求文档.md
docs/47-投顾Agent功能架构文档.md(2026-09-16 建)同号。
- 按「后到者让位」改名:docs/48-可改文件白名单.md / docs/49-底座会签申请单-2026-09-19.md,
同步 8 处引用。修复后:checked 54 documents, no number collision,exit 0。
三、前端品牌残留(W12 合并静默回退)
- employee-advisor/dashboard/index.html 与 customer/advisor-plans/index.html 的
<title> 仍是 南方财富(投顾组分支带回)→ 按 DEC-27 改为 南方基金。
- HTTP 实测两页标题已正确;全仓 app/ 复查 南方财富 = 0。
四、文档口径校准(12 处事实漂移)
- D1.1:D2.1 版本 v5.3 → v6.26(§4.0 / §4.1 / §1 / §2 四处长期错误);
§8 四行遗留项闭合(D-5 / D-6 / D-7 / 仓库副本同步)+ 新增 §22 §23 留痕;
§10.2「本区不在任何 git 仓库内」更正为已入库;新增两编号 ⇒ 计数 56 → 58 全量同步。
- D2.2:顶栏徽标 v2.4 与元数据 v2.5 自相矛盾 → 统一;「投顾已清除」→ 状态更新
(模块 2026-09-20 已恢复,但客服范围裁定 §1.7 / RK-10 不变)。
- D2.3:徽标 v1.0 · 7 批次 51 项 → v1.1 · 8 批次 57 项;投顾清除后果 + §7.1 头号风险
+ 风险表 + 不触碰行全部加恢复口径。
- D2.4:v1.3 变更说明 ⑦ / §1.4 Out of scope / Q-09 加投顾恢复口径。
- D2.5:advisor_t 自相矛盾口径改写为账号表一行 + 口径更正;五项自检首选改为
一键脚本 启动演示.bat / demo.ps1;补 D3.8 与未发布 advisor:* 白名单登记。
- D2.6:门禁数字 1856/2 → 1909/3 skipped、ruff 19 → 20、补 portal_api_check 行;
§10 两项已闭环(密钥轮换已工具化、A-10 组 3/4 已补签);头部加 W12/W13 状态更新。
- D4.5:顶部状态更新补指向 D4.7。
- 新增 开发文档/D4.7-投顾模块恢复记录-2026-09-20.md(CS-PURGE-2026-014):
时间线、8 项恢复动作、客服线不变的结论、DEC-19 理由更正、遗留 1 项、失误登记。
- _consistency.py(维护侧):§三 改为「投顾状态口径检查」,合法语境扩为
清除史 / 恢复史 / 不属本 Agent 范围。
五、回归实测(全绿)
- pytest -q:1909 passed / 3 skipped / 0 failed
- ruff check app tools tests:20(与 W12 持平,未引入新债)
- mypy app:2(= 既有基线)
- tools/check_authoritative_docs.py:54 文档无编号冲突(exit 0)
- tools/e2e_smoke_test.py --read-only:31/31
- tools/portal_api_check.py:40 项 通过 35 / 失败 0 / 跳过 5
- _eval_harness/http_probe.py:11/11 succeeded
- _consistency.py:GATE PASS
- demo.ps1 -SkipStart -NoBrowser:五项自检全过、退出码 0
- 权威副本 ↔ 仓库:逐字节一致(客服agent 24 / 开发文档 52)
六、未做(如实登记)
- 投顾 config_release 工具白名单(advisor:*)仍未发布 ⇒ 投顾 Agent 工具调用 fail closed
(实测 active_agent_tools 仅 customer_service:* 4 项 + risk:* 4 项)。与客服线无关;
要演投顾线先跑 tools/publish_advisor_demo_config.py --apply。
- 两把 key 的实际轮换需你在控制台建新 key(无法代做),流程见 D3.8。
|
2026-09-20 15:27:07 +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
|
560775156c
|
docs: 新增投顾 Agent 需求文档与功能架构文档
依据仓库既有设计与已落地代码反推整理,内容与当前实现一致(非前瞻设计)。
- docs/46-投顾Agent需求文档.md
· 背景与三个真实断点(交付无落点 / 文案不可读 / 客户无入口)
· 目标 G1-G5 与 4 条非目标(不真实下单、不预测收益、不自动投资、不做场外)
· 角色场景 SC-01~SC-05、两条主链路流程图
· 功能需求 FR-01~FR-21(含口径、优先级、验收标准)
· 业务规则:候选池硬约束 fail-closed、5 条合规红线、状态机、FM-03 熔断
· 非功能需求、异常处理表、权限矩阵、数据需求、验收清单、11 项待确认问题
- docs/47-投顾Agent功能架构文档.md
· 五层架构总览、后端模块划分与 3 条设计约束、数据模型与关系
· 接口清单(投顾 8 / 客户 3 / 依赖能力 3 组)
· 三张时序图(生成推荐、审核发布、申报受理)
· 外部依赖降级矩阵、前端模块结构与 2 条硬约定、权限灰度、配置清单、已知约束
|
2026-09-16 18:17:45 +08:00 |
|
zhangshuai_0626
|
8643ad1efc
|
上传文件至「docs/风控业务演示文档」
|
2026-09-15 09:39:23 +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
|
859c89898e
|
补充 purge --apply 的真机验证证据
对账当时报 0 条死向量,--apply 分支没有真实样本。手工造了一个真实死向量
(上传后等向量写好,直接改库置 expired 且不投删除事件)跑通闭环:
对账发现 1 条 → 投递成功 1/0 → Worker 消费 → 复核归零。
|
2026-09-15 09:08:02 +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
|
64ad12e44a
|
修「r1到r5分别代表什么」答不出来:补一条标题对齐问法的短 FAQ(不动门槛策略)
## 现象
客服对「r1到r5分别代表什么」走兜底 + 转人工;「基金申购后多久能确认」用户以为也不答。
## 排查结论(详见 docs/演示用/知识库问答诊断-2026-09-14.md)
- 向量库**有**内容、检索也命中:申购确认那一问 top1 = **0.8468**(高置信,本来就答得出,
库里留着 23:59:06 那次完整问答);R1–R5 那一问 top1 = **0.6621**、次优 0.6412。
- 判定规则:高置信(≥0.75)不看 gap;中置信(0.55–0.75)必须领先次优 ≥0.07
(`customer_service.py:148-150`)。R1–R5 只领先 0.021 → 判"候选并列"。
- 根因是**同一主题多来源 + 命中块太长**:高频问答对(0.6621)、适当性指南第十一条三块
(0.6412/0.5771/0.5520)、产品手册 1.4 节(0.4983) 分数天然挤在一起;
而我先前上传的长条目(488 字切两块)被稀释到 0.6622,**改了两次才找到有效形态**:
短条目 + 标题与问法逐字对齐 → **0.7384 / gap 0.0761 → 可答**。
## 本次改动
- 新增知识正文 `data/knowledge/faq_r1r5.md`(口径取自 `suitability_service.MATRIX_ALLOWED` /
`MATRIX_NEEDS_DISCLOSURE`,避开禁用词)。
- 新增幂等工具 `tools/seed_knowledge_r1r5_faq.py`:默认 dry-run,`--apply` 上传;
已存在同源未过期知识则跳过,并自动复测检索评分。
- 真机执行:删除 3 个试验块(200/201/202)→ 上传 id 203 → 端到端验证。
## 验证
- `tools/seed_knowledge_r1r5_faq.py`(幂等路径)→ `top1=0.7384 次优=0.6623 gap=0.0761 可答`
- 端到端(登录客户 + 访客两条路径):「r1到r5分别代表什么」给出五级含义 +
C1–C5 对应关系;「基金申购后多久能确认」给出 T+1/T+2 确认规则
- 回归 4 条同类问题仍高置信直接答:费率 0.8111 / 场内场外 0.7871 / 最小买多少 0.8289 /
赎回到账 0.8480
## 顺带查清(登记为已知问题,未擅自改)
1. **向量与元数据严重不一致**:Milvus 159/397/297 个向量 vs MySQL 41/161/**0** 行;
答得最好的 `faq/高频问答对.txt`(含 FAQ-0015)与两套政策文件**只在向量里、MySQL 无行**。
⇒ 这**推翻**了我先前"检索按 active 过滤"的建议(会把孤儿但有用的内容一起杀掉),
已在文档里明确撤回。
2. **检索层不看知识状态**:`knowledge_search_service.py` 里 `status` 出现 0 次 →
已 expired 的历史副本照样参与排序(费率这类会变的内容有被答旧值的风险)。
3. **重复与测试垃圾在抢答,且没有清理入口**:产品手册被种了 7 遍(199 行里只有 24 active)、
FAQ 集合 41 行里 38 行是上传链路测试残留;而 `DELETE /api/v1/knowledge/{id}` 对已 expired 行
一律 404「知识文档不存在」→ 清理历史副本目前无入口。
|
2026-09-15 08:47:10 +08:00 |
|
lzf_0626
|
b53b4e0bc7
|
修「r1到r5分别代表什么」答不出来:补一条标题对齐问法的短 FAQ(不动门槛策略)
## 现象
客服对「r1到r5分别代表什么」走兜底 + 转人工;「基金申购后多久能确认」用户以为也不答。
## 排查结论(详见 docs/演示用/知识库问答诊断-2026-09-14.md)
- 向量库**有**内容、检索也命中:申购确认那一问 top1 = **0.8468**(高置信,本来就答得出,
库里留着 23:59:06 那次完整问答);R1–R5 那一问 top1 = **0.6621**、次优 0.6412。
- 判定规则:高置信(≥0.75)不看 gap;中置信(0.55–0.75)必须领先次优 ≥0.07
(`customer_service.py:148-150`)。R1–R5 只领先 0.021 → 判"候选并列"。
- 根因是**同一主题多来源 + 命中块太长**:高频问答对(0.6621)、适当性指南第十一条三块
(0.6412/0.5771/0.5520)、产品手册 1.4 节(0.4983) 分数天然挤在一起;
而我先前上传的长条目(488 字切两块)被稀释到 0.6622,**改了两次才找到有效形态**:
短条目 + 标题与问法逐字对齐 → **0.7384 / gap 0.0761 → 可答**。
## 本次改动
- 新增知识正文 `data/knowledge/faq_r1r5.md`(口径取自 `suitability_service.MATRIX_ALLOWED` /
`MATRIX_NEEDS_DISCLOSURE`,避开禁用词)。
- 新增幂等工具 `tools/seed_knowledge_r1r5_faq.py`:默认 dry-run,`--apply` 上传;
已存在同源未过期知识则跳过,并自动复测检索评分。
- 真机执行:删除 3 个试验块(200/201/202)→ 上传 id 203 → 端到端验证。
## 验证
- `tools/seed_knowledge_r1r5_faq.py`(幂等路径)→ `top1=0.7384 次优=0.6623 gap=0.0761 可答`
- 端到端(登录客户 + 访客两条路径):「r1到r5分别代表什么」给出五级含义 +
C1–C5 对应关系;「基金申购后多久能确认」给出 T+1/T+2 确认规则
- 回归 4 条同类问题仍高置信直接答:费率 0.8111 / 场内场外 0.7871 / 最小买多少 0.8289 /
赎回到账 0.8480
## 顺带查清(登记为已知问题,未擅自改)
1. **向量与元数据严重不一致**:Milvus 159/397/297 个向量 vs MySQL 41/161/**0** 行;
答得最好的 `faq/高频问答对.txt`(含 FAQ-0015)与两套政策文件**只在向量里、MySQL 无行**。
⇒ 这**推翻**了我先前"检索按 active 过滤"的建议(会把孤儿但有用的内容一起杀掉),
已在文档里明确撤回。
2. **检索层不看知识状态**:`knowledge_search_service.py` 里 `status` 出现 0 次 →
已 expired 的历史副本照样参与排序(费率这类会变的内容有被答旧值的风险)。
3. **重复与测试垃圾在抢答,且没有清理入口**:产品手册被种了 7 遍(199 行里只有 24 active)、
FAQ 集合 41 行里 38 行是上传链路测试残留;而 `DELETE /api/v1/knowledge/{id}` 对已 expired 行
一律 404「知识文档不存在」→ 清理历史副本目前无入口。
|
2026-09-15 08:46:38 +08:00 |
|
lzf_0626
|
01e4e6a687
|
修掉"Worker 会自己退出"的真缺陷(场外游标续租失败误取消主任务)
## 现象(2026-09-14 实测,非人为停止)
Worker 进程自己退出,退出码 1,日志末尾是
`ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接`
(发生在 `offsite_worker.close()` 的 IMAP `logout()`),
而真正的起点是更上面那句 `asyncio.exceptions.CancelledError`。
## 根因(两处,都是真缺陷)
1. **取消错了对象**:`OffsiteMailWorker._process_batch` 把
`asyncio.current_task()`(= `__main__.serve()` 的**主循环任务**)交给游标心跳,
心跳在续租失败(`rowcount != 1`)或续租抛异常时执行 `task.cancel()` ——
于是"放弃这一批"变成了"**杀掉整个 Worker**"。`CancelledError` 从 `run_once()`
一路冒到 `serve()`,主循环直接结束。
2. **收尾异常盖掉退出原因**:`serve()` 的 `finally` 里 `await offsite_worker.close()`
在网络已断时抛 `ConnectionResetError`,把 `CancelledError` 顶掉,
表现为"关闭流程崩了",看不出真实原因。
## 改法
- `_process_batch` 把"这一批"跑在**独立任务** `batch` 里,心跳只取消 `batch`;
调用方捕获 `CancelledError` 后区分两种情况:
**本批被放弃**(`batch.cancelled()` 且当前任务自己没有在取消)→ 记 warning、返回 `False`、
Worker 继续下一轮;**外层在取消当前任务**(Ctrl+C / 进程关闭)→ 原样上抛,绝不吞掉。
- `serve()` 的 `finally` 里关闭场外 Worker 包 try/except:收尾失败只记日志,
**不改变退出码与退出原因**。
- 新增 `_current_task_is_cancelling()` 用 `Task.cancelling()` 做这个区分(3.11+)。
## 守卫
`tests/unit/worker/test_offsite_mail_worker.py::test_cursor_lease_loss_abandons_batch_without_cancelling_the_worker`
—— 续租失败(rowcount=0)时断言:批次确实跑起来过、返回 `False`、
**调用方任务没有被取消**。修复前这条用例会挂在"调用方被取消"上。
## 验证
- `pytest tests/unit/worker` → 112 passed
- `pytest tests/unit tests/contract` → 见下方(0 failed)
- `mypy app/worker/offsite_mail_worker.py app/worker/__main__.py` → 0 错(除组员文件里既有的 1 个)
- 重启 Worker 后跑记忆演示链路:候选 → verified → active → **2 秒**收敛,进程稳定
## 文档
`docs/演示用/记忆系统演示文档-2026-09-14.md`:
场景五补"如果发现 Worker 自己退了是怎么回事",问答补"Worker 会不会自己中途退出"。
|
2026-09-15 08:01:00 +08:00 |
|
lzf_0626
|
9490e5043b
|
记忆系统演示文档 + 修掉两处会让演示断链的真问题
## 新增:docs/演示用/记忆系统演示文档-2026-09-14.md
按"操作 → 看到什么 → 这体现什么"写,五个场景(探针看存储 / 记住一件事 / 谁能读谁读不到 /
画像版本与投影 / 停 Worker),每个场景配可复现命令与**实测输出**,另附建议顺序与时长、
六条问答话术、演示前自检清单、受控信号词表摘录。
配套两个新工具(都已实跑):
- `tools/memory_demo_chain.py`:一键走完"客户说 → 候选 → 客户确认 → 管理员批准 →
记忆与画像自动收敛",打印演示前后对比(实测 2 秒收敛)。
- `tools/memory_recall_demo.py`:同一客户换六个身份召回,当场看出"客户只读自己 /
员工要权限码+归属 / 运营读不到 / 管理员有权限码但没归属也读不到"。
## 修掉两处会让演示当场断链的问题(都是真机复现的)
1. **候选批准后画像字段不收敛**
`CustomerProfileCandidateService._promote` 只写画像快照、**不投**
`profile.rebuild_requested`,而 `user_facts` 与画像字段是 `ProfileAssemblyService`
的重建链路写的。后果:批准后记忆变 active、快照版本 +1,但**画像字段停在旧值**
(客户说"三年以内",画像里还是"十年以上"),要等一次无关的重建才收敛 ——
而"客户说完 → 批准 → 画像变了"正是演示主线。
现补一条重建事件(走事件而不是就地重建:本方法所在事务还没提交,
另开 session 看不到刚写入的记忆)。
2. **画像快照的唯一键从来没起作用,而且埋雷**
`uk_profile_snapshot_current` 建在列 `current_customer_id` 上(不是 `is_current`),
但 `ProfileAssemblyService._write_snapshot` 旧行只置 `is_current=False`(不清该列)、
新行**不写**该列(实测 13 行该列全 NULL)。
后果:只要客户**先被重建过一次**,旧 current 行仍占着 `current_customer_id=9001`,
下一次"批准画像候选"就会撞 `Duplicate entry '9001' for key
uk_profile_snapshot_current` → **整次批准 500**(本次实测踩到)。
现按唯一键的真实语义写:清旧行的该列、新行显式写客户号。
(`ProfileGenerationService` / 候选路径本来就是这么写的,只有这一处没对齐。)
## 守卫
新增 `tests/integration/test_profile_snapshot_current_invariant_mysql.py`:
按真实顺序"先重建再批准候选",断言 ① 只有一条 current 且它占着唯一键、
② 历史版本已归还该列、③ 批准不再 500、④ 批准投出了重建事件。
修复前这条用例会在第 ② 步失败。
## 验证
- `pytest tests/unit tests/contract` → 1490 passed, 2 skipped, 0 failed
- 新增集成用例通过;真机实测演示主线:候选 → verified → active → **2 秒内**
`user_facts` 与 `fin_customer_profile.investment_horizon` 都变成新值
- `mypy tools/memory_demo_chain.py tools/memory_recall_demo.py` → 0 错;ruff 全绿
## 文档
`docs/44-演示流程.md`:配套文档清单与"记忆链路"备选场景都指向新演示文档。
|
2026-09-15 00:55:44 +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
|
0e68271a81
|
docs/44:把一份被移动过的引用改成不依赖目录(代码库全面审查报告)
|
2026-09-15 00:16:56 +08:00 |
|
lzf_0626
|
449d078a02
|
新增《记忆架构与底座 Worker 工作流程 · 大白话版》
两部分,各讲一件事,接口处互相对齐:
第一部分 · 记忆架构
- 四个存储的分工:MySQL 是唯一账本(memory_unit / memory_evidence / memory_conflict /
user_facts / fin_customer_profile / profile_snapshots),Neo4j 存关系、Milvus 存向量
(集合 user_long_term_memory_v1 是代码常量)、Redis 只做加速(召回热缓存 300 秒 /
客服短期会话 30 分钟滑动)
- 一条记忆的一生七步;为什么画像重建要绕事件;investor_type 只来自风险测评
- 客服走"候选画像"两把钥匙(客户确认 verified → 管理员批准 active);不抽记忆的三种情形
- 召回三路(MySQL 结构化 + Milvus 语义 0.7 权重 + Redis 热缓存),没有"图召回"这一路;
当前只有风控 Agent 消费召回结果
- 可读范围一个文件说了算(客户只自己;员工要 memory:read:customer 且归属;上限 10)
- 失效/删除链路与"清不掉就如实留痕"
- 实测表行数 + 一条要如实说明的观察:9001 的向量在 Milvus 里,但 memory_sync_outbox
当前没有它的行,不能据此断言投影链路正常
- 八条已知遗留逐条核实(episode 幂等哈希不覆盖记忆内容、投顾两处不发 memory_sources、
风控 system 上下文召回恒空、current_customer_id 从不写入致唯一键形同虚设、user_facts
无唯一键、两个未接线组件、文档过期)
第二部分 · 底座 Worker
- 三段式(受理 202 → 排队 → Worker 执行)与"不跑就没有任何报错"
- 一轮 run_once 的四件事与顺序、单轮上限、每 30 轮才做的片段聚合、失败隔离
- 五条工作线逐个讲:Agent 运行(状态机/领取加锁/租约 60s+心跳 20s/重试 3 次/取消收口/
执行前重新解析身份)、领域事件(12 类、幂等两层、重试 5 次进死信、退避公式)、
记忆与画像投影、会话片段与知识向量、场外收件(四道闸门 + 游标租约 + blocked)
- 显式降级清单(不伪造成功)
- 怎么判断它活着/卡住/积压:看表 + 看日志关键词 + 当前死信实况(570 条 dead 的构成)
- 三个常被搞错的边界(风控定时扫描是独立进程且默认关;AgentRunWorker 生产未用;
GraphProjectionWorker / ProjectionReconciliationService 未接线)+ 一页速查
|
2026-09-15 00:16:35 +08:00 |
|
lzf_0626
|
42a330c4c9
|
docs/44 演示流程按 2026-09-14 实测同步:今日盈亏可讲、投顾自助审核、管理员八页签、权限数与已知空数据
|
2026-09-15 00:14:20 +08:00 |
|
lzf_0626
|
776b504ee6
|
新增《全平台功能流程 · 大白话版》:从登入讲到六门户全部流程
- 读者是第一次接触平台的人:全文不用"链路/编排/收敛"这类词,讲"点一下之后发生了什么"
- 结构:三个角色(浏览器/API/Worker)→ 访客 → 登入与角色落地 → 客户 8 页 → 风控 →
投顾 → 运营三条线 → 管理员 8 个页签 → 底座 9 件事 → 一条主线时间线 → 常见疑问 → 速查
- 每节都标了文件/接口依据,并写清"已知空数据"与"已知小偏差"(通知类型下拉未接线、
NL2SQL 错误信息列恒为 --、推广页无 form 导致 required 不生效、场外默认 dry-run 等)
- 顺带纠正三处易误传的说法:幂等键只防"同一次请求重发"(连点仍会下两笔)、
客户侧前端权限门是装饰性的(后端 403 才是拦截)、FM-03 熔断目前只在前端拦
- 风控演示数据那 6 行 RISKDEMO 场外申购/赎回:代码注释与今日盈亏说明改口径为
"风控异常交易演示的触发材料,刻意保留",不再当脏数据;今日盈亏按 transaction_type 语义排除它们
|
2026-09-15 00:10:23 +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
|
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
|
e85e958389
|
排障表补一条:所有页面一起卡 → Milvus 抖动 + 同步调用阻塞事件循环
## 为什么补这一条
把审查报告的 P1–P3 共 45 项按 8 个演示场景筛完,**"会砸演示"的是 0 条**
(P0 四条已全修)。剩下的分两类:37 项演示路径根本走不到,8 项是"边缘"
(平时无感、特定条件才暴露)。
这 8 项里只有 **P1-2** 属于"一处慢、全平台卡":检索层用的是**同步** Milvus 客户端,
单次 `search` 卡 3 秒会让整个 API 进程停摆 3 秒,连 Worker 心跳一起停。
它同时也是**最容易被误判**的一个 —— 现象是"客服、风控、看板一起变慢",
而排障表原有的 6 条全是**单点**故障(503 行情 / Worker 没起 / 浏览器缓存 /
知识库没同步 / MySQL 没起 / 风控样本用完),**没有一条对得上"全场同时变慢"**。
没有这一条,现场会往"服务挂了"的方向查,而它其实是外部依赖抖动、不需要改代码、
也不需要重启。
所以这条的价值不在改代码,而在**把一个边缘隐患变成一眼能认出的现象**:
演示版本选择不修 P1-2(Milvus 正常时完全无感),代价就是必须知道它长什么样。
## 依据(都是会话内实际核过的)
- 客服链路确认:`app/service/knowledge_tool.py:29` 的 `search_knowledge` 工具
→ `get_knowledge_search_service().search(...)`;
而 `knowledge_search_service.py:207-213 / 224 / 229` 调的是**同步** `MilvusClient`
(`app/infrastructure/vector_memory.py` 的 `def search(...)`,非 `async def`)。
- 对照:项目在其它 10+ 处已用 `asyncio.to_thread`(如
`app/infrastructure/fund_market_adapter.py:273/382`),说明异步规范已建立,
这 4 处是遗漏 —— 但**不是演示阻塞项**,本次不改。
- "风控日报会不会砸"(P1-6,报告标了【确定】)也核过了:`_distribution` 的
`counts` 键是字段**值**,两个调用点用的 `risk_level`(中文高/中/低)与
`alert_type` 都是字符串,`str(key) == key` 恒成立,**触发不了** —— 场景 6 安全。
## 改动范围
只改 `docs/44-演示流程.md` 的 §4 排障表,**新增一行**,未动其它内容。
|
2026-09-14 20:41:34 +08:00 |
|
lzf_0626
|
36c7a9d8d2
|
文档:审查报告入库 + 全量校对补注
## 新入库(`docs/演示用/`)
- `代码库全面审查报告-2026-09-14.md`
- `代码修改方案-2026-09-14.md`
- `记忆系统排查报告-2026-09-14.md`
- `记忆系统修复文档-2026-09-14.md`
- `文档一致性审计报告-2026-09-14.md`
- `多Worker接入方案-2026-09-14.md`
## 全量校对(32 个既有文档 + `AGENTS.md`)
跨 39 个文件、**1125 insertions / 148 deletions**。
⚠️ **这批改动同样不是本次会话写的**。我抽样核对过性质:是**实质内容补充**而不是
格式/换行转换。例如 `docs/44-演示流程.md` 新增两条"2026-09-14 补注":
- `启动金融Agent平台.bat` 只在**桌面**上,仓库里只有 `启动平台.bat` 这一份
(两份由同一个 `tools/make_launcher_bat.py` 产出,改完 `start.ps1` 重跑它一起更新);
- `advisor_t`(9020) 与 `offsite_t`(9006) **不在 `tools/seed_test_rbac.py` 的演示用户里**
(那里只有 `cust_t`/`risk_t`/`admin_t`/`review_t` 四个),由 `grant_*.py` 系列创建,
**重跑种子不会重建它们** —— 换机器时这两个账号登录失败,要先查 `sys_user` 有没有这两行,
而不是查密码。
这两条都是对的地方,与我这一路踩到的现象一致(我确实用到了 `advisor_t`/`offsite_t`)。
**我没有逐字审阅全部 39 个文件**,只抽样确认了改动性质与规模。若其中有需要复核的段落,
请指明文件,我逐处核对。
|
2026-09-14 20:36:00 +08:00 |
|
lzf_0626
|
484cccc145
|
记录三个演示文档的移动,并把 .workbuddy/ 加进 .gitignore
## 文档移动(docs/ -> docs/演示用/)
`代码结构与关键逻辑梳理-2026-09-14.md`、`后端接口文档-2026-09-14.md`、
`软件需求文档-2026-09-14.md` 移入 `docs/演示用/`。
git 识别为 **rename**(状态 `R`),三份文档的历史完整保留,不是"删掉再新建"。
## .workbuddy/ 加入 .gitignore
它与文件里已有的 `.agents/`、`skills-lock.json` 属同一类 —— AI 编码助手的工具产物,
不是项目内容。放在同一节下,避免以后 `git add -A` 时误提交。
## 未包含(有意留下)
`docs/23-记忆分层与画像设计.md` 的工作区改动**没有**一并提交:那不是我改的,
也不在这次确认的范围内,留在工作区由作者决定何时提交。
(暂存时被 `git add docs` 一并带上,已 `git restore --staged` 退回。)
|
2026-09-14 12:09:23 +08:00 |
|
zhangshy
|
8409a2c96e
|
合并主项目最新文档并保留风控文档更新
|
2026-09-14 11:44:45 +08:00 |
|
zhangshy
|
5a83aa6fef
|
更新风控文档分页会话记忆和演示数据规则
|
2026-09-14 11:44:21 +08:00 |
|
lzf_0626
|
e59a9905a0
|
把「今日盈亏是硬编码 0」提级为待业务确认项(SRS Q22)
## 问题
`today_profit_loss` / `today_profit_loss_ratio` 在 `app/service/trade_service.py` 里是
**硬编码 `ZERO`**(持仓列表 L678、账户看板 L748-749),客户看板、持仓页、盈亏分析页的
「今日盈亏」永远显示 0.00。同一函数里的「持有盈亏」是**真算的**
(`market_value - cost_amount`,L660 / L726),所以只有这一项是占位。
三份 2026-09-14 文档其实**都记录了这个事实**(接口文档第 11 条、SRS 第 212 / 458 / 615 行),
但**都埋在「注意事项」里,没有进第 0 章那份「★ 请先回答本章」的待确认清单** ——
业务翻文档时看不到,演示现场被问到就答不上来。而且 SRS 第 212 行还把它**错引到了 Q7**
(Q7 讲的是场外阈值,与此无关)。
## 改动
**docs/软件需求文档-2026-09-14.md**
- 新增 **0.5 节「占位实现与本期边界」**,登记 **Q22**:客户看板的「今日盈亏」本期是否实现。
该节专门收这类「接口有、值还没真算」的偏差(不报错、单元测试全绿、只有业务看得出不对),
以后同类问题继续往这里追加。
- 第 0 章项数 21 -> 22;阻塞项清单补上 Q22。
- 修正 F-4.2 的错误引用(见 Q7 延伸 -> 见 Q22)。
- R2 状态改为「已提级为待决项 -> 见 Q22」。
- **R1 状态改为已修复**:组员提交 `4b7ee13`「修复管理员工作台函数缺少闭合大括号」
已解决 `workspace.js` 的语法错误。按该条自己要求的方式复验过 ——
`node --experimental-vm-modules` + `vm.SourceTextModule` 遍历 `app/static/portal`,
**44 个模块全部通过**(`node --check` 会假通过,不能用来判定)。
**docs/44-演示流程.md**(演示现场用)
- §3「可能被问到的问题」新增话术:直说这是占位值、指出持有盈亏是真算的、
指向 SRS Q22 与两条口径的结论;并建议「时间紧就避开这一栏,被问到不要含糊也不要现编」。
- 场景 2(客户资产)加现场提示:讲「持有盈亏 / 总市值 / 可用资金」,不要指着「今日盈亏」讲。
**docs/后端接口文档-2026-09-14.md**
- 第 11 条补上代码行号与交叉引用,并点明同一响应里的 `profit_loss` 是**真算的**。
## 顺带交给业务:两条口径的可行性(实测,非推测)
业务要决定的是「要不要做」,但「能不能做」也得一起给,否则业务选完还要再问一轮:
| 口径 | 数据现状 | 结论 |
|---|---|---|
| 用行情算(今收 − 昨收) | `fin_market_price` **只在同步时才写行**,实测每个产品**只有 2 行**(515450 只有 09-11 与 09-14,还跨了周末) | ❌ 取不到连续「昨收」 |
| 用净值算(今净值 − 上一交易日净值) | `fin_nav_history` 每个产品 **120~160 行连续交易日净值** | ✅ 建议按这个口径 |
⇒ 文档建议按**净值口径**实现,并提醒业务一并确认语义:净值是日终数据,
该指标实际是「最近一个交易日的盈亏」,**不是盘中实时**。
## 入库说明
三份 `docs/*-2026-09-14.md` 此前是**未跟踪文件**,本次一并入库(同一批文档交付物且互相引用)。
`.workbuddy/`(工具会话记忆)属工作区过程产物,**未入库**。
|
2026-09-14 11:24:51 +08:00 |
|
lzf_0626
|
404f8f6aa5
|
固化接口体检脚本,并交付双击即用的启动 bat
## tools/portal_api_check.py(新)
把 2026-09-13 那轮"照着接口内容把前端测一遍"的验证固化成可重复跑的脚本。
当时靠这套验证查出 6 个真缺陷,但它们只存在于一次会话里,下次改动没人会重跑:
- `K002`/`K003` 是裸信封,前端按 `payload.data` 取 -> 知识库页显示"已入库 0 块 / 为空"
- 委托/成交详情页照着建表字段写,而接口返回视图没带那些列 -> 5 行永远显示 `--`
- 配置项/路由规则的 `PUT` 要 `If-Match`,却没有端点能返回该 digest -> 首次编辑必然 409
- 回复模板 `scene` 只校验长度不校验枚举 -> 非法值撞数据库 CHECK、冒成 500
- 路由规则表单固定 `max_attempts=2` 且 `fallbacks` 为空 -> 后端必然 422
- 模型端点手填 ID -> 未激活的 ID 直接 422
与 `e2e_smoke_test.py` 分工互补:后者测**业务链路通不通**,本脚本测**接口契约对不对**
(状态码、信封形状、字段名与前端期望是否一致)。
三档,默认只跑第一档:
- 默认只读 41 项(不动数据)
- `--write` 加测写操作:配置项 ETag 链路、`max_attempts` 越界、知识库上传+失效、
回复模板非法枚举、敏感词、推广物料全链路
- `--dangerous` 再加测会改生效配置的操作(激活配置版本会整版本替换,默认不跑,
脚本内注明了恢复办法)
实测:只读 39/39、写入档 54/54 全绿。
## 桌面一键启动(双击即用)
桌面 `启动金融Agent平台.bat` + 仓库 `启动平台.bat`,由
`tools/make_launcher_bat.py` 生成,只负责"双击"这一层,启动逻辑复用 `start.ps1`
(不重复实现,避免两边漂移)。参数可透传:`-Port 8100` / `-NoBrowser` / `-SkipPriceSync`。
不要手写这个 bat:必须同时满足 **GBK 编码 + CRLF 换行 + 无 BOM**。生成器自己
读回来校验这三条。踩过的坑:用 `write_bytes` 直接落盘时没转 CRLF,cmd 对 LF-only
批处理会行边界错乱,把 `echo` 的说明文字当命令执行(实测报
`AT 命令已弃用`、`']' 不是内部或外部命令`)。
## start.ps1 的三处修复
1. **解释器探测选错环境(真机双击失败的主因)**
原先只验证 `--version` 成功就选中,结果挑到一个 Python 3.10 环境:
本项目用了 `datetime.UTC`(3.11+)且依赖 `asyncmy`,启动当场 ImportError ——
而报错发生在**行情刷新**那一步,看起来像"行情源坏了"。
现在实测两项:**版本 >= 3.11** + `import fastapi, sqlalchemy, asyncmy, pydantic` 通过,
并在跳过时打印具体原因。
2. **`Select-Object -First 1` 掐断 native 管道**
`& $exe -c ... 2>&1 | Select-Object -First 1` 拿到首个对象后停掉上游管道,
等于把进程掐了、`$LASTEXITCODE` 变脏,于是**每个候选都被误判成"无法执行"**
(连装好的 jr_py313 也被跳过)。改为先整体接住输出、再在结果上取行。
3. **幂等 + 就绪探测 + Docker 自动拉起**
重复双击不再起第二个 API/Worker(API 按端口、Worker 按进程判断);
起完等 `/internal/health/ready` 真正应答才报成功;Milvus 不可达时尝试拉起
Docker Desktop 并最多等 60 秒(它不常驻,是"知识检索静默降级"最常见的原因)。
## 实测
- `启动平台.bat` 重复执行:解释器正确选中 jr_py313、依赖检查全 OK、行情已刷新、
两个服务识别为已在跑并跳过、就绪探测 1 秒
- `-Port 8199` 冷启动:新起 API 窗口,8199 的 `/internal/health/ready` 与 `/portal/` 均 200,
测试后已清理
- `ruff check app tests tools alembic hq.py` -> All checks passed
- `start.ps1` 语法解析通过(BOM 已保留)、bat 三项编码约束校验通过
文档同步:`AGENTS.md`(启动方式、bat 生成器、解释器探测两个坑)、
`docs/44-演示流程.md` §0.3/§0.4(双击启动、两条自检线的分工)。
|
2026-09-14 01:59:46 +08:00 |
|
lzf_0626
|
daf73a2865
|
fix(portal): 照接口逐条核对前端,修掉 5 处「照着表结构写、看着像对」的缺陷
做法:把前端 `api-client.js` 注册的端点**按前端完全相同的方式**(同样的路径、参数、
身份)逐个调用,再拿真实返回去核对前端 render 用到的字段。
**这类问题纯读代码看不出来** —— 只有把真实返回和期望字段摆在一起才会暴露。
## 1. 知识库功能实际是坏的(K002 / K003 是裸信封)
`K002` 的成功体是 `{knowledge_ids, filename, chunk_count}`、`K003` 是 `{items, count}`,
**都没有 `data` 信封**。而 `request()` 默认取 `payload.data`(undefined),于是:
- 上传后前端显示「已入库 **0** 块」,而库里其实切了 23 块;
- 列表永远显示「知识库为空」。
端点表里标 `raw: true` 后(与 `V001` 同一做法)两者都正常。
实测:上传 4805 字符的产品手册 → 切 23 块并出现在列表里。
## 2. 委托/成交详情页有 5 行永远显示「--」
前端按 `fin_sim_order` / `fin_transaction` 的**建表字段**写了 `quote_source`、
`nav`、`fee_rate_snapshot`、`confirmed_at`、`auto_confirmed` ——
但这些字段**接口的返回视图没有带**(表里有、返回里没有)。已按实际返回重写字段表,
并在注释里写明"以接口返回为准,不要照表写"。
## 3. 配置项与路由规则的**编辑功能不可能成功**(接口缺口)
`PUT` 硬性要求 `If-Match`,校验的是该行内容的 digest;而这两个资源是 `detail=False`
—— **没有任何端点能返回这个 digest**(列表的 `meta` 只有 trace_id)。
乐观并发在"读不到版本"的前提下等于死锁:**首次编辑必然 409**。
(配置发布能用,是因为它有详情端点 `A003`。)
- 新增详情端点 `A048` / `A049`(`detail=True`),已登记 `docs/05` §19;
- 前端编辑前先 GET 详情取 etag,再带 `If-Match` 提交。
- 实测:编辑配置项与路由规则均 200;**不带 `If-Match` 仍返回 409**,
说明乐观并发没有被削弱。
## 4. 路由规则表单**必然提交失败**
前端固定写 `max_attempts: 2` 且 `fallbacks: []`,而后端要求
`max_attempts ≤ 端点总数`(主 + 兜底)→ 422「重试次数超过端点数量」。
改为 `1` 并注明约束。
## 5. 主端点手填 ID 会 422
后端对不存在/未激活的 `primary_endpoint_id` 直接 422「模型端点不存在或未激活」。
把输入框改成**下拉**,只列 `status='active'` 的端点(数据复用已有的端点列表)。
## 顺带
- `apiClient` 增加 `del()` / `put()`:发出的方法一直由端点表决定,所以 `post('K004')`
也能发 DELETE —— 语义太绕,现在意图与行为一致。
- 清理了测试期间上传的 31 条知识残留(客服会检索到它们),库内恢复到 23 条产品手册。
## 关于"逐条核对"的方法论
前两轮跑出来的 9 个和 5 个"失败"里,**多数是我测试脚本自己的假设错了**,不是前端问题:
`T001` 是 `{account, summary}` 嵌套、`RK002` 的字段叫 `risk_level`、
`RK002/RK004/RK005` 的 limit 上限是 5/10/10(前端传的正是 5/10/10)、
`AD011/A002/A047` 的 data 是裸 list。每一处都回到前端源码确认后才下结论 ——
**先把"我以为"改成"代码里写的"**,否则报告出去的就是假 bug。
验证:unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;§19 现 93 个端点无重复;e2e 冒烟 **40/40**。
前端等价测试:只读 31 项全绿、写操作(含 ETag 链路)9 项 8 绿 1 项因测试数据过短。
|
2026-09-14 01:19:22 +08:00 |
|
lzf_0626
|
9a5259d787
|
feat(market): 存下行情源给的涨跌幅,产品列表与排行页不再显示"暂无"
## 问题
产品列表与排行页的「最近涨跌」全是"暂无"。原因是它只能由**两个交易日的收盘价**
现算,而 `fin_market_price` 每只产品每天只有一行 —— 库里头一天只有一天数据时
根本算不出来。
## 但行情源本来就给了这个数
腾讯行情接口的第 32 位就是**当日涨跌幅**(适配器此前只解析了开高低收量额,没取它)。
所以不必等第二个交易日去算,把源给的数存下来即可。
## 改动
- **迁移** `20260913_market_price_change_pct`:给 `fin_market_price` **新增一个可空列**
`change_pct DECIMAL(10,4)`。只加列、不改任何已有字段(AGENTS.md 规则 2 允许),
可空且不回填,既有读写方全部不受影响。
- **适配器**:`_parse_tencent` 增解析位置 4(昨收)与位置 32(涨跌幅)。
- **同步服务**:写入 `change_pct`;降级路径(净值兜底)没有这个数就写 **NULL**。
- **接口**:`_resolve_change_pct` **优先用存下来的值**;迁移前的历史行没有该列的值,
才回退到"今日收盘 vs 昨收"现算。仍为 `null` 时前端显示"暂无" ——
**不得当成 0**(`formatPercent(null)` 会渲染成 `+0.00%`,那等于说"今天平盘")。
## 契约测试同步
两处契约断言因结构演进需要跟进 —— 它们的作用正是拦住这类变更:
- `test_fund_readonly_contract.py`:`fin_market_price` 列数 **13 → 14**
- `test_advisor_migration_contract.py`:迁移 head 更新为新版本
(核心断言仍是"链收敛到唯一 head",此处只是钉住末端)
验证:实测 20 只产品**全部有真实涨跌幅**(如 515450 −0.43%、159511 +1.23%);
unit+contract **1397 passed**;integration **110 passed**;ruff 通过;
mypy 251 文件 0 错;`audit_schema` 与 `migration_state_check` 均通过;e2e 冒烟 40/40。
|
2026-09-14 00:30:06 +08:00 |
|
lzf_0626
|
de55c5c60c
|
feat(advisor): 登记 17 个投顾端点并补管理员复核入口(A047 待审队列)
## 1. docs/05 §19 补登 17 个投顾端点
这批端点此前**只存在于代码中**,§19 一条都没登记;而 §12 写的入口
`/api/v1/advisory-plans/**` 与实际路径 `/api/v1/advisor/**` 也不符(已修正)。
- **A041–A046**:管理员治理(配置回测、画像标签与漂移复核、推荐方案审核与发布)
- **AD001–AD011**:投顾自用。**新开 `AD` 号段**的理由:它与 A 段是两个不同的权限面
—— A 段是 `/api/v1/admin/**` 管理面,AD 段是 `/api/v1/advisor/**` 投顾自用;
混在一个号段里,"这条到底谁能调"就得逐条去读权限列。
- 另加 AD 段说明块:`investment-goal` 的两套权限码(`...:self` / `...:customer`)、
AD006/AD007 虽在投顾路径下却要求 `admin`、灰度开关 `enforce_advisor_rollout` 前置、
幂等范围(AD008/AD009 无幂等头)、以及 404/409 的失败口径。
§19 现为 **90 个端点 / 9 个号段**,无重复。
## 2. 管理员复核入口 + A047 待审队列
**发现一个让审核链路不可达的缺口**:`review` / `publish` 都要求调用方先拿到键
(推荐方案是 `content_id`、方案书是 `goal_no`),而此前**没有任何端点能列出待审内容**
—— 管理员拿不到键,投顾生成的东西就永远停在待审状态。
- 新增 `GET /api/v1/admin/advisor/pending-contents`(编号 **A047**):一次返回两类待审内容。
两类内容的"待审"取值不同(推荐方案 `pending_review`、方案书 `pending`),
只判其中一个会整类漏掉,所以用 `PENDING_STATES` 一并匹配。
- **为方案书一并查出 `goal_no`** —— 它的审核/发布端点(AD006/AD007)按 `goal_no` 寻址,
只给 `content_id` 的话管理员拿到列表也调不动。已由 integration 测试守住这一点。
- 管理员工作台新增「投顾复核」标签页:列出待审内容,支持审核通过 / 驳回 / 发布;
前端按 `content_type` 自动选择端点、寻址键与载荷
(方案书发布要 `{publish: true}`,推荐方案发布不读 body)。
- 发布前校验状态:未审核通过不允许发布,与 `publish_book` 的 `IllegalState` 一致。
## 3. ⚠️ 同时发现:投顾的三个分析功能对投顾本人不可用
`ProductRecommendationQuery` **没有 `customer_id`** 字段,而 `generate` 用的是
`int(context.user_id)`(`product_recommendation_service.py:69`)—— 即**把投顾自己**
当成了服务对象。投顾是员工、没有风险测评与持仓,于是实测:
POST /api/v1/advisor/recommendations → {"status": "profile_required"}
POST /api/v1/advisor/asset-allocation → {"status": "profile_required"}
**组合分析、资产配置、生成推荐草案这三个功能,投顾调用必然拿不到结果。**
这是"投顾功能很奇怪"的直接来源之一。修它要改接口契约(加 `customer_id`、
并确定"投顾能对哪些客户生成"的权限口径),属产品决策,未在本提交内改动。
验证:unit+contract **1397 passed**;新增 integration 用例 2 passed;ruff 通过;
mypy 251 文件 0 错;A047 实测管理员 200(带出方案书的 `goal_no`)、投顾 403。
|
2026-09-14 00:17:46 +08:00 |
|
zhangshy
|
2f3138113f
|
Merge remote-tracking branch 'origin/qyqy_develop' into RM2_develop
|
2026-09-13 23:52:17 +08:00 |
|
zhangshy
|
2a3146e050
|
增加风控列表总数并优化站内提醒
|
2026-09-13 23:51:45 +08:00 |
|
lzf_0626
|
3ada1f6c87
|
Merge remote-tracking branch 'origin/qyqy_develop' into qyqy_develop
|
2026-09-13 23:38:57 +08:00 |
|
lzf_0626
|
7208713c17
|
feat(portal): 接入历史净值,产品详情页恢复真实走势图
`fin_nav_history` 一直是**空表**,所以产品详情页画不出走势图 —— 此前那条曲线是
前端 `mock-data.js` 里编的 12 个点位,接入公开产品接口时把它去掉了
(走势图最容易被当成真实业绩),页面改为显示"尚未接入"。本次补上完整链路。
## 1. 取数(app/infrastructure/fund_market_adapter.py)
新增 `fetch_nav_history()`:东方财富历史净值接口(`api.fund.eastmoney.com/f10/lsjz`)
分页取序列。
- **为什么不复用 `fetch_kline`**:它走 `push2his.eastmoney.com`,
该域名在本环境实测连接被拒(`RemoteProtocolError`);
- **为什么不复用 `hq.get_southern_fund_nav_history`**:那个函数校验**南方基金白名单**,
而 `fin_product` 里有非南方基金的产品(510300 是华泰柏瑞的),调它会直接 `ValueError`。
⚠️ 实现里踩到一个坑:接口**会忽略请求中的 `pageSize`**(实测固定每次返回 20 条)。
最初按硬编码的 30 判断"是否最后一页",于是 `len(items) < 30` 永远成立、只取到第一页 ——
走势图看上去"有数据",其实只有最近 20 天,而且毫无报错。
改为按首屏**实际条数** + `TotalCount` 推算页数后,同样区间取到 124 条。
## 2. 落库(tools/sync_nav_history.py,新增)
写入 `fin_nav_history`,按 `(product_id, nav_date)` 幂等 upsert。
实测:20 只产品 / **2513 行** / 2026-03-17 ~ 09-13;重跑**新写入 0 行**。
## 3. 接口(P002)
`GET /api/v1/products/{product_code}/nav-history`,编号 **P002**,已登记 `docs/05` §19。
鉴权口径与 P001 相同(要求有效令牌、不校验权限码,访客令牌可用);`days` 有界 1–365。
**表为空时返回 `count=0` 与空数组,而不是报错** —— 调用方据此显示"尚未接入",
**不得回退到编造曲线**。产品不存在或未上市 → `404`(否则前端分不清"没有数据"
和"没有这只产品")。
## 4. 前端
详情页按序列画 SVG 折线,期数标题改为动态("近 N 个交易日")。
表为空时仍显示"尚未接入"占位,并补上此前缺失的 `.detail-chart__empty` 样式。
`product-detail.js` / `.css` / `index.html` 的缓存版本参数一并 bump 到 `-7`。
## 5. 演示数据
`tools/seed_demo_data.py` 增加第 5 步「历史净值」(现 **11 步**),
否则换台机器演示时走势图又会是空的。
验证:P002 实测 515450 / 510300 各 120 个净值点;ruff 通过;mypy 251 文件 0 错;
unit+contract 1391 passed;integration 108 passed;e2e 冒烟 40/40。
|
2026-09-13 23:38:13 +08:00 |
|
zhangshy
|
38fe6f3689
|
合并主项目最新改动并解决风控前端冲突
|
2026-09-13 23:22:12 +08:00 |
|
zhangshy
|
4f75d32ea1
|
优化风控详情展示与前端交互
|
2026-09-13 23:19:52 +08:00 |
|
lzf_0626
|
9cb0f6e474
|
feat(portal): 公开产品接口(P001)落地,访客三页改读真实数据
访客首页推荐、产品列表、产品详情此前读的是前端手写的
`app/static/portal/common/mock-data.js`:只有 8 只,且**其中 6 只根本不在
`fin_product` 里**(159915 / 512100 / 513100 / 511360 / 159645 / 159925),
还把海富通的 `511360` 标成"南方短融ETF"、把 `510500` 净值写成 6.742(真实 7.6027)。
`README.md` 把它记为"公开产品 HTTP 接口尚未实现"的临时方案。
## 接口
新增 `GET /api/v1/products`(编号 **P001**,已在 `docs/05` §19 总目录与 §19 说明中登记):
- **要求有效令牌但不校验权限码**:访客令牌的角色是 `visitor`、不带任何权限,
这与 `/api/v1/agent-runs`、`/api/v1/conversations` 面向访客的口径一致;
产品信息本身是公开信息。数据面只暴露 `fin_product`(`status='上市'`)与
`fin_market_price` 的最新一行,**不含任何账户/客户字段**(由 integration 测试守着)。
- 字段与 `mock-data.js` 对齐,因此前端渲染与筛选逻辑**一行未改**。
- `change_pct` **可能是 `null`**:当日涨跌需要两个交易日的收盘价,行情只同步过一天时
算不出来。前端对 `null` 显示"暂无" —— `formatPercent(null)` 会渲染成 `+0.00%`,
那等于对客户说"今天平盘",是编出来的结论。
## 前端
- 新增 `common/visitor-token.js`:访客令牌的**唯一实现**(这段逻辑原先只写在客服浮窗里,
现在四处要用;复制四份的话存储 key 与过期判断迟早不一致);`widget.js` 改为复用它。
- 新增 `common/product-notes.js`:产品级披露文案。510300"非本公司发行"那条是**合规披露**,
不能随 mock 一起删掉。
- 三个访客页改读接口,并显式区分 loading / error 状态。
- **删除 `common/mock-data.js`**。
- 产品详情页**不再画走势图**:`fin_nav_history` 目前 0 行,此前那条曲线是 mock 里
12 个编造点位 —— 走势图最容易被当成真数据,宁可不画,并显式说明"尚未接入"。
## 顺带修正
`min_amount` 在库里全是 0.00(那本是场外"最小认购金额"的概念,对场内按手交易的 ETF
不适用),页面不再显示"¥0.00"(会被读成零元起购),改为"1 手(100 份)起"。
验证:接口实测 `count=20`;ruff 通过;mypy 251 文件 0 错;unit+contract 1387 passed;
integration 106 passed;e2e 冒烟 40/40。
|
2026-09-13 22:57:48 +08:00 |
|
lzf_0626
|
f6bfcc26b3
|
fix(demo): 把未上市的 160129 换成真实挂牌的 515450,并修掉种子假行情盖住真行情
## 1. 160129 本就不该在场内清单里
`160129`(南方金利定开债券C)是 `160128`(金利定开债券 **A** 类)的 C 类份额,
而 C 类份额只在场外销售、**不在交易所挂牌** —— 行情源对它永远返回空,下单只能走
净值降级(`source=eastmoney_nav_fallback`),既掩盖了"该产品并不交易"这一事实,
又让成交价带上折溢价偏差。
选型依据:把南方基金全部 **882 个代码**逐个问过腾讯行情源(该源只对交易所上市证券
返回数据),确认真实上市交易的只有 **109 个**;其中 R1/R2 的场内产品
(159700、160128、511070、511810)**原先已在清单内**,所以替换品只能来自 R3 及以上。
选定 **`515450` 红利低波50ETF南方**:仍是南方基金旗下、成交额约 1.3 亿元流动性充足、
红利低波定位偏稳健,与它替换掉的债券 LOF 定位最接近;客户风险等级已覆盖 R3
(原有 `510300` 即 R3)。
改动:`hq.py` 的 `FUND_TYPE_GROUPS`、`tools/import_hq_test_products.py` 的场内映射,
两处都留了注释防止被加回来;`docs/42` / `docs/43` 的产品表与费率表同步
(净值 1.4027、管理费 0.50%、托管费 0.10%);`docs/42` 另加了一条决策记录。
库内用**复用 `fin_product.id = 9100005`** 的方式替换,使 `fin_holding` /
`fin_sim_order` / `fin_transaction` / `fin_market_price` 的既有引用自动跟随,
不产生孤儿数据。那笔历史成交保留 `quote_source='eastmoney_nav_fallback'` ——
它确实是当时的事实,不该被粉饰。同时清掉了 `fin_market_price` 里那条净值降级行,
全库净值降级行情行数归零。
## 2. 种子假行情会盖住真实行情(更严重,且会反复出现)
`seed_sim_account_demo.py` 原来按"今天"upsert 一条假行情(510300=4.50、
510500=6.20,`source='eastmoney_demo_seed'`),而真实行情来自行情源、日期是
**最近交易日**。下单按 `trade_date DESC` 取行情,这条种子行**永远排在真实行情前面**;
它的 `source_updated_at` 只是 seed 运行时刻,过了 `MAX_QUOTE_AGE`(15 分钟)就让整个
产品变成 `503 行情已过期` —— 而库里明明躺着一条刚同步好的真实行情。周末尤其明显:
`trade_date` 落在**非交易日**,价格还是编的。
实测表现:`tools/seed_demo_data.py` 跑完第 4 步刚同步过真实行情,冒烟脚本的下单仍报
`503`,且本该 4.579 的成交价被查到 4.50。
改为:**已有任何行情行就绝不插手**(真实行情优先,种子不参与竞争);
一条都没有时才补,且 `trade_date` 退到最近交易日。
验证:`tools/e2e_smoke_test.py` → **40/40**(下单成交价 4.579 = 真实行情);
ruff 通过;mypy 250 文件 0 错;unit+contract 1381 passed / 0 failed。
|
2026-09-13 22:28:42 +08:00 |
|
lzf_0626
|
4629e7d5dd
|
docs(44): 补一步风控助手演示(意图分派已修复,实测置信度 1.0)
|
2026-09-13 22:08:39 +08:00 |
|
lzf_0626
|
bfd3964ef8
|
feat(demo): 演示数据一键准备、启动脚本与演示流程文档
交付"别人能自己把系统跑起来做演示"所需的四件东西:
- tools/seed_demo_data.py 演示数据一键准备:10 步按依赖排序
(此前散在 10 个脚本里,没人知道该跑哪些、按什么顺序跑,且知识库那步
根本没有脚本、靠手工调接口)
- tools/seed_knowledge_demo.py 知识库演示素材灌入,幂等(先删同 source_file 再灌)
- start.ps1 启动 API + Worker,含依赖检查与**自动刷新行情**
- docs/44-演示流程.md 8 个主线场景的照读流程 + 排障表 + 账号/命令速查
两条硬约束同时写进了脚本和文档:
1. **Worker 必须常驻**:没有它客服对话一直停在 queued(前端只显示"超时")、
新知识不进 Milvus 且**没有任何报错**。
2. **行情有效期仅 15 分钟**(`trade_service.MAX_QUOTE_AGE`):超时后所有委托
直接 503「行情已过期」,而系统**没有自动刷新机制**。故 start.ps1 启动时刷一次,
文档另给"演示中途 503 时补刷、无需重启服务"的处置方法(已实测)。
实测证据:
- start.ps1 在 8099 完整启动,`/docs` 与 `/portal/` 均 HTTP 200(测试进程已清理)
- `tools/e2e_smoke_test.py` → 40/40 通过
- `tools/seed_demo_data.py --dry-run` → 10 步全部正常列出
- 文档中的下单命令实跑:510300 成交价 4.579、金额 457.90、手续费 0.05
|
2026-09-13 21:56:27 +08:00 |
|
lzf_0626
|
2fcc4939e7
|
feat(tools): 新增全流程端到端冒烟脚本 e2e_smoke_test.py
把验收清单里"能用接口判定"的部分自动化:6 条线 40 项,打真实 HTTP、逐条打印 PASS/FAIL。
不是 pytest 的替代 —— 单测看代码正确性,它看"部署后底座是不是活的"。
覆盖:
A 访客 令牌 → 提问 → 收到回答 → 未转人工
B 客户 登录 → 看板/持仓/流水 → 真实下单成交 → 委托详情 → 会话与转人工
C 风控 概览/队列/详情/八类证据/通知/扫描 → 处置闭环(确认→调查→结案)
D 投顾 已发布方案 E 运营 场外邮件与邮箱状态
F 管理员 角色/权限/身份/配置发布/模型端点/审计/转人工工单/画像候选
两个实现细节值得留意:
- **各端点 data 形状不统一**(角色列表裸数组、持仓在 holdings、成交明细在 transactions、
资金流水在 entries、知识列表不套 data),所以这里用 listed(payload, *keys) 依次尝试,
并在注释里写明"别猜字段名"—— 我自己写联调脚本时连续猜错三次。
- 库里没有"待处理"预警时**自动造一条**再验证处置闭环,否则该环节只能跳过
(实测库里的预警常常都已被处置过,第一次跑就跳过了)。
新增 --read-only:跳过所有写操作,用于生产或不想动数据时。
实测:40/40 通过(含真实下单 201 已成交、风控处置闭环三段全 PASS)。
docs/40 补上"一键冒烟"一节。
|
2026-09-13 21:39:37 +08:00 |
|
lzf_0626
|
b6281ba6bc
|
docs(40): 把行情同步列为下单前必跑的前置
全流程验收时踩到:不跑 tools/sync_market_prices.py 就下单,只有 2 只产品有行情、
其余直接 503,而且错误信息只说"行情已过期",看不出根因。把它写进第 0 节,
并说明数据源(腾讯行情 / 东财只有行情类域名不可达)与总份额口径。
|
2026-09-13 21:15:43 +08:00 |
|
zhangshy
|
522c9b19fe
|
Merge remote-tracking branch 'origin/qyqy_develop' into RM2_develop
|
2026-09-13 21:09:09 +08:00 |
|
zhangshy
|
09c80575b7
|
延长风控手动扫描前端超时
|
2026-09-13 21:08:57 +08:00 |
|
lzf_0626
|
de208a039d
|
docs(40): 补全流程验收实测到的接口形状差异
跑全流程 E2E 时连续踩到(都是调用方视角的真实摩擦点):
- 各端点的 data 形状不统一:账户看板 data.account + data.summary、持仓 data.holdings、
成交明细 data.transactions、资金流水 **data.entries**、角色列表 data(裸数组)、
知识列表 items(不套 data)。我写脚本时连续猜错三个字段名,一度误判"资金流水没记录"。
- 创建类接口返回 201 而非 200:下单 T002、创建会话 C001、访客令牌 V001 都是 201。
两条都写进"通用预期",并说明是实测结论。
|
2026-09-13 20:40:16 +08:00 |
|
lzf_0626
|
ff37b0b476
|
docs(40): 记录知识治理端点的两处接口差异
联调时踩到并查明(更正前一版判断:不是接口故障,是调用方解包方式不对):
1) GET /api/v1/knowledge/list **不套 data 信封**,直接返回 {"items": [...], "count": N},
而多数端点返回 {"code":…, "data": {…}}。按 data.items 解包会拿到 0 条,
看着像"库里没数据"——我据此误判过一次,也因此在重灌时没能用列表接口批量删除,
改为按 id 逐个 DELETE。
2) 上传是 POST /api/v1/knowledge/upload(/documents 会 405);
DELETE /api/v1/knowledge/{id} 的语义是标记 expired + 投向量删除事件,不是物理删除。
两条都写进验收清单的"通用预期",避免下一个人重复踩。
|
2026-09-13 20:26:56 +08:00 |
|
lzf_0626
|
7fa4b4c4a5
|
feat(knowledge): 手册补「购买基金需要什么条件」一节并重灌
排查「购买基金需要什么条件」仍转人工的根因:**不是检索缺陷,是知识覆盖不足**。
实测该问法下手册 1.1 节其实排第 1(score=0.5416),但低于 MID_SCORE(0.55),
且与次优间隙只有 0.0195 < MIN_GAP(0.07),所以按置信度规则转人工 —— 规则本身工作正常。
对照:手册里有对应条目的问法分数是 1.0 / 0.7946 / 0.7932,链路健康。
补 1.6 节「购买基金需要什么条件」,只写有依据的事实(开户状态、风险测评与等级匹配、
按手交易的整数倍委托),不编造资金门槛或资质要求。
重灌过程顺带发现:GET /api/v1/knowledge/list 返回 0 块,而 fin_knowledge_meta 当时
有 22 行 active —— 管理端列表与实际入库不一致,所以删除只能按 id 逐个走
DELETE /api/v1/knowledge/{id}。这条接口的问题单独记,不在本次改动范围。
重灌结果:旧 22 块标记过期并投删除事件,新 23 块写入;问「购买基金需要什么条件」
命中 1.6 节 score=0.7793(≥ HIGH_SCORE 0.75),客服从转人工变为直接作答(4.08s)。
|
2026-09-13 20:25:57 +08:00 |
|