ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从RAG到RIG:OpenRIG多跳问答架构解析与五大踩坑实录

从RAG到RIG:OpenRIG多跳问答架构解析与五大踩坑实录 去年我在重构一个内部知识库问答工具时把管线从传统RAG改成了RIGRetrieval-Interleaved Generation检索交错生成。折腾完发现这套思路可以抽出来给其他人复用于是整理成了名为OpenRIG的开源实验项目。这篇文章就聊聊我为什么放弃先检索后生成的RAG套路OpenRIG在架构上做了哪些取舍以及我实测两周踩过的五个坑。如果你正在做知识库问答、Agent型检索应用或者只是本地搭过LLM服务这篇应该能给你省下不少时间。1. 从RAG到RIG为什么生成到一半才想起查资料1.1 传统RAG的两个薄弱环节传统RAG的流程大家都很熟悉用户提问系统先拿问题去向量库检索出一批文档片段拼进上下文再让LLM生成回答。这个流程在单跳问答里表现不错比如这家公司的办公地址在哪一个检索动作就能覆盖。但到了多跳问答场景问题就暴露了。比如用户问我手上这个项目用了MIT协议那它能不能直接商用如果可以需要注意什么这个问题需要两步知识第一步是MIT协议本身的授权条件第二步是商用场景下的常见限制和风险点。传统RAG在第一轮检索时很难用一个query同时命中这两类资料。更麻烦的是LLM生成到后半段发现关于商用注意事项的信息我上下文里根本没有这时候管线已经进入回答阶段没有回头路了。它只能硬编或者回答得含糊其辞。这个问题的本质在于RAG把检索放在了思考之前假设了用户的问题在第一轮就能被完整表达。但真实问题往往是分层的知识缺口是在生成过程中才逐步显现的。1.2 RIG的循环检索意图、暂停、补料、继续RIG的思路正好反过来不是先生成好所有需求再检索而是让LLM边生成边判断我现在缺什么知识一旦发现缺口就暂停生成触发一次检索拿到资料后继续生成。如此循环直到回答完整。OpenRIG实现的循环大致是四步LLM生成一段文本在需要外部知识的位置输出一个特殊标记比如retrieve需要查询的内容/retrieve控制器解析标记提取检索意图构造出一条独立的搜索query检索器去混合检索BM25 向量召回相关资料检索结果被注入上下文LLM接着生成后面的内容。这个循环看起来不复杂但它的意义很大。它让检索不再是生成前的一次性动作而是变成了生成过程中的一个可用能力。LLM什么时候需要外部资料、需要什么资料都由生成过程动态决定而不是由用户的初始问题一把梭。1.3 这个思路更贴近真实问答场景我常常用一个类比来解释RAG和RIG的区别RAG像出门前查一次地图路线规划好了再上路但中途遇到修路封路就只能傻眼RIG像开车时开着实时导航GPS发现前方绕行就立刻重新规划路线既能走也能随时调整。当然RIG不是每次都要检索。OpenRIG的控制器里有触发条件判断模型如果觉得已有上下文足够回答就正常生成完全程不触发检索。这一点非常关键否则系统会退化成每句话都去翻资料延迟和成本都会失控。什么时候适合用RIG而不是RAG我测下来的结论是单跳问答、资料相关性要求极高的场景比如合同条款审阅用传统RAG就很好但多跳问答、需要交叉引用多份资料、以及先给结论再给依据的追问型对话RIG的收益非常明显。如果你做的是内部知识库问答且用户经常问为什么、和另一个方案比怎么样这类需要推理加引用的问题RIG值得一试。2. OpenRIG的架构与几个关键选型决策2.1 总体架构与数据流OpenRIG的架构没有追求复杂核心就五个模块模块职责实现方式LLM客户端与大模型交互负责分段生成适配Ollama/vLLM也可改成OpenAI兼容接口控制器解析检索标记、决定是否检索、控制最大轮数独立进程内的纯Python逻辑检索器混合召回融合BM25和向量检索结果默认ChromaDB存向量BM25用rank_bm25上下文管理器维护对话历史、已注入资料防止重复检索哈希集合 环形缓冲区输出后处理剥离检索标记输出干净文本正则替换数据流是用户问题进入上下文管理器控制器调用LLM客户端生成第一段文本然后检查是否有检索标记。如果有就进入检索流程没有就直接返回给用户。检索结果会被上下文管理器记录下来并注入到提示词里然后继续让LLM生成下一段直到没有检索标记或者达到最大轮数。这个架构本身没什么黑科技真正的细节都在怎么准确判断该不该检索和怎么让检索结果不污染生成风格上面后面专门讲。2.2 选型决策为什么没有直接用Agent框架第一个选型问题是到底自己写控制器还是直接用LangChain或LlamaIndex这类Agent框架里的ReAct/Tool Calling功能我当时把Agent框架文档翻了一遍最后决定自己写控制器。原因有两点一是控制粒度。LangChain的Tool Calling虽然也能实现生成中调用工具但它在设计上更偏向Agent自主决定调用哪个工具并把工具结果当作下一步输入。而RIG的诉求更单纯不是要多工具编排而是要让LLM在生成自然语言回答的过程中随时补充外部知识。工具调用的输出是结构化指令RIG的诉求是把检索结果融合进已有上下文继续写这两者对控制器的要求不一样。二是调试成本。框架层会帮你处理很多隐性问题但出了问题你很难判断是框架的逻辑问题还是你的prompt问题。自己写控制器整个流程都在你眼皮底下哪里触发错了、哪次检索失败了看一眼日志就清楚。当然如果你本身已经在LangChain体系里跑得很熟也可以在框架基础上改造。但从我的实际体验看RIG的核心逻辑加起来不到200行自己维护并不重。2.3 检索时机判定的三种做法这是OpenRIG里最核心的部分。我试过三种判定方式各有各的适用场景第一种让LLM自己输出检索标记。在系统提示词里明确告诉模型当你在生成中遇到需要外部资料才能回答的问题时在句子对应位置输出retrieve需要查询的内容/retrieve。控制器在每次分段生成后扫描这个标记。这种方式最直观我的实测触发准确率大概能做到八成以上前提是模型指令遵循能力不能太差。7B级别的模型在简单场景下也够用。第二种用轻量分类器判断当前段是否需要检索。把已生成的文本输入一个小模型输出二分类需要检索/不需要检索。好处是不依赖模型是否会输出标记坏处是要额外维护一个分类模型还要构造训练数据。我实验后发现在通用问题上性价比不高但在垂直领域比如法律、医学可能值得做。第三种基于困惑度或关键词规则。比如生成文本里出现了我不确定根据相关信息等线索词就触发检索。这个方式最轻量但误触发率很高我测下来不建议单独使用。OpenRIG默认走第一种因为最简单也最可控。关键是要在系统提示词里把触发规则说清楚只有当前上下文确实无法回答时才输出检索标记已有足够资料时不要触发。否则模型会把检索标记当成一种仪式每句话都来一个系统就废了。2.4 query构造与上下文注入模板这里有两个细节很多人会忽略但它们直接影响检索质量。第一个是检索query的构造。检索标记里写的内容通常是模棱两可的比如模型可能在生成长文时写retrieve相关的合同条款/retrieve这个短语本身没有区分度。如果直接拿它去检索召回质量会很差。OpenRIG的做法是把用户原始的完整问题、当前已生成的段落、以及检索标记里的短语拼在一起再让LLM生成一条独立的搜索query。这个过程会多一次模型调用但换来的是检索质量明显提升。实际跑下来多花的那点时间很值。第二个是上下文注入的模板。检索到的资料不能直接原样塞进上下文需要包一个提示词壳子。OpenRIG默认的模板是以下是参考资料可以从中获取事实信息。回答时基于已有事实不要直接大段引用参考资料原文也不要改变回答风格。参考资料中与问题无关的内容可以忽略。 【参考1】... 【参考2】...这个模板的目的是告诉模型资料只是事实来源不是语言范本。如果省略这层提示模型很容易模仿资料里的文风甚至出现根据上述法规第X条……之类的官腔这个问题我会在踩坑章节详细展开。3. 把OpenRIG跑通环境、代码骨架与一个完整Demo3.1 环境准备与目录规划开始之前先说一下运行环境。OpenRIG本身对硬件要求不高因为它不在本地做训练只做推理和检索。我平时在用的是一台带6GB显存显卡的Linux主机跑Qwen2.5-7B-Instruct量化版没有问题。如果你没有GPU用CPU跑小一点的模型比如3B/4B的量化版也能把流程跑通只是生成速度会慢不少。依赖方面我建议用venv或conda隔离环境。需要装的东西有pip install fastapi uvicorn chromadb rank-bm25 pyyaml requests模型服务我用的是Ollama装好后拉一个模型ollama pull qwen2.5:7b如果你已经有vLLM或者OpenAI兼容接口也可以直接用OpenRIG的LLM客户端可以配model_type来区分本地Ollama和通用OpenAI格式接口。目录结构建议这样规划openrig/ ├── app.py # HTTP服务入口 ├── config.yaml # 全局配置 ├── llm_client.py # LLM调用封装 ├── controller.py # RIG控制器主循环 ├── retrieval.py # 混合检索实现 ├── context_manager.py # 上下文与去重管理 ├── prompt_templates.py # 系统提示词与注入模板 └── examples/ └── demo_question.md # 测试用例这个结构够用不需要过早拆太细等逻辑跑通后再按需重构。3.2 控制器核心代码控制器是整个系统的核心。它能串起生成-标记-检索-继续生成的完整循环。下面是一个简化版的核心逻辑方便看出整体脉络import re from typing import Iterator RETRIEVE_PATTERN re.compile(rretrieve(.*?)/retrieve, re.S) def run_rig(question: str, llm_client, retriever, ctx_manager, config): ctx_manager.new_turn(question) for round_idx in range(config[max_retrieval_rounds] 1): segment llm_client.generate(ctx_manager.build_prompt()) yield segment markers RETRIEVE_PATTERN.findall(segment) if not markers: return for marker_text in markers: query llm_client.build_search_query( original_questionquestion, current_textsegment, intentmarker_text.strip(), ) docs retriever.hybrid_search(query, top_kconfig[top_k]) docs ctx_manager.filter_new_docs(docs) ctx_manager.inject_docs(docs)这段代码有几个地方值得说一下yield segment控制器不是一次性生成完而是每生成一段就往外抛一段。这样外层可以把已经生成的内容实时推给用户减少等待感。build_search_query对应上一节说的query构造。实际调用里它会额外走一次LLM让模型基于原始问题和当前文本生成搜索词。filter_new_docs依赖上下文管理器里的哈希集合已经注入过的文档不会再注入第二次避免多轮检索后上下文里堆积重复内容。实际的controller.py里还包括检索标记剥离、日志输出、异常兜底等逻辑但主循环就是这样不复杂。3.3 配置文件详解OpenRIG的配置集中在config.yaml里我贴一份关键配置model: name: qwen2.5:7b type: ollama # 可选: ollama / openai base_url: http://localhost:11434 temperature: 0.7 # 检索标记触发频率与temperature正相关建议0.6-0.8 retrieval: top_k: 5 use_bm25: true use_vector: true vector_store_path: ./data/chroma embedding_model: bge-small-zh-v1.5 controller: max_retrieval_rounds: 3 # 防止无限检索 speak_retrieval_marker: false # 是否把检索过程以文本形式暴露给用户 prompt: system_file: ./prompt_templates/system.txt inject_template: ./prompt_templates/inject.txtmax_retrieval_rounds是安全阀。即使模型一直在触发检索标记最多也只能检索这么多轮防止问答变成无底洞。temperature对检索标记的影响比较微妙温度越低模型越倾向于保守可能该触发时不触发温度越高模型越容易过度触发。我试下来0.7是个比较均衡的值。3.4 跑一个多跳问答Demo配置好之后启动服务python app.py --config config.yaml然后模拟一个多跳问答来验证。我拿这个例子测过MIT协议的开源项目能商用吗如果我要把项目里的代码嵌入到商业软件中需要注意什么这个问题如果直接丢给传统RAG第一轮检索基本都是返回MIT协议定义相关的文档很难同时覆盖商用注意事项这类后半段需要的资料。用OpenRIG跑生成的日志会呈现这样的节奏[round 0] segment: MIT协议是一种宽松的开源许可证它允许使用者自由使用、修改、复制和分发软件包括商业用途。但在此处模型输出检索标记表示需要外部知识补充商用细节 [retrieve] query: MIT协议商用时的注意事项 版权声明 免责条款 [retrieve] hit1: MIT许可证要求所有副本保留原版权声明... [retrieve] hit2: 在商业软件中使用MIT协议代码需要在软件说明中保留原始版权信息和许可声明... [round 1] segment: 如果要把MIT协议的代码嵌入商业软件关键注意点有三条第一保留原始版权声明和许可文本第二不可以用原作者的姓名或商标误导用户第三原作者不对软件提供任何担保...此处不再触发检索生成完成这个Demo能看到RIG最核心的价值第一段回答先给出结论发现需要补充细节时自动触发检索资料注入后接着往后写。整个回答是连贯的一段话而不是结论和资料引用分裂开的碎片。4. 实测两周遇到的五个坑和完整排查过程跑通Demo只是开始真正把OpenRIG接入业务、压到真实场景里用我遇到了五个比较典型的坑。每个都经历了现象→日志→归因→修复的完整过程这里逐一展开。4.1 第一个坑检索阈值过于激进回答像复读机现象刚开始测试时几乎所有回答都会触发3轮以上的检索LLM生成的内容严重碎片化。比如问简要介绍一下X模型第一句就触发检索第二句又触发最后回答像是把几段检索结果拼起来的复读机。排查我先看控制器日志统计每轮对话的检索触发次数分布。发现大部分问题都触发了至少两次检索而正常预期应该是一次以下。接着我单独测试了模型在无检索标记提示词下的回答发现模型本来就能直接回答这类简单问题说明问题出在提示词对触发条件的描述太宽松。归因我在系统提示词里写的是如果生成过程中需要更多信息可以输出检索标记。这个可以给了模型太大的自由度让它把检索当成了一种默认行为而不是应急手段。修复把提示词改成只有当前上下文中确实缺少回答所需的事实信息时才输出检索标记已有知识足以回答时直接生成回答不要输出检索标记。同时把temperature从0.9调回0.7。修改后简单问题的检索触发率从80%降到15%左右。验证重跑同样的测试集检索触发次数明显下降回答完整度不降反升因为模型不再频繁被打断。这个坑教会我一件事RIG的触发条件一定要在提示词里写清楚默认不检索缺信息才检索。模型本身没有判断业务场景的能力它只遵守你给的规则。4.2 第二个坑注入原文污染了模型文风现象检索触发正常了但生成的回答开始用奇怪的句式。例如模型突然用根据资料显示众所周知这类在原始对话中从未出现过的连接词还喜欢把资料里的大段条款直接复述出来答非所问。排查我把一次完整回答的生成日志拉出来把每次注入的参考资料和对应输出段落逐个对照发现每次输出出现文风突变的地方都紧随在同一条资料注入之后。问题很清晰模型在模仿参考资料的语言风格。归因模型中有一个很强的倾向——看到参考资料时会倾向于引用原文而不是转述。如果注入模板只写了以下是参考资料而没有明确使用方式模型就会把资料当成权威文本去模仿或抄录。修复我改了两处。一是修改注入模板强调参考资料仅用于提取事实不要直接引用原文不要模仿资料的语言风格。二是在注入前增加了一个摘要步骤把每条资料压缩成3-5句话的要点长度控制在150字以内而不是把整个检索出来的文档片段都塞进去。验证修改后输出风格稳定了很多模型开始用自己的话组织信息只在必要时提到关键数据。摘要步骤多了0.3秒左右的延迟但输出质量提升明显。4.3 第三个坑query构造失败搜出一堆无关内容现象多轮问答中经常发生模型触发了检索但检索回来的资料完全无关的情况。系统甚至会把用户上一个问题的内容搜回来。排查我在检索器入口打了详细日志把build_search_query的结果单独打印。结果发现query很长动辄一两百个字还夹杂着模型生成的半截句子。比如用户问MIT协议能商用吗生成的query是用户询问MIT协议能否商用MIT协议是一种宽松的许可证允许使用、修改、复制需要保留版权声明但需要注意……——这种含糊文本拿去向量检索维度被稀释召回质量自然差。归因query构造函数的设计有问题。它把原始问题当前段落检索标记全部拼接后直接丢给模型模型不知道该输出什么就照着原文格式继续写了一段结果是一篇小作文不是一条搜索query。修复修改build_search_query的指令明确要求模型输出一条简短的中文搜索关键词不超过20字不要带解释不要带标点符号连续句。同时在返回时加一道后处理只取第一行长度超过30字直接截断。验证改完之后query平均长度从120字降到18字左右召回结果的相关性明显提升。这个坑也说明一个问题RIG里的query构造和普通RAG的query改写不是一回事。普通RAG是一次性改写RIG是在生成过程中反复改写指令必须更明确否则模型很容易把上下文里的冗余信息带进query。4.4 第四个坑同一份资料被反复检索上下文越来越长现象跑三轮以上检索的问题上下文里出现了大量重复片段。我翻了注入日志发现同一份文档在第二轮和第三轮被注入两次导致上下文长度膨胀模型反而开始遗忘早前的关键信息。排查查看上下文管理器的记录发现filter_new_docs只对完全相同的文档ID去重。但很多文档在不同轮次被检索到时返回的只是同一个文档的不同切片ID不同内容大段重合。去重逻辑失效了。归因去重粒度不对。RIG多轮检索的文档往往来自同一个源文件向量库返回的可能是相邻段落它们之间有重复内容。只按ID去重挡不住这种语义层面的重复。修复把去重逻辑改成两层先按文档ID去掉完全相同的再对内容计算简单哈希把相似度超过阈值我用的方法是文本长度首尾句哈希的片段丢弃。上下文管理器里维护一个已注入片段哈希集合每次注入前比对。验证修复后三轮检索的文档重复率从40%降到5%左右上下文长度可控模型在长对话里的表现也稳定了。4.5 第五个坑延迟飙升差点放弃RIG现象RIG响应时间比纯RAG高了一大截。一个完整问答要四五次模型调用初始生成、query构造、检索后继续生成加上向量检索和摘要步骤最长的请求耗时超过20秒几乎不可用。排查我把一次请求的时间拆开统计环节耗时占比初始生成30%query构造LLM调用20%向量检索15%摘要压缩LLM调用20%后续生成15%大头不是检索而是多次LLM调用。尤其是query构造和摘要压缩这两个环节分别都要多调一次模型。归因我在设计时为了交互体验把每个环节都做成了同步阻塞调用。query构造要等模型返回摘要要等模型返回整个过程串行自然慢。修复做了三个优化。第一query构造不再每次都调用LLM而是优先使用检索标记中的短语只有短语过短时才调用模型补全第二摘要步骤改成异步执行先注入原文的截断版本摘要结果在背景生成后更新第三向量检索的embedding改为本地化不再走远程调用。验证优化后平均响应从12秒降到6秒左右。虽然还是比传统RAG慢一些但对于多跳推理类问题这个延迟是可以接受的。如果实在需要更极致的性能可以把生成模型换成更快的量化版本或者用vLLM做并行推理。5. 小样本评测结果与接下来想做的三件事5.1 小样本评测方法与结果为了验证RIG到底有没有改进效果我做了一个小样本评测。测试集用40道题分为四类单跳事实问答10道多跳推理问答10道对比类问题10道比如A和B有什么区别长上下文理解10道评测指标是回答正确率人工判断关键事实是否准确和平均延迟。结果如下类型RAG正确率RIG正确率RAG延迟RIG延迟单跳事实问答85%82%1.8s2.9s多跳推理问答55%80%3.2s5.1s对比类问题60%78%2.7s4.6s长上下文理解70%72%4.1s5.8s从这个结果能看出来单跳场景RIG没有优势反而慢了多跳和对比场景正确率提升明显多跳提升了25个百分点左右。长上下文场景两者差别不大因为这类问题的知识来源更多在上下文内部外部检索帮助有限。我的结论是RIG适合用来替代需要多步取证的问答场景而不是所有场景的通用方案。如果你只是做FAQ问答传统RAG又简单又快没必要上RIG。5.2 接下来想做的三件事OpenRIG现在只是实验版本后续我有三个明确方向第一个方向是替换检索标记判定机制。目前依赖LLM输出标记虽然简单但遇到指令遵循能力弱的模型就不稳定。我想训练一个轻量的二分类模型来充当控制器专门判断当前上下文是否缺失信息这样不依赖模型的标记输出能力。第二个方向是多路检索并发。一轮检索请求到不同数据源本地知识库、网页搜索API、数据库等全部返回后再统一注入上下文。这能提升单一检索器的召回率但对上下文管理器的去重和排序要求也更高。第三个方向是开放评估集。现在40道题的测试集规模太小说服力有限。我打算整理一个开源的多跳检索问答评估集统一格式让想试RIG的人可以直接跑同一套测试对比。写在最后OpenRIG这个项目做下来我的一个深刻体会是检索增强生成这条路可以走的方向比大多数教程展示的要多得多。传统RAG把它做成了一个固定的Pipeline但在实际业务里用户的真实需求往往是动态的、分步的、需要临时补充信息的。RIG把检索决策交还给生成过程本身虽然多了几次模型调用却换来了更强的推理能力和更贴近真实交互的问答体验。如果你正准备把RAG管线升级到RIG我的建议是不要一上来就推翻现有系统先在多跳问答这类最能体现RIG优势的场景里跑一个对照测试看看正确率提升是否值得多付出的那几秒延迟。如果想动手试也可以直接拿OpenRIG的这套流程改核心代码量不大翻车成本很低。
返回列表