本节摘要:实战项目收官:先跑通,再拧旋钮。演示:造一个三文件的小仓库(auth/storage/tasks),跑
pipeline.py建索引,四类查询(标识符/意图/混排/换说法)给出示意输出与读法。排查:效果不达标时的归因顺序表——金标对不对、切分好不好、召回有没有、排序对不对,逐层往下问,反对上来就调参。调优:四张参数扫描表(chunk 行数上限、每路召回宽度、两路权重配比、重排深度),每张表给示意数字与走势解读——数字全部标注"示意/经验值",真实结论必须用第 9 章的指标在你自己的评测集上复测。顺序主张:先切分(它是上限)、再召回宽度(便宜)、再权重(要测)、最后重排深度(最贵)——顺序本身就是工程判断。
阅读完本节,你应当能够:
# demo_repo/auth/session.py — 演示仓库三件之一(另两件见下) SESSION_TTL = 3600 def kill_session(user_id: str) -> None: """注销:删除 session 记录并吊销 refresh token。""" _delete(f"session:{user_id}") revoke_refresh_token(user_id) # 来自 storage/token_store.py def refresh_session(user_id: str) -> str: ...
# demo_repo/storage/token_store.py — 演示仓库三件之二 def revoke_refresh_token(user_id: str) -> None: """从可信存储吊销 refresh token(黑名单策略)。""" _redis.sadd("revoked_tokens", _token_of(user_id))
# demo_repo/tasks/upload_task.py — 演示仓库三件之三 MAX_RETRY = 3 def upload_with_retry(path: str) -> bool: """上传文件,失败按指数退避重试。""" for attempt in range(MAX_RETRY): if _put_object(path): return True _sleep(2 ** attempt) return False
# demo_run.sh — 端到端演示(示意输出见下) python pipeline.py demo_repo --out index python ask.py "kill_session" --index index python ask.py "注销用户的时候都删了什么" --index index --rerank 30 python ask.py "上传失败重试的退避怎么算的" --index index python ask.py "登出以后 token 怎么处理" --index index
[0.0328] demo_repo/auth/session.py:3-8 kill_session (function) ← 标识符型,第一位 [0.0163] demo_repo/tasks/upload_task.py:3-10 upload_with_retry (function) ← 融合分垫底 ---(--rerank 30 后)--- [0.9120] demo_repo/auth/session.py:3-8 kill_session ← 意图型:重排把词法噪声压下去 [0.7435] demo_repo/storage/token_store.py:1-4 revoke_refresh_token [0.6721] demo_repo/tasks/upload_task.py:3-10 upload_with_retry ← 换说法型:"退避"召回退避
(分数为示意:RRF 分量级 ~1/(60+rank),重排分为相关度。)读法:标识符型靠 BM25 保底必中;意图型与换说法型体现语义路价值——"注销/登出"与 kill_session、"退避"与 2 ** attempt 的对齐;重排把"看起来都对"变成"最像答案的在前"。
| 症状 | 第一问 | 归因方向 | 对应章节 |
|---|---|---|---|
| 正确 chunk 压根不在索引里 | 切出来了吗? | 切分:巨类下切、孤儿兜底、行号对不对 | 3.3 症状学 |
| 在索引里但没被召回 | 两路各自召回了没? | 单路失灵看另一路权重;双路都漏看嵌入/分词 | 5.1 / 2.2 |
| 召回了但排不进 top-K | RRF 后排第几? | 权重配比;重排深度不够或重排器拉胯 | 5.1 / 5.2 |
| 排第一但"不像答案" | 金标本身对吗? | 查询与金标的匹配口径;chunk 粒度太粗 | 9.2 / 3.1 |
| 时快时慢、时对时错 | 索引新鲜吗? | state 对账:model_ver、增量是否漏跑 | 7.2 / 4.2 |
💡 这张表就是第 9.3 节"坏例归因流程"的肉眼版。归因先于调参的铁律:对着症状调参数,十个旋钮九个白拧。
以下数字均为示意(基于教学规模演示仓库与经验值,写法演示用):走势可信、数值不可信——你仓库上的真实曲线要用第 9 章的 MRR/Recall@K 复测。
表 1:chunk 行数上限 max_lines(重建索引后测)
| max_lines | 40 | 60 | 80(默认) | 120 |
|---|---|---|---|---|
| Recall@5(示意) | 0.78 | 0.83 | 0.85 | 0.82 |
| 症状 | 碎片化,类被切散 | — | 均衡 | 巨块稀释,重排变慢 |
解读:太小则身份信息(类名/前缀)被稀释、同函数碎片互相竞争;太大则"一个 chunk 说多件事",嵌入被平均(3.3 症状二)。60~80 是常见甜点(经验值)。
表 2:每路召回宽度 recall
| recall | 10 | 20 | 30(默认) | 50 | 100 |
|---|---|---|---|---|---|
| Recall@5(示意) | 0.74 | 0.82 | 0.85 | 0.86 | 0.86 |
| 延迟(相对) | 0.8x | 0.9x | 1x | 1.3x | 2.1x |
解读:召回宽度解决"漏",对"排"无帮助——宽到正确答案已进候选集后再加宽是纯开销。30~50 后收益断崖(经验值)。
表 3:两路权重 bm25_w : vec_w
| 配比 | 0.5:1 | 0.7:1 | 1:1(默认) | 1.5:1 | 2:1 |
|---|---|---|---|---|---|
| 意图型 MRR(示意) | 0.71 | 0.74 | 0.76 | 0.72 | 0.68 |
| 标识符型 MRR(示意) | 0.80 | 0.86 | 0.90 | 0.93 | 0.91 |
解读:权重搬的是两路的话语权——查询分布偏意图(Agent 口语提问多)就升语义路,偏标识符(工具调用多)就升词法路。没有"通用最优",只有"贴合你的查询分布"(这就是 9.2 要给评测集分类型打标的原因)。
表 4:重排深度 --rerank
| depth | 0 | 10 | 20 | 30 | 50 |
|---|---|---|---|---|---|
| MRR(示意) | 0.62 | 0.74 | 0.78 | 0.79 | 0.79 |
| 端到端延迟(相对) | 1x | 1.4x | 1.8x | 2.3x | 3.5x |
解读:重排是"从几十到五"的精修——depth 超过融合结果的精度上限后纯属烧钱。20~30 常见收敛(经验值);depth=0 那一列就是消融基线。
①切分(max_lines) 上限工程:切分错,后面全错——先在 3.3 症状学里过关 ▼ ②召回宽度(recall) 便宜:纯参数,不重建索引,宽了再看要不要收 ▼ ③混合权重(两路配比) 要测:必须配合第 9 章的分组指标,否则是玄学 ▼ ④重排深度(depth) 最贵:延迟线性涨,最后再买这段精度
主张的依据是改动成本与影响半径:切分牵动全库重建(最贵、影响最大)所以最先定;权重只影响排序(要靠指标才动得对);重排深度是纯买精度(最后买)。真实顺序里 ①③ 会有回环——切分变了,权重的甜点也会移,第 9.3 节的"一次一变量 + 全量评测"循环就是管这个回环的纪律。
⚠️ 本章扫描表最大的误用是把示意数字当结论。仓库语言、命名风格、查询分布、嵌入模型任一不同,曲线形状就会变。正确用法:抄扫描的维度与顺序,数字自己测——这正是下一章之后、第 9 章的正题。
一台完整可跑、可调优的 codeqa 交付了。下一章抬头看业界:自己造过一遍管线,再看 Sourcegraph、Greptile、Copilot 时,看到的将不再是功能列表,而是"他们把成本押注在了哪一段"。