1. 文档加载

langchian Document

  • page_content: 文档内容
  • metadata: 文档元信息
1
2
3
4
5
6
from langchain.schema import Document

document = Document(
page_content="Hello, world!",
metadata={"source": "https://example.com"}
)

html

  • 在线网页:from langchain_community.document_loaders import WebBaseLoader

  • 本地文件:from langchain_community.document_loaders import BSHTMLLoader

  • 解析代码:from bs4 import BeautifulSoup

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from bs4 import BeautifulSoup

    # 读取 HTML 文件内容
    html_txt = ''
    with open("./file_load/test.html", 'r') as f:
    for line in f.readlines():
    html_txt += line

    # 解析 HTML
    soup = BeautifulSoup(html_txt, 'lxml')

    # 代码块 td class="code"
    code_content = soup.find_all('td', class_="code")
    for ele in code_content:
    print(ele.text)
    print("+"*100)

    这里对代码块解析时的 class 需要根据具体网页的元素定义进行更换,不过大体思路都一样(也不局限于代码块)。

PDF

  • 加载文件:from langchain_community.document_loaders import PyMuPDFLoader

  • 解析表格:import fitz

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import fitz

    doc = fitz.open("./file_load/fixtures/zhidu_travel.pdf")

    table_data = []
    text_data = []

    doc_tables = []
    for idx, page in enumerate(doc):
    text = page.get_text()
    text_data.append(text)
    tabs = page.find_tables()
    for i, tab in enumerate(tabs):
    ds = tab.to_pandas()
    table_data.append(ds.to_markdown())

    for tab in table_data:
    print(tab)
    print("="*100)

Unstructured

Unstructured 是由 Unstructured.IO 开发的开源 Python 库,专为处理非结构化数据(如 PDF、Word、HTML、XML 等)设计。在 LangChain 中,它作为文档加载的核心工具,实现以下功能:

  1. 格式支持广泛:解析 PDF、DOCX、PPTX、HTML、XML、CSV 等格式,甚至支持扫描件中的 OCR 文本提取。
  2. 元素分区(Partitioning):将文档拆分为结构化元素(标题、段落、表格、列表),保留原始布局和元数据。
  3. 数据清洗:自动清理文档中的无关符号、页眉页脚,生成纯净文本。

使用 langchain_unstructured 需要安装:

1
2
3
4
5
6
7
8
9
10
11
uv add unstructured
uv add langchain_unstructured
uv add unstructured_inference
uv add unstructured_pytesseract

# 系统依赖(macOS)
brew install poppler
brew install tesseract
brew install libmagic
brew install ghostscript
brew install pandoc

PDF

需要额外安装:

1
2
3
uv remove camelot-py # 如果有 camelot 需要先移出,在一些版本上存在冲突

uv add "unstructured[pdf]"

使用时导入包:

1
from langchain_unstructured import UnstructuredLoader

PPT

需要安装额外依赖:

1
uv add python-pptx

使用时导入包:

1
from langchain_community.document_loaders import UnstructuredPowerPointLoader

解析 PPT 中的表格及其他特殊类型,可以使用原始的 python-pptx 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE

ppt = Presentation("./file_load/fixtures/test_ppt.pptx")

for slide_number, slide in enumerate(ppt.slides, start=1):
print(f"Slide {slide_number}:")
for shape in slide.shapes:
if shape.has_text_frame: # 文本信息
print(shape.text)

if shape.has_table: # 表格信息
table = shape.table
for row_idx, row in enumerate(table.rows):
for col_idx, cell in enumerate(row.cells):
cell_text = cell.text
print(f"Row {row_idx + 1}, Column {col_idx + 1}: {cell_text}")

if shape.shape_type == MSO_SHAPE_TYPE.PICTURE: # 图片信息
imgae = shape.image
image_filename = "./file_load/fixtures/pic_from_ppt.jpg"
with open(image_filename, 'wb') as f:
f.write(imgae.blob)

Word

需要安装额外依赖:

1
2
uv add docx2txt
uv add python-docx

使用时导入包:

1
from langchain_community.document_loaders import Docx2txtLoader

解析 Word 中的表格及其他特殊类型,可以使用原始的 python-docx 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from docx import Document

def read_docx(file_path):
doc = Document(file_path)
for para in doc.paragraphs:
print(para.text)

for table in doc.tables:
for row in table.rows:
for cell in row.cells:
print(cell.text, end=' | ')
print()

file_path = "./file_load/fixtures/test_word.docx"
read_docx(file_path=file_path)

Excel

需要安装额外依赖:

1
uv add openpyxl

ragflow.deepdoc

RAGFlow 是一个开源的、基于"深度文档理解"的 RAG 引擎。

RAGFlow 的主要特点:

  1. 开箱即用: 提供 Web UI 界面,用户可以通过简单的几次点击,无需编写代码,就能完成知识库的建立和问答测试。通过 Docker 可以一键部署,非常方便。
  2. 工作流自动化 (Automated Workflow): RAGFlow 将复杂的 RAG 流程(文档解析、切块、向量化、存储、检索、生成)模板化。用户可以选择不同的模板来适应不同的数据和任务需求,整个过程高度自动化。
  3. 可视化与可解释性: 在处理文档时,RAGFlow 会生成一个可视化的解析结果图,让用户能清晰地看到文档是如何被理解和切分的,大大增强了系统的透明度和可调试性。
  4. 企业级特性: 它支持多种文档格式,能够生成可溯源的答案(即答案会附上来源出处),并且兼容多种 LLM 和向量数据库,易于集成到现有企业环境中。

如果说 RAGFlow 是一个高效的问答“工厂”,那么 DeepDoc 就是这个工厂里最核心、最先进的“原材料加工车间”。所有外部文档在进入知识库之前,都必须经过 DeepDoc 的精细处理。

DeepDoc 的全称是 Deep Document Understanding(深度文档理解),它是 RAGFlow 实现高质量检索的基石。它并非简单地提取文本,而是试图像人一样“看”和“理解”文档的版面布局和内在逻辑。

DeepDoc 的工作原理与核心能力:

  1. 视觉版面分析 (Vision-based Layout Analysis):
    • 理论: DeepDoc 首先会利用计算机视觉(CV)模型,像人眼一样扫描整个文档页面。它不是逐行读取字符,而是先识别出页面上的宏观结构,例如:这是标题、那是段落、这是一个表格、这是一张图片、这是一个页眉/页脚。
    • 实践: 对于一个两栏布局的 PDF 报告,传统的文本提取工具可能会把左边一行的结尾和右边一行的开头错误地拼在一起。而 DeepDoc 的视觉分析能准确识别出两个独立的栏目,并按照正确的阅读顺序(先读完左栏,再读右栏)来处理文本。
  2. 智能分块 (Intelligent Chunking):
    • 理论: 这是 DeepDoc 最具价值的一点。在理解了文档布局之后,它会进行“语义分块”而非“物理分块”。传统的 RAG 会把文档切成固定长度(如 500 个字符)的块,这常常会将一个完整的表格或一段逻辑连贯的话拦腰截断。
    • 实践: DeepDoc 会将一个完整的表格识别出来并视为一个独立的“块”(Chunk)。一个标题和它紧随其后的段落也会被智能地划分在一起。这样做的好处是,当用户提问与表格相关的问题时,系统检索到的就是这个包含完整上下文的表格块,而不是表格的某几行碎片。这极大地保证了提供给 LLM 的上下文信息的完整性和逻辑性。
  3. 高质量光学字符识别 (OCR):
    • 理论: 对于扫描的 PDF 文件或者文档中嵌入的图片,DeepDoc 内置了高质量的 OCR 引擎。
    • 实践: 即便文档是扫描的复印件,它也能尽可能准确地提取出其中的文字内容,并将其融入到上述的版面分析中,确保信息不丢失。
  4. 表格解析与转译:
    • 理论: 识别出表格只是第一步,更关键的是让 LLM 能“读懂”表格。
    • 实践: DeepDoc 能够提取出表格的结构化数据,并将其转换为 LLM 更容易理解的格式,例如 Markdown 格式。一个复杂的表格图片,在经过 DeepDoc 处理后,可能会变成一个 Markdown 文本表格,这样 LLM 就能轻松地理解其行列关系,并回答诸如“请总结一下表格中第三季度销售额最高的产品是哪个?”这类的问题。

笔者在实践过程中,通过精简 ragflow.deepdoc 中的 pdfparser,抽出了一个组件 deepdoc_pdfparser.

2. 分块策略

RAG 五大分块策略

参考:5-chunking-strategies-for-rag

3. 向量嵌入

3.1 嵌入模型评测

Hugging Face 的 MTEB (Massive Text Embedding Benchmark) 是一个大规模的文本嵌入模型评测基准。它的核心作用是为各种文本嵌入模型提供一个统一、全面、客观的性能衡量标准

涵盖了文本嵌入在现实世界中最常见的 8 种应用场景,共计 58 个数据集和 112 种语言。这 8 大任务分别是:

  • Bitext Mining (双语文本挖掘): 在不同语言的句子中找出翻译对。
  • Classification (分类): 将文本划分到预定义的类别中。
  • Clustering (聚类): 将相似的文本分组在一起。
  • Pair Classification (句子对分类): 判断两个句子是否具有某种关系 (如释义、矛盾等)。
  • Reranking (重排序): 对一个已经排好序的列表 (如搜索结果) 进行重新排序,以提升质量。
  • Retrieval (检索): 从一个大规模的文档语料库中找出与查询最相关的文档。这是目前文本嵌入最核心和最热门的应用之一。
  • Semantic Textual Similarity (STS, 语义文本相似度): 判断两个句子的语义相似程度,通常给出一个从 0 到 5 的分数。
  • Summarization (摘要): 评估生成的摘要与原文的语义相似度。
MTEB

3.2 稀疏嵌入(Sparse Embedding)

特征 说明
维度 通常等于完整词表或特征集合的大小,可达 10⁵ – 10⁶;大多数维度为 0,只有少数位置有权重
构造方式 基于词频或词频-逆文档频率(TF-IDF)、BM25 等统计方法,不依赖深度学习
权重含义 每个非零维可直观解释为某个词或特征的重要度,具有高度可解释性
检索/存储 用倒排索引即可实现 O(1) 级精确匹配;在线增量更新代价低
优势 对长文档、术语精确匹配友好 易于调参(停用词、词根化) 资源消耗小、无推理延迟
劣势 维度极高,逐向量暴力计算代价大 只捕获词面共现,无法理解语义或同义词 对拼写/语序变化鲁棒性差

TF-IDF(Term Frequency - Inverse Document Frequency,词频-逆文档频率)

一种经典的加权方案,用来衡量 词语 t 对 文档 d 在 语料库 D 中的重要程度。

  • 一句话:词在整个语料库中出现得越少,但在本篇文档中出现得越多,那它就越重要。

公式:\(TF-IDF(t,d,D) = TF(t,d) × IDF(t,D)\)

TF(局部权重):

  • 计数:tf = #t 出现次数
  • 频率:tf = #t / |d|
  • 对数平滑:tf = 1 + log(#t)

ID(全局权重):\[IDF(t) = log\frac{N-df(t)+0.5}{df(t)+0.5} \]

  • N = 语料中文档总数
  • df(t) = 含词 t 的文档数
  • 加 1 或 0.5 可以避免分母为 0,并抑制长尾噪声。

BM25(Best Matching 25)

可视为 TF-IDF 的扩展版,进一步引入:

  • k₁ 控制 TF 饱和:TF 越大,增益递减。
  • b 长度归一化:文档越长,单词 TF 权重被抑制。

公式:\[w(t,d)=IDF(t)⋅\frac{TF(k_{1} +1)}{TF+k_{1}·(1-b+b·\frac{文档长度}{平均文档长度} )} \]

角色 控制对象 常见区间 极值行为 直觉比喻
k₁ (saturation factor) TF 饱和曲线斜率——同一个词在同一文档中重复出现到第 n 次时,还能再加多少分 1.0 – 2.0 k₁ → 0:完全不计重复词;k₁ → ∞:线性计数,退化为 TF-IDF 沾一滴酱油 vs. 倒一瓶酱油:味道总有极限,不会永远 1 → 2 → 3 倍变浓
b (length normalizer) 文档长度惩罚强度——长文能否用“大块头”刷分 0.3 – 0.9 b = 0:不考虑长度(BM15)b = 1:长度全量归一化(BM11) 打篮球按身高加分:b=0 不管身高;b=1 按身高严格扣分;中间值折中

3.3 密集嵌入(Dense Embedding)

特征 说明
维度 兼顾效率与表达力,常见 128 – 1536;每一维几乎都非零。
构造方式 由深度模型(BERT、Sentence-BERT、OpenAI text-embedding-3-small 等)端对端学习,捕获上下文语义
权重含义 单维难以直观解释,但整体向量在低维空间中编码了丰富的语义相似度
检索/存储 需专门的 ANN(HNSW、Faiss IVF-PQ 等)索引;向量更新需重新编码
优势 具备语义泛化能力,能跨同义词、拼写、语序可跨语言、跨模态(图文)在 RAG/问答场景提升召回率
劣势 训练与推理成本高(GPU/CPU 向量化计算)结果可解释性弱 在线增量写入需再编码、重建索引

3.4 ColBERT

ColBERT 是一种让 BERT 用“词级小向量”做快速、精准文本检索的方法 —— 既不像传统 TF-IDF 那样粗糙,也不像跨编码器那样慢。

ColBERT = “把 BERT 的句向量拆成 token 向量,再用 Late Interaction 重新拼起来做检索”的工程化改造版 BERT。

Late Interaction 就是把查询(Q)和文档(D)先独立编码,等到最后打分时再让它们在 token 级别 做一次“小范围、轻量级”的互动——既不像 Cross-Encoder 那样“一上来就深度交互”,也不像 Bi-Encoder 那样“全程零交互”。

换言之,BERT 提供语言理解底座,ColBERT 在此之上加了面向检索的输出格式与打分逻辑,二者既同宗又分工明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
           预训练阶段(同一个 BERT 权重)

┌───────────────┐
│ Google BERT │ ← 海量文本上做 MLM/NSP
└───────────────┘
│ (加载相同参数)
╭───────────┴───────────╮
│ │
│ ↓ 普通微调 │ ↓ ColBERT 微调
│ (分类、NER…) │ (稠密检索)
│ │
│ 取 [CLS] 整句向量 │ 保留 每个 token 向量
│ + 任务特定头 │ + Late-Interaction 打分
│ │
╰───────────┬───────────╯

下游推理/检索

3.5 BGE-M3

BGE-M3 是由智源研究院(BAAI)开发的新一代旗舰文本嵌入模型,它开创性地在单一模型内集成了多语言(支持超过 100 种语言)、长文本 (支持 8192 词符)和多功能检索(同时支持稠密、稀疏和多向量检索)的强大能力。

M 含义 具体能力 参考
Multi-Functionality 多功能 同时产出 稠密向量(dense)、多向量/ColBERT(colbert) 和 稀疏向量(sparse),一套模型即可覆盖混合检索需求。 huggingface.bge-m3
Multi-Linguality 多语种 覆盖 100+ 语言,是目前公开数据集中多语检索任务的 SOTA。 arXiv.bge-m3
Multi-Granularity 多粒度 最长输入 8 192 token,既能编码短句也能处理长文档。 huggingface.bge-m3

4. 查询增强技术

4.1 查询构建

4.1.1 Text-to-SQL

Text-to-SQL
  1. 构建 DDL 知识库:schema 提取与切片;
  2. 构建 Q-SQL 知识库:示例对注入;
  3. 构建 DB 描述知识库:业务描述补充;
  4. 提供 RAG 检索上下文;
  5. 调用 LLM 进行 SQL 生成;
  6. 执行 SQL 并反馈结果;
  7. 迭代直到正确解决问题。

常用框架:

  • vanna
  • Chat2DB
  • DB-GPT

4.1.2 Text-to-Cypher

跟 Text-to-SQL 一样,只不过是生成图数据库(neo4j)查询语句。

Text-to-Cypher
  1. 构建图元模型(Graph Metamodel)知识库;
  2. 构建 Q-Cypher 知识库(示例对注入);
  3. 构建图描述(Graph Description)知识库;
  4. 提供 RAG 检索上下文;
  5. 调用 LLM 进行 SQL 生成;
  6. 执行 SQL 并反馈结果;
  7. 迭代直到正确解决问题。

4.1.3 从查询中提取元数据构建过滤器

从查询中提取元数据构建过滤器
  1. 将自然语言转为向量查询语句;
  2. 利用 LLM 推断出元数据过滤条件;
  3. 在查询检索时,根据过滤条件进行文档过滤;
  4. 返回过滤后的相似文档;

实战案例:

  • https://ragflow.io/blog/implementing-text2sql-with-ragflow
  • https://medium.com/neo4j/generating-cypher-queries-with-chatgpt-4-on-any-graph-schema-a57d7082a7e7

4.2 查询翻译

通过对用户查询进行改造和扩展,使其更加清晰、具体,从而提高检索精度。

常用工具:

方案 链接 说明
ragbear GitHub - lexiforest/ragbear rewrite= 参数多种改写模式
LangChain Query Transformations 内置链式改写
LlamaIndex Query Transform Cookbook ¶ 多策略组合
Haystack Advanced RAG: Automated Structured Metadata Enrichment | Haystack pipeline node

4.2.1 Query2Doc

Query2Doc 是指将 query 直接交给 LLM 去生成一份相关文档,然后将 query 和生成的文档一起去进行检索。虽然 LLM 生成的文档可能不对,但是提供了更丰富的信息、丰富了问题的语义,有助于提高检索时的精度。

1
2
3
4
5
def query2doc(query):
prompt = f"你是一名公司员工制度的问答助手,熟悉公司规章制度,请简短回答以下问题:{query}"
doc_info = llm(prompt)
context_query = f"{query}, {doc_info}"
return context_query

4.2.2 HyDE

HyDE(Hypothetical Document Embeddings,假设文档向量)让 LLM 根据 query 去生成一系列假设性文档,然后将这些文档跟 query 一起做向量化,取向量均值去进行检索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def hyde(query, include_query=True):
prompt_template = """你是一名公司员工制度的问答助手,熟悉公司规章制度,请简短回答以下问题:
Question: {question}
Answer:"""

prompt = PromptTemplate(input_variables=["question"], template=prompt_template)
embeddings = HypotheticalDocumentEmbedder(llm_chain= prompt | llm,
base_embeddings=embedding_model.get_embedding_fun())
hyde_embedding = embeddings.embed_query(query)

if include_query:
query_embeddings = embedding_model.get_embedding_fun().embed_query(query)
result = (np.array(query_embeddings) + np.array(hyde_embedding)) / 2
result = list(result)
else:
result = hyde_embedding
result = list(map(float, result))
return result

4.2.3 子问题查询

当问题比较复杂时,可以利用 LLM 将问题拆解成子问题,每个子问题都生成检索上下文,可以根据合并后总的上下文回答,也可以每个上下文独立回答后汇总。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def sun_question(query):
prompt_template = """你是一名公司员工制度的问答助手,熟悉公司规章制度。
你的任务是对复杂问题继续拆解,以便理解员工的意图。
请根据以下问题创建一个子问题列表:

复杂问题:{question}

请执行以下步骤:
1. 识别主要问题:找出问题中的核心概念或主题。
2. 分解成子问题:将主要问题分解成可以独立理解和解决的多个子问题。
3. 只返回子问题列表,不包含其他解释信息,格式为:
1. 子问题1
2. 子问题2
3. 子问题3
...

"""

prompt = PromptTemplate(input_variables=["question"], template=prompt_template)

llm_chain = prompt | llm
sub_queries = llm_chain.invoke(query).split('\n')
return sub_queries

4.2.4 查询改写

当问题表达不清、措辞差、缺少关键信息时,使用 LLM 根据用户问题多角度重写问题,增加额外的信息,提高检索质量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def question_rewrite(query):
prompt_template = """你是一名公司员工制度的问答助手,熟悉公司规章制度。
你的任务是需要为给定的问题,从不同层次生成这个问题的转述版本,使其更易于检索,转述的版本增加一些公司规章制度的关键词。
问题:{question}
请直接给出转述后的问题列表,不包含其他解释信息,格式为:
1. 转述问题1
2. 转述问题2
3. 转述问题3
..."""

prompt = PromptTemplate(input_variables=["question"], template=prompt_template)

llmchain = prompt | llm
rewrote_question = llmchain.invoke(query)
return rewrote_question

4.2.5 查询抽象

查询抽象(Take a Step Back)是指当问题包含太多的细节,可能导致检索时忽略了关键的信息,降低检索质量。可以将用户的具体问题转化为一个更高层次的抽象问题,一个更广泛的问题,关注于高级概念或原则,从而提高检索质量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts.chat import ChatPromptTemplate
from langchain_core.prompts.few_shot import FewShotChatMessagePromptTemplate

# 将复杂问题抽象化,使其更聚焦在本质问题上
def take_step_back(query):
examples = [
{
"input": "我祖父去世了,我要回去几天",
"output": "公司丧葬假有什么规定?",
},
{
"input": "我去北京出差,北京的消费高,有什么额外的补助?",
"output": "员工出差的交通费、住宿费、伙食补助费的规定是什么?"
},
]

example_prompt = ChatPromptTemplate.from_messages(
[
("human", "{input}"),
("ai", "{output}"),
]
)

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

prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""你是一名公司员工制度的问答助手,熟悉公司规章制度。
你的任务是将输入的问题通过归纳、提炼,转换为关于公司规章制度制定相关的一般性问题,使得问题更容易捕捉问题的意图。
请参考下面的例子,按照同样的风格直接返回一个转述后的问题:"""
),
# few shot exmaples,
few_shot_prompt,
# new question
("user", "{question}")
]
)

question_gen = prompt | llm | StrOutputParser()
res = question_gen.invoke({"question": query}).removeprefix("AI: ")
return res

4.3 查询路由

查询路由(Query Routing)是指根据用户问题的具体意图,自动判断应该将该问题导向最合适的数据源(例如向量知识库、SQL 数据库、图数据库或特定 API)以获取最精准信息的决策过程。

思路 图示
逻辑路由 逻辑路由
语义路由 语义路由

5. 索引优化技术

6. 检索后优化技术

7. 响应生成

8. 系统性优化

9. 评估

10. Graph RAG

11. ReAct RAG

12. RAG 相关思考