LangChain 在生命科学与医疗保健领域的应用——“幻觉”与 RAG 系统

200 阅读56分钟

在大型语言模型(LLM)日益强大的时代,它们生成的内容常常表现得条理清晰、准确且富有洞察力,令人容易产生隐性信任。前不久,我与一位熟悉某科学领域的同事讨论相关话题,我所有的论点都“通过”了 ChatGPT 的复核——这款模型基于论坛、维基百科等多种数据训练。确实,LLM 能够生成连贯的叙述,提供详细解释,甚至模仿人类的推理过程。然而,这种信任可能会产生误导。随着对其回答的深入挖掘,你会发现偶尔的错误乃至完全捏造的内容。事实上,尽管能力强大,LLM 有时会混淆事实,虚构从未发生的事件,本质上就是在“幻觉”。

幻觉的产生源于 AI 模型基于概率分布进行采样(第2章已讲解),而非确定性地运算。不同于人类通常对同一问题给出一致答案,AI 模型每次可能因不同答案的概率权重而产生不同回答。语言模型核心并不真正“理解”或“验证”所生成的信息,而仅是根据海量训练数据预测下一个词。对 LLM 来说,选择使用正确事实还是虚构内容,本质上就像在不同语法结构或代词间做选择。这种特性使 AI 在创意任务中表现优异,能够探索常规路径外的内容,但在需要事实准确性的应用场景中却带来严重挑战。

幻觉的原因与影响

幻觉的后果可能非常严重。例如,一家律师事务所因使用 ChatGPT 生成的虚假法律研究材料而被法院罚款,凸显了幻觉风险。理解幻觉根源对于制定有效缓解策略至关重要,因为仅凭采样过程无法完全解释为何模型会生成训练数据中似乎未曾出现的内容。

2021年,DeepMind 研究人员提出一种假说:语言模型出现幻觉,是因为它们无法区分输入数据与自身生成内容。一旦模型起始时生成了稍有偏差的表述,后续处理便将该生成内容视作事实,导致错误层层累积,形成“雪球式幻觉”或“自我欺骗”。这会使模型在本可正确回答的问题上犯错,根源在于早期错误。

基于自我欺骗假说,有两种潜在缓解技术:一是利用强化学习,帮助模型区分用户输入的提示与自身生成的标记;二是在训练数据中通过监督学习融入真实与反事实信号。此举旨在解决模型将自身输出与外部输入等同对待的问题,防止初始错误引发严重幻觉。

另一假说认为幻觉源于模型内部知识与人工标注者知识不匹配。监督微调阶段,模型被训练去模仿标注者的回答,然而标注者可能引入模型本身并不具备的知识,间接教会模型产生幻觉。理想情况下,标注者应随每个回答附上其知识来源,但实际上难以实现。

图4-1展示了模型根据如下提示生成图像时出现的类似视觉幻觉:穿西装的男子背景中有黑色汽车、三岁儿童的生日蛋糕、杯中满溢的红酒,以及红酒被倒入酒杯的场景。这些图像揭示了当前 AI 系统的根本限制:它们不具备人类意义上的“知识”。它们拥有来自训练数据的模式、相关性和统计关系,但缺乏以下能力:

  • 因果理解
    理解事物为何如此发生的能力。
  • 物理直觉
    掌握物体在现实世界中的行为方式。
  • 概念抽象
    在不同上下文间灵活运用概念的能力。

image.png

当大型语言模型(LLM)生成的文本或图像看起来错误时,其实它们只是根据统计模式做出最佳猜测,并没有真正理解所涉及的概念。例如,脸上多出一条腿、一个手指或一颗雀斑,源于模型对解剖学知识的掌握以及缺乏物理直觉。AI 难以灵活运用“蜡烛数量通常代表庆祝年龄”这一概念,体现出其概念抽象能力不足。AI 不理解某些分子结构为何存在或不存在(化学键合规则的因果理解),也无法正确地在不同情境中应用化学概念。当我们看一只酒杯,能直观理解其透明度、容量、体积以及液体在容器中的行为,但 LLM 并非如此,它仅识别训练数据中的模式,无法“理解”酒杯能被倒满到不同高度,或倒酒的过程长什么样。模型无法分辨真假,它努力生成最符合概率的文本续写。这种局限意味着,如果没有外部事实校验机制,LLM 提供的信息可靠性可能不稳定。

我见过很多情况,将语言模型的任何错误都称为“幻觉”,其实不然。某些错误是幻觉,但有些并非如此,主要区别在于它们的起因和影响。大多数情况下,数据错误表现为事实不准确或逻辑不一致,这通常源于模型对输入数据或上下文的误解或曲解,尽管模型试图准确回答,但仍会出现错误。

幻觉则指模型生成完全捏造的信息或细节,未基于输入数据或已知事实。数据错误反映的是模型处理或回忆信息的限制,幻觉则突出模型有能力生成新颖但可能虚假的内容,缺乏现实基础。下表(表4-1)列举了数据错误与幻觉的示例:

数据错误模型幻觉
LLM 引用了因训练数据有误而被撤回的研究,错误地宣称某疫苗与自闭症相关。LLM 引用了不存在的文章,称某疫苗导致自闭症。
LLM 给出了基于过时研究的糖尿病治疗指南。LLM 编造了无关的糖尿病治疗方案。
LLM 误称成年人平均血容量为7升(实际接近5升)。LLM 说成年人有5升血,但给出训练数据中不存在的比例和数字。
“阿尔伯特·爱因斯坦曾说,疯狂的定义是不断重复同一件事却期待不同结果。”(该引用广泛误传)“哈佛医学院2022年研究发现,日落时喝咖啡能提升27%认知表现。”(该研究不存在)
“根据Smith 2017年的《量子计算前沿》论文,量子霸权于2015年实现。”(论文存在但无此论断)

幻觉产生的原因多样,导致错误类型不同。以下是生命科学和医学领域可能出现的几种情况:

  • 训练数据质量
    若模型训练用的化学数据包含错误标注,模型可能建议合成与药理特性不符的化合物,源于真实属性与标注数据的差异,产生误导。
  • 时间依赖性
    训练数据可能过时,尤其是训练周期长的大型模型,导致新研究无法有效纠正旧观念。
  • 训练数据偏差
    多模态模型若主要以某一人群图片训练,可能在其他人群上表现欠佳,如以浅肤色训练的模型难以识别深肤色患者的皮肤癌。
  • 模型错误不可避免
    例如基因研究中,模型可能“幻觉”出不存在的基因互作,过拟合和遗传数据固有噪声导致伪相关。
  • 模型能力限制
    让模型设计新分子结构时,生成的新化合物可能化学不稳定或生物无活性。
  • 错误假设
    药理模型错误假设结构相似的化合物生物活性相似,导致药效预测失误。

LLM 有时被戏称为“大型谎言模型”,因其易出错。意识到这一缺陷后,科技巨头投入大量资源缓解问题,从接入外部数据库到开发复杂算法以标记潜在错误。尽管如此,完美方案尚未出现。许多公司不得不声明模型存在局限,提醒用户模型可能出错、建议复核回答等,这凸显了人工监督的必要性和对 AI 生成信息保持健康怀疑态度的重要性。

AI 幻觉对生命科学研究挑战尤为严峻,可能带来错误或误导性信息,尤其是错误或虚构引用。研究显示 GPT-3 引用的178条参考文献中,有69条不正确或不存在,28条无数字对象唯一标识(DOI)且无法通过谷歌检索到。除错误引用外,LLM 还曾伪造信息来源及作者,加剧学术研究中验证难度,如第1章所述。

新模型训练数据量更大,且支持访问网络工具,但即使引用文献准确,提取的信息也不一定正确。AI 常在错误上越陷越深,面对质疑时反而提供更多误导细节,严重影响科研输出的可靠性及对 AI 辅助科研的信任。

幻觉及其可能的解决方案

我们能采取哪些措施来生成更可靠的答案?收集高质量的训练数据是最有效的策略之一,因为使用多样且结构良好的数据集可以减少偏差、提升准确性;不过,由于大型语言模型(LLM)之所以强大,正是依赖于海量训练数据,较小的数据集则会带来不同的问题。训练时应用常见的机器学习正则化技术并设定明确的AI回答边界,有助于防止过拟合并减少幻觉。然而,最有效但代价较高的方案是引入人工审核流程以验证AI输出。模型提供商普遍采用这些方法来减少模型“撒谎”的概率。

不过,如前所述,幻觉不仅由模型能力驱动,还受数据错误影响。数据每天都在变得不准确和过时,因此微调可能不是生成最新答案的最佳方法,因为这将是一场与时间赛跑——模型在训练完成前就可能已经过时。


基础训练和使用高质量数据的微调是提升模型整体性能的必要步骤,这也是多数模型提供商一年发布多次新版本的主要原因。

集成潜在的事实核查系统,将AI生成内容与经过验证的数据库及现实知识进行对比,能够提升AI模型输出的真实性。这类机制作为二级验证层,确保输出内容符合事实。此外,这些系统可被其他LLM模拟。如果多次向同一模型提问相同问题,或向多个模型提问相同问题,便易于识别幻觉,因为模型们对正确答案一致,但对幻觉的内容则各不相同。

另一种方案是采用明确的提示工程。简单的提示策略有助于缓解幻觉,例如明确指示模型“尽可能真实回答,若不确定请说‘抱歉,我不知道’”。请求简洁回答也有益,因为生成更少的标记减少了捏造的机会。更复杂的提示及上下文构建技术,包括设计清晰、具体且富含上下文的提示,可降低幻觉出现的频率和严重度。提供详细指令并将复杂查询拆解成更小、易处理的部分,结合链式思维(chain-of-thought)和ReAct等方法,让模型逐步解释推理过程,这些在某些情况下有效,但除非结合特定工具,否则仍无法保证获得最新信息。

迄今为止,防止幻觉效果最佳的解决方案是检索增强生成(RAG)。上一章已简要介绍过RAG。总结来说,RAG是通过将语言模型与外部知识库结合来增强模型的技术。模型不再仅依赖训练时内部数据,而是整合经过筛选的知识库中的相关信息,使生成的回答更具事实基础,从而减少幻觉发生的可能性。此外,RAG系统还可结合其他减少幻觉的最佳实践,如提示工程和事实核查。


文中所说的“RAG”指的是“RAG流水线”,技术上RAG是缩写,不能直接复数化。

RAG通常通过多阶段流程运作,如图4-2所示:

  1. 索引与数据准备
    将外部文档存储并建立索引,以便基于特定查询可被检索。这一步发生在提出任何问题之前。
  2. 问题解析
    解释并优化用户的查询,以理解其意图和上下文。
  3. 路由至正确数据库/索引
    根据查询内容将问题导向适当的数据库或索引。
  4. 查询构建
    为选定的数据库或索引构造有效查询。
  5. 数据检索
    基于处理后的查询搜索并排序相关数据。
  6. 数据增强与回答生成
    将排名靠前的数据整合进LLM的提示中,结合语言能力生成连贯且相关的回答。

image.png

深入探讨 RAG 流程,同时继续利用 RAG 方法构建多个链和智能体。

检索增强生成(Retrieval-Augmented Generation)

RAG 不仅仅是一种避免幻觉的方法。它通过整合实时的多源数据,增强了大型语言模型(LLM),使其能够生成具备上下文意识和合规性的回答。这种整合使得 RAG 能够访问并结合来自多种来源的结构化和非结构化数据,从而提升 AI 生成内容的准确性和可靠性。


第10章会展示如何实现无代码的 RAG 流水线。这里讨论的概念保持一致,主要区别在于你可以使用更多预定义组件。这种方式缺乏灵活性,结果可能未达最优,但适合探索和快速开发。

首先,示例4-1展示了一个基础的 LangChain RAG 流水线。我们先从 LangChain Hub 拉取预定义的提示模板 retrieval_qa_chat_prompt,该模板作为问答聊天系统的基础。代码中,模板指示模型“仅基于上下文给出回答”。检索器通常设置为能够基于查询检索相关文档或信息的组件。

我们会用两个加载器 —— PyPDFLoader 和 WebBaseLoader —— 分别加载两个不同来源的内容:完整PDF文章“CRISPR/Cas介导的植物基因组编辑:实施十年后的重大挑战”及 PubMed Central 中的“通过 CRISPR-Cas9 技术构建抗病植物”。此外,我们会应用自定义函数清理文献引用,避免引用内容影响检索。

最终,我们将结合多个链。create_stuff_documents_chain 用于结合 LLM 与检索提示,创建文档处理链以处理尚未定义的上下文并生成连贯回复;retrieval_chain 通过 create_retrieval_chain 函数结合检索器和文档处理链,将检索到的数据整合进上下文,覆盖从检索相关文档到生成最终答案的整个流程。

# 基础 RAG 常用导入见第3章或官方 GitHub LangChain4LifeSciencesHealthcare 仓库
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_retrieval_chain
from langchain import hub
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 加载文档,链接见官方仓库
loader1 = PyPDFLoader("./file.pdf")
loader2 = WebBaseLoader("https://...")
doc1, doc2 = loader1.load(), loader2.load()
documents = doc1 + doc2

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=400, chunk_overlap=50, separators=["\n"], keep_separator=False
)
chunks_1 = text_splitter.split_documents(doc1)
chunks_2 = text_splitter.split_documents(doc2)

# 自定义函数:移除文献引用部分
def extract_text_before_references(chunks, percentage_above=0.65):
    if not chunks:
        return []
    total_text_length = sum(len(chunk.page_content) for chunk in chunks)
    last_percent_length = total_text_length * (1 - percentage_above)
    current_length = 0
    reference_index = -1

    for i, chunk in enumerate(chunks):
        current_length += len(chunk.page_content)
        if current_length > total_text_length - last_percent_length:
            if "references" in chunk.page_content.lower():
                reference_index = i
                break

    if reference_index != -1:
        return chunks[:reference_index]
    else:
        return chunks

chunks_1_no_reference = extract_text_before_references(chunks_1)
chunks_2_no_reference = extract_text_before_references(chunks_2)
chunks = chunks_1_no_reference + chunks_2_no_reference

retrieval_qa_chat_prompt = hub.pull("langchain-ai/retrieval-qa-chat")
llm = ChatOpenAI()  # 其他大多数 LLM 也适用
vectorstore = Chroma.from_documents(
    documents=chunks, embedding=OpenAIEmbeddings(model="text-embedding-3-large")
)
retriever = vectorstore.as_retriever(search_type="mmr")
combine_docs_chain = create_stuff_documents_chain(llm, retrieval_qa_chat_prompt)
retrieval_chain = create_retrieval_chain(retriever, combine_docs_chain)

基础 RAG 流水线搭建完成后,我们可以提问:“CRISPR 在番茄上的应用有哪些?CRISPR 还被应用于哪些植物?”注意检索出的前三个相似结果来自不同来源,其中第二个结果中并未包含“番茄”一词,但具体列出了潜在作物信息。最终答案基于检索到的文档片段构建:

> retrieval_chain.invoke({
    "input": "What are the applications of CRISPR in tomatoes? Which other plants is CRISPR being applied to?"
})

>>>{'input': 'What are the applications of CRISPR in tomatoes? Which other plants is CRISPR being applied to?',
   'context': [
   Document(metadata={..., 'source': 'https://pmc.ncbi.nlm.nih.gov/articles/PMC7583490/', ...},
            page_content='Many studies have reported the efficiency of CRISPR-Cas9 system to the development of disease-resistant transgenic plants. Recently, there was a report of CRISPR-edited tomato plants, which ... To date, CRISPR-Cas9 edited crops have been exempted from GMO regulations; 1. A white button mushroom ...; 2. Waxy corn ...; 3. green bristle grass ...; 4. camelina ...; 5. Soybean was tolerant to drought stress.11,117'),
   
   Document(metadata={..., 'source': 'https://www.research-collection.ethz.ch/bitstream/handle/20.500.11850/618915/1-s2.0-S1360138523001644-main.pdf', ...},
            page_content='technique, the technology has been successfully applied in close to 120 crops and model plants,\nwith reports of wide applications for as many as half of them (Table S1 in the supplemental information online...'),
   
   Document(metadata={..., 'source': 'https://www.research-collection.ethz.ch/bitstream/handle/20.500.11850/618915/1-s2.0-S1360138523001644-main.pdf', ...},
            page_content="tomato relative), Physalis pruinosa (groundcherry, an orphan crop distantly related to tomato),\n and Oryza alta (a wild tetraploid rice) by modifying 'domestication genes' via CRISPR/Cas-based technologies...")
   ],
   'answer': 'CRISPR is being applied in tomatoes to create disease-resistant transgenic plants by targeting viral coat protein and replicase genes, resulting in a remarkably low viral load and stability up to the 3rd generation. \n\nIn addition to tomatoes, CRISPR is being applied to a variety of other plants, including:\n\n1. White button mushrooms (knocked out the polyphenol oxidase gene for resistance to browning)\n2. Waxy corn (inactivated the wx1 gene for a waxy appearance)\n3. Green bristle grass (modified for delayed flowering time)\n4. Camelina (improved oil content)\n5. Soybean (tolerant to drought stress)\n6. Physalis pruinosa (groundcherry)\n7. Oryza alta (wild tetraploid rice)\n\nThese applications demonstrate the versatility of CRISPR technology in enhancing various traits across different crops.'
}

第3章介绍了提示工程及其对最终回答的影响。最基础的提示为与LLM交互提供了简单接口,随着复杂度提升,出现了少样本提示(few-shot prompting),允许模型从少量示例学习。基于基础提示工程,链式思维(chain-of-thought)、树式思维(tree-of-thought)及各种推理框架代表了模型能力的显著飞跃。这些方法鼓励更结构化和透明的推理过程,将复杂问题拆分为可管理的步骤。

RAG 技术通过外部知识整合,逐步提升模型能力。基础的 RAG 方法(见第3章)可升级并结合更高级优化(本章后续会展示),以进一步提高生成式 AI 应用的质量和多样性。模块化和智能体驱动的 RAG 方法代表了前沿,使模型能动态检索、综合并融合外部信息与自身知识。

微调有时被提及为 RAG 的替代方案,这并不准确。微调关注模型适应性,而对于动态变化的环境,持续更新模型不可行。另一方面,周期性模型更新可与 RAG 结合使用,因为训练通常能提供更贴合需求的模型。

图4-3展示了提示工程、RAG与模型微调的全景图。从左下角到右上角,复杂度和模型潜力呈指数增长。

image.png

让我们深入看看在构建 RAG 流水线时可能出现的问题。第10章将讨论系统容量、安全性以及 AI 和 RAG 流水线的安全问题,而本章重点关注影响 RAG 输出结果性能的部分。

索引与数据准备

高效的索引和数据准备对 RAG 系统至关重要,因为它们决定了外部知识库的质量和覆盖面。对大型文档集合进行索引计算成本高且资源消耗大,尤其是对于非结构化数据。第3章已简要介绍了文档加载器、分割器和向量存储,下面重点讨论我们可以在 RAG 流水线中实施的改进和可能遇到的问题。

一个最重要的问题是多模态数据处理。RAG 的一个简单且强大的理念是将用于答案生成的文档与检索时使用的参考文献分离开来。例如,我们可以为冗长文档创建一个针对向量相似度搜索优化的摘要,同时仍将完整文档提供给 LLM,以在答案合成时保留上下文。摘要可通过 LCEL 链创建:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

# 用于文档摘要的链
chain = (
    {"doc": lambda x: x.page_content}
    | ChatPromptTemplate.from_template("Summarize the following document:\n\n{doc}")
    | llm
    | StrOutputParser()
)
summaries = chain.batch(documents, {"max_concurrency": 5})

同时提供摘要和原始文档有其优势。文档块(chunks)可能只包含部分信息,例如针对查询“CRISPR 在番茄上的应用以及其他植物的应用”,我们必须信任加载的文档块能覆盖查询的所有部分。而摘要则拥有更全面的视角,包含文档中最相关的信息(可能是整篇文档或某个页面)。

结合多个向量存储不限于文本,也可广泛应用于表格或图像,提升 RAG 系统的多样性和效果。示例4-2展示了 LangChain 中多向量索引的实现,注意检索到的最相似文档实际上是摘要,提供了更有效的检索方式。

import uuid
from langchain_core.documents import Document
from langchain_core.stores import InMemoryByteStore
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain.retrievers.multi_vector import MultiVectorRetriever

store = InMemoryByteStore()
id_key = "doc_id"

# 关联文档与摘要
doc_ids = [str(uuid.uuid4()) for _ in documents]
summary_docs = [
    Document(page_content=s, metadata={id_key: "sum_" + doc_ids[i]})
    for i, s in enumerate(summaries)
]

vectorstore.add_documents(summary_docs)
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
    search_type="mmr"
)
retriever.docstore.mset(list(zip(doc_ids, documents)))

query = """What are the applications of CRISPR in tomatoes? Which other plants is CRISPR being applied to?"""
similar_docs = retriever.vectorstore.similarity_search(query)

> similar_docs[0]
>>> [Document(metadata={'doc_id': 'sum_afb01157-a30e-425e-88ea-555893f11d91'},
page_content='The document is a compilation of references related to advancements in plant genome editing, particularly using CRISPR-Cas9 technology. It highlights various studies that demonstrate the application of CRISPR-Cas9 for targeted mutagenesis, gene editing, and transformation in a range of plant species, including maize, tomato, soybean, wheat, and others...'),

另一种保持摘要与原始文档并行的策略是树形递归抽象处理(RAPTOR) 。该方法通过层次化文档抽象树整合检索策略,增强长上下文 LLM 的能力。文档经过递归嵌入、聚类和摘要,生成高层次的综合摘要。RAPTOR 解决了长上下文 LLM 中的延迟和成本问题,允许跨大文档高效检索,避免大量切分。通过同时嵌入原始文档和摘要,RAPTOR 确保了稳健且具有上下文相关性的回答,是需要详细综合知识检索应用的有效工具。

此外,ColBERT(基于 BERT 的上下文晚期交互)和 RAGatouille 是提升 RAG 系统索引部分的先进技术。常规 RAG 将文档和查询嵌入密集向量,计算相似度检索相关文档。ColBERT 采用 BERT 对文档和查询进行高维向量编码,但保留每个词的 token 级别嵌入,不将其合并成单一向量。然后通过“晚期交互”计算相关度,即查询的 token 嵌入与文档的 token 嵌入相互作用,使得匹配更加细致、上下文敏感,从而获得更精准的检索结果。RAGatouille 则结合了 ColBERT 检索与 LLM 生成,前者负责寻找相关文档,后者生成连贯答案,这些方法共同提升了 RAG 的准确性和上下文丰富度。

如果你在数据索引上遇到问题,推荐可视化数据。所有文档块的嵌入距离(包括查询与生成答案间的距离)可以用直方图绘制。另一个常用方法是绘制 t-SNE 图,观察文档块、文档与来源的分布模式。图4-4展示了相关示例。

image.png

由于代码较长,完整代码已托管在 LangChain4LifeSciencesHealthcare 仓库中。

查询翻译与理解

准确理解用户查询是 RAG 系统中的基础步骤,因为它决定了后续的检索和生成流程。然而,每个人的表达方式不同,系统误解查询意图或上下文并不罕见,这可能导致无关或错误的回答。本节讨论一些常见问题及其解决方法。

不完整和未完成的回答

该问题表现为回答内容不全,虽然相关信息存在且可获取。示例包括:

  • 查询某基因功能时,仅返回名称和基本描述,未解释其在生理中的作用;
  • 问“有丝分裂的阶段有哪些?”却只得到“有丝分裂是细胞分裂的过程”;
  • 问“X 的沸点是多少?”只得到“X 的沸点会有所不同,取决于……”;

可能的解决方案是改进查询处理,使其更深入、更全面。一个方案是“假设性文档嵌入(HyDE)”,即让 LLM 针对查询构造假设性回答,并对查询与假设回答分别生成嵌入向量。然后根据生成的向量检索最相似文档(示例 4-3)。如果提示词过长且上下文较多,建议考虑提示压缩。

from langchain.chains import HypotheticalDocumentEmbedder
from langchain_openai import OpenAIEmbeddings

base_embeddings = OpenAIEmbeddings()
hyde_embeddings = HypotheticalDocumentEmbedder.from_llm(llm, base_embeddings)

vectorstore = Chroma.from_documents(
    documents=..., embedding=hyde_embeddings, collection_name=...
)
retriever = vectorstore.as_retriever()

用户查询一点变化,结果大不同

用户查询的表达方式、明确性和关键词使用对 RAG 系统的准确检索与生成有显著影响。例如:

  • 搜索“基因突变对健康的影响”可能得到笼统信息,而“BRCA1基因突变对乳腺癌风险的影响”则获得具体且详尽的研究结果;
  • “增强免疫功能的方法”与“如何提升免疫系统”可能检索出完全不同的建议,前者更偏向科学研究,后者可能包括个人博客甚至讽刺内容。

注意
如果用户掌握基本的提示工程,正确表述查询问题就不会成为难题。

解决这类问题的方法有多种:简单的改写问题保持“标准化”,或者使用更高级的多查询(multiquery)方法。多查询的思想是:原始查询经过嵌入后,可能与想检索的文档不匹配。通过用不同方式重写问题,增加找到目标文档的可能性。然后对不同查询结果进行合并、去重或使用排序融合(rank fusion)检索。

排序融合检索是结合多个检索系统结果的方法,通过不同排序信号合并重排结果,减少偏差,确保最有用信息优先呈现。不同排序方式会突出搜索的不同重要方面,组合起来结果更均衡。

示例 4-4 展示了多查询的实现:

from langchain_core.load import dumps, loads

template = """
    You are an AI language model assistant. Your task is to generate
    five different versions of the given user question to retrieve relevant
    documents from a vector database. By generating multiple perspectives on the
    user question, your goal is to help the user overcome some of the limitations of
    the distance-based similarity search.
    Provide these alternative questions separated by newlines.
    Original question: {question}"""

prompt_perspectives = ChatPromptTemplate.from_template(template)

generate_queries = (
    prompt_perspectives
    | llm
    | StrOutputParser()
    | (lambda x: x.split("\n"))
)

# 去重工具函数
def get_unique_documents(documents: list[list]):
    flattened_documents = [doc for sublist in documents for doc in sublist]
    stringified_documents = [json.dumps(doc) for doc in flattened_documents]
    unique_stringified_documents = set(stringified_documents)
    unique_documents = [json.loads(doc) for doc in unique_stringified_documents]
    return unique_documents

def reciprocal_rank_fusion(results: list[list], k=60):
    fused_scores = {}
    for docs in results:
        # 假设 docs 按相关性排序
        for rank, doc in enumerate(docs):
            doc_str = langchain_load.dumps(doc)
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            previous_score = fused_scores[doc_str]
            fused_scores[doc_str] += 1 / (rank + k)

    reranked_results = [
        (json.loads(doc), score) for doc, score in sorted(
            fused_scores.items(), key=lambda x: x[1], reverse=True
        )
    ]
    return reranked_results

# 返回唯一文档的检索链:
mq_retrieval_chain = generate_queries | retriever.map() | get_unique_documents

# 或 RAG 融合实现:
mq_retrieval_chain = generate_queries | retriever.map() | reciprocal_rank_fusion

这部分内容涵盖了查询理解中常见问题及对应优化策略,有助于提升 RAG 系统的回答完整性和准确度。

拆解复杂问题

另一种提升查询理解的方式是将复杂的问题拆分成多个子问题,逐个解决。有些问题因为涉及多个索引而较为复杂,难以直接回答。通过使用拆解技术,我们可以对复杂问题进行处理,从而升级链式思维提示。为简便起见,这里使用通用的 LLM。

示例 4-5:查询拆解

from langchain_core.prompts import ChatPromptTemplate

template = """
    You are a helpful assistant that generates multiple sub-questions
    related to an input question. \n
    The goal is to break down the input into a set of sub-problems / sub-questions
    that can be answered in isolation. \n
    Generate multiple search queries related to: {question} \n
    Output (3 queries):"""

prompt_decomposition = ChatPromptTemplate.from_template(template)

generate_queries_decomposition = (
    prompt_decomposition
    | llm
    | StrOutputParser()
    | (lambda x: x.split("\n"))
)

# 运行示例
question = """Given a specific cancer cell line, what are the key pathways
    involved in its resistance to both targeted therapy X and immunotherapy Y,
    and how do these pathways interact at the protein level?"""

> generate_queries_decomposition.invoke({"question": question})
>>> [
    '1. What are the key signaling pathways associated with resistance to targeted therapy X in the specific cancer cell line? ',
    '2. How does the specific cancer cell line exhibit resistance to immunotherapy Y, and what pathways are involved in this process?',
    '3. What are the protein-protein interactions among the key pathways involved in resistance to both targeted therapy X and immunotherapy Y in the specific cancer cell line?'
]

接下来,为回答原始问题,你可以选择并行独立地处理拆解的问题,或者递归地使用前一个问题的答案作为当前问题的上下文。

退一步思考

另一种帮助理解复杂查询的技巧是将原始问题重新表述为更抽象、更高层次的查询。该过程是从初始问题中“退一步”,考虑更广泛的上下文,有助于模型生成更准确、更全面的答案。示例 4-6 演示了这一方法,使用了 few-shot 技巧,这些技巧同样可迁移至其他方法。

示例 4-6:退一步思考

# Few Shot 示例
from langchain_core.prompts import FewShotChatMessagePromptTemplate

examples = [
    {
        "input": "How does the microbiome influence the immune system in humans?",
        "output": "what is the role of the microbiome in human health?",
    },
    {
        "input": "What are the primary factors contributing to the development of antibiotic resistance in bacterial populations?",
        "output": "what are the main causes of antibiotic resistance?",
    }
]

# 转换为示例消息格式
example_prompt = ChatPromptTemplate.from_messages(
    [
        ("human", "{input}"),
        ("ai", "{output}"),
    ]
)

few_shot_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt, examples=examples,
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system",
        """You are an expert at world knowledge. Your task is to step back
           and paraphrase a question to a more generic step-back question, which
           is easier to answer. Here are a few examples:"""
        ),
        few_shot_prompt,
        ("user", "{question}"),
    ]
)

generate_queries_step_back = prompt | llm | StrOutputParser()

question = """For a novel drug target Z, what are the potential off-target effects
           in human cardiomyocytes?"""

> generate_queries_step_back.invoke({"question": question})
>>> what are the possible side effects of new drug targets on heart cells?

正确路由到对应数据库/索引

在拥有多个知识源或索引的 RAG 系统中,将查询路由到合适的数据库或索引对于实现高效且准确的检索至关重要。然而,确定相关的知识领域可能较为困难,尤其是对于涉及多个主题或领域的查询。

路由问题可能出现在 LangChain agent 根据问题、上下文和工具描述选择工具时。我们来看几个常见问题,如错误的路由或领域选择、多领域问题处理低效(需要同时检索多个知识库)、路由循环和死循环等。第 10 章会讨论如何正确处理偏题问题的重要性。

路由算法可能会对某些模型或资源产生偏向,即使它们并非最佳选择,也会过度使用。例如:

  • 查询“10 个大气压下水的沸点”时,没有触发 RAG 流程,而是直接返回模型知识库中的答案,忽略了压力影响,返回 100°C(212°F)。
  • 查询“你能帮我吗?”时,未触发特定的“医疗协助”提示,导致返回通用答复而非定制化的医疗指导。
  • 关于 CRISPR-Cas9 基因编辑技术的查询始终被路由到通用生物模型,忽视了专门的基因工程资源。
  • 关于癌症免疫治疗新进展的查询被路由到普通临床研究模型,而非专业肿瘤学数据库。
  • 查询被错误路由到错误的数据库或表,导致错误结果,如药理学数据与毒理学数据混淆。

一种可能的解决方案是通过微调描述和决策模型,随着时间推移不断优化路由逻辑,尽管这需要专业的硬件和知识。示例 4-7 定义了 RouteQuery 数据模型,其中 data source 字段可选值为 chemistry、drug_discovery 或 biology。为了使用 RouteQuery,我们将采用带结构化输出的 LLM 配置(with_structured_output)。同时定义系统提示,指导 LLM 作为专家将问题路由到正确的数据源。路由器是由提示和结构化 LLM 组成的 LCEL 链。这里同样使用通用 LLM 以简化示例,但其他 LLM 也适用。

示例 4-7:逻辑路由

from typing import Literal
from pydantic import BaseModel, Field

# 数据模型
class RouteQuery(BaseModel):
    """将用户查询路由到最相关的数据源。"""
    datasource: Literal["chemistry", "drug_discovery", "biology"] = Field(
        ..., description="根据用户问题选择最相关的数据源"
    )

# 结构化 LLM 输出配置
structured_llm = llm.with_structured_output(RouteQuery)

system_prompt = """你是一个能将用户问题路由到合适数据源的专家。根据问题所涉及的领域,将其路由到相应的数据源。"""
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        ("human", "{question}"),
    ]
)

# 定义路由器
router = prompt | structured_llm

question = """在光照下,氯与苯反应形成的化合物的热力学性质是什么?"""

> router.invoke({"question": question})
>>> RouteQuery(datasource='chemistry')

收集结果后,可以定义算法的路由逻辑:

from langchain_core.runnables import RunnableLambda

def choose_route(result):
    if "chemistry" in result.datasource.lower():
        return "chemical chain"  # 与化学相关的逻辑
    elif "drug_discovery" in result.datasource.lower():
        return "drug_discovery chain"  # 与药物发现相关的逻辑
    else:
        return "biology chain"  # 与生物学相关的逻辑

full_chain = router | RunnableLambda(choose_route)
> full_chain.invoke({"question": question})
>>> chemical chain

除了基于语言模型推理和上下文理解能力的逻辑路由,我们还可以采用语义(基于嵌入向量)路由。该路由通过向量表示捕获语义含义,基于相似度比较文本。它高效且易于扩展,适合内容匹配和检索,但可能不具备 LLM 提供的深度逻辑推理能力。

示例 4-8 展示了语义路由的代码实现。关键是为每个领域定义特定的提示模板,并使用 OpenAIEmbeddings 类对其进行嵌入,再通过余弦相似度比较用户查询的嵌入向量,从而判断最相似的提示模板。prompt_router 函数实现此比较逻辑,并根据判断结果执行相应领域的检索或代理调用。

示例 4-8:语义路由

from langchain_community.utils.math import cosine_similarity

# 领域提示模板
chemistry_template = """你是一位博学的化学教授,擅长回答化学问题...这里有一个问题:{query}"""
drug_discovery_template = """你是药物发现专家,擅长回答药物发现相关问题...这里有一个问题:{query}"""
biology_template = """你是一位知识渊博的生物学教授,擅长回答生物学问题...这里有一个问题:{query}"""

# 嵌入提示模板
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
prompt_templates = [chemistry_template, drug_discovery_template, biology_template]
prompt_embeddings = embeddings.embed_documents(prompt_templates)

# 路由函数
def prompt_router(input):
    query_embedding = embeddings.embed_query(input["query"])
    similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
    most_similar = prompt_templates[similarity.argmax()]

    if most_similar == chemistry_template:
        ...  # 化学相关检索逻辑
    elif most_similar == drug_discovery_template:
        ...  # 药物发现相关检索逻辑
    else:
        ...  # 生物学相关检索逻辑

    return PromptTemplate.from_template(most_similar)

chain = (
    {"query": RunnablePassthrough()}
    | RunnableLambda(prompt_router)
    | llm
    | StrOutputParser()
)

> chain.invoke(question)
>>> chemical chain
当氯在光照下与苯反应时,形成氯苯。热力学性质是...

在后续章节中,我们会探讨多索引检索时的路由问题。有时路由逻辑可能导致循环,将查询在多个资源间传递却不返回结果;或者将查询路由到无法处理的资源,造成死路。一个典型例子是查询被错误地路由到 SQL 数据库,导致 agent 反复生成相同错误查询。一个稳妥的解决方案是为无法通过常规路由解决的查询设置回退选项。

查询构建

到目前为止,我们已经考虑了用户的需求以及最适合回答问题的数据源。为所选数据库或索引构建有效查询对于检索相关信息至关重要。然而,这一过程可能较为复杂,因为它涉及将自然语言查询映射为适合底层数据格式的结构化查询或表示。

表格和 SQL/NoSQL/图数据库查询

从表格、文档或列表等结构化格式中获取具体答案可能很棘手,特别是当你没有传递对数据的适当描述时。最糟糕的情况是,系统需要从表格中提取有意义的信息,却不知道数据的含义,导致回答不完整或错误。

提示
文章中的表格通常适合上下文窗口,并且有上下文环绕。大多数问题可能出在格式化上。

从表格和 SQL 数据库等结构化数据格式中检索具体信息,如果没有精准描述会很困难。大多数表格代理会通过获取几行数据的列名并基于此尝试创建数据模式(schema)。这可能导致答案不完整或错误,原因包括误解列的用途及所需筛选条件,或者由于 LLM 构建方言特定查询的能力有限,生成错误的 SQL 查询。

表 4-2 显示了部分检索到的实验数据表:

ID实验名称研究对象固氮速率成功率
1Phaseolus vulgaris 中根瘤菌-豆科共生Phaseolus vulgaris78.392.5
2大豆根瘤菌共生效率Glycine max85.788.0
5Medicago-Sinorhizobium 共生增强Medicago truncatula92.193.8
9Azorhizobium-Sesbania 根瘤形成效率Sesbania rostrata88.991.0
13苜蓿根瘤发育Medicago sativa95.394.6

示例 4-9 提供了一个基本的 SQL 检索器实现。我们将连接到 SQLite 的 BiologyResearch.db 数据库,并定义一个简单的提示模板,包含用户问题和一个供代理进行推理的工作区。使用 create_sql_agent 函数创建代理,传入 LLM、数据库连接、提示模板,并指定 agent_type 为 openai-tools,以利用 OpenAI 的能力。

示例 4-9:SQL 检索

from langchain.agents.agent_toolkits import create_retriever_tool
from langchain_community.agent_toolkits import create_sql_agent
from langchain_community.utilities import SQLDatabase

# 初始化 SQL 数据库连接
db = SQLDatabase.from_uri("sqlite:///BiologyResearch.db")

prompt_template = """
    You are an agent designed to interact with a SQL database.
    User question: {input}
    {agent_scratchpad}"""

prompt = PromptTemplate.from_template(prompt_template)

# 创建 SQL 代理
agent = create_sql_agent(
    llm=llm, db=db, prompt=prompt,
    agent_type="openai-tools", verbose=True,
)

> agent.invoke({"input": """How many experiments on plant-microbe interactions
                have resulted in increased nitrogen fixation in legumes?"""})

AI 生成内容示例:

> Invoking: `sql_db_query` with `{'query': 'SELECT COUNT(*) AS
increased_nitrogen_fixation_experiments \nFROM BiologicalExperiments \nWHERE
nitrogen_fixation_rate IS NOT NULL AND nitrogen_fixation_rate > 0;'}`
> [(11,)]

结果:

共有 11 个植物-微生物互作实验导致豆科植物固氮率提高。

如你所见,该结果不准确,因为仅使用了 nitrogen_fixation_rate 作为筛选条件。我们关注的实验应基于以下条件筛选:

  • 多种豆科植物物种(如 Phaseolus vulgaris、Glycine max、Medicago truncatula 等)
  • 研究领域为“植物-微生物互作”
  • 固氮率有可测量数值

期望的 SQL 查询类似于:

SELECT COUNT(*)
FROM BiologicalExperiments
WHERE research_area = 'Plant-Microbe Interactions'
AND nitrogen_fixation_rate IS NOT NULL
AND organism_studied IN (
    'Phaseolus vulgaris', 'Glycine max', 'Medicago truncatula',
    'Medicago sativa', 'Pisum sativum', 'Vigna unguiculata', 'Lotus japonicus',
    'Sesbania rostrata'
);

但即便如此,我们也不能保证结果准确,因为假设表格结构规范、数据整洁,这在实际中不总是成立。

提示
对于列名为公司特定术语或缩写的情况(如 NFR 代替 nitrogen fixation rate),SQL 查询质量可能更差,可能出现如下情况:

SELECT COUNT(*) FROM BiologicalExperiments WHERE compound_produced LIKE '%nitrogen fixation%'

最终结果可能为 0。作为最终用户,如果不了解生成的 SQL 查询,无法完全信任这类结果。

第 9 章将讨论更多关于 SQL 表格的示例和技巧。尤其是在处理含有多个表的大型数据库时,提供清晰的上下文信息极为重要——包括说明每个表的内容以及列的含义和取值。例如,对表 4-2,我们应明确什么是固氮,以及固氮率是百分比数值。

NoSQL 数据库设计用于处理大量非结构化数据,但在查询构建方面也存在独特挑战。这类数据库通常缺乏传统关系型数据库的严格模式,导致查询具体数据点时可能产生不一致或模糊的结果。例如,从 NoSQL 数据库中检索详细实验元数据或具体患者记录,可能因数据模型灵活而导致结果不完整,影响数据检索与分析的准确性,需强有力的查询机制和数据校验流程保证信息的准确性和可靠性。

图数据库在多个领域越来越受欢迎,但因关系复杂和查询需精准表达,也存在挑战。例如,提取特定代谢通路或相互作用网络,若查询不能准确捕获期望的关系,结果可能过于复杂或不完整。结果还强烈依赖于用于训练 LLM 的特定查询语言数据量。例如,Neo4j 是较为流行的图数据库,其拥有大量文本到图数据集用于 LLM 训练和检索,如示例 4-10 所示。

示例 4-10:Neo4j 检索

from langchain_neo4j import Neo4jGraph, GraphCypherQAChain

graph = Neo4jGraph()
chain = GraphCypherQAChain.from_llm(graph=graph, llm=llm, verbose=True)
response = chain.invoke({"query": "Which genes are involved in the metabolic pathway of glucose in Homo sapiens?"})

最新模型在最流行数据库的查询构建上表现出良好潜力,但通过提供 few-shot 查询示例和丰富的带描述的模式(schema)可显著提升结果质量。第 8 章将深入探讨基于图的 RAG,采用药物发现图作为示例。

请求API

通过URI构造的API请求常常面临诸多挑战。构建有效的URI查询需要对API的语法、参数以及被查询数据的具体结构有详细的了解。如果误解或错误指定了这些元素,就可能导致返回不完整、无关或过量的数据。

例如,请求特定的基因组序列或蛋白质结构时,如果URI查询参数没有准确设定,可能会返回整个数据集,而非目标信息。此类问题往往源于令牌限制以及复杂输出的处理。另一个问题是URL中存在错误字符,导致返回None响应,系统后续会将其视为无信息或未找到,从而可能得出错误结论。这些复杂性强调了开展充分的few-shot训练和/或使用复杂查询构建工具的重要性,以便有效导航并充分利用生命科学API的潜力。

示例4-11展示了一个查询构建系统,用于基于特定搜索条件从示例API检索科学文章。我们会定义用于过滤的元数据字段,包括出版类型、年份和返回结果数,这些信息存储在metadata_field_info字典中。该元数据会通过get_query_constructor_prompt函数作为提示的一部分传入。最终,我们将结合提示模板、LLM和格式化结果的输出解析器,创建一个结构化查询管道。

当执行自然语言查询“2020年后关于CRISPR基因编辑及提升农作物产量的10篇最新植物生物学文章”时,LLM会将其翻译成结构化查询格式。该翻译将自由文本请求转化为正式查询,明确使用AND操作符结合“年份大于2020”和“返回10条结果”两个条件。生成的结构化查询对象可用于从数据库或检索系统中获取相关文档。

示例4-11:示例API调用

from langchain.chains.query_constructor.base import (
    StructuredQueryOutputParser, get_query_constructor_prompt,
)

# 定义文档内容描述及元数据字段信息
document_content_description = "Scientific articles"
metadata_field_info = [
    {"name": "publication_type", "type": "string", "description": "Type of publication"},
    {"name": "year", "type": "integer", "description": "Year of publication"},
    {"name": "return", "type": "integer", "description": "Number of publications to return"},
]

prompt = get_query_constructor_prompt(
    document_content_description, metadata_field_info
)
output_parser = StructuredQueryOutputParser.from_components()

# 创建查询构建链
query_constructor = prompt | llm | output_parser

query = "10 recent articles after 2020 on CRISPR gene editing in plant biology focusing on increasing crop yield"

> query_constructor.invoke({"query":query})
>>> StructuredQuery(query='CRISPR gene editing in plant biology increasing crop yield',
    filter=Operation(operator=<Operator.AND: 'and'>, arguments=[Comparison
    (comparator=<Comparator.GT: 'gt'>, attribute='year', value=2020), Comparison
    (comparator=<Comparator.EQ: 'eq'>, attribute='return', value=10)]), limit=None)

示例4-11展示了如何将自然语言请求转化为机器可读的API查询。此类查询不仅可用于查询出版物API(第5章我们会创建类似的代理),还可以用于查询任何具有已知模式的API服务。

HTML、代码与PDF相关挑战

从复杂文档布局(如HTML、代码和PDF)中提取信息,在数据检索方面可能面临显著挑战。这些文档通常包含嵌入于表格、图表或其他非文本元素中的重要信息,系统难以准确解析。例如,系统可能遗漏关键信息,导致结果不完整或误导;又或者PDF的布局过于复杂,检索出来的文档内容会损坏(你有没有试过把.pdf转成.docx或.odt?)。这些问题强调了采用先进解析技术和工具的重要性,以准确解读和提取复杂文档布局中的数据,确保信息检索的全面性和可靠性。后续章节我们将探讨如何实现计算机视觉能力,用于解析文档中的表格和图纸。

小贴士
许多用户查询最优解并非通过查找文档或嵌入空间中的相似数据,而是利用数据本身固有的结构以及查询中表达的结构特征。例如,对于查询“2023年发表的最新癌症治疗研究文章有哪些?”,其中“癌症治疗”部分我们希望用语义搜索,而“年份==2023”部分则希望精准匹配。

在查询时利用元数据有时非常有效。这意味着利用关于数据的结构化信息来提升搜索的准确性和相关性。元数据包括作者、日期、文件类型、标签和类别等细节,能够帮助过滤和细化搜索结果。将元数据纳入查询构建,尤其在处理大型数据集时,能让搜索更加精准高效。通过指定必须满足的条件(比如按特定作者或日期范围过滤文档),可以实现精准信息检索,从而提升整体搜索体验。

数据检索

既然我们已经构建了必要的查询,接下来就是基于处理后的查询检索并排序相关数据,这是RAG系统中至关重要的一步。然而,在这个阶段可能会出现一些令人恼火的问题。

文档排序不准确

糟糕的文档排序算法可能导致相关信息被排在不相关内容之下,甚至重要文档因排名低于无关信息而被忽视。这种问题常见于通过语义搜索查询数据时。为了解决这个问题,可以采取一些技巧,比如使用不同的chunk_size(文本块大小)、变化相似度度量和嵌入模型,以及实施重排序策略(reranking)来提升相关文档的检索准确性。在“查询翻译与理解”中我们简要提到了针对查询改写带来的问题,可以使用前述的互惠排名融合(reciprocal rank fusion)或其他排序算法来合并多个排序列表的结果。

信息不完整和低质量

信息不完整或过时可能带来严重问题,包括误导用户。另一个问题是当系统在某个数据源未找到数据时,可能转而使用自身的知识库,甚至更糟的是“杜撰”数据——这是最危险的幻觉类型。如果用户请求的数据属性在数据源中根本不存在,这也会导致信息不完整的问题。举例来说,若请求化合物的毒性数据,但数据源只包含热力学性质,过滤条件无法应用,系统可能错误得出数据集中不存在有毒化合物的结论。

一种创新方法是引入自我反思机制以提升检索文档的质量和相关性,即纠正性检索增强生成(Corrective Retrieval-Augmented Generation,CRAG)。与传统RAG只执行一次检索后生成答案不同,CRAG增加了一个评分机制,用于评估检索文档与用户查询的相关度(见图4-5)。如果文档相关,会进行精炼处理以提取和保留最重要的信息;如果文档模糊或不正确,系统会执行额外的网络搜索以补充初始检索,确保生成的回答基于最准确和全面的信息。

第5章将详细介绍LangGraph。在此之前,可以将LangGraph想象成一个由节点和边组成的“管弦乐团”,每个节点是一个代理(agent),每条边是通信线路。代理负责对检索文档的相关度进行评分,并使用网络搜索工具辅助检索。

image.png

数据增强与回答生成

将排名靠前的数据整合进大型语言模型(LLM)的提示(prompt)中,生成连贯且相关的回答,是RAG系统的最终目标。这个过程的最后一步——数据增强与回答生成——需要将模型的语言能力与外部知识结合起来。在这一阶段,需要注意许多潜在问题,并应用各种优化措施。

缺失相关上下文

可能出现的问题之一是系统忽略或未充分考虑重要的上下文,导致关键信息丢失,从而产生不够准确的回答。最糟糕的情况是缺乏上下文,使回答变得泛泛而谈或无关紧要,忽略了查询的细微差别。例如:

  • 询问特定患者群体某药物的副作用,却只得到该药物的一般信息;
  • 查询在特定实验条件下基因表达的变化,却只获得关于基因表达的宽泛信息。

解决方案可能是增强系统对问题上下文的理解能力,从而获取更准确的答案。链条中使用的记忆机制也可能存在问题,需加以关注。

数据溢出

当信息量过载时,RAG流程可能无法提取正确的信息,因为存在大量噪声、无关信息和矛盾内容。最糟糕的情况是噪声淹没了系统,导致回答偏离主题、混乱或过于笼统。以下是一些数据溢出的例子:

  • 在大型基因组数据集中搜索特定生物标志物数据,却因为过滤不当而被无关基因信息淹没;
  • 寻找临床试验的详细结果,却收到大量关于临床试验方法学的背景信息。

解决方案是清理数据,重点压缩和总结信息,减少混淆,集中关注需要解决的问题。推荐使用“重写-检索-阅读”(Rewrite-Retrieve-Read,简称RRR)方法,示例4-12展示了这一流程,类似于之前提到的对初始查询进行改写的方法。

示例4-12. 重写-检索-阅读(Rewrite-Retrieve-Read)

from langchain import hub

def parse_text(text):
    return text.strip('"').strip("**")

rewrite_prompt = hub.pull("langchain-ai/rewrite")
rewriter_chain = rewrite_prompt | llm | StrOutputParser() | parse_text
rewrite_retrieve_read_chain = (
    {
        "context": {"x": RunnablePassthrough()} | rewriter_chain | retriever,
        "question": RunnablePassthrough(),
    }
    | prompt
    | model
    | StrOutputParser()
)
result = rewrite_retrieve_read_chain.invoke(...)

输出格式管理

确保输出结果符合指令要求,比如表格或列表格式,至关重要。特别是在当前链条并非最终链条,且其输出将作为另一个链条输入的情况下,格式正确性更为重要。最坏的情况是系统忽视格式要求,导致提供的信息难以使用或理解。通过使用输出解析器(output parsers)、明确指令和重格式化工具,可以帮助规避此类问题。

RAG 变体:Self-RAG、Tree-RAG、CAG 与 Agentic RAG

在阅读本章时,你可能已经注意到,RAG 是一个复杂的系统,可以通过多种方式进行改进。除了优化各个组成部分外,我们还来聊聊最流行的几种对 RAG 本身的改进形式。

Self-RAG

避免幻觉的一个好方法是将检索过程整合到语言模型内部,形成自反式检索增强生成(Self-RAG),如图4-6所示。Self-RAG 涉及一个循环过程,模型构建查询、检索相关文档、评估其相关性,并在不断批判和优化自身输出的同时生成回答。

Self-RAG 的流程从用户提问开始,随后进行查询构建步骤和文档检索步骤,获取相关文档。接着,一个代理对这些文档进行相关性评分。只有当检索到的数据被认为相关时,才进入生成步骤,由模型生成答案;否则重新构建查询。生成的答案还会被另一代理检查是否存在幻觉,如果未发现幻觉且答案令人满意,则将最终答案提供给用户。如果答案包含幻觉或未充分回答问题,则会重新评估并可能重写查询以提高清晰度和准确性,从而重新启动循环。该迭代方法确保答案有充分证据支持,保持高度事实准确性,充分利用查询的语义上下文和从数据库检索的结构化数据。

image.png

Tree-RAG

Tree-RAG(简称 T-RAG)是 RAG 的一种变体,给常规 RAG 添加了两个关键特性:微调和实体检测。微调过程使模型能够更好地理解特定领域的查询,从而生成更契合的回答。同时,实体检测帮助识别用户查询中的关键术语,并利用实体树提供额外的相关细节。这意味着 T-RAG 能通过从层级化的内部数据中提取结构化知识,给出更精准的答案。

没有微调时,RAG 完全依赖基础语言模型对检索文本的理解能力,这种能力可能并不总是针对特定用例进行优化。相比之下,T-RAG 通过实体树结构化检索数据,并借助微调模型优化回答,提高了准确性和相关性。这使得 T-RAG 更适合需要深入上下文和结构化知识的专业领域。

CAG

缓存增强生成(Cache-augmented generation,CAG)采用了与传统 RAG 不同的文档检索方法:它在任何查询发生之前,预先将所有必要的文档加载到模型的内部系统中。CAG 不按需检索文档,而是将它们编码存储为键值(KV)缓存,模型在推理时复用这些缓存。CAG 最适合处理一组定义明确、能够容纳在模型内存中的文档,但对于需要实时更新的庞大且不断变化的数据集则不太适用。相较于 RAG,CAG 缺乏灵活性和扩展性。

Agentic RAG

Agentic RAG 引入了积极参与检索和推理过程的 AI 代理。与遵循固定流水线的方式不同,这些代理可以分析检索到的文档、比较发现,并根据所学内容调整策略。例如,文档代理专注于总结或提取其分配来源的相关细节,而另一代理则协调这些努力,确保答案完整且结构合理。该机制支持多步骤推理、工具使用和超越基础检索与回答生成的决策制定。我们将在下一章详细介绍这一方法。

Agentic RAG 的核心优势在于其能够动态适应不同的查询和信息源。单代理版本像智能决策者一样,选择并整合最佳信息来源。多代理版本则更进一步,为不同代理分配专业角色,并优化在结构化数据库、非结构化文本和实时数据中的检索。

评估RAG系统

到目前为止,我们讨论了改进RAG的方法,现在转向评估RAG性能的问题。根据RAG的输出,可能会出现三种情景:

第一种情景是检索和回答都表现良好。在这种情况下,系统找到了合适的文档并生成了良好的答案。为了衡量这一点,开发者使用正确率(correctness)和命中率(hit rate)等指标。正确率评估回答是否与预期答案匹配,命中率衡量系统是否检索到了正确的文档。这些检查有助于确保RAG按预期工作,避免每次都需要人工评估。

第二种情景是检索和回答均失败。这可能是由于索引和数据准备、问题理解、路由、查询构造等环节出现问题,或者根本没有相关文档来回答查询。我们已经在前面章节中讨论了适用于这类情况的多种改进和示例。

第三种情景是检索到了合适的文档,但系统仍然给出了错误的答案。这通常是因为数据检索、排序或增强、回答生成等环节出现了问题。我们在本章也涉及了类似案例。

问题来了:我们如何评估RAG的性能?

提示
第10章将介绍用于调试、测试和监控生成式AI系统的框架。

最佳的RAG系统会在多个维度进行全面测试,帮助你识别瓶颈并系统性地提升性能。需要考虑的几个关键指标包括:

  • 检索质量或命中率(是否找到了最相关的文档?)
  • 答案正确性(回答是否符合事实真相?)
  • 答案相关性(是否针对用户的问题?)
  • 事实支撑度(回答是否有检索到的文档支持?)

每个维度都能提供对RAG流水线有效性的独特见解,突出可能需要优化的不同方面。

类似传统机器学习,我们需要构建一个评估数据集来了解RAG流水线的表现。这个数据集应包含多样化且具有代表性的问题及其对应的真实答案。你无需从零开始制作:LLM可以帮助生成合成的评估数据集,通过基于知识库生成问题及对应答案。你只需人工评估筛选,确定哪些符合需求。一个良好的做法是使用批判代理(critique agents)来过滤和完善这些合成问题,确保它们基于知识库,有用户相关性,并且在没有额外上下文时也能独立理解。经过筛选的数据集将成为衡量RAG改进的基准,为持续跟踪进展提供一致标准。

提示 在某些情况下,使用其他LLM或甚至多个模型集合作为评判者非常有意义。正如我们开头所说,LLM并非擅长理解,而是识别训练数据模式。例如,你让LLM像刚学编程一样写一个Python代码片段,然后在另一个会话中让它评估这段代码——你会惊讶于评分之高。

基准测试包括在评估数据集上运行RAG流水线,并对每个关键维度进行性能测量。为此,你需要一个评估器或评判者——通常是一个LLM,它能评估回答的质量。有些情况可以使用简单的指标(第10章有介绍),但为了简便,我们目前将LLM作为评判者。

使用LLM作为评判者时,确保提供明确的评分标准,列出每个评分等级的具体判定标准(通常为1到5分)。这有助于确保不同问题和回答之间的评估一致性。通常先让评判者给出详细的理由,再打分,能促使评估更严谨、更准确。

提示
适当将LLM的temperature调高至0.1–0.2,有助于在执行集成评判时获得略有差异的回答。

对RAG流水线的不同方面和配置进行测试,有助于更好地理解潜在瓶颈和最佳超参数设置。比较RAG配置的常见方式是,将它们在测试数据集上的最终指标进行对比,并绘制成直方图。第4-7图展示了这种做法的示例。由于代码较长,完整代码已托管在LangChain4LifeSciencesHealthcare仓库中。

image.png

幻觉的优势

每一次幻觉都可能是一场创新。

到目前为止,我们只谈到了幻觉的负面影响:如何检测和避免它们。正如每朵乌云都有银边,幻觉也有其积极的一面,尤其是在创造力和头脑风暴中。这些模型能够生成新颖且意想不到的想法。与人类思维通常受限于经验和已学知识不同,LLM能产生富有想象力且非传统的输出。LLM的幻觉可以成为激发人类创造力的催化剂,启发新的方向和概念,这些可能无法通过传统的头脑风暴方法产生。后续章节将讨论如何结合RAG与头脑风暴进行应用与实现。

LLM幻觉能够带来洞见,提出创新假设或实验设计。其一个重要优势是生成新颖的研究问题,这些问题可能人类一时难以发现。凭借其训练的大量数据,LLM能够提出基因、蛋白质和疾病等生物实体之间意想不到的关联和联系。虽然这些建议并非总是准确,但它们可能激励研究者探索新方向,发现可能被传统方法忽略的突破性成果。特别是在跨学科研究中,LLM能创造融合不同学科元素的综合假设,极具价值。

另一个优势体现在解决问题的领域。LLM能够提出跳出固有框架的解决方案,发现人类可能忽视的关联。即使这些关联起初看似不现实或不准确,也能激发创新思维,经过反复修正和迭代,最终促成实用方案的诞生。在科学研究或技术创新中,跨学科洞见往往引发突破,LLM通过这些看似“幻觉”的跨界联系实现思想的交叉融合,具有重要意义。

总结

本章深入探讨了AI系统中的幻觉现象及其应对方案。分析了语言模型为何会编造信息,主要涉及两个理论:自我欺骗(模型无法区分自身输出与事实)和知识错配(模型试图模仿人工答案但缺乏相应知识库)。章节中提供了文本和图像幻觉的真实案例,帮助读者理解普通数据错误与真正幻觉的区别。

本章主体围绕RAG作为减少幻觉的主要解决方案展开。详细介绍了RAG流程的每一步:如何准备和建立索引、正确理解用户问题、将问题路由到合适知识源、构造有效查询、检索最相关信息,最终结合上下文生成优质回答。每步均配有常见问题与解决示例。

此外,章节还探讨了若干先进RAG变体,如自我RAG(Self-RAG,模型自检)、树状RAG(Tree-RAG,基于层级知识结构)、缓存增强生成(CAG,预先加载文档)、以及具代理特性的RAG(Agentic RAG,AI代理主动参与检索与推理)。并提供了使用检索质量、答案正确性、相关性和事实支撑度等指标评估RAG系统的实用建议。

最后,本章以幻觉的正面价值作结,讨论AI“编造”能力如何激发创意、新的研究问题及意外联系,这些可能是人类难以独立想到的。此平衡视角展示了幻觉在某些语境下可能是缺陷,但在头脑风暴和创新中却是宝贵的特质。未来章节将频繁使用RAG流水线,下一章将介绍如何构建基于链、代理及多代理系统的智能助手。