很多人本地用 llama.cpp 起了模型,把检索结果一股脑塞进 prompt,结果要么答非所问,要么上下文塞爆、要么响应慢到没法用。这篇不重复 RAG 原理,只讲 llama.cpp 这一环特有的坑:上下文窗口怎么算、检索片段怎么裁、两段服务怎么接、出了问题按什么顺序查。看完能自己把链路搭起来并定位毛病。
很多人本地用 llama.cpp 把模型跑起来了,接上检索之后反而更乱:要么答非所问,要么上下文塞爆直接截断,要么第一句话半天出不来。问题基本不在模型本身,而在检索和生成这两段怎么交接、上下文怎么算账、服务端参数怎么配这几件事上。这篇不重复讲 RAG 是什么,只讲 llama.cpp 这一环特有的坑和对应做法。
读完之后你应该能做到三件事:把检索段和生成段拆干净、按预算把片段拼进 prompt、出问题时按固定顺序定位是检索错了还是生成错了。下面的每一步都给了可验证的动作,照着做一遍比看十篇原理文管用。
先把链路拆成两段:检索和生成各自独立
最常见的第一刀是把检索代码和 llama.cpp 调用写在一个函数里,切片策略一改,生成端也得跟着动,调起来互相干扰。正确姿势是中间隔一层固定的数据格式:检索段只负责吐出「片段文本 + 来源标识 + 可选的相似度分数」的列表,生成段只认这个列表,不关心它是向量搜出来的还是关键词搜出来的。
- 检索段
- 文档切片、向量化、召回,跑在 Python 或任意脚本侧,输出结构化片段列表
- 交接格式
- 每条片段带正文和来源,最好再带一个序号,方便模型引用和排查
- 生成段
- llama.cpp 的 server 或命令行,只接收拼好的 prompt,返回文本
两段之间用 HTTP 或本地文件对接都行,一人用的场景甚至可以先落成 json 文件再喂给生成脚本,调试时肉眼可见。这样换 embedding 模型、换切片粒度,都只动检索那半边,生成端一个字不改。什么情况下不适用:如果你只是临时验证一个想法,两段合在一起写确实更快,但那套代码别往生产里搬。
上下文窗口是硬约束:先算账再拼片段
上下文窗口是死的,拼进去的东西必须提前算账。把窗口拆成三块预算:系统提示和指令占一块,检索片段占一块,剩下的是你自己追问和模型输出的余量。片段块超预算时,先降条数、再降单条长度,千万别想着硬塞——llama.cpp 遇到超长输入不只是变慢,还可能被静默截断,最关键的那段资料就这么丢了,而你在日志里什么异常都看不到。
- 在代码里打印每次实际拼进 prompt 的字符数或 token 数,和窗口上限对照着看,这是最便宜的一道保险
- 片段按相似度降序排,超预算时从尾部砍,别随机砍
- 系统提示尽量短,把「资料里没有就说没有」这类约束写死在前几句,别拉长成一大段
- 如果你的问答一两段资料就能答,主动用短窗口配置,比硬开大窗口快也更稳
检索片段怎么拼进 prompt 才不出错
片段拼装是本地 RAG 最容易被忽略的一步。两个相邻文档的句子直接头尾相接,模型会把它们当成一句话读,意思就串了。每条片段都要有明确的分隔标记,并带上来源,让模型知道哪段话出自哪份文档,回答时才能标出处;没有来源的片段宁可丢掉,留着只会污染答案。
| 拼装问题 | 典型表现 | 改法 |
|---|---|---|
| 片段之间无边界 | 模型把两段话缝成一句,答出不存在的事实 | 每条片段加分隔符和编号,来源单独一行 |
| 指令混在资料里 | 模型把资料里的句子当命令执行 | 指令统一放系统提示最前面,资料区只放资料 |
| 片段无来源 | 回答看着对但没法核对 | 每条带来源,没来源的直接不拼 |
| 约束写在片段后面 | 模型忽略「没有就说没有」 | 约束前置到系统提示开头几句 |
判断拼装对不对,有个笨办法很有效:把最终 prompt 原文打印出来自己读一遍。如果你自己读完都分不清哪些是指令哪些是资料,模型大概率也分不清。这一步花两分钟,能省掉后面几小时的瞎调。
llama.cpp 服务端几个必须调的参数
llama.cpp 起服务时,上下文长度、并行槽数、批处理大小这三样是在抢同一块显存或内存的。很多人直接照抄别人的启动参数,结果自己机器上不是爆内存就是并发一上来就断连。调之前先按模型文件大小和机器余量估一遍,然后一次只改一个参数,改完压测一轮。
- 先定单请求上下文按前面算的预算定一个够用的窗口,不要一上来开最大;用一段长资料发一次请求,看是否正常返回
- 再定并行槽数本地一人用就设小,几个人同时用再往上加;用两个终端同时发请求验证会不会互相拖垮
- 最后调批处理批处理影响吞吐和内存峰值,调大之后观察内存曲线,涨到接近上限就退回来
- 确认接口格式想用 OpenAI 兼容格式接上层应用,先确认你起的服务端开了对应接口,拿一条最小请求验证通了再谈上层怎么配
还有一点容易被忘:对外提供 HTTP 服务时要盯住超时和连接数。本地一人用几乎不会碰到,几个人同时提问就会暴露,表现往往是请求超时或连接被重置,而不是一条清楚的报错。具体该设多少没有通用答案,和机器配置强相关,到 /tools/speed-test 用你的机器实测一轮再定。
答得不对时按这个顺序查,别一上来就换模型
回答不准的时候,绝大多数人的第一反应是换更大的模型,这通常是白费功夫。正确的排查顺序是从上游往下游走,先确认资料有没有被捞到,再看有没有被正确送进模型,最后才怀疑模型本身。
| 步骤 | 查什么 | 判断标准 |
|---|---|---|
| 第一步:只查检索 | 把召回的片段原文打出来 | 该有的资料在列表里,不在就是切片或向量模型的问题 |
| 第二步:查拼装 | 打印最终 prompt,核对长度和分隔 | 资料在但模型没用上,多半是截断、分隔符或指令位置的问题 |
| 第三步:查生成 | 换措辞、调温度、换参数 | 前两步都正常还答错,才考虑换模型或换推理参数 |
这个顺序的价值在于每一步都有明确的判断标准,不靠感觉。第一步就不通过的话,后面两步怎么调都没用,因为你根本没把对的东西喂给模型。另外注意,如果问题是偶发的、只在某些问题上出现,优先看那几类问题的切片方式,而不是整体换模型。
慢和爆内存:本地 RAG 最常挨的两刀
首包慢是本地 RAG 最典型的抱怨。原因通常就两个:上下文太长,或者模型太大。先砍片段条数,很多时候砍掉两三段无关资料,首字时间立刻下来;再检查是不是每次请求都在重新加载模型,这一条在脚本调用场景里特别常见。
- 内存或显存吃紧时,先降并行槽数和批处理,再考虑换量化档位
- 把「检索耗时」「拼装耗时」「首字耗时」「总耗时」分别打日志,别凭感觉猜瓶颈
- 多人同时提问要排队或限并发,否则并发一上来直接崩,表现成连接断开而不是报错
- 长文档问答先做一次预筛选,不要把所有切片都送进向量检索
上线前跑一遍这份清单
本地搭的东西最容易在改来改去中慢慢退化:今天调了切片,昨天调了提示词,过两周发现效果不如从前,却说不清是哪一步改坏的。解决办法是固定一组常见问题做回归测试,每次改动都跑一遍,对比召回结果和回答质量有没有退化。
- 固定一组覆盖各类问题的回归集,改动后必跑,人工看召回和回答
- 日志里至少留三样:检索到的片段、实际送进模型的长度、首字耗时
- 资料库定期更新并重建索引,同时把过期文档下线,不然模型会拿旧资料回答新问题
- 每次改配置只改一个变量,改完记录效果,别一次动三处
- 把启动参数抄进版本管理,别靠命令行历史找配置
这套清单不保证效果一定好,但能保证出问题时你知道从哪查起。什么情况下可以简化:如果只是个人临时查资料用,回归集可以省,但日志三件套建议留着,排查时省下的时间远超过写日志的成本。
下一步把 llama.cpp 接上自己的应用之前,先到站内工具目录挑一个速度测试和参数对照的入口,用你的机器实测一轮,再回来定窗口和并发。→常见问题
llama.cpp 和 RAG 是两个东西还是一套?
是两个环节。llama.cpp 负责把本地模型跑起来做生成,RAG 负责找资料。你要做的是把检索结果按固定格式喂给 llama.cpp 起的服务,两者通过 prompt 交接,不需要把检索逻辑塞进推理框架里。
检索到的资料挺对的,模型就是不用,怎么办?
先看是不是拼进去时被截断,把实际送进模型的文本长度打出来核对;再看指令和资料是不是混在一起。通常把「只依据以下资料回答」放在最前面、给每段资料加明确边界,能解决大部分情况。
本地机器跑不动长上下文怎么办?
降片段条数、提高切片的信息密度,比硬开大窗口更实际。也可以把检索做在前面筛得更狠,只把最相关的几条送进模型,用检索质量换上下文预算。
要不要上向量数据库?
文档量小的时候,本地文件存向量、启动时加载进内存完全够用;到了几十万条以上再考虑专门的库。先跑通链路再谈换组件,别一开始就堆技术栈。
怎么判断是检索的锅还是模型的锅?
把召回的片段单独打出来人眼看一遍。该有的资料没出现就是检索问题;资料在但回答跑偏,就是拼装或生成问题。这一步不做,后面所有调整都是瞎猜。