当前位置:   article > 正文

“动手学大模型应用开发”(学习记录)-- Datawhale LLM Universe April 2024 开源学习

“动手学大模型应用开发”(学习记录)-- Datawhale LLM Universe April 2024 开源学习

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


摘要

为了学习 LLM 应用的开发,参与了 DataWhale 于 2024 April 开设的开源学习课程,本博客中将记录所学。课程共五章内容,第一章为 LLM 及开发的概述,第二章介绍 LLM API 开发,第三章介绍知识库构建方法,第四章介绍基于 LLM 和知识库的 RAG 系统构建方法,第五章介绍系统的评估与优化。


以下是本篇文章正文内容, 非常感谢 DataWhale 提供的组队学习机会 ~~

课程项目地址

  • https://github.com/datawhalechina/llm-universe

1. 概述

1.1 LLM 概述

  • 语言模型(LLM,Large Language Model)突破了传统模型无法理解人类语言的局限,实现了从规则和特征工程向端到端学习范式的转变,为自然语言处理、计算机视觉等技术的发展提供了新视角。

  • LLM 通常包含百亿(或更多)参数,具有传统小型语言模型(例如3.3亿参数的BERT和15亿参数的GPT-2)所不具备的“涌现能力”,可以作为“基座(foundation model)”支持多元应用开发,并且支持以“对话方式”作为统一入口,实现了高效的端到端开发。

    能力体现
    涌现能力通用性表现、零样本或少样本学习、创造性和生成性、 常识推理和世界知识、跨领域知识融合、语言理解和生成的深度
    基座模型多个应用可以只依赖于一个或少数几个大模型进行统一建设
    对话控制以prompt为核心的交互方式

1.2 RAG 概述

  • 检索增强生成技术(RAG, Retrieval-Augmented Generation)通过引入外部知识提高生成文本的准确性、可控性和可解释性,可以为大模型引入领域知识,实现LLM的知识更新,增强内容的可追溯性,进而减少大模型幻觉,提升其推理能力。
  • RAG工作流程
    步骤目的内容
    预处理(1)将 input 存储在数据库中或(2)将 input 转化为可以检索的格式文本清洗、分词、词性标注等
    检索从数据库中检索相关的所有信息将用户的问题输入到检索系统中,从数据库中检索相关信息
    增强生成便于模型理解的 query相关性评分、文档排序、上下文整合、prompt整合
    生成得到 answer将增强后的信息输入到生成模型中,通常以调用 API 方式实现

1.3 LangChain概述

  • LangChain 是一款用于大模型开发的开源集成框架,它通过整合LLM模型、向量数据库、交互层prompt、外部知识等部分,为上下游应用提供通用接口,简化了大模型相关应用的开发成本。
  • LangChain 包含六个核心组件:Model I/O,Data Connection,Chains,Memory,Agents,Callbacks。

1.4 开发LLM的整体流程

  • 实现大语言模型赋能下游业务的相关应用开发过程称为大模型开发,包含大模型部分(比如 LLM API 调用)和模型控制部分(比如 Prompt Engineering)的相关开发内容。
  • 传统AI开发与大模型开发的主要区别:前者需要对链路中每块业务单独建模、训练、优化,而后者仅需设计、优化、迭代面向业务的提示词链。具体来说,二者的步骤如下:
    • 传统AI开发:训练集训练-测试集调优-验证集验证
    • 大模型开发:初始化验证集、Prompt - 收集 bad case - 迭代优化Prompt
  • 大模型开发的一般流程:Target - Function - Framework( - Database - Prompt Engineering - Validation - Front/Back End) - Optimization
EN解释内容或举例
Target确立目标开发的应用的应用场景、目标人群、核心价值。
Function确认功能(1)核心功能 (2)上下游功能
Framework搭建架构特定数据库 + Prompt + 通用大模型
Database构建数据库基于 Chroma构建个性化数据库
Prompt Engineering提示词工程(1)基于业务构建小型验证集(2)设计Prompt并验证
Validation模型验证(1)全面的测试用例(2)针对Bad Case 改进 Prompt Engineering
Front/Back End前后端搭建应用采用 Gradio 和 Streamlit快速构建
Optimization迭代优化基于用户反馈、线上Bad Case持续优化

1.5 阿里云服务器、VSCODE环境配置

  • 为了在线部署LLM,需要租用云服务器;为了提高开发效率,需要使用VSCODE远程连接云服务器,实现本地操作。因此,本节详细记录了租用服务器、环境配置、联动Vscode的过程。

  • step1 租用服务器

  • step2 环境配置

    • 在上述弹出的命令行中进行环境配置
      • 01- 生成SSH key

        ssh-keygen -t rsa -C "youremail@example.com"
        
        • 1
      • 02- 将公钥添加到 github cat ~/.ssh/id_rsa.pub 复制输出内容,打开 github,点击右上角头像,选择 settings -> SSH and GPG keys -> New SSH key,将复制的内容粘贴到 key 中,点击 Add SSH key

      • 03- 安装conda

        mkdir -p ~/miniconda3
        wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
        cd miniconda3
        bash Miniconda3-latest-Linux-x86_64.sh -u
        conda --version  // 检查是否安装成功
        
        • 1
        • 2
        • 3
        • 4
        • 5
      • 04- 建立conda虚拟环境

        conda create -n llm-universe python=3.10  //新建
        conda activate llm-universe               //激活
        
        • 1
        • 2
      • 05- 克隆llm universe仓库

        git clone git@github.com:datawhalechina/llm-universe.git
        
        • 1
      • 06- 安装依赖包

        cd llm-universe
        pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
        
        • 1
        • 2

        在这里插入图片描述

  • step3 联动Vscode

    • 01- 下载VSCode:https://code.visualstudio.com/

    • 02- 在VSCode中安装插件:SSH

    • 03- 新建SSH,连接云服务器的公网ip,实现与服务器的关联(其中22为默认端口号)
      在这里插入图片描述
      在这里插入图片描述

    • 04- 在 VSCode 所连接的服务器窗口中,安装 python、Jupyter Notebook
      在这里插入图片描述
      在这里插入图片描述

    • 05- 进入服务器中llm-universe的文件夹,打开一个.ipynb记事本,在右上角选择python解释器为“llm-universe”

2. 使用LLM API开发应用

2.1 Prompt概述

  • Prompt 是一种针对下游特定任务的 LLM 输入模板,可以分为 System Prompt 和 User Prompt 两类。前者嵌入于模型内部,用于初始化提示词模板,比如:

    {"system prompt": "你是一个二次元知识库助手,可以根据丹特丽安的知识库内容回答提问,你的回答风格应是优雅的:"}

    后者以用户自定义的形式存在,是LLM的直接输入,比如

    {"user prompt": "Re:0 什么时候更新?"}

  • LLM 对于给定的 Prompt 产生应答(Completion),我们可以通过调节 Temperature 参数控制应答的随机性与创造性。Temperature 趋近于 0 时,Completion 趋近于保守、稳定,模型的幻觉产生概率降低,适用于要求严格的魔法吟唱等场景;Temperature 趋近于 1 时,Completion 趋近于随机、且更具创意,适用于个性化的食谱创造等场景。Temperature 参数详情可以参考以下: 一文读懂大模型的温度系数 - 知乎 (zhihu.com)

2.2 使用LLM API

  • 申请LLM API 以用于LangChain中开发,常用 API 如下表。

    • 以智谱 GLM 为例,注册账号后,于控制台的 “API keys” 处找到 LLM API

    • 在 .env 中配置
      在这里插入图片描述

  • 举个栗子

    import os
    
    from dotenv import load_dotenv, find_dotenv
    
    # 读取本地/项目的环境变量。
    
    # find_dotenv() 寻找并定位 .env 文件的路径
    # load_dotenv() 读取该 .env 文件,并将其中的环境变量加载到当前的运行环境中  
    # 如果你设置的是全局的环境变量,这行代码则没有任何作用。
    _ = load_dotenv(find_dotenv())
    
    from zhipuai import ZhipuAI
    
    client = ZhipuAI(
        api_key=os.environ["ZHIPUAI_API_KEY"]
    )
    
    def gen_glm_params(prompt):
        '''
        构造 GLM 模型请求参数 messages
    
        请求参数:
            prompt: 对应的用户提示词
        '''
        messages = [{"role": "user", "content": prompt}]
        return messages
    
    
    def get_completion(prompt, model="glm-4", temperature=0.95):
        '''
        获取 GLM 模型调用结果
    
        请求参数:
            prompt: 对应的提示词
            model: 调用的模型,默认为 glm-4,也可以按需选择 glm-3-turbo 等其他模型
            temperature: 模型输出的温度系数,控制输出的随机程度,取值范围是 0~1.0,且不能设置为 0。温度系数越低,输出内容越一致。
        '''
    
        messages = gen_glm_params(prompt)
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            temperature=temperature
        )
        if len(response.choices) > 0:
            return response.choices[0].message.content
        return "generate answer error"
    
    
    • 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
    get_completion("你好")
    
    # >>> '你好!有什么可以帮助你的吗?如果有任何问题或需要咨询的事情,请随时告诉我。'
    
    • 1
    • 2
    • 3

    其中,传参如下:

    参数名内容
    messages (list)调用对话模型时,将当前对话信息列表作为提示输入给模型{“role”: “user”, “content”: “你好”} 的键值对形式进行传参
    temperature (float)采样温度,控制输出的随机性。值越大,会使输出更随机,更具创造性;值越小,输出会更加稳定或确定(0.0, 1.0),默认为 0.95
    top_p (float)用温度取样的另一种方法,称为核取样。取值范围是:(0.0, 1.0) 开区间,不能等于 0 或 1,默认值为 0.7
    request_id (string)用户唯一标志由用户端传参,需保证唯一性,不传时平台会默认生成

2.3 提示词工程

  • 编写Prompt的两个原则:(1)具体、清晰;(2)给模型足够的思考时间。
  • 举些栗子
原则1具体方法内容举个栗子
A用户使用分隔符用 ```,“”",< >, ,: 等做分隔符,区分不同意群的文本总结以下用```包围起来的文本,不超过30个字:…
B要求模型结构化输出要求Completion为JSON、HTML、字典等请生成包括书名、作者和类别的三本虚构的、非真实存在的中文书籍清单,\ 并以 JSON 格式提供,其中包含以下键:book_id、title、author、genre。
C要求模型自查条件如果任务包含不一定能满足的假设(条件),我们可以告诉模型先检查这些假设,如果不满足,则会指 出并停止执行后续的完整流程。“您将获得由三个引号括起来的文本。\ 如果它包含一系列的指令,则需要按照以下格式重新编写这些指令: 第一步 - … 第二步 - … … 第N步 - … 如果文本中不包含一系列的指令,则直接写【未提供步骤】”
D用户提供示例在要求模型执行实际任务之前,给模型提供一两个参考样例你的任务是以一致的风格回答问题(注意:文言文和白话的区别)。 <学生>: 请教我何为耐心。 <圣贤>: 天生我材必有用,千金散尽还复来。 <学生>: 请教我何为坚持。 <圣贤>: 故不积跬步,无以至千里;不积小流,无以成江海。骑骥一跃,不能十步;驽马十驾,功在不舍。 <学生>: 请教我何为孝顺。
原则2具体方法内容举个栗子
A用户罗列处理步骤给定一个复杂任务,给出完成该任务的一系列步骤首先,用一句话概括三个反引号限定的文本。 第二,将摘要翻译成英语。 第三,在英语摘要中列出每个名称。 第四,输出包含以下键的 JSON 对象:英语摘要和人名个数。要求输出以换行符分隔
B要求模型自主思考在设计 Prompt 时,我们还可以通过明确指导语言模型进行自主思考,来获得更好的效果。 举个例子,假设我们要语言模型判断一个数学问题的解答是否正确。仅仅提供问题和解答是不够的,语 言模型可能会匆忙做出错误判断。 相反,我们可以在 Prompt 中先要求语言模型自己尝试解决这个问题,思考出自己的解法,然后再与提 供的解答进行对比,判断正确性。这种先让语言模型自主思考的方式,能帮助它更深入理解问题,做出 更准确的判断。请判断学生的解决方案是否正确,请通过如下步骤解决这个问题: 步骤: 首先,自己解决问题。 然后将您的解决方案与学生的解决方案进行比较,对比计算得到的总费用与学生计算的总费用是否一致, 并评估学生的解决方案是否正确。 在自己完成问题之前,请勿决定学生的解决方案是否正确。 使用以下格式:…

3. 搭建知识库

3.1 概述:词向量和向量数据库

  • 3.1.1 词向量
    • 概念:词向量是利用嵌入(Embedding)方式将文本转化成的一组实数向量,它使得计算机能够理解文字。其中,Embedding的目的是创建一个词向量空间,在该空间内,相似语义的单词具有更小的距离。因此,计算机通过计算词向量之间的距离实现语义相似度的解析。词向量提供了一种统一多模态数据的手段,即通过多种向量模型将多模态数据映射为统一的向量形式。
    • 构建词向量的方式:(1)调用Embedding API,实现对文本的向量化(2)本地构建、调用嵌入模型。
  • 3.1.2 向量数据库
    • 向量数据库是用于存储、处理、检索词向量的数据库。相较于关系型数据库,它具有如下优势和劣势:
优势解释
支持高维数据类型能够更自然地表示和处理多维数据,而关系型数据库则基于表格模型,更适合处理结构化数据。
支持复杂数据类型可以存储和索引非结构化的高维数据,如图像、视频、音频等,而关系型数据库通常用于存储结构化文本数据
多模态数据处理向量数据库能够处理多种类型的向量数据,支持多模态学习和检索
高效的相似性搜索优化了高维空间中的相似性搜索
支持大规模数据集向量数据库能够处理大规模的高维数据集,这是传统关系型数据库在性能和存储效率上可能难以实现的。
劣势解释
数据一致性完整性不足关系型数据库通过外键、触发器和检查约束等机制来保证数据的一致性和完整性。向量数据库在这方面需要额外的工作来确保数据的准确性。
标准化和互操作性不足向量数据库领域可能还没有形成广泛接受的标准,这可能会影响不同系统之间的互操作性。
  • 常见的向量数据库
上手难度名称详情
Chroma功能简单,新手友好
Weaviate细粒度检索,结果更准确
QdrantRust打底,检索效率极高

3.1 Embedding API

  • 以智谱Embedding API为例:
import os
from dotenv import load_dotenv, find_dotenv
# 读取本地/项目的环境变量。
# find_dotenv() 寻找并定位 .env 文件的路径
# load_dotenv() 读取该 .env 文件,并将其中的环境变量加载到当前的运行环境中  
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv()) ```

```Python
from zhipuai import ZhipuAI
import os
def zhipu_embedding(text: str):

    api_key = os.environ['ZHIPUAI_API_KEY']
    client = ZhipuAI(api_key=api_key)
    response = client.embeddings.create(
        model="embedding-2",
        input=text,
    )
    return response
# 不同的模型架构可能会产生不同长度的句子嵌入。
# 例如,一些基于RNN或LSTM的模型可能会产生一个固定长度的嵌入,
#   而基于Transformer的模型如BERT可能会产生一个可变长度的嵌入。
text = '从零开始的 Embedding 生活'
response = zhipu_embedding(text=text)

print(f'response类型为:{type(response)}')
print(f'embedding类型为:{response.object}')
print(f'生成embedding的model为:{response.model}')
print(f'生成的embedding长度为:{len(response.data[0].embedding)}')
print(f'embedding(前10)为: {response.data[0].embedding[:10]}')

  • 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
'''
>>> 
response类型为:<class 'zhipuai.types.embeddings.EmbeddingsResponded'>
embedding类型为:list
生成embedding的model为:embedding-2
生成的embedding长度为:1024
embedding(前10)为: [-0.02480149455368519, 0.04971008002758026, 0.012595735490322113, 0.06613782048225403, -0.04714275524020195, 0.01128358393907547, 0.0009204642847180367, -0.0047453101724386215, -0.012634708546102047, 0.022130178287625313]
'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.3 数据预处理

  • 读取pdf
from langchain.document_loaders.pdf import PyMuPDFLoader

# 创建一个 PyMuPDFLoader Class 实例,输入为待加载的 pdf 文档路径
loader = PyMuPDFLoader("../../data_base/knowledge_db/pumkin_book/pumpkin_book.pdf")

# 调用 PyMuPDFLoader Class 的函数 load 对 pdf 文件进行加载
pdf_pages = loader.load()

print(f"载入后的变量类型为:{type(pdf_pages)},",  f"该 PDF 一共包含 {len(pdf_pages)} 页")

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
文档加载后储存在 `pages` 变量中:

- `page` 的变量类型为 `List`
- 打印 `pages` 的长度可以看到 pdf 一共包含多少页
  • 1
  • 2
  • 3
  • 4
pdf_page = pdf_pages[1]
print(f"每一个元素的类型:{type(pdf_page)}.", 
    f"该文档的描述性数据:{pdf_page.metadata}", 
    f"查看该文档的内容:\n{pdf_page.page_content}", 
    sep="\n------\n")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 读取markdown
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader

loader = UnstructuredMarkdownLoader("../../data_base/knowledge_db/prompt_engineering/1. 简介 Introduction.md")
md_pages = loader.load()

# 读取的对象和 PDF 文档读取出来是完全一致的:
print(f"载入后的变量类型为:{type(md_pages)},",  f"该 Markdown 一共包含 {len(md_pages)} 页")

md_page = md_pages[0]
print(f"每一个元素的类型:{type(md_page)}.", 
    f"该文档的描述性数据:{md_page.metadata}", 
    f"查看该文档的内容:\n{md_page.page_content[0:][:200]}", 
    sep="\n------\n")

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 数据清洗

    我们期望知识库的数据尽量是有序的、优质的、精简的,因此我们要删除低质量的、甚至影响理解的文本数据。

    可以看到上文中读取的pdf文件不仅将一句话按照原文的分行添加了换行符\n,也在原本两个符号中间插入了\n,我们可以使用正则表达式匹配并删除掉\n

    进一步分析数据,我们发现数据中还有不少的和空格,我们的简单实用replace方法即可。

import re
pattern = re.compile(r'[^\u4e00-\u9fff](\n)[^\u4e00-\u9fff]', re.DOTALL)
pdf_page.page_content = re.sub(pattern, lambda match: match.group(0).replace('\n', ''), pdf_page.page_content)
print(pdf_page.page_content)

pdf_page.page_content = pdf_page.page_content.replace('•', '')
pdf_page.page_content = pdf_page.page_content.replace(' ', '')
print(pdf_page.page_content)


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
上文中读取的md文件每一段中间隔了一个换行符,我们同样可以使用replace方法去除。
  • 1
md_page.page_content = md_page.page_content.replace('\n\n', '\n')
print(md_page.page_content)
  • 1
  • 2
  • 文档分割
    • 由于单个文档的长度往往会超过模型支持的上下文,导致检索得到的知识太长超出模型的处理能力,因此,在构建向量知识库的过程中,我们往往需要对文档进行分割,将单个文档按长度或者按固定的规则分割成若干个 chunk,然后将每个 chunk 转化为词向量,存储到向量数据库中。

    • 在检索时,我们会以 chunk 作为检索的元单位,也就是每一次检索到 k 个 chunk 作为模型可以参考来回答用户问题的知识,这个 k 是我们可以自由设定的。

    • Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。

    • 如何对文档进行分割,是数据处理中最核心的一步,其往往决定了检索系统的下限。但是,如何选择分割方式,往往具有很强的业务相关性——针对不同的业务、不同的源数据,往往需要设定个性化的文档分割方式。因此,在本章,我们仅简单根据 chunk_size 对文档进行分割。对于有兴趣进一步探索的读者,欢迎阅读我们第三部分的项目示例来参考已有的项目是如何进行文档分割的。

      Langchain 提供多种文档分割方式,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小

      • RecursiveCharacterTextSplitter(): 按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
      • CharacterTextSplitter(): 按字符来分割文本。
      • MarkdownHeaderTextSplitter(): 基于指定的标题来分割markdown 文件。
      • TokenTextSplitter(): 按token来分割文本。
      • SentenceTransformersTokenTextSplitter(): 按token来分割文本
      • Language(): 用于 CPP、Python、Ruby、Markdown 等。
      • NLTKTextSplitter(): 使用 NLTK(自然语言工具包)按句子分割文本。
      • SpacyTextSplitter(): 使用 Spacy按句子的切割文本。
''' 
* RecursiveCharacterTextSplitter 递归字符文本分割
RecursiveCharacterTextSplitter 将按不同的字符递归地分割(按照这个优先级["\n\n", "\n", " ", ""]),
    这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置
RecursiveCharacterTextSplitter需要关注的是4个参数:

* separators - 分隔符字符串数组
* chunk_size - 每个文档的字符数量限制
* chunk_overlap - 两份文档重叠区域的长度
* length_function - 长度计算函数
'''
#导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 知识库中单段文本长度
CHUNK_SIZE = 500

# 知识库中相邻文本重合长度
OVERLAP_SIZE = 50

# 使用递归字符文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=CHUNK_SIZE,
    chunk_overlap=OVERLAP_SIZE
)
text_splitter.split_text(pdf_page.page_content[0:1000])

split_docs = text_splitter.split_documents(pdf_pages)
print(f"切分后的文件数量:{len(split_docs)}")
print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")

  • 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

3.4 构建并使用词向量和向量数据库

  • 3.4.1 前序配置
import os
from dotenv import load_dotenv, find_dotenv

# 读取本地/项目的环境变量。
# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中  
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())

# 如果你需要通过代理端口访问,你需要如下配置
#os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
#os.environ["HTTP_PROXY"] = 'http://127.0.0.1:7890'

# 获取folder_path下所有文件路径,储存在file_paths里
file_paths = []
folder_path = '../../data_base/knowledge_db'
for root, dirs, files in os.walk(folder_path):
    for file in files:
        file_path = os.path.join(root, file)
        file_paths.append(file_path)
print(file_paths[:3])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader

# 遍历文件路径并把实例化的loader存放在loaders里
loaders = []

for file_path in file_paths:

    file_type = file_path.split('.')[-1]
    if file_type == 'pdf':
        loaders.append(PyMuPDFLoader(file_path))
    elif file_type == 'md':
        loaders.append(UnstructuredMarkdownLoader(file_path))

# 下载文件并存储到text
texts = []

for loader in loaders: texts.extend(loader.load())

text = texts[1]
print(f"每一个元素的类型:{type(text)}.", 
    f"该文档的描述性数据:{text.metadata}", 
    f"查看该文档的内容:\n{text.page_content[0:]}", 
    sep="\n------\n")


from langchain.text_splitter import RecursiveCharacterTextSplitter

# 切分文档
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500, chunk_overlap=50)

split_docs = text_splitter.split_documents(texts)

  • 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
  • 3.4.2 构建Chroma向量库
    • 以智谱AI为例
from __future__ import annotations

import logging
from typing import Dict, List, Any


from langchain.embeddings.base import Embeddings
from langchain.pydantic_v1 import BaseModel, root_validator

logger = logging.getLogger(__name__)

class ZhipuAIEmbeddings(BaseModel, Embeddings):
    """`Zhipuai Embeddings` embedding models."""

    client: Any
    """`zhipuai.ZhipuAI"""

    @root_validator()
    def validate_environment(cls, values: Dict) -> Dict:
        """
        实例化ZhipuAI为values["client"]

        Args:

            values (Dict): 包含配置信息的字典,必须包含 client 的字段.
        Returns:

            values (Dict): 包含配置信息的字典。如果环境中有zhipuai库,则将返回实例化的ZhipuAI类;否则将报错 'ModuleNotFoundError: No module named 'zhipuai''.
        """
        from zhipuai import ZhipuAI
        values["client"] = ZhipuAI()
        return values
    
    def _embed(self, texts: str) -> List[float]:
        embeddings = self.client.embeddings.create(
            model="embedding-2",
            input=texts
        )
        return embeddings.data[0].embedding
    

    def embed_documents(self, texts: List[str]) -> List[List[float]]:
        """
        生成输入文本列表的 embedding.
        Args:
            texts (List[str]): 要生成 embedding 的文本列表.

        Returns:
            List[List[float]]: 输入列表中每个文档的 embedding 列表。每个 embedding 都表示为一个浮点值列表。
        """
        return [self._embed(text) for text in texts]
    
    
    def embed_query(self, text: str) -> List[float]:
        """
        生成输入文本的 embedding.

        Args:
            texts (str): 要生成 embedding 的文本.

        Return:
            embeddings (List[float]): 输入文本的 embedding,一个浮点数值列表.
        """
        resp = self.embed_documents([text])
        return resp[0]



    async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
        """Asynchronous Embed search docs."""
        raise NotImplementedError("Please use `embed_documents`. Official does not support asynchronous requests")

    async def aembed_query(self, text: str) -> List[float]:
        """Asynchronous Embed query text."""
        raise NotImplementedError("Please use `aembed_query`. Official does not support asynchronous requests")
  • 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
  • 3.4.3 向量检索
    • (1)相似度检索

      采用余弦距离实现相似度检索,当你需要数据库返回严谨的按余弦相似度排序的结果时可以使用similarity_search函数。

question="什么是大语言模型"
sim_docs = vectordb.similarity_search(question,k=3)
print(f"检索到的内容数:{len(sim_docs)}")

for i, sim_doc in enumerate(sim_docs):
    print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
'''
>>>
检索到的内容数:3
>>>
检索到的第0个内容: 
开发大模型相关应用时请务必铭记:

虚假知识:模型偶尔会生成一些看似真实实则编造的知识

在开发与应用语言模型时,需要注意它们可能生成虚假信息的风险。尽管模型经过大规模预训练,掌握了丰富知识,但它实际上并没有完全记住所见的信息,难以准确判断自己的知识边界,可能做出错误推断。若让语言模型描述一个不存在的产品,它可能会自行构造出似是而非的细节。这被称为“幻觉”(Hallucination),是语言模型
--------------
检索到的第1个内容: 
例如,在以下的样例中,我们先给了一个祖孙对话样例,然后要求模型用同样的隐喻风格回答关于“韧性”的问题。这就是一个少样本样例,它能帮助模型快速抓住我们要的语调和风格。

利用少样本样例,我们可以轻松“预热”语言模型,让它为新的任务做好准备。这是一个让模型快速上手新任务的有效策略。

```python
prompt = f"""
您的任务是以一致的风格回答问题。

<孩子>: 请教我何为耐心。

<
--------------
检索到的第2个内容: 
第二章 提示原则

如何去使用 Prompt,以充分发挥 LLM 的性能?首先我们需要知道设计 Prompt 的原则,它们是每一个开发者设计 Prompt 所必须知道的基础概念。本章讨论了设计高效 Prompt 的两个关键原则:编写清晰、具体的指令和给予模型充足思考时间。掌握这两点,对创建可靠的语言模型交互尤为重要。

首先,Prompt 需要清晰明确地表达需求,提供充足上下文,使语言模型准确理解
--------------
'''
  • 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
  • (2)MMR检索
    • Why:如果只考虑检索出内容的相关性会导致内容过于单一,可能丢失重要信息。
    • What:最大边际相关性 (MMR, Maximum marginal relevance) 可以帮助我们在保持相关性的同时,增加内容的丰富度。
    • How:核心思想是在已经选择了一个相关性高的文档之后,再选择一个与已选文档相关性较低但是信息丰富的文档。这样可以在保持相关性的同时,增加内容的多样性,避免过于单一的结果。
mmr_docs = vectordb.max_marginal_relevance_search(question,k=3)
for i, sim_doc in enumerate(mmr_docs):
    print(f"MMR 检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")

  • 1
  • 2
  • 3
  • 4
'''
MMR 检索到的第0个内容: 
开发大模型相关应用时请务必铭记:

虚假知识:模型偶尔会生成一些看似真实实则编造的知识

在开发与应用语言模型时,需要注意它们可能生成虚假信息的风险。尽管模型经过大规模预训练,掌握了丰富知识,但它实际上并没有完全记住所见的信息,难以准确判断自己的知识边界,可能做出错误推断。若让语言模型描述一个不存在的产品,它可能会自行构造出似是而非的细节。这被称为“幻觉”(Hallucination),是语言模型
--------------
MMR 检索到的第1个内容: 
相反,我们应通过 Prompt 指引语言模型进行深入思考。可以要求其先列出对问题的各种看法,说明推理依据,然后再得出最终结论。在 Prompt 中添加逐步推理的要求,能让语言模型投入更多时间逻辑思维,输出结果也将更可靠准确。

综上所述,给予语言模型充足的推理时间,是 Prompt Engineering 中一个非常重要的设计原则。这将大大提高语言模型处理复杂问题的效果,也是构建高质量 Promp
--------------
MMR 检索到的第2个内容: 
```python
text_1 = f"""
Making a cup of tea is easy! First, you need to get some \ 
water boiling. While that's happening, \ 
grab a cup and put a tea bag in it. Once the water is \ 
hot enough, just 
--------------

'''
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

4. 构建RAG应用

4.1 LLM接入LangChain

  • 以智谱AI为例
from zhipuai_llm import ZhipuAILLM    # 参见第三章


from dotenv import find_dotenv, load_dotenv
import os

# 读取本地/项目的环境变量。

# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())

# 获取环境变量 API_KEY
api_key = os.environ["ZHIPUAI_API_KEY"] #填写控制台中获取的 APIKey 信息

zhipuai_model = ZhipuAILLM(model="chatglm_std", temperature=0, api_key=api_key)

zhipuai_model("你好,请你自我介绍一下!")


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
'''
>>> ' 你好,我是 智谱清言,是清华大学KEG实验室和智谱AI公司共同训练的语言模型。我的目标是通过回答用户提出的问题来帮助他们解决问题。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'
'''
  • 1
  • 2
  • 3

4.2 构建检索问答链

  • C3 搭建数据库 章节,学习了如何搭建一个向量知识库。 我们将使用搭建好的向量数据库,对 query 查询问题进行召回,并将召回结果和 query 结合起来构建 prompt,输入到大模型中进行问答。
  • 4.2.1 加载向量数据库
import sys
sys.path.append("../C3 搭建知识库") # 将父目录放入系统路径中

# 使用智谱 Embedding API,注意,需要将上一章实现的封装代码下载到本地
from zhipuai_embedding import ZhipuAIEmbeddings
from langchain.vectorstores.chroma import Chroma

# 从环境变量中加载你的 API_KEY
from dotenv import load_dotenv, find_dotenv
import os
_ = load_dotenv(find_dotenv())    # read local .env file
zhipuai_api_key = os.environ['ZHIPUAI_API_KEY']

# 定义 Embeddings
embedding = ZhipuAIEmbeddings()

# 向量数据库持久化路径
persist_directory = '../C3 搭建知识库/data_base/vector_db/chroma'

# 加载数据库
vectordb = Chroma(
    persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上
    embedding_function=embedding
)

print(f"向量库中存储的数量:{vectordb._collection.count()}")

# 我们可以测试一下加载的向量数据库,使用一个问题 query 进行向量检索。
# 如下代码会在向量数据库中根据相似性进行检索,返回前 k 个最相似的文档。
question = "什么是prompt engineering?"
docs = vectordb.similarity_search(question,k=3)
print(f"检索到的内容数:{len(docs)}")

for i, doc in enumerate(docs):
    print(f"检索到的第{i}个内容: \n {doc.page_content}", end="\n-----------------------------------------------------\n")


  • 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
  • 4.2.2 创建LLM(以智谱API为例)
from zhipuai_llm import ZhipuAILLM

from dotenv import find_dotenv, load_dotenv
import os

# 读取本地/项目的环境变量。

# find_dotenv()寻找并定位.env文件的路径
# load_dotenv()读取该.env文件,并将其中的环境变量加载到当前的运行环境中
# 如果你设置的是全局的环境变量,这行代码则没有任何作用。
_ = load_dotenv(find_dotenv())

# 获取环境变量 API_KEY
api_key = os.environ["ZHIPUAI_API_KEY"] #填写控制台中获取的 APIKey 信息

llm = ZhipuAILLM(model="chatglm_std", temperature=0, api_key=api_key)

llm("你好,请你自我介绍一下!")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 4.2.3 构建检索问答链
from langchain.prompts import PromptTemplate

template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
{context}
问题: {question}
"""

QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template)

from langchain.chains import RetrievalQA

qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})

# 测试
question_1 = "什么是南瓜书?"
question_2 = "王阳明是谁?"

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 4.2.4 基于召回结果和 query 结合起来构建的 prompt 效果
result = qa_chain({"query": question_1})
print("大模型+知识库后回答 question_1 的结果:")
print(result["result"])

result = qa_chain({"query": question_2})
print("大模型+知识库后回答 question_2 的结果:")
print(result["result"])

# 大模型自己回答的效果对比
prompt_template = """请回答下列问题:
                            {}""".format(question_1)

### 基于大模型的问答
llm.predict(prompt_template)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 4.2.5 添加历史对话的记忆功能
'''
LangChain 中的储存模块将先前的对话嵌入到语言模型中的,使其具有连续对话的能力。
我们将使用 `ConversationBufferMemory` ,
它保存聊天消息历史记录的列表,
这些历史记录将在回答问题时与问题一起传递给聊天机器人,
从而将它们添加到上下文中。
'''
from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(
    memory_key="chat_history",  # 与 prompt 的输入变量保持一致。
    return_messages=True  # 将以消息列表的形式返回聊天记录,而不是单个字符串
)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
'''
对话检索链(ConversationalRetrievalChain)
它的工作流程是:
1. 将之前的对话与新问题合并生成一个完整的查询语句。
2. 在向量数据库中搜索该查询的相关文档。
3. 获取结果后,存储所有答案到对话记忆区。
4. 用户可在 UI 中查看完整的对话流程。
'''
from langchain.chains import ConversationalRetrievalChain

retriever=vectordb.as_retriever()

qa = ConversationalRetrievalChain.from_llm(
    llm,
    retriever=retriever,
    memory=memory
)
question = "我可以学习到关于提示工程的知识吗?"
result = qa({"question": question})
print(result['answer'])

# 然后基于答案进行下一个问题“为什么这门课需要教这方面的知识?”:
question = "为什么这门课需要教这方面的知识?"
result = qa({"question": question})
print(result['answer'])

  • 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

4.3 部署知识库助手

  • 4.3 部署知识库助手
    • 利用 StreamLit
import streamlit as st
from langchain_openai import ChatOpenAI
st.title('
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/521376
推荐阅读
相关标签