ถึงตาเราแล้วหรอนุด (แมวส้มที่ผมไปเจอตอนเดินเขาที่ฮ่องกงครับ)

แมวเป็นของเหลวไหม?

ฟังดูเป็นคำถามกวน ๆ และเราก็จะตอบว่า “ไม่!” โดยไม่ต้องคิด และแน่นอนว่า Large Language Model (LLM) ก็จะตอบแบบเดียวกันถ้าเรายิงคำถามนี้เข้าไป (แม้ใจลึก ๆ มันอาจจะลังเลเล็กน้อย เพราะเคยเห็นแมวไหลเข้าไปในกล่องแคบ ๆ มาแล้ว)

ที่น่าสนใจกว่าคือ “แมวเป็นของเหลวหรือไม่” เคยเป็นงานวิจัยที่ได้รับรางวัล Ig Nobel ปี 2017 โดยสรุปได้ว่า จริง ๆ แมวเป็นทั้งสองอย่าง (แล้วแต่เวลา)

ซึ่ง LLM ของเราอย่าง llama3.2 ไม่ได้ถูก train ด้วยข้อมูลเหล่านี้มา ก็เลยตอบ No (ซึ่งก็ถูกของเขา)

แน่สิ แมวไม่ใช่ของเหลว!

RAG (Retrieval-Augmented Generation) เป็นเทคนิคนึงที่ทำให้ Large Language Model สามารถมีความรู้เพิ่มเติมเกี่ยวกับเรื่องเฉพาะ ๆ ซึ่งเอาไปต่อยอดทำอะไรได้มากมายไม่ว่าจะเป็น

  • Chatbot ตอบลูกค้า
  • ระบบ Knowledge Base ภายในองค์กร
  • ระบบช่วยแพทย์ (Clinical Decision Support System) ที่สามารถดึงข้อมูลจากแนวทางเวชปฏิบัติและงานวิจัยทางการแพทย์เพื่อช่วยแพทย์ในการตัดสินใจ
  • และสุดท้าย เอามาเป็นผู้เชี่ยวชาญว่า ทำไมแมวถึงเป็นของเหลวได้!

น่าสนใจใช่ไหมครับหัวข้อวันนี้ เราจะลองพาทำ RAG applicationใช้เองง่าย ๆ บนคอมของเรากัน!

มาดูสิ่งที่เราจะทำวันนี้กัน

  • เตรียมของที่ต้องใช้กันก่อน
  • เข้าใจการทำงานของ RAG แบบพื้นฐาน
  • มาเขียนโค้ดกัน
  • ครอบ UI ให้น้องสักหน่อย

เตรียมของที่ต้องใช้กันก่อน

Python 3.12

เป็น programming language ตัวนึงที่เราจะใช้วันนี้ เพราะว่าเราจะทำ RAG ขึ้นมาจากโค้ด python กัน https://www.python.org/downloads/

วิธีที่แนะนำในการติดตั้งคือ ใช้ pyenv นะครับ จะได้ control python version ง่าย ๆ https://github.com/pyenv/pyenv

# for macos
brew install pyenv
# then install python 3.12
pyenv install 3.12

 

Ollama

เป็น tool สำหรับ run LLM model บนเครื่องคอมของเรา https://ollama.com/

Qdrant

เป็น database ที่ออกแบบมาเพื่อเก็บและค้นหาข้อมูลด้วย vector, ผมแนะนำให้ติดตั้งด้วย container (ไม่ว่าจะเป็น docker หรือ podman ครับ)

# podman
podman run -p 6333:6333 -p 6334:6334 -v $(pwd)/qdrant:/qdrant/storage qdrant/qdrant

# or docker
docker run -p 6333:6333 -p 6334:6334 -v $(pwd)/qdrant:/qdrant/storage qdrant/qdrant

 

เอกสาร PDF สักอัน

เอกสารตัวนี้เป็นข้อมูลที่เราจะนำมาทำ RAG ของเราครับ

สำหรับบทความนี้ ผมจะขอใช้เอกสารจาก Ig Nobel: On the rheology of cats เป็น paper ที่ศึกษาว่า เอ แมวเป็นของเหลวป่ะนะ

เข้าใจการทำงานของ RAG แบบพื้นฐาน

ภาพรวมของ RAG

ลองทำความเข้าใจแต่ละส่วนกัน

จากภาพด้านบน ลองมองเป็นสองส่วนนะครับ

1. เตรียมข้อมูลในรูปแบบที่ LLM เข้าใจและหาได้ง่าย (เส้นเขียว)

  • คือการแปลงเอกสาร PDF ของเราให้เป็นข้อความยาว ๆ ก่อน
  • จากนั้นจึงแบ่งเป็น chunk
  • แล้วนำไปทำ embedding เพื่อจัดเก็บข้อมูลใน vector database ซึ่งเป็น database เฉพาะที่เอาไว้เก็บ vector ของข้อความ

2. นำ prompt ของ user ไปค้นหาส่วนของเอกสารที่เกี่ยวของกับ prompt (เส้นส้ม)

  • User จะ prompt เข้ามาในระบบของเรา
  • เราจะนำข้อความนี้ไปทำ embedding เพื่อเอา vector ไปหาต่อใน vector database เพื่อหาส่วนของข้อมูลที่ใกล้เคียงกัน
  • จากนั้นก็เอาข้อมูลที่ได้จาก vector database มารวมกับ prompt ของเราและ user ก่อนที่จะส่งไปให้ LLM ตอบคำถามต่อไป

มาเขียนโค้ดกัน!

ภาพรวมของ code เรา จะเป็นประมาณนี้ครับ

rag
├── on-the-rheology-of-cats.pdf # paper ที่ศึกษาพฤติกรรมการไหลของแมว
├── constants.py # จุดรวม config และค่าที่ใช้หลายที่ในโค้ด
├── chunk_utils.py # สำหรับเก็บ function เกี่ยวข้องกับการแบ่งข้อความ
├── ollama_utils.py # สำหรับเก็บ function ที่เกี่ยวข้องกับ LLM process
├── pdf_utils.py # สำหรับเก็บ function ที่เกี่ยวข้องกับ PDF
├── rag.py # ไฟล์ python ที่เก็บ class, method ที่เกี่ยวกับ RAG ทั้งหมด
├── prepare_vector_store.py # ไฟล์ python ที่ทำหน้าที่เตรียมข้อมูล และจัดเก็บลง database ให้เรา
├── ask.py # ไฟล์ python ที่ให้เราสามารถถาม RAG จาก CLI
├── app.py # ครอบ UI ให้กับ RAG application ของเรา
└── requirements.txt # python lib(s) ที่เราจะใช้

 

โดยที่ requirements.txt จะมี library ที่เราใช้อยู่ 5 ตัวด้วยกัน

openai>=1.79.0           # ใช้งาน OpenAI API เช่น Chat ,หรือ Embedding
PyPDF2>=3.0.1 # ใช้สำหรับอ่านและแปลงเนื้อหาจากไฟล์ PDF
langchain>=0.3.25 # Framework สำหรับสร้าง LLM application
qdrant-client>=1.14.2 # client สำหรับเชื่อมต่อ vector database (Qdrant)
streamlit>=1.35.0 # เอาไว้สร้าง UI ง่าย ๆ มาครอบ chatbot หรือแอปต้นแบบ

 

และมี constants.py ที่เอาไว้เก็บค่าที่ใช้แชร์กันในหลาย ๆ ที่

OPENAI_API_KEY ตรงนี้เป็นแค่ dummy นะครับ เพราะ Local LLM ของเราไม่ต้องใช้

# Model ที่จะใช้
LLM_MODEL = "llama3.2"
EMBEDDING_MODEL = "mxbai-embed-large"

# OpenAI Config
OPENAI_URL = "http://localhost:11434/v1"
OPENAI_API_KEY = "dummy"

# QdrantConfig
QDRANT_URL = "http://127.0.0.1"
QDRANT_PORT = 6333
COLLECTION_NAME = "cats_rheology"

 

ส่วนแรก: เตรียมข้อมูลในรูปแบบที่ LLM เข้าใจและหาได้ง่าย

เพื่อรวบรวมทุกขั้นตอนในการเตรียมข้อมูลให้อยู่ในที่เดียว และช่วยให้โค้ดของเราดู อ่านง่าย ผมจะสร้างไฟล์ชื่อ prepare_vector_store.py ขึ้นมา โดยเราจะ import ฟังก์ชันต่าง ๆ ที่แยกไว้ในไฟล์อื่นมาใช้งานที่นี่

from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
from qdrant_client.http.models import VectorParams, Distance

from pdf_utils import extract_text_from_pdf
from chunk_utils import create_chunks
from ollama_utils import generate_embeddings
from qdrant_utils import setup_qdrant_collection
from constants import (
OPENAI_URL,
OPENAI_API_KEY,
COLLECTION_NAME,
QDRANT_URL,
QDRANT_PORT,
)

def prepare_vector_store():

# เตรียม client สำหรับ OpenAI และ Qdrant
ollama_client = OpenAI(base_url=OPENAI_URL, api_key=OPENAI_API_KEY)
qdrant_client = QdrantClient(url=QDRANT_URL, port=QDRANT_PORT, prefer_grpc=True)

# อ่านไฟล์ PDF
pdf_path = "on-the-rheology-of-cats.pdf"
text = extract_text_from_pdf(pdf_path)

# สร้าง chunks จากข้อความ
chunks = create_chunks(text)

# เอา chunks แต่ละตัวไปทำ embedding
embeddings = []
for i, chunk in enumerate(chunks):
print(f"Processing chunk {i + 1}/{len(chunk)}: {chunk[:30]}...")
embedding = generate_embeddings(ollama_client, chunk)
embeddings.append(PointStruct(
id=i,
vector=embedding,
payload={
"text": chunk
},
))

# สร้าง collection และเก็บข้อมูล
setup_qdrant_collection(
client=qdrant_client,
collection_name=COLLECTION_NAME,
embeddings=embeddings
)

if __name__ == "__main__":
prepare_vector_store()

 

สำหรับ function ถัดไป extract_text_from_pdf ใน pdf_utils.py ทำหน้าที่ในการอ่าน PDF file ของเรา แล้วนำมาแปลงไป text ยาว ๆ

from PyPDF2 import PdfReader

def extract_text_from_pdf(pdf_path):
"""Extract text from a PDF file."""
reader = PdfReader(pdf_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
return text

 

มาดู function ไฟล์ create_chunksใน chunk_utils.py ทำหน้าที่ในการอ่านแบ่งข้อความยาว ๆ ของเราให้เป็นข้อความที่สั้นลง และให้มีส่วนที่ทับซ้อนกันที่ 200 ตัวอักษร (เพื่อให้ LLM เข้าใจ context มากขึ้น)

from typing import List
from langchain.text_splitter import RecursiveCharacterTextSplitter

def create_chunks(text, chunk_size=1000, chunk_overlap=200) -> List[str]:
"""แบ่งข้อความยาว ๆ เป็น chunk เล็ก ๆ ด้วย RecursiveCharacterTextSplitter"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", " ", ""]
)
chunks = text_splitter.split_text(text)
return chunks

 

ถัดมา generate_embeddings ใน ollama_utils.py ทำหน้าที่แปลงข้อความแต่ละส่วนเป็น vector

from openai import OpenAI
from typing import List, Dict
from constants import EMBEDDING_MODEL

def generate_embeddings(client: OpenAI, text: str) -> List[float]:
response = client.embeddings.create(
input=text,
model=EMBEDDING_MODEL
)
return response.data[0].embedding

 

และสุดท้ายสำหรับ section นี้คือ save ลง database จาก function setup_qdrant_collection ที่ดึงมาจาก qdrant_utils.py

code ส่วนนี้มีทั้งการ remove และ re-create collection เพื่อความง่ายในการทำ demo ไวๆ นะครับ แต่ถ้าขึ้น production เราต้อง optimise ส่วนนี้นะ

from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
from qdrant_client.http.models import Distance, VectorParams

def setup_qdrant_collection(client: QdrantClient, collection_name: str, embeddings):

# เช็คว่ามี collection ที่ชื่อ COLLECTION_NAME อยู่แล้วไหม ถ้ามี ให้ลบออก
existing_collections = map(lambda collection: collection.name, client.get_collections().collections)
if collection_name in existing_collections:
print(f"Collection {collection_name} already exists. Deleting it...")
client.delete_collection(collection_name)

# สร้าง collection ใหม่
client.create_collection(
collection_name=collection_name,
vectors_config=VectorParams(
size=len(embeddings[0].vector),
distance=Distance.COSINE
)
)

# เก็บข้อมูลลงใน collection
print(f"Saving results to vector database {collection_name}...")
client.upsert(
collection_name=collection_name,
points=embeddings
)

 

 

เสร็จแล้ว, ลองเช็คกับ Qdrant dashboard กันว่าข้อมูลของเรามาหรือยังที่ http://localhost:6333/dashboard

collection ของเรามาแล้วครับ 😍

 

ส่วนที่สอง: นำ prompt ของ user ไปค้นหาเอกสารที่เกี่ยวของ

ใน section นี้ เป้าหมายของเราคือ เราอยากจะมี command line สักตัวที่เราสามารถเรียกถามคำถามได้ไว ๆ เช่น

python ask.py "Is cat a liquid?"

 

ใน ask.py จะเป็น file ที่เราเอาไว้ถาม RAG system ของเรา

import sys
from openai import OpenAI
from qdrant_client import QdrantClient
from constants import (
OPENAI_URL,
OPENAI_API_KEY,
QDRANT_URL,
QDRANT_PORT,
)

from rag import RAG

def ask(question: str):

# เตรียม client สำหรับ OpenAI และ Qdrant
ollama_client = OpenAI(base_url=OPENAI_URL, api_key=OPENAI_API_KEY)
qdrant_client = QdrantClient(url=QDRANT_URL, port=QDRANT_PORT, prefer_grpc=True)

# เรียกใช้ RAG
rag = RAG(ollama_client, qdrant_client)
response = rag.ask(question)

print(f"\nResponse to query '{question}':\n\n {response}")

return response

if __name__ == "__main__":

# รับคำถามจาก command line
if len(sys.argv) != 2:
print("Usage: python ask.py <question>")
sys.exit(1)
question = sys.argv[1]

# ถามเลย!
ask(question)

 

ในไฟล์ rag.py จะมี class RAG ที่มี method อยู่สองตัว เพื่อดึงข้อมูลที่เกี่ยวข้องจาก vector database (get_relevant_documents), และเพื่อถาม RAG ของเรา (ask)

from openai import OpenAI
from qdrant_client import QdrantClient
from constants import (
LLM_MODEL,
EMBEDDING_MODEL,
COLLECTION_NAME,
)

class RAG:

# ตัว init ของ class
def __init__(self, openai_client: OpenAI, qdrant_client: QdrantClient):
self.openai_client = openai_client
self.qdrant_client = qdrant_client

# function เพื่่อค้นหาเอกสารที่เกี่ยวข้องจากฐานข้อมูล
def get_relevant_documents(self, query: str, limit=5):
# เอา prompt ไปสร้าง embedding
query_embedding = self.openai_client.embeddings.create(
model=EMBEDDING_MODEL,
input=query
).data[0].embedding

# เอา vector ที่ได้ไปค้นหาในฐานข้อมูล
search_results = self.qdrant_client.query_points(
collection_name=COLLECTION_NAME,
query=query_embedding,
limit=limit
)

# เอาข้อมูลที่ได้จากการค้นหา (search results) มาเก็บใน context_texts
context_texts = [hit.payload["text"] for hit in search_results.points]
return context_texts

# function สำหรับถาม RAG ของเรา
def ask(self, query: str) -> str:
# ค้นหาเอกสารที่เกี่ยวข้องจากฐานข้อมูล
context_docs = self.get_relevant_documents(query)

# เอา context_docs มาต่อกัน
context_text = "\n\n".join(context_docs)

# สร้าง prompt สำหรับถาม LLM
prompt = f"""
You are an expert on the rheology of cat. Answer the following question using ONLY the information in the context. If the answer is not in the context, say "I don't know".
Do not mention the context in your answer.

CONTEXT INFORMATION: {context_text}
USER QUESTION: {query}
"""


# ถาม LLM ของเราเลย!
response = self.openai_client.chat.completions.create(
model=LLM_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
)

return response.choices[0].message.content

 

ลองถาม RAG ของเรากัน!

RAG ของเรายอมรับแล้วว่าแมวเป็นของเหลว (😂😂😂😂😂😂)

ครอบ UI ให้น้องสักหน่อย

ครั่นจะให้เอา command line ไปให้ user ใช้ก็อาจจะดูไม่ค่อย user-friendly เท่าไหร่ เรามาลองครอบ RAG application ของเราด้วย UI ง่าย ๆ ไว ๆ กัน มี lib อยู่ตัวนึงที่ผมชอบมาก ๆ เพราะมันขึ้นได้ไวสุด ๆ ก็คือ https://streamlit.io/

ในไฟล์ app.py ที่ทำหน้าที่เป็น UI creator ของเรา

import streamlit as st
from ask import ask

# ตั้งค่า Streamlit page
st.set_page_config(
page_title="Cat Rheology RAG Assistant",
page_icon="🐱",
layout="centered"
)

st.title("Cat Rheology RAG Assistant")

question = st.text_input("Ask a question about cat rheology:", key="question_input")

submit_button = st.button("Get Answer")

if submit_button and question:
with st.spinner('Retrieving information...'):
response = ask(question)

st.subheader("Answer:")
st.write(response)

with st.expander("Your question"):
st.write(question)

elif submit_button and not question:
st.warning("Please enter a question.")

 

แล้วเราก็สั่งไปเลยว่า

streamlit run app.py

แค่นี้ก็ได้ UI Interface สำหรับ RAG application ของเราแล้ว!

สำหรับตัว code เต็ม ๆ สามารถไปส่องได้ที่นี่เลย https://github.com/kasidis-palo/basic-rag

 

FAQ

ทำไมต้องแบ่งข้อความเป็นท่อน ๆ (chunk) ก่อนทำ embedding ด้วยล่ะ?

มี 3 สาเหตุหลัก ๆ ครับ คือ

  • LLM ที่เราทำงานด้วย มันมี token limit อยู่ ถ้าเราใส่ไปทั้งหมด ข้อความมันจะยาวเกิน token แล้วก็จะไม่ถูกเอาไปคิด
  • ทำให้ RAG ตอบคำถามตรงจุดขึ้น เพราะเราจะเอาแค่ chunk ของข้อความที่เกี่ยวข้อง (จากการไป query vector database) มาใส่ใน context ของเรา
  • มันประหยัดพลังงานกว่า!

TextSplitter คืออะไร มีตัวอื่นด้วยหรอ

TextSplitter คือเครื่องมือที่ช่วยแบ่งข้อความยาว ๆ ออกเป็นท่อนสั้น ๆ เพื่อให้ Large Language Model (LLM) ทำงานได้ดีขึ้น แต่การแบ่งข้อความก็มีหลายทางเหมือนกัน และต้องเลือกใช้ตามความเหมาะสมของข้อมูลเรา เช่น

  • RecursiveCharacterTextSplitter (ตัวที่ใช้ในบทความนี้) — แบ่งตามตัวแบ่งเช่น เว้นบรรทัด ช่องว่าง ซึ่งเหมาะกับบทความ เอกสาร PDF ทั่ว ๆ ไป
  • MarkdownHeaderTextSplitter — แบ่งตามโครงสร้างหัวข้อของ Markdown เช่น #, ##, ### เพื่อรักษาลำดับของเนื้อหา
  • HTMLHeaderTextSplitter — แบ่งตาม tag หัวข้อใน HTML เช่น <h1>, <h2>, <h3> เหมาะกับการดึงข้อมูลจาก website

 

Summary

วันนี้เราได้ลองนำข้อมูลจาก PDF มาต่อยอดให้ LLM ตอบคำถามได้ฉลาดขึ้น (จนสามารถ convince ได้ว่าแมวเป็นของเหลวจริง ๆ) พร้อมสร้าง RAG application ใช้งานเองได้แบบง่าย ๆ บนเครื่องของเราเอง และนี่เป็นแค่จุดเริ่มต้น

ในโลกของ RAG ยังมีเรื่องของการ validation, evaluation และ optimisation ที่น่าสนใจอีกมาก รอติดตามเนื้อหาสนุก ๆ จาก PALO IT กันได้เลยครับ!

 


 

สำหรับใครที่กำลังมองหาวิธีสร้าง RAG Application หรือ Chatbot เพื่อใช้งานในองค์กร ที่ PALO IT เรามีทีมผู้เชี่ยวชาญพร้อมช่วยตั้งแต่เริ่มต้นจนระบบใช้งานได้จริง! ไม่ว่าจะเป็น

  • Data Cleaning — เตรียมข้อมูลให้สะอาด พร้อมใช้งาน เพื่อผลลัพธ์ที่แม่นยำ
  • RAG Optimisation — ปรับแต่งระบบให้ตอบไว ตรงประเด็น และพร้อมรองรับการใช้งานระดับองค์กร
  • Evaluation — ทดสอบและวัดผลลัพธ์ของโมเดล เพื่อให้มั่นใจว่า RAG ของคุณตอบได้ดีจริง

ไม่ว่าคุณจะเพิ่งเริ่มต้น หรือมีระบบอยู่แล้วและอยากต่อยอด เราพร้อมเป็น partner ที่จะช่วยให้คุณไปได้ไกลกว่าเดิม

ทักไปที่เพจ Facebook: PALO IT Thailand ได้เลยครับ 🎉

อ่านบทความผ่าน Medium ได้ที่: ทำ On-Premise RAG ใช้เองง่าย ๆ ด้วย Ollama และ Python

เริ่มติดตามผลกระทบด้านความยั่งยืนขององค์กรคุณได้แล้ววันนี้ พร้อมทีมผู้เชี่ยวชาญที่คอยให้คำแนะนำฟรี