fix(rag): 全文索引在容器里根本没持久化 —— 补 /data 卷 + BLEVE_PATH #8
Reference in New Issue
Block a user
Delete Branch "feat/site"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
排查「fulltext 路 0 命中」时发现的真问题,比原以为的分词器问题严重得多。 现状:mcp-go 容器**没有任何 volume**,也没设 BLEVE_PATH,于是索引落到相对 CWD 的 .data/bleve。而运行阶段没有 WORKDIR(CWD=/)、进程又是非 root(uid 10001), 两种结局都糟: - 在 / 下建不了目录 → openBleve 静默退回内存索引,重启即清零; - 就算建得了,也只是写进容器可写层,每次发版重建容器一样清零。 更糟的是它不报错:混合检索少一路只表现为召回变差,看不出故障。 改动: - Dockerfile:建 /data 并 chown 给 app,WORKDIR 设为 /data; - 两份 compose:BLEVE_PATH=/data/bleve + 挂持久卷(132 那份此前连 顶层 volumes 段都没有); - .env.example:写清默认相对路径相对的是**启动目录**——本地从仓库根起 和从 sundynix-mcp-go/ 起会写出两份互不相干的索引,排查时极易被坑。 附带澄清:中文分词本身没问题。直接开落盘索引验证过,mapping 里 text 字段 确实是 cjk,实测「星间链」命中 4 条、「海关监管」命中 104 条,BM25 打分正常。a4852c3那次修复是有效的,之前怀疑分词器的判断作废。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>清单里那批小毛病,逐条复核后修(「审计详情列缺失」那条已不成立,早补上了)。 1. 审计筛选只在当前页生效 —— 影响最大的一条。分页是服务端的,筛选却在 前端对已取回的 50 条做,于是搜一个用户 ID 显示"无结果"时,后面几页 可能还有几百条。审计的用途就是查证,"搜不到"会被读成"没发生过"。 改为 action/path/q 三个条件全部落到 SQL,前端只管发条件(防抖 300ms)。 2. 服务状态页把 mcp-go/mcp-py 的工具数写死成 23/4 —— 增删工具后一直骗人, 且服务离线时照样显示,看不出工具其实一个都没注册上。改取实际上报值。 3. 模型删除一点即删,无任何确认。补二次确认,并对"正在使用中"的模型 单独说明后果(删掉会立刻打断线上对话/向量能力)。 审计筛选补了 4 组回归测试,两条是踩出来的坑: - q 的 OR 组必须带括号:gorm 以 AND 拼接各 Where,裸 OR 会让 action 条件被绕过(测试里用"同 IP 不同方法"两行钉死这个语义); - LIKE 必须显式写 ESCAPE '\':Postgres 默认拿反斜杠当转义符,SQLite 不写就没有转义符——原来的写法在单测里静默失效,搜 "100%" 命中 0 条。 顺带把 ILIKE 换成 LOWER()+LIKE,这段才能被内存库覆盖。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>全平台任务页此前只能看列表,点进去什么都没有。现在点一行开抽屉,四个页签: 执行轨迹、最终输出、评测明细、提交时的 DSL。不含审批操作——审批是客户端 用户的行为(桌面端 ApprovalBar),管理端只做观测。 不复用用户面的 /tasks/:id/replay:Task/Eval 都在租户插件作用域内,用请求 ctx 查别的租户的任务不会报错,而是静默返回空输出/空轨迹,UI 上表现为"这任务没 产出",比报错难查得多。新增 admin 端点走 WithoutTenant。 数据取自 sundynix_task 收尾落库的 output/trace 列,不依赖 Redis 流(10min TTL)。 所以这是复盘视图,运行中的任务轨迹为空——UI 里明确写出来,免得被当成轨迹丢了。 修的两处与测试环境失真有关(写测试时暴露的): - 测试库没配 NamingStrategy,与 OpenPostgres 不一致:多数模型有显式 TableName() 碰巧对得上,但 Task 这类没有的会退化成 "tasks",导致写裸 SQL 的查询在测试里查无此表。现已对齐 sundynix_ 前缀 + 单数表名。 - graph 的 ::text 换成标准 cast(... as text):前者是 Postgres 专有, 换掉后这条查询才能被内存库覆盖。 新增 5 组测试,其中一组专门先证明"租户过滤在测试环境里确实开着"——否则 "跨租户能读到"的断言可能只是因为插件没装,属于假过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>排查"向量路为什么是空的"花了半小时,因为空就是空,没有任何线索。这次把 整条检索链上的静默降级一次清掉。 真 bug(不只是可观测性): - kb_search 与 Search() 都拿 rag.Ready() 当总闸,而 Ready() 只代表"向量路 可用"(embedding + Milvus)。全文(bleve)与图谱(Neo4j)根本不依赖它们,却 被一并毙掉 → "模型配置没下发"表现为"整个知识库什么都搜不到",还不报错。 改为逐路判定,任一路可用就仍有召回。 不再吞错: - milvus.search 原先把 error 转成 nil,nil —— 检索失败与无召回彻底无法区分; - bleve.search / graph.search 出错直接回 nil,连日志都没有; - searchPaths 丢掉 embedding 的 error。 三处改为如实返回,错误统一打日志。 逐路诊断(RouteDiag):每路上报 ok/empty/disabled/error + 耗时 + 原因,经 kb_search 的 diag 参数(仅试验台传,生产调用返回值不变)→ gateway → 检索 试验台。界面上现在能直接看出"这一路没配置/报错了/确实没匹配",不必翻日志。 内存兜底索引也会在 note 里点明"重启即清零"。 测试:3 组,覆盖"无 embedding 时全文仍可召回"、三种空的区分、内存索引提示。 把总闸加回去验证过第一条确实会红——测试能抓到这个回归,不是摆设。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>上一个提交里,诊断解析被塞进了用户面的 KbSearch,而真正传 diag 的 AdminKbSearch 还在用老代码 —— 于是试验台永远拿不到 routes。写法上是 字符串替换只改了文件里第一处匹配,而这两个 handler 的收尾代码一模一样。 用户面已还原(它不传 diag,拿到的本就是裸数组,多一层解析是错的)。 补 3 组 mcp 层测试钉死 kb_search 的两种返回形状: - 不带 diag(生产调用:dispatcher / Agent 工具链)→ 裸 JSON 数组,契约不变; - 带 diag → {hits, routes} 对象; - not ready 时不被总闸短路,仍逐路诊断。 这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若无条件生效,所有 调用方的解析都会静默失败。 真环境验证(本地 gateway + mcp-go + 真 PG/Milvus/bleve): vector empty 该知识库在 Milvus 中没有向量(未入库或集合被重建过) fulltext ok 15 命中 graph disabled Neo4j 未连接或未配置 以前这三行都只是"0 条"。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>把检索链路那次的做法推到同类位置。判据是"丢了这个 error 的代价",只改代价 高的,不动刻意的 fire-and-forget。 改(丢了就是数据永久丢失或行为不可解释): - task_handler: 任务**输出**与**执行轨迹**的收尾落库。这是唯一的持久副本 (Redis 流 10min TTL),失败则永远无法复盘,界面上只显示"这次运行没有 轨迹"。轨迹的 json.Marshal 失败分支同样是静默跳过,一并补上。 - admin.broadcastActive: 模型配置热更新广播。失败 = dispatcher/mcp-go 仍用 旧配置,症状是"控制台改了模型却不生效",而操作者这边一切正常——正是今天 排查半天的那类问题。 - middleware/audit: 审计留痕写入。不阻断主流程是对的(业务已完成),但静默 失败意味着敏感操作没有记录,且没人知道记录缺了,这是合规缺口。 不改(确认过是合理的): - dispatcher 的 PublishToken/PublishExec:往前端推流,丢一帧是 UI 瑕疵, 且按 token 打日志会刷屏; - 计量回写 PublishUsage:本来就检查 error 并打日志,钱的路径是干净的。 另修一处误导文案:任务下钻的轨迹空态原本把原因说死为"早于该功能上线", 现在"落库失败"也是已知原因,文案改为并列并指向日志里的 [task] 告警。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>管理端「支付 → 订阅」:套餐配置 + 全平台订阅观测。配置时直接算出「一个周期 发几次、合计多少积分」,时长不能被间隔整除时橙字提示到期前会有空档 —— 让人在 配的时候就看见后果,而不是上线后才发现只发了一次。 顺带修了个真 bug,是拿真库验订阅时撞出来的(本地 42 个租户里 11 个中招): credit_balance_micro 是后加的列,早于它创建的租户行值为 NULL。而入账语句是 「余额 + N」—— SQL 里 NULL + N 仍是 NULL,于是这些租户**充值永远不到账**: 分录照写、余额不动、不报错。这条路径是充值/兑换码/退款/扣费/订阅发放共用的, 不是订阅引入的问题。 三处修: - 5 处余额增减一律改 coalesce(credit_balance_micro, 0),新写入自愈; - 启动迁移回填存量 NULL(按账本求和,让「余额 = SUM(ledger)」重新成立); - 模型只加 default:0,**刻意不加 not null** —— 存量库有 NULL 行,AutoMigrate 尝试 SET NOT NULL 会直接失败,而且它在回填之前跑,等于把部署搞挂。 回归测试先证明能失败(去掉 coalesce → 余额 0)再确认修复。第一版测试因为我给 模型加了 not null 而无法造出 NULL,恰好暴露了上面那个部署风险。 真库验证:回填后 42 个租户 0 个 NULL;那个"有分录但余额 NULL"的租户余额 2981 = 账本合计 2981.34。管理端页面显示真实订阅(已发放 3 次 = 首笔 + 补发 2 笔)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>