llama.cpp + rag

接入与开发

给开发者一份本地用 llama.cpp 起服务、接嵌入、搭检索问答链路的工程落地清单,含超时、并发、降级、日志与评测要点。

你大概已经能用 llama.cpp 跑起来一个模型,也想让它回答自己文档里的问题,但一上手就发现:llama-server 起来了,检索那边没人管;嵌入和生成两套超时对不上;并发一压就排队;答错了也不知道是检索错还是生成错。这篇不讲协议兼容和客户端怎么配,只讲这条链路怎么拼、怎么跑得住、出问题按什么顺序查。

你大概已经能用 llama.cpp 跑起来一个模型,也想让它回答自己文档里的问题。真上手就会发现:llama-server 起来了,检索那边没人管;嵌入和生成两套超时对不上;并发一压就排队;答错了也不知道是检索错还是生成错。这篇不讲协议兼容、也不讲客户端怎么配,只讲这条链路怎么拼、怎么跑得住、出问题按什么顺序查。

先给一个心理预期:llama.cpp 本身只解决「把上下文变成回答」这一小段,RAG 剩下的切块、嵌入、检索、拼装、降级,全是你自己的工程活。所以别指望一个命令搞定,把它当成两个独立服务加一段胶水代码来设计,后面调起来会轻松很多。

先把两段服务摆清楚:llama-server 管生成,检索管召回

最容易被忽略的一点:llama-server 只负责一件事,把拼好的上下文变成回答。切块、嵌入、向量库检索都不归它管,也不建议塞进同一个进程。两段服务各自的地址、端口、超时、并发上限都分开配,生产上把它们当成两个可以独立挂掉的依赖来对待。

嵌入模型可以另起一个 llama-server 实例,也可以走单独进程。要注意它会和生成模型抢显存/内存,先算一遍两者同时驻留够不够,不够就串行加载或者把嵌入放到另一台机器。别等压测时才发现在打架。

关注点生成侧(llama-server)检索侧(切块/嵌入/向量库)
职责上下文→回答文档→片段→候选
资源占用长驻、吃显存按需加载、吃内存或显存
超时相对长相对短
挂掉影响降级直答降级无检索
可替换性换模型要重测提示词换嵌入模型要重灌全库

检索侧的四件事:切块、嵌入、存、取

这四件事的顺序别搞反。切块是源头,嵌入和存储是过程,取是结果。源头错了,后面再优化排序也救不回来。切块要按语义边界切,比如段落、章节、函数块,别按固定字符数硬切;切块大小和重叠度写进配置文件,改一次就重新全量入库,不要增量补丁。

  • 嵌入和生成用的模型不必是同一个,但要固定版本;换嵌入模型等于换了一整套向量,必须重灌全库
  • 向量库选本地能跑的轻量方案就够,先跑通再谈规模,别一上来就自建集群
  • 召回条数先给一个保守值,再靠重排或截断往下压;召回给太多等于把噪声直接喂给模型
  • 阈值要留一份,相似度低于阈值时走「没找到」分支,别让模型自由发挥
  • 切块元数据(来源文件、章节、页码)在入库时就存好,后面引用来源全靠它

存的时候顺手把元数据带上:来源文件名、章节标题、更新批次。这东西平时看着多余,等到用户问「这条结论哪来的」你就知道值钱了。取的时候先按相似度粗排,再用重排模型或简单的规则压到几条,控制底噪。

把链路接起来:一次请求的顺序和兜底

  1. 接收问题入口校验长度、去重、限并发;太长的问题先截断或摘要,别直接丢给检索。
  2. 改写(可选)多轮对话里把指代消解掉,或者生成几个查询变体做多路召回;单轮简单问答可以跳过。
  3. 向量检索拿到 top-k 候选,同时带上相似度和元数据;这一步超时要短,检索慢了整体都慢。
  4. 重排与截断按规则或重排模型压到固定条数,顺手把上下文长度预算算出来。
  5. 拼上下文系统提示、检索片段、对话历史抢同一个窗口,谁超谁让位要写死规则,不要临场决定。
  6. 调 llama-server 生成带上超时和取消信号;返回异常或超时就走降级分支,不要无限重试。
  7. 附带来源返回答案里标注引用了哪几条片段;降级时显式说明「未使用知识库」。

降级不是失败,是设计的一部分。llama-server 超时或返回异常时,降级到无检索直答,但要在返回里明确标注,别让用户以为答案是查出来的。同理,检索服务挂了也可以只留生成,但同样要标注。一条链路上任何一段都可以退化,前提是用户能看到退化。

跑起来之后:超时、并发、日志、小评测

两段超时分开设:检索短、生成长。整体超时取两者之和再加一点余量,别用一个值套全部,否则要么检索被误杀,要么生成永远等不到。llama-server 本身有并发上限,超出会排队;入口处要限并发数并给排队提示,不要靠无限重试把队列压垮。

日志至少记这几项:问题原文、召回片段 ID 与分数、拼装后上下文长度、生成耗时、是否降级。缺一项,出问题时都复盘不了。尤其是「是否降级」,很多线上事故就是降级了但没人知道。

日志字段为什么记怎么用
召回片段 ID 和分数判断检索好不好答错时先看是不是没召回
上下文长度判断是否被截断长了可能是片段太碎
生成耗时与是否降级判断生成侧状态区分是模型慢还是检索慢
入口问题原文复现和回归拿去当固定用例

准备一小批固定问题当回归用例,改切块、换模型、调提示词之后都跑一遍,只看两件事:答对没、有没有引用对来源。评测方法、判定标准可以到站内的指南中心看当时的整理,别自己拍脑袋定口径。模型和硬件的实际表现会变,具体数字到 /rankings 核对当时的记录。

RAG 调不好,八成不是模型的问题,是检索给了错的上下文,或者超时和降级没设计好。
下一步把你现在的检索和生成两段依赖列出来,照着上面的表逐项对一遍超时、并发和降级策略;想了解本地推理和工具链的更多实测口径,去站内工具目录找对应条目核对当时的数字。

常见问题

llama.cpp 跑 RAG,嵌入模型必须也用 llama.cpp 吗?

不一定。嵌入可以走任何能吐向量的本地服务,只要向量库和查询端用的是同一个模型、同一套归一化方式就行。用 llama.cpp 起嵌入的好处是依赖统一、离线好打包;坏处是和生成抢内存。先把显存和内存算清楚,再决定是共用一个进程还是分开起。

检索到的内容和模型回答对不上,一般先查哪一步?

先看召回,再看生成。把召回片段和分数打出来,人工判断「答案到底在不在里面」:不在,问题在切块或嵌入;在但没被用上,问题在拼装顺序、上下文截断或提示词。跳过这一步直接调提示词,多半白调。

本地这么搭和直接调云端 API 比,什么时候不值得?

文档量小、访问不频繁、对数据不出内网没有硬要求时,云端 API 省事得多。本地这套只在有明确离线或数据边界要求、或者调用量大到 API 成本压不住时才划算。选之前到 API接入还是本地部署(/api-vs-self-host-decision) 对一遍适用条件。

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