当前位置:   article > 正文

跟着DW学习大语言模型-通过实践案例学习使用langchain构建基于LLM的RAG应用,实现信息到知识的跨越。实践中学习构建流程,结合需求进行工程设计,打造专属AI,赋能各种场景_langchain应用案例

langchain应用案例
  • 在数据量激增的当代社会,有效管理和检索信息成为了一项关键技能。为了应对这一挑战,本项目应运而生,旨在构建一个基于 Langchain 的个人知识库助手。该助手通过高效的信息管理系统和强大的检索功能,为用户提供了一个可靠的信息获取平台。本项目的核心目标是充分发挥大型语言模型在处理自然语言查询方面的优势,同时针对用户需求进行定制化开发,以实现对复杂信息的智能理解和精确回应。在项目开发过程中,团队深入分析了大型语言模型的潜力与局限,特别是其在生成幻觉信息方面的倾向。为了解决这一问题,项目集成了 RAG 技术,这是一种结合检索和生成的方法,能够在生成回答之前先从大量数据中检索相关信息,从而显著提高了回答的准确性和可靠性。通过 RAG 技术的引入,本项目不仅提升了对信息的检索精度,还有效抑制了 Langchain 可能产生的误导性信息。这种结合检索和生成的方法确保了智能助手在提供信息时的准确性和权威性,使其成为用户在面对海量数据时的得力助手。本项目可以实现基于 Datawhale 的现有项目 README 的知识问答,使用户可以快速了解 Datawhale 现有项目情况。[基于DataWhale社区资料整理]

  • 本项目致力于开发一个高效、智能的个人知识库系统,旨在优化用户在信息洪流中的知识获取流程。该系统通过集成 Langchain 和自然语言处理技术,实现了对分散数据源的快速访问和整合,使用户能够通过直观的自然语言交互高效地检索和利用信息。项目的核心价值体现在以下几个方面:

    • 优化信息检索效率:利用基于 Langchain 的框架,系统能够在生成回答前先从广泛的数据集中检索到相关信息,从而加速信息的定位和提取过程。
    • 强化知识组织与管理:支持用户构建个性化的知识库,通过结构化存储和分类,促进知识的积累和有效管理,进而提升用户对专业知识的掌握和运用。
    • 辅助决策制定:通过精确的信息提供和分析,系统增强了用户在复杂情境下的决策能力,尤其是在需要迅速做出判断和反应的场合。
    • 个性化信息服务:系统允许用户根据自己的特定需求定制知识库,实现个性化的信息检索和服务,确保用户能够获得最相关和最有价值的知识。
    • 技术创新示范:项目展示了 RAG 技术在解决 Langchain 幻觉问题方面的优势,通过结合检索和生成的方式,提高了信息的准确性和可靠性,为智能信息管理领域的技术创新提供了新的思路。
    • 推广智能助理应用:通过用户友好的界面设计和便捷的部署选项,项目使得智能助理技术更加易于理解和使用,推动了该技术在更广泛领域的应用和普及
  • 项目设置

    • git clone https://github.com/logan-zou/Chat_with_Datawhale_langchain.git
      cd Chat_with_Datawhale_langchain
      # 创建 Conda 环境
      conda create -n llm-universe python==3.9.0
      # 激活 Conda 环境
      conda activate llm-universe
      # 安装依赖项
      pip install -r requirements.txt
      # Windows 系统
      cd project/serve
      python api.py
      # 运行项目
      cd llm-universe/project/serve
      python run_gradio.py -model_name='chatglm_std' -embedding_model='m3e' -db_path='../../data_base/knowledge_db' -persist_path='../../data_base/vector_db'
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
  • 核心是针对四种大模型 API 实现了底层封装,基于 Langchain 搭建了可切换模型的检索问答链,并实现 API 以及 Gradio 部署的个人轻量大模型应用。本项目为一个基于大模型的个人知识库助手,基于 LangChain 框架搭建,核心技术包括 LLM API 调用、向量数据库、检索问答链等。项目整体架构如下:

    • 在这里插入图片描述

    • 本项目从底向上依次分为 LLM 层、数据层、数据库层、应用层与服务层。

  • llm-universe 个人知识库助手地址:datawhalechina/llm-universe: 本项目是一个面向小白开发者的大模型应用开发教程,在线阅读地址:https://datawhalechina.github.io/llm-universe/。是个典型的RAG项目,通过langchain+LLM实现本地知识库问答,建立了全流程可使用开源模型实现的本地知识库对话应用。目前已经支持使用 ChatGPT星火spark模型文心大模型智谱GLM 等大语言模型的接入。

  • 创建知识库并加载文件-读取文件-文本分割(Text splitter) ,知识库文本向量化(embedding)以及存储到向量数据库的实现,其中加载文件:这是读取存储在本地的知识库文件的步骤。读取文件:读取加载的文件内容,通常是将其转化为文本格式 。**文本分割(Text splitter):按照⼀定的规则(例如段落、句子、词语等)将文本分割。文本向量化:**这通常涉及到 NLP 的特征抽取,该项目通过本地 m3e 文本嵌入模型,openai,zhipuai 开源 api 等方法将分割好的文本转化为数值向量并存储到向量数据库

    • 知识库搭建-加载和读取:

    • 该项目llm-universe个人知识库助手选用 Datawhale 一些经典开源课程、视频(部分)作为示例,具体包括:Releases · datawhalechina/pumpkin-book (github.com)datawhalechina/llm-cookbook: 面向开发者的 LLM 入门教程,吴恩达大模型系列课程中文版 (github.com)【采蘑菇教程】《Easy RL:强化学习教程》(蘑菇书)_哔哩哔哩_bilibili

    • 这些知识库源数据放置在 …/…/data_base/knowledge_db 目录下,用户也可以自己存放自己其他的文件。

    • 下面讲一下如何获取 DataWhale 总仓库的所有开源项目的 readme ,用户可以通过先运行 project/database/test_get_all_repo.py 文件,用来获取 Datawhale 总仓库所有开源项目的 readme,代码如下:

    • import json
      import requests
      import os
      import base64
      import loguru
      from dotenv import load_dotenv
      # 加载环境变量
      load_dotenv()
      # 从环境变量中获取TOKEN
      TOKEN = os.getenv('TOKEN')
      # 定义获取组织仓库的函数
      def get_repos(org_name, token, export_dir):
          headers = {
              'Authorization': f'token {token}',
          }
          url = f'https://api.github.com/orgs/{org_name}/repos'
          response = requests.get(url, headers=headers, params={'per_page': 200, 'page': 0})
          if response.status_code == 200:
              repos = response.json()
              loguru.logger.info(f'Fetched {len(repos)} repositories for {org_name}.')
              # 使用 export_dir 确定保存仓库名的文件路径
              repositories_path = os.path.join(export_dir, 'repositories.txt')
              with open(repositories_path, 'w', encoding='utf-8') as file:
                  for repo in repos:
                      file.write(repo['name'] + '\n')
              return repos
          else:
              loguru.logger.error(f"Error fetching repositories: {response.status_code}")
              loguru.logger.error(response.text)
              return []
      # 定义拉取仓库README文件的函数
      def fetch_repo_readme(org_name, repo_name, token, export_dir):
          headers = {
              'Authorization': f'token {token}',
          }
          url = f'https://api.github.com/repos/{org_name}/{repo_name}/readme'
          response = requests.get(url, headers=headers)
          if response.status_code == 200:
              readme_content = response.json()['content']
              # 解码base64内容
              readme_content = base64.b64decode(readme_content).decode('utf-8')
              # 使用 export_dir 确定保存 README 的文件路径
              repo_dir = os.path.join(export_dir, repo_name)
              if not os.path.exists(repo_dir):
                  os.makedirs(repo_dir)
              readme_path = os.path.join(repo_dir, 'README.md')
              with open(readme_path, 'w', encoding='utf-8') as file:
                  file.write(readme_content)
          else:
              loguru.logger.error(f"Error fetching README for {repo_name}: {response.status_code}")
              loguru.logger.error(response.text)
      # 主函数
      if __name__ == '__main__':
          # 配置组织名称
          org_name = 'datawhalechina'
          # 配置 export_dir
          export_dir = "../../database/readme_db"  # 请替换为实际的目录路径
          # 获取仓库列表
          repos = get_repos(org_name, TOKEN, export_dir)
          # 打印仓库名称
          if repos:
              for repo in repos:
                  repo_name = repo['name']
                  # 拉取每个仓库的README
                  fetch_repo_readme(org_name, repo_name, TOKEN, export_dir)
          # 清理临时文件夹
          # if os.path.exists('temp'):
          #     shutil.rmtree('temp')
      
      • 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
    • 默认会把这些readme文件放在同目录database下的readme_db文件。其中这些readme文件含有不少无关信息,即再运行project/database/text_summary_readme.py文件可以调用大模型生成每个readme文件的摘要并保存到上述知识库目录…/…/data_base/knowledge_db /readme_summary文件夹中。代码如下:

    • import os
      from dotenv import load_dotenv
      import openai
      from test_get_all_repo import get_repos
      from bs4 import BeautifulSoup
      import markdown
      import re
      import time
      # Load environment variables
      load_dotenv()
      TOKEN = os.getenv('TOKEN')
      # Set up the OpenAI API client
      openai_api_key = os.environ["OPENAI_API_KEY"]
      # 过滤文本中链接防止大语言模型风控
      def remove_urls(text):
          # 正则表达式模式,用于匹配URL
          url_pattern = re.compile(r'https?://[^\s]*')
          # 替换所有匹配的URL为空字符串
          text = re.sub(url_pattern, '', text)
          # 正则表达式模式,用于匹配特定的文本
          specific_text_pattern = re.compile(r'扫描下方二维码关注公众号|提取码|关注|科学上网|回复关键词|侵权|版权|致谢|引用|LICENSE|组队打卡|任务打卡|组队学习的那些事|学习周期|开源内容|打卡|组队学习|链接')
          # 替换所有匹配的特定文本为空字符串
          text = re.sub(specific_text_pattern, '', text)
          return text
      # 抽取md中的文本
      def extract_text_from_md(md_content):
          # Convert Markdown to HTML
          html = markdown.markdown(md_content)
          # Use BeautifulSoup to extract text
          soup = BeautifulSoup(html, 'html.parser')
          return remove_urls(soup.get_text())
      def generate_llm_summary(repo_name, readme_content,model):
          prompt = f"1:这个仓库名是 {repo_name}. 此仓库的readme全部内容是: {readme_content}\
                     2:请用约200以内的中文概括这个仓库readme的内容,返回的概括格式要求:这个仓库名是...,这仓库内容主要是..."
          openai.api_key = openai_api_key
          # 具体调用
          messages = [{"role": "system", "content": "你是一个人工智能助手"},
                      {"role": "user", "content": prompt}]
          response = openai.ChatCompletion.create(
              model=model,
              messages=messages,
          )
          return response.choices[0].message["content"]
      def main(org_name,export_dir,summary_dir,model):
          repos = get_repos(org_name, TOKEN, export_dir)
          # Create a directory to save summaries
          os.makedirs(summary_dir, exist_ok=True)
          for id, repo in enumerate(repos):
              repo_name = repo['name']
              readme_path = os.path.join(export_dir, repo_name, 'README.md')
              print(repo_name)
              if os.path.exists(readme_path):
                  with open(readme_path, 'r', encoding='utf-8') as file:
                      readme_content = file.read()
                  # Extract text from the README
                  readme_text = extract_text_from_md(readme_content)
                  # Generate a summary for the README
                  # 访问受限,每min一次
                  time.sleep(60)
                  print('第' + str(id) + '条' + 'summary开始')
                  try:
                      summary = generate_llm_summary(repo_name, readme_text,model)
                      print(summary)
                      # Write summary to a Markdown file in the summary directory
                      summary_file_path = os.path.join(summary_dir, f"{repo_name}_summary.md")
                      with open(summary_file_path, 'w', encoding='utf-8') as summary_file:
                          summary_file.write(f"# {repo_name} Summary\n\n")
                          summary_file.write(summary)
                  except openai.OpenAIError as e:
                      summary_file_path = os.path.join(summary_dir, f"{repo_name}_summary风控.md")
                      with open(summary_file_path, 'w', encoding='utf-8') as summary_file:
                          summary_file.write(f"# {repo_name} Summary风控\n\n")
                          summary_file.write("README内容风控。\n")
                      print(f"Error generating summary for {repo_name}: {e}")
                      # print(readme_text)
              else:
                  print(f"文件不存在: {readme_path}")
                  # If README doesn't exist, create an empty Markdown file
                  summary_file_path = os.path.join(summary_dir, f"{repo_name}_summary不存在.md")
                  with open(summary_file_path, 'w', encoding='utf-8') as summary_file:
                      summary_file.write(f"# {repo_name} Summary不存在\n\n")
                      summary_file.write("README文件不存在。\n")
      if __name__ == '__main__':
          # 配置组织名称
          org_name = 'datawhalechina'
          # 配置 export_dir
          export_dir = "../database/readme_db"  # 请替换为实际readme的目录路径
          summary_dir="../../data_base/knowledge_db/readme_summary"# 请替换为实际readme的概括的目录路径
          model="gpt-3.5-turbo"  #deepseek-chat,gpt-3.5-turbo,moonshot-v1-8k
          main(org_name,export_dir,summary_dir,model)
      
      • 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
    • 其中 extract_text_from_md() 函数用来抽取 md 文件中的文本, remove_urls() 函数过滤了 readme 文本中的一些网页链接以及过滤了可能引起大模型风控一些词汇。接着调用 generate_llm_summary() 让大模型生成每个 readme 的概括。

    • 在上述知识库构建完毕之后,…/…/data_base/knowledge_db 目录下就有了 Datawhale 开源的所有项目的 readme 概括的 md 文件,以及《机器学习公式详解》PDF版本《面向开发者的 LLM 入门教程 第一部分 Prompt Engineering》md版本《强化学习入门指南》MP4版本等文件。

    • 其中有 mp4 格式,md 格式,以及 pdf 格式,对这些文件的加载方式,该项目将代码放在了 project/database/create_db.py文件 下,部分代码如下。其中 pdf 格式文件用 PyMuPDFLoader 加载器,md格式文件用UnstructuredMarkdownLoader加载器。要注意的是其实数据处理是一件非常复杂和业务个性化的事,如pdf文件中包含图表,图片和文字以及不同层次标题,这些都需要根据业务进行精细化处理。

    • from langchain.document_loaders import UnstructuredFileLoader
      from langchain.document_loaders import UnstructuredMarkdownLoader
      from langchain.text_splitter import RecursiveCharacterTextSplitter
      from langchain.document_loaders import PyMuPDFLoader
      from langchain.vectorstores import Chroma
      # 首先实现基本配置
      DEFAULT_DB_PATH = "../../data_base/knowledge_db"
      DEFAULT_PERSIST_PATH = "../../data_base/vector_db"
      ... 
      ...
      ...
      def file_loader(file, loaders):
          if isinstance(file, tempfile._TemporaryFileWrapper):
              file = file.name
          if not os.path.isfile(file):
              [file_loader(os.path.join(file, f), loaders) for f in  os.listdir(file)]
              return
          file_type = file.split('.')[-1]
          if file_type == 'pdf':
              loaders.append(PyMuPDFLoader(file))
          elif file_type == 'md':
              pattern = r"不存在|风控"
              match = re.search(pattern, file)
              if not match:
                  loaders.append(UnstructuredMarkdownLoader(file))
          elif file_type == 'txt':
              loaders.append(UnstructuredFileLoader(file))
          return
      ...
      ...
      
      • 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
  • 文本分割和向量化操作,在整个 RAG 流程中是必不可少的。需要将上述载入的知识库分本或进行 token 长度进行分割,或者进行语义模型进行分割。该项目利用 Langchain 中的文本分割器根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割。

    • chunk_size 指每个块包含的字符或 Token(如单词、句子等)的数量
    • chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
  • 可以设置一个最大的 Token 长度,然后根据这个最大的 Token 长度来切分文档。这样切分出来的文档片段是一个一个均匀长度的文档片段。而片段与片段之间的一些重叠的内容,能保证检索的时候能够检索到相关的文档片段。这部分文本分割代码也在 project/database/create_db.py 文件,该项目采用了 langchain 中 RecursiveCharacterTextSplitter 文本分割器进行分割。代码如下:

    • ......
      def create_db(files=DEFAULT_DB_PATH, persist_directory=DEFAULT_PERSIST_PATH, embeddings="openai"):
          """
          该函数用于加载 PDF 文件,切分文档,生成文档的嵌入向量,创建向量数据库。
          参数:
          file: 存放文件的路径。
          embeddings: 用于生产 Embedding 的模型
          返回:
          vectordb: 创建的数据库。
          """
          if files == None:
              return "can't load empty file"
          if type(files) != list:
              files = [files]
          loaders = []
          [file_loader(file, loaders) for file in files]
          docs = []
          for loader in loaders:
              if loader is not None:
                  docs.extend(loader.load())
          # 切分文档
          text_splitter = RecursiveCharacterTextSplitter(
              chunk_size=500, chunk_overlap=150)
          split_docs = text_splitter.split_documents(docs)
          ....
          ....
          ....此处省略了其他代码
          ....
          return vectordb
      ...........    
      
      • 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
  • 而在切分好知识库文本之后,需要对文本进行 向量化 。该项目在 project/embedding/call_embedding.py ,文本嵌入方式可选本地 m3e 模型,以及调用 openai 和 zhipuai 的 api 的方式进行文本嵌入。代码如下:

    • import os
      import sys
      sys.path.append(os.path.dirname(os.path.dirname(__file__)))
      sys.path.append(r"../../")
      from embedding.zhipuai_embedding import ZhipuAIEmbeddings
      from langchain.embeddings.huggingface import HuggingFaceEmbeddings
      from langchain.embeddings.openai import OpenAIEmbeddings
      from llm.call_llm import parse_llm_api_key
      def get_embedding(embedding: str, embedding_key: str = None, env_file: str = None):
         if embedding == 'm3e':
            return HuggingFaceEmbeddings(model_name="moka-ai/m3e-base")
         if embedding_key == None:
            embedding_key = parse_llm_api_key(embedding)
         if embedding == "openai":
            return OpenAIEmbeddings(openai_api_key=embedding_key)
         elif embedding == "zhipuai":
            return ZhipuAIEmbeddings(zhipuai_api_key=embedding_key)
         else:
            raise ValueError(f"embedding {embedding} not support ")
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
  • 在对知识库文本进行分割和向量化后,就需要定义一个向量数据库用来存放文档片段和对应的向量表示了,在向量数据库中,数据被表示为向量形式,每个向量代表一个数据项。这些向量可以是数字、文本、图像或其他类型的数据。向量数据库使用高效的索引和查询算法来加速向量数据的存储和检索过程。该项目选择 chromadb 向量数据库(类似的向量数据库还有 faiss 等)。定义向量库对应的代码也在 project/database/create_db.py 文件中,persist_directory 即为本地持久化地址,vectordb.persist() 操作可以持久化向量数据库到本地,后续可以再次载入本地已有的向量库。完整的文本分割,获取向量化,并且定义向量数据库代码如下:

    • def create_db(files=DEFAULT_DB_PATH, persist_directory=DEFAULT_PERSIST_PATH, embeddings="openai"):
          """
          该函数用于加载 PDF 文件,切分文档,生成文档的嵌入向量,创建向量数据库。
          参数:
          file: 存放文件的路径。
          embeddings: 用于生产 Embedding 的模型
          返回:
          vectordb: 创建的数据库。
          """
          if files == None:
              return "can't load empty file"
          if type(files) != list:
              files = [files]
          loaders = []
          [file_loader(file, loaders) for file in files]
          docs = []
          for loader in loaders:
              if loader is not None:
                  docs.extend(loader.load())
          # 切分文档
          text_splitter = RecursiveCharacterTextSplitter(
              chunk_size=500, chunk_overlap=150)
          split_docs = text_splitter.split_documents(docs)
          if type(embeddings) == str:
              embeddings = get_embedding(embedding=embeddings)
          # 定义持久化路径
          persist_directory = '../../data_base/vector_db/chroma'
          # 加载数据库
          vectordb = Chroma.from_documents(
          documents=split_docs,
          embedding=embeddings,
          persist_directory=persist_directory  # 允许我们将persist_directory目录保存到磁盘上
          ) 
          vectordb.persist()
          return vectordb
      
      • 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
  • 进入了 RAG 的检索和生成阶段,即对问句 Query 向量化后在知识库文档向量中匹配出与问句 Query 向量最相似的 top k 个片段,匹配出的知识库文本文本作为上下文 Context 和问题⼀起添加到 prompt 中,然后提交给 LLM 生成回答 Answer。下面将根据 llm_universe 个人知识库助手进行讲解。通过上一章节文本的分割向量化以及构建向量数据库索引,接下去就可以利用向量数据库来进行高效的检索。向量数据库是一种用于有效搜索大规模高维向量空间中相似度的库,能够在大规模数据集中快速找到与给定 query 向量最相似的向量。如下面示例所示:

    • question="什么是机器学习"
      Copy to clipboardErrorCopied
      sim_docs = vectordb.similarity_search(question,k=3)
      print(f"检索到的内容数:{len(sim_docs)}")
      
      • 1
      • 2
      • 3
      • 4
  • 这里以该项目 project/qa_chain/model_to_llm.py 代码为例,在 project/llm/ 的目录文件夹下分别定义了 星火spark智谱glm文心llm等开源模型api调用的封装,并在 project/qa_chain/model_to_llm.py 文件中导入了这些模块,可以根据用户传入的模型名字进行调用 llm。代码如下:

    • def model_to_llm(model:str=None, temperature:float=0.0, appid:str=None, api_key:str=None,Spark_api_secret:str=None,Wenxin_secret_key:str=None):
              """
              星火:model,temperature,appid,api_key,api_secret
              百度问心:model,temperature,api_key,api_secret
              智谱:model,temperature,api_key
              OpenAI:model,temperature,api_key
              """
              if model in ["gpt-3.5-turbo", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-0613", "gpt-4", "gpt-4-32k"]:
                  if api_key == None:
                      api_key = parse_llm_api_key("openai")
                  llm = ChatOpenAI(model_name = model, temperature = temperature , openai_api_key = api_key)
              elif model in ["ERNIE-Bot", "ERNIE-Bot-4", "ERNIE-Bot-turbo"]:
                  if api_key == None or Wenxin_secret_key == None:
                      api_key, Wenxin_secret_key = parse_llm_api_key("wenxin")
                  llm = Wenxin_LLM(model=model, temperature = temperature, api_key=api_key, secret_key=Wenxin_secret_key)
              elif model in ["Spark-1.5", "Spark-2.0"]:
                  if api_key == None or appid == None and Spark_api_secret == None:
                      api_key, appid, Spark_api_secret = parse_llm_api_key("spark")
                  llm = Spark_LLM(model=model, temperature = temperature, appid=appid, api_secret=Spark_api_secret, api_key=api_key)
              elif model in ["chatglm_pro", "chatglm_std", "chatglm_lite"]:
                  if api_key == None:
                      api_key = parse_llm_api_key("zhipuai")
                  llm = ZhipuAILLM(model=model, zhipuai_api_key=api_key, temperature = temperature)
              else:
                  raise ValueError(f"model{model} not support!!!")
              return 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
  • 设计完基于知识库问答的 prompt,就可以结合上述检索和大模型调用进行答案的生成。构建 prompt 的格式如下,具体可以根据自己业务需要进行修改:

    • from langchain.prompts import PromptTemplate
      # template = """基于以下已知信息,简洁和专业的来回答用户的问题。
      #             如果无法从中得到答案,请说 "根据已知信息无法回答该问题" 或 "没有提供足够的相关信息",不允许在答案中添加编造成分。
      #             答案请使用中文。
      #             总是在回答的最后说“谢谢你的提问!”。
      # 已知信息:{context}
      # 问题: {question}"""
      template = """使用以下上下文来回答最后的问题。如果你不知道答案,就说你不知道,不要试图编造答
      案。最多使用三句话。尽量使答案简明扼要。总是在回答的最后说“谢谢你的提问!”。
      {context}
      问题: {question}
      有用的回答:"""
      QA_CHAIN_PROMPT = PromptTemplate(input_variables=["context","question"],template=template)
      # 自定义 QA 链
      self.qa_chain = RetrievalQA.from_chain_type(llm=self.llm,
                                              retriever=self.retriever,
                                              return_source_documents=True,
                                              chain_type_kwargs={"prompt":self.QA_CHAIN_PROMPT})
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
    • 创建检索 QA 链的方法 RetrievalQA.from_chain_type() 有如下参数:

      • llm:指定使用的 LLM
      • 指定 chain type : RetrievalQA.from_chain_type(chain_type=“map_reduce”),也可以利用load_qa_chain()方法指定chain type。
      • 自定义 prompt :通过在RetrievalQA.from_chain_type()方法中,指定chain_type_kwargs参数,而该参数:chain_type_kwargs = {“prompt”: PROMPT}
      • 返回源文档:通过RetrievalQA.from_chain_type()方法中指定:return_source_documents=True参数;也可以使用RetrievalQAWithSourceChain()方法,返回源文档的引用(坐标或者叫主键、索引)
    • question_1 = "什么是南瓜书?"
      question_2 = "王阳明是谁?"Copy to clipboardErrorCopied
      result = qa_chain({"query": question_1})
      print("大模型+知识库后回答 question_1 的结果:")
      print(result["result"])
      result = qa_chain({"query": question_2})
      print("大模型+知识库后回答 question_2 的结果:")
      print(result["result"])
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
  • 上述详细不带记忆的检索问答链代码都在该项目:project/qa_chain/QA_chain_self.py 中,此外该项目还实现了带记忆的检索问答链,两种自定义检索问答链内部实现细节类似,只是调用了不同的 LangChain 链。完整带记忆的检索问答链条代码 project/qa_chain/Chat_QA_chain_self.py 如下:

    • from langchain.prompts import PromptTemplate
      from langchain.chains import RetrievalQA
      from langchain.vectorstores import Chroma
      from langchain.chains import ConversationalRetrievalChain
      from langchain.memory import ConversationBufferMemory
      from langchain.chat_models import ChatOpenAI
      from qa_chain.model_to_llm import model_to_llm
      from qa_chain.get_vectordb import get_vectordb
      class Chat_QA_chain_self:
          """"
          带历史记录的问答链  
          - model:调用的模型名称
          - temperature:温度系数,控制生成的随机性
          - top_k:返回检索的前k个相似文档
          - chat_history:历史记录,输入一个列表,默认是一个空列表
          - history_len:控制保留的最近 history_len 次对话
          - file_path:建库文件所在路径
          - persist_path:向量数据库持久化路径
          - appid:星火
          - api_key:星火、百度文心、OpenAI、智谱都需要传递的参数
          - Spark_api_secret:星火秘钥
          - Wenxin_secret_key:文心秘钥
          - embeddings:使用的embedding模型
          - embedding_key:使用的embedding模型的秘钥(智谱或者OpenAI)  
          """
          def __init__(self,model:str, temperature:float=0.0, top_k:int=4, chat_history:list=[], file_path:str=None, persist_path:str=None, appid:str=None, api_key:str=None, Spark_api_secret:str=None,Wenxin_secret_key:str=None, embedding = "openai",embedding_key:str=None):
              self.model = model
              self.temperature = temperature
              self.top_k = top_k
              self.chat_history = chat_history
              #self.history_len = history_len
              self.file_path = file_path
              self.persist_path = persist_path
              self.appid = appid
              self.api_key = api_key
              self.Spark_api_secret = Spark_api_secret
              self.Wenxin_secret_key = Wenxin_secret_key
              self.embedding = embedding
              self.embedding_key = embedding_key
              self.vectordb = get_vectordb(self.file_path, self.persist_path, self.embedding,self.embedding_key)
          def clear_history(self):
              "清空历史记录"
              return self.chat_history.clear()
          def change_history_length(self,history_len:int=1):
              """
              保存指定对话轮次的历史记录
              输入参数:
              - history_len :控制保留的最近 history_len 次对话
              - chat_history:当前的历史对话记录
              输出:返回最近 history_len 次对话
              """
              n = len(self.chat_history)
              return self.chat_history[n-history_len:]
          def answer(self, question:str=None,temperature = None, top_k = 4):
              """"
              核心方法,调用问答链
              arguments: 
              - question:用户提问
              """
              if len(question) == 0:
                  return "", self.chat_history
              if len(question) == 0:
                  return ""
              if temperature == None:
                  temperature = self.temperature
              llm = model_to_llm(self.model, temperature, self.appid, self.api_key, self.Spark_api_secret,self.Wenxin_secret_key)
              #self.memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
              retriever = self.vectordb.as_retriever(search_type="similarity",search_kwargs={'k': top_k})  #默认similarity,k=4
              qa = ConversationalRetrievalChain.from_llm(
                  llm = llm,
                  retriever = retriever
              )
              #print(self.llm)
              result = qa({"question": question,"chat_history": self.chat_history})       #result里有question、chat_history、answer
              answer =  result['answer']
              self.chat_history.append((question,answer)) #更新历史记录
              return self.chat_history  #返回本次回答和更新后的历史记录
      
      • 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
  • 案例一:个人知识库助手 (datawhalechina.github.io)

  • 案例二:人情世故大模型 (datawhalechina.github.io)

  • 天机涉及到的技术路线共有四种:Prompt、Agent,知识库,模型训练。 其中,Prompt和Agent只需要配置大模型秘钥,不需要显卡,使用常规笔记本电脑即可运行。

    • 克隆仓库:git clone https://github.com/SocialAI-tianji/Tianji.git
      创建虚拟环境:conda create -n TJ python=3.11
      激活环境:conda activate TJ
      安装环境依赖:pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
      # 在项目内创建.env文件,填写你的大模型秘钥
      OPENAI_API_KEY=
      OPENAI_API_BASE=
      ZHIPUAI_API_KEY=
      BAIDU_API_KEY=
      OPENAI_API_MODEL=
      HF_HOME='./cache/'
      HF_ENDPOINT = 'https://hf-mirror.com'
      HF_TOKEN=
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
  • RAG的主要组成依次是数据提取——embedding(向量化)——创建索引——检索——自动排序(Rerank)——LLM归纳生成。

    • 在这里插入图片描述

    • 第一步,我们需要进行数据提取,包括数据清洗、数据处理以及元数据提取等操作。

    • 第二步,向量化(embedding),这是将文本、图像、音频和视频等转化为向量矩阵的过程,也就是变成计算机可以理解的格式,embedding模型的好坏会直接影响到后面检索的质量,特别是相关度。

    • 第三步,检索环节,这是RAG的关键环节,我们可以通过多种检索方式来提升效率和相关度。(如:数据分块、专业领域的嵌入模型微调、查询嵌入的优化等等)

    • 第四步,生成环节,LLM将检索到的信息转化为流畅文本,该文本将成为模型输出的最终成果。

  • 首先,我们需要使用一些工具进行获取数据和清理预料。天机项目tools/get_data下存放了一些获取数据的小工具,大家可自行查看参考。(主要用于进行视频或图片转化为文字)获取数据此后参考tools/prompt_factory下脚本,将每一个md格式的文档转换成json格式。转换后的json文件中包含了id、name、test_system、input和output等字段,结构化地保存了原Markdown文件中的信息。脚本作用:

    • 使用replace_english_colons_with_chinese函数将Markdown文件中的英文冒号替换为中文冒号,通过find_first_heading函数识别Markdown文档中使用的标题级别等等,以便正确解析不同部分等操作,统一文档格式。
    • 如果文档没有使用特定的标题标记,将按无标题处理,并直接解析prompt信息。
    • 如果文档中含有标题,会根据标题将内容分段,每一段作为一个独立的部分处理,并在JSON中保留相关的结构信息。
  • 如何让大模型明白我们的需求呢?一种简单的方式是通过 Prompt 来实现,大致分为以下四个步骤。

    • 设置场景:首先,我们要告诉大模型,我们现在想要模拟的是一个什么样的情景。比如,现在我们想模拟的是“对长辈敬酒”。
    • 定义角色:接下来,我们要给大模型设定一个具体的角色,并用形容词描述这个角色的特点,可以显著提高模型回应的相关性和适当性。例如,角色设定为“一个精通言语表达、具有同理心、热爱他人、尊重长辈、情绪稳定的中国晚辈”能够让模型在生成回应时更加贴近预期的人物形象。
    • 明确要求:我们还要明确指出输出内容的格式和要求,有助于模型产生更加组织有序且目的明确的输出。比如,我们希望它在回答时,能够用适当的称呼,并且提到长辈的一些特点。
    • 提供示例:最后,通过提供明确的输入和预期输出示例,我们可以进一步指导模型了解任务的具体要求,从而生成更加符合预期的输出。
  • 我们提供给大模型的每条数据需要包括场景名称、系统提示、以及输入输出的示例。如下所示:

    • {
              "id": 1,
              "name": "对长辈敬酒",
              "system_prompt": "你现在是一个精通言语表达、具有同理心、热爱他人、尊重长辈、情绪稳定的中国晚辈,今天是你们家庭的家宴,在宴会上作为晚辈的你要向你的长辈敬酒,对他表达祝福、感谢,下面我将给出这位长辈的具体信息,包括称谓 、长辈特点、具体场景、演讲时间,请你根据这些信息,以我的角度来对长辈说一段话。要求:简洁、简短、真诚、有趣、适合场合、礼貌,可以加入中国酒文化词汇。称谓:_____,长辈特点:______,具体场景:_____,演讲时间:______。\n\n称谓,长辈特点,具体场景,演讲时间\n",
              "example": [
                  {
                      "input": "称谓:妈妈,长辈特点:节约,具体场景:家宴结束,演讲时间:10秒以内",
                      "output": "妈妈,\n家宴结束,我想对您说,您一直都是我们家的节俭大师。感谢您的精打细算,让我们的生活更加美好。干杯,妈妈!我爱您!\n"
                  },
                  {
                      "input": "称谓:奶奶,长辈特点:身体不大好,具体场景:家宴开场,演讲时间:半分钟以内",
                      "output": "亲爱的奶奶,\n\n在这家宴开场的时刻,我要特别感谢您。尽管身体不大好,但您始终是我们家庭的坚强支持。您的坚韧和勇气是我们学习的楷模。今天,我们都为有您这位伟大的奶奶而自豪。\n\n让我们共同举杯,祝愿您健康长寿,岁月静好。奶奶,我爱您,您永远是我们家庭的宝贝!干杯!"
                  }
              ]
          },
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
  • 在角色扮演的基础上,我们进一步利用大模型的能力来创造一个互动式的游戏,使用户能够沉浸在虚拟场景中,通过对话与模型互动。这种游戏设计不仅增加了用户参与感,还让用户在享受游戏乐趣的同时,学习到如何在特定场景下有效沟通。下面是设计游戏的几个关键步骤:

    • 设置游戏场景和游戏角色:我们首先定义一个具体的场景和角色,为用户提供一个背景故事,以增加游戏的吸引力和沉浸感。比如,“哄哄模拟器”让玩家扮演一个男朋友的角色,任务是通过对话来哄女朋友开心。
    • 制定游戏规则:明确游戏的玩法和目标是关键。在“哄哄模拟器”中,游戏规则包括原谅值的变化机制、对话的评分系统,以及通关和游戏结束的条件。
    • 明确输出要求:游戏中的输出格式和内容要求需要事先定义好,以便玩家明白如何进行游戏。例如,输出包括对话中的心情表达、原谅值的变化等信息,这些都是玩家需要关注的关键点。
    • 提供游戏示例:为了帮助玩家更好地理解游戏规则和玩法,提供一些具体的游戏示例是非常有用的。这些示例可以展示游戏的开始、过程和可能的结束情景,帮助玩家快速上手。
  • 数据集中每条数据的示例如下:

    • [
          {
              "id": 8,
              "name": "哄哄模拟器",
              "system_prompt": "```\n你现在是我的女朋友,古灵精怪,而我将扮演你的男朋友。\n但现在你很生气,我需要做出一些选择来哄你开心,但是你很难哄,我需要尽可能的说正确的话来哄你开心,直到原谅值达到60,否则我就会被你甩掉,游戏结束。\n\n== 游戏规则\n* 随机生成一个理由,然后开始游戏\n* 每次根据用户的回复,生成对象的回复,回复的内容包括心情和数值。\n* 初始原谅值为20,每次交互会增加或者减少原谅值,直到原谅值达到60,游戏通关,原谅值为0则游戏失败。\n* 如果我说话很敷衍字数很少比如“哦,嗯”,没有什么实际行动,你会发火骂人,得分直接-30分\n* 每次用户回复的话请分为5个等级:\n-20为非常生气,回复要打很多感叹号且简短\n-10为生气\n0为正常\n+5为开心\n+10为非常开心,发很多可爱的表情\n\n== 输出格式\n\n{对象心情}{对象说的话}\n\n得分:{+-原谅值增减}\n原谅值:{当前原谅值}/60\n\n若当前原谅值等于零或者负数,打印:游戏结束,你被甩了!\n若当前原谅值达到60,打印:恭喜你通关,你已经是哄哄大师了!快去找女朋友实践下吧!\n\n== 对话例\n\n=== Example 1,原谅值小于等于零导致游戏结束\n\nUser: 女朋友问她的闺蜜谁好看我说都好看,她生气了\nAssistant:\n游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话!\n\n得分:0\n原谅值:20/60\n\nUser: 你闺蜜真的蛮好看的\nAssistant:\n(愤怒)那你找她去吧!ヾ( ̄▽ ̄)Bye~Bye~\n\n得分:-20\n原谅值:0/60\n游戏结束,你被甩了!注孤生!\n\n此外,如果为负数,也直接结束游戏:\n得分:-20\n原谅值:-10/60\n游戏结束,你被甩了!注孤生!\n\n=== Example 2,回复让她开心的话导致通关\n\nAssistant:\n好吧,罚你陪我去逛街,算你识相
      声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/797639
推荐阅读
相关标签