用 llama.cpp 做 RAG,最烦的不是写代码,而是明明跑起来了,回答却总在胡说。或者嵌入模型加载不动、检索召回的段落全不相关、上下文塞爆了还答非所问。这篇按实际动手顺序走一遍:哪一步容易错、错了看什么、怎么改。
先把这条链路拆成两半,别混在一起调
这条链路一混着调,人就会崩溃。检索出错和生成出错表现完全不同:有时是捞回来的段落压根不带答案,有时是答案就在眼前但模型没读明白。你在终端里盯着一句错回答,压根分不清是向量库那边漏了还是提示词那侧串了。所以第一件事是把它物理拆开,先当作两个独立的小工具各自跑通。
| 链路半边 | 负责什么 | 输入输出 | 怎么单独验证 |
|---|---|---|---|
| 检索段 | 把问题变成向量、从库里捞出相关段落 | 问题文本进,若干段原文出 | 固定几个问题跑检索,把捞回的文本原样打印出来看 |
| 生成段 | 把段落和问题拼成提示词,交给 llama.cpp 出答案 | 段落加问题进,一段回答出 | 手写提示词,喂一段肯定的材料,看模型会不会照着说 |
检索段的验证说白了就是人眼看。准备几个你已知答案的问题,跑一遍,把命中的段落直接 print 到终端。如果打印出来的东西跟答案八竿子打不着,那问题就卡在切块、嵌入或检索参数上,跟模型半毛钱关系没有。这一步别接生成,接了只会让你怀疑模型笨,实际是库没捞对。
- 先固定一组问题挑三到五个答案唯一、材料里确实有的问题,写死在脚本里。别用开放式问题,否则你没法定量看召回。
- 只跑到检索为止脚本里在拿到段落之后直接退出,把每段的文本和来源打印出来,先不调 llama.cpp。
- 把段落贴回材料对照逐段看有没有正确答案那一句。没有就是切块太碎或检索参数不对,有就说明这半边过关。
- 再单独验生成手工拼一段包含答案的提示词,直接喂给 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 | 对外就是标准接口,谁都能接 |
- 确认模型文件能被加载起服务或 import 之后第一件事,看日志有没有把模型读进去。报错多半是路径写错、文件不完整或者量化格式对不上,先解决这个再谈别的。
- 发一条最短的 prompt就用「你好,简单介绍一下你自己」这种,别一上来塞长文档。能正常出字,说明加载、推理链路都通了;出乱码或者直接空回,问题在这一层,不在检索。
- 记录这次能跑通的启动参数上下文长度、线程数、端口这些,抄进一个启动脚本里存着。后面调 RAG 出问题,至少有一个已知能用的基线可以对照。
- 再接检索和拼 prompt前两步都稳了,再把向量库和文档拼进来。这时候出错,范围就缩小到检索逻辑本身,排查快得多。
用 llama-cpp-python 的时候有个常见误判:装完包就直接 import,结果提示找不到编译好的后端。这通常是装的时候没带对应的加速选项,或者在没编译环境的地方硬装。遇到这种情况别急着改代码,先看安装日志里后端那几行有没有真正编译进去。这事儿麻烦在报错信息不直观,容易让人以为是模型的问题。
还有一种情况值得提前想清楚:如果你打算后面做多轮对话,上下文要频繁截断重拼,进程内调往往更好控;但如果只是给前端一个问答框,起服务、换地址、完事。别为了「显得可控」去自己拉库,多写的胶水代码后面全是维护成本。
嵌入模型和生成模型不是一回事,别拿同一个文件凑合
第一次搭本地检索问答,十有八九会在这里翻车:把一个 gguf 生成模型丢进 llama.cpp 跑通了聊天,顺手就以为同一套东西能拿去算向量。跑不通,报错还特别含糊——要么维度对不上,要么出来一堆乱七八糟的结果。问题不在代码,在概念没分清。
- 生成模型
- 读你给的问题加检索到的段落,一个字一个字往外写答案,输出是文本。
- 嵌入模型
- 把一段文本压成一串数字(向量),输出不是给人看的,是给向量库比对相似度用的。
- 维度
- 嵌入模型吐出的向量长度,向量库建索引时就按这个长度定死,换模型必须重建。
说白了,这俩是两种工种。生成模型是写手,嵌入模型是索引员。你拿写手去干索引员的活,它也能吐数字,但那些数字之间的远近关系没有意义,检索出来的段落跟问题八竿子打不着。反过来拿嵌入模型去聊天更离谱,它压根不会成句。所以别问「哪个模型能一个顶俩」,没有。
| 你手上的文件 | 该放哪条链路 | 判断依据 |
|---|---|---|
| 对话/补全类 gguf | 生成环节,接在检索结果后面 | 能顺着上下文续写、能听懂指令 |
| 嵌入类 gguf | 建索引和查询时的向量化环节 | 输出固定长度的向量,不产出自然语言 |
| 不确定用途 | 先去站内模型资料库查它的任务类型 | 看模型卡里写的是不是 embedding |
- 先确认嵌入维度把嵌入模型单独跑一次,看它输出的向量长度是多少,拿这个数字去建向量库的索引。别先用默认配置建完再回头改。
- 换模型就重建索引嵌入模型一换,向量空间整个变了,旧索引里的向量和新模型算出来的没法比。正确做法是清空索引、用新模型把全部文档重新跑一遍,只改个配置文件名是没用的。
- 用同义问法验收挑几组意思一样但用词不同的问法,比如「怎么退订」和「取消订阅的步骤」,看召回的是不是同一批段落。召不回就说明这个嵌入模型不适合你的语料,跟它聊不聊得动没关系。
最后这条验收标准最好使。很多人测嵌入模型的方式是丢一句话进去看它「回答」得怎么样,这完全测错了对象。嵌入模型不回答,它只排序。你该盯的是召回结果稳不稳:换个说法问同一件事,命中的段落重合度高,才算选对了。要是你的文档全是专业术语、缩写满天飞,通用嵌入模型召回会明显变差,这种情况下不适用「随便挑个流行的就行」,得换更贴合领域的模型,代价是整个索引重跑一遍。
下一步嵌入模型和向量库的维度对应关系,站内资料库里按任务类型筛一下就能看到每个模型的输出维度,建索引前先核一遍,比事后重建省事得多。→切块切得不好,后面全白搭
切块这一步,很多人第一次做 RAG 时随手按固定字符数一劈就完事,跑起来才发现问答质量飘得厉害。先说块太大:你以为给了模型更多上下文更保险,其实一半是废话。检索回来的段落里塞着上一节的收尾、下一节的开头、还有和问题八竿子打不着的旁支,模型很容易被这些无关内容带跑,回答开始绕圈子。
块太小的问题反过来。一句话被从中间切断,主语留在上一块、结论落在下一块,拿出来单独看根本不成句。模型补不出缺失的那半截,只能硬猜,答案看着像那么回事,细看缺关键条件。中文文档尤其容易踩这个坑,按字符数硬切完全不认标点和段落边界。
| 切法 | 典型毛病 | 怎么判断要不要改 |
|---|---|---|
| 按固定字符数硬切 | 句子被拦腰截断,上下文丢失 | 抽几块读一下,有没有读起来半截的话 |
| 按段落切 | 长段落仍可能偏大,短段落碎 | 看单块是否超过你给模型的上下文预算太多 |
| 整篇当一块 | 召回即全篇,噪声占大头 | 提问后看召回内容里有效信息占比 |
| 代码表格混进正文 | 结构被拆散,模型读不懂行列关系 | 单独检查含代码或表格的块是否完整 |
我的建议是中文文档优先按段落切,段落本身就是作者划好的语义单元。段落太长再往下按句号、分号拆,别按字符数硬砍。代码块和表格单独拎出来处理,别跟正文混在一刀里,表格拆散了行列对应关系就没了,模型拿到一堆孤立单元格只能瞎猜。
- 每个块带上来源标题和小节名,拼接提示词时模型能判断这段属于哪个主题
- 块之间留一点重叠,避免边界处的句子两头不靠
- 切完随手抽十来块通读,看有没有半截话或明显跑题的块
- 含代码或表格的块单独标记,检索时可以按需过滤
- 块大小不是一刀切,正文段落和列表可以用不同的切法
召回不相关,先查向量再查提示词
召回不准,很多人第一反应是去改提示词,加一句「请只根据以下内容回答」。这招有时管用,但它治的是模型愿不愿意听话,治不了压根没捞到相关段落这件事。顺序别搞反:先看检索回来的东西对不对,再看提示词有没有把模型带偏。
还有个更前置的问题:用户那句话,到底是在问关键词,还是在问意思。像查一个报错码、一个函数名、一个配置项,这类查询本质是字符串匹配,向量检索天生吃亏——它把整句话压成一个点,细节被抹平了。反过来,问「这个流程大概怎么走」这种,语义检索才占便宜。上手前先拿几条真实问题自测一遍,看哪类问法翻车,比盲调参数有用。
| 问法类型 | 典型样子 | 检索上更该靠什么 |
|---|---|---|
| 关键词型 | 报错码、函数名、配置项、专有名词 | 字面匹配优先,向量只做补充 |
| 语义型 | 流程怎么走、这东西干嘛用、有什么区别 | 向量检索为主 |
| 混合型 | 某个模块报错该怎么排查 | 两路都上,但先各自跑通 |
召回条数也不是越多越好。多塞几条无关段落,模型面对一堆材料,反而更容易挑中那条看起来像、其实不对的。先从小条数开始,一条条看回来的是什么,确认里面真有能回答问题的段落,再考虑放宽。放宽之后如果准确率掉了,那是噪声变多,不是模型变笨。
- 单路先跑通只用向量检索或只用关键词匹配,各拿同一批问题跑一遍,把召回的段落原样打印出来看。哪一路连相关段落都捞不到,就先修那一路。
- 核对召回内容别看模型最终答案,只看检索结果里有没有能支撑答案的原文。没有就是检索的锅,提示词先别动。
- 调条数看变化固定其他条件,只改召回条数,从小往大试。记录每次多出来的段落是相关还是噪声,找到准确率开始下滑的那个点。
- 再谈混合两路都确认能单独捞到东西之后,再试把关键词匹配和向量结果混在一起。混合是锦上添花,不是用来盖住基础问题的。
拼提示词那一步,最容易被忽略的三个格子
检索做完、模型也加载好了,很多人就在这里翻车:明明资料就在手边,模型还是张口胡说。问题基本不在模型,在拼提示词时几个格子没填对。这一段就讲三个最常被跳过的位置,改起来都不费劲。
第一个格子是那句约束。你得直白写清楚:只依据下面给出的材料回答,材料里找不到就说不知道,不要自己补。这句话不写,模型会默认「问题得有个答案」,然后拿训练时记住的东西顶上。看着像答上了,其实跟你的文档没关系。
- 约束句要放在问题前面,别混在问题末尾,模型对开头指令的服从度通常更高
- 把「不知道」也当成合法答案写进去,否则模型会把编造当成兜底
- 别写「尽量准确」这种软话,要写「材料里没有就说没有」这种硬条件
- 如果业务上不能答「不知道」,就让它回一句固定话术,方便你在代码里识别
第二个格子是顺序和分隔。材料放前面,问题放后面,中间用明显的标记隔开,比如每段材料前加编号、段与段之间空行加横线。不隔开的话,模型分不清哪段是资料、哪句是你真正想问的,尤其当材料里本身就带问句的时候,它会顺着材料里的问句往下答。
| 容易漏的格子 | 不写会怎样 | 补法 |
|---|---|---|
| 只用材料回答的约束 | 模型拿记忆补答案,看着通顺但不可信 | 在提示词开头写死约束和「不知道」出口 |
| 材料与问题的先后和分隔 | 分不清资料和提问,答偏或复述材料 | 材料在前、问题在后,段间加编号和分隔线 |
| 上下文长度预算 | 超限被截断,砍掉的常是材料尾部或问题 | 拼之前先算 token,超了就减检索条数 |
第三个格子是长度。llama.cpp 的上下文窗口是有上限的,检索回来的几段材料加问题一旦超过,就会被截。麻烦在于截断往往发生在末尾,而末尾可能正好是你最想让它看的那段,或者干脆把问题截掉一半。表现就是回答缺东少西,你还以为是检索没召回。
- 先量再拼把每条检索结果和问题分别估一下 token,加总后跟当前加载模型的上下文上限比,留出一截给回答
- 超了就减条数优先砍相关度最低的材料,而不是把每段都截短,半截材料比没有更误导
- 拼完回看一遍把最终提示词打印出来,确认约束句在、分隔清晰、问题完整没被切
- 跑一个空材料测试故意给一段不相关的材料问个无关问题,看模型会不会说不知道,验证约束生效
出问题时按这个顺序查,别跳步
出了问题别一上来就翻参数。先按这个顺序走:先证明检索有没有把对的材料捞出来,再看提示词拼装有没有丢东西,最后才轮到生成参数和模型。顺序反了,你会在采样温度上折腾半天,问题其实在向量库那一步就没把答案召回来。
第一步永远是把检索结果原样打出来看。不是看相似度分数,是看那段被召回的正文里到底有没有答案。很多时候分数挺高,内容是隔壁段落,问的是「退款多久到账」,捞回来的是「退款申请条件」。这种情况下你后面怎么调都没用,得回去改切分、改查询、加关键词过滤。
- 打印召回的每一条原文,不要只打印 id 和分数
- 人工读一遍,答案是不是真的在里面;不在就直接改检索层
- 在的话,记下是第几条命中的,靠后说明排序可以再收紧
- 同一句话只被切了一半,说明切分粒度有问题
- 召回到了但明显是旧文档,说明索引没重建或者增量没生效
| 现象 | 先怀疑哪里 | 怎么验证 |
|---|---|---|
| 回答完全跑题 | 检索没召回 | 把召回原文打出来,看有没有答案 |
| 回答对一半、缺关键条件 | 提示词被截断 | 打印拼好的完整 prompt,数一下材料还剩几段 |
| 材料齐全但措辞离谱 | 生成参数 | 固定检索结果,只改一条参数再跑 |
| 输出中途断掉、首字很慢 | 接口层 | 脱离 RAG 单独压测流式接口 |
第二步,把最终喂给模型的完整提示词打印出来。注意是拼装之后、发出去之前那一版,不是模板。常见坑是上下文窗口被聊天历史挤掉,检索材料悄悄截断在中间,模型只看到前半段。这种事在日志里看不出来,必须把整段文本落盘看一眼。确认材料完整,再谈生成。
第三步才是调参数。这里有个前提:前两步都验证过没问题。如果你没做前两步就动参数,等于闭着眼睛改。另外说清楚什么情况下这套顺序不适用——如果你的问题本身就是开放式的、知识库里根本没有标准答案,那检索再好也答不准,这时候该调整的是问题范围,不是排查顺序。
最后单独说一类:流式输出断流、首字慢、偶尔超时。这些和 RAG 逻辑没关系,别在提示词里找原因。用一个不接检索的裸接口压一遍,看是不是网络、并发或者超时设置的问题,按接口层单独处理。
常见问题
llama.cpp 做 RAG 必须用它的嵌入接口吗?
不一定。llama.cpp 能跑嵌入模型,也可以另起一个专门的嵌入服务。关键是维度和向量库对得上,换嵌入模型就要重建索引。用哪个跑不重要,能稳定返回同一套向量就行。
为什么本地检索召回的内容看着相关,回答还是错的?
多半是提示词没约束住。材料在旁边,问题在后面,没写明只用这些材料回答,模型会掺自己的先验知识。先把约束句写死,再试一遍。
上下文塞不下所有召回内容怎么办?
要么减少召回条数,要么先做一轮粗排再精排,把最相关的几条留下。硬塞超长内容会被截断,截掉哪段你控制不了,反而更糟。
本地跑 RAG 和直接调在线接口差别大吗?
主要差在成本和延迟可控性上,模型能力得看你本地跑的是哪个量级。具体模型规格和部署测算可以去站内模型库和部署测算页对着看,别凭印象选。
换了个嵌入模型,索引要重建吗?
要。不同嵌入模型产出的向量不在同一个空间,旧索引和新查询根本对不上。重建前先想清楚,别每次小改动都全量重跑。