llama cpp rag:本地跑检索问答,第一次上手最容易卡在哪

接入与开发

从模型加载、嵌入服务到检索拼装,拆解 llama.cpp 做 RAG 时真正会卡住的几处,给出可照着做的排查顺序。

用 llama.cpp 做 RAG,最烦的不是写代码,而是明明跑起来了,回答却总在胡说。或者嵌入模型加载不动、检索召回的段落全不相关、上下文塞爆了还答非所问。这篇按实际动手顺序走一遍:哪一步容易错、错了看什么、怎么改。

先把这条链路拆成两半,别混在一起调

这条链路一混着调,人就会崩溃。检索出错和生成出错表现完全不同:有时是捞回来的段落压根不带答案,有时是答案就在眼前但模型没读明白。你在终端里盯着一句错回答,压根分不清是向量库那边漏了还是提示词那侧串了。所以第一件事是把它物理拆开,先当作两个独立的小工具各自跑通。

链路半边负责什么输入输出怎么单独验证
检索段把问题变成向量、从库里捞出相关段落问题文本进,若干段原文出固定几个问题跑检索,把捞回的文本原样打印出来看
生成段把段落和问题拼成提示词,交给 llama.cpp 出答案段落加问题进,一段回答出手写提示词,喂一段肯定的材料,看模型会不会照着说

检索段的验证说白了就是人眼看。准备几个你已知答案的问题,跑一遍,把命中的段落直接 print 到终端。如果打印出来的东西跟答案八竿子打不着,那问题就卡在切块、嵌入或检索参数上,跟模型半毛钱关系没有。这一步别接生成,接了只会让你怀疑模型笨,实际是库没捞对。

  1. 先固定一组问题挑三到五个答案唯一、材料里确实有的问题,写死在脚本里。别用开放式问题,否则你没法定量看召回。
  2. 只跑到检索为止脚本里在拿到段落之后直接退出,把每段的文本和来源打印出来,先不调 llama.cpp。
  3. 把段落贴回材料对照逐段看有没有正确答案那一句。没有就是切块太碎或检索参数不对,有就说明这半边过关。
  4. 再单独验生成手工拼一段包含答案的提示词,直接喂给 llama.cpp,看它能否照着材料回答。这步不过,调检索也没用。

这套拆法也有不适用的时候:如果你的问题是跨多段综合、答案散在好几块材料里,那检索段很难靠单段命中来判定,这时得改成看多段合起来是否覆盖要点。另外切块策略和嵌入模型在站内有专门页面,具体选型别在这篇里纠结,去 RAG 切块与检索指南(/guides/rag-chunking) 看当时的做法更省时间。

模型怎么起:llama-server 还是自己拉库

先把这个顺序摆正:RAG 那套东西(切块、向量库、拼 prompt)都是后面的事。第一步只需要证明「模型能起来、能吐字」。很多人一上来就写检索脚本,结果跑不通,回头一查是模型文件根本没加载成功,白折腾半天。

起服务有两条路。想让客户端几乎零改代码,就起 llama-server;想把采样参数、上下文长度、并发这些都捏在自己进程里,才用 llama-cpp-python。选哪个不看哪个更潮,看你后面要不要在代码里频繁调这些旋钮。

你的情况建议原因
已经有 OpenAI SDK 或现成客户端,只想换地址llama-server它给的就是兼容接口,客户端改个 Base URL 就能连
要在 Python 里逐 token 控制、动态切上下文llama-cpp-python进程内直接调,参数不用绕一层 HTTP
做批量离线跑、要自己管队列llama-cpp-python批量逻辑写在脚本里更顺,省得跟服务端较劲
团队里有人不写 Python,只想接个接口llama-server对外就是标准接口,谁都能接
  1. 确认模型文件能被加载起服务或 import 之后第一件事,看日志有没有把模型读进去。报错多半是路径写错、文件不完整或者量化格式对不上,先解决这个再谈别的。
  2. 发一条最短的 prompt就用「你好,简单介绍一下你自己」这种,别一上来塞长文档。能正常出字,说明加载、推理链路都通了;出乱码或者直接空回,问题在这一层,不在检索。
  3. 记录这次能跑通的启动参数上下文长度、线程数、端口这些,抄进一个启动脚本里存着。后面调 RAG 出问题,至少有一个已知能用的基线可以对照。
  4. 再接检索和拼 prompt前两步都稳了,再把向量库和文档拼进来。这时候出错,范围就缩小到检索逻辑本身,排查快得多。

用 llama-cpp-python 的时候有个常见误判:装完包就直接 import,结果提示找不到编译好的后端。这通常是装的时候没带对应的加速选项,或者在没编译环境的地方硬装。遇到这种情况别急着改代码,先看安装日志里后端那几行有没有真正编译进去。这事儿麻烦在报错信息不直观,容易让人以为是模型的问题。

还有一种情况值得提前想清楚:如果你打算后面做多轮对话,上下文要频繁截断重拼,进程内调往往更好控;但如果只是给前端一个问答框,起服务、换地址、完事。别为了「显得可控」去自己拉库,多写的胶水代码后面全是维护成本。

嵌入模型和生成模型不是一回事,别拿同一个文件凑合

第一次搭本地检索问答,十有八九会在这里翻车:把一个 gguf 生成模型丢进 llama.cpp 跑通了聊天,顺手就以为同一套东西能拿去算向量。跑不通,报错还特别含糊——要么维度对不上,要么出来一堆乱七八糟的结果。问题不在代码,在概念没分清。

生成模型
读你给的问题加检索到的段落,一个字一个字往外写答案,输出是文本。
嵌入模型
把一段文本压成一串数字(向量),输出不是给人看的,是给向量库比对相似度用的。
维度
嵌入模型吐出的向量长度,向量库建索引时就按这个长度定死,换模型必须重建。

说白了,这俩是两种工种。生成模型是写手,嵌入模型是索引员。你拿写手去干索引员的活,它也能吐数字,但那些数字之间的远近关系没有意义,检索出来的段落跟问题八竿子打不着。反过来拿嵌入模型去聊天更离谱,它压根不会成句。所以别问「哪个模型能一个顶俩」,没有。

你手上的文件该放哪条链路判断依据
对话/补全类 gguf生成环节,接在检索结果后面能顺着上下文续写、能听懂指令
嵌入类 gguf建索引和查询时的向量化环节输出固定长度的向量,不产出自然语言
不确定用途先去站内模型资料库查它的任务类型看模型卡里写的是不是 embedding
  1. 先确认嵌入维度把嵌入模型单独跑一次,看它输出的向量长度是多少,拿这个数字去建向量库的索引。别先用默认配置建完再回头改。
  2. 换模型就重建索引嵌入模型一换,向量空间整个变了,旧索引里的向量和新模型算出来的没法比。正确做法是清空索引、用新模型把全部文档重新跑一遍,只改个配置文件名是没用的。
  3. 用同义问法验收挑几组意思一样但用词不同的问法,比如「怎么退订」和「取消订阅的步骤」,看召回的是不是同一批段落。召不回就说明这个嵌入模型不适合你的语料,跟它聊不聊得动没关系。

最后这条验收标准最好使。很多人测嵌入模型的方式是丢一句话进去看它「回答」得怎么样,这完全测错了对象。嵌入模型不回答,它只排序。你该盯的是召回结果稳不稳:换个说法问同一件事,命中的段落重合度高,才算选对了。要是你的文档全是专业术语、缩写满天飞,通用嵌入模型召回会明显变差,这种情况下不适用「随便挑个流行的就行」,得换更贴合领域的模型,代价是整个索引重跑一遍。

下一步嵌入模型和向量库的维度对应关系,站内资料库里按任务类型筛一下就能看到每个模型的输出维度,建索引前先核一遍,比事后重建省事得多。

切块切得不好,后面全白搭

切块这一步,很多人第一次做 RAG 时随手按固定字符数一劈就完事,跑起来才发现问答质量飘得厉害。先说块太大:你以为给了模型更多上下文更保险,其实一半是废话。检索回来的段落里塞着上一节的收尾、下一节的开头、还有和问题八竿子打不着的旁支,模型很容易被这些无关内容带跑,回答开始绕圈子。

块太小的问题反过来。一句话被从中间切断,主语留在上一块、结论落在下一块,拿出来单独看根本不成句。模型补不出缺失的那半截,只能硬猜,答案看着像那么回事,细看缺关键条件。中文文档尤其容易踩这个坑,按字符数硬切完全不认标点和段落边界。

切法典型毛病怎么判断要不要改
按固定字符数硬切句子被拦腰截断,上下文丢失抽几块读一下,有没有读起来半截的话
按段落切长段落仍可能偏大,短段落碎看单块是否超过你给模型的上下文预算太多
整篇当一块召回即全篇,噪声占大头提问后看召回内容里有效信息占比
代码表格混进正文结构被拆散,模型读不懂行列关系单独检查含代码或表格的块是否完整

我的建议是中文文档优先按段落切,段落本身就是作者划好的语义单元。段落太长再往下按句号、分号拆,别按字符数硬砍。代码块和表格单独拎出来处理,别跟正文混在一刀里,表格拆散了行列对应关系就没了,模型拿到一堆孤立单元格只能瞎猜。

  • 每个块带上来源标题和小节名,拼接提示词时模型能判断这段属于哪个主题
  • 块之间留一点重叠,避免边界处的句子两头不靠
  • 切完随手抽十来块通读,看有没有半截话或明显跑题的块
  • 含代码或表格的块单独标记,检索时可以按需过滤
  • 块大小不是一刀切,正文段落和列表可以用不同的切法

召回不相关,先查向量再查提示词

召回不准,很多人第一反应是去改提示词,加一句「请只根据以下内容回答」。这招有时管用,但它治的是模型愿不愿意听话,治不了压根没捞到相关段落这件事。顺序别搞反:先看检索回来的东西对不对,再看提示词有没有把模型带偏。

还有个更前置的问题:用户那句话,到底是在问关键词,还是在问意思。像查一个报错码、一个函数名、一个配置项,这类查询本质是字符串匹配,向量检索天生吃亏——它把整句话压成一个点,细节被抹平了。反过来,问「这个流程大概怎么走」这种,语义检索才占便宜。上手前先拿几条真实问题自测一遍,看哪类问法翻车,比盲调参数有用。

问法类型典型样子检索上更该靠什么
关键词型报错码、函数名、配置项、专有名词字面匹配优先,向量只做补充
语义型流程怎么走、这东西干嘛用、有什么区别向量检索为主
混合型某个模块报错该怎么排查两路都上,但先各自跑通

召回条数也不是越多越好。多塞几条无关段落,模型面对一堆材料,反而更容易挑中那条看起来像、其实不对的。先从小条数开始,一条条看回来的是什么,确认里面真有能回答问题的段落,再考虑放宽。放宽之后如果准确率掉了,那是噪声变多,不是模型变笨。

  1. 单路先跑通只用向量检索或只用关键词匹配,各拿同一批问题跑一遍,把召回的段落原样打印出来看。哪一路连相关段落都捞不到,就先修那一路。
  2. 核对召回内容别看模型最终答案,只看检索结果里有没有能支撑答案的原文。没有就是检索的锅,提示词先别动。
  3. 调条数看变化固定其他条件,只改召回条数,从小往大试。记录每次多出来的段落是相关还是噪声,找到准确率开始下滑的那个点。
  4. 再谈混合两路都确认能单独捞到东西之后,再试把关键词匹配和向量结果混在一起。混合是锦上添花,不是用来盖住基础问题的。

拼提示词那一步,最容易被忽略的三个格子

检索做完、模型也加载好了,很多人就在这里翻车:明明资料就在手边,模型还是张口胡说。问题基本不在模型,在拼提示词时几个格子没填对。这一段就讲三个最常被跳过的位置,改起来都不费劲。

第一个格子是那句约束。你得直白写清楚:只依据下面给出的材料回答,材料里找不到就说不知道,不要自己补。这句话不写,模型会默认「问题得有个答案」,然后拿训练时记住的东西顶上。看着像答上了,其实跟你的文档没关系。

  • 约束句要放在问题前面,别混在问题末尾,模型对开头指令的服从度通常更高
  • 把「不知道」也当成合法答案写进去,否则模型会把编造当成兜底
  • 别写「尽量准确」这种软话,要写「材料里没有就说没有」这种硬条件
  • 如果业务上不能答「不知道」,就让它回一句固定话术,方便你在代码里识别

第二个格子是顺序和分隔。材料放前面,问题放后面,中间用明显的标记隔开,比如每段材料前加编号、段与段之间空行加横线。不隔开的话,模型分不清哪段是资料、哪句是你真正想问的,尤其当材料里本身就带问句的时候,它会顺着材料里的问句往下答。

容易漏的格子不写会怎样补法
只用材料回答的约束模型拿记忆补答案,看着通顺但不可信在提示词开头写死约束和「不知道」出口
材料与问题的先后和分隔分不清资料和提问,答偏或复述材料材料在前、问题在后,段间加编号和分隔线
上下文长度预算超限被截断,砍掉的常是材料尾部或问题拼之前先算 token,超了就减检索条数

第三个格子是长度。llama.cpp 的上下文窗口是有上限的,检索回来的几段材料加问题一旦超过,就会被截。麻烦在于截断往往发生在末尾,而末尾可能正好是你最想让它看的那段,或者干脆把问题截掉一半。表现就是回答缺东少西,你还以为是检索没召回。

  1. 先量再拼把每条检索结果和问题分别估一下 token,加总后跟当前加载模型的上下文上限比,留出一截给回答
  2. 超了就减条数优先砍相关度最低的材料,而不是把每段都截短,半截材料比没有更误导
  3. 拼完回看一遍把最终提示词打印出来,确认约束句在、分隔清晰、问题完整没被切
  4. 跑一个空材料测试故意给一段不相关的材料问个无关问题,看模型会不会说不知道,验证约束生效
下一步想确认本地这套检索问答的每一步该配什么参数,去站内指南中心对照着看当时的说明,比在群里问快。

出问题时按这个顺序查,别跳步

出了问题别一上来就翻参数。先按这个顺序走:先证明检索有没有把对的材料捞出来,再看提示词拼装有没有丢东西,最后才轮到生成参数和模型。顺序反了,你会在采样温度上折腾半天,问题其实在向量库那一步就没把答案召回来。

第一步永远是把检索结果原样打出来看。不是看相似度分数,是看那段被召回的正文里到底有没有答案。很多时候分数挺高,内容是隔壁段落,问的是「退款多久到账」,捞回来的是「退款申请条件」。这种情况下你后面怎么调都没用,得回去改切分、改查询、加关键词过滤。

  • 打印召回的每一条原文,不要只打印 id 和分数
  • 人工读一遍,答案是不是真的在里面;不在就直接改检索层
  • 在的话,记下是第几条命中的,靠后说明排序可以再收紧
  • 同一句话只被切了一半,说明切分粒度有问题
  • 召回到了但明显是旧文档,说明索引没重建或者增量没生效
现象先怀疑哪里怎么验证
回答完全跑题检索没召回把召回原文打出来,看有没有答案
回答对一半、缺关键条件提示词被截断打印拼好的完整 prompt,数一下材料还剩几段
材料齐全但措辞离谱生成参数固定检索结果,只改一条参数再跑
输出中途断掉、首字很慢接口层脱离 RAG 单独压测流式接口

第二步,把最终喂给模型的完整提示词打印出来。注意是拼装之后、发出去之前那一版,不是模板。常见坑是上下文窗口被聊天历史挤掉,检索材料悄悄截断在中间,模型只看到前半段。这种事在日志里看不出来,必须把整段文本落盘看一眼。确认材料完整,再谈生成。

第三步才是调参数。这里有个前提:前两步都验证过没问题。如果你没做前两步就动参数,等于闭着眼睛改。另外说清楚什么情况下这套顺序不适用——如果你的问题本身就是开放式的、知识库里根本没有标准答案,那检索再好也答不准,这时候该调整的是问题范围,不是排查顺序。

最后单独说一类:流式输出断流、首字慢、偶尔超时。这些和 RAG 逻辑没关系,别在提示词里找原因。用一个不接检索的裸接口压一遍,看是不是网络、并发或者超时设置的问题,按接口层单独处理。

常见问题

llama.cpp 做 RAG 必须用它的嵌入接口吗?

不一定。llama.cpp 能跑嵌入模型,也可以另起一个专门的嵌入服务。关键是维度和向量库对得上,换嵌入模型就要重建索引。用哪个跑不重要,能稳定返回同一套向量就行。

为什么本地检索召回的内容看着相关,回答还是错的?

多半是提示词没约束住。材料在旁边,问题在后面,没写明只用这些材料回答,模型会掺自己的先验知识。先把约束句写死,再试一遍。

上下文塞不下所有召回内容怎么办?

要么减少召回条数,要么先做一轮粗排再精排,把最相关的几条留下。硬塞超长内容会被截断,截掉哪段你控制不了,反而更糟。

本地跑 RAG 和直接调在线接口差别大吗?

主要差在成本和延迟可控性上,模型能力得看你本地跑的是哪个量级。具体模型规格和部署测算可以去站内模型库和部署测算页对着看,别凭印象选。

换了个嵌入模型,索引要重建吗?

要。不同嵌入模型产出的向量不在同一个空间,旧索引和新查询根本对不上。重建前先想清楚,别每次小改动都全量重跑。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。