llama cpp rag,常见坑和正确做法一次讲清和判断标准

接入与开发

讲清 llama.cpp 接 RAG 时最常踩的坑:切块、向量化、上下文塞爆、模板不匹配、速度幻觉,并给出判断标准和可落地的排查顺序。

本地跑 llama.cpp 的人做 RAG,多半会在几个地方卡住:切块太粗召回不到、向量模型和生成模型不是一套服务、检索结果一股脑塞进上下文把窗口挤爆、模型没套对对话模板导致答非所问、CPU 上慢得以为程序卡死。这篇不讲大道理,按「检索段」和「生成段」拆开,把每个坑的成因、判断标准和不适用的情况说清楚,最后给一份从能跑到跑好的排查顺序。

本地用 llama.cpp 跑 RAG,很多人卡住不是因为模型不行,而是链路里某一段悄悄坏了:召回结果本身就看不懂,却一直在调 prompt;上下文塞爆被静默截断,以为是模型笨;对话模板没配套,模型开始自问自答。这些问题的共同点是都不会报错,只表现为「回答怪怪的」。

下面按检索段和生成段拆开讲,每节给一个能当场验证的判断标准,最后给一份从能跑到跑好的排查顺序。这里不写死任何数字,涉及具体参数和开销,请到站内对应页面核对当时的值。

先把链路拆成两段:检索和生成不是一个进程的事

检索段做切块、向量化、召回,生成段只负责拼 prompt 和调 llama.cpp 推理。这两段可以用不同语言、不同进程甚至不同机器,中间用一条 JSON 或 HTTP 接口隔着。拆开的好处是出问题能定位:到底是没捞到正确材料,还是捞到了但模型不会用。

最常见的坏习惯是让 llama.cpp 的主生成模型顺手做 embedding。主模型的向量不是为相似度检索训练的,召回质量会明显打折,而且它一边做向量一边生成,两边互相排队抢资源。正确做法是单独起一个 embedding 服务,哪怕跑在同一台机器上也算两套东西。

链路环节出问题时的表现先看哪里
切块召回片段缺前因后果打印块内容肉眼读
向量化相似度排序离谱固定问题查前几条
召回条数回答缺后半段查上下文窗口配置
对话模板自问自答、复述指令看模板与模型是否配套

切块:切太粗召回不到,切太碎模型拼不出意思

按固定字数硬切是最省事也最容易出事的做法。它会把一张表格切成两半、把一段代码从中间截断、把结论和前提分开。检索命中的时候看着像中了,实际给模型的是半句话,模型只能猜。

更稳的做法是顺着文档结构切:按标题层级先分大类,再按段落或问答对切小段;每块头部带上来源文件名和所在小标题。这样回答被要求给出处时,有东西可引用,事后排查也知道是哪份材料出的问题。

  • 块与块之间留少量重叠,避免一句话被切断后两边都读不通
  • 表格和代码块尽量整块保留,宁可超一点长度也不要在中间切
  • 每块附加来源元信息:文件名、标题路径、块序号
  • 短问答型资料就按一问一答切,别硬套多层标题

不适用的情况要说清:如果知识库本身就是一条条独立短问答,强行做层级切块反而把完整意思拆散,检索指标未必更好。判断标准是拿几条有标准答案的问题去跑,看命中的块拼起来能不能直接回答问题。

向量化服务单独起,别和推理抢同一个端口

llama.cpp 的 server 模式跑 embedding 和跑生成是两套参数,混在同一个实例里,并发请求容易互相拖到超时。建议 embedding 单独起一个进程,端口分开,日志分开,这样检索段慢的时候一眼能看出来。

另一个硬约束是向量维度和索引库必须对齐。换 embedding 模型等于重建整个索引,这件事要提前想好怎么重建、重建期间服务怎么降级,别等上线了才发现旧索引全废。

  1. 固定一句测试话从知识库里挑一句原文,拿它去检索,看前几条里有没有这句原文本身。
  2. 看相似度分布如果前几条相似度都挤在一起分不开,多半是向量质量问题或切块太粗。
  3. 换模型先建小索引换 embedding 模型时先在几十条样本上重建,验证召回没问题再全量重建。
  4. 留重建预案记录当前索引对应的模型和维度,写进配置,避免以后没人记得当初用的哪个。

把召回结果塞进上下文:塞多少是判断问题不是越多越好

llama.cpp 的上下文窗口是显式配置的参数。塞爆了通常不报错,而是悄悄截断,表现是回答只覆盖前半段材料,或者后半段问题直接答偏。所以召回条数不能凭感觉,得先按 token 估算留出生成余量,再决定塞几条。

原则是宁少而准。召回十条里有八条不相关,不只是浪费窗口,还会把模型带偏,让它引用错材料。可以先按相似度卡一个阈值,过线的才进 prompt;过线太少就说明检索段还有问题,不该在生成段硬凑。

  • 先算 prompt 占用:系统提示加召回片段加用户问题,估一个总量
  • 给生成留余量,别把窗口占满,否则回答中途被截
  • 召回条数做成配置项,改的时候能回滚
  • 不同条数下的实际开销到 /tools/token-price-calculator 估一下,别凭感觉

对话模板和系统提示没套对,模型会用错角色说话

llama.cpp 支持多种对话模板,模板和模型不配套时,症状很典型:模型自问自答、把系统指令当正文复述、回答里冒出不存在的角色标签。这不是模型能力问题,是格式喂错了。

检索内容放在系统消息还是用户消息,也会影响模型愿不愿意引用来源。放在用户消息里并明确要求标注出处,通常比塞进系统提示更稳;但如果你需要严格约束行为,系统消息仍然要留。两种都试一遍看输出稳定性。

症状可能原因验证方式
自问自答模板与模型不配套换配套模板再问同一句
复述指令指令写进了正文位置把指令移到系统消息
硬编答案缺少「不知道就说不知道」约束问一个库里明确没有的问题
不给出处没要求引用或来源没带进块检查块里有没有来源元信息

速度幻觉:慢不一定是 bug,先分清是加载慢还是生成慢

第一次请求特别慢,多半是模型加载和缓存没热,跟 RAG 链路没关系,别急着改检索。区分方法很简单:连续发两次同样的请求,第一次慢第二次明显快,那就是加载问题,不是链路问题。

生成慢要分两种情况:一是 prompt 被召回条数撑长了,长 prompt 在本地推理里就是线性变慢;二是硬件本身吃不下这个模型规模。前者改召回条数就能缓解,后者改代码没用。

加载慢
首次请求慢、重复请求变快,属于模型或缓存冷启动
生成慢
每次请求都慢,看 prompt 长度和模型规模
检索慢
生成还没开始就卡住,看 embedding 服务的日志

硬件够不够,别拿别人的机器预期套自己。可以到 /models/deploy 按模型规模反推所需资源,再对照自己的配置判断。想要一个粗略的体感基准,也可以用 /tools/speed-test 跑一次看当时的数字。

从能跑到跑好的排查顺序

排查最忌讳一次改三处,改完不知道是哪处生效。按下面顺序单点验证,每一步只动一个变量,跑通了再进下一步。

  1. 只看检索打印召回片段和相似度,肉眼判断相关性;这一步不过关,后面全白搭。
  2. 只看生成把召回片段手工粘进 prompt,确认模型能拿这些材料作答,排除模型本身的问题。
  3. 再看拼接和模板检查角色分配、分隔符、上下文长度,确认没有静默截断。
  4. 做回归固定几条问答当测试集,任何一环改动后重跑一遍,别凭一次对话下结论。

这套顺序不适用的情况也要说:如果你的知识库极小、问题类型很单一,可能不需要完整 RAG,直接把全部材料塞进 prompt 反而更稳更快。这时先判断值不值得搭检索,再谈优化。

另外,换模型、换 embedding、改切块策略这类变动,都会让旧的回归结果失效,需要重新建立基线。把这些变动记录在配置里,比事后回忆省事得多。

下一步把召回和生成两段的日志先分开,再拿几条固定问答跑一遍,结果会告诉你该往哪个方向改。

常见问题

llama.cpp 里能不能直接用它的 embedding 接口做检索?

能,但要单独确认它加载的是向量模型而不是对话模型,且向量维度和你建的索引一致。很多人失败是因为拿生成模型顺手做向量化,相似度排序完全没意义。建议把向量化和生成分成两个实例,各自调参互不干扰。

为什么检索到了正确片段,模型还是答错?

多半是拼装环节:片段被截断、角色消息用错、或者指令里没说清「只能依据材料回答」。把召回片段手工粘进对话先验证一次,能答对就说明问题在拼接模板,答不对再回头查模型本身和提示词。

本地跑 RAG,召回条数给几条合适?

没有通用数字,取决于你的上下文配置和单块长度。做法是先估一块大概占多少 token,留出生成余量后反推条数,再拿几条已知答案的问题试。条数增加但准确率不再涨,就是该停的地方;检索不准时加条数通常只会更糟。

改了切块大小,需要重建索引吗?

需要。切块决定入库内容,旧索引里的片段不会自动跟着变,混着用会出现新旧两种粒度互相打架。换 embedding 模型同理。建议把切块参数和嵌入模型版本记在配置里,改动时一起走重建流程。

CPU 上跑能撑住 RAG 问答吗?

看并发和资料量。单用户偶尔问一句通常能用,多人同时问会明显排队,表现为越问越慢而不是报错。如果主要卡在长 prompt 上,先减召回条数比换硬件见效快;要多人共用再考虑上 GPU,去 /models/deploy 按规模测算一下再决定。

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