最近把 openrig 从源码跑通了一遍,顺手接了一个内部知识库问答的场景。先给它一个定位:openrig 不是那种大而全的 RAG 平台,它更像一个专门实现 Retrieval-Interleaved Generation(RIG)思路的开源框架。RIG 这个名字你可能觉得陌生,但传统 RAG 的那一锤子买卖你一定不陌生——先检索、后生成,检索结果一旦不够准,后面再怎么提示词补救都白搭。OpenRIG 的做法很直接:让模型在写答案的过程中自己判断“这个地方要不要查资料”,查完再接着写。这篇东西我会从原理拆解、参数调优、实操跑通、踩坑记录四个角度展开,适合正在用检索增强方案做问答落地的同学参考。
1. OpenRIG 的核心思路:把“一次检索”改成“边写边查”
我第一次看 openrig 的时候,脑子里冒出的是“它在给大模型装一个临时记忆开关”。传统 RAG 和 RIG 看似都是检索增强,实际运行逻辑完全不同。理解这点,比会调几个参数重要得多。
1.1 传统 RAG 到底卡在哪
日常写的 RAG 流程大体是这样:用户提问 -> 用 embedding 把问题向量化 -> 在向量库里召回 top-k 片段 -> 把片段和问题拼进 prompt -> LLM 生成答案。看起来没毛病,可一旦放到真实业务里,问题就藏不住了。
举个例子,用户问“华东区三季度退货率最高的三个品类,分别对应哪些售后对策”。系统先做一次语义检索,会按相似度召回很多“华东区”、“退货率”、“售后”相关的文本,但这些文本往往分散在不同文档里:一个讲退货率统计口径,一个讲售后流程,另一个讲品类定义。第一次召回结果里如果没有包含“对策清单”,那 LLM 就算再聪明,也只能对着不完整的资料编。这就是传统 RAG 最别扭的地方——检索是一次性的,生成到一半发现缺信息,也没有回头路可走。
更麻烦的是检索成本问题。为了让召回质量高一点,大家习惯每次提问都取 5 到 8 个片段,但你问一个“退货流程有几步”这种小问题,根本不值得召回这么多。上下文被无关片段塞满,回答变慢,幻觉概率反而升高。OpenRIG 解决的问题,恰恰就是“省着点查、花在刀刃上”。
1.2 RIG 的循环决策机制
OpenRIG 的核心机制可以总结成一句话:生成、判断、检索、继续生成。它不是拿到问题先检索,而是让生成器先基于已有能力写一小段,同时一个轻量决策器会盯着当前已经生成的内容,判断这里是不是有事实缺口。
什么算事实缺口?比如出现了明确数字、专有名词、型号、日期,或者一个“它”指代不清的对象,这些地方模型自己心里也发虚,继续硬写大概率会幻觉。决策器一旦判定需要外部证据,就会暂停生成,把当前不完整的 draft、用户原始问题、以及刚刚提到的关键实体一起打包成检索 query,去库里查回几个片段,然后把片段塞回上下文,生成器继续往下写。
技术上怎么实现的,后面配置里会讲,但你先记住这个循环:一条答案可能是“生成 2 句 -> 停一下 -> 查资料 -> 写 3 句 -> 再停 -> 再查 -> 收尾”。整个过程中,检索从一次前置动作变成了穿插在生成路径里的动态动作。这其实很像真人写汇报:先搭个框架,发现数据不清楚,去电脑里翻文件夹,拿到数据再接着往下写,而不是没查完就不许动笔。
1.3 整体架构与模块划分
openrig 这个项目给我的感觉是,模块数量不多,但接口边界很清楚。跑过一遍之后,我大概把它分成了四块:
| 模块 | 名称 | 主要职责 |
|---|---|---|
| 生成器 | Generator | 负责逐段生成答案文本,可以对接任意主流的 LLM API/本地模型 |
| 决策器 | Decider | 根据当前生成内容判断是否需要检索,是整个循环的“红绿灯” |
| 检索器 | Retriever | 接收查询,从知识库索引中返回候选片段,支持多种检索后端 |
| 上下文管理器 | Context Manager | 维护生成历史与检索片段的拼接规则,控制 token 预算 |
如果你看过别的 RAG 框架,会观察到 openrig 特别的地方在 Decider。这个模块通常很小,甚至在简单场景里可以用一个带阈值的规则函数实现,但它决定了整个系统的行为方式。检索器本身反而是标准化的——BM25、向量检索、混合检索都能接,openrig 不太挑检索后端,这是它做得好的一点。它的设计哲学是“检索策略可替换,生成控制内聚”,意味着你想从 Elasticsearch 换成 Milvus,或者从稠密向量换成稀疏关键字,都不影响决策逻辑。做业务集成的时候,这种解耦能省不少事。
2. 核心功能与关键参数,怎么调才不翻车
框架能跑只是第一步,真正花时间的永远是调参。openrig 的默认值更适合跑通 demo,离“稳定可用”还差得远。这里我根据实际调过的几轮,把几个关键旋钮讲清楚。
2.1 什么时候触发检索:阈值与最大迭代次数
决策器最关键的两个参数,一个是触发阈值,另一个是最大检索迭代次数。阈值决定模型对当前答案内容的“自信程度”下限:低于阈值就去查,高于阈值就继续写。你可以把它理解成一个安检门,设置得太松,什么人都要开箱检查,响应速度直线下降;设置得太紧,真带了违禁品也直接放行,准确率崩给你看。
我在测试集上做过一个小实验,把触发阈值从 0.5 调到 0.9,结果很有意思:
| 阈值 | 每轮平均检索次数 | 答案忠实度得分 | 平均响应时间 |
|---|---|---|---|
| 0.5 | 5.2 | 0.88 | 6.8s |
| 0.7 | 3.4 | 0.91 | 4.5s |
| 0.9 | 1.9 | 0.77 | 3.1s |
阈值太低,模型遇到任何没见过的小细节都要查一遍,响应时间快赶上以前用慢速接口的年代;阈值太高,模型又容易自信过头,很多需要引用的地方直接凭记忆带过。对我来说,0.7 左右算是一个比较平衡的位置。
最大迭代次数也要单独说。这个参数是防止“边写边查”变成死循环的保险丝。如果一次生成过程中连续检索了 6 次还在继续查,说明决策器可能出了问题,要么是检索结果一直没解决缺口,要么是上下文被无关片段带偏了。我一般设置成 3 到 5 次,超过次数就放弃检索,强制让模型基于已有上下文完成回答,并返回一个低置信度标记。
2.2 检索器选型与多路召回
openrig 本身不限定检索器,但哪种检索方式适合什么场景,还是得分清楚。只做向量检索是不够的,尤其是内部知识库经常充斥着型号、编号、人名这种“精确匹配”需求。
举个例子,知识库里有一个“KL320 继电器”,你用 embedding 向量检索去查“KL320 继电器的故障代码”,往往召回了一堆 KL302、KL350 等形状相似的邻居。因为它们的向量表示非常接近,语义距离根本无法区分数字编号之间的微小差异。这时候 BM25 的倒排索引反而更稳,能通过精确 token 匹配把 KL320 的记忆精准捞出来。
所以我把 openrig 的检索器配成了混合召回:一路用向量检索做语义泛化,另一路用 BM25 做字面匹配,最后通过 RRF(Reciprocal Rank Fusion)把两个排序结果融合起来。 top_k 不是越大越好,经验值是 3 到 5。你可能会想,召回多一点不是更保险吗?问题在于每次检索结果都会塞进上下文,召回片段超过 5 个,生成器的注意力基本被稀释,最后引用的反而不是真正有用的内容。
2.3 生成参数和上下文预算
生成参数反而是最容易照抄的部分,但它也需要说明为什么这么设。openrig 适合的事实问答场景,temperature 我一般压在 0.1 到 0.3。这个范围既保留一点多样性,又不会让模型在关键事实上来回跳。如果是头脑风暴或者文案生成,可以放开到 0.7,但 RIG 本身是为检索增强问答设计的,别把它当成聊天机器人来调。
上下文预算的管理是另一个容易忽略的坑。openrig 的 Context Manager 会把“原始问题 + 当前生成的草稿 + 历次检索片段”一起喂给生成器,但如果每次检索都往上下文里塞 2000 个 token,三轮检索之后上下文直接爆掉。现在的做法是:给检索片段设置一个总预算,比如 4096 个 token,每次新检索回来的片段,先把旧片段里与当前 query 相关度最低的挤出去,再插进来。这样既保留了最近几次检索的关键信息,又不会让上下文无限膨胀。记住,上下文是有限的资源,检索不是越多越好,而是要精。
2.4 一个可参考的最小配置模板
纸上谈兵没用,直接给一份我用下来比较稳的配置骨架。不同版本字段名可能会变,但核心含义差不多:
generator: provider: openai_compatible model: qwen2.5:14b temperature: 0.2 max_tokens: 2048 decider: trigger_threshold: 0.7 max_iterations: 4 skip_if_no_entity: true retriever: type: hybrid vector: backend: milvus top_k: 3 metric: cosine sparse: backend: bm25 top_k: 3 fusion: method: rrf rrf_k: 60 query_mode: consolidate context: max_retrieval_tokens: 4096 max_history_tokens: 2048 citation_format: "[[citation:{id}]]"这里面的 query_mode 值得一提。默认情况下,决策器会用“当前草稿 + 问题”组成检索 query,但有时候草稿太长,噪声也多。我试过改成 extract_keyword,也就是只抽取出当前待验证的实体名来检索,结果在型号类问题上准确率提升了大约 6 个百分点。这个开关可以根据你的知识库类型灵活切换。
3. 实操过程:从零跑通一个 OpenRIG 问答服务
配置看得再多,不如自己跑一遍。下面把这个流程按我实际操作时的顺序写出来,尽量把每一步怎么想、为什么这么做也讲清楚。
3.1 环境准备与安装
先说版本选择。openrig 对 Python 的要求不算苛刻,至少 3.10 以上。我这边用的是 3.11 和 uv 工具管理虚拟环境。如果你熟悉 conda 也行,但建议用 uv,装依赖快很多,对源码项目也友好。
第一次接触的话,可以直接用源码方式安装:先把仓库 clone 到本地,进入目录后执行uv sync或者pip install -e .。很多开源框架喜欢把依赖打包得很“重”,openrig 这点做得不错,核心依赖只有 pydantic、fastapi、httpx 和几个插件接口,向量库和模型服务都是通过独立后端接入的,所以安装过程基本不会碰到底层依赖打架的问题。
装完以后,需要准备一个大模型服务和一套知识库检索服务。我用的是本地部署的 OLLAMA 跑 14B 模型,既方便调试,也省去网络调用延迟;检索服务则用 Milvus 存放向量索引,用 Elasticsearch 存放 BM25 索引,两个索引指向同一份分块文档。如果你没有现成的向量库,openrig 社区版里有 sqlite-vss 这种轻量后端可以选择,先跑通 demo 完全够用。建议第一遍别追求复杂后端,能起来比什么都重要。
3.2 知识库的清洗、分块和索引
很多同学翻车在索引这一步。openrig 只负责检索和生成,不负责帮你把一堆 PDF、Word 变干净。我这次接入的是一个售后知识库,里面既有规范文档,也有聊天记录整理出来的 FAQ。清洗时最重要的一条是:把标题、表格、列表等结构信息保留下来,不要为了省事全转成纯文本。因为“华东区退货率”这种问题,分块时必须知道它属于哪个大区章节,否则召回片段会缺少上下文。
分块参数我用了 chunk_size=512、overlap=64。这个 chunk_size 不是越大越好,512 个 token 左右的块,既能保留相对完整的段落语义,又不会让决策器在判断时被无关细节干扰。overlap 的作用是和前一块保持衔接,防止一个段落被切在关键句子中间。如果你发现很多问题正好卡在“上一块结尾”和“下一块开头”之间,就把 overlap 调大到 128。
建立双索引时,要注意两个索引的分块应该完全一致。我当时犯过一个错:向量索引用了 Markdown 段落切块,BM25 索引却用了固定字符数切块,结果同一个问题在两个检索器里召回的片段完全是两个世界的文本,RRF 融合后反而把真正相关的内容排到后面去了。后来统一成同一套 JSON Lines 分块文件,两个索引各自读这个文件,问题立刻消失。索引构建命令很简单,openrig 提供了一个build_index入口,指定文档目录和后端类型即可,背后会做 embedding 和倒排表生成。
3.3 启动问答服务与接口演示
知识库索引建完之后,启动服务也比想象中简单。我用的启动命令是:
python -m openrig serve --config config.yaml --host 0.0.0.0 --port 8100openrig 会基于 FastAPI 起一个 HTTP 服务,默认提供/chat接口。如果前面的配置没问题,启动日志里会看到模型连接成功、检索后端连接成功。这里给一个最小请求示例:
curl -X POST http://localhost:8100/chat \ -H "Content-Type: application/json" \ -d '{"question": "华东区三季度退货率最高的三个品类分别有哪些售后对策?"}'返回结果里最值得关注的是两个字段:一个是used_retrieval_times,表示这次回答实际触发了几次检索;另一个是citations,包含最终答案中引用到的片段 ID。如果一个问题 you 的答案里没有任何引用,说明决策器一次都没触发检索,这通常意味着它对问题直接开答了——你要警惕,这种回答在事实型任务里往往不靠谱。
3.4 实测效果对比
为了验证 openrig 到底有没有用,我在同样一份售后知识库上跑了一个 50 条真实问题的测试集。对照组是常见的固定 RAG 流程,top_k=5,一次性把 5 个片段塞进上下文;实验组用 openrig 默认配置,触发阈值 0.7,max_iterations=4。用忠实度评分和人工打分两个维度看,结果大概是这样的:
| 方案 | 忠实度得分 | 人工好评率 | 平均检索片段数 |
|---|---|---|---|
| 传统 RAG(top_k=5) | 0.74 | 62% | 5 |
| OpenRIG(默认) | 0.83 | 78% | 2.9 |
OpenRIG 在平均检索片段数更少的情况下,忠实度反而更高。因为它没有一次性把所有可能相关的段落都灌进上下文,而是根据生成过程中的缺口定向查询,上下文更干净,模型注意力更聚焦。当然这不是说 openrig 万能,它在多跳推理问题上还是会出现“查了后面忘前面”的情况,解决办法是配合多轮历史记录和摘要模块,这部分我后面会讲。
4. 常见问题与调试实录
跑通只是开始,真正折磨人的是后续调试。整理了四个我自己踩过、也在团队里反复出现的坑,按排查顺序写一下。
4.1 检索触发得太频繁,响应时间暴涨
如果你发现一次回答平均检索次数超过 5 次,而且响应时间翻倍,大概率是决策器阈值设得太低。但先别急着把阈值拉高,否则容易直接走向另一个极端。
我碰到过一次特别有意思的故障:触发频繁的原因不是阈值,而是检索结果质量太差。决策器本来判断“这里缺一个型号信息”,但检索器返回的片段里恰好没有那个型号,于是模型的自检信号一直得不到满足,它只能反复检索类似的关键词,陷入“越查越不放心”的循环。这种情况下光调阈值没用,得去看召回片段里是不是丢了精确匹配结果。我最后是调整了混合检索中 BM25 的权重,让精确匹配排在前面,触发频率立刻降了下来。所以排查检索过频,核心思路是“先确认检索结果对不对,再调整门控阈值”。
4.2 生成结果还是出现明显事实错误
有时候检索次数没问题,引用格式也正常,回答里却存在事实性错误。后来我把错误答案逐条拉出来看,发现三个高复发原因:
第一种,检索到的片段本身是对的,但片段里包含的信息是经过二次转述的二手结论,缺少原始数据支撑。解决方式是在索引阶段做数据来源标注,让配置里的 citation 字段额外带上文档路径和更新时间。第二种,top_k 太小且检索时机不对,比如抽检到的片段覆盖了退货率,却没有覆盖“售后对策”,决策器误以为已经满足了一个实体,就停止继续检索。解决方式是让决策器维护一个“检查清单”,把问题中解析出的所有实体都列出来,只要还有实体未覆盖,就继续检索。第三种,我对引用验证不够严格。openrig 生成答案后,会保留引用片段 ID,但不会自动校验“这句话真的是从那个片段里总结出来的吗”。我在后面加了一个忠告校验步骤,拿答案句子逐条去算与引用片段的语义相似度,低于 0.5 的句子直接打回重写,这个操作把忠实度又拉高了近 5 个点。
4.3 上下文窗口溢出
大模型上下文窗口是硬约束,openrig 也不能免俗。特别是当历史对话很长,又穿插了多次检索片段时,任你 128K 上下文也撑不住。
我的经验是设置两级预算。第一级,在 context 里限制max_retrieval_tokens=4096,每次注入新检索片段前,先按“与当前 draft 的相似度”把最不相关的旧片段移除,保证检索片段总量不超过预算。第二级,开启历史摘要。当历史 token 超过max_history_tokens=2048时,把更早的对话压缩成一段摘要,而不是原封不动地继续拼。这样虽然会丢失一些细节,但能保住对话主线。如果两三级预算都试过还是溢出,那就应该怀疑是不是 max_iterations 设太高,把检索次数降下来是最直接的止血方案。
4.4 自建评估集,比什么都重要
关于评估,我最想强调的一点是:openrig 的每一次循环决策都留下了可观测的痕迹,不利用起来实在可惜。我在调试阶段做了一个简单的评估流程:准备 30 条有明确答案的测试题,每题运行 3 次,记录每次生成的忠实度、引用准确率、检索次数和响应时间。人工抽看 10% 的答案,其余用 LLM 打分。
打分的 prompt 里我固定要求三件事:一是检查答案是否和引用片段一致,二是看引用片段是否足够支撑完整答案,三是看是否有多余检索浪费。跑完 30 题,很快就能发现参数调优的方向。比如一开始忠实度普遍低,就重点看检索 top_k 和触发阈值;引用准确率低,就检查索引分块和 RRF 融合权重。这个闭环做好之后,openrig 在你自己的知识库上才真正“调熟”了,不再是一个黑盒演示品。
回过头来想,openrig 这个项目最打动我的一点,是它把一个很朴素的道理落到了代码里:写东西的时候不懂就去查,别硬编。你当然可以去探索更多扩展方向,比如把决策器从规则改成小模型打分,或者在检索循环里加入知识图谱的实体链接,但先把“生成 - 决策 - 检索 - 再生成”这个闭环跑稳,比盲目堆功能更值得。我目前把它用在了两个内部知识库问答服务上,线上运行最明显的变化是,维护人员不再需要天天盯着“幻觉反馈”了。这条路径我自己会继续往下走,也希望这篇记录能让你少走一点弯路。