“动手学大模型应用开发”(学习记录)-- 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"
        
      • 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  // 检查是否安装成功
        
      • 04- 建立conda虚拟环境

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

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

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

        在这里插入图片描述

  • 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"
    
    
    get_completion("你好")
    
    # >>> '你好!有什么可以帮助你的吗?如果有任何问题或需要咨询的事情,请随时告诉我。'
    

    其中,传参如下:

    参数名内容
    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]}')

'''
>>> 
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]
'''

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)} 页")

文档加载后储存在 `pages` 变量中:

- `page` 的变量类型为 `List`
- 打印 `pages` 的长度可以看到 pdf 一共包含多少页
pdf_page = pdf_pages[1]
print(f"每一个元素的类型:{type(pdf_page)}.", 
    f"该文档的描述性数据:{pdf_page.metadata}", 
    f"查看该文档的内容:\n{pdf_page.page_content}", 
    sep="\n------\n")
  • 读取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")

  • 数据清洗

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

    可以看到上文中读取的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)


上文中读取的md文件每一段中间隔了一个换行符,我们同样可以使用replace方法去除。
md_page.page_content = md_page.page_content.replace('\n\n', '\n')
print(md_page.page_content)
  • 文档分割
    • 由于单个文档的长度往往会超过模型支持的上下文,导致检索得到的知识太长超出模型的处理能力,因此,在构建向量知识库的过程中,我们往往需要对文档进行分割,将单个文档按长度或者按固定的规则分割成若干个 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])}")

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])
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)

  • 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")
  • 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")

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

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

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

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

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

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

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

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

首先,Prompt 需要清晰明确地表达需求,提供充足上下文,使语言模型准确理解
--------------
'''
  • (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")

'''
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 
--------------

'''

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("你好,请你自我介绍一下!")


'''
>>> ' 你好,我是 智谱清言,是清华大学KEG实验室和智谱AI公司共同训练的语言模型。我的目标是通过回答用户提出的问题来帮助他们解决问题。由于我是一个计算机程序,所以我没有自我意识,也不能像人类一样感知世界。我只能通过分析我所学到的信息来回答问题。'
'''

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")


  • 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("你好,请你自我介绍一下!")
  • 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 = "王阳明是谁?"

  • 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)

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

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

'''
对话检索链(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'])

4.3 部署知识库助手

  • 4.3 部署知识库助手
    • 利用 StreamLit
import streamlit as st
from langchain_openai import ChatOpenAI
st.title('🦜🔗 动手学大模型应用开发')
openai_api_key = st.sidebar.text_input('OpenAI API Key', type='password')

def generate_response(input_text):
    llm = ChatOpenAI(temperature=0.7, openai_api_key=openai_api_key)
    st.info(llm(input_text))

# 使用`st.form()`创建一个文本框(st.text_area())供用户输入。
# 当用户单击`Submit`时,`generate-response()`将使用用户的输入作为参数来调用该函数  
with st.form('my_form'):
    text = st.text_area('Enter text:', 'What are the three key pieces of advice for learning how to code?')
    submitted = st.form_submit_button('Submit')
    if not openai_api_key.startswith('sk-'):
        st.warning('Please enter your OpenAI API key!', icon='⚠')
    if submitted and openai_api_key.startswith('sk-'):
        generate_response(text)
        

streamlit run streamlit_app.py
# Streamlit 应用程序界面
# 使用 `st.session_state` 来存储对话历史,可以在用户与应用程序交互时保留整个对话的上下文。
def main():
    st.title('🦜🔗 动手学大模型应用开发')
    openai_api_key = st.sidebar.text_input('OpenAI API Key', type='password')

    # 用于跟踪对话历史
    if 'messages' not in st.session_state:
        st.session_state.messages = []

    messages = st.container(height=300)
    if prompt := st.chat_input("Say something"):
        # 将用户输入添加到对话历史中
        st.session_state.messages.append({"role": "user", "text": prompt})

        # 调用 respond 函数获取回答
        answer = generate_response(prompt, openai_api_key)
        # 检查回答是否为 None
        if answer is not None:
            # 将LLM的回答添加到对话历史中
            st.session_state.messages.append({"role": "assistant", "text": answer})

        # 显示整个对话历史
        for message in st.session_state.messages:
            if message["role"] == "user":
                messages.chat_message("user").write(message["text"])
            elif message["role"] == "assistant":
                messages.chat_message("assistant").write(message["text"])   
  • 添加检索问答
def get_vectordb():
    # 定义 Embeddings
    embedding = ZhipuAIEmbeddings()
    # 向量数据库持久化路径
    persist_directory = '../C3 搭建知识库/data_base/vector_db/chroma'
    # 加载数据库
    vectordb = Chroma(
        persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上
        embedding_function=embedding
    )
    return vectordb

#带有历史记录的问答链
def get_chat_qa_chain(question:str,openai_api_key:str):
    vectordb = get_vectordb()
    llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0,openai_api_key = openai_api_key)
    memory = ConversationBufferMemory(
        memory_key="chat_history",  # 与 prompt 的输入变量保持一致。
        return_messages=True  # 将以消息列表的形式返回聊天记录,而不是单个字符串
    )
    retriever=vectordb.as_retriever()
    qa = ConversationalRetrievalChain.from_llm(
        llm,
        retriever=retriever,
        memory=memory
    )
    result = qa({"question": question})
    return result['answer']

#不带历史记录的问答链
def get_qa_chain(question:str,openai_api_key:str):
    vectordb = get_vectordb()
    llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0,openai_api_key = openai_api_key)
    template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
        案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
        {context}
        问题: {question}
        """
    QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                 template=template)
    qa_chain = RetrievalQA.from_chain_type(llm,
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
    result = qa_chain({"query": question})
    return result["result"]


'''
添加一个单选按钮部件st.radio,选择进行问答的模式:
- None:不使用检索问答的普通模式
- qa_chain:不带历史记录的检索问答模式
- chat_qa_chain:带历史记录的检索问答模式
'''
selected_method = st.radio(
        "你想选择哪种模式进行对话?",
        ["None", "qa_chain", "chat_qa_chain"],
        captions = ["不使用检索问答的普通模式", "不带历史记录的检索问答模式", "带历史记录的检索问答模式"])

  • 部署应用程序 (摘自教程原文)
要将应用程序部署到 Streamlit Cloud,请执行以下步骤:  
  
1. 为应用程序创建 GitHub 存储库。您的存储库应包含两个文件:  
  
   your-repository/  
   ├── streamlit_app.py  
   └── requirements.txt  
  
2. 转到 [Streamlit Community Cloud](http://share.streamlit.io/),单击工作区中的`New app`按钮,然后指定存储库、分支和主文件路径。或者,您可以通过选择自定义子域来自定义应用程序的 URL
  
3. 点击`Deploy!`按钮  
  
您的应用程序现在将部署到 Streamlit Community Cloud,并且可以从世界各地访问! 🌎  

5. 系统评估与优化

  • 5.1 如何评估 LLM
    • 评估 LLM 的核心思路是迭代验证。首先,以包含较少数量测试用例(cases)的验证集开始,不断调整prompt,使大模型在这些用例上产生较好的效果。其次,在后续的使用中,不断积累无法依靠调整 prompt(或算法)使大模型产生良好效果的场景(定义为 bad cases ),将它们纳入到大模型的验证集中。在不断的积累下,构建一个丰富的质量验证集。由于此时 cases 数量多,难以实现逐条对比,因此可以设计多维度统计性评估指标,用以量化评估大模型在验证集上的表现,常见指标包括检索正确率、回答幻觉比例、回答一致性、回答正确性、逻辑性等。
    • 为了克服主观题评估判别难,进而阻碍大模型评估自动化的问题,可以采用“构造客观题”和“计算相似度”两种方法。前者通过牺牲部分条件,将“主观题”转化为“客观题”,给出问题和标准答案,实现对大模型回答的评估。尽管这种方法能有效实现评估,但并不是所有主管问题都可以客观化,此外,客观化的过程牺牲了主观问题的部分细节。后者通过构造主观题的标准回答,进一步计算LLM生成回答和标准回答的相似度(如基于 BLEU 的相似度计算),实现对 LLM 回答的自动评估。但是,第二种方法依赖人工设计标准回答,无法实现全自动的自动化评估,并可能会阻碍模型生成优于标准答案的回答。
    • 为了实现更高效的自动化,可以采用基于 LLM 的评估方法。选取优于被测 LLM 的高级 LLM(比如 GPT-4 )作为评估员,替代人工评估过程。
prompt = '''
你是一个模型回答评估员。
接下来,我将给你一个问题、对应的知识片段以及模型根据知识片段对问题的回答。
请你依次评估以下维度模型回答的表现,分别给出打分:

① 知识查找正确性。评估系统给定的知识片段是否能够对问题做出回答。如果知识片段不能做出回答,打分为0;如果知识片段可以做出回答,打分为1。

② 回答一致性。评估系统的回答是否针对用户问题展开,是否有偏题、错误理解题意的情况,打分分值在0~1之间,0为完全偏题,1为完全切题。

③ 回答幻觉比例。该维度需要综合系统回答与查找到的知识片段,评估系统的回答是否出现幻觉,打分分值在0~1之间,0为全部是模型幻觉,1为没有任何幻觉。

④ 回答正确性。该维度评估系统回答是否正确,是否充分解答了用户问题,打分分值在0~1之间,0为完全不正确,1为完全正确。

⑤ 逻辑性。该维度评估系统回答是否逻辑连贯,是否出现前后冲突、逻辑混乱的情况。打分分值在0~1之间,0为逻辑完全混乱,1为完全没有逻辑问题。

⑥ 通顺性。该维度评估系统回答是否通顺、合乎语法。打分分值在0~1之间,0为语句完全不通顺,1为语句完全通顺没有任何语法问题。

⑦ 智能性。该维度评估系统回答是否拟人化、智能化,是否能充分让用户混淆人工回答与智能回答。打分分值在0~1之间,0为非常明显的模型回答,1为与人工回答高度一致。

你应该是比较严苛的评估员,很少给出满分的高评估。
用户问题:

{}

待评估的回答:

{}

给定的知识片段:

{}

你应该返回给我一个可直接解析的 Python 字典,字典的键是如上维度,值是每一个维度对应的评估打分。
不要输出任何其他内容。
'''
  • 当然,评估方法是手段,获得优质的模型是目标。因此,针对于当前模型的具体情况,应合理采用多种评估手段,实现多维度高效的模型评估

5.2 评估并优化生成部分

  • 5.2 评估并优化生成部分
    • RAG 有两个核心部分:检索和生成。前者保证系统根据用户 query 能够查找到对应的答案片段,后者保证系统在获得了正确的答案片段之后,可以生成一个满足用户要求的正确回答。

    • 优化一个大模型应用,我们往往需要从这两部分同时入手,分别评估检索部分和优化部分的性能,找出 Bad Case 并针对性进行性能的优化。对于生成部分,在已限定使用的大模型基座的情况下,我们往往会通过优化 Prompt Engineering 来优化生成的回答

    • 结合我们刚刚搭建出的大模型应用实例——个人知识库助手,学习如何评估分析生成部分性能,针对性找出 Bad Case,并通过优化 Prompt Engineering 的方式来优化生成部分。

      • 先加载我们的向量数据库与检索链:
      import sys
      sys.path.append("../C3 搭建知识库") # 将父目录放入系统路径中
      
      # 使用智谱 Embedding API,注意,需要将上一章实现的封装代码下载到本地
      from zhipuai_embedding import ZhipuAIEmbeddings
      
      from langchain.vectorstores.chroma import Chroma
      from langchain_openai import ChatOpenAI
      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']
      OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
      
      # 定义 Embeddings
      embedding = ZhipuAIEmbeddings()
      
      # 向量数据库持久化路径
      persist_directory = '../../data_base/vector_db/chroma'
      
      # 加载数据库
      vectordb = Chroma(
          persist_directory=persist_directory,  # 允许我们将persist_directory目录保存到磁盘上
          embedding_function=embedding
      )
      
      # 使用 OpenAI GPT-3.5 模型
      llm = ChatOpenAI(model_name = "gpt-3.5-turbo", temperature = 0)
      
      os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
      os.environ["HTTP_PROXY"] = 'http://127.0.0.1:7890'
      
      
        - 使用初始化的 Prompt 创建一个基于模板的检索链:
      
      from langchain.prompts import PromptTemplate
      from langchain.chains import RetrievalQA
      
      
      template_v1 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
      案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
      {context}
      问题: {question}
      """
      
      QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                       template=template_v1)
      
      
      
      qa_chain = RetrievalQA.from_chain_type(llm,
                                             retriever=vectordb.as_retriever(),
                                             return_source_documents=True,
                                             chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
      question = "什么是南瓜书"
      result = qa_chain({"query": question})
      print(result["result"]) 
      
      '''
      南瓜书是对《机器学习》(西瓜书)中比较难理解的公式进行
      解析和补充推导细节的书籍。南瓜书的最佳使用方法是以西瓜书
      为主线,遇到推导困难或看不懂的公式时再来查阅南瓜书。
      谢谢你的提问!
      '''                                    
      
    • 1- 提升直观回答质量

      寻找 Bad Case 的思路有很多,最直观也最简单的就是评估直观回答的质量,结合原有资料内容,判断在什么方面有所不足。例如,上述的测试我们可以构造成一个 Bad Case:

      -问题:什么是南瓜书

      -初始回答:南瓜书是对《机器学习》(西瓜书)中难以理解的公式进行解析和补充推导细节的一本书。谢谢你的提问!

      -存在不足:回答太简略,需要回答更具体;谢谢你的提问感觉比较死板,可以去掉

      我们再针对性地修改 Prompt 模板,加入要求其回答具体,并去掉“谢谢你的提问”的部分:

      template_v2 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
      案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
      {context}
      问题: {question}
      有用的回答:"""
      
      QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                       template=template_v2)
      qa_chain = RetrievalQA.from_chain_type(llm,
                                             retriever=vectordb.as_retriever(),
                                             return_source_documents=True,
                                             chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
      
      question = "什么是南瓜书"
      result = qa_chain({"query": question})
      print(result["result"])
      
      • 可以看到,针对我们关于 LLM 课程的提问,模型回答确实详细具体,也充分参考了课程内容,但回答使用首先、其次等词开头,同时将整体答案分成了4段,导致答案不是特别重点清晰,不容易阅读。因此,我们构造以下 Bad Case:

        问题:使用大模型时,构造 Prompt 的原则有哪些

        初始回答:略

        存在不足:没有重点,模糊不清

        针对该 Bad Case,我们可以改进 Prompt,要求其对有几点的答案进行分点标号,让答案清晰具体:

    • 2- 表明知识来源,提高可信度

      • 由于大模型存在幻觉问题,有时我们会怀疑模型回答并非源于已有知识库内容,这对一些需要保证真实性的场景来说尤为重要
      template_v4 = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
      案。你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
      如果答案有几点,你应该分点标号回答,让答案清晰具体。
      请你附上回答的来源原文,以保证回答的正确性。
      {context}
      问题: {question}
      有用的回答:"""
      
      QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                       template=template_v4)
      qa_chain = RetrievalQA.from_chain_type(llm,
                                             retriever=vectordb.as_retriever(),
                                             return_source_documents=True,
                                             chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
      
      question = "强化学习的定义是什么"
      result = qa_chain({"query": question})
      print(result["result"])
      
    • 3- 构造思维链

      • 大模型往往可以很好地理解并执行指令,但模型本身还存在一些能力的限制,例如大模型的幻觉、无法理解较为复杂的指令、无法执行复杂步骤等。我们可以通过构造思维链,将 Prompt 构造成一系列步骤来尽量减少其能力限制,例如,我们可以构造一个两步的思维链,要求模型在第二步做出反思,以尽可能消除大模型的幻觉问题。
      template_v4 = """
      请你依次执行以下步骤:
      ① 使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答案。
      你应该使答案尽可能详细具体,但不要偏题。如果答案比较长,请酌情进行分段,以提高答案的阅读体验。
      如果答案有几点,你应该分点标号回答,让答案清晰具体。
      上下文:
      {context}
      问题: 
      {question}
      有用的回答:
      ② 基于提供的上下文,反思回答中有没有不正确或不是基于上下文得到的内容,如果有,回答你不知道
      确保你执行了每一个步骤,不要跳过任意一个步骤。
      """
      
      QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],
                                       template=template_v4)
      qa_chain = RetrievalQA.from_chain_type(llm,
                                             retriever=vectordb.as_retriever(),
                                             return_source_documents=True,
                                             chain_type_kwargs={"prompt":QA_CHAIN_PROMPT})
      
      question = "我们应该如何去构造一个LLM项目"
      result = qa_chain({"query": question})
      print(result["result"])
      
    • 4- 增加一个指令解析

      • 我们往往会面临一个需求,即我们需要模型以我们指定的格式进行输出。但是,由于我们使用了 Prompt Template 来填充用户问题,用户问题中存在的格式要求往往会被忽略,例如:
      question = "LLM的分类是什么?给我返回一个 Python List"
      result = qa_chain({"query": question})
      print(result["result"])
      

5.3 评估并优化检索部分

  • “生成”的前提是“检索”,只有当我们应用的检索部分能够根据用户 query 检索到正确的答案文档时,大模型的生成结果才可能是正确的。因此,检索部分的检索精确率和召回率其实更大程度影响了应用的整体性能。但是,检索部分的优化是一个更工程也更深入的命题,我们往往需要使用到很多高级的、源于搜索的进阶技巧并探索更多实用工具,甚至手写一些工具来进行优化。
  • 检索常见问题及优化思路
问题解释思路
知识片段被割裂导致答案丢失对于一个用户 query,我们可以确定其问题一定是存在于知识库之中的,但是我们发现检索到的知识片段将正确答案分割开了,导致不能形成一个完整、合理的答案。优化文本切割方式
query 提问需要长上下文概括回答部分 query 提出的问题需要检索部分跨越很长的上下文来做出概括性回答,也就是需要跨越多个 chunk 来综合回答问题。优化知识库构建方式
关键词误导对于一个用户 query,系统检索到的知识片段有很多与 query 强相关的关键词,但知识片段本身并非针对 query 做出的回答。对用户 query 进行改写
匹配关系不合理匹配到的强相关文本段并没有包含答案文本优化向量模型或是构建倒排索引

总结

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值