LangChain自0.1版本发布以来,已经历了显著的进化,特别是向AI时代的适应性提升。在0.1版本中,LangChain主要聚焦于提供基本的链式操作和工具集成,帮助开发者构建简单的语言模型应用。该版本适用于处理简单任务,但在应对更复杂的AI需求时显得有些局限。
相比之下,LangChain 0.3版本展现了更为全面和强大的功能,进一步优化了其模块化架构,增强了与现代AI工具和框架的兼容性。这一版本加入了更多针对AI时代的特性,包括增强的多模态支持、自动化的推理链处理、以及更强的上下文管理能力。LangChain 0.3不仅扩展了与大型语言模型(LLM)的协作功能,还引入了对更多外部工具和数据库的原生支持,使得构建复杂的RAG(Retrieval-Augmented Generation)系统变得更加简便。
总的来说,LangChain 0.3版本不仅提升了功能的深度,还大幅优化了开发者的使用体验,为AI应用的快速迭代和部署提供了更多便利,是AI应用开发者在实际项目中的理想选择。
本篇文章是我在参考官网文档学习时,按照自认为适合新人入门的排列顺序写成的,希望能帮到大家!
目录
- LangChain 介绍
- 安装langChain
- 生态包
- ChatModel和LLM组件简介
- Message组件
- 修剪消息
- 编写自定义令牌计数器
- 链接
- 使用聊天消息历史
- 过滤消息
- 链接
- 合并相同类型的连续消息
- 链接
- Prompt组件
- 字符串Prompt模板
- 部分格式化提示词模板
- 聊天Prompt模板
- 组合提示词
- 消息占位符Placeholder
- 示例选择器
- 自定义示例选择器
- 按长度选择示例
- 通过相似性选择示例
- 通过 n-gram 重叠选择示例
- 通过最大边际相关性 (MMR) 选择示例
- FewShotPrompt
- 少量示例格式化器
- 结合示例选择器对示例进行筛选
- 在聊天模型中使用少量示例
- 文档加载器组件
- 加载PDF文件
- 简单快速的文本提取
- PDF上的向量搜索
- 布局分析和从图像中提取文本
- 提取表格和其他结构
- 从特定部分提取文本
- 从图像中提取文本
- 多模态模型的使用
- 加载网页
- 简单快速的文本提取
- 高级解析
- 从特定部分提取内容
- 对页面内容进行向量搜索
- 加载CSV文件
- 从目录加载文档
- 加载HTML
- 加载 JSON
- 加载Markdown
- 加载 Microsoft Office 文件
- 创建自定义文档加载器
- 文本分割组件
- 按字符递归分割文本
- 按HTML分割文本
- 按HTML 头部进行分割
- 按HTML成分进行分割
- 按字符分割
- 分割代码
- 按标题分割Markdown
- 分割 JSON 数据
- 根据语义相似性分割文本
- 嵌入模型/向量存储
- 文本嵌入模型
- 缓存嵌入结果
- 创建和查询向量存储
LangChain 介绍
LangChain 是一个用于开发由大型语言模型 (LLMs) 驱动的应用程序的框架。
LangChain 简化了 LLM 应用程序生命周期的每个阶段:
- 开发阶段:使用 LangChain 的开源 构建模块、组件 和 第三方集成 构建您的应用程序。 使用 LangGraph 构建具有一流流式处理和人机协作支持的有状态代理。
- 生产化阶段:使用 LangSmith 检查、监控和评估您的链,以便您可以持续优化并自信地部署。
- 部署阶段:将您的 LangGraph 应用程序转变为生产就绪的 API 和助手,使用 LangGraph Cloud。
具体来说,该框架由以下开源库组成:
- langchain-core: 基础抽象和LangChain表达式 (LCEL)。
- langchain-community: 第三方集成。
- 合作伙伴库(例如 langchain-openai、langchain-anthropic 等):一些集成已进一步拆分为自己的轻量级库,仅依赖于 langchain-core。
- langchain: 组成应用程序认知架构的链、代理和检索策略。
- LangGraph: 通过将步骤建模为图中的边和节点,构建强大且有状态的多参与者应用程序。与LangChain无缝集成,但也可以单独使用。
- LangServe: 将LangChain链部署为REST API。
- LangSmith: 一个开发者平台,让您调试、测试、评估和监控LLM应用程序。
从官网的架构图可以清楚的看到langChain生态的完善,langChain已经从一个简单的LLM Agent库变成了一个大而全的开发平台。
同时,langChain社区庞大而活跃,引入了许许多多新特性和新集成,例如,Deepseek爆火的同时,langChain已经有了与deepseek交互的库:langChain-deepseek。
再回头去看看LangChain 框架的两个主要的价值主张:
- 组件:LangChain 为处理语言模型所需的组件提供模块化的抽象。LangChain 还为所有这些抽象提供了实现的集合。
- 用例特定链:链可以被看作是以特定方式组装这些组件,以便最好地完成特定用例。这旨在成为一个更高级别的接口,使人们可以轻松地开始特定的用例。这些链也旨在可定制化。
毫无疑问,langChain做到了!我们的应用可以看作一条链条,这个链是由多个模块串联而成,对于每一个模块你随时可以替换成另一个模块。
安装langChain
要安装主要的 langchain 包,请运行:
pip install langchain
这个包是 LangChain 的底层代码, 但 LangChain 的大部分价值在于与各种大模型供应商、数据存储等的集成。
生态包
除了 langsmith SDK,LangChain 生态系统中的所有包都依赖于 langchain-core,它包含其他包使用的基础类和抽象。 下面的依赖图显示了不同包之间的关系。 一个有向箭头表示源包依赖于目标包:
LangChain核心:langchain-core 包包含其余 LangChain 生态系统使用的基础抽象,以及 LangChain 表达式语言。它由 langchain 自动安装,但也可以单独使用。安装命令:pip install langchain-core
集成包:如 OpenAI 和 Anthropic,有自己的包。 任何需要自己包的集成将在 集成文档 中进行说明。 可以在 API 参考 的 “第三方库” 下拉菜单中查看所有集成包的列表。 要安装其中一个,可以运行:pip install langchain-openai
任何尚未拆分为自己包的集成将保留在 langchain-community 包中。安装方法:pip install langchain-community
LangGraph:langgraph 是一个用于构建有状态的多参与者应用程序的库,支持大型语言模型(LLMs)。它与 LangChain 无缝集成,但也可以单独使用。 安装方式:pip install langgraph
…
注:从 0.3 版本开始,LangChain 在内部使用 Pydantic 2,用户应安装 Pydantic 2,并建议避免在 LangChain API 中使用 Pydantic 2 的 pydantic.v1 命名空间!
ChatModel和LLM组件简介
ChatModel:聊天模型是使用一系列消息作为输入并返回聊天消息作为输出的语言模型(与使用纯文本相对), 这些通常是较新的模型(较旧的模型通常是LLMs)。
聊天模型支持为对话消息分配不同的角色,有助于区分来自AI、用户和系统消息等指令的消息。尽管底层模型是消息输入、消息输出,但LangChain的包装器也允许这些模型接受字符串作为输入。这意味着可以轻松地使用聊天模型替代LLMs。
当字符串作为输入传入时,它会被转换为HumanMessage,然后传递给底层模型。
LangChain不托管任何聊天模型,而是依赖于第三方集成。
在构建ChatModels时,有一些标准化参数:
- model: 模型名称
- temperature: 采样温度
- timeout: 请求超时
- max_tokens: 生成的最大令牌数
- stop: 默认停止序列
- max_retries: 请求重试的最大次数
- api_key: 大模型供应商的API密钥
- base_url: 发送请求的端点
注意:
- 标准参数仅适用于公开具有预期功能的参数的大模型供应商。例如,一些大模型供应商不公开最大输出令牌的配置,因此在这些大模型供应商上无法支持max_tokens。
- 标准参数目前仅在具有自己集成包的集成上强制执行(例如 langchain-openai、langchain-anthropic 等),在 langchain-community 中的模型上不强制执行。
多模态性:一些聊天模型是多模态的,接受图像、音频甚至视频作为输入。这些模型仍然较为少见,这意味着大模型供应商尚未在定义API的“最佳”方式上达成标准。多模态输出则更为少见。因此,langChain保持了多模态抽象的相对轻量,并计划在该领域成熟时进一步巩固多模态API和交互模式。
在LangChain中,大多数支持多模态输入的聊天模型也接受OpenAI内容块格式的这些值。目前这仅限于图像输入。对于支持视频和其他字节输入的模型,如Gemini,API也支持原生的、特定于模型的表示。
LLM:将字符串作为输入并返回字符串的语言模型(这些通常是较旧的模型)。
纯文本输入/输出的大型语言模型往往较旧或较低级。即使对于非聊天用例,许多新的流行模型也最好用作聊天模型。
尽管底层模型是字符串输入、字符串输出,但LangChain的包装器也允许这些模型接受消息作为输入。 这使它们具有与聊天模型相同的接口。 当消息作为输入传入时,它们将在底层被格式化为字符串,然后传递给底层模型。
LangChain不托管任何大型语言模型,而是依赖于第三方集成。
我们都知道,在LangChain v0.1版本中,ChatModel和LLM(大语言模型)被区分开来,是因为最初的设计中,这两者是为了解决不同的任务和使用场景:
- LLM(如GPT-3或其他基础语言模型)主要用于直接生成文本,比较传统的语言模型应用场景。
- ChatModel是为适应更复杂的对话任务设计的,特别是在对话上下文保持(例如多轮对话)和用户交互中的情境管理。
langChain v0.1将模型分为ChatModel和LLM,而在v0.3则直接建议一律使用聊天模型:
-
统一接口:随着LangChain的迭代,团队意识到无论是
处理单轮任务(如问题回答)还是多轮对话
,底层的模型架构和接口越来越相似,ChatModel可以灵活适应两种情况。这种统一设计简化了开发者的使用体验,不需要选择不同的模型类型来处理不同的任务。 -
增强对话管理能力:即使是单次任务,ChatModel的设计仍然能够提供更好的上下文管理功能。通过ChatModel,开发者可以更加灵活地管理对话历史、上下文以及任务状态,而这在某些复杂的任务中,尤其是需要保持上下文的场景下非常有用。
-
灵活扩展性:随着技术的发展,更多的应用场景可能需要更复杂的模型功能,比如强化对话生成的控制、定制化的消息处理、对话状态的追踪等。ChatModel的设计更适合应对这些需求。
-
后续兼容性:未来,LangChain可能会增加更多与对话相关的功能(如自定义对话框架、任务引擎等),因此将所有模型统一为ChatModel可以更方便地进行扩展和维护。
因此,我们下文使用的模型都是满足ChatModel接口的聊天模型。
Message组件
一些模型将消息列表作为输入并返回一条消息。 消息有几种不同的类型。 所有消息都有 role、content 和 response_metadata 属性。
role 描述了谁在说这条消息。标准角色是 ‘user’、‘assistant’、‘system’ 和 ‘tool’。 LangChain 为不同角色提供了不同的消息类。
content 属性描述了消息的内容。 这可以是几种不同的东西:
- 一个字符串(大多数模型处理这种类型的内容)
- 一个字典列表(用于多模态输入,其中字典包含关于该输入类型和输入位置的信息)
可选地,消息可以有一个 name 属性,用于区分具有相同角色的多个发言者。 例如,如果聊天历史中有两个用户,区分它们可能会很有用,但并不是所有模型都支持这一点。
下面是角色对应的类型:
-
UserMessage:这表示角色为“用户”的消息。
-
AIMessage:这表示角色为“助手”的消息。除了content属性,这些消息还有:
-
response_metadata:response_metadata属性包含有关响应的附加元数据。这里的数据通常是特定于每个大模型供应商的。 这里可能存储诸如日志概率和令牌使用等信息。
-
tool_calls:这些表示语言模型调用工具的决策。它们作为AI消息输出的一部分包含在内。 可以通过 .tool_calls 属性从那里访问。该属性返回一个 ToolCall 的列表。ToolCall 是一个包含以下参数的字典:
- name: 应该被调用的工具的名称。
- args: 该工具的参数。
- id: 该工具调用的 id。
-
-
SystemMessage:这表示一个角色为 “system” 的消息,告诉模型如何行为,并不是每个大模型供应商都支持这个。
-
ToolMessage:这表示一个角色为 “tool” 的消息,包含调用工具的结果。除了 role 和 content,该消息还有:
- 一个 tool_call_id 字段,传达调用该工具以生成此结果的调用 id。
- 一个 artifact 字段,可以用于传递工具执行的任意工件,这些工件有助于跟踪,但不应发送给模型。
在大多数聊天模型中,ToolMessage 只能在包含已填充 tool_calls 字段的 AIMessage 之后出现在聊天历史中。
-
(遗留)FunctionMessage:这是一种遗留消息类型,对应于 OpenAI 的遗留函数调用 API。应使用 ToolMessage 来对应更新后的工具调用 API。这表示函数调用的结果。除了 role 和 content,此消息还有一个 name 参数,用于传达调用以生成此结果的函数名称。
修剪消息
所有模型都有有限的上下文窗口,这意味着它们可以作为输入的令牌数量是有限的。如果你有非常长的消息或一个累积了长消息历史的链/代理,您需要管理传递给模型的消息长度。
trim_messages 工具提供了一些基本策略,用于将消息列表修剪为特定的令牌长度。
假设我们在使用过程中积攒或导入了如下大量Message:
messages = [SystemMessage("you're a good assistant, you always respond with a joke."),HumanMessage("i wonder why it's called langchain"),AIMessage('Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!'),HumanMessage("and who is harrison chasing anyways"),AIMessage("Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"),HumanMessage("what do you call a speechless parrot"),
]
此时假设模型上下文允许的最大token量为45个token!
要从下往上获取max_tokens 个token的信息,可以设置 strategy=“last”:
# pip install -U langchain-openai
from langchain_core.messages import (AIMessage,HumanMessage,SystemMessage,trim_messages,
)
from langchain_openai import ChatOpenAImessages = [...]trim_messages(messages,max_tokens=45,strategy="last",token_counter=ChatOpenAI(model="gpt-4o"),
)
可以看到,我们从下往上利用gpt-4o的tokenizer截取了45个token的消息:
[AIMessage(content="Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"),HumanMessage(content='what do you call a speechless parrot')]
注:对于token_counter参数,我们可以传入一个函数或一个语言模型(因为语言模型有消息令牌计数方法)。当你在修剪消息以适应特定模型的上下文窗口时,建议传入要适配的模型。
如果我们想始终保留初始系统消息,可以指定 include_system=True:
trim_messages(messages,max_tokens=45,strategy="last",token_counter=ChatOpenAI(model="gpt-4o"),include_system=True,
)
[SystemMessage(content="you're a good assistant, you always respond with a joke."),HumanMessage(content='what do you call a speechless parrot')]
如果我们想允许拆分消息的内容,可以指定 allow_partial=True:
trim_messages(messages,max_tokens=56, // 增大token数,否则体现不出来做了拆分strategy="last",token_counter=ChatOpenAI(model="gpt-4o"),include_system=True,allow_partial=True,
)
[SystemMessage(content="you're a good assistant, you always respond with a joke."),AIMessage(content="\nWhy, he's probably chasing after the last cup of coffee in the office!"),HumanMessage(content='what do you call a speechless parrot')]
如果我们需要确保我们的第一条消息(不包括系统消息)始终是特定类型,可以指定 start_on:
trim_messages(messages,max_tokens=60,strategy="last",token_counter=ChatOpenAI(model="gpt-4o"),include_system=True,start_on="human",
)
[SystemMessage(content="you're a good assistant, you always respond with a joke."),HumanMessage(content='what do you call a speechless parrot')]
如果想从前往后获取max_tokens 个token的Message,可以通过指定 strategy=“first” :
trim_messages(messages,max_tokens=45,strategy="first",token_counter=ChatOpenAI(model="gpt-4o"),
)
[SystemMessage(content="you're a good assistant, you always respond with a joke."),HumanMessage(content="i wonder why it's called langchain")]
编写自定义令牌计数器
前面说过,对于token_counter参数,我们可以传入一个函数或一个语言模型。我们现在来编写一个自定义令牌计数器函数,该函数接受消息列表并返回一个整数。
from typing import List
from langchain_core.messages import (AIMessage,HumanMessage,SystemMessage,trim_messages,
)
# pip install tiktoken
import tiktoken
from langchain_core.messages import BaseMessage, ToolMessagedef str_token_counter(text: str) -> int:enc = tiktoken.get_encoding("o200k_base")return len(enc.encode(text))def tiktoken_counter(messages: List[BaseMessage]) -> int:"""Approximately reproduce https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynbFor simplicity only supports str Message.contents."""num_tokens = 3 # every reply is primed with <|start|>assistant<|message|>tokens_per_message = 3tokens_per_name = 1for msg in messages:if isinstance(msg, HumanMessage):role = "user"elif isinstance(msg, AIMessage):role = "assistant"elif isinstance(msg, ToolMessage):role = "tool"elif isinstance(msg, SystemMessage):role = "system"else:raise ValueError(f"Unsupported messages type {msg.__class__}")num_tokens += (tokens_per_message+ str_token_counter(role)+ str_token_counter(msg.content))if msg.name:num_tokens += tokens_per_name + str_token_counter(msg.name)return num_tokenstrim_messages(messages,max_tokens=45,strategy="last",token_counter=tiktoken_counter,
)
-
num_tokens = 3
:
这个值的含义是每个回复都由一个起始标记<|start|>assistant<|message|>
开始。这通常是模型的初始状态或开头的标记,它告诉模型回复即将开始。每个消息都会有这些额外的token,初始化为3是为了模拟这个标记的占用。 -
tokens_per_message = 3
:
每个消息(不论是用户、助手、工具还是系统消息)都需要额外的token,这三个token通常是消息头部的结构性标记,像是“<|message|>”等。这个初始化值代表每个消息可能需要的基础token数。比如,它可以是对话中的每一条消息被格式化时需要的最小token数。 -
tokens_per_name = 1
:
这个值表示如果消息中包含一个name
字段(例如在某些工具消息中可能会有),那么这个name
字段本身也会占用一定数量的token。通常情况下,name字段只会占用一个token,因为它是一个单一的标识符。
[AIMessage(content="Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"),HumanMessage(content='what do you call a speechless parrot')]
链接
trim_messages 可以以命令式(上文)或声明式使用,使其易于与链中的其他组件组合:
llm = ChatOpenAI(model="gpt-4o")# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
trimmer = trim_messages(max_tokens=45,strategy="last",token_counter=llm,include_system=True,
)chain = trimmer | llm
chain.invoke(messages)
AIMessage(content='A: A "Polly-gone"!', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 32, 'total_tokens': 41}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_66b29dffce', 'finish_reason': 'stop', 'logprobs': None}, id='run-83e96ddf-bcaa-4f63-824c-98b0f8a0d474-0', usage_metadata={'input_tokens': 32, 'output_tokens': 9, 'total_tokens': 41})
仅从修剪器来看,我们可以看到它是一个可运行的对象,可以像所有可运行对象一样被调用:
trimmer.invoke(messages)
[SystemMessage(content="you're a good assistant, you always respond with a joke."),HumanMessage(content='what do you call a speechless parrot')]
使用聊天消息历史
修剪消息在处理聊天历史时特别有用,因为聊天历史可能会变得非常长:
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistorychat_history = InMemoryChatMessageHistory(messages=messages[:-1])def dummy_get_session_history(session_id):if session_id != "1":return InMemoryChatMessageHistory()return chat_historyllm = ChatOpenAI(model="gpt-4o")trimmer = trim_messages(max_tokens=45,strategy="last",token_counter=llm,include_system=True,
)chain = trimmer | llm
chain_with_history = RunnableWithMessageHistory(chain, dummy_get_session_history)
chain_with_history.invoke([HumanMessage("what do you call a speechless parrot")],config={"configurable": {"session_id": "1"}},
)
在消息传递给模型之前,它们被修剪为仅包含系统消息和最后一条人类消息!
AIMessage(content='A "polly-no-wanna-cracker"!', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 32, 'total_tokens': 42}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_5bf7397cd3', 'finish_reason': 'stop', 'logprobs': None}, id='run-054dd309-3497-4e7b-b22a-c1859f11d32e-0', usage_metadata={'input_tokens': 32, 'output_tokens': 10, 'total_tokens': 42})
过滤消息
在更复杂的链和代理中,我们可能会通过消息列表来跟踪状态。这个列表可能会开始积累来自多个不同模型、发言者、子链等的消息,我们可能只想将这个完整消息列表的子集传递给链/代理中的每个模型调用。
filter_messages 工具使按类型、ID 或名称过滤消息变得简单。
from langchain_core.messages import (AIMessage,HumanMessage,SystemMessage,filter_messages,
)messages = [SystemMessage("you are a good assistant", id="1"),HumanMessage("example input", id="2", name="example_user"),AIMessage("example output", id="3", name="example_assistant"),HumanMessage("real input", id="4", name="bob"),AIMessage("real output", id="5", name="alice"),
]filter_messages(messages, include_types="human")
[HumanMessage(content='example input', name='example_user', id='2'),HumanMessage(content='real input', name='bob', id='4')]
按照name过滤:
filter_messages(messages, exclude_names=["example_user", "example_assistant"])
[SystemMessage(content='you are a good assistant', id='1'),HumanMessage(content='real input', name='bob', id='4'),AIMessage(content='real output', name='alice', id='5')]
按照ID过滤:
filter_messages(messages, include_types=[HumanMessage, AIMessage], exclude_ids=["3"])
[HumanMessage(content='example input', name='example_user', id='2'),HumanMessage(content='real input', name='bob', id='4'),AIMessage(content='real output', name='alice', id='5')]
链接
filter_messages 可以以命令式(如上所示)或声明式使用,使其易于与链中的其他组件组合:
# pip install -U langchain-anthropic
from langchain_anthropic import ChatAnthropicllm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=0)
# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
filter_ = filter_messages(exclude_names=["example_user", "example_assistant"])
chain = filter_ | llm
chain.invoke(messages)
AIMessage(content=[], response_metadata={'id': 'msg_01Wz7gBHahAwkZ1KCBNtXmwA', 'model': 'claude-3-sonnet-20240229', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 16, 'output_tokens': 3}}, id='run-b5d8a3fe-004f-4502-a071-a6c025031827-0', usage_metadata={'input_tokens': 16, 'output_tokens': 3, 'total_tokens': 19})
仅查看 filter_,我们可以看到它是一个可运行对象,可以像所有可运行对象一样被调用:
filter_.invoke(messages)[HumanMessage(content='real input', name='bob', id='4'),AIMessage(content='real output', name='alice', id='5')]
合并相同类型的连续消息
某些模型不支持传递相同类型的连续消息(即相同消息类型的“运行”)。
merge_message_runs 工具使合并相同类型的连续消息变得简单。
from langchain_core.messages import (AIMessage,HumanMessage,SystemMessage,merge_message_runs,
)messages = [SystemMessage("you're a good assistant."),SystemMessage("you always respond with a joke."),HumanMessage([{"type": "text", "text": "i wonder why it's called langchain"}]),HumanMessage("and who is harrison chasing anyways"),AIMessage('Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!'),AIMessage("Why, he's probably chasing after the last cup of coffee in the office!"),
]merged = merge_message_runs(messages)
print("\n\n".join([repr(x) for x in merged]))
SystemMessage(content="you're a good assistant.\nyou always respond with a joke.", additional_kwargs={}, response_metadata={})HumanMessage(content=[{'type': 'text', 'text': "i wonder why it's called langchain"}, 'and who is harrison chasing anyways'], additional_kwargs={}, response_metadata={})AIMessage(content='Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!\nWhy, he\'s probably chasing after the last cup of coffee in the office!', additional_kwargs={}, response_metadata={})
请注意,如果要合并的消息内容是一个内容块的列表,则合并后的消息将包含一个内容块的列表。如果要合并的两个消息都有字符串内容,则这些内容将用换行符连接。
链接
merge_message_runs 可以以命令式(如上所示)或声明式使用,使其易于与链中的其他组件组合:
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import (AIMessage,HumanMessage,SystemMessage,merge_message_runs,
)llm = ChatAnthropic(model="claude-3-sonnet-20240229", temperature=0)
# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
merger = merge_message_runs()
chain = merger | llm
chain.invoke(messages)
AIMessage(content=[], additional_kwargs={}, response_metadata={'id': 'msg_01KNGUMTuzBVfwNouLDpUMwf', 'model': 'claude-3-sonnet-20240229', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 84, 'output_tokens': 3}}, id='run-b908b198-9c24-450b-9749-9d4a8182937b-0', usage_metadata={'input_tokens': 84, 'output_tokens': 3, 'total_tokens': 87})
仅查看合并器,我们可以看到它是一个可运行对象,可以像所有可运行对象一样被调用:
merger.invoke(messages)
[SystemMessage(content="you're a good assistant.\nyou always respond with a joke.", additional_kwargs={}, response_metadata={}),HumanMessage(content=[{'type': 'text', 'text': "i wonder why it's called langchain"}, 'and who is harrison chasing anyways'], additional_kwargs={}, response_metadata={}),AIMessage(content='Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!\nWhy, he\'s probably chasing after the last cup of coffee in the office!', additional_kwargs={}, response_metadata={})]
可以注意到将merger追加到链中后,并没有显式传入message!这是因为,LangChain 中的一种抽象组件(例如 merger 或 llm)通常是一个“可运行的”对象(Runnable),这些对象能够自动处理传入的数据。
也就是说,当你调用 chain.invoke(messages) 时:messages 会首先流经 merger(合并消息)。然后,合并后的消息会被传递给 llm(模型处理)。
merge_message_runs 也可以放在提示之后:
from langchain_core.prompts import ChatPromptTemplateprompt = ChatPromptTemplate([("system", "You're great a {skill}"),("system", "You're also great at explaining things"),("human", "{query}"),]
)
chain = prompt | merger | llm
chain.invoke({"skill": "math", "query": "what's the definition of a convergent series"})
AIMessage(content='A convergent series is an infinite series whose partial sums approach a finite value as more terms are added. In other words, the sequence of partial sums has a limit.\n\nMore formally, an infinite series Σ an (where an are the terms of the series) is said to be convergent if the sequence of partial sums:\n\nS1 = a1\nS2 = a1 + a2 \nS3 = a1 + a2 + a3\n...\nSn = a1 + a2 + a3 + ... + an\n...\n\nconverges to some finite number S as n goes to infinity. We write:\n\nlim n→∞ Sn = S\n\nThe finite number S is called the sum of the convergent infinite series.\n\nIf the sequence of partial sums does not approach any finite limit, the infinite series is said to be divergent.\n\nSome key properties:\n- A series converges if and only if the sequence of its partial sums is a Cauchy sequence.\n- Absolute/conditional convergence criteria help determine if a given series converges.\n- Convergent series have many important applications in mathematics, physics, engineering etc.', additional_kwargs={}, response_metadata={'id': 'msg_01MfV6y2hep7ZNvDz24A36U4', 'model': 'claude-3-sonnet-20240229', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 29, 'output_tokens': 267}}, id='run-9d925f58-021e-4bd0-94fc-f8f5e91010a4-0', usage_metadata={'input_tokens': 29, 'output_tokens': 267, 'total_tokens': 296})
Prompt组件
提示词模板有助于将用户输入和参数转换为语言模型的指令。 这可以用于指导模型的响应,帮助其理解上下文并生成相关且连贯的基于语言的输出。
提示词模板的输入是一个字典,其中每个键表示要填充的提示词模板中的变量。
提示词模板输出一个 PromptValue。此 PromptValue 可以传递给 LLM 或 ChatModel,也可以转换为字符串或消息列表。 这个 PromptValue 的存在是为了方便在字符串和消息之间切换。
字符串Prompt模板
这些提示词模板用于格式化单个字符串,通常用于更简单的输入。 例如,构造和使用 PromptTemplate 的一种常见方式如下:
from langchain_core.prompts import PromptTemplateprompt_template = PromptTemplate.from_template("Tell me a joke about {topic}")prompt = prompt_template.invoke({"topic": "cats"})
print(prompt)
print(type(prompt))
text='Tell me a joke about cats'
<class 'langchain_core.prompt_values.StringPromptValue'>
看到{var}
符号,其实很容易就可以猜到,langChain prompt模板是使用了jinja2模板语法的,模板语法都大差不差,再比如ollama的ModelFile的模板语法就是采用的golang的模板语法。
部分格式化提示词模板
LangChain 以两种方式支持部分格式化提示词模板:
- 使用字符串值进行部分格式化。
- 使用返回字符串值的函数进行部分格式化。
例如,假设您有一个提示词模板,需要两个变量,foo 和 baz。如果您在链中早期获得了 foo 值,但稍后才获得 baz 值,那么将两个变量传递整个链可能会很不方便。相反,您可以使用 foo 值部分格式化提示词模板,然后将部分格式化的提示词模板传递下去并仅使用它。下面是一个实现此操作的示例:
from langchain_core.prompts import PromptTemplateprompt = PromptTemplate.from_template("{foo}{bar}")
partial_prompt = prompt.partial(foo="foo")
print(partial_prompt.format(bar="baz"))
# foobaz
还可以仅使用部分变量初始化提示词。
prompt = PromptTemplate(template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"}
)
print(prompt.format(bar="baz"))
# foobaz
另一个常见的用法是与函数进行部分应用。使用场景是当您有一个变量,您知道总是想以一种常见的方式获取它。一个典型的例子是日期或时间。想象一下,您有一个提示词,您总是希望它包含当前日期。您不能在提示词中硬编码它,并且将其与其他输入变量一起传递是不方便的。在这种情况下,能够使用一个始终返回当前日期的函数来部分应用提示词是很方便的。
from datetime import datetimedef _get_datetime():now = datetime.now()return now.strftime("%m/%d/%Y, %H:%M:%S")prompt = PromptTemplate(template="Tell me a {adjective} joke about the day {date}",input_variables=["adjective", "date"],
)
partial_prompt = prompt.partial(date=_get_datetime)
print(partial_prompt.format(adjective="funny"))
# Tell me a funny joke about the day 04/21/2024, 19:43:57
还可以仅使用部分变量初始化提示词,这在此工作流程中通常更有意义。
prompt = PromptTemplate(template="Tell me a {adjective} joke about the day {date}",input_variables=["adjective"],partial_variables={"date": _get_datetime},
)
print(prompt.format(adjective="funny"))
Tell me a funny joke about the day 04/21/2024, 19:43:57
聊天Prompt模板
这些提示词模板用于格式化消息列表。这些“模板”本身由一系列模板组成。 例如,构建和使用 ChatPromptTemplate 的一种常见方式如下:
from langchain_core.prompts import ChatPromptTemplateprompt_template = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"),("user", "Tell me a joke about {topic}")
])prompt = prompt_template.invoke({"topic": "cats"})
print(prompt)
print(type(prompt))
messages=[SystemMessage(content='You are a helpful assistant', additional_kwargs={}, response_metadata={}), HumanMessage(content='Tell me a joke about cats', additional_kwargs={}, response_metadata={})]
<class 'langchain_core.prompt_values.ChatPromptValue'>
在上述示例中,当调用此 ChatPromptTemplate 时,将构造两个消息。 第一个是SystemMessage,没有变量需要格式化。 第二个是 HumanMessage,将由用户传入的 topic 变量进行格式化。
组合提示词
LangChain 提供了一个用户友好的界面,用于将提示词的不同部分组合在一起。您可以使用字符串提示词或聊天提示词来实现这一点。以这种方式构建提示词可以方便地重用组件。
在处理字符串提示词时,每个模板是连接在一起的。您可以直接使用提示词或字符串(列表中的第一个元素需要是一个提示词)。
from langchain_core.prompts import PromptTemplateprompt = (PromptTemplate.from_template("Tell me a joke about {topic}")+ ", make it funny"+ "\n\nand in {language}"
)print(prompt)
PromptTemplate(input_variables=['language', 'topic'], template='Tell me a joke about {topic}, make it funny\n\nand in {language}')
prompt.format(topic="sports", language="spanish")'Tell me a joke about sports, make it funny\n\nand in spanish'
聊天提示词由一系列消息组成。与上面的示例类似,我们可以连接聊天提示词模板。每个新元素都是最终提示词中的一条新消息。
from langchain_core.messages import AIMessage, HumanMessage, SystemMessageprompt = SystemMessage(content="You are a nice pirate")
然后,您可以轻松创建一个管道,将其与其他消息 或 消息模板结合起来。 当没有变量需要格式化时使用 Message,当有变量需要格式化时使用 MessageTemplate。您也可以仅使用一个字符串(注意:这将自动推断为一个 HumanMessagePromptTemplate)。
new_prompt = (prompt + HumanMessage(content="hi") + AIMessage(content="what?") + "{input}"
)
在底层,这会创建一个 ChatPromptTemplate 类的实例,因此您可以像之前一样使用它!
new_prompt.format_messages(input="i said hi")[SystemMessage(content='You are a nice pirate'),HumanMessage(content='hi'),AIMessage(content='what?'),HumanMessage(content='i said hi')]
LangChain 还包含一个名为 PipelinePromptTemplate 的类,当想重用提示词的部分时,这个类非常有用。PipelinePrompt 由两个主要部分组成:
- 最终提示词:返回的最终提示词
- 管道提示词:一个元组列表,由字符串名称和提示词模板组成。每个提示词模板将被格式化,然后作为具有相同名称的变量传递给未来的提示词模板。
from langchain_core.prompts import PipelinePromptTemplate, PromptTemplatefull_template = """{introduction}{example}{start}"""
full_prompt = PromptTemplate.from_template(full_template)introduction_template = """You are impersonating {person}."""
introduction_prompt = PromptTemplate.from_template(introduction_template)example_template = """Here's an example of an interaction:Q: {example_q}
A: {example_a}"""
example_prompt = PromptTemplate.from_template(example_template)start_template = """Now, do this for real!Q: {input}
A:"""
start_prompt = PromptTemplate.from_template(start_template)input_prompts = [("introduction", introduction_prompt),("example", example_prompt),("start", start_prompt),
]
pipeline_prompt = PipelinePromptTemplate(final_prompt=full_prompt, pipeline_prompts=input_prompts
)pipeline_prompt.input_variables
# ['person', 'example_a', 'example_q', 'input']
print(pipeline_prompt.format(person="Elon Musk",example_q="What's your favorite car?",example_a="Tesla",input="What's your favorite social media site?",)
)You are impersonating Elon Musk.Here's an example of an interaction:Q: What's your favorite car?
A: TeslaNow, do this for real!Q: What's your favorite social media site?
A:
消息占位符Placeholder
此提示词模板负责在特定位置添加消息列表。 在上面的 ChatPromptTemplate 中,我们看到如何格式化两个消息,每个消息都是一个字符串。 但是如果我们希望用户传入一个消息列表,并将其插入到特定位置呢? 这就是如何使用 MessagesPlaceholder。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessageprompt_template = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"),MessagesPlaceholder("msgs")
])prompt = prompt_template.invoke({"msgs": [HumanMessage(content="hi!")]})
print(prompt)
messages=[SystemMessage(content='You are a helpful assistant', additional_kwargs={}, response_metadata={}), HumanMessage(content='hi!', additional_kwargs={}, response_metadata={})]
这将生成一个包含两个消息的列表,第一个是系统消息,第二个是我们传入的 HumanMessage。 如果我们传入了 5 条消息,那么总共将生成 6 条消息(系统消息加上 5 条传入的消息)。 这对于将消息列表插入到特定位置非常有用。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessageprompt_template = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"),MessagesPlaceholder("msgs")
])prompt = prompt_template.invoke({"msgs": [HumanMessage(content="hi!"), HumanMessage(content="hello!")]})
print(prompt)
messages=[SystemMessage(content='You are a helpful assistant', additional_kwargs={}, response_metadata={}), HumanMessage(content='hi!', additional_kwargs={}, response_metadata={}), HumanMessage(content='fickl', additional_kwargs={}, response_metadata={})]
一种不显式使用 MessagesPlaceholder 类来实现相同功能的替代方法是:
prompt_template = ChatPromptTemplate.from_messages([("system", "You are a helpful assistant"),("placeholder", "{msgs}") # <-- This is the changed part
])
但这里还是推荐使用MessagesPlaceholder占位,从代码可读性上更胜一筹!
示例选择器
一种常见的提示技术是将示例作为提示的一部分,以实现更好的性能。 这被称为少量示例提示(FewShotPromp)。 这给语言模型提供了具体的示例,说明它应该如何表现。 有时这些示例是硬编码到提示中的,但在更高级的情况下,动态选择它们可能更好。
示例选择器则是负责选择并将示例格式化为提示的类。
如果你有大量示例并且可能需要选择哪些示例包含在提示中,而示例选择器是负责执行此操作的类。基本接口定义如下:
class BaseExampleSelector(ABC):"""Interface for selecting examples to include in prompts."""@abstractmethoddef select_examples(self, input_variables: Dict[str, str]) -> List[dict]:"""Select which examples to use based on the inputs."""@abstractmethoddef add_example(self, example: Dict[str, str]) -> Any:"""Add new example to store."""
它需要定义的唯一方法是 select_examples 方法。该方法接受输入变量,然后返回一个示例列表。如何选择这些示例由每个具体实现决定。
LangChain 有几种不同类型的示例选择器:
名称 | 描述 |
---|---|
相似性 | 使用输入和示例之间的语义相似性来决定选择哪些示例。 |
MMR | 使用输入和示例之间的最大边际相关性来决定选择哪些示例。 |
长度 | 根据可以适应特定长度的示例数量来选择示例。 |
Ngram | 使用输入和示例之间的n-gram重叠来决定选择哪些示例。 |
为了使用示例选择器,我们需要创建一个示例列表。这些通常应该是示例输入和输出。为了演示的目的,让我们想象一下我们正在选择如何将英语翻译成意大利语的示例。
examples = [// 输入:英语// 输入:意大利语{"input": "hi", "output": "ciao"},{"input": "bye", "output": "arrivederci"},{"input": "soccer", "output": "calcio"},
]
自定义示例选择器
编写一个示例选择器,根据单词的长度选择要挑选的示例:
from langchain_core.example_selectors.base import BaseExampleSelectorclass CustomExampleSelector(BaseExampleSelector):def __init__(self, examples):self.examples = examplesdef add_example(self, example):self.examples.append(example)def select_examples(self, input_variables):# 这里假设输入中会有一个名为 'input' 的键new_word = input_variables["input"]new_word_length = len(new_word)# 初始化变量以存储最佳匹配和其长度差异best_match = Nonesmallest_diff = float("inf")# 遍历每个示例for example in self.examples:# 计算与当前示例第一个单词的长度差异current_diff = abs(len(example["input"]) - new_word_length)# 如果当前的长度差异更小,则更新最佳匹配if current_diff < smallest_diff:smallest_diff = current_diffbest_match = examplereturn [best_match]
然后初始化使用:
example_selector = CustomExampleSelector(examples)
example_selector.select_examples({"input": "okay"})
>>>[{'input': 'bye', 'output': 'arrivederci'}]example_selector.add_example({"input": "hand", "output": "mano"})
example_selector.select_examples({"input": "okay"})
>>>[{'input': 'hand', 'output': 'mano'}]
我们现在可以在提示中使用这个示例选择器:
from langchain_core.prompts.few_shot import FewShotPromptTemplate
from langchain_core.prompts.prompt import PromptTemplateexample_prompt = PromptTemplate.from_template("Input: {input} -> Output: {output}")
prompt = FewShotPromptTemplate(example_selector=example_selector,example_prompt=example_prompt,suffix="Input: {input} -> Output:",prefix="Translate the following words from English to Italian:",input_variables=["input"],
)print(prompt.format(input="word"))
Translate the following words from English to Italian:Input: hand -> Output: manoInput: word -> Output:
按长度选择示例
此示例选择器根据长度选择要使用的示例。当您担心构建的提示会超出上下文窗口的长度时,这非常有用。对于较长的输入,它将选择较少的示例进行包含,而对于较短的输入,它将选择更多的示例。
from langchain_core.example_selectors import LengthBasedExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate# 一个假设任务的示例:创建反义词。
examples = [{"input": "happy", "output": "sad"},{"input": "tall", "output": "short"},{"input": "energetic", "output": "lethargic"},{"input": "sunny", "output": "gloomy"},{"input": "windy", "output": "calm"},
]example_prompt = PromptTemplate(input_variables=["input", "output"],template="Input: {input}\nOutput: {output}",
)
example_selector = LengthBasedExampleSelector(# 它可供选择的示例。examples=examples,# 用于格式化示例的PromptTemplate。example_prompt=example_prompt,# 格式化后的示例应该达到的最大长度。# 长度是通过下面的 get_text_length 函数来衡量的。max_length=25,# 用于获取字符串长度的函数,它用于确定要包含哪些示例。如果没有指定,则使用默认值。# get_text_length: Callable[[str], int] = lambda x: len(re.split("\n| ", x))
)
dynamic_prompt = FewShotPromptTemplate(# 我们提供了一个 ExampleSelector 而不是直接提供示例。example_selector=example_selector,example_prompt=example_prompt,prefix="Give the antonym of every input",suffix="Input: {adjective}\nOutput:",input_variables=["adjective"],
)
输入一个长度较小的“输入”,则会拼接较多示例:
# An example with small input, so it selects all examples.
print(dynamic_prompt.format(adjective="big"))>>>
Give the antonym of every inputInput: happy
Output: sadInput: tall
Output: shortInput: energetic
Output: lethargicInput: sunny
Output: gloomyInput: windy
Output: calmInput: big
Output:
输入一个长文本,由于length的限制,则会返回很少的样例:
# An example with long input, so it selects only one example.
long_string = "big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else"
print(dynamic_prompt.format(adjective=long_string))
Give the antonym of every inputInput: happy
Output: sadInput: big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else
Output:
也可以添加新的案例:
# You can add an example to an example selector as well.
new_example = {"input": "big", "output": "small"}
dynamic_prompt.example_selector.add_example(new_example)
print(dynamic_prompt.format(adjective="enthusiastic"))
通过相似性选择示例
该对象根据与输入的相似性选择示例。它通过找到与输入具有最大余弦相似度的嵌入示例来实现这一点。
余弦相似度基于词频(如词袋模型或TF-IDF)进行计算,因此忽略了词序和上下文信息。这意味着,如果两个句子在语法结构上不同但使用了相同的词汇,它们的相似度可能会很高,而实际语义可能并不相同。
OpenAIEmbeddings 将句子(而不仅仅是单词)转化为一个高维的向量,这个向量捕捉到了句子层次的语义信息。因此,余弦相似度在计算句子之间的相似度时,能够更好地反映出它们的语义相似性。
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
from langchain_openai import OpenAIEmbeddingsexample_prompt = PromptTemplate(input_variables=["input", "output"],template="Input: {input}\nOutput: {output}",
)# Examples of a pretend task of creating antonyms.
examples = [{"input": "happy", "output": "sad"},{"input": "tall", "output": "short"},{"input": "energetic", "output": "lethargic"},{"input": "sunny", "output": "gloomy"},{"input": "windy", "output": "calm"},
]example_selector = SemanticSimilarityExampleSelector.from_examples(# The list of examples available to select from.examples,# The embedding class used to produce embeddings which are used to measure semantic similarity.OpenAIEmbeddings(),# The VectorStore class that is used to store the embeddings and do a similarity search over.Chroma,# The number of examples to produce.k=1,
)
similar_prompt = FewShotPromptTemplate(# We provide an ExampleSelector instead of examples.example_selector=example_selector,example_prompt=example_prompt,prefix="Give the antonym of every input",suffix="Input: {adjective}\nOutput:",input_variables=["adjective"],
)
输入一个表示情感的词汇:
# Input is a feeling, so should select the happy/sad example
print(similar_prompt.format(adjective="worried"))>>>
Give the antonym of every inputInput: happy
Output: sadInput: worried
Output:
通过 n-gram 重叠选择示例
n-gram 重叠(n-gram overlap)是指两个文本(或句子)在n-gram(n元组)级别上的相似度,通常用于衡量两个文本之间的相似性或重合程度。
n-gram 重叠度指的是两个文本中共同出现的n-gram数量。假设你有两个文本,文本A和文本B,n-gram重叠就是计算它们在n-gram层面上重合的部分。比如,两个文本中同时出现了多少相同的2-gram(bigram)。
文本A:"I love dogs"
文本B:"I love cats"文本A的2-gram(bigram):["I love", "love dogs"]
文本B的2-gram(bigram):["I love", "love cats"]
它们的2-gram重叠是[“I love”],因为它们的2-gram中有"I love"是相同的。
计算重叠度:常用的计算方式包括:
- 重叠率:共享的n-gram数量除以文本A或文本B中的n-gram总数。
- Jaccard相似度:Jaccard(𝐴,𝐵)=∣𝐴∩𝐵∣/∣𝐴∪𝐵∣,即共享n-gram的数量除以A和B所有n-gram的并集的大小。
n-gram重叠并不考虑词语的语义,只关注词的顺序和出现。因此,如果两个文本表达相似的意义但使用了不同的词汇,n-gram重叠可能无法反映这一相似性。
NGramOverlapExampleSelector 根据与输入的 ngram 重叠分数选择和排序示例。ngram 重叠分数是一个介于 0.0 和 1.0 之间的浮点数(包括 0.0 和 1.0)。
选择器允许设置阈值分数。ngram 重叠分数小于或等于阈值的示例将被排除。默认情况下,阈值设置为 -1.0,因此不会排除任何示例,只会重新排序。将阈值设置为 0.0 将排除与输入没有 ngram 重叠的示例。
from langchain_community.example_selectors import NGramOverlapExampleSelector
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplateexample_prompt = PromptTemplate(input_variables=["input", "output"],template="Input: {input}\nOutput: {output}",
)# Examples of a fictional translation task.
examples = [{"input": "See Spot run.", "output": "Ver correr a Spot."},{"input": "My dog barks.", "output": "Mi perro ladra."},{"input": "Spot can run.", "output": "Spot puede correr."},
]
example_selector = NGramOverlapExampleSelector(# 提供可以选择的示例。examples=examples,# 用于格式化示例的PromptTemplate。example_prompt=example_prompt,# 阈值,达到该阈值时选择器停止。# 默认设置为-1.0。threshold=-1.0,# 对于负阈值:# 选择器按n-gram重叠得分对示例进行排序,并且不排除任何示例。# 对于大于1.0的阈值:# 选择器排除所有示例,并返回一个空列表。# 对于阈值等于0.0:# 选择器按n-gram重叠得分对示例进行排序,# 并排除那些与输入没有n-gram重叠的示例。
)dynamic_prompt = FewShotPromptTemplate(# 我们提供一个ExampleSelector,而不是直接提供示例。example_selector=example_selector,example_prompt=example_prompt,prefix="给出每个输入的西班牙语翻译",suffix="输入: {sentence}\n输出:",input_variables=["sentence"],
)
# An example input with large ngram overlap with "Spot can run."
# and no overlap with "My dog barks."
print(dynamic_prompt.format(sentence="Spot can run fast."))>>>
Give the Spanish translation of every inputInput: Spot can run.
Output: Spot puede correr.Input: See Spot run.
Output: Ver correr a Spot.Input: My dog barks.
Output: Mi perro ladra.Input: Spot can run fast.
Output:
也可以动态增加案例:
# You can add examples to NGramOverlapExampleSelector as well.
new_example = {"input": "Spot plays fetch.", "output": "Spot juega a buscar."}example_selector.add_example(new_example)
print(dynamic_prompt.format(sentence="Spot can run fast."))
或者修改阈值:
# 你可以设置一个阈值,用于排除示例。
# 例如,将阈值设置为0.0,
# 这样会排除那些与输入没有n-gram重叠的示例。
# 由于“My dog barks.”与“Spot can run fast.”没有n-gram重叠,
# 所以它会被排除。
example_selector.threshold = 0.0
print(dynamic_prompt.format(sentence="Spot can run fast."))
Give the Spanish translation of every inputInput: Spot can run.
Output: Spot puede correr.Input: See Spot run.
Output: Ver correr a Spot.Input: Spot plays fetch.
Output: Spot juega a buscar.Input: Spot can run fast.
Output:
通过最大边际相关性 (MMR) 选择示例
MaxMarginalRelevanceExampleSelector 根据与输入最相似的示例的组合来选择示例,同时优化多样性。它通过找到与输入具有最大余弦相似度的嵌入示例来实现这一点,然后在迭代添加这些示例的同时,对与已选择示例的接近程度进行惩罚。
from langchain_community.vectorstores import FAISS
from langchain_core.example_selectors import (MaxMarginalRelevanceExampleSelector,SemanticSimilarityExampleSelector,
)
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
from langchain_openai import OpenAIEmbeddingsexample_prompt = PromptTemplate(input_variables=["input", "output"],template="Input: {input}\nOutput: {output}",
)# Examples of a pretend task of creating antonyms.
examples = [{"input": "happy", "output": "sad"},{"input": "tall", "output": "short"},{"input": "energetic", "output": "lethargic"},{"input": "sunny", "output": "gloomy"},{"input": "windy", "output": "calm"},
]
example_selector = MaxMarginalRelevanceExampleSelector.from_examples(# The list of examples available to select from.examples,# The embedding class used to produce embeddings which are used to measure semantic similarity.OpenAIEmbeddings(),# The VectorStore class that is used to store the embeddings and do a similarity search over.FAISS,# The number of examples to produce.k=2,
)
mmr_prompt = FewShotPromptTemplate(# We provide an ExampleSelector instead of examples.example_selector=example_selector,example_prompt=example_prompt,prefix="Give the antonym of every input",suffix="Input: {adjective}\nOutput:",input_variables=["adjective"],
)
# Input is a feeling, so should select the happy/sad example as the first one
print(mmr_prompt.format(adjective="worried"))
FewShotPrompt
提高模型性能的最有效方法之一是给模型提供希望它执行的示例。将示例输入和预期输出 添加到模型提示中的技术称为“少量示例提示”。
在进行少量示例提示时,有几个需要考虑的事项:
- 示例是如何生成的?
- 每个提示中有多少个示例?
- 示例是如何在运行时选择的?
- 示例在提示中是如何格式化的?
少量示例格式化器
我们首先为少量示例创建格式化器(模板),将少量示例格式化为字符串。该格式化器应为 PromptTemplate 对象。
from langchain_core.prompts import PromptTemplateexample_prompt = PromptTemplate.from_template("Question: {question}\n{answer}")
接下来,我们将创建一个少量示例的列表。每个示例应该是一个字典,表示我们上面定义的格式化提示的示例输入。
examples = [{"question": "Who lived longer, Muhammad Ali or Alan Turing?","answer": """
Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
""",},{"question": "When was the founder of craigslist born?","answer": """
Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952
""",},{"question": "Who was the maternal grandfather of George Washington?","answer": """
Are follow up questions needed here: Yes.
Follow up: Who was the mother of George Washington?
Intermediate answer: The mother of George Washington was Mary Ball Washington.
Follow up: Who was the father of Mary Ball Washington?
Intermediate answer: The father of Mary Ball Washington was Joseph Ball.
So the final answer is: Joseph Ball
""",},{"question": "Are both the directors of Jaws and Casino Royale from the same country?","answer": """
Are follow up questions needed here: Yes.
Follow up: Who is the director of Jaws?
Intermediate Answer: The director of Jaws is Steven Spielberg.
Follow up: Where is Steven Spielberg from?
Intermediate Answer: The United States.
Follow up: Who is the director of Casino Royale?
Intermediate Answer: The director of Casino Royale is Martin Campbell.
Follow up: Where is Martin Campbell from?
Intermediate Answer: New Zealand.
So the final answer is: No
""",},
]
测试格式化提示:
print(example_prompt.invoke(examples[0]).to_string())
Question: Who lived longer, Muhammad Ali or Alan Turing?Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
最后,创建一个 FewShotPromptTemplate 对象。该对象接受少量示例和少量示例的格式化器。当这个 FewShotPromptTemplate 被格式化时,它使用 example_prompt 格式化传递的示例,然后将它们添加到最终提示的 suffix 之前:
from langchain_core.prompts import FewShotPromptTemplateprompt = FewShotPromptTemplate(examples=examples,example_prompt=example_prompt,suffix="Question: {input}",input_variables=["input"],
)print(prompt.invoke({"input": "Who was the father of Mary Ball Washington?"}).to_string()
)
打印结果如下:
Question: Who lived longer, Muhammad Ali or Alan Turing?Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad AliQuestion: When was the founder of craigslist born?Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952Question: Who was the maternal grandfather of George Washington?Are follow up questions needed here: Yes.
Follow up: Who was the mother of George Washington?
Intermediate answer: The mother of George Washington was Mary Ball Washington.
Follow up: Who was the father of Mary Ball Washington?
Intermediate answer: The father of Mary Ball Washington was Joseph Ball.
So the final answer is: Joseph BallQuestion: Are both the directors of Jaws and Casino Royale from the same country?Are follow up questions needed here: Yes.
Follow up: Who is the director of Jaws?
Intermediate Answer: The director of Jaws is Steven Spielberg.
Follow up: Where is Steven Spielberg from?
Intermediate Answer: The United States.
Follow up: Who is the director of Casino Royale?
Intermediate Answer: The director of Casino Royale is Martin Campbell.
Follow up: Where is Martin Campbell from?
Intermediate Answer: New Zealand.
So the final answer is: NoQuestion: Who was the father of Mary Ball Washington?
结合示例选择器对示例进行筛选
将上文提及到的示例输入到一个名为 SemanticSimilarityExampleSelector 的 ExampleSelector 实现实例中。该类根据输入与初始集中的少量示例的相似性选择少量示例。它使用嵌入模型计算输入与少量示例之间的相似性,并使用向量存储执行最近邻搜索。
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddingsexample_selector = SemanticSimilarityExampleSelector.from_examples(# This is the list of examples available to select from.examples,# This is the embedding class used to produce embeddings which are used to measure semantic similarity.OpenAIEmbeddings(),# This is the VectorStore class that is used to store the embeddings and do a similarity search over.Chroma,# This is the number of examples to produce.k=1,
)
现在,创建一个 FewShotPromptTemplate 对象,这次我们要传入示例选择器。
example_prompt = PromptTemplate.from_template("Question: {question}\n{answer}")prompt = FewShotPromptTemplate(example_selector=example_selector,example_prompt=example_prompt,suffix="Question: {input}",input_variables=["input"],
)print(prompt.invoke({"input": "Who was the father of Mary Ball Washington?"}).to_string()
)
可以看到,我们的问题经过调用后,会先经过一遍模型进行向量化,然后根据相似度匹配得到最佳示例,然后填充到prompt模板中:
Question: Who was the maternal grandfather of George Washington?Are follow up questions needed here: Yes.
Follow up: Who was the mother of George Washington?
Intermediate answer: The mother of George Washington was Mary Ball Washington.
Follow up: Who was the father of Mary Ball Washington?
Intermediate answer: The father of Mary Ball Washington was Joseph Ball.
So the final answer is: Joseph BallQuestion: Who was the father of Mary Ball Washington?
在聊天模型中使用少量示例
我们给LLM一个不熟悉的数学运算符,用“🦜”表情符号表示,如果我们尝试询问模型这个表达式的结果,它将失败。
因为大模型的权重文件是没有关于这个“🦜”运算符的知识的,因此我们需要添加一些案例,让大模型“明白”自己需要做什么。
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplateexamples = [{"input": "2 🦜 2", "output": "4"},{"input": "2 🦜 3", "output": "5"},
]# 接下来,将它们组装成少量示例提示模板。
example_prompt = ChatPromptTemplate.from_messages([("human", "{input}"),("ai", "{output}"),]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(example_prompt=example_prompt,examples=examples,
)print(few_shot_prompt.invoke({}).to_messages())
# [HumanMessage(content='2 🦜 2'), AIMessage(content='4'), HumanMessage(content='2 🦜 3'), AIMessage(content='5')]# 最后,我们将最终提示组装如下,将 few_shot_prompt 直接传递给 from_messages 工厂方法,并与模型一起使用:
final_prompt = ChatPromptTemplate.from_messages([("system", "You are a wondrous wizard of math."),few_shot_prompt,("human", "{input}"),]
)
现在让我们向模型提出最初的问题:
from langchain_openai import ChatOpenAIchain = final_prompt | modelchain.invoke({"input": "What is 2 🦜 9?"})
AIMessage(content='11', response_metadata={'token_usage': {'completion_tokens': 1, 'prompt_tokens': 60, 'total_tokens': 61}, 'model_name': 'gpt-4o-mini', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-5ec4e051-262f-408e-ad00-3f2ebeb561c3-0', usage_metadata={'input_tokens': 60, 'output_tokens': 1, 'total_tokens': 61})
看样子大模型已经可以很好的理解,🦜运算符等价于加法运算符。
有时我们可能希望根据输入仅选择整体集合中的少量示例进行展示。为此,可以用 example_selector 替换传递给 FewShotChatMessagePromptTemplate 的 examples。
- example_selector:负责为给定输入选择少量示例(以及返回的顺序)。这些实现了 BaseExampleSelector 接口。一个常见的例子是基于向量存储的 SemanticSimilarityExampleSelector
- example_prompt:通过其 format_messages 方法将每个示例转换为 1 个或多个消息。一个常见的例子是将每个示例转换为一个人类消息和一个 AI 消息响应,或者一个人类消息后跟一个函数调用消息。
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddingsexamples = [{"input": "2 🦜 2", "output": "4"},{"input": "2 🦜 3", "output": "5"},{"input": "2 🦜 4", "output": "6"},{"input": "What did the cow say to the moon?", "output": "nothing at all"},{"input": "Write me a poem about the moon","output": "One for the moon, and one for me, who are we to talk about the moon?",},
]to_vectorize = [" ".join(example.values()) for example in examples]
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(to_vectorize, embeddings, metadatas=examples)
这里的Chroma是一个内存向量库!https://github.com/chroma-core/chroma
向量数据库其实最早在传统的人工智能和机器学习场景中就有所应用。在大模型兴起后,由于目前大模型的token数限制,很多开发者倾向于将数据量庞大的知识、新闻、文献、语料等先通过嵌入(embedding)算法转变为向量数据,然后存储在Chroma等向量数据库中。当用户在大模型输入问题后,将问题本身也embedding,转化为向量,在向量数据库中查找与之最匹配的相关知识,组成大模型的上下文,将其输入给大模型,最终返回大模型处理后的文本给用户,这种方式不仅降低大模型的计算量,提高响应速度,也降低成本,并避免了大模型的tokens限制,是一种简单高效的处理手段。此外,向量数据库还在大模型记忆存储等领域发挥其不可替代的作用。
主流的向量数据库对比如下所示:
向量数据库 | URL | GitHub Star | Language |
---|---|---|---|
chroma | https://github.com/chroma-core/chroma | 7.4K | Python |
milvus | https://github.com/milvus-io/milvus | 21.5K | Go/Python/C++ |
pinecone | https://www.pinecone.io/ | ❌ | ❌ |
qdrant | https://github.com/qdrant/qdrant | 11.8K | Rust |
typesense | https://github.com/typesense/typesense | 12.9K | C++ |
weaviate | https://github.com/weaviate/weaviate | 6.9K | Go |
目前比较火的有milvus和chroma,chroma凭借其轻量级(如同sqllite比较与mysql)也是声名大噪:
创建了向量存储后,我们可以创建 example_selector。在这里,我们将单独调用它,并将 k 设置为仅获取与输入最接近的两个示例。
example_selector = SemanticSimilarityExampleSelector(vectorstore=vectorstore,k=2,
)# The prompt template will load examples by passing the input do the `select_examples` method
example_selector.select_examples({"input": "horse"})
[{'input': 'What did the cow say to the moon?', 'output': 'nothing at all'},{'input': '2 🦜 4', 'output': '6'}]
我们现在组装提示模板,使用上面创建的 example_selector:
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate# Define the few-shot prompt.
few_shot_prompt = FewShotChatMessagePromptTemplate(# The input variables select the values to pass to the example_selectorinput_variables=["input"],example_selector=example_selector,# Define how each example will be formatted.# In this case, each example will become 2 messages:# 1 human, and 1 AIexample_prompt=ChatPromptTemplate.from_messages([("human", "{input}"), ("ai", "{output}")]),
)print(few_shot_prompt.invoke(input="What's 3 🦜 3?").to_messages())
当我们询问🦜运算符时,选择器帮我们选择了最高相似度的2个示例:
[HumanMessage(content='2 🦜 3'), AIMessage(content='5'), HumanMessage(content='2 🦜 4'), AIMessage(content='6')]
我们可以将这个少量示例聊天消息提示模板传递到另一个聊天提示模板中:
final_prompt = ChatPromptTemplate.from_messages([("system", "You are a wondrous wizard of math."),few_shot_prompt,("human", "{input}"),]
)print(few_shot_prompt.invoke(input="What's 3 🦜 3?"))
最后,可以将模型连接到少量示例提示:
chain = final_prompt | ChatOpenAI(model="gpt-4o-mini", temperature=0.0)chain.invoke({"input": "What's 3 🦜 3?"})
我们的输入会经过prompt富化后再询问模型,这样询问模型的效果会显著提高。
文档加载器组件
LangChain与各种数据源有数百个集成,可以从中加载数据。每个DocumentLoader都有其特定的参数,但它们都可以通过.load方法以相同的方式调用。 一个示例用例如下:
from langchain_community.document_loaders.csv_loader import CSVLoaderloader = CSVLoader(... # <-- Integration specific parameters here
)
data = loader.load()
做RAG的时候可以基于langChain文档加载器封装形成更加便捷的文档加载器。
加载PDF文件
可移植文档格式 (PDF),标准化为ISO 32000,是由Adobe于1992年开发的一种文件格式,用于以独立于应用软件、硬件和操作系统的方式呈现文档,包括文本格式和图像。
PDF中的文本通常通过文本框表示。它们也可能包含图像。PDF解析器可能会执行以下某种组合:
- 通过启发式或机器学习推断将文本框聚合成行、段落和其他结构;
- 对图像运行光学字符识别 (OCR)以检测其中的文本;
- 将文本分类为段落、列表、表格或其他结构;
- 将文本结构化为表格行和列,或键值对。
LangChain与多种PDF解析器集成。一些解析器简单且相对低级;其他解析器将支持OCR和图像处理,或执行高级文档布局分析。正确的选择将取决于您的需求。
我曾经负责写过某产品中的pdf解析器,具体可以参考:python操作PDF中各类文本内容的方法,其中OCR引擎使用huggingface上下载量较大的GOT-2.0,号称下一代OCR光学字符识别引擎。
简单快速的文本提取
如果你的pdf只有简单的文本,那么下面的方法是合适的你的需求的。简单文本pdf对于熟悉pdf协议的人完全可以自己写代码实现,不过langChain已经为我们提供了与第三方包的桥梁。
LangChain 文档加载器实现了lazy_load及其异步变体alazy_load,返回Document对象的迭代器。
pip install -qU pypdf
from langchain_community.document_loaders import PyPDFLoaderloader = PyPDFLoader(file_path)
pages = []
async for page in loader.alazy_load():pages.append(page)
print(f"{pages[0].metadata}\n")
print(pages[0].page_content)
可以看到,每个文档的元数据存储了相应的页码。
{'source': '../../docs/integrations/document_loaders/example_data/layout-parser-paper.pdf', 'page': 0}LayoutParser : A Unified Toolkit for Deep
Learning Based Document Image Analysis
Zejiang Shen1( �), Ruochen Zhang2, Melissa Dell3, Benjamin Charles Germain
...
PDF上的向量搜索
一旦我们将PDF加载到LangChain Document对象中,我们可以以通常的方式对其进行索引(例如,RAG应用)。下面我们使用OpenAI嵌入,尽管任何LangChain 嵌入模型都可以。
pip install -qU langchain-openai
import getpass
import osif "OPENAI_API_KEY" not in os.environ:os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddingsvector_store = InMemoryVectorStore.from_documents(pages, OpenAIEmbeddings())
docs = vector_store.similarity_search("What is LayoutParser?", k=2)
for doc in docs:print(f'Page {doc.metadata["page"]}: {doc.page_content[:300]}\n
Page 13: 14 Z. Shen et al.
6 Conclusion
LayoutParser provides a comprehensive toolkit for deep learning-based document
image analysis. The off-the-shelf library is easy to install, and can be used to
build flexible and accurate pipelines for processing documents with complicated
structures. It also supports hiPage 0: LayoutParser : A Unified Toolkit for Deep
Learning Based Document Image Analysis
Zejiang Shen1( �), Ruochen Zhang2, Melissa Dell3, Benjamin Charles Germain
Lee4, Jacob Carlson3, and Weining Li5
由于我们是按页进行向量化的,因此根据相似度匹配后,返回的也是页面,正常RAG往往会根据复杂的规则进行切分。
布局分析和从图像中提取文本
如果您需要对文本进行更细粒度的分割(例如,分成不同的段落、标题、表格或其他结构)或需要从图像中提取文本,下面的方法是合适的。它将返回一个Document对象的列表,其中每个对象表示页面上的一个结构。文档的元数据存储了页码和与对象相关的其他信息(例如,在表格对象的情况下,它可能存储表格的行和列)。
在底层,它使用langchain-unstructured库。
Unstructured支持多个参数用于PDF解析:
- strategy(例如,“fast"或"hi-res”)
- API或本地处理。您需要一个API密钥才能使用API。
对于某些文档类型(例如图像和 PDF),Unstructured 产品提供了多种预处理方式,这些方式通过 strategy
参数进行控制。
以 PDF 文档为例,它们的质量和复杂性各不相同。在简单情况下,传统的 NLP 提取技术可能足以提取文档中的所有文本。但在其他情况下,则需要先进的图像转文本模型来处理 PDF。可以将这些策略理解为两种类型:“基于规则的”工作流(因此速度较快,即 fast
),或“基于模型的”工作流(由于需要进行模型推理,因此速度较慢,但精度更高,即 hi_res
)。在选择分区策略时,需要权衡质量和速度。例如,fast
策略的速度大约是领先的图像转文本模型的 100 倍。
可用选项:
- auto(默认策略):
"auto"
策略会根据文档特性和函数的参数(kwargs)自动选择合适的分区策略。 - fast:
"fast"
采用基于规则的策略,利用传统 NLP 提取技术快速提取文本元素。不推荐用于基于图像的文件类型。 - hi_res:
"hi_res"
采用基于模型的策略来识别文档布局。其优势在于利用文档的布局信息获取更多文档元素的额外信息。如果您的应用场景对文档元素的分类准确性要求较高,建议使用该策略。 - ocr_only:
"ocr_only"
也是一种基于模型的策略,使用光学字符识别(OCR)技术从图像文件中提取文本。 - vlm:
"vlm"
使用视觉语言模型(VLM)从以下文件类型中提取文本:.bmp
,.gif
,.heic
,.jpeg
,.jpg
,.pdf
,.png
,.tiff
,.webp
。
可用于以下分区函数:
文档类型 | 分区函数 | 支持的策略 | 是否支持表格 | 选项 |
---|---|---|---|---|
图像(.png/.jpg/.heic) | partition_image | "auto" , "hi_res" , "ocr_only" | 是 | 编码、包含分页符、推断表格结构、OCR 语言、策略 |
PDF(.pdf) | partition_pdf | "auto" , "fast" , "hi_res" , "ocr_only" | 是 | 编码、包含分页符、推断表格结构、最大分区、OCR 语言、策略 |
pip install -qU langchain-unstructured
import getpass
import osif "UNSTRUCTURED_API_KEY" not in os.environ:# 到UNSTRUCTURED官网申请apikeyos.environ["UNSTRUCTURED_API_KEY"] = getpass.getpass("Unstructured API Key:")
与之前一样,我们初始化一个加载器并懒加载文档:
from langchain_unstructured import UnstructuredLoaderloader = UnstructuredLoader(file_path=file_path,strategy="hi_res",partition_via_api=True,coordinates=True,
)
docs = []
for doc in loader.lazy_load():docs.append(doc)
可以看出在解析过程中会请unstructured的官网:
INFO: Preparing to split document for partition.
INFO: Starting page number set to 1
INFO: Allow failed set to 0
INFO: Concurrency level set to 5
INFO: Splitting pages 1 to 16 (16 total)
INFO: Determined optimal split size of 4 pages.
INFO: Partitioning 4 files with 4 page(s) each.
INFO: Partitioning set #1 (pages 1-4).
INFO: Partitioning set #2 (pages 5-8).
INFO: Partitioning set #3 (pages 9-12).
INFO: Partitioning set #4 (pages 13-16).
INFO: HTTP Request: POST https://api.unstructuredapp.io/general/v0/general "HTTP/1.1 200 OK"
INFO: HTTP Request: POST https://api.unstructuredapp.io/general/v0/general "HTTP/1.1 200 OK"
INFO: HTTP Request: POST https://api.unstructuredapp.io/general/v0/general "HTTP/1.1 200 OK"
INFO: HTTP Request: POST https://api.unstructuredapp.io/general/v0/general "HTTP/1.1 200 OK"
INFO: Successfully partitioned set #1, elements added to the final result.
INFO: Successfully partitioned set #2, elements added to the final result.
INFO: Successfully partitioned set #3, elements added to the final result.
INFO: Successfully partitioned set #4, elements added to the final result.
我们可以使用文档元数据从单个页面恢复内容:
first_page_docs = [doc for doc in docs if doc.metadata.get("page_number") == 1]for doc in first_page_docs:print(doc.page_content)LayoutParser: A Unified Toolkit for Deep Learning Based Document Image Analysis
1 2 0 2 n u J 1 2 ] V C . s c [ 2 v 8 4 3 5 1 . 3 0 1 2 : v i X r a
Zejiang Shen® (<), Ruochen Zhang?, Melissa Dell®, Benjamin Charles Germain Lee?, Jacob Carlson®, and Weining Li®
提取表格和其他结构
上文中,我们加载的每个Document代表一个结构,如标题、段落或表格。下面,我们识别并提取一个表格,这个pdf可以在langChain的仓库中找到:
pip install -qU matplotlib PyMuPDF pillow
MuPDF 是一个轻量级的 PDF、XPS和电子书查看器。MuPDF 由软件库、命令行工具和各种平台的查看器组成。
MuPDF
中的渲染器专为高质量抗锯齿图形量身定制。它以精确到像素的几分之一内的度量和间距呈现文本,以在屏幕上再现打印页面的外观时获得最高保真度。
import fitz
import matplotlib.patches as patches
import matplotlib.pyplot as plt
from PIL import Image
from langchain_unstructured import UnstructuredLoaderdef plot_pdf_with_boxes(pdf_page, segments):pix = pdf_page.get_pixmap()pil_image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)fig, ax = plt.subplots(1, figsize=(10, 10))ax.imshow(pil_image)categories = set()category_to_color = {"Title": "orchid","Image": "forestgreen","Table": "tomato",}for segment in segments:points = segment["coordinates"]["points"]layout_width = segment["coordinates"]["layout_width"]layout_height = segment["coordinates"]["layout_height"]scaled_points = [(x * pix.width / layout_width, y * pix.height / layout_height)for x, y in points]box_color = category_to_color.get(segment["category"], "deepskyblue")categories.add(segment["category"])rect = patches.Polygon(scaled_points, linewidth=1, edgecolor=box_color, facecolor="none")ax.add_patch(rect)# Make legendlegend_handles = [patches.Patch(color="deepskyblue", label="Text")]for category in ["Title", "Image", "Table"]:if category in categories:legend_handles.append(patches.Patch(color=category_to_color[category], label=category))ax.axis("off")ax.legend(handles=legend_handles, loc="upper right")plt.tight_layout()plt.show()def render_page(doc_list: list, page_number: int, print_text=True) -> None:pdf_page = fitz.open(file_path).load_page(page_number - 1)page_docs = [doc for doc in doc_list if doc.metadata.get("page_number") == page_number]segments = [doc.metadata for doc in page_docs]plot_pdf_with_boxes(pdf_page, segments)if print_text:for doc in page_docs:print(f"{doc.page_content}\n")if __name__ == '__main__':file_path = "layout-parser-paper.pdf"loader = UnstructuredLoader(file_path=file_path,strategy="hi_res",partition_via_api=True,coordinates=True,)docs = []for doc in loader.lazy_load():docs.append(doc)render_page(docs, 5)
由于我没有apikey就不再演示,至于plot_pdf_with_boxes函数只是根据文档类型提取区域坐标进行展示的函数,我简化为如下:
import matplotlib.patches as patches
import matplotlib.pyplot as plt
from PIL import Imagedef plot_image_with_boxes(image_path, boxes):"""在图片上绘制指定的框。:param image_path: 图片路径:param boxes: 颜色 -> [x1, y1, x2, y2] 的字典"""# 加载图片pil_image = Image.open(image_path)# 创建画布# 单位是英寸(1 英寸 = 100 dpi 约等于 100 像素)。fig, ax = plt.subplots(figsize=(10, 10))ax.imshow(pil_image)# 绘制矩形框for color, coords in boxes.items():x1, y1, x2, y2 = coordswidth, height = x2 - x1, y2 - y1# 画框rect = patches.Rectangle((x1, y1), width, height, linewidth=2, edgecolor=color, facecolor="none")ax.add_patch(rect)# 去掉坐标轴ax.axis("off")plt.show()# 示例输入
boxes = {"red": [50, 50, 200, 200],"blue": [300, 100, 450, 250],"green": [150, 300, 350, 450]
}if __name__ == "__main__":plot_image_with_boxes("1.png", boxes)
需要注意的是:在 Matplotlib 中,像素坐标原点 (0, 0) 默认在 图片的左上角,x 轴向右递增,y 轴向下递增。
下面给出langChain官网的案例:
LayoutParser: A Unified Toolkit for DL-Based DIA5Table 1: Current layout detection models in the LayoutParser model zooDataset Base Model1 Large Model Notes PubLayNet [38] PRImA [3] Newspaper [17] TableBank [18] HJDataset [31] F / M M F F F / M M - - F - Layouts of modern scientific documents Layouts of scanned modern magazines and scientific reports Layouts of scanned US newspapers from the 20th century Table region on modern scientific and business document Layouts of history Japanese documents1 For each dataset, we train several models of different sizes for different needs (the trade-off between accuracy vs. computational cost). For “base model” and “large model”, we refer to using the ResNet 50 or ResNet 101 backbones [13], respectively. One can train models of different architectures, like Faster R-CNN [28] (F) and Mask R-CNN [12] (M). For example, an F in the Large Model column indicates it has a Faster R-CNN model trained using the ResNet 101 backbone. The platform is maintained and a number of additions will be made to the model zoo in coming months.layout data structures, which are optimized for efficiency and versatility. 3) When necessary, users can employ existing or customized OCR models via the unified API provided in the OCR module. 4) LayoutParser comes with a set of utility functions for the visualization and storage of the layout data. 5) LayoutParser is also highly customizable, via its integration with functions for layout data annotation and model training. We now provide detailed descriptions for each component.3.1 Layout Detection ModelsIn LayoutParser, a layout model takes a document image as an input and generates a list of rectangular boxes for the target content regions. Different from traditional methods, it relies on deep convolutional neural networks rather than manually curated rules to identify content regions. It is formulated as an object detection problem and state-of-the-art models like Faster R-CNN [28] and Mask R-CNN [12] are used. This yields prediction results of high accuracy and makes it possible to build a concise, generalized interface for layout detection. LayoutParser, built upon Detectron2 [35], provides a minimal API that can perform layout detection with only four lines of code in Python:1 import layoutparser as lp 2 image = cv2 . imread ( " image_file " ) # load images 3 model = lp . De t e c tro n2 Lay outM odel ( " lp :// PubLayNet / f as t er _ r c nn _ R _ 50 _ F P N_ 3 x / config " ) 4 5 layout = model . detect ( image )LayoutParser provides a wealth of pre-trained model weights using various datasets covering different languages, time periods, and document types. Due to domain shift [7], the prediction performance can notably drop when models are ap- plied to target samples that are significantly different from the training dataset. As document structures and layouts vary greatly in different domains, it is important to select models trained on a dataset similar to the test samples. A semantic syntax is used for initializing the model weights in LayoutParser, using both the dataset name and model name lp://<dataset-name>/<model-architecture-name>.
尽管表格文本在文档内容中被压缩为一个字符串,但元数据包含其行和列的表示:
from IPython.display import HTML, displaysegments = [doc.metadatafor doc in docsif doc.metadata.get("page_number") == 5 and doc.metadata.get("category") == "Table"
]display(HTML(segments[0]["text_as_html"]))
利用Ipython即可展示出来:
从特定部分提取文本
结构可能具有父子关系——例如,一个段落可能属于一个有标题的部分。如果某个部分特别重要(例如,用于索引),我们可以隔离相应的 Document 对象。
下面,我们提取与文档的“结论”部分相关的所有文本:
render_page(docs, 14, print_text=False)
conclusion_docs = []
parent_id = -1
for doc in docs:if doc.metadata["category"] == "Title" and "Conclusion" in doc.page_content:parent_id = doc.metadata["element_id"]if doc.metadata.get("parent_id") == parent_id:conclusion_docs.append(doc)for doc in conclusion_docs:print(doc.page_content)
LayoutParser provides a comprehensive toolkit for deep learning-based document image analysis. The off-the-shelf library is easy to install, and can be used to build flexible and accurate pipelines for processing documents with complicated structures. It also supports high-level customization and enables easy labeling and training of DL models on unique document image datasets. The LayoutParser community platform facilitates sharing DL models and DIA pipelines, inviting discussion and promoting code reproducibility and reusability. The LayoutParser team is committed to keeping the library updated continuously and bringing the most recent advances in DL-based DIA, such as multi-modal document modeling [37, 36, 9] (an upcoming priority), to a diverse audience of end-users.
Acknowledgements We thank the anonymous reviewers for their comments and suggestions. This project is supported in part by NSF Grant OIA-2033558 and funding from the Harvard Data Science Initiative and Harvard Catalyst. Zejiang Shen thanks Doug Downey for suggestions.
从图像中提取文本
对图像运行OCR,从中提取文本:
render_page(docs, 11)
请注意,右侧图中的文本已提取并纳入Document的内容中:
LayoutParser: A Unified Toolkit for DL-Based DIAfocuses on precision, efficiency, and robustness. The target documents may have complicated structures, and may require training multiple layout detection models to achieve the optimal accuracy. Light-weight pipelines are built for relatively simple documents, with an emphasis on development ease, speed and flexibility. Ideally one only needs to use existing resources, and model training should be avoided. Through two exemplar projects, we show how practitioners in both academia and industry can easily build such pipelines using LayoutParser and extract high-quality structured document data for their downstream tasks. The source code for these projects will be publicly available in the LayoutParser community hub.115.1 A Comprehensive Historical Document Digitization PipelineThe digitization of historical documents can unlock valuable data that can shed light on many important social, economic, and historical questions. Yet due to scan noises, page wearing, and the prevalence of complicated layout structures, ob- taining a structured representation of historical document scans is often extremely complicated. In this example, LayoutParser was used to develop a comprehensive pipeline, shown in Figure 5, to gener- ate high-quality structured data from historical Japanese firm financial ta- bles with complicated layouts. The pipeline applies two layout models to identify different levels of document structures and two customized OCR engines for optimized character recog- nition accuracy.‘Active Learning Layout Annotate Layout Dataset | +—— Annotation Toolkit A4 Deep Learning Layout Layout Detection Model Training & Inference, A Post-processing — Handy Data Structures & \ Lo orajport 7 ) Al Pls for Layout Data A4 Default and Customized Text Recognition 0CR Models ¥ Visualization & Export Layout Structure Visualization & Storage The Japanese Document Helpful LayoutParser Modules Digitization PipelineAs shown in Figure 4 (a), the document contains columns of text written vertically 15, a common style in Japanese. Due to scanning noise and archaic printing technology, the columns can be skewed or have vari- able widths, and hence cannot be eas- ily identified via rule-based methods. Within each column, words are sepa- rated by white spaces of variable size, and the vertical positions of objects can be an indicator of their layout type.Fig. 5: Illustration of how LayoutParser helps with the historical document digi- tization pipeline.15 A document page consists of eight rows like this. For simplicity we skip the row segmentation discussion and refer readers to the source code when available.
本地解析图片需要安装额外的依赖项。
Poppler(PDF分析)
- Linux: apt-get install poppler-utils
- Mac: brew install poppler
- Windows: https://github.com/oschwartz10612/poppler-windows
Tesseract(OCR)
- Linux: apt-get install tesseract-ocr
- Mac: brew install tesseract
- Windows: https://github.com/UB-Mannheim/tesseract/wiki#tesseract-installer-for-windows
还需要安装 unstructured PDF 附加组件:
pip install -qU "unstructured[pdf]"
我们可以以类似的方式使用 UnstructuredLoader,不需要 API 密钥和 partition_via_api 设置:
loader_local = UnstructuredLoader(file_path=file_path,strategy="hi_res",
)
docs_local = []
for doc in loader_local.lazy_load():docs_local.append(doc)
WARNING: This function will be deprecated in a future release and `unstructured` will simply use the DEFAULT_MODEL from `unstructured_inference.model.base` to set default model name
INFO: Reading PDF for file: /Users/chestercurme/repos/langchain/libs/community/tests/integration_tests/examples/layout-parser-paper.pdf ...
INFO: Detecting page elements ...
INFO: Detecting page elements ...
INFO: Detecting page elements ...
INFO: Detecting page elements ...
多模态模型的使用
许多现代大型语言模型支持对多模态输入(例如,图像)的推理。原则上,我们可以使用任何支持多模态输入的 LangChain 聊天模型。
定义一个简短的工具函数,将 PDF 页面转换为 base64 编码的图像:
import base64
import ioimport fitz
from PIL import Imagedef pdf_page_to_base64(pdf_path: str, page_number: int):pdf_document = fitz.open(pdf_path)page = pdf_document.load_page(page_number - 1) # input is one-indexedpix = page.get_pixmap()img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)buffer = io.BytesIO()img.save(buffer, format="PNG")return base64.b64encode(buffer.getvalue()).decode("utf-8")
from IPython.display import Image as IPImage
from IPython.display import displaybase64_image = pdf_page_to_base64(file_path, 11)
display(IPImage(data=base64.b64decode(base64_image)))
然后我们可以以正常查询模型。下面我们向它询问与页面上的图表相关的问题。
from langchain_openai import ChatOpenAIllm = ChatOpenAI(model="gpt-4o-mini")from langchain_core.messages import HumanMessagequery = "What is the name of the first step in the pipeline?"message = HumanMessage(content=[{"type": "text", "text": query},{"type": "image_url","image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},},],
)
response = llm.invoke([message])
print(response.content)
INFO: HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
``````output
The first step in the pipeline is "Annotate Layout Dataset."
加载网页
LangChain 集成了一系列适合网页的解析器。
- 简单快速 解析,每个网页一个 Document,其内容表示为“扁平化”字符串;
- 高级 解析,每个页面多个 Document 对象,允许识别和遍历部分、链接、表格和其他结构。、
对于“简单快速”解析,我们需要 langchain-community 和 beautifulsoup4 库:
pip install -qU langchain-community beautifulsoup4
对于高级解析,我们将使用 langchain-unstructured:
pip install -qU langchain-unstructured
简单快速的文本提取
在底层,它使用 beautifulsoup4:
import bs4
from langchain_community.document_loaders import WebBaseLoaderpage_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"loader = WebBaseLoader(web_paths=[page_url])
docs = []
async for doc in loader.alazy_load():docs.append(doc)assert len(docs) == 1
doc = docs[0]print(f"{doc.metadata}\n")
print(doc.page_content[:500].strip())
{'source': 'https://python.langchain.com/docs/how_to/chatbots_memory/', 'title': 'How to add memory to chatbots | \uf8ffü¶úÔ∏è\uf8ffüîó LangChain', 'description': 'A key feature of chatbots is their ability to use content of previous conversation turns as context. This state management can take several forms, including:', 'language': 'en'}How to add memory to chatbots | ü¶úÔ∏èüîó LangChain
这基本上是页面 HTML 中文本的转储。它可能包含多余的信息,如标题和导航栏。可以通过 BeautifulSoup 指定所需的 <div>
类和其他参数:
loader = WebBaseLoader(web_paths=[page_url],bs_kwargs={"parse_only": bs4.SoupStrainer(class_="theme-doc-markdown markdown"),},bs_get_text_kwargs={"separator": " | ", "strip": True},
)docs = []
async for doc in loader.alazy_load():docs.append(doc)assert len(docs) == 1
doc = docs[0]
我们可以使用各种设置对 WebBaseLoader 进行参数化,允许指定请求头、速率限制、解析器和其他 BeautifulSoup 的关键字参数。
高级解析
如果我们想对页面内容进行更细粒度的控制或处理,这种方法是合适的。加载器将文档切分为多个Document,表示页面上的不同结构。这些结构可以包括章节标题及其对应的主体文本、列表或枚举、表格等。
在底层,它使用 langchain-unstructured 库。
from langchain_unstructured import UnstructuredLoaderpage_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = UnstructuredLoader(web_url=page_url)docs = []
async for doc in loader.alazy_load():docs.append(doc)
INFO: Note: NumExpr detected 12 cores but "NUMEXPR_MAX_THREADS" not set, so enforcing safe limit of 8.
INFO: NumExpr defaulting to 8 threads.
从特定部分提取内容
每个 Document 对象代表页面的一个元素。其元数据包含有用的信息,例如其类别:
for doc in docs[:5]:print(f'{doc.metadata["category"]}: {doc.page_content}')
Title: How to add memory to chatbots
NarrativeText: A key feature of chatbots is their ability to use content of previous conversation turns as context. This state management can take several forms, including:
ListItem: Simply stuffing previous messages into a chat model prompt.
ListItem: The above, but trimming old messages to reduce the amount of distracting information the model has to deal with.
ListItem: More complex modifications like synthesizing summaries for long running conversations.
元素之间也可能存在父子关系,一个段落可能属于一个有标题的部分,下面我们加载两个网页的“设置”部分的内容:
from typing import Listfrom langchain_core.documents import Documentasync def _get_setup_docs_from_url(url: str) -> List[Document]:loader = UnstructuredLoader(web_url=url)setup_docs = []parent_id = -1async for doc in loader.alazy_load():if doc.metadata["category"] == "Title" and doc.page_content.startswith("Setup"):parent_id = doc.metadata["element_id"]if doc.metadata.get("parent_id") == parent_id:setup_docs.append(doc)return setup_docspage_urls = ["https://python.langchain.com/docs/how_to/chatbots_memory/","https://python.langchain.com/docs/how_to/chatbots_tools/",
]
setup_docs = []
for url in page_urls:page_setup_docs = await _get_setup_docs_from_url(url)setup_docs.extend(page_setup_docs)
from collections import defaultdictsetup_text = defaultdict(str)for doc in setup_docs:url = doc.metadata["url"]setup_text[url] += f"{doc.page_content}\n"dict(setup_text)
{'https://python.langchain.com/docs/how_to/chatbots_memory/': "You'll need to install a few packages, and have your OpenAI API key set as an environment variable named OPENAI_API_KEY:\n%pip install --upgrade --quiet langchain langchain-openai\n\n# Set env var OPENAI_API_KEY or load from a .env file:\nimport dotenv\n\ndotenv.load_dotenv()\n[33mWARNING: You are using pip version 22.0.4; however, version 23.3.2 is available.\nYou should consider upgrading via the '/Users/jacoblee/.pyenv/versions/3.10.5/bin/python -m pip install --upgrade pip' command.[0m[33m\n[0mNote: you may need to restart the kernel to use updated packages.\n",'https://python.langchain.com/docs/how_to/chatbots_tools/': "For this guide, we'll be using a tool calling agent with a single tool for searching the web. The default will be powered by Tavily, but you can switch it out for any similar tool. The rest of this section will assume you're using Tavily.\nYou'll need to sign up for an account on the Tavily website, and install the following packages:\n%pip install --upgrade --quiet langchain-community langchain-openai tavily-python\n\n# Set env var OPENAI_API_KEY or load from a .env file:\nimport dotenv\n\ndotenv.load_dotenv()\nYou will also need your OpenAI key set as OPENAI_API_KEY and your Tavily API key set as TAVILY_API_KEY.\n"}
对页面内容进行向量搜索
一旦我们将页面内容加载到LangChain Document 对象中,就可以对数据进行索引:
import getpass
import osif "OPENAI_API_KEY" not in os.environ:os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API Key:")from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddingsvector_store = InMemoryVectorStore.from_documents(setup_docs, OpenAIEmbeddings())
retrieved_docs = vector_store.similarity_search("Install Tavily", k=2)
for doc in retrieved_docs:print(f'Page {doc.metadata["url"]}: {doc.page_content[:300]}\n')
加载CSV文件
一个 逗号分隔值 (CSV) 文件是一个使用逗号分隔值的定界文本文件。文件的每一行都是一个数据记录。每个记录由一个或多个字段组成,字段之间用逗号分隔。
LangChain 实现了一个 CSV 加载器,可以将 CSV 文件加载为一系列 文档 对象。CSV 文件的每一行被转换为一个文档。
from langchain_community.document_loaders.csv_loader import CSVLoaderfile_path = ("../../../docs/integrations/document_loaders/example_data/mlb_teams_2012.csv"
)loader = CSVLoader(file_path=file_path)
data = loader.load()for record in data[:2]:print(record)
page_content='Team: Nationals\n"Payroll (millions)": 81.34\n"Wins": 98' metadata={'source': '../../../docs/integrations/document_loaders/example_data/mlb_teams_2012.csv', 'row': 0}
page_content='Team: Reds\n"Payroll (millions)": 82.20\n"Wins": 97' metadata={'source': '../../../docs/integrations/document_loaders/example_data/mlb_teams_2012.csv', 'row': 1}
CSVLoader 接受一个 csv_args 关键字参数,支持传递给 Python 自定义的 csv.DictReader 的参数。
loader = CSVLoader(file_path=file_path,csv_args={"delimiter": ",","quotechar": '"',"fieldnames": ["MLB Team", "Payroll in millions", "Wins"],},
)data = loader.load()
for record in data[:2]:print(record)
可以使用 CSV 的一列作为 Document 元数据中的 “source” 键,loader只会提取这一列的值,否则,将默认使用 file_path 作为source。
loader = CSVLoader(file_path=file_path, source_column="Team")data = loader.load()
for record in data[:2]:print(record)
page_content='Team: Nationals\n"Payroll (millions)": 81.34\n"Wins": 98' metadata={'source': 'Nationals', 'row': 0}
page_content='Team: Reds\n"Payroll (millions)": 82.20\n"Wins": 97' metadata={'source': 'Reds', 'row': 1}
在直接处理 CSV 字符串时,可以使用 Python 的 tempfile。
import tempfile
from io import StringIOstring_data = """
"Team", "Payroll (millions)", "Wins"
"Nationals", 81.34, 98
"Reds", 82.20, 97
"Yankees", 197.96, 95
"Giants", 117.62, 94
""".strip()with tempfile.NamedTemporaryFile(delete=False, mode="w+") as temp_file:temp_file.write(string_data)temp_file_path = temp_file.nameloader = CSVLoader(file_path=temp_file_path)
loader.load()
for record in data[:2]:print(record)
page_content='Team: Nationals\n"Payroll (millions)": 81.34\n"Wins": 98' metadata={'source': 'Nationals', 'row': 0}
page_content='Team: Reds\n"Payroll (millions)": 82.20\n"Wins": 97' metadata={'source': 'Reds', 'row': 1}
从目录加载文档
LangChain 的 DirectoryLoader 实现了从磁盘读取文件到 LangChain Document 对象的功能。
from langchain_community.document_loaders import DirectoryLoader
DirectoryLoader 接受一个 loader_cls 关键字参数,默认为 UnstructuredLoader。Unstructured 支持解析多种格式,如 PDF 和 HTML。在这里我们用它来读取一个 markdown (.md) 文件。
我们可以使用 glob 参数来控制加载哪些文件。请注意,这里不会加载 .rst 文件或 .html 文件。
loader = DirectoryLoader("../", glob="**/*.md")
docs = loader.load()
len(docs)
20
print(docs[0].page_content[:100])
SecurityLangChain has a large ecosystem of integrations with various external resources like local
默认情况下不会显示进度条。要显示进度条,需要安装 tqdm 库(例如 pip install tqdm),并将 show_progress 参数设置为 True。
loader = DirectoryLoader("../", glob="**/*.md", show_progress=True)
docs = loader.load()
默认情况下加载在一个线程中进行。为了利用多个线程,请将 use_multithreading 标志设置为 true。
loader = DirectoryLoader("../", glob="**/*.md", use_multithreading=True)
docs = loader.load()
默认情况下使用 UnstructuredLoader 类。要自定义加载器,需要在 loader_cls 关键字参数中指定加载器类。
比如使用指明使用 TextLoader:
from langchain_community.document_loaders import TextLoaderloader = DirectoryLoader("../", glob="**/*.md", loader_cls=TextLoader)
docs = loader.load()print(docs[0].page_content[:100])
# LangChain has a large ecosystem of integrations with various external resources like loc
注意,UnstructuredLoader 解析 Markdown 头部,TextLoader 不解析。
如果您需要加载 Python 源代码文件,请使用 PythonLoader:
from langchain_community.document_loaders import PythonLoaderloader = DirectoryLoader("../../../../../", glob="**/*.py", loader_cls=PythonLoader)
DirectoryLoader可以帮助管理由于文件编码差异而导致的错误。下面我们将尝试加载一组文件,其中一个文件包含非UTF8编码。
path = "../../../../libs/langchain/tests/unit_tests/examples/"loader = DirectoryLoader(path, glob="**/*.txt", loader_cls=TextLoader)
默认情况下,我们会引发一个错误:
Error loading file ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt
文件 example-non-utf8.txt 使用了不同的编码,因此 load() 函数失败,并提供了一个有用的消息,指示哪个文件解码失败。在 TextLoader 的默认行为下,任何文档加载失败都会导致整个加载过程失败。
我们可以将参数 silent_errors 传递给 DirectoryLoader,以跳过无法加载的文件并继续加载过程。
loader = DirectoryLoader(path, glob="**/*.txt", loader_cls=TextLoader, silent_errors=True
)
docs = loader.load()
Error loading file ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt: Error loading ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt
doc_sources = [doc.metadata["source"] for doc in docs]
doc_sources
['../../../../libs/langchain/tests/unit_tests/examples/example-utf8.txt']
我们还可以要求 TextLoader 在失败之前自动检测文件编码,通过将 autodetect_encoding 传递给加载器类。
text_loader_kwargs = {"autodetect_encoding": True}
loader = DirectoryLoader(path, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs=text_loader_kwargs
)
docs = loader.load()
doc_sources = [doc.metadata["source"] for doc in docs]
doc_sources['../../../../libs/langchain/tests/unit_tests/examples/example-utf8.txt','../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt']
加载HTML
解析HTML文件通常需要专门的工具。langChain主要通过Unstructured和BeautifulSoup4进行解析,这些工具可以通过pip安装。
pip install unstructured
from langchain_community.document_loaders import UnstructuredHTMLLoaderfile_path = "../../docs/integrations/document_loaders/example_data/fake-content.html"loader = UnstructuredHTMLLoader(file_path)
data = loader.load()print(data)
[Document(page_content='My First Heading\n\nMy first paragraph.', metadata={'source': '../../docs/integrations/document_loaders/example_data/fake-content.html'})]
我们还可以使用 BeautifulSoup4 通过 BSHTMLLoader 加载 HTML 文档。这将从 HTML 中提取文本到 page_content,并将页面标题作为 title 提取到 metadata。
pip install bs4
from langchain_community.document_loaders import BSHTMLLoaderloader = BSHTMLLoader(file_path)
data = loader.load()print(data)
[Document(page_content='\nTest Title\n\n\nMy First Heading\nMy first paragraph.\n\n\n', metadata={'source': '../../docs/integrations/document_loaders/example_data/fake-content.html', 'title': 'Test Title'})]
加载 JSON
JSON (JavaScript 对象表示法) 是一种开放标准文件格式和数据交换格式,使用人类可读的文本来存储和传输由属性-值对和数组(或其他可序列化值)组成的数据对象。
JSON Lines 是一种文件格式,其中每一行都是一个有效的 JSON 值。
LangChain 实现了一个 JSONLoader 用于将 JSON 和 JSONL 数据转换为 LangChain 文档 对象。 它使用指定的 jq schema(jq python 包)来解析 JSON 文件,允许将特定字段提取到内容 和 LangChain 文档的元数据中。
pip install jq
from langchain_community.document_loaders import JSONLoaderimport json
from pathlib import Path
from pprint import pprintfile_path='./example_data/facebook_chat.json'
data = json.loads(Path(file_path).read_text())pprint(data)
假设我们想提取 JSON 数据中 messages 键下 content 字段的值。这可以通过下面的 JSONLoader 轻松完成。
loader = JSONLoader(file_path='./example_data/facebook_chat.json',jq_schema='.messages[].content',text_content=False)data = loader.load()
pprint(data)
[Document(page_content='Bye!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 1}),Document(page_content='Oh no worries! Bye', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 2}),Document(page_content='No Im sorry it was my mistake, the blue one is not for sale', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 3}),Document(page_content='I thought you were selling the blue one!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 4}),Document(page_content='Im not interested in this bag. Im interested in the blue one!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 5}),Document(page_content='Here is $129', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 6}),Document(page_content='', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 7}),Document(page_content='Online is at least $100', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 8}),Document(page_content='How much do you want?', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 9}),Document(page_content='Goodmorning! $50 is too low.', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 10}),Document(page_content='Hi! Im interested in your bag. Im offering $50. Let me know if you are interested. Thanks!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 11})]
如果您想从 JSON Lines 文件加载文档,请传递 json_lines=True 并指定 jq_schema 以从单个 JSON 对象中提取 page_content。
('{"sender_name": "User 2", "timestamp_ms": 1675597571851, "content": "Bye!"}\n''{"sender_name": "User 1", "timestamp_ms": 1675597435669, "content": "Oh no ''worries! Bye"}\n''{"sender_name": "User 2", "timestamp_ms": 1675596277579, "content": "No Im ''sorry it was my mistake, the blue one is not for sale"}\n')loader = JSONLoader(file_path='./example_data/facebook_chat_messages.jsonl',jq_schema='.content',text_content=False,json_lines=True)data = loader.load()
可以看到我们只取到了content值:
[Document(page_content='Bye!', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat_messages.jsonl', 'seq_num': 1}),Document(page_content='Oh no worries! Bye', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat_messages.jsonl', 'seq_num': 2}),Document(page_content='No Im sorry it was my mistake, the blue one is not for sale', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat_messages.jsonl', 'seq_num': 3})]
另一个选项是设置 jq_schema=‘.’ 并提供 content_key:
loader = JSONLoader(file_path='./example_data/facebook_chat_messages.jsonl',jq_schema='.',content_key='sender_name',json_lines=True)data = loader.load()
要使用 jq schema 中的 content_key 从 JSON 文件加载文档,请设置 is_content_key_jq_parsable=True。 确保 content_key 兼容并可以使用 jq schema 进行解析。
loader = JSONLoader(file_path=file_path,jq_schema=".data[]",content_key=".attributes.message",is_content_key_jq_parsable=True,
)data = loader.load()
通常,我们希望将JSON文件中可用的元数据包含到我们从内容创建的文档中。在之前的示例中,我们没有收集元数据,我们直接在模式中指定了page_content的值可以从哪里提取。
.messages[].content
在当前示例中,我们必须告诉加载器遍历messages字段中的记录。jq_schema必须是:
.messages[]
这允许我们将记录(字典)传递给必须实现的metadata_func。metadata_func负责识别记录中哪些信息应包含在最终Document对象中存储的元数据中。
此外,我们现在必须通过content_key参数在加载器中明确指定记录中提取page_content值的键。
def metadata_func(record: dict, metadata: dict) -> dict:metadata["sender_name"] = record.get("sender_name")metadata["timestamp_ms"] = record.get("timestamp_ms")return metadataloader = JSONLoader(file_path='./example_data/facebook_chat.json',jq_schema='.messages[]',content_key="content",metadata_func=metadata_func
)data = loader.load()
pprint(data)
[Document(page_content='Bye!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 1, 'sender_name': 'User 2', 'timestamp_ms': 1675597571851}),Document(page_content='Oh no worries! Bye', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 2, 'sender_name': 'User 1', 'timestamp_ms': 1675597435669}),Document(page_content='No Im sorry it was my mistake, the blue one is not for sale', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 3, 'sender_name': 'User 2', 'timestamp_ms': 1675596277579}),Document(page_content='I thought you were selling the blue one!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 4, 'sender_name': 'User 1', 'timestamp_ms': 1675595140251}),Document(page_content='Im not interested in this bag. Im interested in the blue one!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 5, 'sender_name': 'User 1', 'timestamp_ms': 1675595109305}),Document(page_content='Here is $129', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 6, 'sender_name': 'User 2', 'timestamp_ms': 1675595068468}),Document(page_content='', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 7, 'sender_name': 'User 2', 'timestamp_ms': 1675595060730}),Document(page_content='Online is at least $100', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 8, 'sender_name': 'User 2', 'timestamp_ms': 1675595045152}),Document(page_content='How much do you want?', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 9, 'sender_name': 'User 1', 'timestamp_ms': 1675594799696}),Document(page_content='Goodmorning! $50 is too low.', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 10, 'sender_name': 'User 2', 'timestamp_ms': 1675577876645}),Document(page_content='Hi! Im interested in your bag. Im offering $50. Let me know if you are interested. Thanks!', metadata={'source': '/Users/avsolatorio/WBG/langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 11, 'sender_name': 'User 1', 'timestamp_ms': 1675549022673})]
如上所示,metadata_func接受由JSONLoader生成的默认元数据。这使用户能够完全控制元数据的格式。
例如,默认元数据包含 source 和 seq_num 键。然而,JSON 数据中也可能包含这些键。用户可以利用 metadata_func 来重命名默认键,并使用 JSON 数据中的键。
# Define the metadata extraction function.
def metadata_func(record: dict, metadata: dict) -> dict:metadata["sender_name"] = record.get("sender_name")metadata["timestamp_ms"] = record.get("timestamp_ms")if "source" in metadata:source = metadata["source"].split("/")source = source[source.index("langchain"):]metadata["source"] = "/".join(source)return metadataloader = JSONLoader(file_path='./example_data/facebook_chat.json',jq_schema='.messages[]',content_key="content",metadata_func=metadata_func
)data = loader.load()pprint(data)
[Document(page_content='Bye!', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 1, 'sender_name': 'User 2', 'timestamp_ms': 1675597571851}),Document(page_content='Oh no worries! Bye', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 2, 'sender_name': 'User 1', 'timestamp_ms': 1675597435669}),Document(page_content='No Im sorry it was my mistake, the blue one is not for sale', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 3, 'sender_name': 'User 2', 'timestamp_ms': 1675596277579}),Document(page_content='I thought you were selling the blue one!', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 4, 'sender_name': 'User 1', 'timestamp_ms': 1675595140251}),Document(page_content='Im not interested in this bag. Im interested in the blue one!', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 5, 'sender_name': 'User 1', 'timestamp_ms': 1675595109305}),Document(page_content='Here is $129', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 6, 'sender_name': 'User 2', 'timestamp_ms': 1675595068468}),Document(page_content='', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 7, 'sender_name': 'User 2', 'timestamp_ms': 1675595060730}),Document(page_content='Online is at least $100', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 8, 'sender_name': 'User 2', 'timestamp_ms': 1675595045152}),Document(page_content='How much do you want?', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 9, 'sender_name': 'User 1', 'timestamp_ms': 1675594799696}),Document(page_content='Goodmorning! $50 is too low.', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 10, 'sender_name': 'User 2', 'timestamp_ms': 1675577876645}),Document(page_content='Hi! Im interested in your bag. Im offering $50. Let me know if you are interested. Thanks!', metadata={'source': 'langchain/docs/modules/indexes/document_loaders/examples/example_data/facebook_chat.json', 'seq_num': 11, 'sender_name': 'User 1', 'timestamp_ms': 1675549022673})]
下面的列表提供了用户可以使用的可能的 jq_schema 参考,以根据结构从 JSON 数据中提取内容。
JSON -> [{"text": ...}, {"text": ...}, {"text": ...}]
jq_schema -> ".[].text"JSON -> {"key": [{"text": ...}, {"text": ...}, {"text": ...}]}
jq_schema -> ".key[].text"JSON -> ["...", "...", "..."]
jq_schema -> ".[]"
加载Markdown
Markdown 是一种轻量级标记语言,用于使用纯文本编辑器创建格式化文本。LangChain 实现了一个 UnstructuredMarkdownLoader 对象,该对象需要 Unstructured 包。
pip install "unstructured[md]" nltk
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Documentmarkdown_path = "../../../README.md"
loader = UnstructuredMarkdownLoader(markdown_path)data = loader.load()
assert len(data) == 1
assert isinstance(data[0], Document)
readme_content = data[0].page_content
print(readme_content[:250])
基本用法将会把一个Markdown文件导入为一个单一文档。
🦜️🔗 LangChain⚡ Build context-aware reasoning applications ⚡Looking for the JS/TS library? Check out LangChain.js.To help you ship LangChain apps to production faster, check out LangSmith.
LangSmith is a unified developer platform for building,
在底层,Unstructured为不同的文本块创建不同的“元素”。默认情况下这些元素将被组合在一起,但可以通过指定mode="elements"轻松保持这种分离。
loader = UnstructuredMarkdownLoader(markdown_path, mode="elements")data = loader.load()
print(f"Number of documents: {len(data)}\n")for document in data[:2]:print(f"{document}\n")
Number of documents: 66page_content='🦜️🔗 LangChain' metadata={'source': '../../../README.md', 'category_depth': 0, 'last_modified': '2024-06-28T15:20:01', 'languages': ['eng'], 'filetype': 'text/markdown', 'file_directory': '../../..', 'filename': 'README.md', 'category': 'Title'}page_content='⚡ Build context-aware reasoning applications ⚡' metadata={'source': '../../../README.md', 'last_modified': '2024-06-28T15:20:01', 'languages': ['eng'], 'parent_id': '200b8a7d0dd03f66e4f13456566d2b3a', 'filetype': 'text/markdown', 'file_directory': '../../..', 'filename': 'README.md', 'category': 'NarrativeText'}
请注意,在这种情况下,我们得到了三种不同的元素类型:
print(set(document.metadata["category"] for document in data))
{'ListItem', 'NarrativeText', 'Title'}
加载 Microsoft Office 文件
Microsoft Office 办公软件套件包括 Microsoft Word、Microsoft Excel、Microsoft PowerPoint、Microsoft Outlook 和 Microsoft OneNote。它可用于 Microsoft Windows 和 macOS 操作系统,也可在 Android 和 iOS 上使用。
Azure AI Document Intelligence(前称 Azure Form Recognizer)是基于机器学习的 服务,能够从数字或扫描的 PDF、图像、Office 和 HTML 文件中提取文本(包括手写)、表格、文档结构(例如标题、章节标题等)和键值对。 文档智能支持 PDF、JPEG/JPG、PNG、BMP、TIFF、HEIF、DOCX、XLSX、PPTX 和 HTML。
这个加载器可以逐页整合内容并将其转换为 LangChain 文档。默认输出格式为 markdown,可以与 MarkdownHeaderTextSplitter 轻松链式处理以进行语义文档分块。还可以使用 mode="single" 或 mode="page"
返回单页或按页分割的纯文本。
from langchain_community.document_loaders import AzureAIDocumentIntelligenceLoaderfile_path = "<filepath>"
endpoint = "<endpoint>"
key = "<key>"
loader = AzureAIDocumentIntelligenceLoader(api_endpoint=endpoint, api_key=key, file_path=file_path, api_model="prebuilt-layout"
)documents = loader.load()
创建自定义文档加载器
基于大型语言模型(LLMs)的应用程序通常涉及从数据库或文件(如PDF)中提取数据,并将其转换为LLMs可以使用的格式。在LangChain中,这通常涉及创建文档对象(Document),它封装了提取的文本(page_content)以及元数据——一个包含有关文档的详细信息的字典,例如作者的姓名或出版日期。
Document对象通常被格式化为提示词,输入到LLM中,使LLM能够使用Document中的信息生成所需的响应(例如,总结文档)。 Documents可以立即使用,也可以索引到向量存储中以便将来检索和使用。
文档加载的主要抽象是:
组件 | 描述 |
---|---|
Document | 包含文本 和元数据 |
BaseLoader | 用于将原始数据转换为Documents |
Blob | 二进制数据的表示,位于文件或内存中 |
BaseBlobParser | 解析 Blob 的逻辑,以生成 Document 对象 |
文档加载器可以通过从 BaseLoader 子类化来实现,后者提供了加载文档的标准接口。
方法名称 | 说明 |
---|---|
lazy_load | 用于懒加载文档,一次加载一个。用于生产代码。 |
alazy_load | lazy_load 的异步变体 |
load | 用于急加载所有文档到内存中。用于原型设计或交互式工作。 |
aload | 用于急加载所有文档到内存中。用于原型设计或交互式工作。于2024-04添加到LangChain。 |
- load方法是一个便利方法,仅用于原型设计工作 – 它只是调用list(self.lazy_load())。
- alazy_load有一个默认实现,将委托给lazy_load。如果您使用异步,我们建议覆盖默认实现并提供原生异步实现。
注意:
- 实现文档加载器时不要通过lazy_load或alazy_load方法提供参数。
- 所有配置预计通过初始化器(init)传递。这是LangChain做出的设计选择,以确保一旦实例化文档加载器,它就拥有加载文档所需的所有信息。
让我们创建一个标准文档加载器的示例,该加载器加载一个文件并从文件中的每一行创建一个文档。
from typing import AsyncIterator, Iteratorfrom langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Documentclass CustomDocumentLoader(BaseLoader):"""An example document loader that reads a file line by line."""def __init__(self, file_path: str) -> None:"""Initialize the loader with a file path.Args:file_path: The path to the file to load."""self.file_path = file_pathdef lazy_load(self) -> Iterator[Document]: # <-- Does not take any arguments"""A lazy loader that reads a file line by line.When you're implementing lazy load methods, you should use a generatorto yield documents one by one."""with open(self.file_path, encoding="utf-8") as f:line_number = 0for line in f:yield Document(page_content=line,metadata={"line_number": line_number, "source": self.file_path},)line_number += 1# alazy_load is OPTIONAL.# If you leave out the implementation, a default implementation which delegates to lazy_load will be used!async def alazy_load(self,) -> AsyncIterator[Document]: # <-- Does not take any arguments"""An async lazy loader that reads a file line by line."""# Requires aiofiles# Install with `pip install aiofiles`# https://github.com/Tinche/aiofilesimport aiofilesasync with aiofiles.open(self.file_path, encoding="utf-8") as f:line_number = 0async for line in f:yield Document(page_content=line,metadata={"line_number": line_number, "source": self.file_path},)line_number += 1
测试文档加载器,
with open("./meow.txt", "w", encoding="utf-8") as f:quality_content = "meow meow🐱 \n meow meow🐱 \n meow😻😻"f.write(quality_content)loader = CustomDocumentLoader("./meow.txt")## Test out the lazy load interface
for doc in loader.lazy_load():print()print(type(doc))print(doc)
<class 'langchain_core.documents.base.Document'>
page_content='meow meow🐱 \n' metadata={'line_number': 0, 'source': './meow.txt'}<class 'langchain_core.documents.base.Document'>
page_content=' meow meow🐱 \n' metadata={'line_number': 1, 'source': './meow.txt'}<class 'langchain_core.documents.base.Document'>
page_content=' meow😻😻' metadata={'line_number': 2, 'source': './meow.txt'}
## Test out the async implementation
async for doc in loader.alazy_load():print()print(type(doc))print(doc)<class 'langchain_core.documents.base.Document'>
page_content='meow meow🐱 \n' metadata={'line_number': 0, 'source': './meow.txt'}<class 'langchain_core.documents.base.Document'>
page_content=' meow meow🐱 \n' metadata={'line_number': 1, 'source': './meow.txt'}<class 'langchain_core.documents.base.Document'>
page_content=' meow😻😻' metadata={'line_number': 2, 'source': './meow.txt'}
避免在生产代码中直接使用load(),因为加载所有内容到入内存中是十分危险的。
loader.load()
[Document(page_content='meow meow🐱 \n', metadata={'line_number': 0, 'source': './meow.txt'}),Document(page_content=' meow meow🐱 \n', metadata={'line_number': 1, 'source': './meow.txt'}),Document(page_content=' meow😻😻', metadata={'line_number': 2, 'source': './meow.txt'})]
许多文档加载器涉及解析文件。这些加载器之间的区别通常源于文件的解析方式,而不是文件的加载方式。例如,您可以使用 open 来读取 PDF 或 markdown 文件的二进制内容,但您需要不同的解析逻辑将该二进制数据转换为文本。
因此,将解析逻辑与加载逻辑解耦可能会很有帮助,这使得无论数据是如何加载的,都更容易重用给定的解析器。
from langchain_core.document_loaders import BaseBlobParser, Blobclass MyParser(BaseBlobParser):"""A simple parser that creates a document from each line."""def lazy_parse(self, blob: Blob) -> Iterator[Document]:"""Parse a blob into a document line by line."""line_number = 0with blob.as_bytes_io() as f:for line in f:line_number += 1yield Document(page_content=line,metadata={"line_number": line_number, "source": blob.source},)
blob = Blob.from_path("./meow.txt")
parser = MyParser()list(parser.lazy_parse(blob))[Document(page_content='meow meow🐱 \n', metadata={'line_number': 1, 'source': './meow.txt'}),Document(page_content=' meow meow🐱 \n', metadata={'line_number': 2, 'source': './meow.txt'}),Document(page_content=' meow😻😻', metadata={'line_number': 3, 'source': './meow.txt'})]
使用 blob API 还允许直接从内存加载内容,而无需从文件中读取!
blob = Blob(data=b"some data from memory\nmeow")
list(parser.lazy_parse(blob))
[Document(page_content='some data from memory\n', metadata={'line_number': 1, 'source': None}),Document(page_content='meow', metadata={'line_number': 2, 'source': None})]
Blob API:
blob = Blob.from_path("./meow.txt", metadata={"foo": "bar"})
blob.encoding
'utf-8'blob.as_bytes()
b'meow meow\xf0\x9f\x90\xb1 \n meow meow\xf0\x9f\x90\xb1 \n meow\xf0\x9f\x98\xbb\xf0\x9f\x98\xbb'blob.as_string()
'meow meow🐱 \n meow meow🐱 \n meow😻😻'blob.as_bytes_io()
<contextlib._GeneratorContextManager at 0x743f34324450>blob.metadata
{'foo': 'bar'}blob.source
'./meow.txt'
虽然解析器封装了将二进制数据解析为文档所需的逻辑,但 blob 加载器 封装了从给定存储位置加载 blobs 所需的逻辑。
目前,LangChain 仅支持 FileSystemBlobLoader。
您可以使用 FileSystemBlobLoader 加载 blobs,然后使用解析器对其进行解析。
from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoaderblob_loader = FileSystemBlobLoader(path=".", glob="*.mdx", show_progress=True)parser = MyParser()
for blob in blob_loader.yield_blobs():for doc in parser.lazy_parse(blob):print(doc)break
0%| | 0/8 [00:00<?, ?it/s]page_content='# Microsoft Office\n' metadata={'line_number': 1, 'source': 'office_file.mdx'}
page_content='# Markdown\n' metadata={'line_number': 1, 'source': 'markdown.mdx'}
page_content='# JSON\n' metadata={'line_number': 1, 'source': 'json.mdx'}
page_content='---\n' metadata={'line_number': 1, 'source': 'pdf.mdx'}
page_content='---\n' metadata={'line_number': 1, 'source': 'index.mdx'}
page_content='# File Directory\n' metadata={'line_number': 1, 'source': 'file_directory.mdx'}
page_content='# CSV\n' metadata={'line_number': 1, 'source': 'csv.mdx'}
page_content='# HTML\n' metadata={'line_number': 1, 'source': 'html.mdx'}
LangChain 具有一个 GenericLoader 抽象,它将 BlobLoader 与 BaseBlobParser 组合在一起。
GenericLoader 旨在提供标准化的类方法,使使用现有的 BlobLoader 实现变得简单。目前,仅支持 FileSystemBlobLoader。
from langchain_community.document_loaders.generic import GenericLoaderloader = GenericLoader.from_filesystem(path=".", glob="*.mdx", show_progress=True, parser=MyParser()
)for idx, doc in enumerate(loader.lazy_load()):if idx < 5:print(doc)print("... output truncated for demo purposes")
0%| | 0/8 [00:00<?, ?it/s]page_content='# Microsoft Office\n' metadata={'line_number': 1, 'source': 'office_file.mdx'}
page_content='\n' metadata={'line_number': 2, 'source': 'office_file.mdx'}
page_content='>[The Microsoft Office](https://www.office.com/) suite of productivity software includes Microsoft Word, Microsoft Excel, Microsoft PowerPoint, Microsoft Outlook, and Microsoft OneNote. It is available for Microsoft Windows and macOS operating systems. It is also available on Android and iOS.\n' metadata={'line_number': 3, 'source': 'office_file.mdx'}
page_content='\n' metadata={'line_number': 4, 'source': 'office_file.mdx'}
page_content='This covers how to load commonly used file formats including `DOCX`, `XLSX` and `PPTX` documents into a document format that we can use downstream.\n' metadata={'line_number': 5, 'source': 'office_file.mdx'}
... output truncated for demo purposes
如果你真的喜欢创建类,你可以继承并创建一个类来封装逻辑。
你可以从这个类继承,以使用现有的加载器加载内容。
from typing import Anyclass MyCustomLoader(GenericLoader):@staticmethoddef get_parser(**kwargs: Any) -> BaseBlobParser:"""Override this method to associate a default parser with the class."""return MyParser()
loader = MyCustomLoader.from_filesystem(path=".", glob="*.mdx", show_progress=True)for idx, doc in enumerate(loader.lazy_load()):if idx < 5:print(doc)print("... output truncated for demo purposes")
0%| | 0/8 [00:00<?, ?it/s]page_content='# Microsoft Office\n' metadata={'line_number': 1, 'source': 'office_file.mdx'}
page_content='\n' metadata={'line_number': 2, 'source': 'office_file.mdx'}
page_content='>[The Microsoft Office](https://www.office.com/) suite of productivity software includes Microsoft Word, Microsoft Excel, Microsoft PowerPoint, Microsoft Outlook, and Microsoft OneNote. It is available for Microsoft Windows and macOS operating systems. It is also available on Android and iOS.\n' metadata={'line_number': 3, 'source': 'office_file.mdx'}
page_content='\n' metadata={'line_number': 4, 'source': 'office_file.mdx'}
page_content='This covers how to load commonly used file formats including `DOCX`, `XLSX` and `PPTX` documents into a document format that we can use downstream.\n' metadata={'line_number': 5, 'source': 'office_file.mdx'}
... output truncated for demo purposes
文本分割组件
一旦加载文档,通常需要对其进行转换,使其更适应您的应用场景。最常见的需求是将一篇长文档拆分成适合模型上下文窗口的小块。LangChain 提供了多种内置的文档转换工具,方便进行拆分、合并、过滤等操作。
在处理长文本时,拆分文本是必要的。虽然看似简单,但其中存在许多潜在的复杂性。理想情况下,希望将语义相关的文本片段放在一起,而“语义相关”具体指什么,则取决于文本的类型。本示例展示了几种不同的实现方式。
从整体流程来看,文本分割器的工作方式如下:
- 将文本拆分成较小的、具有语义意义的片段(通常是句子)。
- 逐步合并这些小片段,直到达到设定的块大小(通过特定的度量方式确定)。
- 一旦达到该大小,就将其作为独立文本块,并开始创建新的文本块,同时保留部分重叠,以维持块之间的上下文联系。
因此,文本分割器可以在两个方面进行定制:
- 文本的拆分方式:如何划分文本片段
- 块大小的衡量标准:如何确定合适的块大小
按字符递归分割文本
这个文本分割器是推荐用于通用文本的。它通过字符列表进行参数化。它尝试按顺序在这些字符上进行分割,直到块的大小足够小。默认列表是 ['\n\n', '\n', ' ', '']
。这会尽量保持所有段落(然后是句子,再然后是单词)在一起,因为这些通常被认为是语义上最相关的文本片段。
注:
- 要直接获取字符串内容,请使用 .split_text。
- 要创建 LangChain 文档 对象(例如,用于下游任务),请使用 .create_documents。
pip install -qU langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter# Load example document
with open("state_of_the_union.txt") as f:state_of_the_union = f.read()text_splitter = RecursiveCharacterTextSplitter(# Set a really small chunk size, just to show.chunk_size=100,chunk_overlap=20,length_function=len,is_separator_regex=False,
)
texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])
print(texts[1])
//
page_content='Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and'
page_content='of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans.'
如果只需要切分后的文档信息,可以使用split_text:
text_splitter.split_text(state_of_the_union)[:2]['Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and','of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans.']
上面为 RecursiveCharacterTextSplitter 设置的参数:
- chunk_size: 每个块的最大大小,大小由 length_function 决定。
- chunk_overlap: 块之间的目标重叠。重叠的块有助于减轻在块之间划分上下文时信息的丢失。
- length_function: 确定块大小的函数。
- is_separator_regex: 分隔符列表(默认为 [‘\n\n’, ‘\n’, ’ ', ‘’])是否应被解释为正则表达式。
某些语言的书写系统没有明显的词边界,例如中文、日文和泰文。如果使用默认的分隔符列表 ['\n\n', '\n', ' ', '']
进行文本拆分,可能会导致单词被错误地拆分到不同的块中。为了尽可能保持单词完整,可以自定义分隔符列表,并额外添加以下标点符号:
- 句号:包括 ASCII 句号
.
、Unicode 全角句号.
(常用于中文文本)以及表意全角句号。
(用于日文和中文)。 - 零宽空格:用于泰文、缅甸文、柬埔寨文和日文,以更自然地分隔文本。
- 逗号:包括 ASCII 逗号
,
、Unicode 全角逗号,
以及表意逗号、
(常用于中文和日文)。
text_splitter = RecursiveCharacterTextSplitter(separators=["\n\n","\n"," ",".",",","\u200b", # Zero-width space"\uff0c", # Fullwidth comma"\u3001", # Ideographic comma"\uff0e", # Fullwidth full stop"\u3002", # Ideographic full stop"",],# Existing args
)
按HTML分割文本
按HTML 头部进行分割
HTMLHeaderTextSplitter
是一种 结构感知 的文本分块器,它会在 HTML 元素级别拆分文本,并为与特定块 相关 的标题添加元数据。它可以选择逐个元素返回文本块,或者将具有相同元数据的元素合并,以达到以下目的:
- 保持语义相关的文本尽可能分组,避免内容被不当拆分。
- 保留文档结构中的上下文信息,充分利用 HTML 结构中的层次关系。
该分块器可以与其他文本分割器配合使用,作为文本处理管道的一部分。
在使用 HTMLHeaderTextSplitter
时,可以通过 headers_to_split_on
指定需要拆分的标题级别,如下所示:
from langchain_text_splitters import HTMLSectionSplitterhtml_string = """
<!DOCTYPE html>
<html>
<body><div><h1>Foo</h1><p>Some intro text about Foo.</p><div><h2>Bar main section</h2><p>Some intro text about Bar.</p><h3>Bar subsection 1</h3><p>Some text about the first subtopic of Bar.</p><h3>Bar subsection 2</h3><p>Some text about the second subtopic of Bar.</p></div><div><h2>Baz</h2><p>Some text about Baz</p></div><br><p>Some concluding text about Foo</p></div>
</body>
</html>
"""headers_to_split_on = [("h1", "Header 1"),("h2", "Header 2"),("h3", "Header 3"),
]html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text(html_string)
html_header_splits
[Document(page_content='Foo'),Document(page_content='Some intro text about Foo. \nBar main section Bar subsection 1 Bar subsection 2', metadata={'Header 1': 'Foo'}),Document(page_content='Some intro text about Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section'}),Document(page_content='Some text about the first subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 1'}),Document(page_content='Some text about the second subtopic of Bar.', metadata={'Header 1': 'Foo', 'Header 2': 'Bar main section', 'Header 3': 'Bar subsection 2'}),Document(page_content='Baz', metadata={'Header 1': 'Foo'}),Document(page_content='Some text about Baz', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'}),Document(page_content='Some concluding text about Foo', metadata={'Header 1': 'Foo'})]
要将每个元素与其相关的标题一起返回,在实例化 HTMLHeaderTextSplitter 时指定 return_each_element=True:
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on,return_each_element=True,
)
html_header_splits_elements = html_splitter.split_text(html_string)
与上述内容相比,元素按其标题聚合:
for element in html_header_splits[:2]:print(element)page_content='Foo'
page_content='Some intro text about Foo. \nBar main section Bar subsection 1 Bar subsection 2' metadata={'Header 1': 'Foo'}
现在每个元素作为一个独立的 Document 返回:
for element in html_header_splits_elements[:3]:print(element)page_content='Foo'
page_content='Some intro text about Foo.' metadata={'Header 1': 'Foo'}
page_content='Bar main section Bar subsection 1 Bar subsection 2' metadata={'Header 1': 'Foo'}
如何从 URL 或 HTML 文件中拆分?
要直接从 URL 读取,可以将 URL 字符串传递给 split_text_from_url 方法。同样,可以将本地 HTML 文件传递给 split_text_from_file 方法。
url = "https://plato.stanford.edu/entries/goedel/"headers_to_split_on = [("h1", "Header 1"),("h2", "Header 2"),("h3", "Header 3"),("h4", "Header 4"),
]html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)# for local file use html_splitter.split_text_from_file(<path_to_file>)
html_header_splits = html_splitter.split_text_from_url(url)
HTMLHeaderTextSplitter 根据 HTML 标题进行拆分,可以与另一个根据字符长度限制拆分的拆分器组合,例如 RecursiveCharacterTextSplitter。
这可以通过第二个拆分器的 .split_documents 方法完成:
from langchain_text_splitters import RecursiveCharacterTextSplitterchunk_size = 500
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap
)# Split
splits = text_splitter.split_documents(html_header_splits)
splits[80:85]
[Document(page_content='We see that Gödel first tried to reduce the consistency problem for analysis to that of arithmetic. This seemed to require a truth definition for arithmetic, which in turn led to paradoxes, such as the Liar paradox (“This sentence is false”) and Berry’s paradox (“The least number not defined by an expression consisting of just fourteen English words”). Gödel then noticed that such paradoxes would not necessarily arise if truth were replaced by provability. But this means that arithmetic truth', metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}),Document(page_content='means that arithmetic truth and arithmetic provability are not co-extensive — whence the First Incompleteness Theorem.', metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}),Document(page_content='This account of Gödel’s discovery was told to Hao Wang very much after the fact; but in Gödel’s contemporary correspondence with Bernays and Zermelo, essentially the same description of his path to the theorems is given. (See Gödel 2003a and Gödel 2003b respectively.) From those accounts we see that the undefinability of truth in arithmetic, a result credited to Tarski, was likely obtained in some form by Gödel by 1931. But he neither publicized nor published the result; the biases logicians', metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}),Document(page_content='result; the biases logicians had expressed at the time concerning the notion of truth, biases which came vehemently to the fore when Tarski announced his results on the undefinability of truth in formal systems 1935, may have served as a deterrent to Gödel’s publication of that theorem.', metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.1 The First Incompleteness Theorem'}),Document(page_content='We now describe the proof of the two theorems, formulating Gödel’s results in Peano arithmetic. Gödel himself used a system related to that defined in Principia Mathematica, but containing Peano arithmetic. In our presentation of the First and Second Incompleteness Theorems we refer to Peano arithmetic as P, following Gödel’s notation.', metadata={'Header 1': 'Kurt Gödel', 'Header 2': '2. Gödel’s Mathematical Work', 'Header 3': '2.2 The Incompleteness Theorems', 'Header 4': '2.2.2 The proof of the First Incompleteness Theorem'})]
不同的HTML文档之间可能存在相当大的结构变化,虽然HTMLHeaderTextSplitter会尝试将所有“相关”的标题附加到任何给定的块上,但有时它可能会遗漏某些标题。
例如,该算法假设存在一个信息层次结构,其中标题总是在与其相关文本“上方”的节点上,即前兄弟、祖先及其组合。在以下新闻文章中(截至本文撰写时),文档的结构使得顶级标题的文本虽然标记为“h1”,但与我们期望它“上方”的文本元素处于不同的子树中——因此我们可以观察到“h1”元素及其相关文本未出现在块元数据中(但在适用的情况下,我们确实看到了“h2”及其相关文本):
url = "https://www.cnn.com/2023/09/25/weather/el-nino-winter-us-climate/index.html"headers_to_split_on = [("h1", "Header 1"),("h2", "Header 2"),
]html_splitter = HTMLHeaderTextSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text_from_url(url)
print(html_header_splits[1].page_content[:500])
No two El Niño winters are the same, but many have temperature and precipitation trends in common.
Average conditions during an El Niño winter across the continental US.
One of the major reasons is the position of the jet stream, which often shifts south during an El Niño winter. This shift typically brings wetter and cooler weather to the South while the North becomes drier and warmer, according to NOAA.
Because the jet stream is essentially a river of air that storms flow through, they c
按HTML成分进行分割
与 HTMLHeaderTextSplitter
类似,HTMLSectionSplitter
也是一种 结构感知 的分块器,它会在 HTML 元素级别拆分文本,并为每个与特定块 相关 的标题添加元数据。
该分块器可以选择逐个返回文本块,或者将具有相同元数据的元素合并,以实现以下目标:
- 保持语义相关的文本尽可能归为一组,避免破坏上下文结构。
- 保留 HTML 结构中编码的上下文信息,以便更准确地理解文本层次。
HTML 转换与 XSLT 支持
HTMLSectionSplitter
支持使用 xslt_path
指定一个 XSLT 文件的 绝对路径,用于转换 HTML 结构,以便更精准地检测文本块。
默认情况下,它会使用 data_connection/document_transformers
目录下的 converting_to_header.xslt
文件,将 HTML 转换为 更易于分块 的格式。例如,可以根据 字体大小 将 <span>
标签转换为标题标签,使其在分块时被识别为独立的部分。
from langchain_text_splitters import HTMLSectionSplitterhtml_string = """<!DOCTYPE html><html><body><div><h1>Foo</h1><p>Some intro text about Foo.</p><div><h2>Bar main section</h2><p>Some intro text about Bar.</p><h3>Bar subsection 1</h3><p>Some text about the first subtopic of Bar.</p><h3>Bar subsection 2</h3><p>Some text about the second subtopic of Bar.</p></div><div><h2>Baz</h2><p>Some text about Baz</p></div><br><p>Some concluding text about Foo</p></div></body></html>
"""headers_to_split_on = [("h1", "Header 1"), ("h2", "Header 2")]html_splitter = HTMLSectionSplitter(headers_to_split_on)
html_header_splits = html_splitter.split_text(html_string)
html_header_splits
HTMLSectionSplitter 可以与其他文本分割器一起使用,作为分块管道的一部分。内部,当节的大小大于块的大小时,它使用 RecursiveCharacterTextSplitter。它还考虑文本的字体大小,以根据确定的字体大小阈值来判断它是否为一个节。
from langchain_text_splitters import RecursiveCharacterTextSplitterhtml_string = """<!DOCTYPE html><html><body><div><h1>Foo</h1><p>Some intro text about Foo.</p><div><h2>Bar main section</h2><p>Some intro text about Bar.</p><h3>Bar subsection 1</h3><p>Some text about the first subtopic of Bar.</p><h3>Bar subsection 2</h3><p>Some text about the second subtopic of Bar.</p></div><div><h2>Baz</h2><p>Some text about Baz</p></div><br><p>Some concluding text about Foo</p></div></body></html>
"""headers_to_split_on = [("h1", "Header 1"),("h2", "Header 2"),("h3", "Header 3"),("h4", "Header 4"),
]html_splitter = HTMLSectionSplitter(headers_to_split_on)html_header_splits = html_splitter.split_text(html_string)chunk_size = 500
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap
)# Split
splits = text_splitter.split_documents(html_header_splits)
splits
[Document(page_content='Foo \n Some intro text about Foo.', metadata={'Header 1': 'Foo'}),Document(page_content='Bar main section \n Some intro text about Bar.', metadata={'Header 2': 'Bar main section'}),Document(page_content='Bar subsection 1 \n Some text about the first subtopic of Bar.', metadata={'Header 3': 'Bar subsection 1'}),Document(page_content='Bar subsection 2 \n Some text about the second subtopic of Bar.', metadata={'Header 3': 'Bar subsection 2'}),Document(page_content='Baz \n Some text about Baz \n \n \n Some concluding text about Foo', metadata={'Header 2': 'Baz'})]
注意:HTMLHeaderTextSplitter和HTMLSectionSplitter属于两种特殊分割器,并没有继承BaseSpliter,所以没有create_documents等方法;
按字符分割
这是最简单的方法。它基于给定的字符序列进行分割,默认值为"\n\n"。块长度以字符数来衡量。(要创建LangChain 文档 对象(例如,用于下游任务),请使用.create_documents。)
from langchain_text_splitters import CharacterTextSplitter# Load an example document
with open("state_of_the_union.txt") as f:state_of_the_union = f.read()text_splitter = CharacterTextSplitter(separator="\n\n",chunk_size=1000,chunk_overlap=200,length_function=len,is_separator_regex=False,
)
texts = text_splitter.create_documents([state_of_the_union])
print(texts[0])
page_content='Madam Speaker, Madam Vice President, our First Lady and Second Gentleman. Members of Congress and the Cabinet. Justices of the Supreme Court. My fellow Americans. \n\nLast year COVID-19 kept us apart. This year we are finally together again. \n\nTonight, we meet as Democrats Republicans and Independents. But most importantly as Americans. \n\nWith a duty to one another to the American people to the Constitution. \n\nAnd with an unwavering resolve that freedom will always triumph over tyranny. \n\nSix days ago, Russia’s Vladimir Putin sought to shake the foundations of the free world thinking he could make it bend to his menacing ways. But he badly miscalculated. \n\nHe thought he could roll into Ukraine and the world would roll over. Instead he met a wall of strength he never imagined. \n\nHe met the Ukrainian people. \n\nFrom President Zelenskyy to every Ukrainian, their fearlessness, their courage, their determination, inspires the world.'
使用 .create_documents
将与每个文档相关的元数据传播到输出块:
metadatas = [{"document": 1}, {"document": 2}]
documents = text_splitter.create_documents([state_of_the_union, state_of_the_union], metadatas=metadatas
)
print(documents[0])
使用 .split_text 直接获取字符串内容:
text_splitter.split_text(state_of_the_union)[0]
分割代码
递归字符文本分割器 包含用于在特定编程语言中分割文本的预构建分隔符列表。
支持的语言存储在 langchain_text_splitters.Language 枚举中。它们包括:
"cpp",
"go",
"java",
"kotlin",
"js",
"ts",
"php",
"proto",
"python",
"rst",
"ruby",
"rust",
"scala",
"swift",
"markdown",
"latex",
"html",
"sol",
"csharp",
"cobol",
"c",
"lua",
"perl",
"haskell"
要查看给定语言的分隔符列表,请将此枚举中的值传入
RecursiveCharacterTextSplitter.get_separators_for_language
要实例化一个针对特定语言的分割器,请将枚举中的值传入
RecursiveCharacterTextSplitter.from_language
可以查看给定语言使用的分隔符:
from langchain_text_splitters import (Language,RecursiveCharacterTextSplitter,
)
RecursiveCharacterTextSplitter.get_separators_for_language(Language.PYTHON)
分割python:
PYTHON_CODE = """
def hello_world():print("Hello, World!")# Call the function
hello_world()
"""
python_splitter = RecursiveCharacterTextSplitter.from_language(language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents([PYTHON_CODE])
python_docs[Document(page_content='def hello_world():\n print("Hello, World!")'),Document(page_content='# Call the function\nhello_world()')]
Markdown:
markdown_text = """
# 🦜️🔗 LangChain⚡ Building applications with LLMs through composability ⚡## Quick Install# Hopefully this code block isn't split
pip install langchainAs an open-source project in a rapidly developing field, we are extremely open to contributions.
"""md_splitter = RecursiveCharacterTextSplitter.from_language(language=Language.MARKDOWN, chunk_size=60, chunk_overlap=0
)
md_docs = md_splitter.create_documents([markdown_text])
md_docs[Document(page_content='# 🦜️🔗 LangChain'),Document(page_content='⚡ Building applications with LLMs through composability ⚡'),Document(page_content='## Quick Install'),Document(page_content="# Hopefully this code block isn't split"),Document(page_content='pip install langchain'),Document(page_content='As an open-source project in a rapidly developing field, we'),Document(page_content='are extremely open to contributions.')]
按标题分割Markdown
markdown 文件是通过标题组织的。在特定标题组内创建块是一个直观的想法。为了解决这个挑战,我们可以使用 MarkdownHeaderTextSplitter。这将根据指定的标题集拆分 markdown 文件。
from langchain_text_splitters import MarkdownHeaderTextSplittermarkdown_document = "# Foo\n\n ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo \n\n Hi this is Lance \n\n ## Baz\n\n Hi this is Molly"headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),("###", "Header 3"),
]markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits[Document(page_content='Hi this is Jim \nHi this is Joe', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),Document(page_content='Hi this is Lance', metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}),Document(page_content='Hi this is Molly', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'})]
默认情况下,MarkdownHeaderTextSplitter 会从输出块的内容中剥离正在拆分的标题。通过设置 strip_headers = False 可以禁用此功能。
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on, strip_headers=False)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits[Document(page_content='# Foo \n## Bar \nHi this is Jim \nHi this is Joe', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),Document(page_content='### Boo \nHi this is Lance', metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}),Document(page_content='## Baz \nHi this is Molly', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'})]
默认情况下,MarkdownHeaderTextSplitter 根据 headers_to_split_on 中指定的标题聚合行。我们可以通过指定 return_each_line 来禁用此功能:
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on,return_each_line=True,
)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits[Document(page_content='Hi this is Jim', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),Document(page_content='Hi this is Joe', metadata={'Header 1': 'Foo', 'Header 2': 'Bar'}),Document(page_content='Hi this is Lance', metadata={'Header 1': 'Foo', 'Header 2': 'Bar', 'Header 3': 'Boo'}),Document(page_content='Hi this is Molly', metadata={'Header 1': 'Foo', 'Header 2': 'Baz'})]
在每个markdown组内,我们可以应用任何我们想要的文本分割器,例如RecursiveCharacterTextSplitter,它允许进一步控制块大小。
markdown_document = "# Intro \n\n ## History \n\n Markdown[9] is a lightweight markup language for creating formatted text using a plain-text editor. John Gruber created Markdown in 2004 as a markup language that is appealing to human readers in its source code form.[9] \n\n Markdown is widely used in blogging, instant messaging, online forums, collaborative software, documentation pages, and readme files. \n\n ## Rise and divergence \n\n As Markdown popularity grew rapidly, many Markdown implementations appeared, driven mostly by the need for \n\n additional features such as tables, footnotes, definition lists,[note 1] and Markdown inside HTML blocks. \n\n #### Standardization \n\n From 2012, a group of people, including Jeff Atwood and John MacFarlane, launched what Atwood characterised as a standardisation effort. \n\n ## Implementations \n\n Implementations of Markdown are available for over a dozen programming languages."headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),
]# MD splits
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on, strip_headers=False
)
md_header_splits = markdown_splitter.split_text(markdown_document)# Char-level splits
from langchain_text_splitters import RecursiveCharacterTextSplitterchunk_size = 250
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap
)# Split
splits = text_splitter.split_documents(md_header_splits)
splits
分割 JSON 数据
JSONSplitter
允许在 控制块大小 的同时对 JSON 数据进行分割。它采用 深度优先遍历 的方式,构建较小的 JSON 块,并尽量保持嵌套的 JSON 结构完整。但如果 JSON 结构过大,为了符合 min_chunk_size
和 max_chunk_size
之间的限制,仍可能会对其进行拆分。
处理逻辑
- 嵌套对象:尽可能保持完整,但如果超出块大小限制,则进行拆分。
- 长字符串:如果 JSON 值是一个 非常大的字符串,则不会自动拆分。如果需要严格控制块大小,可结合 递归文本分割器 进一步处理。
- 列表处理(可选):可以先将列表转换为 JSON 字典,然后再进行拆分,以更精确地控制块大小。
分割规则
- 如何拆分:按 JSON 值 进行分割。
- 如何计算块大小:按 字符数 测量。
指定 max_chunk_size 来限制块大小:
from langchain_text_splitters import RecursiveJsonSplittersplitter = RecursiveJsonSplitter(max_chunk_size=300)
要获取json块,请使用 .split_json 方法:
json_chunks = splitter.split_json(json_data=json_data)for chunk in json_chunks[:3]:print(chunk)
{'openapi': '3.1.0', 'info': {'title': 'LangSmith', 'version': '0.1.0'}, 'servers': [{'url': 'https://api.smith.langchain.com', 'description': 'LangSmith API endpoint.'}]}
{'paths': {'/api/v1/sessions/{session_id}': {'get': {'tags': ['tracer-sessions'], 'summary': 'Read Tracer Session', 'description': 'Get a specific session.', 'operationId': 'read_tracer_session_api_v1_sessions__session_id__get'}}}}
{'paths': {'/api/v1/sessions/{session_id}': {'get': {'security': [{'API Key': []}, {'Tenant ID': []}, {'Bearer Auth': []}]}}}}
要获取 LangChain 文档 对象,使用 .create_documents 方法:
# The splitter can also output documents
docs = splitter.create_documents(texts=[json_data])for doc in docs[:3]:print(doc)
或者使用 .split_text 直接获取字符串内容:
texts = splitter.split_text(json_data=json_data)print(texts[0])
print(texts[1])
上文示例中的一个块大于指定的 max_chunk_size 300。查看这个较大的块,我们看到里面有一个列表对象:
{"paths": {"/api/v1/sessions/{session_id}": {"get": {"parameters": [{"name": "session_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Session Id"}}, {"name": "include_stats", "in": "query", "required": false, "schema": {"type": "boolean", "default": false, "title": "Include Stats"}}, {"name": "accept", "in": "header", "required": false, "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Accept"}}]}}}}
默认情况下,json分割器不会分割列表。
指定 convert_lists=True 来预处理json,将列表内容转换为 index:item 形式的字典,作为 key:val 对:
texts = splitter.split_text(json_data=json_data, convert_lists=True)
列表已被转换为字典,但即使分成多个块,仍保留所有所需的上下文信息:
print(texts[1])
{"paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": {"0": "tracer-sessions"}, "summary": "Read Tracer Session", "description": "Get a specific session.", "operationId": "read_tracer_session_api_v1_sessions__session_id__get"}}}}docs[1]
Document(page_content='{"paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session.", "operationId": "read_tracer_session_api_v1_sessions__session_id__get"}}}}')
根据语义相似性分割文本
pip install --quiet langchain_experimental langchain_openai
加载示例数据:
# This is a long document we can split up.
with open("state_of_the_union.txt") as f:state_of_the_union = f.read()
要实例化一个 SemanticChunker,我们必须指定一个嵌入模型。下面我们将使用 OpenAIEmbeddings。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddingstext_splitter = SemanticChunker(OpenAIEmbeddings())
我们以通常的方式分割文本,例如,通过调用 .create_documents 来创建 LangChain 文档 对象:
docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
这个分割器通过确定何时“断开”句子来工作。这是通过查看任何两个句子之间的嵌入差异来完成的。当该差异超过某个阈值时,它们就会被分割。
有几种方法可以确定该阈值,这些方法由 breakpoint_threshold_type 关键字参数控制。
默认的分割方式是基于百分位。在这种方法中,计算句子之间的所有差异,然后将任何大于 X 百分位的差异进行分割。
text_splitter = SemanticChunker(OpenAIEmbeddings(), breakpoint_threshold_type="percentile"
)docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
标准差:在此方法中,任何大于 X 个标准差的差异都会被拆分。
text_splitter = SemanticChunker(OpenAIEmbeddings(), breakpoint_threshold_type="standard_deviation"
)docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
四分位数:在此方法中,使用四分位距来拆分块。
text_splitter = SemanticChunker(OpenAIEmbeddings(), breakpoint_threshold_type="interquartile"
)docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
梯度:在此方法中,使用距离的梯度与百分位法一起拆分块。 当块之间高度相关或特定于某个领域(例如法律或医学)时,此方法非常有用。其思想是对梯度数组应用异常检测,以便分布变得更宽,从而更容易识别高度语义数据中的边界。
text_splitter = SemanticChunker(OpenAIEmbeddings(), breakpoint_threshold_type="gradient"
)docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
语义分割的思路来源于大佬的笔记:https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/tutorials/LevelsOfTextSplitting/5_Levels_Of_Text_Splitting.ipynb
可以看到,大佬的笔记中记录了5种等级的文本分割方法:
嵌入模型/向量存储
嵌入模型创建文本片段的向量表示。
可以将向量视为一个数字数组,它捕捉了文本的语义含义。 通过这种方式表示文本,您可以执行数学运算,从而进行诸如搜索其他在意义上最相似的文本等操作。 这些自然语言搜索能力支撑着许多类型的上下文检索, 在这里,我们为大型语言模型提供其有效响应查询所需的相关数据。
Embeddings类是一个用于与文本嵌入模型接口的类。存在许多不同的嵌入大模型供应商(OpenAI、Cohere、Hugging Face等)和本地模型,此类旨在为它们提供标准接口。
LangChain中的基础嵌入类提供了两种方法:一种用于嵌入文档,另一种用于嵌入查询。前者接受多个文本作为输入,而后者接受单个文本。将这两者作为两个单独的方法的原因是某些嵌入大模型供应商对文档(待搜索的内容)和查询(搜索查询本身)有不同的嵌入方法。(存储和搜索非结构化数据的最常见方法之一是将其嵌入并存储生成的嵌入向量, 然后在查询时嵌入非结构化查询并检索与嵌入查询 ‘最相似’ 的嵌入向量。 向量存储负责为您存储嵌入数据并执行向量搜索。大多数向量存储还可以存储有关嵌入向量的元数据,并支持在相似性搜索之前对该元数据进行过滤, 让您对返回的文档有更多控制。)
文本嵌入模型
Embeddings 类是一个用于与文本嵌入模型接口的类。有很多嵌入大模型供应商(OpenAI、Cohere、Hugging Face 等) - 这个类旨在为它们提供一个标准接口。
嵌入会创建一段文本的向量表示。这是有用的,因为这意味着我们可以在向量空间中思考文本,并进行语义搜索,寻找在向量空间中最相似的文本片段。
LangChain 中的基础 Embeddings 类提供了两个方法:一个用于嵌入文档,一个用于嵌入查询。前者,.embed_documents,接受多个文本作为输入,而后者,.embed_query,接受单个文本。将这两个方法分开是因为某些嵌入大模型供应商对文档(待搜索的内容)和查询(搜索查询本身)有不同的嵌入方法。 .embed_query 将返回一个浮点数列表,而 .embed_documents 返回一个浮点数列表的列表(向量)。
对于openai合作伙伴包:
pip install langchain-openai
对于Hugging Face合作伙伴包:
pip install langchain-huggingface
访问openai API需要一个API密钥,一旦我们有了密钥,我们将通过运行将其设置为环境变量:
export OPENAI_API_KEY="..."
如果您不想设置环境变量,可以在初始化OpenAI LLM类时通过api_key命名参数直接传递密钥:
from langchain_openai import OpenAIEmbeddingsembeddings_model = OpenAIEmbeddings(api_key="...")
本地模型也可以在没有任何参数的情况下进行初始化:
from langchain_openai import OpenAIEmbeddingsembeddings_model = OpenAIEmbeddings()
也可以从Hugging Face Hub加载任何emb模型。
from langchain_huggingface import HuggingFaceEmbeddingsembeddings_model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
嵌入文本列表:使用 .embed_documents 嵌入字符串列表,返回嵌入列表:
embeddings = embeddings_model.embed_documents(["Hi there!","Oh, hello!","What's your name?","My friends call me World","Hello World!"]
)
len(embeddings), len(embeddings[0])
(5, 1536)
嵌入单个查询:使用 .embed_query 嵌入单个文本(例如,用于与其他嵌入文本进行比较)
embedded_query = embeddings_model.embed_query("What was the name mentioned in the conversation?")
embedded_query[:5]
[0.0053587136790156364,-0.0004999046213924885,0.038883671164512634,-0.003001077566295862,-0.00900818221271038]
缓存嵌入结果
为了避免重复计算,嵌入结果可以 存储 或 临时缓存。
使用 CacheBackedEmbeddings
进行嵌入缓存
CacheBackedEmbeddings
作为一个 封装器,可以在 键值存储 中缓存嵌入结果。它通过 对文本进行哈希处理 生成唯一键,并将计算后的嵌入存入缓存,以加速后续查询。
初始化 CacheBackedEmbeddings
推荐使用 from_bytes_store
方法进行初始化,主要参数如下:
underlying_embedder
:实际用于计算嵌入的模型。document_embedding_cache
:用于存储文档嵌入的 ByteStore(字节存储)。batch_size
(可选,默认为None
):控制 批量处理的文档数量,减少存储更新频率。namespace
(可选,默认为""
):命名空间,用于区分不同的缓存,避免 不同模型对相同文本的嵌入发生冲突。建议设置为嵌入模型的名称。query_embedding_cache
(可选,默认为None
或 不缓存):- 可提供一个 ByteStore 以缓存查询嵌入。
- 也可以设为
True
,直接复用document_embedding_cache
作为查询缓存。
注意事项
✅ 建议设置 namespace
,以免不同模型嵌入相同文本时出现缓存冲突。
✅ 默认情况下,不缓存查询嵌入。如果需要缓存查询结果,需要 显式设置 query_embedding_cache
。
from langchain.embeddings import CacheBackedEmbeddings
首先,让我们看一个使用本地文件系统存储嵌入并使用FAISS向量存储进行检索的示例。(FAISS 是 Facebook 推出的向量搜索库,里面提供了高性能的向量搜索工具。)
pip install --upgrade --quiet langchain-openai faiss-cpu
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitterunderlying_embeddings = OpenAIEmbeddings()store = LocalFileStore("./cache/")cached_embedder = CacheBackedEmbeddings.from_bytes_store(underlying_embeddings, store, namespace=underlying_embeddings.model
)
在嵌入之前缓存是空的:
list(store.yield_keys())
加载文档,将其拆分为块,嵌入每个块并将其加载到向量存储中。
raw_documents = TextLoader("state_of_the_union.txt").load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents(raw_documents)
创建向量存储:
db = FAISS.from_documents(documents, cached_embedder)
CPU times: user 218 ms, sys: 29.7 ms, total: 248 ms
Wall time: 1.02 s
如果我们尝试再次创建向量存储,它会快得多,因为不需要重新计算任何嵌入。
db2 = FAISS.from_documents(documents, cached_embedder)
CPU times: user 15.7 ms, sys: 2.22 ms, total: 18 ms
Wall time: 17.2 ms
这里是一些创建的嵌入:
list(store.yield_keys())[:5]['text-embedding-ada-00217a6727d-8916-54eb-b196-ec9c9d6ca472','text-embedding-ada-0025fc0d904-bd80-52da-95c9-441015bfb438','text-embedding-ada-002e4ad20ef-dfaa-5916-9459-f90c6d8e8159','text-embedding-ada-002ed199159-c1cd-5597-9757-f80498e8f17b','text-embedding-ada-0021297d37a-2bc1-5e19-bf13-6c950f075062']
为了使用不同的 ByteStore,只需在创建 CacheBackedEmbeddings 时使用它。下面,我们创建一个等效的缓存嵌入对象,但使用非持久的 InMemoryByteStore:
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import InMemoryByteStorestore = InMemoryByteStore()cached_embedder = CacheBackedEmbeddings.from_bytes_store(underlying_embeddings, store, namespace=underlying_embeddings.model
)
创建和查询向量存储
上文讲述了文本向量化以及缓存的使用,但缓存是为了加速已有的向量运算的,而不是存储向量的!
存储和搜索非结构化数据的最常见方法之一是将其嵌入并存储生成的嵌入向量, 然后在查询时嵌入非结构化查询并检索与嵌入查询“最相似”的嵌入向量。 向量存储负责存储嵌入数据并执行向量搜索。
Vectorstore | Delete by ID | Filtering | Search by Vector | Search with score | Async | Passes Standard Tests | Multi Tenancy | IDs in add Documents |
---|---|---|---|---|---|---|---|---|
AstraDBVectorStore | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Chroma | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Clickhouse | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
CouchbaseVectorStore | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
DatabricksVectorSearch | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
ElasticsearchStore | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
FAISS | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
InMemoryVectorStore | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
Milvus | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
MongoDBAtlasVectorSearch | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
PGVector | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
PineconeVectorStore | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
QdrantVectorStore | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Redis | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
使用向量存储的关键部分是创建要放入其中的向量, 通常通过嵌入创建。
有许多优秀的向量存储选项,以下是一些免费的、开源的,并且完全在本地机器上运行的选项。(其中FAISS, Chroma非常轻量级,轻量到和sqllite一样)
以chroma为例:
pip install langchain-chroma
from langchain_chroma import Chromadb = FAISS.from_documents(documents, OpenAIEmbeddings())
或
db = Chroma.from_documents(documents, OpenAIEmbeddings())
所有向量存储都暴露了 similarity_search 方法。 这将接收传入的文档,创建它们的嵌入,然后找到所有具有最相似嵌入的文档。
query = "What did the president say about Ketanji Brown Jackson"
docs = db.similarity_search(query)
print(docs[0].page_content)
也可以使用 similarity_search_by_vector 搜索与给定嵌入向量相似的文档,该方法接受嵌入向量作为参数,而不是字符串。
embedding_vector = OpenAIEmbeddings().embed_query(query)
docs = db.similarity_search_by_vector(embedding_vector)
print(docs[0].page_content)
向量存储通常作为一个单独的服务运行,需要一些IO操作,因此它们可能会被异步调用。LangChain支持在向量存储上进行异步操作。所有方法都可以使用其异步对应方法调用。
docs = await db.asimilarity_search(query)
docs
[Document(page_content='Tonight. I call...
很多github的示例服务很多都是基于python的fastapi构建的,新版本的flask也支持asyncio,而django就有点太笨重了。