LangChain v1.3.4 笔记 - 06 RAG 检索增强生成

对应 LangChain 中的 Retrieval 内容

虽然前面通过 store | checkpoint 保证了模型具备上下文的能力。但仍然存在 消息滞后、知识缺失 的问题,因为模型的训练数据都是公开数据并且时效性不是当前;从而导致在回答问题时出现 幻觉

RAG 就是将模型触及不到知识作为模型的知识库每次回答问题时去检索出来,然后再回答给用户,从而在某些特定领域让模型的回答更加靠谱。

什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成)

  • 解析语料内容,将非结构化的数据转换为结构化数据 Loader
  • 通过语义或者字符等手段对结构化数据进行拆分 Chunk
  • 通过向量模型,将拆分后的数据解析转换成向量存入数据库中 Embedding
  • 用户的输入也转为向量,做数据的相似度检索,将检索出来的数据作为上下文结合用户的输入提交给模型 Retrieval
  • 模型根据上下文生成回答内容 Generation

最典型的莫过于客服系统,包括飞书开放文档的智能助手等

RAG 优点就是提高问答的可靠性和业务数据的私密性;缺点就是上下文会比较长时间比较慢,因为每次都会查都会查库,并且携带的上下午比较多

Document Loader 文档加载器

实际业务中文档的形式五花八门,LangChain 也提供了多种加载方案 加载器

需要提前安装 uv add langchain-community

py 复制代码
from langchain_community.document_loaders import (
  TextLoader, # 文本
  CSVLoader, # csv
  PyPDFLoader, # pdf
  JSONLoader, # json
  UnstructuredWordDocumentLoader, # word
  UnstructuredMarkdownLoader, # markdown
  UnstructuredHTMLLoader, # html
  DirectoryLoader, # 配合 PythonLoader 处理目录
  PythonLoader
)

所有的 Loader 都继承至 BaseLoader 都实现了 load | lazy_load 方法加载文档最终返回一个 list[Docutment] 对象包含 metadata | page_content 属性。

TextLoader

py 复制代码
from langchain_community import TextLoader

loader = TextLoader(file_path=f"{assets_dir}/01.txt", encoding="utf-8")
rprint(loader.load())
# [
#     Document(
#         metadata={'source': '/Users/a1234/Documents/at-guigu-ai/assets/01.txt'},
#         page_content='Ant Design 设计规范和技术上支持灵活的样式定制,以满足业务和品牌上多样化的视觉需求,包括但不限于全局样式(主色、圆角、边框)和指定组件的视觉定制。\n\n自从 5.0版本以来,我们提供了一套全新的定制主题方案。不同于 4.x 版本的 less 和 CSS 变量,有了 CSS-in-JS 的加持后,动态主题的能力也得到了加强,包括但不限于:\n\n1. 支持动态切换主题;\n2. 支持同时存在多个主题;\n3. 支持针对某个/某些组件修改主题变量;\n4. ...'
#     )
# ]

CSVLoader

每列数据作为一个 Document 输出

py 复制代码
from langchain_community import CSVLoader

loader = CSVLoader(file_path=f"{assets_dir}/01.csv")
rprint(loader.load())
# [
#     Document(metadata={'source': '.../assets/01.csv', 'row': 0}, page_content='标题1: 1行1列\n标题2: 1行2列'),
#     Document(metadata={'source': '.../assets/01.csv', 'row': 1}, page_content='标题1: 2行1列\n标题2: 2行2列'),
#     Document(metadata={'source': '.../assets/01.csv', 'row': 2}, page_content='标题1: 3行1列\n标题2: 3行2列'),
#     Document(metadata={'source': '.../assets/01.csv', 'row': 3}, page_content='标题1: 4行1列\n标题2: 4行2列')
# ]

JSONLoader

接收一个 json_schema 参数加载数据中指定数据,通过 jq 实现

py 复制代码
from langchain_community import JSONLoader

loader = JSONLoader(
  file_path=f"{assets_dir}/01.json", 
  jq_schema=".",  # . 表示提取所有
  text_content=False # 是否提取字符串默认 True, 只有提取某个字段时才不需要显示指定
)
rprint(loader.load())

# .data 表示提取根对象下的 data 属性引用类型或者值类型
# .list[] 表示提取根对象下的 list 列表包含内容的所有属性
# .list[].content 表示提取 list 列表中的所有 content 属性

PyPDFLoader

依赖 pypdf 实现,常用参数有

  • mode: single 或 page 决定加载出来文档的个数默认 page
  • extraction_mode: plain 提取文本也是默认值; layout 布局感知会插入大量的空格
  • password 加密 pdf 的密码
py 复制代码
from langchain_community import PyPDFLoader

loader = PyPDFLoader(f"{assets_dir}/xxx.pdf", mode="single")
rprint(loader.load())

UnstructuredWordDocumentLoader

提取非结构化数据依赖 unstructured 常用参数有

  • mode: single 或 elements,决定按标题加载还是整个一块且,默认 single
  • strategy: fast 快速模型不纠结布局,hi_res 高速模型

还需要安装 python-docx

如果运行仍然报错,参考信息继续安装内容,总结处理起来很麻烦

bash 复制代码
FileNotFoundError: soffice command was not found. Please install libreoffice on your system and try again.
- Install instructions: https://www.libreoffice.org/get-help/install-howto/
- Mac: https://formulae.brew.sh/cask/libreoffice
- Debian: https://wiki.debian.org/LibreOffice
py 复制代码
from langchain_community import UnstructuredWordDocumentLoader

loader = UnstructuredWordDocumentLoader(
  f"{assets_dir}/01.doc", 
  mode="elements", 
  strategy="fast"
)
rprint(loader.load())

UnstructuredMarkdownLoader

依赖 markdown

py 复制代码
from langchain_community import UnstructuredMarkdownLoader

loader = UnstructuredMarkdownLoader(
  f"{assets_dir}/01.md",
  mode="single" # elements 标题
)
rprint(loader.load())

UnstructuredHTMLLoader

py 复制代码
from langchain_community import UnstructuredHTMLLoader

loader = UnstructuredHTMLLoader(
  f"{assets_dir}/01.html",
  mode="single" # elements | paged 拥有更丰富的 metadata 包含 filename type 等
)
rprint(loader.load())

DirectoryLoader、PythonLoader

py 复制代码
from langchain_community import DirectoryLoader, PythonLoader

loader = DirectoryLoader(
  f"{assets_dir}",
  glob="*", # 提取的正则表达式
  use_multithreading=True, # 是否开启多线程
  show_progress=True, # 是否在控制台显示进度
  loader_cls=PythonLoader # 指定加载器, PythonLoader 本质就是读文件可以切换其他
)
rprint(loader.load())

Text Splitter 文本切割

太大的 Document 会影响模型的判断增加不必要的消耗,根据策略将语料进一步拆分为一个个的 Chunk 可以减少内容噪音和减少 token 消耗。

  • 根据句子 按照自然语句的边界切分,保证语义的完整性
  • 固定字符数 可能会把语义给搞坏掉
  • 固定字符数 + 重叠窗口 切分的前后各多保留一些内容,避免切分关键内容,确保信息的连贯性
  • 递归字符 通过递归字符的方式,动态确认切分点,可以根据文档的复杂度和信息密度调整 chunk 的大小
  • 根据语义内容 保证相关内容的语义和完整性,不过需要额外引入一个模型去切分

4、5切分方案比较推荐,5 不能保证 chunk 的大小,切效率不高。 切分方案在线测试

LangChain 切分方案实例,切分类都基于 TextSplitter 实现,需要额外安装 uv add langchain-text-splitters

常用参数

  • chunk_size 默认 4000,切分的大小
  • chunk_overlap 默认 200,chunk 之间的重叠的字符数
  • length_function 默认 len 函数 测量给定块长度的函数
  • keep_separator 默认 false,可选 bool | 'start' | 'end' 是否保留分隔符及应该放在哪个位置
  • add_start_index 默认 false 是否在元数据中包含索引
  • strip_whitespace 默认 true 是否去除开头和结尾的空格

常用方法

py 复制代码
def split_text(self, text: str) -> list[str]:

def create_documents(
    self, texts: list[str], metadatas: list[dict[Any, Any]] | None = None
) -> list[Document]:

def split_documents(self, documents: Iterable[Document]) -> list[Document]:

# 实际上内部的实现,也是自己在调用
# split_documents -> create_documents -> split_text

CharacterTextSplitter 字符切分

separator 存在优先原则,当指定分隔符时,会优先考虑分隔符切割其实才是 chunk_size 避免句子中间硬性切断,chunk_size 过小会导致 chunk_overlap 失效

额外参数 separator 分隔符,默认 \n\n

py 复制代码
from langchain_text_splitters import CharacterTextSplitter

loader = TextLoader(file_path=f"{assets_dir}/01.txt", encoding="utf-8")
splitter = CharacterTextSplitter(
  chunk_size=50, # 50 字符切割
  chunk_overlap=5, # chunk 之间的重复字符数
  separator="" # 空表示不使用分隔符
)

texts = splitter.split_text(loader.load()[0].page_content)
rprint(texts)
# [
#     'Ant Design 设计规范和技术上支',
#     '和技术上支持灵活的样式定制,以满足业务和',
#     '满足业务和品牌上多样化的视觉需求,包括但',
#     '求,包括但不限于全局样式(主色、圆角、边',
#     '、圆角、边框)和指定组件的视觉定制。',
# ]

RecursiveCharacterTextSplitter 递归字符切分

遵循先拆分后合并的方案

拆分 先按照分隔符列表的顺序,从第一个分隔符开始切分,切分后如果大于 chunk_size 就继续按照第二个分隔符继续拆分,直到所有内容都小于 chunk_size

合并 遍历 chunk_list,从 当前 chunk + 1 开始进行合并

  • 无 overlap
    • 如果 当前块大小 + 下一个碎片大小 ≤ chunk_size → 合并,继续
    • 如果 当前块大小 + 下一个碎片大小 > chunk_size → 保存当前块,以下一个碎片作为新块的起点,继续遍历
  • 有 overlap 逻辑同上,但 "保存当前块" 时
    • 从当前块的末尾取出 chunk_overlap 长度的内容,新块 = overlap 内容 + 下一个碎片
    • 如果 新块大小 > chunk_size → 递归切分下一个碎片继续遍历

常用参数

  • separator 默认值 ["\n\n", "\n", " ", ""]
  • keep_separator 默认 True 是否保留句尾标点(如 ......),避免切割后丢失语气和逻辑
py 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = TextLoader(file_path=f"{assets_dir}/01.txt", encoding="utf-8")
splitter = RecursiveCharacterTextSplitter(
  chunk_size=20, 
  chunk_overlap=10,
  separators=["...", "\n\n", "\n", ""],
  keep_separator=True
)

texts = splitter.split_text(loader.load()[0].page_content)

代码切分

py 复制代码
from langchain_text_splitters import RecursiveCharacterTextSplitter, Language
# 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', 'elixir', 'powershell']

RecursiveCharacterTextSplitter.from_language(
  language=Language.PYTHON,
  chunk_size=50,
  chunk_overlap=0
)

TokenTextSplitter

encoding_name 是指定 token 编码器,将文本切分为 token 序列 tiktoken -> model.py 中有很多类型支持

py 复制代码
from langchain_text_splitters import TokenTextSplitter

docs = TextLoader(file_path=f"{assets_dir}/01.txt", encoding="utf-8").load()
splitter = TokenTextSplitter(encoding_name="o200k_base", chunk_size=20, chunk_overlap=0)
rprint(splitter.split_documents(docs))

CharacterTextSplitter 也支持使用 token 切分

py 复制代码
splitter = CharacterTextSplitter.from_tiktoken_encoder(
  encoding_name="o200k_base", 
  chunk_size=20, 
  chunk_overlap=0, 
  separator="。"
)
splitter.split_documents(docs)

SemanticChunker 语义切割

需要安装 uv add langchain-experimental 需要使用向量模型将文本转换为向量,当发现前后语义差异过大时就切分

py 复制代码
from langchain_experimental.text_splitter import SemanticChunker
from langchain.embeddings import init_embeddings

embed_model = init_embeddings("ollama:qwen3-embedding:0.6b")

docs = TextLoader(file_path=f"{assets_dir}/01.txt", encoding="utf-8").load()
splitter = SemanticChunker(
  # 向量模型
  embeddings=embed_model, 
  # 断点阈值类型 "percentile" 默认值, "standard_deviation", "interquartile", "gradient"
  # 百分位数,标准差,四分位距,梯度 每个默认值对应的有 breakpoint_threshold_amount 可以不指定这个参数
  breakpoint_threshold_type="percentile", 
  breakpoint_threshold_amount=65.0,
  sentence_split_regex=r"(?<=[。?!])\s+" # 匹配到正则规则时,先按照正则进行拆分
)
splitter.split_documents(docs)

HTMLHeaderTextSplitter

py 复制代码
from langchain_text_splitters import HTMLHeaderTextSplitter

docs = TextLoader(file_path=f"{assets_dir}/01.html", encoding="utf-8").load()
splitter = HTMLHeaderTextSplitter(headers_to_split_on=[("h1", "切分的依据会放进 metadata 中"),])
splitter.split_text(docs[0].page_content)

MarkdownTextSplitter

py 复制代码
from langchain_text_splitters import MarkdownTextSplitter

docs = TextLoader(file_path=f"{assets_dir}/01.md", encoding="utf-8").load()
splitter = MarkdownTextSplitter(chunk_size=50, chunk_overlap=0)
splitter.split_documents(docs)

向量模型 init_embeddings

通过向量模型将文本等内容转换为向量;不同的模型有不同的维度,比如 qwen3-embeddings 最大支持 4096,可以理解拆分的维度越高,匹配时得到的信息越精准

定义模型的方式和 init_chat_model 一致

py 复制代码
from langchain.embeddings import init_embeddings

# 因为下载了 langchain-ollama 所以不用手动加载环境变量
# api_key, base_url 如果第三方模型仍然需要传递
embed_model = init_embeddings(
  "ollama:qwen3-embedding:0.6b",
  dimensions=1024 # 指定维度
)

提供两个方法 embed_query | embed_documents 分别是字符串向量化和列表字符串向量化,返回小数列表。

py 复制代码
text = "What was the name mentioned in the conversation?"
vector = embed_model.embed_query(text)
# 1024 [-0.02478226, -0.063406475, -0.0063675, -0.038862742, -0.044847116]
print(len(vector), vector[:5])

text = ["What was the name mentioned in the conversation?"]
vectors = embed_model.embed_documents(text)
# 1024 [-0.02478226, -0.063406475, -0.0063675, -0.038862742, -0.044847116]
print(len(vectors[0]), vectors[0][:5])

向量数据库 Milvus

世面很多支持向量的数据库 FAISS | Chroma | Milvus | Pgvector | Redis | Elasticsearch | Pinecone

Milvus 比较纯粹,专门为了向量存储设计云原生数据库,轻量级高并发都能用 官网

个人测试可以使用 lite 版本 无需安装多余的服务 uv add "pymilvus[milvus-lite]" 类似于 SQLLite

与关系型数据库不同的是 Database -> Collection -> Partition -> Entity

  • Collection 集合,与 mongoDB 类似也可以看做是 table
  • Partition 分区,Collection 的子集,不是必须手动创建,每个 Collection 都有默认
  • Entity 数据,可以看做 table 中的 row

DDL 数据库操作

初始化及数据库的操作

  • list_databases 列出所有的数据库,默认会有一个 ['default']
  • create_database 创建数据库
  • drop_database 删除数据库
  • use_database 切换数据库
py 复制代码
from pymilvus import MilvusClient

# uri: str = "http://localhost:19530",
# user: str = "",
# password: str = "",
# db_name: str = "",
# token: str = "",
client = MilvusClient("./milvus-demo.db") # 生成 milvus_demo.db 文件夹
db_name = "rag_vector"

if db_name not in client.list_databases():
  client.create_database(db_name=db_name)

# 数据库存在 collection 则无法直接删除,需要先删除 collection
client.drop_database(db_name=db_name)
client.use_database(db_name=db_name)

集合操作

  • list_collections 列表所有集合
  • create_collection 创建集合
  • has_collection 是否包含某个集合
  • drop_collection 删除集合
  • describe_collection 查看集合元数据
  • get_collection_stats 获取集合统计信息
  • load_collection 加载集合到内容,不然无法做后续的 curd
py 复制代码
col_name = "docs"
# 列出所有集合
if col_name not in client.list_collections():
  # 创建集合
  client.create_collection(
    collection_name=col_name, # 集合名称
    dimension=1024, # 嵌入向量维度,注意要和模型的维度保持一致
    metric_type="COSINE" # 向量相似度计算方式,默认余弦 COSINE
  )

# 如果存在集合就删除 has_collection, drop_collection
if client.has_collection(collection_name=col_name):
  client.drop_collection(collection_name=col_name)

# 查看 collection 元数据
metadata = client.describe_collection(collection_name=col_name)
# {
#     'collection_name': 'docs',
#     'auto_id': False,
#     'num_shards': 1,
#     'description': '',
#     'fields': [
#         id 主键,vector 数据嵌入向量
#         {'field_id': 0, 'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'params': {}, 'is_primary': True},
#         {'field_id': 0, 'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 1024}}
#     ],
#     'functions': [],
#     'aliases': [],
#     'collection_id': 0,
#     'consistency_level': 0,
#     'consistency_level_name': 'Strong',
#     'properties': {},
#     'num_partitions': 1,
#     'enable_dynamic_field': True, 支持除 id 和 vector 外动态字段
#     'enable_namespace': False,
#     'schema_version': 0
# }

DML 数据库操作

创建数据有 insert | upsert 后者再插入同样数据时会先删除旧的再插入新的

py 复制代码
texts = [
  "LangChain 是一个用于构建 LLM 应用的开发框架。",
  "RAG 的核心是先检索相关知识,再让大模型生成答案。",
  "Milvus 全球领先的开源向量数据库项目之一"
]

# 使用模型转向量
vectors = embed_model.embed_documents(texts)
data = [
  { 
    "id": i, # 主键 id
    "vector": vectors[i], # 向量数据
    "text": texts[i] # 自定义数据
  } for i in range(len(texts))
]

client.upsert(collection_name=col_name, data=data)

# 保证数据落盘
client.flush(collection_name=col_name)
# 获取 collection 的统计信息 output: {'row_count': 3}
stats = client.get_collection_stats(collection_name=col_name)

DQL 数据库操作

查询可以使用 get | search | query | query_iterator,共同支持 output_fields 参数

  • get 用于 ids 查询
  • query 支持 ids | filter
  • query_iterator 支持 filter | limit | batch_size
  • search 支持 filter | limit | data
py 复制代码
# 查询之前需要先 load 一下
client.load_collection(collection_name=col_name)

# =====  query 精确查询 =====
result: list[dict] = client.query(
  collection_name=col_name,
  filter="id == 1", # 过滤条件 空表示所有,支持表达式
  output_fields=["*"], # 输出的字段 * 表示所有
  ids=["1"], # 主键 id 查找
)
for i in result:
  print(i["id"], i["vector"][:2], i["text"])
  # 0 [0.025912750512361526, 0.05833195894956589] LangChain 是一个用于构建 LLM 应用的开发框架。
  # 1 [-0.060063816606998444, -0.03789394721388817] RAG 的核心是先检索相关知识,再让大模型生成答案。
  # 2 [-0.04220530390739441, -0.013182224705815315] Milvus 全球领先的开源向量数据库项目之一


# =====  query_iterator 迭代器返回 =====
result: QueryIterator = client.query_iterator(
  collection_name=col_name,
  output_fields=["text"], # 查 text 字段
  limit=2,
  batch_size=1000,
  filter=""
)
print(result.next())
# [{'id': 0, 'text': 'LangChain 是一个用于构建 LLM 应用的开发框架。'}, {'id': 1, 'text': 'RAG 的核心是先检索相关知识,再让大模型生成答案。'}]


# =====  get 按 id 查 =====
result: list[dict] = client.get(
  collection_name=col_name,
  output_fields=["*"],
  ids=["1"],
)

# =====  search 按向量相似度查 =====
data: list[float] = embed_model.embed_query("什么是向量数据库?")
result: list[list[dict]] = client.search( # 返回二维数组
  collection_name=col_name,
  data=[data], # 查询数据 传递二维数组
  output_fields=["text"],
  limit=2,
  filter=""
)
# [[
#   {
#     'id': 2, 
#     'distance': 0.7010695934295654,  向量相似度
#     'entity': {'id': 2, 'text': 'Milvus 全球领先的开源向量数据库项目之一'}
#   }, 
#   {
#     'id': 1, 
#     'distance': 0.46880030632019043, 
#     'entity': {'id': 1, 'text': 'RAG 的核心是先检索相关知识,再让大模型生成答案。'}
#   }
# ]]

删除使用 delete 方法,支持 ids | filter 参数

py 复制代码
client.delete(
  collection_name=col_name,
  ids=["1"]
)
client.query(collection_name=col_name, output_fields=["text"])
相关推荐
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十三):前后端数据验证
后端·go·ai编程
YuePeng2 小时前
别再让 AI 直接写 SQL 了:一个注解搞定十亿行数据的语义层
后端·github
半夜里咳嗽的狼2 小时前
Go 1.25 的 WaitGroup.Go 省了两行代码,也补不上这三个并发边界
后端·go
Lihua奏2 小时前
身份验证:登录之后,服务器怎么一直认得你?
后端
XuCoder2 小时前
Redis 缓存和 MySQL 数据不一致怎么办?双写一致性一次讲透
后端
程序员天天困2 小时前
Arthas ognl 表达式从入门到实战:掌握在线调试最强的表达式引擎
java·jvm·后端
Escape3 小时前
为什么你的 AI 越聊越傻?从 Token 到 Agent,彻底搞懂 AI Agent的秘密㊙️
前端·人工智能·后端
用户40966601317513 小时前
Lombok 你用对了吗?@Data 之外的 6 个隐藏神器
java·后端·代码规范
董员外3 小时前
RAG 系统进化论(二):Naive RAG,检索增强生成的最小闭环
前端·人工智能·后端