本地跑 RAG 最容易卡在「模型能跑、问答不能用」:拉模型顺利,接上 LangChain 之后要么检索结果不对,要么生成慢得像卡死,要么报错看不出是哪一段出的问题。这篇不从概念讲起,只按工程顺序过一遍:链路怎么拆成检索和生成两段、每段怎么单独验、超时和并发怎么设、日志该记哪些字段、效果怎么小成本评测。
本地跑 RAG 最常见的失败不是模型拉不下来,而是「模型能跑、问答不能用」。拉模型、跑个 hello world 都挺顺,一接上 LangChain,要么检索出来的片段跟问题不搭边,要么生成慢得像卡死,要么报一堆错但看不出是哪一段出的问题。
这篇不讲概念,按工程顺序过一遍:链路怎么拆成检索和生成两段、每段怎么单独验、超时并发怎么设、日志该记哪些字段、效果怎么用最小成本评测。凡是涉及具体延迟或费用,一律到站内页面看当时的数字,别照抄别人的环境。
先把链路拆成两段:检索一段,生成一段
拆分的标准很简单:检索段不调生成模型也能独立跑完,打印出命中的片段和相似度;生成段塞死一段文本也能出答案。如果做不到,说明两段还缠在一起,后面任何问题都定位不了。
- 检索段
- 吃一个问题,输出若干候选文档片段和相似度,全程只碰嵌入模型和向量库
- 生成段
- 吃问题和候选片段,输出答案,只碰生成模型,不关心片段从哪来
- 为什么不塞改写和重排
- 第一版加进去,出问题时你分不清是检索差还是改写把问题改歪了
- 生成和嵌入尽量别用同一个模型,嵌入模型单独选,可以对照站内 /embeddings 的清单再定
- LangChain 那一侧只留两个职责清晰的函数,中间不要塞改写、压缩、重排,第一版先不加
- 两段各自写一个可单独执行的入口,方便你只跑检索或只跑生成
- 先跑通「问题→片段→答案」这一条最短路径,再考虑加别的环节
建库和查询前,先量一条基线
本地模型的首字延迟和云端差很多,拿云端经验估本地,十有八九会误判成「卡死了」。先用固定的一小批问题,量出本机的模型加载时间、单次生成耗时、单次嵌入耗时,把这三个数记下来当基线。
| 要量的项 | 怎么量 | 量出来干嘛 |
|---|---|---|
| 模型加载耗时 | 第一次请求单独计时 | 判断要不要让模型常驻 |
| 单次生成耗时 | 同一个问题跑两遍 | 看波动大不大,波动大是资源在打架 |
| 单次嵌入耗时 | 批量嵌入一小段文本 | 估算建库总时间,决定要不要预建 |
| 端到端耗时 | 检索加生成一起计 | 作为后续任何改动的对照点 |
同一批问题连续跑两遍,如果耗时波动很大,先别怀疑链路,多半是显存或并发在打架。基线不稳的话,后面调参数全是白调。
超时、并发与降级:把默认值全部改掉
默认超时基本是按云端接口设的,放到本地会出两种极端:要么生成还没开始就被掐断,要么检索卡住整个请求挂在那里。嵌入和生成要分别设超时,生成段的超时通常要宽得多,因为本地模型吐字慢。
- 先设成串行跑通并发数设成一,确认单条请求稳定返回,再考虑加路数
- 一路一路往上加每次加一路,跑到开始排队或内存报错的位置退回来,那里就是这台机器的上限
- 给生成设独立超时超时后走降级逻辑,而不是让调用方一直等
- 把降级写清楚生成超时就退回「只给检索片段」,检索为空就明确告诉调用方没查到,别让模型硬编答案
判断标准:如果加并发后检索耗时开始跟着涨,说明资源被生成任务抢了,这时候要么降并发,要么把嵌入和生成错峰跑。这套判断在单机部署下基本都成立,但如果你把向量库放到另一台机器,检索和生成的资源就不再互相抢,并发可以分开设。
日志记哪几个字段,出问题按什么顺序查
出问题时最怕的就是「报错了但不知道哪一段」。每次请求至少落下这几个字段,基本能一眼判断是检索挂了还是生成挂了。
- 问题原文,保留原样不要截断
- 命中片段数,为零就是检索没结果
- 每段的相似度,看是不是普遍偏低
- 检索耗时和生成耗时,分开记
- 实际使用的模型名,换过模型能对得上
- 请求是否走了降级逻辑,方便统计
| 现象 | 优先怀疑 | 先查什么 |
|---|---|---|
| 答案跟问题无关 | 检索段 | 打印命中的片段和相似度,看片段本身对不对 |
| 检索为空 | 切分或向量库 | 确认向量是否入库、维度是否一致 |
| 生成极慢 | 资源或并发 | 看是不是并发开太高,或模型没常驻 |
| 报维度错误 | 入库环节 | 检查嵌入模型换了之后有没有重建库 |
排查顺序固定成一条线:先确认 Ollama 服务本身响应正常,再确认嵌入是否返回了向量,再看检索候选是否为空,最后才怀疑生成模型。跳着查容易在生成那一层白折腾半天。
给自己留一条能反复跑的小评测
没法量化的改动等于没改。准备一批带标准片段的问题,脚本只比对「命中的文档有没有包含标准片段」,不评答案文采,先看检索准不准。这一步成本很低,但能把「改好了还是改坏了」从感觉变成事实。
- 问题要覆盖你真实场景里的问法,别只挑好答的
- 故意放几条查不到的问题,看链路会不会老实说没查到
- 改动切分方式、换嵌入模型、调 top-k 之后重跑同一批
- 记录每次改动的命中率变化,作为下一版的对照
- 评测脚本和检索代码分开,避免互相影响
这条评测循环在个人项目里够用,但如果你的文档经常变、问题分布也在变,固定的一批用例会慢慢失真,那时候需要定期补新问题。这一点不适用于文档几乎不变的一次性场景,那种情况跑一次就够。
下一步把基线量清楚再动手调参数,站内工具页能直接测本机延迟,顺便核对嵌入模型的选型清单,省得反复推倒重来。→常见问题
本地用 Ollama 跑 RAG,是不是模型越大效果越好?
不一定。生成模型大一点通常回答更顺,但检索效果主要由切分方式和嵌入模型决定,跟生成模型大小关系不大。本机显存有限时,优先把嵌入模型选对、把切分调合理,比换更大的生成模型有效得多。什么时候不适用:如果你的问题本身就是需要长推理的复杂问答,那生成模型的能力才变成瓶颈。
检索出来的片段看着不相关,先查哪里?
按这个顺序:先看切分出来的片段是不是太碎或太长,再看嵌入模型和文档语言是否一致,再看 top-k 是不是设得太小只召回了几条。这三步能覆盖大部分情况。什么时候不适用:如果文档本身是扫描件或表格,问题出在进料阶段,得先把文本清洗干净再谈检索。
本地跑时不时卡住没反应,怎么定位?
先分开计时:单跑一次嵌入看耗时,单跑一次生成看耗时,哪个异常就是哪一段。本地常见原因是显存不够触发换页,或者并发开多了在排队。先把并发降到 1 跑通,再逐步加上去看从第几路开始变慢。什么时候不适用:如果嵌入和生成分开跑都正常、合起来才卡,那是资源争抢,得考虑错峰或分开部署。