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
张胜宇
e85989b344
merge: integrate latest qyqy_develop (auth login + RBAC read) into ZSY branch
...
Incremental merge on top of ef701c8 , which already integrated the earlier qyqy base bbf623a . qyqy_develop only added commits on top of bbf623a , so this merge is conflict-free. Incoming: account/password login (POST /api/v1/auth/tokens), RBAC read-only query API, rate limit dependency, login test console and user management tools. Additive changes in app/main.py, requirements.txt and pyproject.toml from both sides are all preserved. ZSY side capabilities (visitor tokens, customer service agent, knowledge retrieval, profile projection) are unchanged.
2026-09-12 10:37:53 +08:00
张胜宇
ef701c844c
merge: integrate ZSY customer service and profile capabilities
2026-09-11 22:31:51 +08:00
lzf_0626
62501747a6
新增 sys_user 认证现状探查(判断"账号密码登录"可行性)
...
tools/probe_auth_state.py:只读统计 sys_user 的 status / user_type 分布与 password_hash
的格式特征(只输出前缀与长度,不输出完整值)。
结论(docs/evidence/auth-state.json):库里 5 个用户,password_hash 全是占位符 ——
4 个是 'x'(tools/seed_test_rbac.py 写入),1 个是 '!worker-only-no-password-login!'
(alembic 迁移写入的哨兵值,名字本身就表明"不用于密码登录")。
has_real_password_hash = false。
⇒ 在本项目做"账号密码换令牌"不是"加个端点"的事:没有密码可校验,等于要新建一套密码体系
(选算法 + 加依赖 + 定义写入流程 + 改上游数据),而 password_hash 是基线已有字段
(规则 4:不得改变既有业务含义)。docs/05 §11 也已明确该职责属统一身份认证模块。
顺带记录一个数据质量问题:user_type 取值不统一(employee / 员工 两种写法并存)。
2026-09-11 20:30:27 +08:00
lzf_0626
3029d0ca9f
更新 release-state 证据:画像工具白名单已补发(release 254 active)
2026-09-11 20:24:07 +08:00
lzf_0626
927a6fecc0
评审 NL_develop 交付说明:三个环境前提必须先对齐
...
产出 docs/NL_develop交付说明-评审意见.md(可直接转给组员),以及一个只读探查工具
tools/probe_knowledge_collections.py(Milvus 集合 schema 与行数)。
核查中发现的硬矛盾,都有可复核的证据:
1. 配置库不是同一个。对方说 active=216、合并前生效版本是 186;我这边实测 active=201,
最近 5 条 id 就是 201/198/197/196/195 —— release.id 自增,同库不可能一边 216 一边 201。
所以他以为已发布的 customer_service:faq=[search_knowledge, query_customer_profile]
在这边并不存在,而他的画像出口复用的正是这个 key ⇒ 合并后被 ToolExecutor 失败关闭。
同理他说的 fund_query_demo:fund_quote 缺失,我这边是存在的。
2. Milvus 也不是同一个。对方说现库字段是 knowledge_id/snippet、无 visibility、行数
106/177/73;我这边实测是 doc_id/content/chapter/section/.../visibility 共 15 个字段、
行数 125/297/214,且与 knowledge_search_service.py 的 OUTPUT_FIELDS 逐字一致。
他这次的字段映射改动(doc_id→knowledge_id)在这边会直接报 field knowledge_id not
exist,把客服知识检索整条打挂 —— 比他自述的"关闭 visibility 隔离"严重得多。
建议不是 A/B/C 三选一,而是第四种:运行时探测字段名,两套 schema 都能跑。
3. 解释器不同。他用 .venv(项目里不存在,那是他机器上的 gitignore 目录),约定是
D:\conda\envs\jr_py313。所以"mypy 151→181"跑不到本基线 —— 这边是 138 文件 0 错。
另指出:docs/26-JWT密钥管理与轮换.md 会与已存在的 docs/21-JWT密钥管理与轮换.md 重复,
建议并入 21;驳回删除 docs/04/06/10/13/99。
四项待裁决的答复:同意 agent_type 方案(要求补审计);字段映射改为运行时探测;
驳回删除编号文档;26 并入 21。
2026-09-11 16:51:52 +08:00
lzf_0626
8b8883ccca
Worker 失败原因不再只留类名:区分"可落库的固定文案"与"异常消息"
...
起因是查 Worker 运行状态时发现库里 373 条死信的 last_error 全是裸的 "ValueError"
(工具 tools/probe_worker_state.py,证据 docs/evidence/worker-state.json)。
dispatch(run not found)、dispatch_run_completed、dispatch_memory_extraction、
dispatch_profile_rebuild 抛的都是 ValueError,只记类名等于把"哪一处失败"也一起丢了。
但"直接存 str(exc)"是错的:tests/unit/worker/test_outbox_worker.py 那条
RuntimeError("credential=do-not-log") 断言异常消息不得落库 —— 它可能含凭据、SQL 或
客户标识。第一版改动就是这么写的,被这个测试当场拦下(这测试写得值)。
折中:
- 新增 OutboxHandlerError(继承 ValueError,这些失败本就是 ValueError 语义,保持
继承关系才不会改动既有的 except ValueError 行为与断言)。它的 reason 由代码写死、
不含任何请求数据,因此可以落库;
- safe_error_text:OutboxHandlerError → "类名: 固定文案"(截断 500 字符),
其余异常 → 仍只记类名;
- runtime.py 的 5 处 handler 失败改抛 OutboxHandlerError。
测试:新增"固定文案落库"用例;并把既有用例的断言收紧为 last_error == "RuntimeError"
(原先只断言"不含 do-not-log",太松,漏掉的情况测不出来)。
顺带产出 tools/probe_worker_state.py(只读):outbox / agent_run 各状态计数、按事件
类型分组、死信原因聚合。当前环境实测 pending 347、dead 373、published 410、
agent_run 无 queued/running。
门禁:ruff 干净 / mypy 138 文件 / 697 unit+contract / 33 integration。
2026-09-11 16:28:19 +08:00
lzf_0626
c11e56c7aa
同步 docs/24 的过期状态;登录端点不改(docs/05 明确平台不实现)
...
docs/24 是客服阶段的总结,正文保留为当时的事实,本次做两件事:
1. 刷新"当前状态"表的数字:mypy 113→137 文件、unit+contract 497→664、
integration 29→33、active 发布 181(5 项)→201(10 项:9 条 agent_tools + 1 条客服
闲聊提示词)。数字由 tools/probe_release_state.py 只读查库得到,证据留档
docs/evidence/release-state.json。
2. 追加第七节"后续更新",记录阶段总结写完之后的变化:两件待决策事项已落地(闲聊
提示词已按继承语义发布、适当性两侧口径复核通过)、风控合并后的处理要点、以及
三个仍然开着的口子(热线占位符、like %% 全表扫、offset 游标并发跳行)。
登录端点:**不改**。docs/05 §11 明确"JWT 签发、刷新、注销由统一身份认证模块负责,
Agent 平台不重复实现",本平台只做校验(app/core/security.py)。第五节第 8 条据此改写,
同时把第 7 条"风控尚未启动"标记为已完成。
顺带修正"已知缺口"里的一条:列表接口的 next_cursor/has_more 风控侧已补齐,客服侧仍待办。
2026-09-11 14:26:17 +08:00
lzf_0626
ff681df143
落地第十五条豁免额度校验;登记三处业务裁定
...
一、第十五条豁免规则(docs/25 第七节 #2,业务裁定:实现)
政策原文:C3→R4 签署风险揭示书后**可买**,但单只 R4 持仓不超过总资产 20%;
C4→R5 同理,上限 10%。越级购买本身不是违规,超出额度才是 —— 原先扫描侧只看
"留痕是否齐全",于是"签了字但买超额度"这种明确违规没有预警;研判侧也把豁免的
前提条件(留痕齐全)当成了结论,直接判"疑似误报"。
- 扫描侧:新增 EXEMPTION_LIMITS 与 RiskRuleEngine._exemption_state,核算
"单只持仓 / 总资产"并写进证据快照;触发条件改为
gap > 0 and (missing_trace or 超出额度)。
- 研判侧:_assess_rw007 先判额度再判留痕。超限 → 证据支持风险;留痕齐全且在额度
内 → 疑似误报;留痕齐全但快照缺总资产/持仓 → 继续复核。
数据前提(tools/probe_exemption_data.py,证据见 docs/evidence/exemption-data-probe.json):
库内 fin_customer_profile 仅 1 行且 total_asset = 0.00、fin_holding 0 行、无任何申购
交易 —— 这条规则当前不会被触发,与 behavior_score 同源(画像与持仓由本项目之外的
流程写入)。因此刻意不把"算不出来"当成"超限":拿 0 去算会让每一笔 C3→R4 都变成违规,
豁免规则反倒成了误报源。上游把数据写入后无需再改代码即可生效。
二、三处业务裁定(此前挂在"待裁定")
- 模型网关 chat + tools 入口:本轮不补,按基座能力缺口记录。它要贯穿
ModelGateway → … → BaseAgent 整条链路,属公共契约变更,演示联调期影响面大于收益。
- exclude(关闭误报)是否必须先"调查中":保持现状,不加门禁。
- 政策冲突:以第十四条 C ≥ R 为准;客服侧 check_suitability 复核后确认本来就按
C ≥ R 实现,无需改动。
三、其他
- 新增 tests/unit/service/test_risk_judgement_rw007.py(6 例)与扫描侧 4 例。
- 风控文档 03/05 同步 RW-007 的豁免额度条件与研判口径。
2026-09-11 14:24:45 +08:00
lzf_0626
df96275ab5
给 trigger_rule_codes 加 JSON 多值索引;登记 P3 处理结果(docs/25 P3 #21)
...
实测确认 #21 描述准确:fin_risk_alert 只有 11 个普通 BTREE 索引 + 主键 + alert_no
唯一键,规则命中的 JSON_CONTAINS 查询 EXPLAIN 为 type=ALL、possible_keys=NULL,
即全表扫。MySQL 8.0.27 支持多值索引,故新增迁移 20260911_risk_rule_index:
ADD INDEX idx_fin_risk_alert_trigger_rule_codes
((CAST(`trigger_rule_codes` AS CHAR(16) ARRAY)))
迁移幂等(先查 information_schema.STATISTICS),upgrade/downgrade 往返已验证。
生效后 EXPLAIN 变为 access_type=range 且 key 命中该索引,原始证据留档在
docs/evidence/risk-index-probe.json(由 tools/probe_risk_index.py 生成,只读探查)。
只解决一半,另一半如实记为限制:若干 like(f"%{keyword}%") 全表扫无法用 B-tree 索引,
根治需全文索引 + 中文分词组件(部署依赖),本轮不做。
revision 名刻意压到 32 字符以内 —— alembic_version.version_num 是 VARCHAR(32),
超长会在写版本号时报 1406,而 DDL 是非事务的,那时索引已经建好了。
docs/25 追加"P3 处理结果"表,逐条登记 17-25 的状态:#19 是协议级重做(keyset 分页)
不单方面改,#21 部分修复,#25 前半段不成立,其余已修。
2026-09-11 14:19:05 +08:00
张胜宇
24f75636a0
feat: add knowledge import preflight
2026-09-10 20:22:53 +08:00
张胜宇
a789872c79
test: record pre-migration workspace evidence
2026-09-10 17:11:52 +08:00
lzf_0626
6516ccb385
feat: 第二版——接口契约对齐 docs/05,修复静默故障与数据库基线
...
相对第一版 46fc976 的完整变更。组员迁移对照表见 docs/20。
一、对外契约对齐 docs/05(破坏性,共 4 处,组员需按 docs/20 调整)
1) 配置发布端点改为文档规定的复数资源名:submit→validations、
approve→reviews(需 body decision)、activate→activations、
rollback→rollbacks;第一版这 4 个动词式路径 docs/05 从未定义过。
2) 错误码由 8 个笼统码改为 15 个具体语义码(FORBIDDEN→AGENT_PERMISSION_DENIED、
UNAUTHORIZED→AUTHENTICATION_REQUIRED、CONFLICT→RESOURCE_VERSION_CONFLICT、
RESOURCE_NOT_FOUND→RUN_NOT_FOUND/SESSION_NOT_FOUND 等),
输入类错误状态码 400→422。
3) POST /api/v1/agent-runs 与 GET /api/v1/agent-runs/{run_id} 统一为
{data, meta} 信封(data 内字段名与语义未变)。
4) 错误响应体统一为 {error:{code,message,retryable,field_errors}, meta:{trace_id}},
不再返回 FastAPI 默认的 {"detail": ...}。
二、数据库基线与约束
新增 39 张表的基线迁移(链根)与联合唯一键纠偏(4 张表、删 8 增 4,幂等收敛);
撤下 config_release 的双人复核 CHECK(应用层已允许自审,审核节点保留,
自审如实写入 reviewer_id);记忆 active key 生成列与唯一键;
activate 开始记录 supersedes_release_id 使版本链可追溯。
docs/00 基线未修改,未重命名或删除任何表与字段。
三、修复会静默出错或无报错的缺陷
- 跑完集成测试后平台会静默失去生效配置:清理只删自己创建的版本,却没有恢复被它
顶成 superseded 的原生效版本,且审计一并删除因而完全无痕,表现为所有工具被拒
但没有任何报错。已修清理逻辑并加恢复。
- Worker 单轮异常导致进程退出;记忆抽取调用方的“事务已开始”异常;
召回缓存丢失 degraded 标记;连接时区未生效导致 created_at/updated_at 差 8 小时;
.env 与 os.getenv 密钥来源分裂导致“没有可用的已批准模型端点”。
- 记忆信号识别漏判与跨键误命中;SSE 未带 Accept 的协商行为。
四、功能补齐
记忆链路 P1/P2/P3(抽取、受控词表、召回与缓存、生命周期级联及投影事件)、
fin_* 场内交易只读 ORM 层、agent_intent_config 状态流转并在运行期真正生效、
限流(Redis 固定窗口、故障一律放行)、游标校验、trace_id 中间件、
示例业务 Agent fund_query_demo 与一键端到端验证脚本,以及审计/指纹/迁移状态工具。
五、文档与验证
新增 docs/19(业务 Agent 接入实操)、docs/20(第一版迁移指南)与 docs/evidence 证据;
docs/01/02/06/08/09/17 同步实现现状。
验证结果:ruff 通过、mypy 103 文件无错、unit+contract 447 passed、
integration 29 passed、acceptance_check --production 7 PASS、
demo_agent_e2e 9/9 PASS(含失败关闭反证)。
2026-09-10 15:55:54 +08:00