1. 从数据孤岛到智能应用:为什么我们需要LlamaIndex?

如果你正在构建基于大语言模型(LLM)的应用,无论是企业内部的知识库问答、智能客服,还是个人文档助手,几乎都会遇到一个核心矛盾: LLM强大的通用知识生成能力,与你手中那些非结构化、分散的私有数据之间,存在着一道难以逾越的鸿沟 。这些数据可能是堆积如山的PDF报告、散落在各处的Word文档、公司内部的数据库,或是API返回的JSON流。LLM本身并不“知道”这些信息,而直接将这些原始数据“喂”给LLM,不仅效率低下、成本高昂,还常常因为上下文长度限制和无关信息干扰,导致回答质量堪忧。

这就是LlamaIndex(原GPT Index)诞生的背景。它不是一个简单的“包装器”,而是一个 专为LLM应用设计的数据框架 。你可以把它想象成一个超级智能的“图书管理员”兼“研究员”。它的核心工作流程是:首先,帮你从各种数据源(本地文件、网络、数据库等)中“采集”信息;然后,用一种LLM能够高效理解和检索的方式(如向量索引、知识图谱)来“整理编目”这些信息;最后,当用户提出问题时,它能从海量资料中精准定位最相关的片段,并组织成一份精炼的“参考资料”提交给LLM,从而让LLM生成准确、有据可依的回答。

简单来说,LlamaIndex解决了LLM应用落地的“最后一公里”问题—— 如何让LLM安全、可靠、高效地利用你的私有数据 。它既提供了“开箱即用”的高级API,让新手在5行代码内搭建起一个可用的检索增强生成(RAG)系统;也提供了高度模块化和可扩展的低级API,让资深开发者可以深度定制数据连接器、索引策略、检索器和查询引擎的每一个环节,以满足复杂的企业级需求。

2. 核心架构与核心概念深度解析

要真正用好LlamaIndex,不能只停留在调用 VectorStoreIndex.from_documents 的层面。理解其核心架构和设计哲学,能帮助你在遇到复杂场景时游刃有余。整个框架可以抽象为一条清晰的数据处理与查询流水线。

2.1 核心模块拆解

1. 数据连接器(Data Connectors / Readers) 这是数据进入LlamaIndex世界的入口。框架内置了数十种连接器,覆盖了最常见的格式:

  • 文件类 SimpleDirectoryReader (目录读取)、 PDFReader DocxReader MarkdownReader 等。
  • 数据库类 DatabaseReader (支持SQLAlchemy)、 MongoReader
  • 应用类 SlackReader NotionPageReader GoogleDocsReader
  • 网络类 BeautifulSoupWebReader RssReader

注意 :连接器的选择直接影响数据提取的质量。例如,对于复杂的PDF(包含表格、图表),默认的PDF解析器可能效果不佳,此时就需要考虑使用更强大的解析服务,如后面会提到的LlamaParse。

2. 文档与节点(Documents & Nodes) 从连接器读取的原始数据,首先被封装成 Document 对象。一个 Document 代表一份完整的文档,包含文本内容和元数据(如来源、作者)。但直接处理整篇文档效率太低,因此LlamaIndex引入了 Node (节点)的概念。

  • 节点 :是文档经过“分块”后得到的基本语义单元。分块策略(Chunking)至关重要,它决定了检索的粒度。分块过大,可能包含无关信息,稀释关键内容;分块过小,可能丢失上下文,导致语义不完整。LlamaIndex提供了 TokenTextSplitter SentenceSplitter 等多种分块器,也允许你自定义。
  • 节点关系 :除了文本内容,节点之间还可以建立关系(如前驱、后继、父子),这是构建更复杂索引(如知识图谱)的基础。

3. 索引(Indices) 这是LlamaIndex的“大脑”,负责以某种数据结构组织节点,以便快速检索。最常见的索引是 VectorStoreIndex (向量存储索引),它通过嵌入模型(Embedding Model)将每个节点的文本转换为高维向量,并存储在向量数据库(如Chroma、Pinecone、Weaviate)中。检索时,将查询问题也转换为向量,通过计算余弦相似度找到最相关的节点。

  • 其他索引类型
    • SummaryIndex :为每个文档生成摘要,适合快速概览。
    • TreeIndex :以树形结构组织节点,支持从粗到细的层层递进查询。
    • KeywordTableIndex :基于关键词的倒排索引,适合精确术语匹配。
    • KnowledgeGraphIndex :提取实体和关系构建图谱,适合进行复杂的关联推理。

4. 检索器(Retrievers) 给定一个索引,检索器定义了“如何获取相关节点”的策略。例如,对于 VectorStoreIndex ,其对应的 VectorIndexRetriever 会执行向量相似度搜索。你可以配置 similarity_top_k 参数来控制返回节点的数量。更高级的检索器还包括:

  • BM25Retriever :基于传统的关键词匹配算法,与向量检索结合(混合检索)效果往往更好。
  • AutoMergingRetriever :自动合并相邻的小节点,返回更完整的上下文。

5. 查询引擎(Query Engines) 检索器只负责“找材料”,查询引擎则负责“组织答案”。它接收用户查询,调用检索器获取相关上下文,然后将“查询+上下文”组合成一个提示(Prompt),发送给LLM生成最终答案。这是RAG流程的最终组装环节。

  • 你可以为查询引擎配置不同的响应模式(Response Modes),如 compact (压缩上下文以节省token)、 refine (迭代优化答案)、 tree_summarize (基于树索引的总结)等。

6. 智能体(Agents) 这是LlamaIndex向更高阶应用迈进的体现。智能体不再局限于单次问答,而是具备 使用工具(Tools)、记忆(Memory)和规划(Planning)能力 的自主系统。例如,一个文档分析智能体可以按顺序执行以下工具: parse_tool (解析文档)、 summary_tool (生成摘要)、 qa_tool (回答具体问题)。LlamaIndex的 ReActAgent OpenAIAgent 等实现了与LLM的交互,让应用能够处理多步骤的复杂任务。

2.2 全局设置(Settings)的妙用

在最新版本的LlamaIndex中, Settings 是一个全局配置对象,极大地简化了LLM、嵌入模型等组件的管理。如示例所示:

from llama_index.core import Settings
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding

Settings.llm = OpenAI(model=“gpt-4”, temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model=“text-embedding-3-small”)

一旦这样设置,后续创建索引、查询引擎时,如果没有显式指定,都会自动使用这些全局配置。这在开发中非常方便,尤其是在切换不同模型进行测试时,只需修改一处。

实操心得 :虽然 Settings 很方便,但在生产环境的微服务中,我建议 谨慎使用全局状态 。更好的做法是在每个应用实例或请求上下文中显式地创建和传递LLM、Embedding实例,这能避免潜在的线程安全问题,并使依赖关系更清晰。

3. 从零到一:构建你的第一个生产级RAG应用

让我们超越“Hello World”示例,构建一个更健壮、更实用的个人知识库助手。假设我们要处理一个包含多格式文档(PDF、MD、TXT)的文件夹,并提供一个命令行问答界面。

3.1 环境准备与依赖安装

首先,我们采用“核心+定制集成”的安装方式,这比安装完整的 llama-index 包更轻量,且允许我们只引入需要的组件。

# 安装核心框架
pip install llama-index-core

# 安装我们需要的集成包
# 使用OpenAI的LLM和Embedding
pip install llama-index-llms-openai
pip install llama-index-embeddings-openai
# 使用本地向量数据库Chroma(轻量,无需外部服务)
pip install llama-index-vector-stores-chroma
# 可能需要用到的文本分割器
pip install llama-index-text-splitters
# 安装ChromaDB客户端
pip install chromadb

3.2 进阶数据加载与预处理

SimpleDirectoryReader 是入门利器,但对于生产环境,我们需要更多控制。

import os
from llama_index.core import SimpleDirectoryReader
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import MetadataMode

# 1. 配置数据读取
documents = SimpleDirectoryReader(
    input_dir=“./your_data_directory”,
    required_exts=[“.pdf”, “.md”, “.txt”], # 指定文件类型
    recursive=True, # 递归读取子目录
    filename_as_id=True, # 用文件名作为文档ID的一部分
).load_data()

# 2. 高级文本分割与元数据处理
text_splitter = SentenceSplitter(
    chunk_size=1024, # 每个块的目标token数(约等于字符数/4)
    chunk_overlap=200, # 块之间的重叠token数,避免语义割裂
    separator=“ “, # 分割符
)

# 为每个文档添加自定义元数据,这对后续检索过滤很有用
for doc in documents:
    file_path = doc.metadata.get(“file_path”, “”)
    doc.metadata[“category”] = “technical_docs” # 自定义分类
    doc.metadata[“year”] = “2023”
    # 确保元数据也被用于嵌入生成(如果需要的话)
    # 默认情况下,只有文本内容被嵌入。你可以通过重写`get_content`方法改变这一点。

nodes = text_splitter.get_nodes_from_documents(documents)
print(f“将 {len(documents)} 个文档分割成了 {len(nodes)} 个节点。”)

注意事项 chunk_size 的选择是艺术也是科学。对于通用文档,512-1024是一个不错的起点。对于代码或结构化文本,可以更小。重叠( chunk_overlap )通常设置为 chunk_size 的10%-20%,它能有效防止一个完整的句子或概念被硬生生切开,在边界处保留上下文。

3.3 配置LLM与嵌入模型

我们将使用OpenAI的API,但会配置更合理的参数以平衡成本与效果。

from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core import Settings

# 配置LLM
Settings.llm = OpenAI(
    model=“gpt-3.5-turbo”, # 对于大多数RAG任务,3.5-turbo性价比极高
    temperature=0.1, # 低温度保证答案的确定性和一致性,适合事实性问答
    max_tokens=1500, # 限制单次回答长度
    timeout=60, # 设置超时,避免长时间等待
    # api_key=os.environ[“OPENAI_API_KEY”], # 最佳实践是从环境变量读取
)

# 配置嵌入模型
Settings.embed_model = OpenAIEmbedding(
    model=“text-embedding-3-small”, # 性能接近ada-002,但更便宜
    embed_batch_size=100, # 批量处理提高效率
)

# 如果你有大量数据,可以考虑使用异步客户端
# from llama_index.llms.openai import AsyncOpenAI
# Settings.llm = AsyncOpenAI(...)

3.4 构建并持久化向量索引

这里我们使用ChromaDB作为向量存储,它将数据持久化在本地。

import chromadb
from llama_index.core import StorageContext, VectorStoreIndex
from llama_index.vector_stores.chroma import ChromaVectorStore

# 1. 初始化Chroma客户端,指定持久化路径
chroma_client = chromadb.PersistentClient(path=“./chroma_db”)
# 创建一个集合(类似数据库的表)
chroma_collection = chroma_client.get_or_create_collection(“my_knowledge_base”)

# 2. 创建LlamaIndex的向量存储包装器
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

# 3. 创建存储上下文,关联向量存储
storage_context = StorageContext.from_defaults(vector_store=vector_store)

# 4. 从节点创建索引,并指定存储上下文
index = VectorStoreIndex(
    nodes=nodes,
    storage_context=storage_context,
    show_progress=True # 显示构建进度条
)

# 5. 索引构建完成后,存储上下文会自动将向量数据写入ChromaDB。
# 我们也可以显式持久化索引的元数据(非向量部分)。
index.storage_context.persist(persist_dir=“./storage”)
print(“索引构建并持久化完成。”)

3.5 实现高级查询功能

一个基础的 query_engine.query() 可能不够用。我们来实现一个支持多种检索模式、并包含后处理(重排序)的查询引擎。

from llama_index.core import VectorStoreIndex, get_response_synthesizer
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.postprocessor import SimilarityPostprocessor, LLMRerank
from llama_index.core.query_engine import RetrieverQueryEngine

# 1. 从持久化的存储中加载索引(如果是第二次运行)
# storage_context = StorageContext.from_defaults(persist_dir=“./storage”)
# index = load_index_from_storage(storage_context)

# 2. 配置检索器
retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=10, # 初步检索出10个相关节点
)

# 3. 配置响应合成器
response_synthesizer = get_response_synthesizer(
    response_mode=“compact”, # 压缩模式,会尽量将相关上下文填满LLM的上下文窗口
    structured_answer_filtering=True, # 尝试从LLM输出中提取结构化答案
)

# 4. 配置后处理器(非常重要!)
# a) 相似度过滤:过滤掉相似度分数太低的节点
similarity_postprocessor = SimilarityPostprocessor(similarity_cutoff=0.7)

# b) LLM重排序:让LLM对初步检索结果进行精排,选出最相关的几个。效果显著,但会增加延迟和成本。
llm_rerank = LLMRerank(choice_batch_size=5, top_n=3)

# 5. 组装查询引擎
query_engine = RetrieverQueryEngine(
    retriever=retriever,
    response_synthesizer=response_synthesizer,
    node_postprocessors=[similarity_postprocessor, llm_rerank] # 按顺序应用后处理器
)

# 6. 进行查询
response = query_engine.query(“LlamaIndex中如何对PDF文件进行分块?”)
print(response)
# 访问源节点
for i, source_node in enumerate(response.source_nodes):
    print(f“\n--- 来源 {i+1} (相似度分数: {source_node.score:.3f}) ---”)
    print(f“文件: {source_node.metadata.get(‘file_name’, ‘N/A’)}”)
    print(f“内容片段: {source_node.text[:500]}…”) # 打印前500字符

核心技巧 LLMRerank 是提升RAG答案准确性的“神器”。它的原理是让LLM对初步检索到的多个节点进行判断:“哪一个与问题最相关?”。虽然它引入了额外的LLM调用,但通过将 top_n 设小(如3),并将重排序问题设计得非常简单,其带来的精度提升通常远高于其成本。对于关键任务,强烈建议启用。

4. 超越基础:应对复杂场景与性能优化

当你的应用从原型走向生产,会遇到更多挑战。下面分享几个关键场景的解决方案。

4.1 处理复杂文档解析:集成LlamaParse

对于扫描版PDF、表格密集的报表、或格式混乱的文档,默认解析器可能力不从心。LlamaIndex官方提供了企业级解析平台 LlamaParse ,它基于AI代理进行OCR和文档理解,效果远超普通工具。

# 首先,需要注册LlamaCloud获取API Key: https://cloud.llamaindex.ai
import os
from llama_index.core import SimpleDirectoryReader
from llama_parse import LlamaParse

os.environ[“LLAMA_CLOUD_API_KEY”] = “your_api_key”

# 使用LlamaParse作为解析器
parser = LlamaParse(result_type=“markdown”) # 结果可以是markdown或text
documents = SimpleDirectoryReader(
    input_files=[“./complex_report.pdf”],
    file_extractor={“.pdf”: parser} # 指定.pdf文件用LlamaParse处理
).load_data()
# 得到的documents已经是高质量解析后的文本,包含表格、列表等结构信息。

4.2 实现多索引与路由

如果你的知识库包含不同类型的内容(如产品手册、技术博客、客服对话),为每种类型建立独立的索引,并让一个“路由查询引擎”根据问题决定查询哪个索引,效果会更好。

from llama_index.core import VectorStoreIndex, SummaryIndex
from llama_index.core.tools import QueryEngineTool
from llama_index.core.query_engine import RouterQueryEngine
from llama_index.core.selectors import LLMSingleSelector

# 假设我们已经构建了两个索引
product_index = VectorStoreIndex.from_documents(product_docs)
blog_index = SummaryIndex.from_documents(blog_docs) # 摘要索引适合概览性查询

# 为每个索引创建查询引擎,并包装成“工具”
product_tool = QueryEngineTool.from_defaults(
    query_engine=product_index.as_query_engine(),
    description=“适用于查询具体产品功能、规格、使用方法的文档。”
)
blog_tool = QueryEngineTool.from_defaults(
    query_engine=blog_index.as_query_engine(),
    description=“适用于查询技术概念、架构解析、最佳实践等博客文章。”
)

# 创建路由查询引擎,它会用LLM根据问题描述选择最合适的工具
router_query_engine = RouterQueryEngine(
    selector=LLMSingleSelector.from_defaults(), # 使用LLM进行选择
    query_engine_tools=[product_tool, blog_tool]
)

# 现在,问一个产品相关问题,它会自动路由到product_tool
response = router_query_engine.query(“Model X的最大续航里程是多少?”)

4.3 构建具备记忆与工具使用能力的智能体

这是将静态问答系统升级为动态助手的关键。智能体可以记住对话历史,并调用外部工具(如计算器、搜索引擎、数据库查询)。

from llama_index.core.agent import ReActAgent
from llama_index.core.tools import FunctionTool
import requests

# 1. 定义自定义工具(例如,一个获取天气的工具)
def get_weather(city: str) -> str:
    “”“获取指定城市的当前天气。Args: city (str): 城市名。”“”
    # 这里简化处理,实际应调用天气API
    weather_map = {“北京”: “晴,25°C”, “上海”: “多云,28°C”}
    return weather_map.get(city, f“未找到{city}的天气信息。”)

weather_tool = FunctionTool.from_defaults(fn=get_weather)

# 2. 将之前的查询引擎也包装成工具
qa_tool = QueryEngineTool.from_defaults(
    query_engine=query_engine, # 使用我们之前构建的RAG查询引擎
    description=“用于回答基于公司内部知识库的问题。”
)

# 3. 创建智能体,并赋予它工具和记忆
agent = ReActAgent.from_tools(
    tools=[weather_tool, qa_tool],
    llm=Settings.llm,
    verbose=True, # 打印出智能体的思考过程,便于调试
    max_iterations=10, # 限制最大推理步骤,防止死循环
)

# 4. 与智能体聊天
response = agent.chat(“北京今天天气怎么样?”) # 它会调用get_weather工具
print(response)

response = agent.chat(“根据知识库,我们项目的主要技术栈是什么?”) # 它会调用qa_tool
print(response)

response = agent.chat(“我上个问题问的是什么?”) # 它能记住对话历史!
print(response)

4.4 性能优化与成本控制

1. 嵌入缓存 为相同的文本重复计算嵌入向量是巨大的浪费。LlamaIndex支持本地缓存(如SQLite)或远程缓存(如Redis)。

from llama_index.core import Settings
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.embeddings.cache import CacheRetriever
import sqlite3

# 使用SQLite进行本地嵌入缓存
embed_model = OpenAIEmbedding()
# 创建一个缓存检索器包装原始的嵌入模型
cached_embed_model = CacheRetriever(
    embed_model,
    cache=SQLiteCache(sqlite3.connect(“./embed_cache.db”)),
)
Settings.embed_model = cached_embed_model

首次运行时,嵌入向量会被计算并存入数据库。后续遇到相同文本,直接读取缓存,能显著降低API调用成本和延迟。

2. 异步处理 对于批量文档处理或高并发查询,使用异步接口可以大幅提升吞吐量。

import asyncio
from llama_index.core.async_utils import run_async_tasks
from llama_index.core import VectorStoreIndex
from llama_index.llms.openai import AsyncOpenAI

async def async_index_documents(docs):
    Settings.llm = AsyncOpenAI(...)
    Settings.embed_model = ... # 也需要支持异步的Embedding模型
    index = await VectorStoreIndex.afrom_documents(docs) # 注意是异步方法
    return index

# 在主程序中运行
index = asyncio.run(async_index_documents(documents))

3. 成本监控 使用OpenAI等付费API时,监控token消耗至关重要。你可以在调用LLM和Embedding时,通过回调函数记录消耗。

from llama_index.core.callbacks import CallbackManager, TokenCountingHandler
import tiktoken

token_counter = TokenCountingHandler(
    tokenizer=tiktoken.encoding_for_model(“gpt-3.5-turbo”).encode
)

Settings.callback_manager = CallbackManager([token_counter])

# ... 执行查询操作后 ...
print(f“本次查询消耗的Prompt Tokens: {token_counter.prompt_llm_token_count}”)
print(f“本次查询消耗的Completion Tokens: {token_counter.completion_llm_token_count}”)
print(f“本次查询消耗的Embedding Tokens: {token_counter.total_embedding_token_count}”)
token_counter.reset_counts() # 重置计数器

5. 常见问题排查与实战避坑指南

在实际开发中,你一定会遇到各种问题。下面是我踩过坑后总结出的经验。

5.1 检索效果不佳

症状 :LLM的回答与问题无关,或未能从提供的上下文中找到正确答案。

  • 可能原因1:分块策略不当
    • 排查 :检查检索到的源节点文本。它们是否完整包含了答案?还是答案被切分到了两个块里?
    • 解决 :调整 chunk_size chunk_overlap 。尝试按句子、段落或固定标记数分割。对于技术文档,按章节标题分割可能更有效。
  • 可能原因2:嵌入模型不匹配
    • 排查 :用于构建索引的嵌入模型和当前查询时使用的嵌入模型是否一致?如果不一致,向量空间将对不上。
    • 解决 :确保 Settings.embed_model 在索引构建和查询时是同一个实例或相同配置。将嵌入模型名称和参数记录在配置文件中。
  • 可能原因3:检索到的节点数量不足或过多
    • 排查 :检查 similarity_top_k 的值。太小可能漏掉相关文档,太大可能引入噪声。
    • 解决 :尝试不同的k值(如5, 10, 20),并结合 SimilarityPostprocessor 设置一个相似度阈值(如0.75)进行过滤。 启用 LLMRerank 是提升精度的最有效手段之一
  • 可能原因4:查询问题表述不清
    • 解决 :实现“查询重写”(Query Rewriting)。在检索前,先用一个简单的LLM调用将用户的口语化问题改写成更贴近文档表述的关键词组合。例如,将“咋装这个软件?”重写为“安装步骤 配置要求”。

5.2 回答出现“幻觉”(Hallucination)

症状 :LLM的答案听起来合理,但细看发现是编造的,上下文中并无依据。

  • 根本原因 :LLM的生成能力太强,当检索到的上下文不相关或信息不足时,它会倾向于“脑补”。
  • 解决
    1. 强化提示工程 :在发送给LLM的提示词中,明确指令“ 严格基于提供的上下文信息回答问题。如果上下文没有提供足够信息,请直接说‘根据已知信息无法回答该问题’,不要编造信息。
    2. 引用溯源 :要求LLM在答案中引用来源节点的ID或片段。这不仅能验证答案,也增加了可信度。可以通过定制 response_synthesizer 的提示模板实现。
    3. 设置置信度阈值 :如果所有检索到的节点相似度分数都低于某个阈值(如0.6),则直接返回“未找到相关信息”,不调用LLM。

5.3 处理速度慢

症状 :索引构建或查询响应时间过长。

  • 索引构建慢
    • 批量处理嵌入 :确保 embed_batch_size 设置合理(OpenAI建议最大2048)。对于数万文档,考虑使用异步并行处理。
    • 使用本地嵌入模型 :对于对延迟敏感或数据敏感的场景,可以换用本地部署的嵌入模型,如 HuggingFaceEmbedding (选用 BAAI/bge-small-zh-v1.5 等轻量级模型)。
  • 查询响应慢
    • 向量数据库优化 :如果使用本地Chroma,确保有足够内存。对于超大规模数据(百万级以上),考虑使用专业的向量数据库如Pinecone、Weaviate或Milvus,它们支持索引优化和近似最近邻搜索。
    • 精简上下文 :使用 response_mode=“compact” “tree_summarize” ,它们会尝试压缩上下文,减少发送给LLM的token数,从而降低生成时间。
    • 缓存查询结果 :对于常见、重复的问题,可以在应用层实现查询结果的缓存(如使用 functools.lru_cache )。

5.4 部署与运维问题

  • 版本兼容性 :LlamaIndex更新较快,注意核心包( llama-index-core )与各集成包( llama-index-llms-openai 等)的版本兼容性。建议在 requirements.txt 中锁定主要版本号。
  • 环境变量管理 :API密钥等敏感信息切勿硬编码在代码中。使用 .env 文件配合 python-dotenv ,或使用容器/云服务的秘密管理功能。
  • 持久化与恢复 :确保向量数据库(如Chroma的 persist_directory )和LlamaIndex的元数据存储路径( persist_dir )在容器化部署时被挂载到持久化卷上,否则重启后数据会丢失。
  • 监控与日志 :为关键操作(数据加载、索引构建、查询)添加详细的日志记录。监控LLM API的调用成功率、延迟和token消耗,设置告警。

5.5 一个真实的调试案例:表格数据检索失败

我曾遇到一个需求:从包含大量数据表格的PDF年报中查询具体财务数字。使用默认流程,检索到的文本片段总是丢失表格结构,导致LLM无法正确解读。

排查过程

  1. 首先检查原始解析文本,发现默认PDF解析器将表格转换成了杂乱无章的行,失去了行列关系。
  2. 尝试换用 LlamaParse ,并指定 result_type=“markdown” 。解析后,表格被很好地转换成了Markdown表格语法。
  3. 但检索时发现,向量搜索基于语义相似度,而像“2023年Q4净利润”这样的查询,其语义与表格中具体的数字单元格(如“1.23亿”)并不直接相似。
  4. 解决方案 :采用 混合检索 策略。
    • 路径A(语义检索) :对Markdown文本(包含表格描述性文字)建立向量索引。
    • 路径B(关键词检索) :同时,使用 KeywordTableIndex BM25Retriever 对纯文本(包含数字和特定术语)建立关键词索引。
    • 在查询时,同时使用两种检索器获取结果,然后通过 ReciprocalRerankFusion 或自定义逻辑对结果进行融合重排。这样,既保证了语义理解,又不会错过关键的数字和术语。

最终,通过结合更强大的解析工具(LlamaParse)和混合检索策略,成功解决了表格数据查询的难题。这个案例告诉我们,面对复杂的数据格式和查询需求,往往需要组合多种工具和索引策略,没有一成不变的银弹。

更多推荐