1. 项目概述当RAG不再“照本宣科”如果你最近在折腾大语言模型应用尤其是检索增强生成RAG系统那你肯定对“幻觉”这个词深恶痛绝。模型有时候会一本正经地胡说八道把检索到的文档内容改得面目全非或者凭空捏造出文档里根本没有的信息。为了解决这个问题一个朴素但有效的思路是让模型“原封不动”地引用原文。这就是KRLabsOrg/verbatim-rag这个项目试图解决的核心痛点——实现精确的、逐字逐句的引用生成。verbatim-rag直译过来就是“逐字逐句的RAG”。它不是一个全新的RAG框架而更像是一个针对现有RAG流程的“增强插件”或“最佳实践集合”。它的目标不是替换你现有的向量数据库或检索器而是在你检索到相关文档片段后指导大模型如何更忠实、更可控地使用这些片段来生成答案。简单来说它要确保模型输出的每一句声称来自源文档的话都能在原文中找到几乎一模一样的对应极大减少“添油加醋”和“张冠李戴”的情况。这个项目特别适合那些对事实准确性要求极高的场景比如法律文书分析、医疗问答、金融报告解读、技术文档支持或者任何你需要模型给出有据可查、可追溯答案的领域。对于开发者而言它提供了一套方法论和潜在的实现参考帮助你构建更可靠、更值得信赖的AI应用。接下来我们就深入拆解它的设计思路、关键技术点以及如何将它融入到你自己的项目中。2. 核心设计思路与方案选型2.1 问题根源为什么标准RAG会“编故事”要理解verbatim-rag的价值得先看看标准RAG流程的“阿喀琉斯之踵”。一个典型的RAG流程是用户提问 - 检索相关文档片段 - 将片段和问题一起喂给大模型 - 模型生成答案。问题就出在最后一步大模型是一个生成式模型它的训练目标是生成“流畅、合理”的文本而不是“精确、无误”的文本。当模型将检索到的片段作为上下文时它可能会概括性偏差模型倾向于对原文进行概括总结而不是引用。虽然总结可能更简洁但在概括过程中关键细节、精确数字或特定表述可能被丢失或修改。融合性幻觉模型可能将多个检索片段的信息融合成一句新的陈述这句新陈述单独看逻辑通顺但其中部分信息在任何一个源片段中都不完全成立。补充性幻觉为了让答案更完整模型会基于其内部知识可能过时或不准确补充上下文未提及的信息而这些补充内容未经核实。verbatim-rag的思路是通过设计特定的提示词、输出格式要求和后处理逻辑强行约束模型的生成行为使其模式从“自由创作”转变为“精准引用”。2.2 方案核心约束生成与精确锚定项目的核心方案可以概括为“约束生成”与“精确锚定”两大部分。约束生成主要体现在系统提示词的设计上。普通的RAG提示词可能是“请根据以下上下文回答问题...”。而verbatim-rag风格的提示词会变得非常“强硬”和“具体”例如“你必须仅使用提供的上下文来回答问题。”“如果答案在上下文中请直接引用原文中的完整句子不要进行任何改写、总结或解释。”“在引用的每个句子后用【页码行号】或类似的标记注明其确切出处。”“如果上下文不足以回答问题请明确说‘根据提供的信息无法回答该问题’不要尝试推断。”这种提示词将模型的角色从“聪明的助手”转变为“严谨的文书员”大幅降低了它自由发挥的倾向。精确锚定则涉及更底层的技术。如何让模型知道“引用”的具体边界一种高级做法是结合检索结果的元数据如字符偏移量、句子索引和模型的 token 级注意力机制。例如项目可能会探讨或实现分块与索引策略采用重叠分块、句子分割或语义分块并为每个块赋予精确的全局定位标识符如doc1_sent5_to_sent10。引用标记注入在提供给模型的上下文中不仅在内容上也在结构上嵌入可追踪的标记。例如将每个句子或段落用特殊的 XML 标签包裹如span id“ref_001”...原文句子.../span然后要求模型在输出时使用这些标签。后处理验证模型生成带有引用标记的答案后系统可以解析这些标记回溯到原始文档比对被引用的内容是否与原文一致从而提供一个置信度评分或进行二次校验。2.3 技术选型考量提示词工程 vs. 微调 vs. 智能体框架实现“逐字引用”有多种技术路径verbatim-rag项目更侧重于提示词工程和流程设计这是一个务实的选择。提示词工程本项目重点优点是零训练成本、快速部署、适用于任何黑盒API模型如GPT-4 Claude 文心一言等。通过精心设计的提示词、少样本示例Few-shot和输出格式规定如要求JSON格式输出包含answer和citations字段就能在相当程度上控制模型行为。这是项目立即可用的部分。模型微调可以训练一个专门的“引用生成”模型。这需要构建高质量的问题 上下文 带精确引用的答案三元组数据集。效果可能最好但成本高、周期长且针对特定模型灵活性差。verbatim-rag可能将其作为高级或未来扩展方向。智能体Agent框架可以设计一个智能体其工具包括“检索文档”、“定位句子”、“拼接答案”。智能体通过多次调用工具来组合答案。这种方式更模块化但推理延迟高、流程复杂。项目可能吸收了这种“分步执行”的思想但用更轻量的提示词来模拟。对于绝大多数团队从提示词工程入手是性价比最高的。verbatim-rag提供的价值正是总结了哪些提示词模板、哪些上下文格式化方法、哪些后处理规则最有效形成了一个“最佳实践库”。3. 关键组件与实现细节拆解虽然KRLabsOrg/verbatim-rag的具体代码实现需要查看其仓库但我们可以根据其目标推导并详细拆解一个高可用的verbatim-rag系统应包含的关键组件及其实现细节。3.1 文档预处理与索引策略精确引用的基础是精确检索和精确定位。传统的按固定长度分块如512个字符在这里可能不够用。智能分块Chunking句子感知分块优先在句子边界处进行分割。使用像spaCy、nltk或LangChain的RecursiveCharacterTextSplitter设置separators为[\n\n, \n, . , ? , ! ]可以更好地保持句子完整性。这对于后续按句引用至关重要。重叠分块设置合理的重叠度如50-100个字符。这确保了即使答案关键信息恰好落在分块边缘也能被完整检索到避免引用时上下文不足。元数据注入为每一个文本块chunk记录丰富的元数据至少包括文档ID、起始字符偏移量、结束字符偏移量、所在页码、块内句子索引列表。这些是后续引用的“坐标”。索引与检索双路检索结合密集向量检索和稀疏检索如BM25。向量检索擅长语义匹配BM25擅长精确关键词匹配。将两者的结果融合如 Reciprocal Rank Fusion可以提高召回关键句子的概率。检索后重排Re-ranking使用一个轻量级的交叉编码器模型如bge-reranker对初步检索到的Top-K个片段进行相关性重排。这能确保最相关、最可能包含答案的片段排在前面提供给大模型的上下文质量更高。注意分块大小需要权衡。块太小可能破坏语义完整性块太大会引入无关噪声增加模型处理负担和成本。对于逐字引用倾向于使用较小的、以句子为单位的分块并依赖重叠来保证上下文连贯。3.2 系统提示词与上下文格式化模板这是项目的灵魂所在。一个强大的提示词模板通常包含以下几个部分角色定义明确、强硬地定义模型角色。“你是一个精确的文档分析助手必须严格依据提供的材料作答。”核心指令引用要求“答案中的每一个事实性陈述都必须来自下方提供的‘上下文’。引用时必须复制上下文中完整的原句不得进行任何形式的改写、缩写、概括或意译。”出处标注“在每个引用句子的末尾用方括号标注其出处例如【文档A 第3段】或【来源1 句子2】。你使用的标注格式必须与我提供的示例完全一致。”诚实性声明“如果上下文信息不足以回答全部或部分问题你必须明确声明‘根据所提供信息无法确定...’并停止编造。”输出格式规范要求模型以结构化格式输出这是实现自动化处理的关键。例如{ answer: 完整的答案文本其中包含【引用标记】。, citations: [ {text: 被引用的原句, source: 文档ID: 块ID: 句子索引}, ... ] }少样本示例Few-shot Examples提供1-2个完整的“问题-上下文-答案”示例直观展示期望的行为和格式。这是让模型快速“上手”的最有效方法。上下文格式化同样重要。提供给模型的上下文不应是杂乱无章的文本堆砌。建议格式## 上下文开始 ## [文档片段1 ID: doc1_chunk2] 内容...具体的文本内容... [文档片段2 ID: doc2_chunk5] 内容...具体的文本内容... ## 上下文结束 ##清晰的边界和ID标识有助于模型理解和引用。3.3 引用解析与后处理验证流程模型生成答案后工作并未结束。一个健壮的系统需要解析和验证引用。引用解析编写一个解析器从模型输出的结构化字段如citations或非结构化的答案文本中提取出引用标记如【doc1_chunk2】和对应的被引用文本。原文比对根据引用标记中的定位信息如文档ID、块ID从原始文档存储中取出对应的原文文本。相似度验证计算模型声称引用的句子与原文对应句子的相似度。可以使用简单的字符串匹配精确匹配或模糊匹配也可以使用更鲁棒的文本相似度模型如sentence-transformers。设定一个阈值如0.95的余弦相似度低于此阈值则标记为“引用不准确”或“疑似幻觉”。结果呈现对于最终用户可以将答案中的引用部分高亮显示并悬停或点击后显示原文片段提供极强的可追溯性和可信度。这个后处理流程是确保“逐字”承诺落地的最后一道保险也能用于监控系统的幻觉率持续优化提示词和流程。4. 实战集成构建你自己的Verbatim RAG管道让我们抛开抽象概念动手搭建一个简易但核心的verbatim-rag风格问答系统。我们将使用LangChain和OpenAI API来演示但思路适用于任何技术栈。4.1 环境准备与文档加载首先安装必要库并加载你的文档。假设我们有一份PDF格式的技术白皮书。pip install langchain langchain-openai pypdf chromadb tiktokenfrom langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings, ChatOpenAI import json # 1. 加载文档 loader PyPDFLoader(你的技术白皮书.pdf) raw_documents loader.load() # 2. 智能分块与注入元数据 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 较小的块便于精确引用 chunk_overlap50, separators[\n\n, \n, . , ? , ! ], # 句子感知分割 length_functionlen, ) documents [] for i, doc in enumerate(raw_documents): # 假设原始文档有页码 source_page doc.metadata.get(page, 0) 1 chunks text_splitter.split_text(doc.page_content) for j, chunk in enumerate(chunks): # 为每个块创建丰富的元数据 new_doc Document( page_contentchunk, metadata{ source: 技术白皮书.pdf, page: source_page, chunk_id: fdoc_{i}_chunk_{j}, total_chunks: len(chunks) } ) documents.append(new_doc)4.2 构建检索器与定义提示模板接下来创建向量库并定义我们核心的verbatim-rag提示词。# 3. 创建向量存储 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore Chroma.from_documents(documents, embeddings, collection_nameverbatim_rag_demo) retriever vectorstore.as_retriever(search_kwargs{k: 4}) # 检索4个相关片段 # 4. 定义强大的Verbatim提示模板 from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate system_template 你是一个严谨的技术文档分析专家。你的任务是根据用户提供的“上下文”来回答问题。 请严格遵守以下规则 1. 你的答案必须完全基于提供的“上下文”。禁止使用外部知识。 2. 答案中的每一个事实、数据或结论都必须从“上下文”中直接引用完整的原句。 3. 在每一个引用的原句后面立即用【来源{source} 块{chunk_id}】的格式标注其精确出处。{source}和{chunk_id}来自上下文每个片段的元数据。 4. 如果上下文没有提供足够信息来回答问题请直接说“根据所提供的上下文无法回答此问题。” 5. 请以清晰、有条理的方式组织答案将多个引用有机整合。 上下文如下 {context} human_template 问题{question} chat_prompt ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(system_template), HumanMessagePromptTemplate.from_template(human_template) ]) # 5. 定义输出解析器期望结构化输出以JSON为例 from langchain.output_parsers import StructuredOutputParser, ResponseSchema from langchain.schema import OutputParserException response_schemas [ ResponseSchema(nameanswer, description包含引用标注的完整答案), ResponseSchema(nameverified_citations, description一个字典列表每个字典包含‘text’被引原句和‘source’来源信息), ] output_parser StructuredOutputParser.from_response_schemas(response_schemas) format_instructions output_parser.get_format_instructions() # 将格式指令加入到系统提示中更优做法 final_system_template system_template \n\n请以如下JSON格式输出\n format_instructions chat_prompt ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(final_system_template), HumanMessagePromptTemplate.from_template(human_template) ])4.3 组装链式调用与生成答案现在我们将检索、提示、模型调用和输出解析组装起来。# 6. 组装RAG链 llm ChatOpenAI(modelgpt-4-turbo-preview, temperature0) # temperature设为0减少随机性 from langchain.schema.runnable import RunnablePassthrough def format_docs(docs): 将检索到的文档格式化为提示词中的上下文字符串并携带元数据 formatted [] for doc in docs: # 将元数据也格式化到上下文中便于模型引用 source_info f来源{doc.metadata.get(source, N/A)}, 块ID{doc.metadata.get(chunk_id, N/A)} formatted.append(f[{source_info}]\n{doc.page_content}\n) return \n.join(formatted) rag_chain ( {context: lambda x: format_docs(retriever.get_relevant_documents(x[question])), question: lambda x: x[question]} | chat_prompt | llm ) # 7. 提问并获取答案 question 这份白皮书中提到的主要技术挑战是什么 result rag_chain.invoke({question: question}) try: parsed_output output_parser.parse(result.content) print(答案) print(parsed_output[answer]) print(\n验证引用) for cit in parsed_output[verified_citations]: print(f- 引文{cit[text][:100]}... | 来源{cit[source]}) except OutputParserException: # 如果模型没有返回标准JSON降级处理直接打印内容 print(模型返回非标准格式原始答案) print(result.content)这个流程实现了verbatim-rag的核心检索、格式化上下文、通过强约束提示词调用模型、解析结构化输出。你可以在此基础上增加前面提到的重排、后处理验证等模块使其更加健壮。5. 常见挑战、优化策略与避坑指南在实际操作中你会遇到各种问题。以下是一些常见挑战及应对策略这些是文档里不会写的“踩坑”经验。5.1 挑战一模型“不听话”依然在概括或改写即使使用了强提示词模型特别是能力稍弱的模型有时仍会“偷懒”进行概括。优化策略示例的力量在提示词中提供1-2个完美的“Few-shot”例子。例子要小但精准展示从问题到带精确引用标注的答案的全过程。这比任何文字指令都有效。分步指令将复杂指令拆解。先让模型“找出所有相关句子”再让模型“用这些句子组合成答案”。可以通过LangChain的SequentialChain或使用支持函数调用的模型来模拟此过程。模型升级如果预算允许使用能力更强的模型如GPT-4 Claude 3 Opus。它们在遵循复杂指令方面显著优于小模型。惩罚参数某些API如OpenAI支持在请求中设置frequency_penalty和presence_penalty。适当增加这些值如到0.5-1.0可以降低模型重复常见短语和进行自由发挥的倾向。5.2 挑战二检索质量不高导致“无米之炊”如果检索到的片段本身不包含答案再强的提示词也逼不出正确答案。优化策略查询扩展/重写在检索前对原始用户问题进行扩展或重写。例如使用大模型生成该问题的2-3个不同问法或提取关键词进行同义词扩展然后将所有变体一起用于检索合并结果。混合检索如前所述务必结合密集向量检索和稀疏检索关键词。LangChain的EnsembleRetriever可以方便地实现。小颗粒度检索大颗粒度引用检索时使用小分块如单句或短段落以提高召回率。但在提供给模型上下文时可以将相邻的相关小分块合并成一个更大的上下文块为模型提供更完整的背景信息避免引用断章取义。迭代检索首次检索后让模型判断信息是否足够。如果不够让模型基于已有信息生成一个更精准的搜索查询进行二次检索。5.3 挑战三引用格式混乱难以自动化解析模型可能不严格按照你指定的格式输出引用标记给后处理带来麻烦。优化策略结构化输出是王道尽一切可能要求模型输出结构化数据JSON XML。OpenAI的response_format参数如{“type”: “json_object”}能极大提升模型输出JSON的稳定性。LangChain的PydanticOutputParser是更优雅的解决方案。提供格式模板在提示词中不仅说明格式直接给出一个空的模板框架让模型填充。例如“请按此JSON结构输出{\answer\: \\, \citations\: [{\text\: \\, \source\: \\}]}”。后处理容错编写解析器时加入正则表达式匹配等容错逻辑尝试从非结构化的文本中提取出类似【来源...】的标记。5.4 挑战四处理长文档与多轮对话当文档很长或问题涉及多跳推理时一次性检索所有相关内容可能不现实。优化策略摘要索引层为长文档建立一个摘要层或大纲层。先检索到相关章节再深入该章节进行细粒度检索。智能体Agent模式对于复杂问题可以设计一个智能体工作流。智能体首先判断问题类型然后决定是进行一次性检索还是执行“检索 - 分析 - 提出新问题 - 再检索”的迭代过程。LangChain的AgentExecutor和Tool概念非常适合构建此类流程。对话历史管理在多轮对话中需要将历史问答和引用也作为上下文的一部分。但要小心上下文窗口限制。一个策略是在每一轮只将历史上被证实有效的“引用片段”及其来源作为上下文传递下去而不是完整的对话历史。实操心得不要追求一步到位的完美系统。先从最简单的提示词基础检索开始用一个小的测试集包含一些容易诱发幻觉的问题进行评估。然后针对测试集中出现的问题如“引用不精确”、“无法回答时胡编”逐个优化你的提示词、检索策略或后处理逻辑。这种迭代式开发比一开始就设计复杂系统更高效。6. 效果评估与持续改进构建好系统后如何衡量你的verbatim-rag是否真的减少了幻觉需要建立评估体系。人工评估黄金标准构建一个测试集QA对人工判断每个答案事实准确性答案中的陈述是否与原文一致引用完整性每个事实是否都被正确引用引用精确性引用标记指向的原文是否与答案中使用的句子完全匹配诚实性对于无法回答的问题模型是否如实承认自动评估指标幻觉率可以计算“答案中无法被任何引用片段支持的句子”的比例。这需要自然语言推理模型或文本蕴含模型来判断。引用召回率标准答案中提到的关键事实有多少被你的系统引用到了引用精确率系统提供的引用有多少是真正支持其所在句子的ROUGE-L / BLEU这些文本生成指标可以用来衡量生成答案与基于原文的“标准答案”在表面词序上的相似度但需谨慎使用因为它们不直接衡量事实正确性。持续改进循环收集错误案例建立一个渠道收集生产环境中用户反馈的答案不准确案例。根因分析对每个错误案例分析是检索失败没找到相关文本、提示词失败模型没遵守指令还是后处理失败解析错误。针对性优化根据根因调整对应的模块。例如如果是检索失败就优化分块策略或检索器如果是提示词失败就增加更明确的示例或尝试不同的指令表述。KRLabsOrg/verbatim-rag项目提供的不仅是一个工具更是一种构建可信AI应用的方法论。它强调在追求模型能力的同时必须通过工程化的约束和设计来保证输出的确定性和可验证性。在当今大模型应用落地的深水区这种对“精确性”和“可控性”的追求远比单纯追求答案的“流畅性”和“创造性”更为重要。