【AI】创建自己的基于会话的自定义模型的ChatGPT

2024-09-10T13:11:32+08:00 | 11分钟阅读 | 更新于 2024-09-10T13:11:32+08:00

Macro Zhao

【AI】创建自己的基于会话的自定义模型的ChatGPT

@TOC

推荐超级课程:

开篇

在我的上一篇文章 中,我介绍了一款定制化ChatGPT的开发过程,利用了OpenAI GPT-3.5 LLM、Langchain和LlamaIndex,特别强调了整合独特数据集的过程。 本文探讨了基于会话的自定义ChatGPT模型(ChatBot)的创建,利用了OpenAI GPT-4 LLM和Langchain的ConversationalRetrievalChain更高级的功能。 ConversationalRetrievalChain结合了语言模型和外部信息检索系统(例如文档、数据库数据、网站数据等)的能力。 其目的是通过最新的外部信息增强语言模型的回复。 它通过首先使用信息检索系统来获取来自外部来源的相关信息,然后将这些信息传递给语言模型,并将其融入回复中。

利用ConversationalRetrievalChain功能,Chatbot整合了一个定制数据集,该数据集通过Langchain的RecursiveUrlLoader从在线网站动态获取。用户可以通过由OpenAI的GPT-4 LLM驱动的Chatbot与网站数据进行交互。为演示目的,我选择了Open5GS文档网站(Open5GS是5G核心的C语言实现)。 使用Langchain的RecursiveUrlLoader从Open5GS文档中获取的数据被拆分,然后存储在Chroma向量数据库中作为向量嵌入。 因此,用户可以通过使用GPT-4 LLM构建的Chatbot无缝地与Open5GS文档的内容交互。GPT-4 LLM根据Open5GS文档中的内容为用户提供答案。

此API的一个关键功能是同时处理来自多个用户的请求,并有效地管理它们各自的会话。 这一功能主要依赖于将MongoDB集成到聊天会话内存管理中。 这种设置确保所有用户聊天历史都持久存储在MongoDB中,方便检索过去的互动。这样的记忆系统对于保持丰富、上下文感知的对话历史至关重要,增强了ChatBot进行更有意义和相关性的讨论的能力,这些讨论受过往互动知识的启发。

功能设计

以下部分讨论了Chatbot的主要功能。涵盖这些不同组件的综合功能架构在下面的图中有详细说明。

步骤详解

1. 爬取Web数据

Langchain提供不同类型的文档加载器 来从不同来源加载数据作为DocumentRecursiveUrlLoader是这样一种文档加载器,可以用来将web url中的数据加载到文档中。这一步利用Langchain的RecursiveUrlLoader从web中爬取数据作为文档。RecursiveUrlLoader会递归地爬取给定的url到指定的max_depth,读取web上的数据。这些数据用于创建向量嵌入并回答用户的问题。

2. 拆分文档

处理长文本时,将文本分割成较小的段落至关重要。虽然这项任务看似简单,但实际上可能包含相当大的复杂性。目标是确保语义相关的文本段保持在一起。Langchain文本拆分器有效地完成了这项任务。它将文本分成小的、语义上有意义的单元(通常是句子)。然后这些较小的段落被组合成更大的块,直到达到特定函数确定的一定大小。达到这个大小后,该块被标记为单独的文本片段,然后过程重复开始,有一定的重叠。在这种特定情况下,我使用了RecursiveCharacterTextSplitter将爬取的文档拆分成可管理的块。

3. 创建向量嵌入

一旦数据收集完毕并拆分,下一步涉及将这些文本信息转换为向量嵌入。然后从拆分的数据创建这些嵌入。文本嵌入对LLM操作至关重要。尽管使用自然语言处理语言模型技术是可以的,但存储和检索这样的数据效率非常低。为了增强效率,必须将文本数据转换为向量形式。有专门的机器学习模型专门设计用于从文本创建嵌入。在这种情况下,我使用OpenAIEmbeddings生成向量嵌入。因此,文本被转换成多维向量,从根本上是捕捉语义含义和上下文细微差别的高维数值表示。一旦嵌入,这些数据可以分组、排序、搜索等。我们可以计算两个句子之间的距离,以确定它们的相关程度。重要的是,这些操作超越了传统依赖关键字的数据库搜索,而是捕捉了句子之间的语义接近度。

4. 将向量嵌入存储在Chroma中

生成的向量嵌入然后存储在Chroma向量数据库中。Chroma(通常称为ChromaDB)是一个开源嵌入数据库,通过存储和检索嵌入及其元数据、文档和查询,使构建LLM应用变得容易。Chroma高效地处理这些嵌入,允许快速检索和比较基于文本的数据。传统数据库可用于准确查询,但在理解人类语言的细微差别方面表现不佳。引入向量数据库,是处理语义搜索的一个重要改变。与传统文本匹配依赖精确词语或短语不同,像Postgres with pgvector这样的向量数据库会语义化地处理信息。该数据库是系统匹配用户查询与来自爬取内容中最相关信息之间的能力的基石,实现快速准确的响应。

5. 用户提出问题

系统提供一个API,用户可以通过该API提交问题。在这个用例中,用户可以提出与Open5GS文档内容相关的任何问题。该API作为用户与ChatBot之间的主要交互界面。API接受一个名称为user_id的参数,用于标识不同的用户会话。这个user_id仅用于演示目的。在实际情况下,它可以在HTTP请求中通过授权头(例如JWT承载令牌)进行管理。API设计直观且易于访问,使用户能够轻松输入他们的查询并接收响应。

6. 创建提问的向量嵌入

当用户通过API提交问题时,系统会将这个问题转换成一个向量嵌入。这个嵌入的生成由ConversationalRetrievalChain自动处理。这有助于在向量数据库中语义搜索与问题相关的文档。

7. 语义搜索向量数据库

一旦为问题创建了向量嵌入,系统使用语义搜索扫描向量数据库,识别与用户查询最相关的内容。通过将问题的向量嵌入与存储数据的向量嵌入进行比较,系统可以准确地找到与查询的语境相似或相关的信息。在这种情况下,我使用ConversationalRetrievalChain,该工具会自动处理基于输入查询的语义搜索。语义搜索的结果然后被确定为LLM的“上下文”。

8. 生成提示

接下来,ConversationalRetrievalChain根据用户的问题和语义搜索结果(上下文)生成一个自定义提示 。对于语言模型来说,提示是用户提供的一组指示或输入,用于引导模型的响应。这有助于模型理解上下文并生成相关和连贯的基于语言的输出,如回答问题、完成句子或参与对话。

9. 提交提示到LLM

生成提示后,将其发布到LLM(在我们的情况下是OpenAI GPT-4 LLM)。LLM然后根据提供的上下文找到问题的答案。ConversationalRetrievalChain处理将查询提交给LLM的功能(在幕后,它使用OpenAI API提交问题)。

10. LLM生成答案

LLM利用OpenAI GPT-4的先进功能,在提供的内容的上下文中处理问题。然后生成一个响应并将其发送回去。

11. 在MongoDB中保存查询和响应的聊天历史记录

Langchain提供各种组件来管理对话记忆。在这个ChatBot中,已经采用了MongoDB来管理对话记忆。在这个阶段,用户的问题和ChatBot的回应都被记录在MongoDB存储中作为聊天历史的一部分。这种方法确保了所有用户的聊天历史都被持久地存储在MongoDB中,从而能够检索以前的互动。数据以每个用户会话为基础存储在MongoDB中。为了区分用户会话,API利用了之前提到的user_id参数。这些历史数据对塑造未来的互动至关重要。当同一用户提出后续问题时,聊天历史以及新的语义搜索结果(上下文)被传递到LLM。这个过程确保了ChatBot在对话中能够保持上下文,从而产生更加精确和定制的回应。

12. 将答案发送回用户

最后,从LLM收到的答案通过HTTP API转发给用户。用户可以通过提供相同的user_id在后续请求中继续提出不同的问题。然后系统识别用户的聊天历史并将其包含在发送给LLM的信息中,同时还包括了新的语义搜索结果。这个过程确保了一次无缝且具有上下文意识的对话,通过每一次互动丰富用户体验。

实现

下面详细介绍了这个ChatBot的完整实现。

1. 配置

config.py文件中,我定义了ChatBot中使用的各种配置。这些配置是通过环境变量读取的,遵循12因素应用程序 的原则。

import os

# openai api key
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '')

# define init index
INIT_INDEX = os.getenv('INIT_INDEX', 'false').lower() == 'true'

# vector index persist directory
INDEX_PERSIST_DIRECTORY = os.getenv('INDEX_PERSIST_DIRECTORY', "./data/chromadb")

# target url to scrape
TARGET_URL =  os.getenv('TARGET_URL', "https://open5gs.org/open5gs/docs/")

# http api port
HTTP_PORT = os.getenv('HTTP_PORT', 7654)

# mongodb config host, username, password
MONGO_HOST = os.getenv('MONGO_HOST', 'localhost')
MONGO_PORT = os.getenv('MONGO_PORT', 27017)
MONGO_USER = os.getenv('MONGO_USER', 'testuser')
MONGO_PASS = os.getenv('MONGO_PASS', 'testpass')

2. HTTP API

HTTP API的实现在api.py中进行。这个API包括一个HTTP POST端点api/question,接受一个包含questionuser_id的JSON对象。user_id被用于演示目的。在真实的应用中,这可以通过HTTP请求中的Authorization header(例如,JWT Bearer token)进行管理。当从用户收到问题请求时,它将被转发到ChatBot模型中的chat函数。

from flask import Flask
from flask import jsonify
from flask import request
from flask_cors import CORS
import logging
import sys
from model import init_index
from model import init_conversation
from model import chat
from config import *

app = Flask(__name__)
CORS(app)

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

@app.route('/api/question', methods=['POST'])
def post_question():
    json = request.get_json(silent=True)
    question = json['question']
    user_id = json['user_id']
    logging.info("post question `%s` for user `%s`", question, user_id)

    resp = chat(question, user_id)
    data = {'answer':resp}

    return jsonify(data), 200

if __name__ == '__main__':
    init_index()
    init_conversation()
    app.run(host='0.0.0.0', port=HTTP_PORT, debug=True)
    

3. ChatBot模型

以下是ChatBot模型的实现。它包括一个名为 init_index 的函数,该函数从给定的网址上抓取数据并创建向量存储。一个名为 INIT_INDEX 的环境变量用于确定是否创建索引。init_conversation 函数初始化 LLMChain 并设置自定义提示,而 chat 函数负责向LLM发送问题。在前面的示例中,vectordb 中的文档搜索功能由 ConversationalRetrievalChain 自动处理。在这种情况下,我进行了手动的语义搜索,以在 vectordb 中找到与用户问题相关的文档。然后将这些搜索结果与自定义提示一起传递给 LLM。

import os
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.llms import OpenAI
from langchain.memory import MongoDBChatMessageHistory
from langchain.document_loaders.recursive_url_loader import RecursiveUrlLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from bs4 import BeautifulSoup as Soup
from langchain.utils.html import (PREFIXES_TO_IGNORE_REGEX,
                                  SUFFIXES_TO_IGNORE_REGEX)
import logging
import sys
from config import *

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

global conversation
global vectordb
conversation = None
vectordb = None

def init_index():
    if not INIT_INDEX:
        logging.info("continue without initializing index")
        return

    logging.info("initializing index from `%s`", TARGET_URL)

    # scrape data from web
    documents = RecursiveUrlLoader(
        TARGET_URL,
        max_depth=4,
        extractor=lambda x: Soup(x, "html.parser").text,
        prevent_outside=True,
        use_async=True,
        timeout=600,
        check_response_status=True,
        # drop trailing / to avoid duplicate pages.
        link_regex=(
            f"href=[\"']{PREFIXES_TO_IGNORE_REGEX}((?:{SUFFIXES_TO_IGNORE_REGEX}.)*?)"
            r"(?:[\#'\"]|\/[\#'\"])"
        ),
    ).load()

    logging.info("index created with `%d` documents", len(documents))

    # split text
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=4000, chunk_overlap=200)
    spllited_documents = text_splitter.split_documents(documents)

    # create embedding and persist on vector db
    embeddings = OpenAIEmbeddings()
    vectordb = Chroma.from_documents(
        documents=spllited_documents,
        embedding=embeddings,
        persist_directory=INDEX_PERSIST_DIRECTORY
    )
    vectordb.persist()

def init_conversation():
    global vectordb
    global conversation

    # load index
    embeddings = OpenAIEmbeddings()
    vectordb = Chroma(persist_directory=INDEX_PERSIST_DIRECTORY,embedding_function=embeddings)

    # create conversation with gpt-4 llm
    # max_tokens - define maximum output token limit from gpt
    # max_tokens_limit - defines maximum input token limit to gpt
    conversation = ConversationalRetrievalChain.from_llm(
        ChatOpenAI(temperature=0.7, model_name="gpt-4", max_tokens=1024),
        vectordb.as_retriever(),
        max_tokens_limit=8192,
        return_source_documents=True,
        verbose=True,
    )

    return conversation

def chat(question, user_id):
    global conversation

    # constrct chat history with mongodb
    # session_id is the user_id which comes through http request
    # mongo connection string (e.g mongodb://localhost:27017/)
    connection_string = f"mongodb://{MONGO_HOST}:{MONGO_PORT}/"
    history = MongoDBChatMessageHistory(
        connection_string=connection_string, session_id=user_id
    )

    # gpt conversation
    result = conversation({"question": question, "chat_history": history.messages})
    answer = result['answer']

    logging.info("got response from llm - %s", answer)

    # save history
    history.add_user_message(question)
    history.add_ai_message(answer)

    return answer
    

运行应用程序

以下是操作ChatBot应用程序并与其交互的主要步骤。可以使用HTTP API提交问题,并相应地接收答复。

1. 安装依赖项

在这个应用程序中,我利用了许多Python包,需要使用Python的pip包管理器安装才能运行应用程序。requirements.txt文件列出了所有必要的包。这些包可以通过执行命令pip install -r requirements.txt来轻松安装。

Flask==2.0.1
Werkzeug==2.2.2
flask-cors
pymongo
seaborn
openai
langchain==0.0.352
BeautifulSoup4
chromadb==0.3.29
Cython 
tiktoken

2. 运行 MongoDB

# run mongodb with docker
❯❯ docker run -d -p 27017:27017 --name test-mongo mongo:latest

# running container
❯❯ docker ps
CONTAINER ID  IMAGE                           COMMAND     CREATED       STATUS       PORTS                     NAMES
ba66af3866b1  docker.io/library/mongo:latest  mongod      24 hours ago  Up 24 hours  0.0.0.0:27017->27017/tcp  test-mongo

3. 运行ChatBot

ChatBot可以通过下面列出的api.py启动。在运行之前,需要通过环境变量设置一些配置。一旦执行app.py,它将启动HTTP API,使用户能够发布他们的问题。

# set openai API key
❯❯ export OPENAI_API_KEY=<add openai api key here>

# init index determine weather needs to create the index
❯❯ export INIT_INDEX=true

# mongo db host(local machine ip in this case)
❯❯ export MONGO_HOST=10.13.40.29

# run the chatbot application
❯❯ python api.py
2023-12-28 11:19:19,796 - INFO - initializing index from `https://open5gs.org/open5gs/docs/`
2023-12-28 11:19:20,515 - INFO - index created with `18` documents
2023-12-28 11:19:20,614 - INFO - Note: NumExpr detected 12 cores but "NUMEXPR_MAX_THREADS" not set, so enforcing safe limit of 8.
2023-12-28 11:19:20,614 - INFO - NumExpr defaulting to 8 threads.
2023-12-28 11:19:20,725 - INFO - Anonymized telemetry enabled. See https://docs.trychroma.com/telemetry for more information.
2023-12-28 11:19:20,825 - INFO - loaded in 258 embeddings
2023-12-28 11:19:20,827 - INFO - loaded in 1 collections
2023-12-28 11:19:20,829 - INFO - collection with name langchain already exists, returning existing collection
2023-12-28 11:19:24,212 - INFO - Persisting DB to disk, putting it in the save folder: ./data/chromadb
2023-12-28 11:19:24,248 - INFO - Anonymized telemetry enabled. See https://docs.trychroma.com/telemetry for more information.
2023-12-28 11:19:24,272 - INFO - loaded in 344 embeddings
2023-12-28 11:19:24,272 - INFO - loaded in 1 collections
2023-12-28 11:19:24,273 - INFO - collection with name langchain already exists, returning existing collection
 * Serving Flask app 'api' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
2023-12-28 11:19:24,468 - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:7654
 * Running on http://10.13.40.29:7654
2023-12-28 11:19:24,469 - INFO - Press CTRL+C to quit
2023-12-28 11:19:24,469 - INFO -  * Restarting with stat
2023-12-28 11:19:25,127 - INFO - initializing index from `https://open5gs.org/open5gs/docs/`
2023-12-28 11:19:25,481 - INFO - index created with `18` documents
2023-12-28 11:19:25,578 - INFO - Note: NumExpr detected 12 cores but "NUMEXPR_MAX_THREADS" not set, so enforcing safe limit of 8.
2023-12-28 11:19:25,578 - INFO - NumExpr defaulting to 8 threads.
2023-12-28 11:19:25,705 - INFO - Anonymized telemetry enabled. See https://docs.trychroma.com/telemetry for more information.
2023-12-28 11:19:25,825 - INFO - loaded in 344 embeddings
2023-12-28 11:19:25,828 - INFO - loaded in 1 collections
2023-12-28 11:19:25,831 - INFO - collection with name langchain already exists, returning existing collection
2023-12-28 11:19:28,608 - INFO - Persisting DB to disk, putting it in the save folder: ./data/chromadb
2023-12-28 11:19:28,652 - INFO - Anonymized telemetry enabled. See https://docs.trychroma.com/telemetry for more information.
2023-12-28 11:19:28,680 - INFO - loaded in 430 embeddings
2023-12-28 11:19:28,680 - INFO - loaded in 1 collections
2023-12-28 11:19:28,681 - INFO - collection with name langchain already exists, returning existing collection
2023-12-28 11:19:28,684 - WARNING -  * Debugger is active!
2023-12-28 11:19:28,694 - INFO -  * Debugger PIN: 973-611-562

4. 提问题

一旦ChatBot应用程序正在运行,我可以通过HTTP API提交与Open5GS文档有关的问题。

# ask question
curl -i -XPOST "http://localhost:7654/api/question" \
--header "Content-Type: application/json" \
--data '
{
  "question": "what is open5gs",
  "user_id": "test"
}
'

# response status
HTTP/1.1 200 OK
Server: Werkzeug/2.3.7 Python/3.11.2
Date: Thu, 28 Dec 2023 17:12:29 GMT
Content-Type: application/json
Content-Length: 131
Access-Control-Allow-Origin: *
Connection: close


# response
{
  "answer": "Open5GS is a C-language implementation of 5G xxx"
}

---


# ask next question
curl -i -XPOST "http://localhost:7654/api/question" \
--header "Content-Type: application/json" \
--data '
{
  "question": "what are the enodebs and gnodebs tested in open5gs",
  "user_id": "test"
}
'
# response
{
  "answer": "The eNodeBs and gNodeBs xxxx"
}



---



# ask question with previous context
❯❯ curl -i -XPOST "http://localhost:7654/api/question" \
--header "Content-Type: application/json" \
--data '
{
  "question": "give more information about it",
  "user_id": "test"
}
'

参考

© 2011 - 2025 Macro Zhao的分享站

关于我

如遇到加载502错误,请尝试刷新😄

Hi,欢迎访问 Macro Zhao 的博客。Macro Zhao(或 Macro)是我在互联网上经常使用的名字。

我是一个热衷于技术探索和分享的IT工程师,在这里我会记录分享一些关于技术、工作和生活上的事情。

我的CSDN博客:
https://macro-zhao.blog.csdn.net/

欢迎你通过评论或者邮件与我交流。
Mail Me

推荐好玩(You'll Like)
  • AI 动·画
    • 这是一款有趣·免费的能让您画的画中的角色动起来的AI工具。
    • 支持几十种动作生成。
我的项目(My Projects)
  • 爱学习网

  • 小乙日语App

    • 这是一个帮助日语学习者学习日语的App。
      (当然初衷也是为了自用😄)
    • 界面干净,简洁,漂亮!
    • 其中包含 N1 + N2 的全部单词和语法。
    • 不需注册,更不需要订阅!完全免费!
  • 小乙日文阅读器

    • 词汇不够?照样能读日语名著!
    • 越读积累越多,积跬步致千里!
    • 哪里不会点哪里!妈妈再也不担心我读不了原版读物了!
赞助我(Sponsor Me)

如果你喜欢我的作品或者发现它们对你有所帮助,可以考虑给我买一杯咖啡 ☕️。这将激励我在未来创作和分享更多的项目和技术。🦾

👉 请我喝一杯咖啡

If you like my works or find them helpful, please consider buying me a cup of coffee ☕️. It inspires me to create and share more projects in the future. 🦾

👉 Buy me a coffee