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

ในปัจจุบันมีการใช้ AI อย่างแพร่หลาย ซึ่งเราจะเห็นหลายเจ้าเริ่มนำ AI มาใช้งาน ยกตัวอย่างเช่น Shopee เมื่อก่อนเวลาเราไปสอบถามข้อมูลในแชทกับ Shopee ก็จะมีพนักงานมาตอบคำถามให้กับเรา

 เเต่ปัจจุบันนี้ Shopee ก็มาใช้ระบบ chat bot มาช่วยตอบคำถามลูกค้า ซึ่งเมื่อ shopee มีลูกค้ามากขึ้น เเต่พนักงานที่มีอยู่นั้นมีน้อยกว่าจำนวนลูกค้า ดังนั้นในการตอบคำถามลูกค้าก็จะต้องใช้เวลารอค่อนข้างนาน เมื่อเราใน AI มาช่วยตอบคำถามใน chat  จะช่วยให้สามารถตอบได้อย่างรวดเร็ว

ในโปรเจ็ควันนี้ที่เราจะทำกันคือ  Q&A Chatbot เมื่อ User พิมพ์ prompt คำถามเข้าไป LLMs  จะทำการตัดสินใจว่าควรจะไปถาม RAG หรือ LLMs  หลังจากที่ Chatbot ตัดสินใจแล้ว เช่นจะถาม RAG ก็จะ generate prompt เพื่อนำไปใช้ถาม RAG  เมื่อเราถามคำถามในครั้งถัดไป ก็จะนำ promptที่ได้ไปquery ใน RAG หรือ LLMs อีกครั้งหนึ่ง

แล้วทำไมถึงเราต้องให้ LLMs ไปตัดสินใจก่อนละ? 🧐

การให้ Large Language Models (LLMs) ตัดสินใจก่อนว่าจะใช้ Retrieval-Augmented Generation (RAG) หรือไม่ในการตอบคำถามของผู้ใช้ใน Q&A Chatbot มีหลายเหตุผลที่สำคัญ:

  1. การเลือกแหล่งข้อมูลที่เหมาะสม: บางคำถามอาจไม่จำเป็นต้องใช้ข้อมูลจาก RAG และสามารถตอบได้โดยตรงจากความรู้ภายในของ LLMs
  2. ความถูกต้อง: LLMs สามารถประเมินว่าข้อมูลที่มีอยู่ภายในข้อมูล ที่มีมันเองมีความเพียงพอและถูกต้องสำหรับการตอบคำถามหรือไม่

โดยรวมแล้ว, การใช้ LLMs เป็นตัวตัดสินใจเบื้องต้นช่วยให้แน่ใจว่าแต่ละคำถามจะได้รับการจัดการอย่างเหมาะสม

Lib หรือ Framwork มาใช้งานดังต่อไปนี้

Next.js:

เริ่มโปรเจกต์ใหม่โดยใช้คำสั่ง create-next-app:

React Hook Form:

และ เราเลือกใช้ zod ในการจัดการ ทำ validate input form

Axios:

ติดตั้ง Axios ในโปรเจกต์:

ส่วนของ User Interface

ส่วนประกอบของ UI จะหลักจะประกอบด้วย

  1. Chat header   จะประกอบ ด้วย setting สำหรับการ chat และ ปุ่มสำหรับ reset chat
  2. Chat input  Input สำหรับพิมพ์ข้อความและส่งข้อความ
  3. Chat widget สำหรับแสดง บทสนทนา

ขั้นตอนในการสร้าง

Step 1: Define Schema and Form

ในการขั้นตอนนี้ เราจะใช้ library react-hook-form, @hookform/resolvers/zod และ 'zod'

ซึ่งในการขั้นแรกเราจะทำการสร้าง schema ขึ้นมา

ตัวของ schema จะประกอบด้วย query สำหรับ input และ bot สำหรับใช้แสดง message error สำหรับตัว bot

export const askScheme = z.object({
  query: z.string().trim().min(1, { message: 'Please enter your message' }),
  bot: z.string({}).optional(),
})

เมื่อเราทำการสร้าง schema เเล้วเราจะทำการสร้าง type interface สำหรับ Form นี้

export interface IOpenAIForm extends z.infer<typeof askScheme> {}

หลังจากนั้นเราจะทำการสร้าง methods ขึ้นมา

const methods = useForm<IOpenAIForm>({
    resolver: zodResolver(askScheme),
    shouldFocusError: true,
    defaultValues: {
      query: '',
    },
  })
  
  const { handleSubmit, setError, setValue } = methods
  
  const onSubmit = async (data: IOpenAIForm) => {
  ...TO DO Something
  }
  
  
  return (
    <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
      ...
    </FormProvider>
  )

จากตัวของ schema จะทำการ validate input ที่เรารับมาจาก user โดย หาก user ทำการ submit โดยไม่พิมพ์จะแสดง Error message ใน user interface ว่า 'Please enter your message'

ซึ่งใน component input จะใช้ lib ของ 'react-hook-form'และใช้ useFormContext, Controller

ในการจัดการ input

import { InputHTMLAttributes } from 'react'
import { useFormContext, Controller } from 'react-hook-form'
import { twMerge } from 'tailwind-merge'

// ----------------------------------------------------------------------

interface IProps extends InputHTMLAttributes<HTMLInputElement> {
  name: string
  helperText?: string
  label?: string
}

const RHFTextField = ({
  name,
  helperText,
  label,
  className,
  ...other
}: IProps) => {
  const { control } = useFormContext()

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState: { error } }) => (
        <div className='w-full flex flex-col gap-y-2 '>
          {label && <label className='text-sm font-semibold'>{label}</label>}
          <input
            {...field}
            {...other}
            className={twMerge(
              'outline-none w-full  border border-gray-200 rounded-lg px-2 py-1',
              className
            )}
          />
          {(!!error || helperText) && (
            <div className={twMerge(error?.message && 'text-rose-500 text-sm')}>
              {error?.message || helperText}
            </div>
          )}
        </div>
      )}
    />
  )
}
export default RHFTextField

ซึ่งในส่วนของการเรียกใช้ input เราจะใส่ แอตทริบิวต์ name เป็น query เหมือนใน schema ที่เราได้ทำการประกาศตัวแปรในschema นั้นไว้

const methods = useForm<IOpenAIForm>({
  resolver: zodResolver(askScheme),
  shouldFocusError: true,
  defaultValues: {
    query: '',
  },
})

const { handleSubmit, setError, setValue } = methods

const onSubmit = async (data: IOpenAIForm) => {
  console.log(data)
}


return (
  <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
      ....
     <RHFTextField
        type='text'
        placeholder='What do you need ? ...'
        className='outline-none w-full border-none'
        name='query'
      />
      <button
        type='submit'
        disabled={isLoading}
        className='text-gray-400 disabled:text-gray-200'
      >Submit</button>
  </FormProvider>
)

เมื่อเราทำการ กด submit ถ้า input validate ถูกต้องเเล้วเราจะเห็นข้อมูลที่เรา log มาจาก func onSubmit ซึ่งเราจะนำข้อมูลที่เราได้ไปเชื่อต่อกับ backend ได้เเล้ว

Step 2: Connect Backend

เราจะทำการ set default base URL ไว้

axios.defaults.baseURL = process.env.NEXT_PUBLIC_API

เมื่อทำการ set เสร็จเเล้ว เราจะทำการเชื่อม API

const onSubmit = async (data: IOpenAIForm) => {
  try {

    const { data: result } = await axios.post(
      `/chat`,
      {qurey:data.query},
    )
    console.log(result) //ค่าที่ได้จาก API
    //TO DO Something
  } catch (error) {
    const err = error as AxiosError<{ detail: string }>
    setError('bot', {
      message: err?.response?.data?.detail ?? 'Something went wrong',
    })   
  } 
}

เมื่อเราลอง submit form เราจะได้ค่าจาก api เป็นดังรูป

เราจะทำการนำข้อมูลที่ได้มาเชื่อมต่อกับ UI ของ Chat bot โดยเราจะใช้  React และ State Management

ใช้  useState จาก React จาก state management ในการจัดการ message เพื่อนำมาแสดงใน UI  โดย เราจะมี answer และ setAnswer ในการเก็บ คำถามและคำตอบของ user และ bot ซึ่งหน้าของ array จะเป็นดังนี้

[
    {
        "id": "0",
        "role": "user",
        "message": "What were some of the biggest risk factors in 2022 for Uber?",
        "raw": ""
    },
    {
        "id": "2",
        "role": "ai",
        "message": "Some of the biggest risk factors for Uber in 2022 include:\n\n1. Reclassification of drivers: There is a risk that drivers may be reclassified as employees or workers instead of independent contractors. This could result in increased costs for Uber, including higher wages, benefits, and potential legal liabilities.\n\n2. Intense competition: Uber faces intense competition in the mobility, delivery, and logistics industries. Competitors may offer similar services at lower prices or with better features, which could result in a loss of market share for Uber.\n\n3. Need to lower fares or service fees: To remain competitive, Uber may need to lower fares or service fees. This could impact the company's revenue and profitability.\n\n4. Significant losses: Uber has incurred significant losses since its inception. The company may continue to experience losses in the future, which could impact its financial stability and ability to attract investors.\n\n5. Uncertainty of achieving profitability: There is uncertainty regarding Uber's ability to achieve or maintain profitability. The company expects operating expenses to increase, which could make it challenging to achieve profitability in the near term.\n\nThese risk factors highlight the challenges and uncertainties that Uber faces in 2022.",
        "raw": "Some of the biggest risk factors for Uber in 2022 include:\n\n1. Reclassification of drivers: There is a risk that drivers may be reclassified as employees or workers instead of independent contractors. This could result in increased costs for Uber, including higher wages, benefits, and potential legal liabilities.\n\n2. Intense competition: Uber faces intense competition in the mobility, delivery, and logistics industries. Competitors may offer similar services at lower prices or with better features, which could result in a loss of market share for Uber.\n\n3. Need to lower fares or service fees: To remain competitive, Uber may need to lower fares or service fees. This could impact the company's revenue and profitability.\n\n4. Significant losses: Uber has incurred significant losses since its inception. The company may continue to experience losses in the future, which could impact its financial stability and ability to attract investors.\n\n5. Uncertainty of achieving profitability: There is uncertainty regarding Uber's ability to achieve or maintain profitability. The company expects operating expenses to increase, which could make it challenging to achieve profitability in the near term.\n\nThese risk factors highlight the challenges and uncertainties that Uber faces in 2022."
    }
]

และ เราก็จะจัดการ state สำหรับการยิง API เมื่อเราถามจะมีให้เราเลือกว่า จะใช้ RAG หรือ ไม่ โดย เราจะมี hasRag และ setHasRag ในการจัดการ state เพื่อให้เรานำค่านี้ไปใช้ check ก่อนส่ง API ว่าจะยิงไป อันไหน

import ChatWidget, { ChatProps } from '@/app/components/ChatWidget'
import FormProvider from '@/app/components/hook-form/FormProvider'
import { zodResolver } from '@hookform/resolvers/zod'
import axios, { AxiosError } from 'axios'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

export const askScheme = z.object({
  query: z.string().trim().min(1, { message: 'Please enter your message' }),
  bot: z.string({}).optional(),
})

export enum ChatType {
  Basic,
  WithoutRag,
}

axios.defaults.baseURL = process.env.NEXT_PUBLIC_API

export interface IOpenAIForm extends z.infer<typeof askScheme> {}

export default function ChatBotDemo() {
  const [answer, setAnswer] = useState<ChatProps[]>([])
  const [hasRag, setHasRag] = useState(true)

  const methods = useForm<IOpenAIForm>({
    resolver: zodResolver(askScheme),
    shouldFocusError: true,
    defaultValues: {
      query: '',
    },
  })

  const { handleSubmit, setError, setValue } = methods

  const onSubmit = async (data: IOpenAIForm) => {
    try {
      const id = answer.length
      setAnswer((prevState) => [
        ...prevState,
        {
          id: id.toString(),
          role: 'user',
          message: data.query,
          raw: '',
        },
      ])
      setValue('query', '')
      const { data: result } = await axios.post(
        `${hasRag ? '/chat' : '/chatWithoutRAG'}`,
        {
          query: data.query,
        }
      )
      setAnswer((prevState) => [
        ...prevState,
        {
          id: (prevState.length + 1).toString(),
          role: 'ai',
          message: result.answer,
          raw: result.answer,
        },
      ])
    } catch (error) {
      const err = error as AxiosError<{ detail: string }>
      setError('bot', {
        message: err?.response?.data?.detail ?? 'Something went wrong',
      })
    }
  }
  const handleChangeRag = () => {
    setHasRag(!hasRag)
  }

  return (
    <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
      <div className='flex justify-center flex-col items-center bg-white mx-auto max-w-7xl h-screen '>
        <ChatWidget answer={answer} option={{ hasRag, handleChangeRag }} />
      </div>
    </FormProvider>
  )
}

Step 3: Handle Form Submission

ในขั้นตอนนี้จะเป็นขั้นตอนสุดท้ายที่เราจะจัดการ state ต่างๆ ไม่ว่าเป็นการ load, submit และ error  เราก็จะเรียกใช้ state จาก 'useFormContext' เพื่อแสดง ซึ่งค่าที่เราใช้จะมีค่า isSubmitting, errors

export default function ChatWidget({ answer }: { answer: ChatProps[] }) {
  const chatWindowRef = useRef<HTMLDivElement>(null)
  const {
    formState: { isSubmitting, errors },
  } = useFormContext()

  return (
    <div className='h-full  flex flex-col w-full'>
      <Header />
      <ChatWindow
        messages={answer}
        isLoading={isSubmitting}
        error={errors?.bot?.message as string}
        chatWindowRef={chatWindowRef}
      />
      <ChatInput isLoading={isSubmitting} />
    </div>
  )
}

ซึ่งใน Component ChatWindow เราจะทำการจัดการ state ต่างๆ เพื่อแสดงในส่วนของ UI รวมถึงการแสดงผลลัพธ์จาก API ที่เราได้มาด้วย

import { useEffect } from 'react'
import Image from 'next/image'
import { CopyClipboard } from './CopyClipboard'

interface Message {
  role: 'user' | 'ai'
  message: string
  id: string
  raw: string
}

interface ChatWindowProps {
  messages: Message[]
  isLoading?: boolean
  error?: string
  chatWindowRef: any | null
}

export const ChatWindow: React.FC<ChatWindowProps> = ({
  messages,
  isLoading,
  error,
  chatWindowRef,
}) => {
  useEffect(() => {
    if (
      chatWindowRef !== null &&
      chatWindowRef?.current &&
      messages.length > 0
    ) {
      chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight
    }
  }, [messages.length, chatWindowRef])

  return (
    <div
      ref={chatWindowRef}
      className='flex-1 overflow-y-auto p-4 space-y-8'
      id='chatWindow'
    >
      {messages.map((item, index) => (
        <div key={item.id} className='w-full'>
          {item.role === 'user' ? (
            <div className='flex gap-x-8 '>
              <div className='min-w-[48px] min-h-[48px]'>
                <Image
                  src='/img/chicken.png'
                  width={48}
                  height={48}
                  alt='user'
                />
              </div>
              <div>
                <p className='font-bold'>User</p>
                <p>{item.message}</p>
              </div>
            </div>
          ) : (
            <div className='flex gap-x-8 w-full'>
              <div className='min-w-[48px] min-h-[48px]'>
                <Image
                  src='/img/robot.png'
                  width={48}
                  height={48}
                  alt='robot'
                />
              </div>
              <div className='w-full'>
                <div className='flex justify-between mb-1 w-full '>
                  <p className='font-bold'>Ai</p>
                  <div />
                  <CopyClipboard content={item.raw} />
                </div>

                <div
                  className='prose whitespace-pre-line'
                  dangerouslySetInnerHTML={{ __html: item.message }}
                />
              </div>
            </div>
          )}
        </div>
      ))}
      {isLoading && (
        <div className='flex gap-x-8 w-full mx-auto'>
          <div className='min-w-[48px] min-h-[48px]'>
            <Image src='/img/robot.png' width={48} height={48} alt='robot' />
          </div>
          <div>
            <p className='font-bold'>Ai</p>

            <div className='mt-4 flex space-x-2 items-center '>
              <p>Hang on a second </p>
              <span className='sr-only'>Loading...</span>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.3s]'></div>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.15s]'></div>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce'></div>
            </div>
          </div>
        </div>
      )}
      {error && (
        <div className='flex gap-x-8 w-full mx-auto'>
          <div className='min-w-[48px] min-h-[48px]'>
            <Image src='/img/error.png' width={48} height={48} alt='error' />
          </div>
          <div>
            <p className='font-bold'>Ai</p>
            <p className='text-rose-500'>{error}</p>
          </div>
        </div>
      )}
    </div>
  )
}

เมื่อเราทำเสร็จเเล้ว นี้ก็จะเป็น ผลลัพธ์ ที่เราได้มา 😆 ซึ่งสามารถ  เข้ามาถามคำถามได้ เลือกได้ว่าจะ Chat กับ RAGหรือไม่ และสามารถ Reset Chat ได้นั้นเอง

Conclusion

ในปัจจุบันธุรกิจต่างๆ มีการเติบโตอย่างรวดเร็ว ตัวอย่างที่ชัดเจนคือการใช้งาน chatbot ในบริการลูกค้า แต่ก่อนการตอบคำถามลูกค้าเป็นหน้าที่ของพนักงาน แต่ปัจจุบันได้เปลี่ยนมาใช้ระบบ chatbot เพื่อให้บริการลูกค้าทำได้รวดเร็วและมีประสิทธิภาพขึ้น

ในโปรเจ็กต์ Q&A Chatbot ที่กำลังพัฒนา, เมื่อผู้ใช้พิมพ์คำถาม, Large Language Models (LLMs) จะตัดสินใจว่าจะใช้ข้อมูลจาก Retrieval-Augmented Generation (RAG) หรือไม่

การใช้ LLMs ตัดสินใจในขั้นต้นนี้มีข้อดีหลายประการ เช่น การเลือกข้อมูลที่เหมาะสม, ประสิทธิภาพ, และความถูกต้อง

ในส่วนของการพัฒนา, มีการใช้หลายไลบรารีและเฟรมเวิร์ค เช่น Next.js, React Hook Form, Zod สำหรับการจัดการฟอร์ม, Axios สำหรับการเชื่อมต่อกับ backend, และการใช้ React ในการจัดการ UI รวมถึง state ต่างๆ


Aa

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