你大概已经能用 llama.cpp 跑起来一个模型,也想让它回答自己文档里的问题,但一上手就发现:llama-server 起来了,检索那边没人管;嵌入和生成两套超时对不上;并发一压就排队;答错了也不知道是检索错还是生成错。这篇不讲协议兼容和客户端怎么配,只讲这条链路怎么拼、怎么跑得住、出问题按什么顺序查。
你大概已经能用 llama.cpp 跑起来一个模型,也想让它回答自己文档里的问题。真上手就会发现:llama-server 起来了,检索那边没人管;嵌入和生成两套超时对不上;并发一压就排队;答错了也不知道是检索错还是生成错。这篇不讲协议兼容、也不讲客户端怎么配,只讲这条链路怎么拼、怎么跑得住、出问题按什么顺序查。
先给一个心理预期:llama.cpp 本身只解决「把上下文变成回答」这一小段,RAG 剩下的切块、嵌入、检索、拼装、降级,全是你自己的工程活。所以别指望一个命令搞定,把它当成两个独立服务加一段胶水代码来设计,后面调起来会轻松很多。
先把两段服务摆清楚:llama-server 管生成,检索管召回
最容易被忽略的一点:llama-server 只负责一件事,把拼好的上下文变成回答。切块、嵌入、向量库检索都不归它管,也不建议塞进同一个进程。两段服务各自的地址、端口、超时、并发上限都分开配,生产上把它们当成两个可以独立挂掉的依赖来对待。
嵌入模型可以另起一个 llama-server 实例,也可以走单独进程。要注意它会和生成模型抢显存/内存,先算一遍两者同时驻留够不够,不够就串行加载或者把嵌入放到另一台机器。别等压测时才发现在打架。
| 关注点 | 生成侧(llama-server) | 检索侧(切块/嵌入/向量库) |
|---|---|---|
| 职责 | 上下文→回答 | 文档→片段→候选 |
| 资源占用 | 长驻、吃显存 | 按需加载、吃内存或显存 |
| 超时 | 相对长 | 相对短 |
| 挂掉影响 | 降级直答 | 降级无检索 |
| 可替换性 | 换模型要重测提示词 | 换嵌入模型要重灌全库 |
检索侧的四件事:切块、嵌入、存、取
这四件事的顺序别搞反。切块是源头,嵌入和存储是过程,取是结果。源头错了,后面再优化排序也救不回来。切块要按语义边界切,比如段落、章节、函数块,别按固定字符数硬切;切块大小和重叠度写进配置文件,改一次就重新全量入库,不要增量补丁。
- 嵌入和生成用的模型不必是同一个,但要固定版本;换嵌入模型等于换了一整套向量,必须重灌全库
- 向量库选本地能跑的轻量方案就够,先跑通再谈规模,别一上来就自建集群
- 召回条数先给一个保守值,再靠重排或截断往下压;召回给太多等于把噪声直接喂给模型
- 阈值要留一份,相似度低于阈值时走「没找到」分支,别让模型自由发挥
- 切块元数据(来源文件、章节、页码)在入库时就存好,后面引用来源全靠它
存的时候顺手把元数据带上:来源文件名、章节标题、更新批次。这东西平时看着多余,等到用户问「这条结论哪来的」你就知道值钱了。取的时候先按相似度粗排,再用重排模型或简单的规则压到几条,控制底噪。
把链路接起来:一次请求的顺序和兜底
- 接收问题入口校验长度、去重、限并发;太长的问题先截断或摘要,别直接丢给检索。
- 改写(可选)多轮对话里把指代消解掉,或者生成几个查询变体做多路召回;单轮简单问答可以跳过。
- 向量检索拿到 top-k 候选,同时带上相似度和元数据;这一步超时要短,检索慢了整体都慢。
- 重排与截断按规则或重排模型压到固定条数,顺手把上下文长度预算算出来。
- 拼上下文系统提示、检索片段、对话历史抢同一个窗口,谁超谁让位要写死规则,不要临场决定。
- 调 llama-server 生成带上超时和取消信号;返回异常或超时就走降级分支,不要无限重试。
- 附带来源返回答案里标注引用了哪几条片段;降级时显式说明「未使用知识库」。
降级不是失败,是设计的一部分。llama-server 超时或返回异常时,降级到无检索直答,但要在返回里明确标注,别让用户以为答案是查出来的。同理,检索服务挂了也可以只留生成,但同样要标注。一条链路上任何一段都可以退化,前提是用户能看到退化。
跑起来之后:超时、并发、日志、小评测
两段超时分开设:检索短、生成长。整体超时取两者之和再加一点余量,别用一个值套全部,否则要么检索被误杀,要么生成永远等不到。llama-server 本身有并发上限,超出会排队;入口处要限并发数并给排队提示,不要靠无限重试把队列压垮。
日志至少记这几项:问题原文、召回片段 ID 与分数、拼装后上下文长度、生成耗时、是否降级。缺一项,出问题时都复盘不了。尤其是「是否降级」,很多线上事故就是降级了但没人知道。
| 日志字段 | 为什么记 | 怎么用 |
|---|---|---|
| 召回片段 ID 和分数 | 判断检索好不好 | 答错时先看是不是没召回 |
| 上下文长度 | 判断是否被截断 | 长了可能是片段太碎 |
| 生成耗时与是否降级 | 判断生成侧状态 | 区分是模型慢还是检索慢 |
| 入口问题原文 | 复现和回归 | 拿去当固定用例 |
准备一小批固定问题当回归用例,改切块、换模型、调提示词之后都跑一遍,只看两件事:答对没、有没有引用对来源。评测方法、判定标准可以到站内的指南中心看当时的整理,别自己拍脑袋定口径。模型和硬件的实际表现会变,具体数字到 /rankings 核对当时的记录。
RAG 调不好,八成不是模型的问题,是检索给了错的上下文,或者超时和降级没设计好。下一步把你现在的检索和生成两段依赖列出来,照着上面的表逐项对一遍超时、并发和降级策略;想了解本地推理和工具链的更多实测口径,去站内工具目录找对应条目核对当时的数字。→
常见问题
llama.cpp 跑 RAG,嵌入模型必须也用 llama.cpp 吗?
不一定。嵌入可以走任何能吐向量的本地服务,只要向量库和查询端用的是同一个模型、同一套归一化方式就行。用 llama.cpp 起嵌入的好处是依赖统一、离线好打包;坏处是和生成抢内存。先把显存和内存算清楚,再决定是共用一个进程还是分开起。
检索到的内容和模型回答对不上,一般先查哪一步?
先看召回,再看生成。把召回片段和分数打出来,人工判断「答案到底在不在里面」:不在,问题在切块或嵌入;在但没被用上,问题在拼装顺序、上下文截断或提示词。跳过这一步直接调提示词,多半白调。
本地这么搭和直接调云端 API 比,什么时候不值得?
文档量小、访问不频繁、对数据不出内网没有硬要求时,云端 API 省事得多。本地这套只在有明确离线或数据边界要求、或者调用量大到 API 成本压不住时才划算。选之前到 API接入还是本地部署(/api-vs-self-host-decision) 对一遍适用条件。