使用 LangChain 和 watsonx.ai 实现 RAG 分块策略

在本教程中,您将使用 LangChain 和最新 IBM® Granite 模型(现已在 watsonx.ai 上提供),尝试多种分块策略。总体目标是执行检索增强生成 (RAG)

什么是分块?

分块是指将大段文本拆分成较小的文本片段或块的过程。为了强调分块的重要性,理解 RAG 会很有帮助。RAG 是自然语言处理 (NLP) 中的一种技术,它将信息检索和大语言模型 (LLM) 结合起来,从补充数据集中检索相关信息,以优化 LLM 输出的质量。为了管理大型文档,我们可以使用分块将文本拆分成更小的、有意义的片段。然后,可以使用嵌入模型将这些文本块嵌入并存储在矢量数据库中。最后,RAG 系统可以使用语义搜索仅检索最相关的块。较小的分块通常比较大的分块表现更好,因为对于上下文窗口较小的模型来说,它们更易于处理。

分块的一些关键组件包括:

  • 分块策略:为 RAG 应用程序选择正确的分块策略非常重要,因为它决定了设置分块的边界。我们将在下一节深入了解其中的一些问题。
  • 数据块大小:每个数据块中可包含的最大令牌数。确定适当的分块大小通常需要进行一些试验。
  • 块重叠:分块之间重叠的令牌数量,用于保持上下文。这是一个可选参数。

分块策略

有几种不同的分块策略可供选择。为您的 LLM 应用的特定使用场景选择最有效的分块技术非常重要。一些常用的分块处理方法包括:

  • 固定大小的分块:以特定的分块大小和可选的分块重叠进行文本分割。这种方法是最常见、最直接的方式。
  • 递归分块:迭代默认分隔符,直到其中一个生成所需的分块大小。默认分隔符包括 ["\n\n", "\n", " ", ""]。这种分块方法使用分层分隔符,使段落、句子和单词尽可能保持在一起。
  • 语义分块:以一种基于句子嵌入的语义相似性进行分组的方式拆分文本。语义相似度高的嵌入比语义相似度低的嵌入更紧密。这样就能生成上下文相关的数据块。
  • 基于文档的分块:根据文档结构进行分块。该拆分器可以利用 Markdown 文本、图像、表格,甚至 Python 代码类和函数来确定结构。这样,LLM 可以对大型文档进行分块和处理。
  • 智能体式分块:利用智能体式 AI,允许 LLM 根据语义意义以及内容结构(如段落类型、章节标题、逐步说明等)确定合适的文档拆分方式。该分块方法属于实验性,旨在在处理长文档时模拟人类推理。

步骤

第 1 步:设置环境

虽然您可以选择多种工具,本教程将引导您如何设置 IBM 帐户以使用 Jupyter Notebook。

  1. 使用您的 IBM Cloud 帐户登录 watsonx.ai

  2. 创建一个 watsonx.ai 项目

    您可以从项目内部获取项目 ID。点击管理选项卡。然后,从常规页面的详细信息部分复制项目 ID。您需要此 ID 来完成本教程。

  3. 创建一个 Jupyter Notebook

此步骤将打开一个 Notebook 环境,您可以在其中复制本教程中的代码。或者,您可以将此笔记本下载到本地系统并将其作为资产上传到您的 watsonx.ai 项目。要查看更多 Granite 教程,请访问 IBM Granite 社区。可以在 GitHub 上找到此 Jupyter Notebook 以及使用的数据集。

第 2 步:设置 watsonx.ai 运行时实例和 API 密钥

  1. 创建一个 watsonx.ai 运行时服务实例(选择适当的区域并选择精简计划,这是一个免费实例)。

  2. 生成 API 密钥

  3. 将 watsonx.ai 运行时服务实例与您在 watsonx.ai 中创建的项目关联。

第 3 步:安装并导入相关库,并设置您的凭据

#installations
!pip install -q langchain langchain-ibm langchain_experimental langchain-text-splitters langchain_chroma transformers bs4 langchain_huggingface sentence-transformers
# imports
import getpass
from langchain_ibm import WatsonxLLM
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from transformers import AutoTokenizer

为了设置凭据,我们需要您在第 1 步中生成的 WATSONX_APIKEY 和 WATSONX_PROJECT_ID。我们还将设置作为 API 端点的 URL。

WATSONX_APIKEY = getpass.getpass("Please enter your watsonx.ai Runtime API key (hit enter): ")
WATSONX_PROJECT_ID = getpass.getpass("Please enter your project ID (hit enter): ")
URL = "https://us-south.ml.cloud.ibm.com"

第 4 步:初始化 LLM

我们将使用 Granite 3.1 作为本教程的 LLM。为了初始化 LLM,我们需要设置模型参数。了解更多关于这些模型参数的信息,例如最小和最大令牌限制,请参阅文档

llm = WatsonxLLM(
        model_id= "ibm/granite-3-8b-instruct",
        url=URL,
        apikey=WATSONX_APIKEY,
        project_id=WATSONX_PROJECT_ID,
        params={
            GenParams.DECODING_METHOD: "greedy",
            GenParams.TEMPERATURE: 0,
            GenParams.MIN_NEW_TOKENS: 5,
            GenParams.MAX_NEW_TOKENS: 2000,
            GenParams.REPETITION_PENALTY:1.2
        }
)

第 5 步:加载文档

我们在 RAG 管道中使用的上下文是 IBM 官方发布的 Granite 3.1 公告。我们可以使用 LangChain 的 WebBaseLoader 直接从网页将该博客加载到文档中。

url = "https://www.ibm.com/cn-zh/new/announcements/ibm-granite-3-1-powerful-performance-long-context-and-more"
doc = WebBaseLoader(url).load()

第 6 步:执行文本拆分

让我们提供一些示例代码,用于实现我们在本教程前面介绍的每种分块策略,这些代码可以通过 LangChain 获取。

固定大小分块

为了实现固定大小的分块,我们可以使用 LangChain 的 CharacterTextSplitter,并设置 chunk_size 和 chunk_overlap。chunk_size 以字符数来衡量。可以自由尝试不同的值。我们还将分隔符设置为换行符,以便我们可以区分段落。对于令牌化,我们可以使用 granite-3.1-8b-instruct分词器。分词器将文本分解成 LLM 可以处理的令牌。

from langchain_text_splitters import CharacterTextSplitter
tokenizer = AutoTokenizer.from_pretrained(“ibm-granite/granite-3.1-8b-instruct”)
text_splitter = CharacterTextSplitter.from_huggingface_tokenizer(
                    tokenizer,
                    separator=”\n”, #default: “\n\n”
                    chunk_size=1200, chunk_overlap=200)
fixed_size_chunks = text_splitter.create_documents([doc[0].page_content])

我们可以输出其中一个分块,以更好地理解它们的结构。

fixed_size_chunks[1]

输出:(已截断)

Document(metadata={}, page_content=’As always, IBM’s historical commitment to open source is reflected in the permissive and standard open source licensing for every offering discussed in this article.\n\r\n Granite 3.1 8B Instruct: raising the bar for lightweight enterprise models\r\n \nIBM’s efforts in the ongoing optimization the Granite series are most evident in the growth of its flagship 8B dense model. IBM Granite 3.1 8B Instruct now bests most open models in its weight class in average scores on the academic benchmarks evaluations included in the Hugging Face OpenLLM Leaderboard...’)

我们还可以使用分词器来验证我们的处理过程,并检查每个分块中存在的令牌数量。此步骤为可选步骤,仅供参考。

for idx, val in enumerate(fixed_size_chunks):
    token_count = len(tokenizer.encode(val.page_content))
    print(f”The chunk at index {idx} contains {token_count} tokens.”)

输出

The chunk at index 0 contains 1106 tokens.
The chunk at index 1 contains 1102 tokens.
The chunk at index 2 contains 1183 tokens.
The chunk at index 3 contains 1010 tokens.

太棒了!看来我们的数据块大小设置得当。

递归分块

对于递归分块,我们可以使用 LangChain 的 RecursiveCharacterTextSplitter。与固定大小的分块示例一样,我们可以尝试不同的块和重叠大小。

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
recursive_chunks = text_splitter.create_documents([doc[0].page_content])
recursive_chunks[:5]

输出

[Document(metadata={}, page_content=’IBM Granite 3.1: powerful performance, longer context and more’),
Document(metadata={}, page_content=’IBM Granite 3.1: powerful performance, longer context, new embedding models and more’),
Document(metadata={}, page_content=’Artificial Intelligence’),
Document(metadata={}, page_content=’Compute and servers’),
Document(metadata={}, page_content=’IT automation’)]

拆分器使用默认分隔符成功地对文本进行分块:[“\n\n”, “\n”, “ “, “”]。

语义分块

语义分块需要一个嵌入模型或编码器模型。我们可以使用 granite-embedding-30m-english 模型作为我们的嵌入模型。我们也可以输出其中一个块,以便更好地理解它们的结构。

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_experimental.text_splitter import SemanticChunker

embeddings_model = HuggingFaceEmbeddings(model_name=”ibm-granite/granite-embedding-30m-english”)
text_splitter = SemanticChunker(embeddings_model)
semantic_chunks = text_splitter.create_documents([doc[0].page_content])
semantic_chunks[1]

输出:(已截断)

Document(metadata={}, page_content=’Our latest dense models (Granite 3.1 8B, Granite 3.1 2B), MoE models (Granite 3.1 3B-A800M, Granite 3.1 1B-A400M) and guardrail models (Granite Guardian 3.1 8B, Granite Guardian 3.1 2B) all feature a 128K token context length.We’re releasing a family of all-new embedding models. The new retrieval-optimized Granite Embedding models are offered in four sizes, ranging from 30M–278M parameters. Like their generative counterparts, they offer multilingual support across 12 different languages: English, German, Spanish, French, Japanese, Portuguese, Arabic, Czech, Italian, Korean, Dutch and Chinese. Granite Guardian 3.1 8B and 2B feature a new function calling hallucination detection capability, allowing increased control over and observability for agents making tool calls...’)

基于文档的分块

各种文件类型的文档均与 LangChain 的基于文档的文本拆分器兼容。在本教程中,我们将使用 Markdown 文件。有关递归 JSON 拆分、代码拆分和 HTML 拆分的示例,请参阅 LangChain 文档

我们可以加载的 Markdown 文件的示例是 IBM GitHub 上 Granite 3.1 的 README 文件。

url = “https://raw.githubusercontent.com/ibm-granite/granite-3.1-language-models/refs/heads/main/README.md”
markdown_doc = WebBaseLoader(url).load()
markdown_doc

输出

[Document(metadata={‘source’: ‘https://raw.githubusercontent.com/ibm-granite/granite-3.1-language-models/refs/heads/main/README.md’}, page_content=’\n\n\n\n :books: Paper (comming soon)\xa0 | :hugs: HuggingFace Collection\xa0 | \n :speech_balloon: Discussions Page\xa0 | 📘 IBM Granite Docs\n\n\n---\n## Introduction to Granite 3.1 Language Models\nGranite 3.1 language models are lightweight, state-of-the-art, open foundation models that natively support multilinguality, coding, reasoning, and tool usage, including the potential to be run on constrained compute resources. All the models are publicly released under an Apache 2.0 license for both research and commercial use. The models\’ data curation and training procedure were designed for enterprise usage and customization, with a process that evaluates datasets for governance, risk and compliance (GRC) criteria, in addition to IBM\’s standard data clearance process and document quality checks...’)]

现在,我们可以使用 LangChain 的 MarkdownHeaderTextSplitter 按标题类型拆分文件,标题类型在 headers_to_split_on 列表中设置。我们还将输出其中一个块作为示例。

#document based chunking
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
    (“#”, “Header 1”),
    (“##”, “Header 2”),
    (“###”, “Header 3”),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
document_based_chunks = markdown_splitter.split_text(markdown_doc[0].page_content)
document_based_chunks[3]

输出

Document(metadata={‘Header 2’: ‘How to Use our Models?’, ‘Header 3’: ‘Inference’}, page_content=’This is a simple example of how to use Granite-3.1-1B-A400M-Instruct model. \n```python\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\ndevice = “auto”\nmodel_path = “ibm-granite/granite-3.1-1b-a400m-instruct”\ntokenizer = AutoTokenizer.from_pretrained(model_path)\n# drop device_map if running on CPU\nmodel = AutoModelForCausalLM.from_pretrained(model_path, device_map=device)\nmodel.eval()\n# change input text as desired\nchat = [\n{ “role”: “user”, “content”: “Please list one IBM Research laboratory located in the United States. You should only output its name and location.” },\n]\nchat = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n# tokenize the text\ninput_tokens = tokenizer(chat, return_tensors=”pt”).to(device)\n# generate output tokens\noutput = model.generate(**input_tokens,\nmax_new_tokens=100)\n# decode output tokens into text\noutput = tokenizer.batch_decode(output)\n# print output\nprint(output)\n```’)

正如在输出中看到的,分块已成功按标题类型拆分了文本。

第 7 步:创建矢量存储

既然我们已经尝试了各种分块策略,现在让我们继续进行 RAG 的实现。在本教程中,我们将选择语义拆分产生的块,并将它们转换为矢量嵌入。我们可以使用的一个开源矢量存储是 Chroma DB。我们可以通过 langchain_ Chroma 包轻松访问 Chroma 功能。

让我们初始化 Chroma 矢量数据库,为它提供嵌入模型,并添加由语义分块生成的文档。

vector_db = Chroma(
    collection_name=”example_collection”,
    embedding_function=embeddings_model,
    persist_directory=”./chroma_langchain_db”, # Where to save data locally
)
vector_db.add_documents(semantic_chunks)

输出

[‘84fcc1f6-45bb-4031-b12e-031139450cf8’,
‘433da718-0fce-4ae8-a04a-e62f9aa0590d’,
‘4bd97cd3-526a-4f70-abe3-b95b8b47661e’,
‘342c7609-b1df-45f3-ae25-9d9833829105’,
‘46a452f6-2f02-4120-a408-9382c240a26e’]

第 7 步:构建提示模板

接下来,我们可以开始为我们的 LLM 创建提示模板。这个提示模板允许我们在不改变初始提示结构的情况下提出多个问题。我们还可以将矢量存储作为检索器提供。此步骤最终确定了 RAG 的结构。

from langchain.chains import create_retrieval_chain
from langchain.prompts import PromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain

prompt_template = """<|start_of_role|>user<|end_of_role|>Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
{context}
Question: {input}<|end_of_text|>
<|start_of_role|>assistant<|end_of_role|>"""

qa_chain_prompt = PromptTemplate.from_template(prompt_template)
combine_docs_chain = create_stuff_documents_chain(llm, qa_chain_prompt)
rag_chain = create_retrieval_chain(vector_db.as_retriever(), combine_docs_chain)

第 9 步. 提示 RAG 链

使用我们完成的 RAG 工作流,让我们调用一个用户查询。首先,我们可以在不使用已构建矢量存储中任何额外上下文的情况下,有策略地向模型发出提示,以测试模型是依靠内置知识,还是确实在使用 RAG 上下文。Granite 3.1 的公告博客提到了Docling,这是 IBM 用于解析各种文档类型并将其转换为 Markdown 或 JSON 的工具。我们来向 LLM 询问 Dodling 的情况吧

output = llm.invoke(“What is Docling?”)
output

输出

‘?\n\n”Docling” does not appear to be a standard term in English. It might be a typo or a slang term specific to certain contexts. If you meant “documenting,” it refers to the process of creating and maintaining records, reports, or other written materials that provide information about an activity, event, or situation. Please check your spelling or context for clarification.’

显然,该模型没有接受有关 Docling 信息的训练,如果没有外部工具或信息,它就无法为我们提供这些信息。现在,我们来尝试为我们构建的 RAG 链提供相同的查询。

rag_output = rag_chain.invoke({“input”: “What is Docling?”})
rag_output[‘answer’]

输出

‘Docling is a powerful tool developed by IBM Deep Search for parsing documents in various formats such as PDF, DOCX, images, PPTX, XLSX, HTML, and AsciiDoc, and converting them into model-friendly formats like Markdown or JSON. This enables easier access to the information within these documents for models like Granite for tasks such as RAG and other workflows. Docling is designed to integrate seamlessly with agentic frameworks like LlamaIndex, LangChain, and Bee, providing developers with the flexibility to incorporate its assistance into their preferred ecosystem. It surpasses basic optical character recognition (OCR) and text extraction methods by employing advanced contextual and element-based preprocessing techniques. Currently, Docling is open-sourced under the permissive MIT License, and the team continues to develop additional features, including equation and code extraction, as well as metadata extraction.’

太棒了!Granite 模型正确地利用 RAG 上下文告诉我们有关 Docling 的正确信息,同时保持语义连贯性。我们证明,如果不使用 RAG,就不可能取得同样的结果。

摘要

在本教程中,您创建了一个 RAG 管道,并尝试了几种分块策略来提高系统的检索准确率。使用 Granite 3.1 模型,我们成功地针对与提供的文档上下文相关的用户查询生成了适当的模型响应。用于本次 RAG 实现的文本,来源于 ibm.com 上一篇关于 Granite 3.1 发布的博客。该模型提供的信息仅能通过所提供的上下文获取,因为这些信息并不属于模型的初始知识库。

对于希望进一步阅读的人,请查看一项项目的结果,该项目比较了使用 HTML 结构化分块与 watsonx 分块的 LLM 性能。

相关解决方案
IBM watsonx.ai

使用面向 AI 构建器的新一代企业级开发平台 IBM watsonx.ai,可以训练、验证、调整和部署生成式 AI、基础模型和机器学习功能。使用一小部分数据,即可在很短的时间内构建 AI 应用程序。

了解 watsonx.ai
人工智能 (AI) 解决方案

借助 IBM 业界领先的人工智能专业知识和解决方案组合,让人工智能在您的业务中发挥作用。

深入了解 AI 解决方案
人工智能服务

通过增加 AI 重塑关键工作流程和运营,最大限度提升体验、实时决策和商业价值。

深入了解人工智能服务
采取后续步骤

一站式访问跨越 AI 开发生命周期的功能。利用用户友好型界面、工作流并访问行业标准 API 和 SDK,生成功能强大的 AI 解决方案。

深入了解 watsonx.ai 预约实时演示