本地跑一套检索增强问答,常见卡点不在模型能不能起,而在两边拼起来之后:生成慢、检索和推理抢显存、超时设不出来、坏了不知道哪一段断。这篇按工程落地的角度,把 llama.cpp 负责生成、RAGFlow 负责检索这条链路上的参数、并发、降级和排查顺序摆成清单,方便你照着调。
本地跑一套检索增强问答,卡点往往不在模型能不能起来,而在两边拼起来之后:生成慢、检索和推理抢显存、超时设不出来、坏了不知道哪一段断。这篇按工程落地的角度,把 llama.cpp 负责生成、RAGFlow 负责检索这条链路上的参数、并发、降级和排查顺序摆成清单,方便你照着调。
先说适用边界:这套做法适合愿意自己管进程、盯日志的团队,如果你更想开箱即用、不想碰推理进程,那托管接口反而更省事。下面所有跟硬件档位相关的数字,别照抄别人博客,到站内页面按自己机器的实际峰值重新量一遍。
先把两边的活分清楚
最常见的踩坑是让同一台机器既扛向量检索又扛推理。检索吃内存和磁盘 IO、推理吃显存,两个高峰叠在一起,表现就是整机卡死而不是某一端报错。动手前先画一张职责表,谁负责什么写死,后面排查才不会来回甩锅。
| 环节 | 交给谁 | 你要盯的指标 |
|---|---|---|
| 文本切片、向量化、召回、重排 | RAGFlow | 召回条数、索引内存占用 |
| 把拼接后的上下文变成回答 | llama.cpp | 首字延迟、生成总时长、显存峰值 |
| 超时、重试、降级、日志 | 你自己的中间层 | 三档超时、命中哪条降级分支 |
- 确认两边是 HTTP 调用还是进程内嵌:走接口就多一层超时和连接池要管,进程内嵌则要留意内存峰值叠加,两种形态的排查顺序不一样
- 对齐输入输出格式:RAGFlow 交出来的上下文长度直接决定生成端首字延迟,先拿一段长文本量一遍再定截断档位
- 把职责写成文档而不是口头约定,出问题时按表对号,先看是哪一段的时间戳断了
超时、并发与资源争抢怎么配
超时拆成三档是这套链路上性价比最高的改动:检索超时管住向量库,生成超时管住推理进程,整体超时兜在最外层。整体时长必须大于前两档之和,否则外层永远先掐断,你连日志都看不到是哪一段慢。
- 量单次延迟串行发一批请求,记录检索耗时、生成首字时间、生成总时长,得到三条分布曲线,取一个偏保守的上限作为基准
- 定三档超时检索超时略小于整体超时的一半,生成超时按首字时间加缓冲算,整体超时大于两者之和
- 压并发找拐点逐步加并发,观察延迟曲线从平稳到陡升的那个点,槽位数设在这个点之前
- 留资源余量按峰值而不是均值规划内存和显存,向量索引常驻内存这一块最容易漏算
把响应做成流式,用户先看到字才愿意等。但要注意流式下超时判断要按「多久没吐下一个字」来算,不能用总时长,否则长答案会被误杀。显存不够时不适用提高并发,只能减槽位或者把检索挪到另一台机器,没有第三条路。
降级路径与日志该记什么
降级不是兜底代码写没写的问题,而是每条分支都要能在日志里被认出来。检索为空和生成超时是两件完全不同的事,如果都返回同一个错误码,你永远分不清是数据问题还是性能问题。
- 检索为空
- 不让模型硬答,返回固定话术加建议换关键词,比编一个答案省排查时间
- 生成超时
- 先把检索到的原始片段列表返回给用户,链路不算报错,用户自己看
- 上下文超长
- 按优先级截断而不是一刀切,把最相关的段落留在前面
- 每次请求至少埋这些字段:检索耗时、召回条数、拼进上下文的字符数、生成耗时、首字时间、命中哪条降级分支
- 日志按请求 ID 串起来,检索和生成分开打,出问题先比对两段的时间戳在哪断的
- 降级分支要有计数,某个分支频繁被命中,说明上游参数该调了,而不是这条分支写得好
- 别把完整上下文全量打进日志,字符数够用,全量存盘会拖慢写入还占磁盘
搭完怎么验证答得准不准
链路通了不等于答得对。验证的关键是把「检索没找到」和「找到了但答错」分开看,这两类错的修法完全不同,混在一起统计只会得出「效果一般」这种没法行动的结论。
| 观察维度 | 看什么 | 不适用的情况 |
|---|---|---|
| 检索命中率 | 标准依据段落有没有被召回进上下文 | 问题本身就没有唯一依据时,这个指标没有意义 |
| 生成忠实度 | 回答是否只用了召回内容,有没有自己加戏 | 开放创作类问答本来就需要模型补充,别硬套 |
| 端到端体感 | 用户实际等多久、愿不愿意等完 | 内部工具对延迟容忍度高时,体感权重可以降 |
做法上准备一组固定问题加标准依据段落,每次改切片、改召回条数、换模型都跑一遍,别凭感觉判断好没好。把改动前后的结果记在同一张表里,才看得出是变好了还是只是换了个错法。想横向对比推理端的表现,可以到站内工具目录挑对应的测试项自己跑,别引用别人的结论。
出问题时的排查顺序
排查顺序的核心原则是先拆后合:先证明每一段单独能跑,再怀疑拼接层,最后才怀疑模型。跳过前两步直接调提示词,是最费时间的一条路。
- 单独调检索直接打 RAGFlow 的检索接口,确认它能返回片段和相似度,返回为空就去看索引和切片
- 单独调生成把一段固定文字发给 llama.cpp,确认它能正常吐出回答,不通就查进程和显存
- 再合起来两边都通之后的故障基本都在拼接层:字段名对不上、上下文被超长截断、编码或转义出错
- 看间歇性失败如果是不定时失败而不是稳定报错,优先怀疑上游限流或连接池耗尽,按时间分布看日志而不是看单条
- 最后怀疑模型把同一段提示词直接发给推理进程,能定位问题在模型还是在自己的链路上
出问题时先问「哪一段的时间戳断了」,比先问「是不是模型不行」省下大半排查时间。下一步把上面这几个参数落到你自己的机器上,需要对照不同推理方案的实测表现和选型标准时,去站内页面核对当时的数字再决定,别直接抄别人的配置。→
常见问题
为什么我用 llama.cpp 配 RAGFlow,回答总是很慢?
大概率是两个瓶颈之一:检索把太多片段塞进上下文,生成端要处理的字符数暴涨;或者推理并发开太高,请求在排队。先量检索返回条数和拼进上下文的字符数,再压一遍并发看延迟曲线拐点,两个数摆出来就知道该动哪边。
检索不到内容时,应该让模型自由发挥吗?
不建议。本地部署的意义就是答案可控,检索为空时直接走兜底话术并提示用户换个问法,同时把这次请求记进日志。硬答出来的内容没法验证来源,等到有人当真去用,排查成本远超省下的那点体验。
这套本地方案什么时候不该用?
如果语料更新频繁、团队多人同时用、又没人维护机器,本地这套的运维成本会超过直接买托管接口。反过来,数据不能出内网、查询量小、能接受单机可用性,那本地就很合适,权衡点主要在数据边界和维护人力。