LangChain概念指南(4)
技术
流式传输
单独的 LLM 调用通常比传统的资源请求运行时间要长得多。当您构建需要多个推理步骤的更复杂链或代理时,这种情况会加剧。
幸运的是,LLM 以迭代的方式生成输出,这意味着在最终响应准备好之前,可以显示合理的中间结果。因此,在构建 LLM 应用程序的用户体验中,尽快使用可用的输出已成为缓解延迟问题的重要部分,LangChain 旨在为流式传输提供一流的支持。
下面,我们将讨论 LangChain 中流式传输的一些概念和考虑因素。
.stream() 和 .astream()
LangChain 中的大多数模块都包括 .stream() 方法(以及等效的 .astream() 方法,用于异步环境),作为人体工程学流式传输接口。.stream() 返回一个迭代器,您可以使用简单的 for 循环进行消费。这里有一个聊天模型的示例:
from langchain_anthropic import ChatAnthropic model = ChatAnthropic(model="claude-3-sonnet-20240229") for chunk in model.stream("what color is the sky?"): print(chunk.content, end="|", flush=True)
对于(或其他组件)不支持原生流式传输的模型,这个迭代器只会生成一个单一的块,但是您仍然可以使用相同的通用模式调用它们。使用 .stream() 还将自动以流式模式调用模型,无需提供额外的配置。
每个输出的块的类型取决于组件的类型 - 例如,聊天模型产生 AIMessageChunks。因为这个方法是由 LangChain 表达式语言的一部分,您可以使用输出解析器来处理来自不同输出的格式差异。
您可以查看此指南,了解如何使用 .stream() 的更多详细信息。
.astream_events()
虽然 .stream() 方法很直观,但它只能返回链的最终生成值。这对于单个 LLM 调用来说是可以的,但当您构建由多个 LLM 调用组成的更复杂的链时,您可能想要使用链的中间值以及最终输出 - 例如,在构建聊天应用程序时,返回源以及最终生成时。
有方法可以使用回调,或者通过构建链的方式,使用像链式 .assign() 调用这样的东西将中间值传递到末端,但 LangChain 还包括一个 .astream_events() 方法,它结合了回调的灵活性和 .stream() 的人体工程学。当调用时,它返回一个迭代器,它产生各种类型的事件,您可以根据项目的需求进行过滤和处理。
这里有一个只打印包含流式传输聊天模型输出的事件的小型示例:
from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_anthropic import ChatAnthropic model = ChatAnthropic(model="claude-3-sonnet-20240229") prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}") parser = StrOutputParser() chain = prompt | model | parser async for event in chain.astream_events({"topic": "parrot"}, version="v2"): kind = event["event"] if kind == "on_chat_model_stream": print(event, end="|", flush=True)
您可以大致将其视为回调事件的迭代器(尽管格式不同) - 并且您可以在几乎所有 LangChain 组件上使用它!
回调
在 LangChain 中从 LLM 流式传输输出的最低级别方式是通过回调系统。您可以传递一个回调处理程序,该处理程序处理 on_llm_new_token 事件到 LangChain 组件。当该组件被调用时,组件中包含的任何 LLM 或聊天模型都会使用生成的令牌调用回调。在回调中,您可以将令牌管道传输到其他目的地,例如 HTTP 响应。您还可以处理 on_llm_end 事件以执行任何必要的清理。
回调是在 LangChain 中引入流式传输的第一个技术。尽管功能强大且通用,但它们可能对开发人员来说不太方便。例如:
您需要显式初始化和管理一些聚合器或其他流以收集结果。
执行顺序没有明确保证,您理论上可能在 .invoke() 方法完成后运行回调。
提供商通常要求您传递一个额外的参数以流式传输输出,而不是一次性返回它们全部。
您通常会忽略实际模型调用的结果,而更倾向于回调结果。
令牌
大多数模型提供商用于测量输入和输出的单位是通过称为令牌的单位。令牌是语言模型在处理或生成文本时读取和生成的基本单位。令牌的确切定义可能会根据模型的训练方式而有所不同 - 例如,在英语中,令牌可以是一个像 "apple" 这样的单词,或者是 "app" 这样的单词的一部分。
当您向模型发送提示时,提示中的单词和字符使用分词器编码成令牌。然后,模型将流式传输回生成的输出令牌,分词器将其解码为人类可读的文本。下面的例子展示了 OpenAI 模型是如何对 "LangChain is cool!" 进行分词的:
h ttps://python.langchain.com/v0.2/assets/images/tokenization-10f566ab6774724e63dd99646f69655c.png
您可以看到它被拆分成了 5 个不同的令牌,令牌之间的边界并不完全与单词边界相同。
语言模型使用令牌而不是像 "字符" 这样更直观的东西的原因与它们处理和理解文本的方式有关。在高层次上,语言模型基于初始输入和之前的生成迭代预测它们接下来的生成输出。使用令牌训练语言模型可以处理具有意义的语言单位(如单词或子词),而不是单个字符,这使得模型更容易学习并理解语言的结构,包括语法和上下文。此外,使用令牌也可以提高效率,因为模型处理的文本单元比字符级处理要少。
结构化输出
LLM 能够生成任意文本。这使得模型能够适当地响应广泛的输入,但对于某些用例来说,将 LLM 的输出限制为特定格式或结构可能会很有用。这被称为结构化输出。
例如,如果输出要存储在关系数据库中,那么如果模型生成的输出符合定义的模式或格式,将会容易得多。从非结构化文本中提取特定信息是另一个特别有用的例子。最常见的输出格式将是 JSON,尽管像 YAML 这样的其他格式也可能有用。下面,我们将讨论一些在 LangChain 中从模型获取结构化输出的方法。
.with_structured_output()
为了方便,一些 LangChain 聊天模型支持 .with_structured_output() 方法。这个方法只需要一个模式作为输入,并返回一个字典或 Pydantic 对象。通常,这个方法只出现在支持下面更高级方法的模型上,并在内部使用其中之一。它负责导入合适的输出解析器,并为模型格式化模式的正确格式。
这里有一个例子:
from typing import Optional from langchain_core.pydantic_v1 import BaseModel, Field class Joke(BaseModel): """Joke to tell user.""" setup: str = Field(description="The setup of the joke") punchline: str = Field(description="The punchline to the joke") rating: Optional[int] = Field(description="How funny the joke is, from 1 to 10") structured_llm = llm.with_structured_output(Joke) structured_llm.invoke("Tell me a joke about cats") Joke(setup='Why was the cat sitting on the computer?', punchline='To keep an eye on the mouse!', rating=None)
我们建议将这个方法作为处理结构化输出的起点:
它在幕后使用其他模型特定的功能,无需导入输出解析器。
对于使用工具调用的模型,不需要特殊的提示。
如果支持多种底层技术,您可以提供 method 参数来切换使用哪一个。
如果:
您使用的聊天模型不支持工具调用。
您正在处理非常复杂的模式,并且模型在生成符合输出时存在困难。
您可能需要或想要使用其他技术。有关更多信息,请查看此操作指南。
您也可以查看此表格,列出了支持 with_structured_output() 的模型。
原始提示
让模型结构化输出的最直观的方法是礼貌地提出请求。除了您的查询外,您还可以给出描述您希望得到什么样的输出的说明,然后使用输出解析器将原始模型消息或字符串输出转换为更易于操作的内容。
原始提示的最大好处是其灵活性:
原始提示不需要任何特殊的模型功能,只需要足够的推理能力来理解传递的模式。
您可以提示任何格式,不仅仅是 JSON。如果模型在某种特定类型的数据上受过更多训练,这可能很有用,例如 XML 或 YAML。
然而,也有一些缺点:
LLM 是非确定性的,提示 LLM 以完全正确的格式一致地输出数据以便于顺利解析可能会出奇地困难和特定于模型。
个别模型根据它们训练的数据有不同的特点,优化提示可能相当困难。有些可能更擅长解释 JSON 模式,有些可能最适合 TypeScript 定义,还有一些可能更喜欢 XML。
尽管模型提供商提供的功能可能增加可靠性,但无论您选择哪种方法,提示技术对于调整结果都很重要。
JSON
使用完整文档进行答案生成。
名称 索引类型 使用 LLM 使用场景 描述
向量存储 向量存储 否 如果您刚开始并寻找快速简单的方法。 这是最简单的方法,也是最容易开始使用的方法。它涉及为每段文本创建嵌入。
父文档 向量存储 + 文档存储 否 如果您的页面有许多较小的独立信息片段,最好单独索引,但最好一起检索。 这涉及为每个文档索引多个块。然后找到在嵌入空间中最相似的块,但检索整个父文档并返回它(而不是单个块)。
多向量 向量存储 + 文档存储 有时在索引期间 如果您能够从文档中提取信息,您认为这些信息比文本本身更适合索引。 这涉及为每个文档创建多个向量。每个向量可以以多种方式创建 - 例如文本的摘要和假设问题。
时间加权向量存储 向量存储 否 如果您的文档有关联的时间戳,并且您想检索最新的文档。 这根据语义相似性(如正常的向量检索)和最新性(查看索引文档的时间戳)的组合来获取文档。
提示:
查看我们的 RAG from Scratch 视频,了解索引基础。
查看我们的 RAG from Scratch 视频,了解多向量检索器。
第五,考虑如何改进您的相似性搜索本身的质量。嵌入模型将文本压缩成固定长度的(向量)表示,捕获文档的语义内容。这种压缩对于搜索 / 检索很有用,但将语义细微差别 / 细节的重担放在单一的向量表示上。
ColBERT 是一个有趣的方法,通过更高粒度的嵌入来解决这个问题:(1) 为文档和查询中的每个令牌生成受上下文影响的嵌入,(2) 计算查询令牌和所有文档令牌之间的相似度,(3) 取最大值,(4) 对于所有查询令牌,重复第 3 步,(5) 将所有查询令牌在第 3 步中的最大分数之和作为查询-文档相似度分数;这种基于令牌的评分可以产生强大的结果。
https://python.langchain.com/v0.2/assets/images/colbert-0bf5bd7485724d0005a2f5bdadbdaedb.png
还有一些额外的技巧可以提高您的检索质量。嵌入模型擅长捕获语义信息,但可能难以处理基于关键字的查询。许多向量存储提供内置的混合搜索,结合关键字和语义相似性,这结合了两种方法的优点。此外,许多向量存储具有最大边际相关性,它试图使搜索结果多样化,避免返回相似和冗余的文档。
名称 使用场景 描述
ColBERT 当需要更高粒度的嵌入时。 ColBERT 使用上下文影响的嵌入为文档和查询中的每个令牌获取细粒度的查询-文档相似度分数。
混合搜索 当结合基于关键字和语义相似性时。 混合搜索结合了关键字和语义相似性,结合了两种方法的优点。
最大边际相关性 (MMR) 当需要使搜索结果多样化时。 MMR 试图使搜索结果多样化,避免返回相似和冗余的文档。
提示:
查看我们的 RAG from Scratch 视频,了解 ColBERT。
后处理
第六,考虑过滤或对检索到的文档进行排名的方法。如果您从多个来源检索文档,这非常有用,因为它可以降低相关性较低的文档的排名和/或压缩相似文档。
名称 索引类型 使用 LLM 使用场景 描述
上下文压缩 任何 有时 如果您发现检索到的文档包含太多不相关信息,并且分散了 LLM 的注意力。 这是在另一个检索器之上的后处理步骤,从检索到的文档中提取最相关的信息。这可以使用嵌入或 LLM 完成。
集成 任何 否 如果您有多种检索方法,并希望尝试将它们结合起来。 这从多个检索器中获取文档,然后将它们组合起来。
重新排名 任何 是 如果您想要根据相关性对检索到的文档进行排名,特别是如果您想要结合来自多种检索方法的结果。 给定一个查询和一组文档,重新排名将文档从语义相关性最高到最低进行索引。
提示:
查看我们的 RAG from Scratch 视频,了解 RAG-Fusion,这是一种跨多个查询的后处理方法:从多个角度重写用户问题,检索每个重写问题的相关文档,并使用互惠排名融合 (RRF) 将多个搜索结果列表的排名组合成一个统一的排名。
生成
最后,考虑将自我校正构建到您的 RAG 系统中。RAG 系统可能会受到检索质量低下(例如,如果用户问题对于索引领域是未知的)和/或生成中的幻觉的影响。一个简单的检索-生成管道没有能力检测或自我校正这些类型的错误。在代码生成的背景下引入了“流程工程”的概念:使用单元测试迭代构建代码问题的答案并检查和自我校正错误。几项工作已经将此应用于 RAG,例如 Self-RAG 和 Corrective-RAG。在这两种情况下,在 RAG 答案生成流程中执行文档相关性、幻觉和/或答案质量的检查。
我们发现图形是可靠表达逻辑流程的好方法,并且我们已经使用 LangGraph 实现了这些论文中的想法,如图下图所示(红色 - 路由,蓝色 - 回退,绿色 - 自我校正):
路由:自适应 RAG(论文)。如上所述,将问题路由到不同的检索方法
回退:校正 RAG(论文)。如果文档与查询不相关,则回退到网络搜索
自我校正:Self-RAG(论文)。修正包含幻觉或未解决问题的答案
名称 使用场景 描述
Self-RAG 当需要修正包含幻觉或不相关内容的答案时。 Self-RAG 在 RAG 答案生成流程中执行文档相关性、幻觉和答案质量的检查,迭代构建答案并自我校正错误。
Corrective-RAG 当需要低相关文档的回退机制时。 Corrective-RAG 包括一个回退(例如,到网络搜索),如果检索到的文档与查询不相关,确保更高质量和更相关的检索。
提示:
查看几个视频和食谱,展示使用 LangGraph 进行 RAG:
LangGraph 校正 RAG
LangGraph 结合自适应、Self-RAG 和校正 RAG
使用 LangGraph 进行 RAG 的食谱
查看我们的 LangGraph RAG 食谱合作伙伴:
Meta
Mistral
文本分割
LangChain 提供了许多不同类型的文本分割器。这些都包含在 langchain-text-splitters 包中。
表格列:
名称:文本分割器的名称
类:实现此文本分割器的类
分割依据:此文本分割器如何分割文本
添加元数据:此文本分割器是否添加有关每个块来源的元数据
描述:分割器的描述,包括何时使用的推荐
名称 类 分割依据 添加元数据 描述
递归 RecursiveCharacterTextSplitter, RecursiveJsonSplitter 用户定义字符列表 递归地分割文本。这种分割尝试将相关的文本片段保持在一起。这是开始分割文本的推荐方式。
HTML HTMLHeaderTextSplitter, HTMLSectionSplitter HTML 特定字符 ✅ 基于 HTML 特定字符分割文本。值得注意的是,这会添加关于该片段来源的相关信息(基于 HTML)。
Markdown MarkdownHeaderTextSplitter, Markdown 特定字符 ✅ 基于 Markdown 特定字符分割文本。值得注意的是,这会添加关于该片段来源的相关信息(基于 Markdown)。
代码 多种语言 代码(Python, JS)特定字符 基于特定编程语言的字符分割文本。有 15 种不同的语言可供选择。
令牌 多个类 令牌 基于令牌分割文本。存在一些不同的方法来衡量令牌。
字符 CharacterTextSplitter 用户定义字符 基于用户定义字符分割文本。这是最简单的方法之一。
语义分块器 (实验性) SemanticChunker 句子 首先基于句子分割。然后将相邻的句子组合在一起如果它们在语义上足够相似。来自 Greg Kamradt
集成:AI21 语义 AI21SemanticTextSplitter ✅ 识别形成连贯文本片段的不同主题,并沿着这些分割。
评估
评估是评估您的 LLM 驱动应用程序的性能和有效性的过程。它涉及根据一组预定义的标准或基准测试模型的响应,以确保它满足所需的质量标准并实现预期目的。这个过程对于构建可靠的应用程序至关重要。
LangSmith 以几种方式帮助这个过程:
它通过其跟踪和注释功能,使创建和策划数据集变得更加容易
它提供了一个评估框架,帮助您定义指标并针对您的数据集运行应用程序
它允许您跟踪结果随时间的变化,并自动按计划或作为 CI/Code 的一部分运行您的评估器
要了解更多信息,请查看此 LangSmith 指南。
跟踪
跟踪本质上是应用程序从输入到输出所采取的一系列步骤。跟踪包含称为 runs 的单独步骤。这些可以是来自模型、检索器、工具或子链的单独调用。跟踪为您的链和代理提供了可观测性,并且在诊断问题时至关重要。