LangChain使用之Retrieval

1、Retrieval模块的设计意义

  • Retrieval直接翻译过来即“检索”,本章Retrieval模块包括与检索步骤相关的所有内容,例如数据的获 取、切分、向量化、向量存储、向量检索等模块。常被应用于构建一个“企业/私人的知识库”,提升大 模型的整体能力。

1.1 大模型的幻觉问题

  • 拥有记忆后,确实扩展了AI工程的应用场景。
  • 但是在专有领域,LLM无法学习到所有的专业知识细节,因此在面向专业领域知识的提问时,无法给出可靠准确的回答,甚至会“胡言乱语”,这种现象称之为LLM的“幻觉”
  • 大模型生成内容的不可控,尤其是在金融和医疗领域等领域,一次金额评估的错误,一次医疗诊断的失误,哪怕只出现一次都是致命的。但,对于非专业人士来说可能难以辨识。目前还没有能够百分之百解决这种情况的方案。
  • 当前大家普遍达成共识的一个方案:
    • 首先,为大模型提供一定的上下文信息,让其输出会变得更稳定。
    • 其次,利用本章的RAG,将检索出来的文档和提示词输送给大模型,生成更可靠的答案。

1.2 RAG的解决方案

  • 可以说,当应用需求集中在利用大模型去回答特定私有领域的知识,且知识库足够大,那么除了微调大模型外,RAG就是非常有效的一种缓解大模型推理的“幻觉”问题的解决方案。

  • LangChain对这一流程提供了解决方案。

    如果说LangChain相当于给LLM这个“大脑”安装了“四肢和躯干”,RAG则是为LLM提供了接入“人类知识图书馆”的能力。

  • 目前,已经出现了非常多的产品几乎完全建立在 RAG 之上,包括客服系统、基于大模型的数据分析, 以及成千上万的数据驱动聊天应用,应用场景五花八门。

1.3 RAG的优缺点

  • RAG的优点
    • 相比提示词工程,RAG有更丰富的上下文和数据样本,可以不需要用户提供过多的背景描述,就能生成比较符合用户预期的答案。
    • 相比于模型微调,RAG可以提升问答内容的时效性可靠性
    • 在一定程度上保护了业务数据的隐私性
  • RAG的缺点
    • 由于每次问答都涉及外部系统数据检索,因此RAG的响应时延相对较高。
    • 引用的外部知识数据会消耗大量的模型Token资源。

1.4 Retrieval流程

环节1:Source(数据源)

  • 指的是RAG架构中所外挂的知识库。这里有三点说明:

    • 原始数据源类型多样:如:视频、图片、文本、代码、文档等

    • 形式的多样性:

      • 可以是上百个.csv文件,可以是上千个.json文件,也可以是上万个.pdf文件

      • 可以是某一个业务流程外放的API,可以是某个网站的实时数据等

环节2:Load(加载)

  • 文档加载器(Document Loaders)负责将来自不同数据源的非结构化文本,加载到内存,成为文档(Document)对象

  • 文档对象包含文档内容和相关元数据信息,例如TXT、CSV、HTML、JSON、Markdown、PDF,甚至 YouTube 视频转录等。

  • 文档加载器还支持“延迟加载”模式,以缓解处理大文件时的内存压力。

  • langchain封装好的loader地址:https://python.langchain.com/v0.1/docs/integrations/document_loaders/

  • 文档加载器的编程接口使用起来非常简单,以下给出加载TXT格式文档的例子。

    1
    2
    3
    4
    from langchain.document_loadersimport TextLoader

    loader = TextLoader("./test.txt")
    print(loader.load())

环节3:Transform(转换)

  • 文档转换器(Document Transformers)负责对加载的文档进行转换和处理,以便更好地适应下游任务的需求。
  • 文档转换器提供了一致的接口(工具)来操作文档,主要包括以下几类:
    • 文本拆分器(Text Splitters):将长文本拆分成语义上相关的小块,以适应语言模型的上下文窗口限制。
    • 冗余过滤器(Redundancy Filters):识别并过滤重复的文档。
    • 元数据提取器(Metadata Extractors):从文档中提取标题、语调等结构化元数据。
    • 多语言转换器(Multi-lingual Transformers):实现文档的机器翻译。
    • 对话转换器(Conversational Transformers):将非结构化对话转换为问答格式的文档。
  • 总的来说,文档转换器是 LangChain 处理管道中非常重要的一个组件,它丰富了框架对文档的表示和操作能力。
  • 在这些功能中,文档拆分器是必须的操作。下面单独说明。

环节3.1:Text Splitting(文档拆分)

  • 拆分/分块的必要性:前一个环节加载后的文档对象可以直接传入文档拆分器进行拆分,而文档切块后才能向量化并存入数据库中。

  • 文档拆分器的多样性:LangChain提供了丰富的文档拆分器,不仅能够切分普通文本,还能切分Markdown、JSON、HTML、代码等特殊格式的文本。

  • 拆分/分块的挑战性:实际拆分操作中需要处理许多细节问题,不同类型的文本不同的使用场景都需要采用不同的分块策略。

    • 可以按照数据类型进行切片处理,比如针对文本类数据,可以直接按照字符、段落进行切片;代码类数据则需要进一步细分以保证代码的功能性;
    • 可以直接根据token进行切片处理
  • 在构建RAG应用程序的整个流程中,拆分/分块是最具挑战性的环节之一,它显著影响检索效果。目前还没有通用的方法可以明确指出哪一种分块策略最为有效。不同的使用场景和数据类型都会影响分块策略的选择。

环节4:Embed(嵌入)

  • 文档嵌入模型(Text Embedding Models)负责将文本转换为向量表示,即模型赋予了文本计算机可理解的数值表示,使文本可用于向量空间中的各种运算,大大拓展了文本分析的可能性,是自然语言处理领域非常重要的技术。

  • 举例:

  • 实现原理:通过特定算法(如Word2Vec)将语义信息编码为固定维度的向量,具体算法细节需后续深入。

  • 关键特性:相似的词在向量空间中距离相近,例如”猫”和”犬”的向量夹角小于”猫”和”汽车”。

  • 文本嵌入为 LangChain 中的问答、检索、推荐等功能提供了重要支持。具体为:

    • 语义匹配:通过计算两个文本的向量余弦相似度,判断它们在语义上的相似程度,实现语义匹配。
    • 文本检索:通过计算不同文本之间的向量相似度,可以实现语义搜索,找到向量空间中最相似的文本。
    • 信息推荐:根据用户的历史记录或兴趣嵌入生成用户向量,计算不同信息的向量与用户向量的相似度,推荐相似的信息。
    • 知识挖掘:可以通过聚类、降维等手段分析文本向量的分布,发现文本之间的潜在关联,挖掘知识。
    • 自然语言处理:将词语、句子等表示为稠密向量,为神经网络等下游任务提供输入。

环节5:Store(存储)

  • LangChain 还支持把文本嵌入存储到向量存储或临时缓存,以避免需要重新计算它们。这里就出现了数据库,支持这些嵌入的高效存储搜索的需求。

环节6:Retrieve(检索)

  • 检索器(Retrievers)是一种用于响应非结构化查询的接口,它可以返回符合查询要求的文档。
  • LangChain 提供了一些常用的检索器,如向量检索器文档检索器网站研究检索器等。
  • 通过配置不同的检索器,LangChain 可以灵活地平衡检索的精度、召回率与效率。检索结果将为后续的问答生成提供信息支持,以产生更加准确和完整的回答。

2、文档加载器Document Loaders

  • LangChain的设计:对于 中多种不同的数据源,我们可以用一种统一的形式读取、调用。

2.1 加载txt

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
from langchain_community.document_loaders import TextLoader, PyPDFLoader

# 指明txt文档的路径
file_path = "./asset/load/01-langchain-utf-8.txt"

# 创建一个TextLoader的实例
text_loader = TextLoader(
file_path=file_path,
encoding="utf-8",
)

# 调用load(),返回一个list[Document]
docs = text_loader.load()

print(docs)
# [Document(metadata={'source': './asset/load/01-langchain-utf-8.txt'}, page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,构建端到端的应用程序。它提供了一套模块化工具和接口,支持从简单的文本生成到复杂的多步骤推理任务')]

print(len(docs)) # 查看列表中元素的个数
# 1

print(docs[0])
# page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,构建端到端的应用程序。它提供了一套模块化工具和接口,支持从简单的文本生成到复杂的多步骤推理任务' metadata={'source': './asset/load/01-langchain-utf-8.txt'}

# 显示Document对象的元数据
print(docs[0].metadata)
# {'source': './asset/load/01-langchain-utf-8.txt'}

# 显示文档中的内容信息
print(docs[0].page_content)
# LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,构建端到端的应用程序。它提供了一套模块化工具和接口,支持从简单的文本生成到复杂的多步骤推理任务


  • Documment对象中有两个重要的属性:
    • page_content:真正的文档内容
    • metadata:文档内容的原数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_community.document_loaders import TextLoader

# 指明txt文档的路径
file_path = "./asset/load/01-langchain-gbk.txt"

# 创建一个TextLoader的实例
text_loader = TextLoader(
file_path=file_path,
encoding="gbk", #此时使用的解码集一定要与当初存储此文件使用的编码集相同,否则报错!
)

# 调用load(),返回一个list[Document]
docs = text_loader.load()

print(docs)
# [Document(metadata={'source': './asset/load/01-langchain-gbk.txt'}, page_content='LangChain 是一个用于构建基于大语言模型(LLM)应用的开发框架,旨在帮助开发者更高效地集成、管理和增强大语言模型的能力,构建端到端的应用程序。它提供了一套模块化工具和接口,支持从简单的文本生成到复杂的多步骤推理任务')]

2.2 加载pdf

  • 举例1:LangChain加载PDF文件使用的是pypdf,先安装:

    1
    pip install pypdf
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from langchain_community.document_loaders.pdf import PyPDFLoader

    pdf_loader = PyPDFLoader(
    file_path="./asset/load/02-load.pdf"
    )

    docs = pdf_loader.load()

    print(docs)
    # [Document(metadata={'producer': 'Microsoft® Word 2019', 'creator': 'Microsoft® Word 2019', 'creationdate': '2025-06-20T17:18:19+08:00', 'moddate': '2025-06-20T17:18:19+08:00', 'source': './asset/load/02-load.pdf', 'total_pages': 1, 'page': 0, 'page_label': '1'}, page_content='"他的车,他的命! 他忽然想起来,一年,二年,至少有三四年;一滴汗,两滴汗,不\n知道多少万滴汗,才挣出那辆车。从风里雨里的咬牙,从饭里茶里的自苦,才赚出那辆车。\n那辆车是他的一切挣扎与困苦的总结果与报酬,像身经百战的武士的一颗徽章。……他老想\n着远远的一辆车,可以使他自由,独立,像自己的手脚的那么一辆车。" \n \n"他吃,他喝,他嫖,他赌,他懒,他狡猾, 因为他没了心,他的心被人家摘了去。他\n只剩下那个高大的肉架子,等着溃烂,预备着到乱死岗子去。……体面的、要强的、好梦想\n的、利己的、个人的、健壮的、伟大的祥子,不知陪着人家送了多少回殡;不知道何时何地\n会埋起他自己来, 埋起这堕落的、 自私的、 不幸的、 社会病胎里的产儿, 个人主义的末路鬼!\n"')]

    print(len(docs))
    # 1
  • 也可以加载网络中的一个文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from langchain_community.document_loaders.pdf import PyPDFLoader

    pdf_loader = PyPDFLoader(
    file_path="https://arxiv.org/pdf/2302.03803"
    )

    docs = pdf_loader.load()

    print(len(docs))

    for doc in docs:
    print(doc)

2.3 加载CSV

  • 举例1:加载csv所有列

    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
    from langchain_community.document_loaders import CSVLoader

    csv_loader = CSVLoader(
    file_path="./asset/load/03-load.csv"
    )

    docs = csv_loader.load()

    print(len(docs))
    # 4

    print(docs)
    # [Document(metadata={'source': './asset/load/03-load.csv', 'row': 0}, page_content='id: 1\ntitle: Introduction to Python\ncontent: Python is a popular programming language.\nauthor: John Doe'), Document(metadata={'source': './asset/load/03-load.csv', 'row': 1}, page_content='id: 2\ntitle: Data Science Basics\ncontent: Data science involves statistics and machine learning.\nauthor: Jane Smith'), Document(metadata={'source': './asset/load/03-load.csv', 'row': 2}, page_content='id: 3\ntitle: Web Development\ncontent: HTML, CSS and JavaScript are core web technologies.\nauthor: Mike Johnson'), Document(metadata={'source': './asset/load/03-load.csv', 'row': 3}, page_content='id: 4\ntitle: Artificial Intelligence\ncontent: AI is transforming many industries.\nauthor: Sarah Williams')]

    for doc in docs:
    print(doc)

    # page_content='id: 1
    # title: Introduction to Python
    # content: Python is a popular programming language.
    # author: John Doe' metadata={'source': './asset/load/03-load.csv', 'row': 0}
    # page_content='id: 2
    # title: Data Science Basics
    # content: Data science involves statistics and machine learning.
    # author: Jane Smith' metadata={'source': './asset/load/03-load.csv', 'row': 1}
    # page_content='id: 3
    # title: Web Development
    # content: HTML, CSS and JavaScript are core web technologies.
    # author: Mike Johnson' metadata={'source': './asset/load/03-load.csv', 'row': 2}
    # page_content='id: 4
    # title: Artificial Intelligence
    # content: AI is transforming many industries.
    # author: Sarah Williams' metadata={'source': './asset/load/03-load.csv', 'row': 3}
  • 使用source_column参数指定文件加载的列,保存在source变量中。

    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
    from langchain_community.document_loaders import CSVLoader

    csv_loader = CSVLoader(
    file_path="./asset/load/03-load.csv",
    source_column="author"
    )

    docs = csv_loader.load()

    for doc in docs:
    print(doc)

    # page_content='id: 1
    # title: Introduction to Python
    # content: Python is a popular programming language.
    # author: John Doe' metadata={'source': 'John Doe', 'row': 0}
    # page_content='id: 2
    # title: Data Science Basics
    # content: Data science involves statistics and machine learning.
    # author: Jane Smith' metadata={'source': 'Jane Smith', 'row': 1}
    # page_content='id: 3
    # title: Web Development
    # content: HTML, CSS and JavaScript are core web technologies.
    # author: Mike Johnson' metadata={'source': 'Mike Johnson', 'row': 2}
    # page_content='id: 4
    # title: Artificial Intelligence
    # content: AI is transforming many industries.
    # author: Sarah Williams' metadata={'source': 'Sarah Williams', 'row': 3}

2.4 加载JSON

  • LangChain提供的JSON格式的文档加载器是JSONLoader。在实际应用场景中,JSON格式的数据占有很大比例,而且JSON的形式也是多样的。我们需要特别关注。

  • JSONLoader使用指定的jq结构来解析JSON文件。jq是一个轻量级的命令行JSON处理器 ,可以对JSON格式的数据进行各种复杂的处理,包括数据过滤、映射、减少和转换,是处理 JSON 数据的首选工具之一。

    1
    pip install jq
  • 04-load.json:

    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
    {
    "messages": [
    {
    "sender": "Alice",
    "content": "Hello, how are you today?",
    "timestamp": "2023-05-15T10:00:00"
    },
    {
    "sender": "Bob",
    "content": "I'm doing well, thanks for asking!",
    "timestamp": "2023-05-15T10:02:00"
    },
    {
    "sender": "Alice",
    "content": "Would you like to meet for lunch?",
    "timestamp": "2023-05-15T10:05:00"
    },
    {
    "sender": "Bob",
    "content": "Sure, that sounds great!",
    "timestamp": "2023-05-15T10:07:00"
    }
    ],
    "conversation_id": "conv_12345",
    "participants": ["Alice", "Bob"]
    }
  • 04-response.json:

    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
    {
    "status": "success",
    "data": {
    "page": 2,
    "per_page": 3,
    "total_pages": 5,
    "total_items": 14,
    "items": [
    {
    "id": 101,
    "title": "Understanding JSONLoader",
    "content": "This article explains how to parse API responses...",
    "author": {
    "id": "user_1",
    "name": "Alice"
    },
    "created_at": "2023-10-05T08:12:33Z"
    },
    {
    "id": 102,
    "title": "Advanced jq Schema Patterns",
    "content": "Learn to handle nested structures with...",
    "author": {
    "id": "user_2",
    "name": "Bob"
    },
    "created_at": "2023-10-05T09:15:21Z"
    },
    {
    "id": 103,
    "title": "LangChain Metadata Handling",
    "content": "Best practices for preserving metadata...",
    "author": {
    "id": "user_3",
    "name": "Charlie"
    },
    "created_at": "2023-10-05T10:03:47Z"
    }
    ]
    }
    }
  • 举例1:加载json文件中的所有的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from langchain_community.document_loaders import JSONLoader

    json_loader = JSONLoader(
    file_path="./asset/load/04-load.json",
    jq_schema=".", #表示加载所有的字段
    text_content=False, #将加载的json对象转换为json字符串
    )

    docs = json_loader.load()

    print(docs)
    # [Document(metadata={'source': '/Users/json/workspace/pythonProject/python-learning/langchain/7_RAG/asset/load/04-load.json', 'seq_num': 1}, page_content='{"messages": [{"sender": "Alice", "content": "Hello, how are you today?", "timestamp": "2023-05-15T10:00:00"}, {"sender": "Bob", "content": "I\'m doing well, thanks for asking!", "timestamp": "2023-05-15T10:02:00"}, {"sender": "Alice", "content": "Would you like to meet for lunch?", "timestamp": "2023-05-15T10:05:00"}, {"sender": "Bob", "content": "Sure, that sounds great!", "timestamp": "2023-05-15T10:07:00"}], "conversation_id": "conv_12345", "participants": ["Alice", "Bob"]}')]
  • 举例2:加载json文件中messages[]中的所有的content字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from langchain_community.document_loaders import JSONLoader

    json_loader = JSONLoader(
    file_path="./asset/load/04-load.json",
    jq_schema=".messages[].content", #加载messages[]的所有的content字段
    #text_content=False, #将加载的json对象转换为json字符串
    )

    docs = json_loader.load()

    for doc in docs:
    print(doc.page_content)
    # Hello, how are you today?
    # I'm doing well, thanks for asking!
    # Would you like to meet for lunch?
    # Sure, that sounds great!
  • 举例3:提取04-response.json文件中嵌套在 data.items[].content 的文本

    • 如果希望处理 JSON 中的 嵌套字段、数组元素提取,可以使用 content_key 配合 is_content_key_jq_parsable=True,通过 jq 语法精准定位目标数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    from langchain_community.document_loaders import JSONLoader

    # 方式1:
    # json_loader = JSONLoader(
    # file_path="./asset/load/04-response.json",
    # jq_schema=".data.items[].content", #data.items[].content
    # )

    # 方式2:
    json_loader = JSONLoader(
    file_path="./asset/load/04-response.json",
    jq_schema=".data.items[]", #data.items[].content
    content_key=".content",
    is_content_key_jq_parsable=True, #用jq解析content_key
    )

    docs = json_loader.load()

    for doc in docs:
    print(doc.page_content)
    # This article explains how to parse API responses...
    # Learn to handle nested structures with...
    # Best practices for preserving metadata...
  • 举例4:提取04-response.json文件中嵌套在 data.items[] 里的 title、content 和 其文本

    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    # 1.导入相关依赖
    from langchain_community.document_loaders import JSONLoader
    from pprint import pprint

    # 2.定义json文件的路径
    file_path = 'asset/load/04-response.json'

    # 3.定义JSONLoader对象
    # 提取嵌套在 data.items[].content 的文本,并保留其他字段作为元数据
    # loader = JSONLoader(
    # file_path=file_path,
    # # jq_schema=".data.items[] | {id, author, text: (.title + '\n' + .content)}",
    # jq_schema='''.data.items[] | {
    # id,
    # author,
    # created_at,
    # title, # 保留title字段
    # text: (.title + "\n" + .content)
    # }''',
    # content_key=".text", # 再从条目中提取 content 字段
    # is_content_key_jq_parsable=True # 用jq解析content_key
    # )
    loader = JSONLoader(
    file_path=file_path,
    # jq_schema=".data.items[] | {id, author, text: (.title + '\n' + .content)}",
    jq_schema=".data.items[]",
    content_key='.title + "\\n\\n" + .content',
    is_content_key_jq_parsable=True # 用jq解析content_key
    )

    # loader = JSONLoader(
    # file_path=file_path,
    # # jq_schema=".data.items[] | {id, author, text: (.title + '\n' + .content)}",
    # jq_schema='''
    # .data.items[] | {
    # metadata: {
    # id,
    # author,
    # created_at
    # },
    # content: (.title + "\n\n" + .content)
    # }
    # ''', # 构建新结构
    # content_key='.title + "\\n\\n" + .content',
    # is_content_key_jq_parsable=True # 用jq解析content_key
    # )

    # 4.加载
    data = loader.load()

    for doc in data:
    print(doc.page_content)
    # Understanding JSONLoader
    #
    # This article explains how to parse API responses...
    # Advanced jq Schema Patterns
    #
    # Learn to handle nested structures with...
    # LangChain Metadata Handling
    #
    # Best practices for preserving metadata...

2.5 加载HTML(了解)

1
pip install unstructured
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
# 1.导入相关的依赖
from langchain.document_loaders import UnstructuredHTMLLoader

# 2.定义UnstructuredHTMLLoader对象
# strategy:
# "fast" 解析加载html文件速度是比较快(但可能丢失部分结构或元数据)
# "hi_res": (高分辨率解析) 解析精准(速度慢一些)
# "ocr_only" 强制使用ocr提取文本,仅仅适用于图像(对HTML无效)

# mode :one of `{'paged', 'elements', 'single'}
# "elements" 按语义元素(标题、段落、列表、表格等)拆分成多个独立的小文档

html_loader = UnstructuredHTMLLoader(
file_path="asset/load/05-load.html",
mode="elements",
strategy="fast"
)

# 3.加载
docs = html_loader.load()

print(len(docs)) # 16

# 4.打印
for doc in docs:
print(doc)

2.6 加载Markdown(了解)

1
2
3
pip install markdown

pip install unstructured
  • 举例1:使用MarkDownLoader加载md文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 1.导入相关的依赖
    from langchain.document_loaders import UnstructuredMarkdownLoader
    from pprint import pprint

    # 2.定义UnstructuredMarkdownLoader对象
    md_loader = UnstructuredMarkdownLoader(
    file_path="asset/load/06-load.md",
    strategy="fast"
    )

    # 3.加载
    docs = md_loader.load()

    print(len(docs))
    # 4.打印
    for doc in docs:
    pprint(doc)
  • 举例2:精细分割文档,保留结构信息

    • 将Markdown文档按语义元素(标题、段落、列表、表格等)拆分成多个独立的小文档(Element对象),而不是返回单个大文档。通过指定mode="elements"轻松保持这种分离。
    • 每个分割后的元素会包含元数据。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 1.导入相关的依赖
    from langchain.document_loaders import UnstructuredMarkdownLoader
    from pprint import pprint

    # 2.定义UnstructuredMarkdownLoader对象
    md_loader = UnstructuredMarkdownLoader(
    file_path="asset/load/06-load.md",
    strategy="fast",
    mode="elements"
    )

    # 3.加载
    docs = md_loader.load()

    print(len(docs))
    # 4.打印
    for doc in docs:
    pprint(doc)

2.7 加载File Directory(了解)

  • 除了上述的单个文件加载,我们也可以批量加载一个文件夹内的所有文件。

    1
    pip install unstructured
  • 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 1.导入相关的依赖
    from langchain.document_loaders import DirectoryLoader
    from langchain.document_loaders import PythonLoader
    from pprint import pprint

    # 2.定义DirectoryLoader对象,指定要加载的文件夹路径、要加载的文件类型和是否使用多线程
    directory_loader = DirectoryLoader(
    path="./asset/load",
    glob="*.py",
    use_multithreading=True,
    show_progress=True,
    loader_cls=PythonLoader
    )

    # 3.加载
    docs = directory_loader.load()

    # 4.打印
    print(len(docs))
    for doc in docs:
    pprint(doc)

3、文档拆分器Text Splitters

3.1 为什么要拆分/分块/切分

  • 当拿到统一的一个Document对象后,接下来需要切分成Chunks。如果不切分,而是考虑作为一个整体的Document对象,会存在两点问题:
    • 假设提问的Query的答案出现在某一个Document对象中,那么将检索到的整个Document对象直接放入Prompt中并不是最优的选择,因为其中一定会包含非常多无关的信息,而无效信息越多,对大模型后续的推理影响越大。
    • 任何一个大模型都存在最大输入的Token限制,如果一个Document非常大,比如一个几百兆的PDF,那么肯定无法容纳如此多的信息。
  • 基于此,一个有效的解决方案就是将完整的Document对象进行分块处理(Chunking)。无论是在存储还是检索过程中,都将以这些块(chunks)为基本单位,这样有效地避免内容不相关性问题和超出最大输入限制的问题。

3.2 Chunking拆分的策略

  • 方法1:根据句子切分:这种方法按照自然句子边界进行切分,以保持语义完整性。

  • 方法2:按照固定字符数来切分:这种策略根据特定的字符数量来划分文本,但可能会在不适当的位置切断句子。

  • 方法3:按固定字符数来切分,结合重叠窗口(overlapping windows):此方法与按字符数切分相似,但通过重叠窗口技术避免切分关键内容,确保信息连贯性。

  • 方法4:递归方法:通过递归方式动态确定切分点,这种方法可以根据文档的复杂性和内容密度来调整块的大小。

  • 方法5:根据语义切割:这种高级策略依据文本的语义内容来划分块,旨在保持相关信息的集中和完整,适用于需要高度语义保持的应用场景。

    第2种方法(按照字符数切分)和第3种方法(按固定字符数切分结合重叠窗口)主要基于字符进行文本的切分,而不考虑文章的实际内容和语义。这种方式虽简单,但可能会导致主题或语义上的断裂

    相对而言,第4种递归方法更加灵活和高效,它结合了固定长度切分和语义分析。通常是首选策略,因为它能够更好地确保每个段落包含一个完整的主题。

    而第5种方法,基于语义的分割虽然能精确地切分出完整的主题段落,但这种方法效率较低。它需要运行复杂的分段算法(segmentation algorithm),处理速度较慢,并且段落长度可能极不均匀(有的主题段落可能很长,而有的则较短)。因此,尽管它在某些需要高精度语义保持的场景下有其应用价值,但并不适合所有情况

  • 这些方法各有优势和局限,选择适当的分块策略取决于具体的应用需求和预期的检索效果。接下来我们依次尝试用常规手段应该如何实现上述几种方法的文本切分。

3.3 Text Splitter源码分析

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class TextSplitter(BaseDocumentTransformer, ABC):
"""用于将文本分割成块的接口。"""

def __init__(
self,
chunk_size: int = 4000, #单位是字符数。
chunk_overlap: int = 200, #通常会设置为chunk_size的10% - 20%
length_function: Callable[[str], int] = len, #计算字符数的函数
keep_separator: bool = False,
add_start_index: bool = False,
strip_whitespace: bool = True,
) -> None:
"""
创建一个新的文本分割器。

参数:
chunk_size: 返回块的最大尺寸
chunk_overlap: 块之间的字符重叠,避免信息断裂
length_function: 用于测量给定块长度的函数
keep_separator: 是否在块中保留分隔符
add_start_index: 如果为 `True`,则在元数据中包含块的起始索引
strip_whitespace: 如果为 `True`,则从每个文档的开始和结束处去除空白字符
"""
if chunk_overlap > chunk_size:
raise ValueError(
f"重叠大小({chunk_overlap})大于块大小({chunk_size}),应该更小。"
)

@abstractmethod
def split_text(self, text: str) -> List[str]:
"""将文本分割成多个组件。"""

def create_documents(
self, texts: List[str], metadatas: Optional[List[dict]] = None
) -> List[Document]:
"""从文本列表创建文档,其作用是将普通的文件对象转化成Document对象"""

def split_documents(self, documents: Iterable[Document]) -> List[Document]:
"""分割文档。"""

@classmethod
def from_huggingface_tokenizer(cls, tokenizer: Any, **kwargs: Any) -> TextSplitter:
"""使用 HuggingFace 的分词器创建基于字符计数的文本分割器。"""

@classmethod
def from_tiktoken_encoder(
cls: Type[TS],
encoding_name: str = "gpt2",
model_name: Optional[str] = None,
allowed_special: Union[Literal["all"], AbstractSet[str]] = set(),
disallowed_special: Union[Literal["all"], Collection[str]] = "all",
**kwargs: Any,
) -> TS:
"""从 TikToken 编码器创建一个文本分割器,可以设置允许和禁止的特殊字符。"""


def transform_documents(
self, documents: Sequence[Document], **kwargs: Any
) -> Sequence[Document]:
"""通过分割它们来转换文档序列。"""
return self.split_documents(list(documents))
  • 小结:几个常用的文档切分器的方法的调用
    • 情况1:按照字符串进行拆分:
      • split_text(xxx) : 传入的参数类型:字符串 ; 返回值的类型:List[str]
      • create_documents(xxx) : 传入的参数类型:List[str] ; 返回值的类型:List[Document]。底层调用了split_text(xxx)
    • 情况2:按照Document对象进行拆分:
    • split_documents(xxx) : 传入的参数类型:List[Document] ; 返回值的类型:List[Document]。底层调用了create_documents(xxx)
    • Document对象 与 Str 是什么关系?
    • 文档切分器可以按照字符进行切分,也可以按照Document进行切分。其中,Str可以理解为是Document对象的page_content属性。
  • 这里提供了一个可视化展示文本如何分割的工具,https://chunkviz.up.railway.app/

3.4 具体实现

3.4.1 CharacterTextSplitter:Split by character

  • 参数情况说明:

    • chunk_size:每个切块的最大token数量(由长度函数测量)。
    • chunk_overlap:相邻两个切块之间的最大重叠token数量。设置重叠可以使信息不会因为在边界处被切断而丢失。一般设为chunk_size的10-20%
    • separator:分割使用的分隔符,默认值为”\n\n”。
    • length_function:用于计算切块长度的方法。默认赋值为len函数。len函数在Python中按Unicode字符计数,所以一个汉字、一个英文字母、一个符号都算一个字符。
  • 举例1:字符串文本的分割

    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
    # 1.导入相关依赖
    from langchain.text_splitter import CharacterTextSplitter
    from zipp.glob import separate

    # 2.示例文本
    text = """
    LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者能够更容易地构建复杂的应用程序。
    """

    # 3.定义字符分割器
    splitter = CharacterTextSplitter(
    chunk_size=51, # 每块大小
    chunk_overlap=7,# 块与块之间的重复字符数
    #length_function=len,
    separator="" # 设置为空字符串时,表示禁用分隔符
    )

    # 4.分割文本
    texts = splitter.split_text(text)

    # 5.打印结果
    for i, chunk in enumerate(texts):
    print(f"块 {i+1}:长度:{len(chunk)}")
    print(chunk)
    print("-" * 50)

    # 块 1:长度:50
    # LangChain 是一个用于开发由语言模型驱动的应用程序的框架的。它提供了一套工具和抽象,使开发者
    # --------------------------------------------------
    # 块 2:长度:23
    # 抽象,使开发者能够更容易地构建复杂的应用程序。
    # --------------------------------------------------
    • 说明:若必须禁用分隔符(如处理无空格文本),需**容忍实际块长略小于 chunk_size**(尤其对中文)
  • 举例2:指定分割符

    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
    # 1.导入相关依赖
    from langchain.text_splitter import CharacterTextSplitter

    # 2.定义要分割的文本
    text = "这是一个示例文本啊。我们将使用CharacterTextSplitter将其分割成小块。分割基于字符数。"

    # text = """
    # LangChain 是一个用于开发由语言模型。驱动的应用程序的框架的。它提供了一套工具和抽象。使开发者能够更容易地构建复杂的应用程序。
    # """

    # 3.定义分割器实例
    text_splitter = CharacterTextSplitter(
    chunk_size=30, # 每个块的最大字符数
    # chunk_size=43, # 每个块的最大字符数
    chunk_overlap=0, # 块之间的重叠字符数
    separator="。", # 按句号分割 (分隔符优先)
    )

    # 4.开始分割
    chunks = text_splitter.split_text(text)

    # 5.打印效果
    for i,chunk in enumerate(chunks):
    print(f"块 {i + 1}:长度:{len(chunk)}")
    print(chunk)
    print("-"*50)
    # Created a chunk of size 33, which is longer than the specified 30
    # 块 1:长度:9
    # 这是一个示例文本啊
    # --------------------------------------------------
    # 块 2:长度:33
    # 我们将使用CharacterTextSplitter将其分割成小块
    # --------------------------------------------------
    # 块 3:长度:7
    # 分割基于字符数
    # --------------------------------------------------
    • separator优先原则:当设置了 separator(如”。”),分割器会首先尝试在分隔符处分割,然后再考虑 chunk_size。这是为了避免在句子中间硬性切断。这种设计是为了:
      • 优先保持语义完整性(不切断句子)
      • 避免产生无意义的碎片(如半个单词/不完整句子)
      • 如果 chunk_size 比片段小,无法拆分片段,导致 overlap失效。
      • chunk_overlap仅在合并后的片段之间生效(如果 chunk_size 足够大)。如果没有合并的片段,则 overlap失效。
  • 举例3:熟悉 keep_separator、separator 以及 chunk_overlap何时生效

    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
    # 1.导入相关依赖
    from langchain.text_splitter import CharacterTextSplitter

    # 2.定义要分割的文本
    text = "这是第一段文本。这是第二段内容。最后一段结束。"

    # 3.定义字符分割器
    text_splitter = CharacterTextSplitter(
    separator="。",
    chunk_size=20,
    chunk_overlap=8,
    keep_separator=True #chunk中是否保留切割符
    )

    # 4.分割文本
    chunks = text_splitter.split_text(text)

    # 5.打印结果
    for i,chunk in enumerate(chunks):
    print(f"块 {i + 1}:长度:{len(chunk)}")
    print(chunk)
    print("-"*50)
    # 块 1:长度:15
    # 这是第一段文本。这是第二段内容
    # --------------------------------------------------
    # 块 2:长度:16
    # 。这是第二段内容。最后一段结束。
    # --------------------------------------------------
    • 注意:有重叠。此时,文本“这是第二段内容。”的token正好就是8。

3.4.2 RecursiveCharacterTextSplitter:最常用

  • 文档切分器中较常用的是RecursiveCharacterTextSplitter,遇到特定字符时进行分割。默认情况下,它尝试进行切割的字符包括 ["\n\n", "\n", " ", ""]。此外,还可以考虑添加,。等分割字符。

  • 具体为:该文本分割器接受一个字符列表作为参数, 根据第一个字符进行切块,但如果任何切块太大,则会继续移动到下一个字符,并以此类推。

  • 此外,还可以考虑添加,。等分割字符。

  • 特点

    • 智能分段:通过递归尝试多种分隔符,将文本分割为大小接近chunk_size的片段,同时避免在句子中间截断。
    • 保留上下文:优先在自然语言边界(如段落、句子结尾)处分割,减少信息碎片化
    • 灵活适配:适用于多种文本类型(代码、Markdown、普通文本等),是LangChain中最通用的文本分割器。
  • 除此之外,还可以指定的参数包括:

    • chunk_size:同TextSplitter(父类) 。
    • chunk_overlap:同TextSplitter(父类) 。
    • length_function:同TextSplitter(父类) 。
    • add_start_index:同TextSplitter(父类) 。
  • 举例1:使用split_text()方法演示

    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
    # 1.导入相关依赖
    from langchain.text_splitter import RecursiveCharacterTextSplitter

    # 2.定义RecursiveCharacterTextSplitter分割器对象
    text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    #add_start_index=True,
    )

    # 3.定义拆分的内容
    text="LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"

    # 4.拆分器分割
    paragraphs = text_splitter.split_text(text)

    for para in paragraphs:
    print(para)
    print('-------')
    # LangChain框
    # -------
    # 架特性
    # -------
    # 多模型集成(GPT
    # -------
    # /Claude)
    # -------
    # 记忆管理功能
    # -------
    # 链式调用设计。文档
    # -------
    # 分析场景示例:需要处
    # -------
    # 理PDF/Word等
    # -------
    # 格式。
    # -------
  • 举例2:使用create_documents()方法演示,传入字符串列表,返回Document对象列表

    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
    # 1.导入相关依赖
    from langchain.text_splitter import RecursiveCharacterTextSplitter

    # 2.定义RecursiveCharacterTextSplitter分割器对象
    text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=0,
    add_start_index=True,
    )

    # 3.定义拆分的内容
    text_list = ["LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"]

    # 4.拆分器分割
    paragraphs = text_splitter.create_documents(text_list)

    for para in paragraphs:
    print(para)
    print('-------')
    # page_content='LangChain框' metadata={'start_index': 0}
    # -------
    # page_content='架特性' metadata={'start_index': 10}
    # -------
    # page_content='多模型集成(GPT' metadata={'start_index': 15}
    # -------
    # page_content='/Claude)' metadata={'start_index': 24}
    # -------
    # page_content='记忆管理功能' metadata={'start_index': 33}
    # -------
    # page_content='链式调用设计。文档' metadata={'start_index': 40}
    # -------
    # page_content='分析场景示例:需要处' metadata={'start_index': 49}
    # -------
    # page_content='理PDF/Word等' metadata={'start_index': 59}
    # -------
    # page_content='格式。' metadata={'start_index': 69}
    # -------
    • 第一阶段:顶级分割(按\n\n

      首次分割:

      1
      2
      3
      4
      5
      text.split("\n\n") → 
      [
      "LangChain框架特性",
      "多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
      ]
      • 第一部分长度:12字符 > 10 → 需要继续分割
      • 第二部分长度:79字符 > 10 → 需要继续分割
    • 第二阶段:递归分割第一部分 "LangChain框架特性"

      • 尝试\n:无匹配

      • 尝试 (空格):

        • 检查字符串:"LangChain框架特性"(无空格)
      • 回退到""(字符级分割):

        1
        2
        list("LangChain框架特性") → 
        ['L','a','n','g','C','h','a','i','n','框','架','特','性']
        • 前10字符:"LangChain框"

        • 剩余部分:"架特性"

    • 第三阶段:递归分割第二部分(长段落)

      • \n分割:

        1
        2
        3
        4
        5
        6
        "多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档...".split("\n") →
        [
        "多模型集成(GPT/Claude)", # 17字符
        "记忆管理功能", # 6字符
        "链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。" # 36字符
        ]
        • 第1块:17字符 > 10 → 继续分割
        • 第2块:6字符 ≤ 10 → 直接保留
        • 第3块:36字符 > 10 → 继续分割
      • 分割"多模型集成(GPT/Claude)"

        • 尝试 :无空格
        • 回退到""
          • 前10字符:"多模型集成(GPT"
          • 剩余7字符:"/Claude)"
      • 分割"链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"

        • 尝试 :无空格

        • 回退到""

          • 按10字符分段:

            1
            2
            3
            4
            5
            6
            7
            8
            "链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"

            [
            "链式调用设计。文档",
            "分析场景示例:需要处",
            "理PDF/Word等",
            "格式。"
            ]
  • 举例3:使用create_documents()方法演示,将本地文件内容加载成字符串,进行拆分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 1.导入相关依赖
    from langchain_text_splitters import RecursiveCharacterTextSplitter

    # 2.打开.txt文件
    with open("asset/load/08-ai.txt", encoding="utf-8") as f:
    state_of_the_union = f.read() #返回的是字符串

    print(type(state_of_the_union)) # <class 'str'>

    # 3.定义RecursiveCharacterTextSplitter(递归字符分割器)
    text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=100,
    chunk_overlap=20,
    #chunk_overlap=0,
    length_function=len
    )

    # 4.分割文本
    texts = text_splitter.create_documents([state_of_the_union])

    # 5.打印分割文本
    for document in texts:
    print(f"🔥{document.page_content}")
  • 举例4:使用split_documents()方法演示,利用PDFLoader加载文档,对文档的内容用递归切割器切割

    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
    # 1.导入相关依赖
    from langchain_community.document_loaders import PyPDFLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter

    # 2.定义PyPDFLoader加载器
    loader = PyPDFLoader("./asset/load/02-load.pdf")

    # 3.加载和切割文档对象
    docs = loader.load() # 返回Document对象构成的list
    # print(f"第0页:\n{docs[0]}")

    # 4.定义切割器
    text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    #chunk_size=120,
    chunk_overlap=0,
    # chunk_overlap=100,
    length_function=len,
    add_start_index=True,
    )

    # 5.对pdf内容进行切割得到文档对象
    paragraphs = text_splitter.split_documents(docs)
    #paragraphs = text_splitter.create_documents([text])
    for para in paragraphs:
    print(para.page_content)
    print('-------')
  • 举例5:自定义分隔符。有些书写系统没有单词边界,例如中文、日文和泰文。使用默认分隔符列表[“\n\n”, “\n”, “” “, “”]分割文本可能导致单词错误的分割。为了保持单词在一起,你可以自定义分割字符,覆盖分隔符列表以包含额外的标点符号。

    1
    2
    3
    4
    5
    6
    7
    text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=200,
    chunk_overlap=20, # 增加重叠字符
    separators=["\n\n", "\n", "。", "!", "?", "……", ",", ""], # 添加中文标点
    length_function=len,
    keep_separator=True #保留句尾标点(如 ……),避免切割后丢失语气和逻辑
    )

    效果:算法优先在句号、省略号处切割,保持句子完整性。

3.4.3 TokenTextSplitter/CharacterTextSplitter:Split by tokens

  • 当我们将文本拆分为块时,除了字符以外,还可以:按Token的数量分割 (而非字符或单词数),将长文本切分成多个小块。

  • 什么是Token?

    • 对模型而言,Token是文本的最小处理单位。例如:
      • 英文:"hello" → 1个Token,"ChatGPT" → 2个Token("Chat" + "GPT")。
      • 中文:"人工智能" → 可能拆分为2-3个Token(取决于分词器)。
  • 特别注意:字符长度不等于 Token 数量

  • 为什么按Token分割?

    • 语言模型对输入长度的限制是基于Token数(如GPT-4的8k/32k Token上限),直接按字符或单词分割可能导致实际Token数超限。(确保每个文本块不超过模型的Token上限)
    • 大语言模型(LLM)通常是以token的数量作为其计量(或收费)的依据,所以采用token分割也有助于我们在使用时更好的控制成本。
  • TokenTextSplitter 使用说明:

    • 核心依据:Token数量 + 自然边界。(TokenTextSplitter 严格按照 token 数量进行分割,但同时 会优先在自然边界(如句尾)处切断,以尽量保证语义的完整性。)
    • 优点:与LLM的Token计数逻辑一致,能尽量保持语义完整。
    • 缺点:对非英语或特定领域文本,Token化效果可能不佳。
    • 典型场景:需要精确控制Token数输入LLM的场景。
  • 举例1:使用TokenTextSplitter

    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
    # 1.导入相关依赖
    from langchain_text_splitters import TokenTextSplitter

    # 2.初始化 TokenTextSplitter
    text_splitter = TokenTextSplitter(
    chunk_size=33, #最大 token 数为 32
    chunk_overlap=0, #重叠 token 数为 0
    encoding_name="cl100k_base", # 使用 OpenAI 的编码器,将文本转换为 token 序列

    )
    # 3.定义文本
    text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50年代诞生以来,人工智能经历了多次起伏。"

    # 4.开始切割
    texts = text_splitter.split_text(text)

    # 打印分割结果
    print(f"原始文本被分割成了 {len(texts)} 个块:")
    for i, chunk in enumerate(texts):
    print(f"块 {i+1}: 长度:{len(chunk)} 内容:{chunk}")
    print("-" * 50)
    # 原始文本被分割成了 3 个块:
    # 块 1: 长度:29 内容:人工智能是一个强大的开发框架。它支持多种语言模型和工具链。
    # --------------------------------------------------
    # 块 2: 长度:32 内容:人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50
    # --------------------------------------------------
    # 块 3: 长度:19 内容:年代诞生以来,人工智能经历了多次起伏。
    # --------------------------------------------------
    • 为什么会出现这样的分割?
      • 第一块(29字符):内容是一个完整的句子,以句号结尾。TokenTextSplitter识别到这是一个自然的语义边界,即使这里的 token 数量可能尚未达到 33,它也选择在此处切割,以保证第一块语义的完整性。
      • 第二块(32字符):内容包含了另一个完整句子”人工智能是指通过计算机程序模拟人类智能的一门科学。“以及下一句的开头”自20世纪50“。分割器在处理完第一个句子的 token 后,可能 token 数量已经接近cunk_size,于是 在下一个自然边界(这里是句号)之后继续读取了少量 token(“自20世纪50”),直到非常接近 33 token 的限制。注意:“50” 之后被切断,是因为编码器很可能将“50”识别为一个独立的 token,而“年代”是另一个 token。为了保证 token 的完整性,它不会在“50”字符中间切断。
      • 第三块(19字符):是第二块中断内容的剩余部分,形成了一个较短的块。这是因为剩余内容本身的 token 数量就较少。
  • 举例2:使用CharacterTextSplitter

    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
    # 1.导入相关依赖
    from langchain_text_splitters import CharacterTextSplitter
    import tiktoken # 用于计算Token数量


    # 2.定义通过Token切割器
    text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
    encoding_name="cl100k_base", # 使用 OpenAI 的编码器
    chunk_size=18, #设置最大的token数
    chunk_overlap=0,
    separator="。", # 指定中文句号为分隔符
    keep_separator=False, # chunk中是否保留分隔符
    )
    # 3.定义文本
    text = "人工智能是一个强大的开发框架。它支持多种语言模型和工具链。今天天气很好,想出去踏青。但是又比较懒不想出去,怎么办"

    # 4.开始切割
    texts = text_splitter.split_text(text)
    print(f"分割后的块数: {len(texts)}")

    # 5.初始化tiktoken编码器(用于Token计数)
    encoder = tiktoken.get_encoding("cl100k_base") # 确保与CharacterTextSplitter的encoding_name一致

    # 6.打印每个块的Token数和内容
    for i, chunk in enumerate(texts):
    tokens = encoder.encode(chunk) # 现在encoder已定义
    print(f"块 {i + 1}: {len(tokens)} Token\n内容: {chunk}\n")
    # 分割后的块数: 4
    # 块 1: 17 Token
    # 内容: 人工智能是一个强大的开发框架
    #
    # 块 2: 14 Token
    # 内容: 它支持多种语言模型和工具链
    #
    # 块 3: 18 Token
    # 内容: 今天天气很好,想出去踏青
    #
    # 块 4: 21 Token
    # 内容: 但是又比较懒不想出去,怎么办

3.4.4 SemanticChunker:语义分块

  • Semantic Chunking(语义分块)是 LangChain 中一种更高级的文本分割方法,它超越了传统的基于字符或固定大小的分块方式,而是根据文本的语义结构进行智能分块,使每个分块保持语义完整性,从而提高检索增强生成(RAG)等应用的效果。

  • 语义分割 vs 传统分割

    特性 语义分割(SemanticChunker) 传统字符分割(RecursiveCharacter)
    分割依据 嵌入向量相似度 固定字符/换行符
    语义完整性 ✅ 保持主题连贯 ❌ 可能切断句子逻辑
    计算成本 ❌ 高(需嵌入模型) ✅ 低
    适用场景 知识库构建、RAG系统 简单文本预处理
  • 举例:

    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
    from langchain_experimental.text_splitter import SemanticChunker
    from langchain_openai.embeddings import OpenAIEmbeddings
    import os
    import dotenv

    dotenv.load_dotenv()

    # 加载文本
    with open("asset/load/09-ai1.txt", encoding="utf-8") as f:
    state_of_the_union = f.read() #返回字符串

    # 获取嵌入模型
    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
    embed_model = OpenAIEmbeddings(
    model="text-embedding-3-large"
    )

    # 获取切割器
    text_splitter = SemanticChunker(
    embeddings=embed_model,
    breakpoint_threshold_type="percentile",#断点阈值类型:字面值["百分位数", "标准差", "四分位距", "梯度"] 选其一
    breakpoint_threshold_amount=65.0 #断点阈值数量 (极低阈值 → 高分割敏感度)
    )

    # 切分文档
    docs = text_splitter.create_documents(texts = [state_of_the_union])
    print(len(docs))
    for doc in docs:
    print(f"🔍 文档 {doc}:")
  • 关于参数的说明:

    1. breakpoint_threshold_type(断点阈值类型)
    • 作用:定义文本语义边界的检测算法,决定何时分割文本块。

    • 可选值及原理:

      类型 原理说明 适用场景
      percentile 计算相邻句子嵌入向量的余弦距离,取距离分布的第N百分位值作为阈值,高于此值则分割 常规文本(如文章、报告)
      standard_deviation 均值 + N倍标准差为阈值,识别语义突变点 语义变化剧烈的文档(如技术手册)
      interquartile 四分位距(IQR) 定义异常值边界,超过则分割 长文档(如书籍)
      gradient 基于嵌入向量变化的梯度检测分割点(需自定义实现) 实验性需求
    1. breakpoint_threshold_amount(断点阈值量)
    • 作用:控制分割的粒度敏感度,值越小分割越细(块越多),值越大分割越粗(块越少)。
    • 取值范围与示例:
      • percentile 模式:0.0~100.0,用户代码设 65.0 表示仅当余弦距离 > 所有距离中最低的65.0%值时分割 。默认值是:95.0,兼顾语义完整性与检索效率。值过小(比如0.1),会产生大量小文本块,过度分割可能导致上下文断裂。
      • standard_deviation 模式:浮点数(如 1.5 表示均值+1.5倍标准差)。
      • interquartile 模式:倍数(如 1.5 是IQR标准值)。

3.4.5 其它拆分器

类型1:HTMLHeaderTextSplitter:Split by HTML header

  • HTMLHeaderTextSplitter是一种专门用于处理HTML文档的文本分割方法,它根据HTML的标题标签(如

    等) 将文档划分为逻辑分块,同时保留标题的层级结构信息。

  • 举例:

    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
    # 1.导入相关依赖
    from langchain.text_splitter import HTMLHeaderTextSplitter

    # 2.定义HTML文件
    html_string = """
    <!DOCTYPE html>
    <html>
    <body>
    <div>
    <h1>欢迎来到尚硅谷!</h1>
    <p>尚硅谷是专门培训IT技术方向</p>
    <div>
    <h2>尚硅谷老师简介</h2>
    <p>尚硅谷老师拥有多年教学经验,都是从一线互联网下来</p>
    <h3>尚硅谷北京校区</h3>
    <p>北京校区位于宏福科技园区</p>
    </div>
    </div>
    </body>
    </html>
    """

    # 4.用于指定要根据哪些HTML标签来分割文本
    headers_to_split_on = [
    ("h1", "标题1"),
    ("h2", "标题2"),
    ("h3", "标题3"),
    ]

    # 5.定义HTMLHeaderTextSplitter分割器
    html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

    # 6.分割器分割
    html_header_splits = html_splitter.split_text(html_string)

    html_header_splits
    # [Document(metadata={'标题1': '欢迎来到尚硅谷!'}, page_content='欢迎来到尚硅谷!'),
    # Document(metadata={'标题1': '欢迎来到尚硅谷!'}, page_content='尚硅谷是专门培训IT技术方向'),
    # Document(metadata={'标题1': '欢迎来到尚硅谷!', '标题2': '尚硅谷老师简介'}, page_content='尚硅谷老师简介'),
    # Document(metadata={'标题1': '欢迎来到尚硅谷!', '标题2': '尚硅谷老师简介'}, page_content='尚硅谷老师拥有多年教学经验,都是从一线互联网下来'),
    # Document(metadata={'标题1': '欢迎来到尚硅谷!', '标题2': '尚硅谷老师简介', '标题3': '尚硅谷北京校区'}, page_content='尚硅谷北京校区'),
    # Document(metadata={'标题1': '欢迎来到尚硅谷!', '标题2': '尚硅谷老师简介', '标题3': '尚硅谷北京校区'}, page_content='北京校区位于宏福科技园区')]
    • 标题下文本内容所属标题的层级信息保存在元数据中。
    • 每个分块会自动继承父级标题的上下文,避免信息割裂。

类型2:CodeTextSplitter:Split code

  • CodeTextSplitter是一个 专为代码文件 设计的文本分割器(Text Splitter),支持代码的语言包括[‘cpp’, ‘go’, ‘java’, ‘js’, ‘php’, ‘proto’, ‘python’, ‘rst’, ‘ruby’, ‘rust’, ‘scala’, ‘swift’, ‘markdown’, ‘latex’, ‘html’, ‘sol’]。它能够根据编程语言的语法结构(如函数、类、代码块等)智能地拆分代码,保持代码逻辑的完整性。

  • 与递归文本分割器(如RecursiveCharacterTextSplitter)不同,CodeTextSplitter 针对代码的特性进 行了优化,避免在函数或类的中间截断

  • 举例1:支持的语言

    1
    pip install langchain-text-splitters
    1
    2
    3
    4
    5
    6
    7
    from langchain.text_splitter import Language

    # 支持分割语言类型
    # Full list of supported languages
    langs = [e.value for e in Language]
    print(langs)
    # ['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', 'visualbasic6']
  • 举例2:

    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
    # 1.导入相关依赖
    from langchain.text_splitter import (
    Language,
    RecursiveCharacterTextSplitter,
    )
    from pprint import pprint

    # 2.定义要分割的python代码片段
    PYTHON_CODE = """
    def hello_world():
    print("Hello, World!")
    def hello_world1():
    print("Hello, World1!")
    """

    # 3.定义递归字符切分器
    python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=50,
    chunk_overlap=0
    )

    # 4.文档切分
    python_docs = python_splitter.create_documents(texts=[PYTHON_CODE])
    pprint(python_docs)
    # [Document(metadata={}, page_content='def hello_world():\n print("Hello, World!")'),
    # Document(metadata={}, page_content='def hello_world1():\n print("Hello, World1!")')]

类型3:MarkdownTextSplitter:md数据类型

  • 因为Markdown格式有特定的语法,一般整体内容由h1、h2、h3等多级标题组织,所以MarkdownHeaderTextSplitter的切分策略就是根据标题来分割文本内容。举例:

    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
    from langchain.text_splitter import MarkdownTextSplitter

    markdown_text = """
    # 一级标题\n
    这是一级标题下的内容\n\n
    ## 二级标题\n
    - 二级下列表项1\n
    - 二级下列表项2\n
    """

    # 关键步骤:直接修改实例属性
    splitter = MarkdownTextSplitter(chunk_size=30, chunk_overlap=0)
    splitter._is_separator_regex = True # 强制将分隔符视为正则表达式

    # 执行分割
    docs = splitter.create_documents(texts=[markdown_text])

    # print(len(docs))

    for i, doc in enumerate(docs):
    print(f"\n分块 {i + 1}:")
    print(doc.page_content)
    # 分块 1:
    # # 一级标题
    #
    # 这是一级标题下的内容
    #
    # 分块 2:
    # ## 二级标题
    #
    # - 二级下列表项1
    #
    # - 二级下列表项2

4、文档嵌入模型Text Embedding Models

4.1 嵌入模型概述

  • Text Embedding Models:文档嵌入模型,提供将文本编码为向量的能力,即文档向量化文档写入用户查询匹配前都会先执行文档嵌入编码,即向量化。

  • LangChain提供了超过25种不同的嵌入提供商和方法的集成,从开源到专有API,总有一款适合你。

  • Hugging Face等开源社区提供了一些文本向量化模型(例如BGE),效果比闭源且调用API的向量化模型效果好,并且向量化模型参数量小,在CPU上即可运行。所以,这里推荐在开发RAG应用的过程中,使用开源的文本向量化模型。此外,开源模型还可以根据应用场景下收集的数据对模型进行微调,提高模型效果。

  • LangChain提供了一个标准接口,允许你通过统一的 API 调用这些不同的文本嵌入模型,以及轻松地在模型之间进行交换。(面向接口编程)

  • 向量化模型的封装提供了两种接口,一种针对文档的向量化(embed_documents),一种针对句子的向量化embed_query。文档的向量化,接收的参数是字符串数组。

4.2 句子的向量化(embed_query)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_openai import OpenAIEmbeddings
import os
import dotenv

dotenv.load_dotenv()

os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

embedding_model = OpenAIEmbeddings(
# model="text-embedding-ada-002"
model = "text-embedding-3-large"
)

text = "Nice to meet you!"

embed_query = embedding_model.embed_query(text = text,)

print(len(embed_query)) # 1536 --> 3072

print(embed_query[:10])
# [-0.027939368039369583, 0.03950421139597893, -0.020670808851718903, -0.0008068201714195311, 0.015577414073050022, -0.00793732050806284, -0.013834580779075623, 0.01602325402200222, 0.015712516382336617, 0.06263390183448792]

4.3 文档的向量化(embed_documents)

  • 文档的向量化,接收的参数是字符串数组。

    • 举例1:

      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
      from langchain_openai import OpenAIEmbeddings
      import numpy as np
      import pandas as pd
      import os
      import dotenv

      dotenv.load_dotenv()

      os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
      os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

      # 初始化嵌入模型
      embeddings_model = OpenAIEmbeddings(model="text-embedding-ada-002")

      # 待嵌入的文本列表
      texts = [
      "Hi there!",
      "Oh, hello!",
      "What's your name?",
      "My friends call me World",
      "Hello World!"
      ]

      # 生成嵌入向量
      embeddings = embeddings_model.embed_documents(texts)


      for i in range(len(texts)):
      print(f"{texts[i]}:{embeddings[i][:3]}",end="\n\n")
      # Hi there!:[-0.020325319841504097, -0.007096723187714815, -0.022839006036520004]
      #
      # Oh, hello!:[0.004446744918823242, -0.014353534206748009, 0.0019785689655691385]
      #
      # What's your name?:[-0.004887457471340895, -0.009618516080081463, 0.007236444391310215]
      #
      # My friends call me World:[-0.004588752053678036, -0.014497518539428711, 0.01022490207105875]
      #
      # Hello World!:[0.002393412170931697, 0.0002737858740147203, -0.0023398753255605698]
    • 举例2:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      from dotenv import load_dotenv
      from langchain_community.document_loaders import CSVLoader
      from langchain_openai import OpenAIEmbeddings


      embeddings_model = OpenAIEmbeddings(
      model="text-embedding-3-large",
      )

      # 情况1:
      loader = CSVLoader("./asset/load/03-load.csv", encoding="utf-8")
      docs = loader.load_and_split()

      #print(len(docs))

      # 存放的是每一个chrunk的embedding。
      embeded_docs = embeddings_model.embed_documents([doc.page_content for doc in docs])
      print(len(embeded_docs))
      # 4
      # 表示的是每一个chrunk的embedding的维度
      print(len(embeded_docs[0]))
      # 3072
      print(embeded_docs[0][:10])
      # [0.0011534227523952723, 0.005336394999176264, -0.008949915878474712, 0.030998842790722847, -0.0017970810877159238, 0.011092216707766056, 0.021087471395730972, 0.048524416983127594, 0.0002677876618690789, -0.00012219828204251826]

5、向量存储(Vector Stores)

5.1 理解向量存储

  • 将文本向量化之后,下一步就是进行向量的存储。这部分包含两块:

    • 向量的存储:将非结构化数据向量化后,完成存储。
    • 向量的查询:查询时,嵌入非结构化查询并检索与嵌入查询“最相似”的嵌入向量。即具有相似性检索能力。

5.2 常用的向量数据库

  • LangChain提供了超过50种不同向量存储(Vector Stores)的集成,从开源的本地向量存储云托管的私有向量存储,允许你选择最适合需求的向量存储。

  • LangChain支持的向量存储参考VectorStore接口和实现。

    • 典型的介绍如下:

      向量数据库 描述
      Chroma 开源、免费的嵌入式数据库
      FAISS Meta出品,开源、免费,Facebook AI相似性搜索服务。(Facebook AI Similarity Search,Facebook AI 相似性搜索库) /fæs/
      Milvus 用于存储、索引和管理由深度神经网络和其他ML模型产生的大量嵌入向量的数据库
      Pinecone 具有广泛功能的向量数据库
      Redis 基于Redis的检索器

5.3 向量数据库的理解

  • 假设你是一名摄影师,拍了大量的照片。为了方便管理和查找,你决定将这些照片存储到一个数据库中。传统的关系型数据库(如 MySQL、PostgreSQL 等)可以帮助你存储照片的元数据,比如拍摄时间、地点、相机型号等。
  • 但是,当你想要根据照片的内容(如颜色、纹理、物体等)进行搜索时,传统数据库可能无法满足你的需求,因为它们通常以数据表的形式存储数据,并使用查询语句进行精确搜索。那么此时,向量数据库就可以派上用场
  • 我们可以构建一个多维的空间使得每张照片特征都存在于这个空间内,并用已有的维度进行表示,比如时间、地点、相机型号、颜色….此照片的信息将作为一个点,存储于其中。以此类推,即可在该空间中构建出无数的点,而后我们将这些点与空间坐标轴的原点相连接,就成为了一条条向量,当这些点变为向量之后,即可利用向量的计算进一步获取更多的信息。当要进行照片的检索时,也会变得更容易更快捷。
  • 注意,在向量数据库中进行检索时,检索并不是唯一的而是查询和目标向量最为相似的一些向量,具有模糊性。
  • 延伸思考一下,只要对图片、视频、商品等素材进行向量化,就可以实现以图搜图、视频相关推荐、相似宝贝推荐等功能。

5.4 代码实现

  • 使用向量数据库组件时需要同时传入包含文本块的Document类对象以及文本向量化组件,向量数据库组件会自动完成将文本向量化的工作,并写入数据库中。

5.4.1 数据的存储

  • 举例1:从TXT文档中加载数据,向量化后存储到Chroma数据库。安装模块:

    1
    pip install langchain-chroma
    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
    from langchain_chroma import Chroma
    from langchain_community.document_loaders import TextLoader
    from langchain_text_splitters import CharacterTextSplitter
    from langchain_openai import OpenAIEmbeddings
    import os
    import dotenv

    dotenv.load_dotenv()

    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

    # 步骤1:创建一个TextLoader的实例,并将指定的文档加载
    loader = TextLoader(
    file_path="./asset/load/09-ai1.txt",
    encoding="utf-8",
    )

    docs = loader.load()


    # 步骤2:创建文本拆分器,并拆分文档
    text_splitter = CharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    )

    splitter_docs = text_splitter.split_documents(docs)

    # print(len(splitter_docs))

    # 步骤3:创建嵌入模型
    embedding_model = OpenAIEmbeddings(model="text-embedding-ada-002")

    # 步骤4:将文档及嵌入模型传入到Chroma相关的结构中,进行数据的存储
    db = Chroma.from_documents(
    documents=splitter_docs,
    embedding=embedding_model,
    )

    query = "人工智能的核心技术有哪些呢?"

    docs = db.similarity_search(query)

    print(docs[0].page_content)

    思考:此时数据存储在哪里呢?

    注意:Chroma主要有两种存储模式:内存模式持久化模式。当使用persist_directory参数时,数据会保存到指定目录;如果没有指定,则默认使用内存存储。需要明确,在向量数据库中,不仅存储了数据(或文档)的向量,而且还存储了数据(或文档)本身。

    1
    2
    3
    4
    5
    db1 = Chroma.from_documents(
    documents=splitter_docs,
    embedding=embedding_model,
    persist_directory="./asset/chroma-1",
    )
  • 举例2:操作csv文档,并向量化。

    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
    from langchain.text_splitter import CharacterTextSplitter
    from langchain_community.document_loaders import CSVLoader
    from langchain_openai import OpenAIEmbeddings
    from langchain_chroma import Chroma

    import os
    import dotenv

    dotenv.load_dotenv()
    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

    # 获取嵌入模型
    embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

    # 加载文档并拆分(第1次拆分)
    loader = CSVLoader("./asset/load/03-load.csv", encoding='utf-8')
    pages = loader.load_and_split()
    #print(len(pages)) # 4

    # 文本拆分(第2次拆分)
    text_spliter = CharacterTextSplitter.from_tiktoken_encoder(chunk_size=500)
    docs = text_spliter.split_documents(pages)

    # 向量存储
    db_path = './asset/chroma-2'
    db = Chroma.from_documents(docs, embeddings, persist_directory=db_path)

5.4.2 数据的检索

  • 举例:一个包含构建Chroma向量数据库以及向量检索的代码。

  • 前置代码:

    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    # 1.导入相关依赖
    from langchain_chroma import Chroma
    from langchain_core.documents import Document
    from langchain_openai import OpenAIEmbeddings

    # 2.定义文档
    raw_documents = [
    Document(
    page_content="葡萄是一种常见的水果,属于葡萄科葡萄属植物。它的果实呈圆形或椭圆形,颜色有绿色、紫色、红色等多种。葡萄富含维生素C和抗氧化物质,可以直接食用或酿造成葡萄酒。",
    metadata={"source": "水果", "type": "植物"}
    ),
    Document(
    page_content="白菜是十字花科蔬菜,原产于中国北方。它的叶片层层包裹形成紧密的球状,口感清脆微甜。白菜富含膳食纤维和维生素K,常用于制作泡菜、炒菜或煮汤。",
    metadata={"source": "蔬菜", "type": "植物"}
    ),
    Document(
    page_content="狗是人类最早驯化的动物之一,属于犬科。它们具有高度社会性,能理解人类情绪,常被用作宠物、导盲犬或警犬。不同品种的狗在体型、毛色和性格上有很大差异。",
    metadata={"source": "动物", "type": "哺乳动物"}
    ),
    Document(
    page_content="猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。",
    metadata={"source": "动物", "type": "哺乳动物"}
    ),
    Document(
    page_content="人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。",
    metadata={"source": "生物", "type": "灵长类"}
    ),
    Document(
    page_content="太阳是太阳系的中心恒星,直径约139万公里,主要由氢和氦组成。它通过核聚变反应产生能量,为地球提供光和热。太阳活动周期约为11年,会影响地球气候。",
    metadata={"source": "天文", "type": "恒星"}
    ),
    Document(
    page_content="长城是中国古代的军事防御工程,总长度超过2万公里。它始建于春秋战国时期,秦朝连接各段,明朝大规模重修。长城是世界文化遗产和人类建筑奇迹。",
    metadata={"source": "历史", "type": "建筑"}
    ),
    Document(
    page_content="量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。",
    metadata={"source": "物理", "type": "科学"}
    ),
    Document(
    page_content="《红楼梦》是中国古典文学四大名著之一,作者曹雪芹。小说以贾、史、王、薛四大家族的兴衰为背景,描绘了贾宝玉与林黛玉的爱情悲剧,反映了封建社会的种种矛盾。",
    metadata={"source": "文学", "type": "小说"}
    ),
    Document(
    page_content="新冠病毒(SARS-CoV-2)是一种可引起呼吸道疾病的冠状病毒。它通过飞沫传播,主要症状包括发热、咳嗽、乏力。疫苗和戴口罩是有效的预防措施。",
    metadata={"source": "医学", "type": "病毒"}
    )
    ]
    # 3. 创建嵌入模型
    embedding = OpenAIEmbeddings(model="text-embedding-ada-002")

    # 4.创建向量数据库
    db = Chroma.from_documents(
    documents=raw_documents,
    embedding=embedding,
    persist_directory="./asset/chroma-3",
    )
  • 接收字符串作为参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 检索示例(返回前3个最相关结果)
    query = "哺乳动物"
    docs = db.similarity_search(query, k=3) # k=3表示返回3个最相关文档
    print(f"查询: '{query}' 的结果:")
    for i, doc in enumerate(docs, 1):
    print(f"\n结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")

    # 查询: '哺乳动物' 的结果:
    #
    # 结果 1:
    # 内容: 猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。
    # 元数据: {'source': '动物', 'type': '哺乳动物'}
    #
    # 结果 2:
    # 内容: 狗是人类最早驯化的动物之一,属于犬科。它们具有高度社会性,能理解人类情绪,常被用作宠物、导盲犬或警犬。不同品种的狗在体型、毛色和性格上有很大差异。
    # 元数据: {'type': '哺乳动物', 'source': '动物'}
    #
    # 结果 3:
    # 内容: 人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。
    # 元数据: {'source': '生物', 'type': '灵长类'}

②支持直接对问题向量查询(similarity_search_by_vector)

  • 搜索与给定嵌入向量相似的文档,它接受嵌入向量作为参数,而不是字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    query = "哺乳动物"
    embedding_vector = embedding.embed_query(query)

    docs = db.similarity_search_by_vector(embedding_vector, k=3)

    print(f"查询: '{query}' 的结果:")
    for i, doc in enumerate(docs, 1):
    print(f"\n结果 {i}:")
    print(f"内容: {doc.page_content}")
    print(f"元数据: {doc.metadata}")
    # 查询: '哺乳动物' 的结果:
    #
    # 结果 1:
    # 内容: 猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。
    # 元数据: {'type': '哺乳动物', 'source': '动物'}
    #
    # 结果 2:
    # 内容: 狗是人类最早驯化的动物之一,属于犬科。它们具有高度社会性,能理解人类情绪,常被用作宠物、导盲犬或警犬。不同品种的狗在体型、毛色和性格上有很大差异。
    # 元数据: {'source': '动物', 'type': '哺乳动物'}
    #
    # 结果 3:
    # 内容: 人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。
    # 元数据: {'source': '生物', 'type': '灵长类'}

③相似性检索,支持过滤元数据(filter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
query = "哺乳动物"

docs = db.similarity_search(
query=query,
k=3,
filter={"source": "动物"})

for i, doc in enumerate(docs, 1):
print(f"\n结果 {i}:")
print(f"内容: {doc.page_content}")
print(f"元数据: {doc.metadata}")
# 结果 1:
# 内容: 猫是小型肉食性哺乳动物,性格独立但也能与人类建立亲密关系。它们夜视能力极强,擅长捕猎老鼠。家猫的品种包括波斯猫、暹罗猫等,毛色和花纹多样。
# 元数据: {'source': '动物', 'type': '哺乳动物'}
#
# 结果 2:
# 内容: 狗是人类最早驯化的动物之一,属于犬科。它们具有高度社会性,能理解人类情绪,常被用作宠物、导盲犬或警犬。不同品种的狗在体型、毛色和性格上有很大差异。
# 元数据: {'type': '哺乳动物', 'source': '动物'}

④通过L2距离分数进行搜索(similarity_search_with_score)

  • 分数值越小,检索到的文档越和问题相似。分值取值范围:[0,正无穷]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    docs = db.similarity_search_with_score(
    "量子力学是什么?"
    )
    for doc, score in docs:
    print(f" [L2距离得分={score:.3f}] {doc.page_content} [{doc.metadata}]")
    # [L2距离得分=0.182] 量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。 [{'source': '物理', 'type': '科学'}]
    # [L2距离得分=0.447] 太阳是太阳系的中心恒星,直径约139万公里,主要由氢和氦组成。它通过核聚变反应产生能量,为地球提供光和热。太阳活动周期约为11年,会影响地球气候。 [{'source': '天文', 'type': '恒星'}]
    # [L2距离得分=0.463] 人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。 [{'type': '灵长类', 'source': '生物'}]
    # [L2距离得分=0.488] 新冠病毒(SARS-CoV-2)是一种可引起呼吸道疾病的冠状病毒。它通过飞沫传播,主要症状包括发热、咳嗽、乏力。疫苗和戴口罩是有效的预防措施。 [{'source': '医学', 'type': '病毒'}]

⑤通过余弦相似度分数进行搜索(_similarity_search_with_relevance_scores)

  • 分数值越接近1(上限),检索到的文档越和问题相似。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    docs = db._similarity_search_with_relevance_scores(
    "量子力学是什么?"
    )
    for doc, score in docs:
    print(f"* [余弦相似度得分={score:.3f}] {doc.page_content} [{doc.metadata}]")
    # * [余弦相似度得分=0.871] 量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。 [{'type': '科学', 'source': '物理'}]
    # * [余弦相似度得分=0.684] 太阳是太阳系的中心恒星,直径约139万公里,主要由氢和氦组成。它通过核聚变反应产生能量,为地球提供光和热。太阳活动周期约为11年,会影响地球气候。 [{'source': '天文', 'type': '恒星'}]
    # * [余弦相似度得分=0.672] 人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。 [{'type': '灵长类', 'source': '生物'}]
    # * [余弦相似度得分=0.655] 新冠病毒(SARS-CoV-2)是一种可引起呼吸道疾病的冠状病毒。它通过飞沫传播,主要症状包括发热、咳嗽、乏力。疫苗和戴口罩是有效的预防措施。 [{'source': '医学', 'type': '病毒'}]
  • MMR 是一种平衡相关性多样性的检索策略,避免返回高度相似的冗余结果。

    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
    docs = db.max_marginal_relevance_search(
    query="量子力学是什么",
    lambda_mult=0.8, # 侧重相似性
    )

    print("🔍 关于【量子力学是什么】的搜索结果:")
    print("=" * 50)
    for i, doc in enumerate(docs):
    print(f"\n📖 结果 {i+1}:")
    print(f"📌 内容: {doc.page_content}")
    print(f"🏷️ 标签: {', '.join(f'{k}={v}' for k, v in doc.metadata.items())}")
    # 🔍 关于【量子力学是什么】的搜索结果:
    # ==================================================
    #
    # 📖 结果 1:
    # 📌 内容: 量子力学是研究微观粒子运动规律的物理学分支。它提出了波粒二象性、测不准原理等概念,彻底改变了人类对物质世界的认知。量子计算机正是基于这一理论发展而来。
    # 🏷️ 标签: source=物理, type=科学
    #
    # 📖 结果 2:
    # 📌 内容: 太阳是太阳系的中心恒星,直径约139万公里,主要由氢和氦组成。它通过核聚变反应产生能量,为地球提供光和热。太阳活动周期约为11年,会影响地球气候。
    # 🏷️ 标签: source=天文, type=恒星
    #
    # 📖 结果 3:
    # 📌 内容: 人类是地球上最具智慧的生物,属于灵长目人科。现代人类(智人)拥有高度发达的大脑,创造了语言、工具和文明。人类的平均寿命约70-80年,分布在全球各地。
    # 🏷️ 标签: type=灵长类, source=生物
    #
    # 📖 结果 4:
    # 📌 内容: 新冠病毒(SARS-CoV-2)是一种可引起呼吸道疾病的冠状病毒。它通过飞沫传播,主要症状包括发热、咳嗽、乏力。疫苗和戴口罩是有效的预防措施。
    # 🏷️ 标签: source=医学, type=病毒

    参数说明:lambda_mult参数值介于 0 到 1 之间,用于确定结果之间的多样性程度,其中 0 对应最大多样性,1 对应最小多样性。默认值为 0.5。

6、检索器(召回器)Retrievers

6.1 介绍

  • 从“向量存储组件”的代码实现5.4.2中可以看到,向量数据库本身已经包含了实现召回功能的函数方法 ( similarity_search )。该函数通过计算原始查询向量与数据库中存储向量之间的相似度来实现召回。

  • LangChain还提供了更加复杂的召回策略,这些策略被集成在Retrievers(检索器或召回器)组件中:

  • Retrievers 的执行步骤:

    • 步骤1:将输入查询转换为向量表示(通常使用词嵌入或预训练的语言模型)。
    • 步骤2:在向量存储中搜索与查询向量最相似的文档向量(通常使用余弦相似度或欧几里得距离等度量方法)。
    • 步骤3:返回与查询最相关的文档或文本片段,以及它们的相似度得分。

6.2 代码实现

  • Retriever一般和VectorStore配套实现,通过as_retriever()方法获取。

  • 举例:

    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    # 1.导入相关依赖
    from langchain_community.document_loaders import TextLoader
    from langchain_community.vectorstores import FAISS
    from langchain_openai import OpenAIEmbeddings
    from langchain_text_splitters import CharacterTextSplitter
    import os
    import dotenv
    dotenv.load_dotenv()


    # 2.定义文档加载器
    loader = TextLoader(file_path='./asset/load/09-ai1.txt',encoding="utf-8")

    # 3.加载文档
    documents = loader.load()

    # 4.定义文本切割器
    text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)

    # 5.切割文档
    docs = text_splitter.split_documents(documents)

    # 6.定义嵌入模型
    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
    embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large"
    )

    # 获取向量数据库
    db = FAISS.from_documents(documents=docs,embedding=embeddings)

    # 基于向量数据库获取检索器
    retriever = db.as_retriever()

    # 进行数据的检索
    docs = retriever.invoke(input = "深度学习是什么?")

    print(len(docs))

    for doc in docs:
    print(f"------{doc}")

    # 3
    # ------page_content='人工智能综述:发展、应用与未来展望
    #
    # 摘要
    # 人工智能(Artificial Intelligence,AI)作为计算机科学的一个重要分支,近年来取得了突飞猛进的发展。本文综述了人工智能的发展历程、核心技术、应用领域以及未来发展趋势。通过对人工智能的定义、历史背景、主要技术(如机器学习、深度学习、自然语言处理等)的详细介绍,探讨了人工智能在医疗、金融、教育、交通等领域的应用,并分析了人工智能发展过程中面临的挑战与机遇。最后,本文对人工智能的未来发展进行了展望,提出了可能的突破方向。
    #
    # 1. 引言
    # 人工智能是指通过计算机程序模拟人类智能的一门科学。自20世纪50年代诞生以来,人工智能经历了多次起伏,近年来随着计算能力的提升和大数据的普及,人工智能技术取得了显著的进展。人工智能的应用已经渗透到日常生活的方方面面,从智能手机的语音助手到自动驾驶汽车,从医疗诊断到金融分析,人工智能正在改变着人类社会的运行方式。
    #
    # 2. 人工智能的发展历程
    # 2.1 早期发展
    # 人工智能的概念最早可以追溯到20世纪50年代。1956年,达特茅斯会议(Dartmouth Conference)被认为是人工智能研究的正式开端。在随后的几十年里,人工智能研究经历了多次高潮与低谷。早期的研究主要集中在符号逻辑和专家系统上,但由于计算能力的限制和算法的不足,进展缓慢。
    # 2.2 机器学习的兴起
    # 20世纪90年代,随着统计学习方法的引入,机器学习逐渐成为人工智能研究的主流。支持向量机(SVM)、决策树、随机森林等算法在分类和回归任务中取得了良好的效果。这一时期,机器学习开始应用于数据挖掘、模式识别等领域。
    # 2.3 深度学习的突破
    # 2012年,深度学习在图像识别领域取得了突破性进展,标志着人工智能进入了一个新的阶段。深度学习通过多层神经网络模拟人脑的工作方式,能够自动提取特征并进行复杂的模式识别。卷积神经网络(CNN)、循环神经网络(RNN)和长短期记忆网络(LSTM)等深度学习模型在图像处理、自然语言处理、语音识别等领域取得了显著成果。' metadata={'source': './asset/load/09-ai1.txt'}
    # ------page_content='3. 人工智能的核心技术
    # 3.1 机器学习
    # 机器学习是人工智能的核心技术之一,通过算法使计算机从数据中学习并做出决策。常见的机器学习算法包括监督学习、无监督学习和强化学习。监督学习通过标记数据进行训练,无监督学习则从未标记数据中寻找模式,强化学习则通过与环境交互来优化决策。
    # 3.2 深度学习
    # 深度学习是机器学习的一个子领域,通过多层神经网络进行特征提取和模式识别。深度学习在图像识别、自然语言处理、语音识别等领域取得了显著成果。常见的深度学习模型包括卷积神经网络(CNN)、循环神经网络(RNN)和长短期记忆网络(LSTM)。
    # 3.3 自然语言处理
    # 自然语言处理(NLP)是人工智能的一个重要分支,致力于使计算机能够理解和生成人类语言。NLP技术广泛应用于机器翻译、情感分析、文本分类等领域。近年来,基于深度学习的NLP模型(如BERT、GPT)在语言理解任务中取得了突破性进展。
    # 3.4 计算机视觉
    # 计算机视觉是人工智能的另一个重要分支,致力于使计算机能够理解和处理图像和视频。计算机视觉技术广泛应用于图像识别、目标检测、人脸识别等领域。深度学习模型(如CNN)在计算机视觉任务中取得了显著成果。
    #
    # 4. 人工智能的应用领域
    # 4.1 医疗健康
    # 人工智能在医疗健康领域的应用包括疾病诊断、药物研发、个性化医疗等。通过分析医学影像和患者数据,人工智能可以帮助医生更准确地诊断疾病,提高治疗效果。
    # 4.2 金融
    # 人工智能在金融领域的应用包括风险评估、欺诈检测、算法交易等。通过分析市场数据和交易记录,人工智能可以帮助金融机构做出更明智的决策,提高运营效率。
    # 4.3 教育
    # 人工智能在教育领域的应用包括个性化学习、智能辅导、自动评分等。通过分析学生的学习数据,人工智能可以为学生提供个性化的学习建议,提高学习效果。
    # 4.4 交通
    # 人工智能在交通领域的应用包括自动驾驶、交通管理、智能导航等。通过分析交通数据和路况信息,人工智能可以帮助优化交通流量,提高交通安全。' metadata={'source': './asset/load/09-ai1.txt'}
    # ------page_content='5. 人工智能的挑战与机遇
    # 5.1 挑战
    # 人工智能发展过程中面临的主要挑战包括数据隐私、算法偏见、安全性问题等。数据隐私问题涉及到个人数据的收集和使用,算法偏见问题则涉及到算法的公平性和透明度,安全性问题则涉及到人工智能系统的可靠性和稳定性。
    # 5.2 机遇
    # 尽管面临挑战,人工智能的发展也带来了巨大的机遇。人工智能技术的进步将推动各行各业的创新,提高生产效率,改善生活质量。未来,人工智能有望在更多领域取得突破,为人类社会带来更多的便利和福祉。
    #
    # 6. 未来展望
    # 6.1 技术突破
    # 未来,人工智能技术有望在以下几个方面取得突破:一是算法的优化和创新,提高模型的效率和准确性;二是计算能力的提升,支持更复杂的模型和更大规模的数据处理;三是跨学科研究的深入,推动人工智能与其他领域的融合。
    # 6.2 应用拓展
    # 随着技术的进步,人工智能的应用领域将进一步拓展。未来,人工智能有望在更多领域发挥重要作用,如环境保护、能源管理、智能制造等。人工智能将成为推动社会进步的重要力量。
    #
    # 7. 结论
    # 人工智能作为一门快速发展的科学,正在改变着人类社会的运行方式。通过不断的技术创新和应用拓展,人工智能将为人类社会带来更多的便利和福祉。然而,人工智能的发展也面临着诸多挑战,需要社会各界共同努力,推动人工智能的健康发展。' metadata={'source': './asset/load/09-ai1.txt'}

6.3 使用相关检索策略

  • 前置代码:

    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    # 1.导入相关依赖
    from langchain_community.vectorstores import FAISS
    from langchain_openai import OpenAIEmbeddings
    from langchain_core.documents import Document

    # 2.定义文档
    document_1 = Document(
    page_content="经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!",
    )
    document_2 = Document(
    page_content="基础设施:政府将投资1万亿美元用于修复道路、桥梁和宽带网络。",
    )
    document_3 = Document(
    page_content="气候变化:承诺到2030年将温室气体排放量减少50%。",
    )
    document_4 = Document(
    page_content=" 医疗保健:降低处方药价格,扩大医疗保险覆盖范围。",
    )
    document_5 = Document(
    page_content="教育:提供免费的社区大学教育。。",
    )
    document_6 = Document(
    page_content="科技:增加对半导体产业的投资以减少对外国供应链的依赖。。",
    )
    document_7 = Document(
    page_content="外交政策:继续支持乌克兰对抗俄罗斯的侵略。",
    )
    document_8 = Document(
    page_content="枪支管制:呼吁国会通过更严格的枪支管制法律。",
    )
    document_9 = Document(
    page_content="移民改革:提出全面的移民改革方案。",
    )
    document_10 = Document(
    page_content="社会正义:承诺解决系统性种族歧视问题。",
    )
    documents = [
    document_1,
    document_2,
    document_3,
    document_4,
    document_5,
    document_6,
    document_7,
    document_8,
    document_9,
    document_10,
    ]

    # 3.创建向量存储
    embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large"
    )

    # 4.将文档向量化,添加到向量数据库索引中,得到向量数据库对象
    db = FAISS.from_documents(documents, embeddings)

①默认检索器使用相似性搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 获取检索器
retriever = db.as_retriever(search_kwargs={"k": 3}) #这里设置返回的文档数

docs = retriever.invoke("经济政策")

for i, doc in enumerate(docs):
print(f"\n结果 {i+1}:\n{doc.page_content}\n")

# 结果 1:
# 经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!
#
#
# 结果 2:
# 科技:增加对半导体产业的投资以减少对外国供应链的依赖。。
#
#
# 结果 3:
# 外交政策:继续支持乌克兰对抗俄罗斯的侵略。

②分数阈值查询

1
2
3
4
5
6
7
8
9
retriever = db.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.1}
)
docs = retriever.invoke("经济政策")

for doc in docs:
print(f"📌 内容: {doc.page_content}")
# 📌 内容: 经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!

注意只会返回满足阈值分数的文档,不会获取文档的得分。如果想查询文档的得分是否满足阈值,可以使用向量数据库的similarity_search_with_relevance_scores查看(在5.4.2情况5中讲过)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docs_with_scores = db.similarity_search_with_relevance_scores("经济政策")

for doc, score in docs_with_scores:
print(f"\n相似度分数: {score:.4f}")
print(f"📌 内容: {doc.page_content}")
# 相似度分数: 0.1015
# 📌 内容: 经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!
#
# 相似度分数: 0.0482
# 📌 内容: 科技:增加对半导体产业的投资以减少对外国供应链的依赖。。
#
# 相似度分数: 0.0451
# 📌 内容: 外交政策:继续支持乌克兰对抗俄罗斯的侵略。
#
# 相似度分数: 0.0274
# 📌 内容: 基础设施:政府将投资1万亿美元用于修复道路、桥梁和宽带网络。

③MMR搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
retriever = db.as_retriever(
search_type="mmr",
search_kwargs={"fetch_k":2}
)

docs = retriever.invoke("经济政策")

print(len(docs))

for doc in docs:
print(f"📌 内容: {doc.page_content}")
# 2
# 📌 内容: 经济复苏:美国经济正在从疫情中强劲复苏,失业率降至历史低点。!
# 📌 内容: 科技:增加对半导体产业的投资以减少对外国供应链的依赖。。

6.4 结合LLM

  • 举例1:通过FAISS构建一个可搜索的向量索引数据库,并结合RAG技术让LLM去回答问题。

    • 情况1:不用RAG给LLM灌输上下文数据。

      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
      from langchain_openai import ChatOpenAI
      import os
      import dotenv
      dotenv.load_dotenv()

      os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
      os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

      # 创建大模型实例
      llm = ChatOpenAI(model="gpt-4o-mini")

      # 调用
      response = llm.invoke("北京有什么著名的建筑?")
      print(response.content)
      # 北京作为中国的首都,拥有众多著名的建筑。以下是一些代表性的建筑:
      #
      # 1. **故宫**:汉族古代宫殿建筑的杰出代表,是世界上保存最完好的古代皇宫之一。
      #
      # 2. **天安门广场和天安门**:是中国的象征之一,广场是世界上最大的城市广场,天安门则是重要的历史和政治象征。
      #
      # 3. **长城**:虽然长城跨越多个省份,但北京境内的八达岭和慕田峪段是最为著名的游览区。
      #
      # 4. **颐和园**:中国古代皇家园林,以其精美的建筑和自然景观而闻名。
      #
      # 5. **鸟巢(国家体育场)**:2008年北京奥运会的主场馆,以其独特的建筑设计而受到广泛关注。
      #
      # 6. **水立方(国家游泳中心)**:同样是2008年奥运会的赛事场馆,以其独特的水泡外观而闻名。
      #
      # 7. **天坛**:明清时期皇帝祭天祈谷的地方,以其独特的建筑和园林设计而享有盛名。
      #
      # 8. **中轴线建筑群**:包括钟楼、鼓楼等,是北京城市规划的核心部分,具有重要的历史和文化价值。
      #
      # 9. **北京首都博物馆**:现代建筑的代表之一,展示了丰富的历史与文化。
      #
      # 10. **中国中央电视台总部大楼**(CCTV headquarters):以其独特的造型和现代化设计而著称。
      #
      # 这些建筑不仅展示了北京的历史与文化底蕴,也展现了现代建筑的魅力。
    • 情况2:使用RAG给LLM灌输上下文数据。

      1
      pip install faiss-cpu
      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
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      # 1. 导入所有需要的包
      from langchain.prompts import PromptTemplate
      from langchain_openai import ChatOpenAI,OpenAIEmbeddings
      from langchain_community.document_loaders import TextLoader
      from langchain.text_splitter import CharacterTextSplitter
      from langchain_community.vectorstores import FAISS
      import os
      import dotenv

      dotenv.load_dotenv()

      # 2. 创建自定义提示词模板
      prompt_template = """请使用以下提供的文本内容来回答问题。仅使用提供的文本信息,如果文本中没有相关信息,请回答"抱歉,提供的文本中没有这个信息"。

      文本内容:
      {context}

      问题:{question}

      回答:
      "
      """

      prompt = PromptTemplate.from_template(prompt_template)

      # 3. 初始化模型
      os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
      os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
      llm = ChatOpenAI(
      model="gpt-4o-mini",
      temperature=0
      )

      embedding_model = OpenAIEmbeddings(model="text-embedding-3-large")

      # 4. 加载文档
      loader = TextLoader("./asset/load/10-test_doc.txt", encoding='utf-8')
      documents = loader.load()

      # 5. 分割文档
      text_splitter = CharacterTextSplitter(
      chunk_size=1000,
      chunk_overlap=100,
      )
      texts = text_splitter.split_documents(documents)

      #print(f"文档个数:{len(texts)}")

      # 6. 创建向量存储
      vectorstore = FAISS.from_documents(
      documents=texts,
      embedding=embedding_model
      )

      # 7.获取检索器
      retriever = vectorstore.as_retriever()

      docs = retriever.invoke("北京有什么著名的建筑?")

      # 8. 创建Runnable链
      chain = prompt | llm

      # 9. 提问
      result = chain.invoke(input={"question":"北京有什么著名的建筑?","context":docs})
      print("\n回答:", result.content)
      # 回答: 北京有以下著名的建筑:
      #
      # 1. 故宫 - 明清两代的皇家宫殿,世界上现存规模最大、保存最完整的木质结构古建筑群之一。
      # 2. 天安门 - 北京的标志性建筑之一,天安门广场是世界上最大的城市广场。
      # 3. 颐和园 - 清朝时期的皇家园林,融合了江南园林的设计风格。
      # 4. 天坛 - 明清两代皇帝祭天、祈谷的场所,具有深厚的文化内涵。
      # 5. 长城(八达岭段) - 最著名的北京段,被誉为"世界第八大奇迹"。
      # 6. 国家体育场(鸟巢) - 2008年奥运会主体育场,以独特的钢结构设计闻名。
      # 7. 中央电视台总部大楼 - 现代北京最具争议和识别度的建筑之一。
      # 8. 国家大剧院 - 因其蛋壳造型被称为"巨蛋",是世界最大的穹顶建筑之一。
      # 9. 北京大兴国际机场 - 超大型国际航空枢纽,被誉为"新世界七大奇迹"之一。
      # 10. 鼓楼和钟楼 - 古代中国的计时中心,展现了古代的计时智慧。
  • 举例2:使用Chroma数据库 (与举例1类似)

    • 阶段1:文档的切分

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      ## 1. 文档加载
      from langchain_community.document_loaders import UnstructuredMarkdownLoader
      from langchain.text_splitter import MarkdownTextSplitter

      markdown_path = "asset/load/11-langchain.md"

      # 2.定义UnstructuredMarkdownLoader对象
      loader = UnstructuredMarkdownLoader(markdown_path)

      # 3.加载
      data = loader.load()

      splitter = MarkdownTextSplitter(chunk_size=1000, chunk_overlap=100)

      # 4.执行分割
      documents = splitter.split_documents(data)
      print(len(documents))
      for i, doc in enumerate(documents):
      print(f"\n🔍 分块 {i + 1}:")
      print(doc.page_content)
    • 阶段2:向量存储与检索

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      from langchain_openai import OpenAIEmbeddings
      from langchain_community.vectorstores import Chroma

      # 5. 获取嵌入模型
      import os
      import dotenv
      dotenv.load_dotenv()

      os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
      os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")
      embeddings = OpenAIEmbeddings()

      # 6. 向量数据存储(默认存储到内存中)
      db = Chroma.from_documents(documents, embeddings)

      # 7. 向量检索
      retriever = db.as_retriever()
      docs = retriever.invoke("what is Chat Models?")
      for i, doc in enumerate(docs):
      print(f"\n🔍 分块 {i + 1}:")
      print(doc.page_content)
    • 阶段3:

      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
      from langchain_openai import ChatOpenAI
      from langchain.prompts import PromptTemplate

      llm = ChatOpenAI(model="gpt-4o-mini")

      # 8.定义提示词模版
      prompt_template = """
      你是一个问答机器人。
      你的任务是根据下述给定的已知信息回答用户问题。
      确保你的回复完全依据下述已知信息。不要编造答案。
      如果下述已知信息不足以回答用户的问题,请直接回复"我无法回答您的问题"。

      已知信息:
      {info}

      用户问:
      {question}

      请用中文回答用户问题。
      """
      # 7.得到提示词模版对象
      template = PromptTemplate.from_template(prompt_template)

      # 8.得到提示词对象
      prompt = template.format(info=docs, question='what is Chat Models?')

      ## 9. 调用LLM
      response = llm.invoke(prompt)
      print(response.content)

7、项目:智能对话助手

7.1 需求分析

  • 我们将构建一个可以与多种不同工具进行交互的Agent:一个是本地数据库,另一个是搜索引擎。你能够向该Agent提问,观察它调用工具,并与它进行对话。
  • 涉及的功能:
    • 使用语言模型,特别是它们的工具调用能力
    • 创建检索器以向我们的Agent公开特定信息
    • 使用搜索工具在线查找信息
    • 提供聊天历史,允许聊天机器人“记住”不同id过去的交互,并在回答后续问题时考虑它们。

7.2 代码实现

7.2.1 定义工具

1
2
3
4
5
6
7
8
9
10
11
import os
from langchain_community.tools.tavily_search import TavilySearchResults

# 定义 AVILY_KEY 密钥
os.environ["TAVILY_API_KEY"] = "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"
# 查询 Tavily 搜索 API
search = TavilySearchResults(max_results=1)
# 执行查询
res = search.invoke("今天上海天气怎么样")
print(res)
# [{'title': '上海 - 中国气象局-天气预报-城市预报', 'url': 'https://weather.cma.cn/web/weather/58367.html', 'content': '| | | | | | | | | |\n --- --- --- --- \n| 时间 | 05:00 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 |\n| 天气 |\n| 气温 | 16.6℃ | 17.9℃ | 21.1℃ | 21.8℃ | 19.5℃ | 18.9℃ | 18.8℃ | 17.6℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.3m/s | 3.1m/s | 3.2m/s | 3.3m/s | 3m/s | 3m/s | 3.3m/s | 3.2m/s |\n| 风向 | 西北风 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 |\n| 气压 | 1024.4hPa | 1025.4hPa | 1025.1hPa | 1023.6hPa | 1023.9hPa | 1025.2hPa | 1025.2hPa | 1024hPa | [...] | | | | | | | | | |\n --- --- --- --- \n| 时间 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 | 05:00 |\n| 天气 |\n| 气温 | 17.8℃ | 21.3℃ | 20.8℃ | 20.4℃ | 19.6℃ | 17.6℃ | 15.8℃ | 15.8℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.2m/s | 3.2m/s | 3.3m/s | 3.3m/s | 3.1m/s | 3.3m/s | 3m/s | 2.9m/s |\n| 风向 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 |\n| 气压 | 1027.4hPa | 1026.6hPa | 1025.9hPa | 1026.4hPa | 1027hPa | 1026.6hPa | 1026.2hPa | 1026.4hPa | [...] | | | | | | | | | |\n --- --- --- --- \n| 时间 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 | 05:00 |\n| 天气 |\n| 气温 | 18.4℃ | 22.6℃ | 21.4℃ | 20.1℃ | 19.4℃ | 18.8℃ | 19.2℃ | 16.6℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.3m/s | 3.2m/s | 3.3m/s | 2.8m/s | 2.9m/s | 3.2m/s | 3.2m/s | 3.3m/s |\n| 风向 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 西北风 | 西北风 | 西北风 |\n| 气压 | 1022.2hPa | 1022hPa | 1019.8hPa | 1019.6hPa | 1020.3hPa | 1020.4hPa | 1019.9hPa | 1019.4hPa |', 'score': 0.7797419}]

7.2.2 定义Retriever

  • Retriever 是 langchain 库中的一个模块,用于检索工具。检索工具的主要用途是从大型文本集合或知识库中找到相关信息。它们通常用于问答系统、对话代理和其他需要从大量文本数据中提取信息的应用程序。

  • 根据上述查询结果中的某个URL中,获取一些数据创建一个检索器。

    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
    from langchain_community.document_loaders import WebBaseLoader
    from langchain_community.vectorstores import FAISS
    from langchain_openai import OpenAIEmbeddings
    from langchain_text_splitters import RecursiveCharacterTextSplitter
    import os
    import dotenv
    dotenv.load_dotenv()

    # 1. 提供一个大模型
    os.environ['OPENAI_API_KEY'] = os.getenv("OPENAI_API_KEY1")
    os.environ['OPENAI_BASE_URL'] = os.getenv("OPENAI_BASE_URL")

    embedding_model = OpenAIEmbeddings()

    # 2.加载HTML内容为一个文档对象
    loader = WebBaseLoader("https://zh.wikipedia.org/wiki/%E7%8C%AB")
    docs = loader.load()
    #print(docs)

    # 3.分割文档
    splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200
    )

    documents = splitter.split_documents(docs)

    # 4.向量化 得到向量数据库对象
    vector = FAISS.from_documents(documents, embedding_model)

    # 5.创建检索器
    retriever = vector.as_retriever()

    # 测试检索结果
    # print(retriever.invoke("猫的特征")[0])

7.2.3 创建工具、工具集

  • 前面,我们已经填充了我们将要进行Retriever的索引,我们可以轻松地将其转换为一个工具(代理程序正确使用所需的格式)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from langchain.tools.retriever import create_retriever_tool

    # 创建一个工具来检索文档
    retriever_tool = create_retriever_tool(
    retriever=retriever,
    name="wiki_search",
    description="搜索维基百科",
    )

    # 构建工具集
    tools = [search, retriever_tool]

7.2.4 语言模型调用工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

# 获取大模型
model = ChatOpenAI(model="gpt-4o-mini")

# 模型绑定工具
model_with_tools = model.bind_tools(tools)

# 根据输入自动调用工具
messages = [HumanMessage(content="今天上海天气怎么样")]
response = model_with_tools.invoke(messages)
print(f"ContentString: {response.content}")
print(f"ToolCalls: {response.tool_calls}")
# ContentString:
# ToolCalls: [{'name': 'tavily_search_results_json', 'args': {'query': '今天上海天气'}, 'id': 'call_vUQYGToeMLQs3ZPVUjaKWYuk', 'type': 'tool_call'}]
  • 我们可以看到现在没有内容,但有一个工具调用!它要求我们调用Tavily Search工具。
  • 这并不是在调用该工具 - 它只是告诉我们要调用。为了实际调用它,我们将创建我们的Agent程序。

7.2.5 创建Agent程序(使用通用方式)

  • 以FUNCTION CALL方式进行调用:

    1
    2
    3
    4
    5
    from langchain import hub
    prompt = hub.pull("hwchase17/openai-functions-agent")

    print(prompt.messages)
    # [SystemMessagePromptTemplate(prompt=PromptTemplate(input_variables=[], input_types={}, partial_variables={}, template='You are a helpful assistant'), additional_kwargs={}), MessagesPlaceholder(variable_name='chat_history', optional=True), HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['input'], input_types={}, partial_variables={}, template='{input}'), additional_kwargs={}), MessagesPlaceholder(variable_name='agent_scratchpad')]
  • 现在,我们可以使用LLM、提示和工具初始化Agent。Agent 负责接收输入并决定采取什么行动。关键的是,Agent 不执行这些操作 - 这是由AgentExecutor(下一步)完成的。

    1
    2
    3
    4
    5
    6
    7
    from langchain_classic.agents import create_tool_calling_agent, AgentExecutor

    # 创建Agent对象
    agent = create_tool_calling_agent(model, tools, prompt)

    # 创建AgentExecutor对象
    agent_executor = AgentExecutor(agent=agent, tools=tools,verbose=True)

7.2.6 运行Agent

  • 现在,我们可以在几个查询上运行Agent!请注意,目前这些都是无状态查询(它不会记住先前的交互)。

  • 首先,让我们看看当不需要调用工具时它如何回应:

    1
    print(agent_executor.invoke({"input": "猫的特征"}))
  • 现在让我们尝试一个需要调用搜索工具的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    print(agent_executor.invoke({"input": "今天上海天气怎么样"}))
    # > Entering new AgentExecutor chain...
    #
    # Invoking: `tavily_search_results_json` with `{'query': '上海天气'}`
    #
    #
    # [{'title': '上海 - 中国气象局-天气预报-城市预报', 'url': 'https://weather.cma.cn/web/weather/58367.html', 'content': '| | | | | | | | | |\n --- --- --- --- \n| 时间 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 | 05:00 |\n| 天气 |\n| 气温 | 18.4℃ | 22.6℃ | 21.4℃ | 20.1℃ | 19.4℃ | 18.8℃ | 19.2℃ | 16.6℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.3m/s | 3.2m/s | 3.3m/s | 2.8m/s | 2.9m/s | 3.2m/s | 3.2m/s | 3.3m/s |\n| 风向 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 西北风 | 西北风 | 西北风 |\n| 气压 | 1022.2hPa | 1022hPa | 1019.8hPa | 1019.6hPa | 1020.3hPa | 1020.4hPa | 1019.9hPa | 1019.4hPa | [...] | | | | | | | | | |\n --- --- --- --- \n| 时间 | 05:00 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 |\n| 天气 |\n| 气温 | 16.6℃ | 17.9℃ | 21.1℃ | 21.8℃ | 19.5℃ | 18.9℃ | 18.8℃ | 17.6℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.3m/s | 3.1m/s | 3.2m/s | 3.3m/s | 3m/s | 3m/s | 3.3m/s | 3.2m/s |\n| 风向 | 西北风 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 |\n| 气压 | 1024.4hPa | 1025.4hPa | 1025.1hPa | 1023.6hPa | 1023.9hPa | 1025.2hPa | 1025.2hPa | 1024hPa | [...] | | | | | | | | | |\n --- --- --- --- \n| 时间 | 08:00 | 11:00 | 14:00 | 17:00 | 20:00 | 23:00 | 02:00 | 05:00 |\n| 天气 |\n| 气温 | 17.8℃ | 21.3℃ | 20.8℃ | 20.4℃ | 19.6℃ | 17.6℃ | 15.8℃ | 15.8℃ |\n| 降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 | 无降水 |\n| 风速 | 3.2m/s | 3.2m/s | 3.3m/s | 3.3m/s | 3.1m/s | 3.3m/s | 3m/s | 2.9m/s |\n| 风向 | 西北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 | 东北风 |\n| 气压 | 1027.4hPa | 1026.6hPa | 1025.9hPa | 1026.4hPa | 1027hPa | 1026.6hPa | 1026.2hPa | 1026.4hPa |', 'score': 0.80625874}]今天上海的天气情况如下:
    #
    # - **天气状况**: 无降水
    # - **气温**:
    # - 08:00时:18.4℃
    # - 11:00时:22.6℃
    # - 14:00时:21.4℃
    # - 17:00时:20.1℃
    # - 20:00时:19.4℃
    # - 23:00时:18.8℃
    # - **风速**: 2.8m/s - 3.3m/s
    # - **风向**: 主要为东北风
    #
    # 无明显降水,非常适合外出。如果想要更详细的信息,可以访问[天气预报网站](https://weather.cma.cn/web/weather/58367.html)。
    #
    # > Finished chain.
    # {'input': '今天上海天气怎么样', 'output': '今天上海的天气情况如下:\n\n- **天气状况**: 无降水\n- **气温**: \n - 08:00时:18.4℃\n - 11:00时:22.6℃\n - 14:00时:21.4℃\n - 17:00时:20.1℃\n - 20:00时:19.4℃\n - 23:00时:18.8℃\n- **风速**: 2.8m/s - 3.3m/s\n- **风向**: 主要为东北风\n\n无明显降水,非常适合外出。如果想要更详细的信息,可以访问[天气预报网站](https://weather.cma.cn/web/weather/58367.html)。'}

7.2.7 添加记忆

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
from langchain_community.chat_message_histories import ChatMessageHistory

from langchain_core.chat_history import BaseChatMessageHistory

from langchain_core.runnables.history import RunnableWithMessageHistory

store = {}

# 调取指定session_id对应的memory
def get_session_history(session_id: str) -> BaseChatMessageHistory:

if session_id not in store:
store[session_id] = ChatMessageHistory()

return store[session_id]

agent_with_chat_history = RunnableWithMessageHistory(
runnable=agent_executor,
get_session_history=get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)

response = agent_with_chat_history.invoke(
{"input": "Hi,我的名字是Cyber"},
config={"configurable": {"session_id": "123"}},
)

print(response)
# > Entering new AgentExecutor chain...
# 你好,Cyber!很高兴见到你!有什么我可以帮助你的吗?
#
# > Finished chain.
# {'input': 'Hi,我的名字是Cyber', 'chat_history': [], 'output': '你好,Cyber!很高兴见到你!有什么我可以帮助你的吗?'}

继续提问:

1
2
3
4
5
6
7
8
9
10
11
response = agent_with_chat_history.invoke(
{"input": "我叫什么名字?"},
config={"configurable": {"session_id": "123"}},
)

print(response)
# > Entering new AgentExecutor chain...
# 你叫Cyber。有什么我能帮你的吗,Cyber?
#
# > Finished chain.
# {'input': '我叫什么名字?', 'chat_history': [HumanMessage(content='Hi,我的名字是Cyber', additional_kwargs={}, response_metadata={}), AIMessage(content='你好,Cyber!很高兴见到你!有什么我可以帮助你的吗?', additional_kwargs={}, response_metadata={})], 'output': '你叫Cyber。有什么我能帮你的吗,Cyber?'}

继续:

1
2
3
4
5
6
7
8
9
10
response = agent_with_chat_history.invoke(
{"input": "我叫什么名字?"},
config={"configurable": {"session_id": "4566"}},
)
print(response)
# > Entering new AgentExecutor chain...
# 抱歉,我无法知道您的名字。如果您愿意,可以告诉我您的名字!
#
# > Finished chain.
# {'input': '我叫什么名字?', 'chat_history': [], 'output': '抱歉,我无法知道您的名字。如果您愿意,可以告诉我您的名字!'}

8、项目:知识库问答助手(了解)

  • 大语言模型虽然可以很好地回答很多领域的各种问题,但是由于其知识是通过语言模型训练及指令微调等方式注入模型参数中的,因此针对本地知识库中的内容,大语言模型很难通过此前的方式有效地进行学习。通过 LangChain 框架,可以有效地融合本地知识库内容与大语言模型的知识问答能力。

  • 基于 LangChain 的知识库问答助手框架如下图所示。

  • 知识库问答助手的工作流程主要包含以下几个步骤。

    • 收集领域知识数据构造知识库,这些数据应当能够尽可能地全面覆盖问答需求。
    • 对知识库中的非结构数据进行文本提取和文本分割,得到文本块。
    • 利用嵌入向量表示模型给出文本块的嵌入表示,并利用向量数据库进行保存。
    • 根据用户输入信息的嵌入表示,通过向量数据库检索得到最相关的文本片段,将提示词模板与用户提交问题及历史消息合并输入大语言模型。
    • 将大语言模型结果返回给用户。
  • 代码:

    1
    2
    pip install docx2txt
    pip install langchain-chroma
    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
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    #导入必须的包
    # Import the required packages
    from langchain_community.document_loaders import UnstructuredExcelLoader, Docx2txtLoader, PyPDFLoader
    from langchain_text_splitters import CharacterTextSplitter
    from langchain_openai import OpenAIEmbeddings
    from langchain_chroma import Chroma
    #导入聊天所需的模块
    # Import the required packages for chat
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate


    #定义chatdoc
    # Define the ChatDoc class
    class ChatDoc():

    def __init__(self):
    self.doc = None
    self.splitText = [] #分割后的文本 split text
    self.template = [
    ("system",
    "你是一个处理文档的秘书,你从不说自己是一个大模型或者AI助手,你会根据下面提供的上下文内容来继续回答问题.\n 上下文内容\n {context} \n"),
    ("human", "你好!"),
    ("ai", "您好,我是尚硅谷秘书"),
    ("human", "{question}"),
    ]
    self.prompt = ChatPromptTemplate.from_messages(self.template)

    def getFile(self):
    doc = self.doc
    loaders = {
    "docx": Docx2txtLoader
    }
    file_extension = doc.split(".")[-1]
    loader_class = loaders.get(file_extension)
    if loader_class:
    try:
    loader = loader_class(doc)
    text = loader.load()
    return text
    except Exception as e:
    print(f"Error loading {file_extension} files:{e}")
    else:
    print(f"Unsupported file extension: {file_extension}")
    return None

    #处理文档的函数
    def splitSentences(self):
    full_text = self.getFile() #获取文档内容 get the content of the document
    if full_text != None:
    #对文档进行分割
    # Split the document
    text_split = CharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=100,
    separator="\n\n",
    length_function=len,
    is_separator_regex=False
    )
    texts = text_split.split_documents(full_text)
    self.splitText = texts

    #向量化与向量存储
    def embeddingAndVectorDB(self):
    # embeddings = OpenAIEmbeddings(
    # model="BAAI/bge-m3",
    # api_key=os.getenv("SILICON_API_KEY"),
    # base_url="https://api.siliconflow.cn/v1"
    # )
    embeddings = OpenAIEmbeddings()

    db = Chroma.from_documents(
    documents=self.splitText,
    embedding=embeddings,
    )

    return db

    #提问并找到相关的文本块
    def askAndFindFiles(self, question):
    db = self.embeddingAndVectorDB()
    print(db._collection.count())
    #retriever = db.as_retriever(search_type="mmr")
    retriever = db.as_retriever()
    return retriever.invoke(input=question)

    #用自然语言和文档聊天
    def chatWithDoc(self, question):
    _content = ""
    context = self.askAndFindFiles(question)
    for i in context:
    _content += i.page_content
    print(f"{_content}", "_content")
    messages = self.prompt.format_messages(context=_content, question=question)
    print("message:", messages)
    llm = ChatOpenAI(model="gpt-4",
    temperature=0,
    api_key=os.getenv("OPENAI_API_KEY1"),
    base_url=os.getenv("OPENAI_BASE_URL"))
    return llm.invoke(messages)


    chat_doc = ChatDoc()
    chat_doc.doc = "./asset/load/13-sgg_chat.docx"
    chat_doc.splitSentences()
    response = chat_doc.chatWithDoc("尚硅地址在哪")
    print(response.content)