本地跑 llama.cpp 的人常卡在这一步:模型能起来,RAG 也能召回,但一到真实请求就开始首字慢、并发一高就排队、上下文塞爆、出错还查不出来。这篇不重复讲 RAG 原理,只给工程落地的判断标准和动作清单:怎么切分检索与生成两段超时、并发怎么限、失败怎么降级、日志记什么、上线前用哪几个指标自测。
本地用 llama.cpp 把模型跑起来只是第一步。真正让人头疼的是接上 RAG 之后:检索和生成搅在一起,首字慢、并发一高就排队、上下文说爆就爆、出了错还翻不出原因。这篇不重复讲 RAG 是什么,只按落地的顺序,把拆链路、对齐参数、拼提示词、卡超时并发、设降级、记日志、做自测这几件事一次讲透。
核心思路就一句:把检索和生成当成两个独立服务来对待,各自设超时、各自降级、各自打日志。llama.cpp 只管生成这一段,检索通常交给向量库或关键词索引。两段失败原因完全不同,混在一起排查只会绕远路。下面按环节给判断标准和动作清单,能直接照着改配置。
先把链路拆成两段:检索一段、生成一段
很多人的第一版是把「召回片段→拼提示词→调 llama.cpp→返回」写成一个函数,串行跑完。这样出问题时,你看到的是一个整体超时,根本不知道是检索卡了还是模型在慢。建议从第一天就按两段来组织代码:检索模块返回片段列表,生成模块接收片段列表返回文本,中间那层拼装逻辑单独拎出来。
- 检索段给短超时,比如设成几百毫秒量级,超了直接返回空结果或部分结果,不要拖着等
- 生成段要分两个指标:首字延迟和总时长。首字延迟决定用户感知的「快不快」,总时长决定这个请求最终会不会成功
- 两段各自可独立重试。检索重试成本低可以多试一次,生成重试成本高,通常不值得重跑
- 别用一个笼统的「请求总超时」兜底,那等于放弃了分段定位能力
llama.cpp 服务端的几个参数先对齐
拆完链路,第二件事是把 llama.cpp 服务端的几个关键参数摸清楚,因为它们直接决定你的并发上限和单次能塞多少检索片段。参数别抄别人的,要按你自己的机器和模型量出来。
- 上下文窗口
- 单次请求能容纳的总 token 数上限,检索片段、系统提示、用户问题、模型输出都算在里面
- 并行槽位
- 服务端同时能处理几个请求,槽位开多了显存吃紧、单请求变慢,宁少勿多
- 批处理大小
- 一次前向计算塞多少 token,影响吞吐也影响显存占用
- 模型加载策略
- 是常驻内存还是每次请求重载,后者会让首字延迟飙高,务必固定成常驻
上下文窗口的用法是反着算的:先量出你检索片段的平均长度,再看还剩多少空间留给用户问题和输出,最后倒推出一次召回几条。别默认拉满,拉满的结果往往是模型注意力被稀释、答得更差。并行槽位和显存的关系,得按你的具体显卡和模型量化档位算一遍,到 /models/deploy 核对当时的配置建议和数字。
还有一个容易被忽略的点:模型加载和卸载策略要写死。如果你用的是按请求动态加载,首字延迟会高得离谱,而且高并发下反复加载卸载很容易把进程搞崩。固定成启动时加载、常驻进程,是本地部署的基本功。
检索结果怎么拼进提示词才不撑爆上下文
检索回来的片段要拼成提示词,这一步的坑最多。最典型的是「按条数截断」:不管片段多长,反正取前十条。结果十条里七条是低分噪声,既挤占上下文又干扰模型。正确做法是按相关度截断。
| 拼接策略 | 做法 | 适用情况 |
|---|---|---|
| 按条数截断 | 固定取前 N 条 | 片段长度高度一致、召回质量稳定的场景 |
| 按相关度截断 | 设一个分数阈值,低于阈值不要 | 大多数场景的默认选择 |
| 按字符预算截断 | 从高分往低分加,加到预算上限为止 | 片段长短差异大的场景 |
| 多轮检索 | 先召回一批,模型判断不够再召一轮 | 多跳问答、跨片段推理任务 |
- 给每个片段编号并保留来源字段,方便模型引用,也方便你事后核对是哪条召回错了
- 片段前加一句简短说明,比如「以下是与问题相关的资料」,让模型的注意力有个落点
- 超长文档入库前先做清洗和分块,格式乱、表格多、目录多的文档尤其要过一遍进料流程,具体做法参考 /guides/pdf-ocr-doc-cleaning
- 用户问题里如果带了明显的关键词或编号,可以在检索前先做一轮关键词过滤,减少无效召回
什么时候按相关度截断不适用?多跳问答和需要跨片段推理的任务。这类问题里,单独看每一条片段分数都不高,但合起来才有答案。这时候硬截断会丢信息,得改成多轮检索:先召回一批,让模型判断够不够,不够再带着已知信息召第二轮。
超时与并发:本地单机的容量天花板怎么定
本地部署最大的误区是照抄别人文章里的超时秒数。你的机器、模型、量化档位、上下文长度都跟别人不一样,超时必须基于实测。先用站内的测速工具跑一遍,拿到你这套配置的延迟基线,具体数字到 /tools/speed-test 看当时的结果,别硬记。
- 量基线用固定长度的问题和固定长度的上下文各跑若干次,记录首字延迟和总耗时的分布,取一个偏保守的上限作为超时值
- 定并发上限并发上限设成并行槽位的整数倍最稳妥。超出这个数的请求不要直接丢,先放进有界队列
- 设排队阈值队列积压超过某个长度就快速失败并返回「稍后重试」,让请求堆在内存里把进程拖死是最糟的结局
- 写四段日志排队时长、检索耗时、首字耗时、生成总耗时,四段分开记,慢在哪一段一眼看得出
这里的原则是「排队比崩掉好,快速失败比静默超时好」。用户拿到一个明确的排队状态或重试提示,体验远好于等半天拿到一个报错。并发上限不是越大越好,开太大反而让每个请求都变慢。
降级路径:模型没起来、显存不够、上下文超限怎么办
降级不是可有可无的优化,是本地部署的必需项。本地环境比云端更容易出意外:显存被别的进程占了、模型文件损坏、上下文临时超限,这些都会让生成段失败。失败之后返回什么,决定了用户是拿到半份资料还是拿到一个空白报错。
- 生成段失败时,先返回检索到的原文片段加一句说明,用户至少拿到资料,而不是一个红色报错
- 上下文超限时,按比例丢弃评分最低的片段重试一次,而不是直接把请求打回
- 如果本地实在跑不动,可以把生成段切到远端接口,检索段留在本地,路由规则写在配置文件里而不是硬编码进代码,参考 /guides/local-cloud-api-routing
- 降级要留痕,每次走了降级都在日志里标记,方便统计降级率
远端降级这条要特别注意适用边界:如果数据完全不能出内网,这条路径必须直接关掉,只保留「返回原文片段」这种不依赖外部服务的兜底。路由规则写在配置里,就是为了让你能按环境快速开关,而不是改代码重新部署。
日志该记哪几个字段
日志是你唯一的排查手段。记少了,用户说「答错了」,你无从下手;记多了,全是明文提示词,反而有数据泄露风险。下面这几类字段是建议的最小集。
| 字段 | 用途 | 注意 |
|---|---|---|
| 请求 ID | 串联一次请求的检索和生成两段日志 | 全链路透传 |
| 召回条数与最高分 | 判断是检索没召回到还是召回到但没用上 | 只记数字即可 |
| 拼进提示词的字符数 | 监控上下文是否逼近上限 | 超限时预警 |
| 四段耗时 | 定位慢在哪一段 | 排队/检索/首字/总时长 |
| 是否降级 | 统计降级率 | 连续走高说明该调参数 |
- 召回片段的 ID 一起落盘,用户反馈答错时能直接定位是检索环节还是生成环节
- 触发降级的请求单独打标,方便按天统计降级率和排队率
- 别把完整提示词明文长期存盘,涉及内部资料时做脱敏或只存摘要和哈希
- 日志按天切割并设保留期,本地磁盘空间通常比你想的紧张
两个关键指标:降级率和排队率。这两个数字连续几天走高,基本可以确认是容量到顶了,该调参数或换机器,而不是继续撑着硬扛。
上线前拿什么自测:一份最小评测清单
改分块大小、改召回条数、改提示词模板,这些动作每次都会影响最终答案。如果全靠感觉判断「好像变好了」,你会陷入反复调参出不来的状态。解决办法是准备一小组固定问题,每次改动后重跑同一组。
- 准备一小组固定问题,覆盖三类:资料里明确有的、资料里没有的、资料里有但表述绕的
- 每次改动后重跑同一组问题,对比答案是否变好,别凭感觉改
- 记录三种失败:该召回没召回、召回了但没用上、模型自己编了
- 三种失败分别对应检索、拼装、提示词三个环节,定位方向完全不同
- 把每次改动的参数和对应结果记下来,形成你自己的调参记录
什么时候不用急着做评测?纯探索阶段,先把流程跑通更重要。但只要你开始反复调参,就该把这份清单固定下来,否则你调的不是参数,是玄学。
本地 RAG 的问题很少出在模型上,大多出在链路没拆开、参数没量过、失败没兜底。把这三件事做扎实,比换个更大的模型有用得多。下一步先在站内测速工具上量一遍你这套配置的延迟基线,再按本文的清单检查一遍你的超时、并发和降级配置。站内还有中转站可信度检测和价格追踪页面,如果你打算把生成段部分或全部切到远端,那两页的数字值得先核对一遍。→
常见问题
llama.cpp 跑 RAG,检索和生成放同一个进程里行不行?
小规模自用可以,进程内直接调向量库省事。但请求量一上来,检索的阻塞会连带拖慢生成,而且一个查询失败另一个也受影响。建议至少把检索做成独立模块或独立服务,两段分别设超时和日志,排错时能少绕很多弯路。
本地并发一高就变慢,是模型不行还是参数没调对?
多数情况是并行槽位和上下文分配的问题,不是模型能力。先确认槽位开几个、每个请求实际占多少上下文,再按实测的首字延迟设超时。用接口测速量出基线以后,把并发上限压到槽位附近,让超出部分排队,比硬扛要稳。
上下文塞不下所有召回片段,该怎么取舍?
按相关度截断,保留分数最高的几条,并在拼装时给每条编号和来源。片段长度不一时,先统一分块再比分数,否则长片段会挤掉高分短片段。如果任务本身需要跨片段推理,别硬塞,改成两轮检索更划算。
检索结果没问题,但模型还是答错,怎么判断是哪一环的锅?
把召回片段和最终答案对着看:片段里有答案而模型没说出来,是拼装或提示词的问题;片段里根本没有,是检索的问题。日志里同时记召回 ID 和提示词长度,这种对照几分钟就能做完,不用反复猜。
本地设备跑 RAG 到底要多少显存?
取决于模型规模、上下文长度和并行槽位三个变量,没有统一答案。把这三项填进部署测算页面按你的卡算一遍更靠谱。先按能跑起来的保守配置上线,再拿实测的排队率和首字延迟决定要不要加卡。