ในปัจจุบันมีการใช้ 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 มีหลายเหตุผลที่สำคัญ:
การเลือกแหล่งข้อมูลที่เหมาะสม : บางคำถามอาจไม่จำเป็นต้องใช้ข้อมูลจาก RAG และสามารถตอบได้โดยตรงจากความรู้ภายในของ LLMsความถูกต้อง : LLMs สามารถประเมินว่าข้อมูลที่มีอยู่ภายในข้อมูล ที่มีมันเองมีความเพียงพอและถูกต้องสำหรับการตอบคำถามหรือไม่ โดยรวมแล้ว, การใช้ LLMs เป็นตัวตัดสินใจเบื้องต้นช่วยให้แน่ใจว่าแต่ละคำถามจะได้รับการจัดการอย่างเหมาะสม
Lib หรือ Framwork มาใช้งานดังต่อไปนี้ Next.js:
เริ่มโปรเจกต์ใหม่โดยใช้คำสั่ง create-next-app:
React Hook Form:
และ เราเลือกใช้ zod ในการจัดการ ทำ validate input form
Axios:
ติดตั้ง Axios ในโปรเจกต์:
ส่วนของ User Interface
ส่วนประกอบของ UI จะหลักจะประกอบด้วย
Chat header จะประกอบ ด้วย setting สำหรับการ chat และ ปุ่มสำหรับ reset chat Chat input Input สำหรับพิมพ์ข้อความและส่งข้อความ 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 ต่างๆ