วิธีสร้าง Chatbot และ RAG ด้วย OpenAI และ Llamaindex backend

*แนะนำบทความ -> สร้าง RAG ภาษาไทยด้วย LlamaIndex, Weaviate และ SeaLLM*

LLM มีส่วนสำคัญอย่างมากในการพัฒนา chatbot ในปัจจุบัน แต่ทว่าในข้อจำกัดการเรียนรู้ข้อมูลใหม่ๆ เพื่อให้ตอบโจทย์การใช้งานของผู้ใช้โดยทันที อาจจะเป็นไปได้ยาก

ตัวอย่างนี้จะเป็นการนำ RAG กับ LLM มาใช้งานร่วมกัน เพื่อให้ตอบโจทย์การทำงานของเรามากที่สุด เป็นการสร้าง Chatbot ที่ตอบคำถามจาก คลังความรู้ของตัวมันเองร่วมกับ Document ที่เราจัดเตรียมไว้ให้ เพื่อให้ได้ออกมาเป็นคำตอบที่ดีที่สุด

และสำหรับงานของ Backend ในตัวอย่างนี้เรามีส่วนร่วมอย่างไรบ้าง

ทำ function ในการแยกและจัดเก็บ embedding document.

LLM function ในการตัดสินใจการเลือกใช้งานแต่ละ Agent

Chatbot flow

Setting Up a FastAPI Projectเหมือนกับทุกๆตัวอย่างที่ผ่านมา เราเลือกใช้งาน FastAPI เป็น Framework ในการสร้าง API ขึ้นมาใช้งาน

ทำการติดตั้ง python3 library ที่จำเป็นสำหรับ FastAPI

pip install fastapi
pip install "uvicorn[standard]"

สร้างไฟล์ app.py และทำการ initial FastAPI ขึ้นมา

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
@app.get("/helloworld")
async def helloworld():
    return {"message": "Hello World"}

จากตัวอย่าง code จะเป็นการ initial FastAPI project แล้ว enable CORS สำหรับการเชื่อมต่อกับ frontend ให้เรียบร้อย และการสั่ง run จะใช้คำสั่งว่า

uvicorn app:app

เป็นการสั่งให้ FastAPI ที่เราประกาศไว้ในไฟล์ app.py ทำงาน โดย server จะรันที่ port 8000 เป็น default

จากนั้นเราจะสร้าง folder data และ storage เพื่อเอาไว้เก็บ document ตัวอย่าง และ storage ของ vector DB

Preparation and Ingest Dataเริ่มต้นเราต้องมาเตรียม env และ OpenAI-key ที่ต้องใช้งานก่อน

import os
import openai
import dotenv
from llama_hub.file.unstructured.base import UnstructuredReader
from pathlib import Path
from llama_index import VectorStoreIndex, ServiceContext, StorageContext
from llama_index import load_index_from_storage
from llama_index.tools import QueryEngineTool, ToolMetadata
from llama_index.query_engine import SubQuestionQueryEngine
from llama_index.agent import OpenAIAgent
import nest_asyncio
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
dotenv.load_dotenv()
openai.api_key = os.environ["OPENAI_API_KEY"]
nest_asyncio.apply()
agent = None

ต่อไปเราจะทำการ Load ข้อมูลเข้าไปเก็บยัง VectorDB โดยข้อมูลที่ใช้เราได้นำตัวอย่างข้อมูลมาจาก raw UBER 10-K HTML ที่มีข้อมูลตั้งแต่ปี 2019-2022 แต่ในตัวอย่างนี้เราได้ทำการ download และเก็บไว้ใน folder data ให้เรียบร้อยแล้ว

ขั้นตอนต่อไปเราจะทำการ load ข้อมูลจาก RAW file เข้าไปยัง VectorDB และก่อนอื่นเราต้องติดตั้ง library ที่จำเป็นซะก่อน

pip install llama-hub unstructured

สร้าง function ในการ load ข้อมูลให้อยู่ในรูปแบบ document

def read_data(years):
    loader = UnstructuredReader()
    doc_set = {}
    all_docs = []
    for year in years:
        year_docs = loader.load_data(
            file=Path(f"./data/UBER/UBER_{year}.html"), split_documents=False
        )
        # insert year metadata into each year
        for d in year_docs:
            d.metadata = {"year": year}
        doc_set[year] = year_docs
        all_docs.extend(year_docs)
    return doc_set

ทำการ loade ข้อมูลที่เป็น document และจัดเก็บลง VectorDB แต่ว่าเราจะสร้างโดยแบ่งตามปีของข้อมูลที่เรามี

def store_data(years, doc_set,service_context):
    index_set = {}
    for year in years:
        storage_context = StorageContext.from_defaults()
        cur_index = VectorStoreIndex.from_documents(
            doc_set[year],
            service_context=service_context,
            storage_context=storage_context,
        )
        index_set[year] = cur_index
        storage_context.persist(persist_dir=f"./storage/{year}")
    return index_set

Setting up a Sub Question Query Engine

function ต่อไปเราจะทำการสร้าง Query Engine สำหรับ data แต่ละปีขึ้นมา โดยเริ่มจากการ load index ขึ้นมาจาก VectorDB แบ่งเป็นข้อมูลแต่ละปี

def load_data(years,service_context):
    index_set = {}
    for year in years:
        storage_context = StorageContext.from_defaults(
            persist_dir=f"./storage/{year}"
        )
        cur_index = load_index_from_storage(
            storage_context, service_context=service_context
        )
        index_set[year] = cur_index
    return index_set

และเอาชุดของ Index ที่ได้ออกมาแต่ละปีไปสร้าง Query engine ของแต่ละชุดข้อมูล

def create_individual_query_tool(index_set,years):
    individual_query_engine_tools = [
        QueryEngineTool(
            query_engine=index_set[year].as_query_engine(),
            metadata=ToolMetadata(
                name=f"vector_index_{year}",
                description=f"useful for when you want to answer queries about the {year} SEC 10-K for Uber",
            ),
        )
        for year in years
    ]
    return individual_query_engine_tools

ซึ่งตรงนี้ในส่วนของ description เราจะกำหนดไว้ว่า เราจะใช้งาน engine นี้สำหรับการค้นหาข้อมูลของปีที่ระบุเท่านั้น

Synthesize answers across the datafunction นี้จะเป็นการสร้างคำถามสำหรับ individual_query_engine_tools แต่ละตัวเพื่อให้หาคำตอบของตัวเองออกมา

def create_synthesizer(individual_query_engine_tool,service_context):
    query_engine = SubQuestionQueryEngine.from_defaults(
        query_engine_tools=individual_query_engine_tool,
        service_context=service_context,
    )
    return query_engine

จากนั้นเราจะทำการสร้าง QueryEngineTool ที่ใช้ SubQuestionQueryEngine ขึ้นมาโดย

def create_sub_question_tool(query_engine):
    query_engine_tool = QueryEngineTool(
        query_engine=query_engine,
        metadata=ToolMetadata(
            name="sub_question_query_engine",
            description="useful for when you want to answer queries that require analyzing multiple SEC 10-K documents for Uber",
        ),
    )
    return query_engine_tool

ถึงตรงนี้เราจะมี QueryEngineTool ที่พร้อมใช้งานแล้วคือ

QueryEngineTool ของแต่ละปี หากมีการถามคำถามที่ระบุปีที่ชัดเจน (individual_query_engine_tool)

QueryEngineTool ที่สามารถ query ข้อมูลของทุกปีได้ (sub_question_query_engine)

Create General engineEngine ตัวสุดท้ายที่เราจะสร้างก็จะทำหน้าที่เป็น querytool ที่เอาไว้ค้นหาข้อมูลที่ไม่ได้อยู่ในขอบเขตของ data ที่เราเตรียมไว้ หรือจะเรียกว่าเป็น chatbot สำหรับตอบคำถามทั่วๆไปก็ได้

def agent_chat():
    chat_engine_tool = [
        QueryEngineTool(
            query_engine=OpenAIAgent.from_tools([]),
            metadata=ToolMetadata(
                name="gpt_agent", description="Agent that can answer the general question."
            ),
        ),
    ]
    return chat_engine_tool

อันนี้ก็จะเป็น QueryEngineTool ตัวที่ 3 ที่เราจะใช้งานสำหรับตัวอย่างนี้

Create OpenAIAgent from toolsขั้นตอนนี้เราจะทำการรวม QueryEngineTool ที่เราสร้างมาทั้งหมด ให้อยู่ใน List เดียวกันสำหรับเป็น Agent ที่พร้อมให้ LLM ตัดสินใจเลือกใช้งาน

def build_chat_engine(individual_query_engine_tools,query_engine_tool,gpt_agent):
    global agent 
    tools = gpt_agent + individual_query_engine_tools + [query_engine_tool]
    agent = OpenAIAgent.from_tools(tools, verbose=False)
    return agent

จาก Topic ทั้งหมดที่เราเล่ามานั้นจะเป็นส่วนของ Function ที่ต้องใช้งานทั้งหมดสำหรับการสร้าง API ในส่วนถัดไปเราจะมาพูดถึงการสร้าง API ที่จะเอาไป integrate กับ frontend ต่อไป

Create RAG APIเริ่มต้นด้วยการสร้าง endpoint สำหรับการ load ข้อมูลเข้าไปเก็บที่ VertorDB

@app.post('/buildRAG')
def build_RAG():
    years = [2022, 2021, 2020, 2019]
    service_context = ServiceContext.from_defaults(chunk_size=512)
    doc_set = read_data(years)
    store_data(years,doc_set,service_context)
    # index_set = load_data(years,service_context)

    return JSONResponse({'status': 'success'})

Create chat endpointAPI นี้เราจะใช้สำหรับการรับคำถามเข้ามาและนำไปประมวลผลหาคำตอบที่เราต้องการ

@app.post('/chat')
def chat(query : str = 'What were some of the biggest risk factors in 2022 for Uber?'):
    global agent
    years = [2022, 2021, 2020, 2019]
    service_context = ServiceContext.from_defaults(chunk_size=512)
    index_set = load_data(years,service_context)
    individual_query_engine_tools = create_individual_query_tool(index_set,years)
    query_engine = create_synthesizer(individual_query_engine_tools,service_context)
    query_engine_tool = create_sub_question_tool(query_engine)
    gpt_agent = agent_chat()
    if(agent == None):
        print('agent is none')
        agent = build_chat_engine(individual_query_engine_tools,query_engine_tool,gpt_agent)
    
    answer = agent.chat(query)

    return JSONResponse({
        'answer': str(answer)
    })


ตัว API จะเป็นคนตัดสินใจเองว่าจะใช้งาน Agent ไหนสำหรับตอบคำถามนั้น ซึ่งในระหว่างทางจะประกอบไปด้วย function ต่างๆที่เราได้เล่าถึงไปในตอนแรก และเราจะได้รับคำตอบคือไปในรูปแบบ string format

Create utility endpointนอกจากนี้เรายังมี API อีก 2 ตัวที่เอามาใช้งานร่วมด้วยแบ่งออกเป็น

1. Reset ข้อมูลของ Agent ที่สร้างขึ้นมา

@app.post('/resetChat')
def resetChat():
    global agent
    agent.reset()
    return JSONResponse({
        'status' : 'complete'
    })

2. การใช้งานในรูปแบบ chatbot ปกติโดยไม่ผ่าน agent

@app.post('/chatWithoutRAG')
def chatWithoutRAG(query : str = 'What were some of the biggest risk factors in 2022 for Uber?'):
    gpt_agent = agent_chat()
    agent = OpenAIAgent.from_tools(gpt_agent, verbose=False)
    answer = agent.chat(query)

    return JSONResponse({
        'answer': str(answer)
    })


Deploying and Monitoring on EC2มาถึงขั้นตอนในการ deploy FastAPI เพื่อใช้งานบน EC2 instance โดยจะมีขั้นตอนที่ไม่ยากเลยดังนี้

Step 1: สร้าง session โดยค่า name ให้เราเปลี่ยนเป็นชื่อที่เราต้องการได้

screen -S name

Step 2: เข้าไปยัง folder ของ api

cd path/to/api

Step 3: สั่ง start FastAPI ให้รันที่ port 8000

uvicorn app:app --host 0.0.0.0

Step 4: ออกจาก session screen ปัจจุบัน (detach)

Ctrl+a d

เราก็จะได้ server ทำงานอยู่ background สำหรับการใช้งานได้ แม้เราออกจาก server ไปแล้ว

Setting Up API Gateway and Implementing CORSและในการนำไปใช้งานที่สะดวกมากขึ้น เราจะทำการ implement ตัว API ที่เราทำเข้ากับ API Gateway เพื่อให้การ manage และจำกัดการใช้งาน

ทำการเพิ่ม configuration สำหรับ allow CORS ให้กับ FastAPI

from fastapi.middleware.cors import CORSMiddleware
app.app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

จากนั้นเราจะเชื่อมต่อ server นี้ไปยัง AWS API Gateway เพื่อให้ API Gateway ช่วยในการจัดการเรื่อง authentication และ usage ต่างๆสำหรับคนที่จะเข้ามาใช้งานในแต่ละ request ที่เกิดขึ้น และทางเราได้มี document ที่พูดถึงการใช้งาน API Gateway · VulturePrime ไว้สำหรับทุกคนแล้ว

Conclusionสรุปเรื่องราวทั้งหมดที่เราได้ลงมือทำในตัวอย่างนี้ เราได้ทำการสร้าง RAG ที่เป็นชุดข้อมูล 4 ปีข้อง Uber และสร้าง QueryEngineTool ที่เอาไว้ค้นหาข้อมูลรายปี, ค้นหาข้อมูลจากทุกปี และค้นหาข้อมูลทั่วไปนอกเหนือจากชุดข้อมูลดังกล่าว ซึ่งผมว่าตัวอย่างนี้เหมาะแก่การนำไปปรับใช้กับทุกทีมที่อยากได้ chatbot มาช่วยงานในทีม ที่สามารถตอบได้ทั้งข้อมูลภายในและข้อมูลทั่วไปได้ในครั้งเดียว

Aa

© 2023, All Rights Reserved, VulturePrime co., ltd.