Repository navigation
Releases: XiaoleC05/CongoRAG
Release list
Site site-v2026.10.20
Static site and daily digest binary. Deploy with ops/deploy-site.sh.
ConGoRAG v5.0.2
v5.0.1 修掉了集成测试的失败,CI 于是跑到了下一步,又暴露出第二个问题。
这一版同样只影响测试与 CI,不影响任何运行时行为 —— 已经发布的
v5.0 / v5.0.1 产物都无需替换。
修复
- CI 的契约测试会红:
scripts/contract-test.mjs的检索那条断言期待 200,
但它跑在集成测试之后、用的是同一个库 —— 而internal/llm那批集成测试
会模拟"从没做过 BYOK 的全新数据库",把 embedding 模型配置清掉。于是 api
返回 404(no embedding model configured yet),而契约对这条请求同样
声明了 404。这个矛盾一直存在,只是前几次 CI 都在上一步就停了、这一步
从没跑到过。
修法是让那条断言按契约放行两种结局(200 时仍校验hits是数组),
并给call的expect加上多值支持
完整变更:v5.0.2...HEAD
ConGoRAG v5.0.1
v5.0 的集成测试在 CI 上会失败,只影响测试,不影响任何运行时行为 ——
已经发布的 v5.0 产物无需替换。
修复
- CI 的 Integration job 会红:
internal/llm的集成测试会把
document_chunks.embeddingALTER 成halfvec(768)(那个文件自己记着这条
「会动全局 schema」的副作用),而 v5.0 新增的internal/retrieval集成测试
用的是 1 维的假 embedder。两者共享同一个库时(CI 就是一个容器跑
go test ./...)必然撞上different halfvec dimensions 1 and 768。
本地make test-integration看不出来 —— 那条路每个包起自己的容器。
修法是让那几条测试自己把向量列设成它要用的维度,用完恢复,不再依赖
别的包留下的状态
完整变更:v5.0.1...HEAD
ConGoRAG v5.0
这一版没有新功能:它是对 v4.0 落地之后的一次全面审查与修复。五路并行审查
(域逻辑 / API 层 / 数据库 / 前端 / 工程化)产出 34 条(#97–#130),全部落地,
改的都是已经交付的行为里那些不正确的、会静默出错的部分。
行为变更
| 变更 | 说明 |
|---|---|
升级前必须先跑 make migrate-up |
新增三条迁移:0012_token_usage_indexes(补上 token_usage 两个外键列的索引 —— 级联删会话或删模型时不再全表扫描这张唯一会无限增长的表)、0013_enum_constraints(七处文本枚举列补 CHECK,并删掉一条被唯一约束完全覆盖的冗余索引)、0014_retention_indexes(事件表剪枝用的索引)。漏跑不会立刻出错,但用量表会随使用线性变慢 |
CONGORAG_PORT 环境变量被移除 |
它此前从未生效(Config.Port 全仓零读取点),真正决定监听地址的一直是 CONGORAG_LISTEN_ADDR。README 的配置表里也删掉了它 —— 留着会让人以为改了它就能换端口。compose 里那个同名变量是另一回事(宿主机端口映射),不受影响 |
| 聊天与 Agent 输入有了长度上限 | SendMessageRequest.text 与 StartAgentRunRequest.input 在契约里补了 maxLength: 8000。超限返回 invalid_argument,且不写任何消息行(此前会留下一条永远没有回答、也永远不会被清理的占位气泡)。直连 API 的调用方按严格 schema 解析时需要跟着改 |
| JSON 端点有了 1 MiB 请求体上限 | 此前只有文档上传有上限。超限返回 400、文案里带上限值。上传仍走 CONGORAG_MAX_UPLOAD_BYTES,不受影响 |
服务端新增 5 分钟 ReadTimeout |
此前只设了 ReadHeaderTimeout。对 SSE 安全 —— 读超时只覆盖读请求,而 SSE 的长处在响应方向(这一点是实测确认的,不是照抄结论) |
| SSE 的错误帧不再泄漏 5xx 内部错误原文 | 此前工具失败会把 SQLSTATE、表名、连接串一路发到前端 toast 的第二行;而"5xx 不返回原文"这条策略此前只在兜底帧成立,兜底帧又只在主帧发不出去时才跑 —— 所以那条测试是假绿。现在两条路径走同一个判据 |
SSE 的 id: 0 不再出现在线路上 |
"运行根本没开始"那类错误帧(空输入 / agent 不存在 / 没有可用模型)此前会带 id: 0。按协议,不带 id 的帧才不更新续传游标;写 0 会把游标退回起点、下次续传重放整轮。协议文档一直是对的,是实现在违反它 |
| SSE 的错误类型补齐了三档 | conflict / conflict_duplicate_key / not_found 此前在 SSE 侧一律落成 internal_error,而 REST 侧是分开的 —— 同一类失败在两条通道上 type 不同。现在由同一张表产出 |
| 工具结果截断到 16 KiB | 此前工具结果原样进模型消息历史,一个检索工具就能把上下文撑爆;而撑爆后上游返回的错误会被归类成 upstream_llm_error,把用户引向"检查 API Key 和配额"—— 排查方向从第一句话起就是错的。超限结果现在截断,并显式告诉模型"结果被截断了" |
| 事件表的保留窗口是 24 小时 | conversation_events / run_events 现在按时间剪枝(此前只增不减 —— 实测 6 个会话攒了 4853 行,其中 97.5% 是流式 token 事件)。代价:超过 24 小时的 run 走 run 级重订阅只会拿到空历史(GET /runs/{id} 的 input/output/steps 不受影响)。tool_effect_log 不按年龄剪 —— 它是"工具是否已执行过"的唯一判据,剪早了会让恢复重放已执行过的工具 |
| 流式 token 事件改为攒批落库 | 此前每个 chunk 写一行库(两个往返)。现在按 300ms / 1 KiB 合成一条。客户端看到的帧类型与语义不变 |
修复
(以下都不改变 API 形状,列出来是为了让"这一版到底改了什么"可查。)
- 文档索引把 embedding 的 HTTP 调用包在写事务里 —— 一条连接被占住最长 30 分钟,10 个并发文档任务就能占满连接池、把同队列的维护任务一起饿死
- 取消 / 断线时正在执行的那一步工具会永远停在
running,而 ADR-007 崩溃表的第一判据就是"有没有一行 running 的 step" - 工具效果账本用会被取消的请求 ctx 写、失败只记一行日志 —— 会让"工具到底跑没跑过"在最需要它的那一刻失准
Resume重放失败的收尾用请求 ctx 且丢弃错误,run 会卡在running且既不可恢复、界面也不会显示失败- 模型配置不校验
contextWindow > maxOutputTokens,两个数字填反后每条消息都失败、且没有任何线索指向引导页 - 文档列表的分块数聚合抹掉了 keyset 分页的索引优势 —— 每翻一页都要扫全库文档再排序
- 两个周期任务对每个会话单独发查询(N+1),且用不上
0011新建的updated_at索引 - 幂等补发每 100ms 轮询一次事件表、无退避,最长持续 10 分钟
Bootstrap在请求路径的事务里做表重写 + 非CONCURRENTLY建 HNSW 索引,全程 ACCESS EXCLUSIVE- SSE 已经开始后 handler panic,Recovery 会把一段 JSON 写进
text/event-stream,客户端表现为流静默结束 - worker 只配了一个队列,一次批量上传就能把周期维护任务饿死数小时
- 前端:乐观上屏的消息被插进"已加载页里最旧"的那一页 —— 翻过历史后提问会跑到记录中间,表现为"提问看不见、回答在跑"
- 前端:离开页面或切换会话不中止 SSE —— 额度继续烧,而那个位置上的取消入口已经跟着页面消失
- 前端:
conversations/:id没有按 id 加 key,会话间前进/后退时流式状态会串到另一个会话上 - 前端:流式过程中每个 token 都给每张工具卡片做一次全量
JSON.stringify(截断发生在序列化之后) - 前端:发送之后输入框失焦,键盘用户每一轮都要重新点一次
- 前端:语法高亮把 36 种语言全打进会话页(单个 chunk 334 KB,其余页面 2–20 KB)→ 改为 16 种语言按需加载,会话页降到 228 KB
make release-dry承诺"不碰 git、不联网"却仍在顶层发一次git ls-remoteupgrade.mjs/rollback.mjs零自动化 —— 它们会DROP DATABASE+ 清卷,是全项目代价最高的一条路径,却一个门禁都没有- CI 五个 job 没有一个碰 Docker(Dockerfile、两份 compose、
migrate-entrypoint.sh全无覆盖),改坏了要等到打 tag 才红 evals/measure_recall.sh的「N/A(分块数为 0)」分支永远不可达 ——grep无匹配返回 1,配上set -e让脚本半路无输出地死掉- SPA 路径穿越的回归测试是自证的:假文件系统里没有哨兵文件,断言在任何实现下都成立
- 若干文档与注释漂移(
agents/runs/{runId}/events这个不存在的旧路径、ADR 引用的迁移文件名、SSE 心跳、README 里 test-integration 的描述停在两版之前)
完整变更:v5.0...HEAD
ConGoRAG v3.0
这一版是能力建设,不是缺陷修复:v2.0 修完了 v1.0 的 36 个缺陷,v3.0 把
那些"文档里承诺了、代码里没有"的功能补齐,并还掉一批工程债。
行为变更
| 变更 | 说明 |
|---|---|
| 换 embedding 模型不再一律被拒 | v2.0 起换模型返回 409,且应用内没有恢复入口,只能手工清库。现在第一次提交仍返回 409,但 type 是 embedding_change_requires_reindex,前端据此弹确认框;用户确认后带 allowEmbeddingReset: true 重发,服务端在同一个事务里清空旧向量、改列类型、把全部文档重新排队重建 |
| 未声明工具能力的模型会让带工具的 Agent 不可用 | Agent 的创建与运行现在会读 Capabilities.ToolCalling。为了不"升级即坏",引导页的复选框默认改成勾选,并有一条迁移把已有的 chat 模型行回填上这一位——所以对已有配置是零变化。但如果你在引导页把它取消勾选,带工具的 Agent 会立刻不能创建/运行 |
| 三个列表端点改成游标分页 | GET /conversations/{id}/messages、GET /knowledge-bases/{id}/documents、GET /agents/{id}/runs 的响应从裸数组变成 {items, nextCursor},并新增 limit / cursor 查询参数。这是破坏性变更,直连 API 的调用方必须改 |
| 会话消息的第一页是最新的 50 条 | 不是最旧的。翻下一页拿更早的(聊天页打开就该看到最近发生的事) |
/healthz 现在只报进程存活 |
新增 GET /readyz 探数据库。交付期做健康检查时:重启策略看 /healthz,流量门禁看 /readyz——反过来会让数据库抖动变成一次实例重启 |
首次启动需要能访问一次 openaipublic.blob.core.windows.net |
tiktoken 词表现在在启动时预热,失败就拒绝启动(而不是起来之后每条消息都报错)。缓存落在 CONGORAG_TIKTOKEN_CACHE_DIR(默认 ./data/tiktoken-cache),只需要成功下载一次;离线环境的报错里带着缓存目录、下载地址和文件名怎么算 |
| 新增三条迁移 | 0006_idempotency_replay(幂等键)、0007_agent_tool_calling(能力位回填)、0008_list_pagination_indexes(分页索引)。升级前必须先跑 make migrate-up——漏跑 0006 的直接后果是聊天整条路径 500 |
| token 用量开始记账 | 每次模型调用写一行 token_usage(此前这张表没有任何写入者)。行数 = 调用次数,所以本地库会开始增长 |
Added
- 幂等键重放:
POST /conversations/{id}/messages接受可选的
Idempotency-Key请求头。同一个会话带同一个键重复提交时,服务端不重新
生成,而是把那一轮已记录的事件补发一遍(同样的帧类型、同样的真实
event_id)。键保留 24 小时;同一个键配不同的正文会返回
invalid_argument的错误帧,而不是静默重放旧答案 - 重新索引:
POST /documents/{id}/reindex与
POST /knowledge-bases/{id}/reindex。换模型之后重建、或者只重跑某一份处理
失败的文档 - 就绪探针
GET /readyz:数据库不可用时返回 503 +
application/problem+json - 用量读端点
GET /api/v1/usage:按模型聚合的 token 用量,带
since/until时间窗 - 优雅退出:两个进程都接上信号。api 会先拒新请求、再取消在途请求的 ctx
排空 SSE(在途请求最长等 30 秒,超时强制关闭连接);worker 给正在跑的任务
留 60 秒收尾,超时后强取消 - 静态资源缓存头:
assets/下的内容哈希产物是
public, max-age=31536000, immutable,index.html与favicon.svg是
no-cache,错误响应是no-store - 前端路由级拆包:6 个页面改成
React.lazy,并新增
RouteErrorBoundary(chunk 加载失败时给一个"刷新页面"的按钮,而不是白屏) - 按钮级错误提示(toast):写操作失败改为 toast;查询失败与流式生成失败
仍然保留页内 Alert(前者失败后页面本来就没内容,后者是"这一轮失败了"的
持久记录) make test-integration:本地一键跑集成测试。它在每次重建的独立测试库
(congorag_test)上跑,不碰开发库- eval 数据集:
evals/下有语料、20 道题(14 道能答 + 6 道不该答)与
指标脚本。它测的是"检索到的内容对不对",与原有的measure_recall.sh
(测 HNSW 近似索引的误差)分工不同 - 文档:
docs/adr/从 1 篇补到 5 篇;新增docs/testing.md
(平台相关行为怎么测)与docs/releasing.md(发布流程)
Changed
Capabilities.ToolCalling的契约默认值改成true,引导页复选框默认
勾选(见上面的行为变更)GET /conversations/{id}/events的契约描述改为事实:它补发完历史就结束
响应,不持有连接等新事件(那条承诺从来没有被实现)docs/sse-protocol.md里"命中幂等键后用after_event_id=0重新订阅"那段
是错的,已改写:event_id按会话发号,0会把此前每一轮的 token 全部重放contracts/openapi.yaml的info.version从0.1.0改成2.0,从此
跟产品版本走(见 ADR-002)。
兼容性承诺的边界仍然是路径里的/api/v1,不是这个号
Fixed
useTheme的多个消费者会失同步(每个实例各存一份主题状态)——侧栏切
主题时其它消费者永远停在旧值。改成共享 storeReady状态的文档落进ProcessDocument的 default 分支报"状态冲突"
——重新索引的安全网现在接住它TestIntegration_Bootstrap_EndToEnd的向量验证会在空库上跳过——它原本
依赖"环境里恰好有一个知识库",改成自己造探针数据- 对话页加载更早一页时会把视口强行拉到底部(滚动的依赖从整个列表改成最后
一条消息的 id)
未实现(有意为之)
memories的重嵌入交给周期任务,不是切换事务的一部分:清空在那个事务
里完成,重建由 worker 的下一次 tick 自愈。所以换模型后长期记忆会有一段时间
检索不到messages.token_usage这一列继续留空:token_usage表是唯一真相,
两处同时维护必然漂移。要按消息显示用量时用where message_id = ...查GET /api/v1/usage只做按模型聚合,不做按会话聚合——后者要 JOIN
messages,而只有聊天主路径有message_id,按会话加起来会天然少于总量- 契约版本仍不表示兼容性:见 ADR-002 的"必须一起记住的"
已知限制
- 本机跑不了
go test -race(没有 gcc 且CGO_ENABLED=0),竞态检测只有
CI 的 ubuntu runner 会跑。这是本地验证与 CI 的唯一差异 - 平台相关行为的测试证据:临时文件清理顺序那条修复现在有一条与平台无关
的测试(closeAndRemoveTempFile的接缝),但"Windows 上句柄未关时删不掉"
这个事实本身仍然只在 Windows 上可复现。见docs/testing.md - 完整 eval 不进 CI:它要打真实的 embedding / chat API、花钱、依赖网络,
且回答生成没有固定 temperature 所以结果随机——放进 CI 会得到一条随机翻红的
流水线。只有指标数学的单测进 CI - 服务端主动关停时,在途 SSE 收到的错误类型不够诚实:优雅退出(#40)会让在途的流以一条
type: internal_error、detail 为client disconnected: conflict的错误帧收场——原因是
关停复用了"客户端断开"那条路径。对本地单机应用影响很小(关停时用户本来就在重启服务),
要修得新增一个service_unavailable错误类型并同步改sseerr/problem.go/ 前端文案表,
那是一次影响所有错误路径的改动,不放进这一版 - 本次提交与 tag 未签名(本机没有可用的 GPG 私钥)
完整变更:v3.0...HEAD
ConGoRAG v2.0
v1.0 全量代码经过一轮 15 个维度、带对抗性验证的审查,发现的 36 个缺陷全部修复。每个缺陷有独立 issue(都在 v2.0 里程碑),每个修复带一条回归测试。
本次发布含对外行为变更,升级前请读「行为变更」一节。
修了什么
对话与上下文
- 历史按最新在前交给模型(
RecentMessages倒序返回,调用链上没有一层重排):现在查询内层取最近 N 条、外层按时间序返回 - 本轮自己刚写入的消息被当作历史回放:当前提问不再被重复计入预算,空的 assistant 占位行不再进 prompt
- 回放历史的角色被压平成
user:ctxmgr.Item补上Role并一路传下去,助手自己说过的话不再记在用户头上 - 检索片段与记忆被拼进 system 消息:改为单独的 user 角色消息 + 定界符,并剥离正文里自带的定界符
- 摘要水位线跨过仍在流式生成的行:批次截到最后一条已定稿的消息,已完成的回答不再被永久挤出后续 prompt
- 空 completion 覆盖已有摘要:空值判定后拒绝写入,水位线与旧摘要都保留
- 压缩器忽略
targetTokens:预算写进提示词,压缩结果不优于被替换区域时整条丢弃
流式与失败语义
- 客户端断开时清空已生成的部分回答:正文保留,只改状态
- 数据库故障时 error 帧发不出去、客户端拿到 200 空响应:补一条不依赖落库的兜底帧
文档处理
- 失败时用已取消的 job ctx 写状态且丢弃错误 → 用脱离取消的 ctx,并补记日志
- 终态
failed与 River 重试自相矛盾(重试 25 次全撞在"状态冲突")→ 只有最后一次 attempt 落终态;终态重投返回 nil - 整篇文档塞进一次 embedding 请求 → 按 256 分批并校验每批数量
- 临时文件清理在句柄未关闭时执行(Windows 上是空操作)→ 先关闭再删除;新增 tmp 目录清扫
- 不校验编码 → 上传挡扩展名、处理前挡非 UTF-8
- 上传无大小上限 → 见「行为变更」
- 文档处理继承 River 1 分钟默认超时 → 见「行为变更」
BYOK 与密钥
- 重复引导会执行
ALTER ... USING NULL清空全部已存向量 → 见「行为变更」 - 集成测试自身的清理也清空向量、且断言发现不了 → 改为断言向量存活
tokenizer_type保存时不校验、聊天时才拒绝 → 保存时校验并 trim 落库- 主密钥首次生成非原子(api 与 worker 同时启动各装一把不同的密钥,败者加密的数据永久无法解密)→ 临时文件 + fsync +
os.Link抢占 + 回读校验
Agent
- 客户端断开后 run 永久停在
running→ 失败收尾用脱离请求的 ctx;补上interrupted终态 - 无缓冲通道导致 goroutine 与 Eino iterator 每次中断泄漏一个 → 发送可取消
- 工具错误(如除零)打死整轮 run → 参数级错误降级为 tool result 交回模型
- 省略
toolNames时 nil 绑定进NOT NULL列返回 500 → 归一化为空切片 - 一轮里多个并行工具调用写出多条
llm步骤 → 同一轮只记一次 - 失败一律报
upstream_llm_error→ 保留错误自带的 sentinel
检索与平台
- HNSW 后置过滤导致
Search返回少于topK(甚至零条)→ 在查询内设置 pgvector 扫描参数 - 周期维护任务继承 60s 默认超时、被截断的一轮仍记为成功 → 显式超时并如实上报
isLoopbackHost无条件按末位冒号截断,无端口 IPv6 回环被判 403 → 改用net.SplitHostPort
前端
- 保存 provider 成功后仍被弹回空白引导页(过期的空缓存 + 无效的失效调用)→ 只在"结论还是空"时等待
- 发消息时用户那条从不进消息缓存,失败时从界面静默消失 → 发送那一刻乐观插入,失败也重拉
- Agent 运行失败后历史列表不刷新 → 缓存失效从
done分支挪进finally - 补上前端测试(vitest + jsdom + Testing Library),并让 CI 真的跑它们——此前前端代码在 CI 上没有任何断言门禁
文档与契约
- README /
docs/sse-protocol.md声称支持Idempotency-Key重放,实际没有代码读这个头 → 文档改为描述真实行为 openapi.yaml缺uploadDocument/deleteDocument/createConversation会返回的 404 → 补齐Capabilities.ToolCalling注释声明门控 Agent 可用性,全仓没有一处读它 → 注释改为说清现状make build-web删除被跟踪的apps/api/web/.gitkeep(提交后会让新 clone 编译失败)→ 构建后补回占位文件,直接跑pnpm --filter web build的路径也覆盖- Makefile 的
PATH用分号拼接,只在 cmd.exe 下成立 → 按平台选分隔符
行为变更
| 变更 | 说明 |
|---|---|
| 换 embedding 模型会被拒绝(409) | 之前重复引导会静默清空全部向量,文档仍显示 ready、检索静默返回零条。现在:列类型已经是目标值就跳过 ALTER;已存向量不属于当前模型时返回 409。代价:换模型需要手工清库——v1.0 没有重嵌入路径,应用内也没有清空向量的入口 |
| 上传有 32 MiB 上限 | CONGORAG_MAX_UPLOAD_BYTES 可覆盖;写 0 或负数视为没设、退回默认值 |
| 文档处理任务超时 30 分钟 | 之前吃 River 的 1 分钟默认值,一份合法的大文档必然超时。与上传上限相互作用,两条要一起看 |
| 失败语义 | 非最后一次 attempt 不落终态;终态重投返回 nil;客户端断开记 interrupted 而不是 failed |
SSE 的 error 帧可以不带 id |
唯一那一帧由"事件没能落库"触发,不要拿它推进续传游标(已写进协议文档) |
明确没有实现
以下三条本来就是「文档与代码不一致」,按各自的修复方向让两边一致,没有顺手实现功能:
Idempotency-Key重放(SSE 重放语义不简单,属于新功能)Capabilities.ToolCalling门控(表单默认不勾选,真加门控会让默认配置的用户失去 Agent)- 重嵌入 / reindex 路径
验证方式
CI 四个 job 全绿(run #2):Go build / vet / test -race、契约生成物漂移检查、前端 lint + test + 构建 + go:embed、以及在真实 pgvector/pgvector:0.8.6-pg18 上跑的数据库集成测试。
本地另跑:go build ./...、go vet ./...、go test ./...、pnpm --filter web lint、pnpm --filter web test(4 文件 6 用例)、gofmt -l。
每条回归测试都验证过"修复前会失败"——把代码临时退回旧行为跑一遍确认变红,再还原。
已知限制
#22的回归测试只在 Windows 上会失败(POSIX 允许删除仍打开的文件),所以那条"先关闭再删除"的顺序在 Linux CI 上没有门禁,将来被改回去不会被发现。测试注释里写明了。- 集成测试在本地永远是
SKIP(没设CONGORAG_TEST_DB_URL),只有 CI 真正执行它们。涉及 SQL 的改动只能在 CI 上得到验证。 - 本次提交与 tag 都未签名——本机配置了
GIT_SIGN_COMMITS但没有可用的 GPG 私钥。
ConGoRAG v1.0
首个发布。 169 个文件、30722 行插入。
这一节依 tag 消息、提交
362cab0与 README 重建——v1.0 当时只有 tag,
没有对应的 GitHub Release 对象。
Added
- 知识库管理:列表 / 详情 / 新建 / 改名 / 删除
- 文档上传:multipart → 落盘(写临时文件 → fsync → rename)→ 入队 →
解析 / 切分 / 向量化,处理状态可查 - 向量检索:pgvector HNSW,引导时按 embedding 维度建索引,按知识库限定,
返回相似度与文件名 - 流式问答:SSE 逐 token 下发,citation 随流推送,按
after_event_id
断线续传,协议见docs/sse-protocol.md - 上下文管理:按 token 预算组装上下文,超预算的历史交给模型压缩
(真实 tokenizer + 摘要 + 长期记忆) - Agent:Eino ADK,工具注册表(计算器 / 知识库检索 / 会话检索),
运行轨迹逐步落库 - BYOK:provider / model 配置存库,API Key 用 AES-GCM 加密后落盘,
引导页采集 - worker:River 消费端(文档处理 + 孤儿文件对账)
- 单体交付:前端产物
go:embed进 Go 二进制 - 契约
contracts/openapi.yaml与两端的生成物 - 两套迁移系统(业务表用 golang-migrate、River 队列表用它的 CLI)
evals/measure_recall.sh:测 HNSW 近似索引相对精确扫描的召回差异
完整变更:v1.0...HEAD