โมเดลภาษาขนาดใหญ่ (LLM) เช่น GPT-4 และ Claude มีความสามารถสูงอย่างยิ่ง แต่มีข้อจำกัดพื้นฐาน: ความรู้ของพวกมันถูกแช่แข็งไว้ ณ เวลาที่ฝึกอบรม พวกมันไม่สามารถเข้าถึงเอกสารภายใน ฐานข้อมูล หรือข้อมูลแบบเรียลไทม์ของคุณได้ Retrieval-Augmented Generation (RAG) แก้ปัญหานี้โดยผสมผสานพลังในการสร้างของ LLM เข้ากับความสามารถในการดึงข้อมูลจากแหล่งภายนอก
ปัญหา: ข้อจำกัดของ LLM
ก่อนพูดถึง RAG สิ่งสำคัญคือต้องเข้าใจว่าทำไมเราถึงต้องการมัน
- ความรู้แบบสถิต: LLM รู้เฉพาะสิ่งที่เห็นระหว่างการฝึกอบรม หากคุณถามเกี่ยวกับเหตุการณ์ที่เกิดขึ้นหลังจุดตัด มันไม่สามารถตอบได้
- ภาพหลอน: เมื่อ LLM ไม่รู้คำตอบ มันมีแนวโน้มที่จะสร้างขึ้นมา สร้างข้อมูลที่ดูน่าเชื่อถือแต่เป็นเท็จโดยสิ้นเชิง
- ไม่สามารถเข้าถึงข้อมูลส่วนตัว: LLM ทั่วไปไม่มีสิทธิ์เข้าถึงเอกสารภายในของบริษัท ตั๋ว หรือ codebase ของคุณ
RAG แก้ไขปัญหาทั้งสามนี้โดยให้โมเดลได้รับ บริบทที่เกี่ยวข้อง ที่ดึงมาจากแหล่งภายนอก ณ เวลาที่ส่งคำถาม
RAG คืออะไร?
Retrieval-Augmented Generation เป็นสถาปัตยกรรมที่เพิ่มความสมบูรณ์ให้กับ prompt ที่ส่งไปยัง LLM ด้วยข้อมูลที่ดึงมาจากฐานความรู้ภายนอก แทนที่จะพึ่งพาเฉพาะความรู้ที่อยู่ในพารามิเตอร์ของโมเดล RAG จะ ค้นหา ข้อมูลที่เกี่ยวข้องก่อน จากนั้นจึง ฉีด เข้าไปใน prompt ทำให้โมเดลสามารถสร้างคำตอบที่แม่นยำและมีหลักฐานสนับสนุน
RAG ทำงานอย่างไรในรายละเอียด
สถาปัตยกรรม RAG ประกอบด้วยสองเฟสหลัก: การสร้างดัชนี (ออฟไลน์) และ การดึงข้อมูล + การสร้าง (ออนไลน์)
เฟส 1: การสร้างดัชนี (การนำเข้าเอกสาร)
เฟสการสร้างดัชนีเตรียมเอกสารของคุณสำหรับการค้นหาเชิงความหมาย ประกอบด้วยสี่ขั้นตอน
1. การโหลดเอกสาร
เอกสารสามารถมาจากแหล่งใดก็ได้: ไฟล์ PDF, หน้าเว็บ, ฐานข้อมูล, ไฟล์ Markdown, API Document Loader อ่านเอกสารเหล่านี้และแปลงเป็นข้อความที่มีโครงสร้าง
2. การแบ่งข้อความ (Chunking)
LLM มี context window ที่จำกัด และเอกสารอาจยาวมาก Text Splitter แบ่งเอกสารเป็นชิ้นส่วนเล็กๆ ที่เรียกว่า chunks คุณภาพของการ chunking มีความสำคัญมาก: chunks ที่เล็กเกินไปจะสูญเสียบริบท ขณะที่ chunks ที่ใหญ่เกินไปจะลดความเกี่ยวข้อง
กลยุทธ์ที่พบบ่อยที่สุดคือ:
- การแบ่งอักขระแบบเวียนซ้ำ: แบ่งข้อความแบบเวียนซ้ำโดยใช้ตัวคั่นเช่น
\n\n,\n,.โดยเคารพโครงสร้างเอกสาร - การแบ่งเชิงความหมาย: ใช้ embeddings เพื่อหาจุดแบ่งตามธรรมชาติในข้อความ
- Chunk ทับซ้อน: รวมการทับซ้อนระหว่าง chunks ที่ต่อเนื่องกันเพื่อรักษาบริบทที่ขอบเขต
3. Embedding
แต่ละ chunk จะถูกแปลงเป็น เวกเตอร์ตัวเลข (embedding) ผ่านโมเดล embedding (เช่น text-embedding-3-small ของ OpenAI) เวกเตอร์เหล่านี้จับความหมายเชิงความหมายของข้อความ: ประโยคที่มีความหมายใกล้เคียงกันจะมีเวกเตอร์ที่อยู่ใกล้กันในปริภูมิหลายมิติ
4. Vector Store
เวกเตอร์จะถูกบันทึกใน Vector Store (หรือฐานข้อมูลเวกเตอร์) เช่น ChromaDB, Pinecone, Weaviate หรือ FAISS ฐานข้อมูลนี้ถูกปรับให้เหมาะสมสำหรับ การค้นหาความคล้ายคลึง: เมื่อให้คำถาม มันจะค้นหาเวกเตอร์ที่คล้ายที่สุด (และดังนั้นจึงเป็น chunks ข้อความที่เกี่ยวข้องที่สุด)
เฟส 2: การดึงข้อมูล + การสร้าง
เมื่อผู้ใช้ถามคำถาม:
- คำถามจะถูกแปลงเป็น embedding โดยใช้โมเดล embedding เดียวกัน
- Vector Store ค้นหา chunks ที่คล้ายที่สุดผ่าน การค้นหาความคล้ายคลึง (โดยทั่วไปคือ cosine similarity หรือระยะทาง Euclidean)
- chunks ที่ดึงมาจะถูกแทรกเข้าไปใน prompt เป็นบริบท
- LLM สร้างคำตอบตามบริบทที่ให้มา
การสร้าง RAG Pipeline ด้วย LangChain
LangChain เป็น framework Python (และ JavaScript) ที่ได้รับความนิยมมากที่สุดสำหรับสร้างแอปพลิเคชันที่ขับเคลื่อนด้วย LLM มันให้ abstraction ระดับสูงสำหรับทุกส่วนประกอบของ RAG pipeline
การติดตั้ง
pip install langchain langchain-openai langchain-community chromadb
ขั้นตอนที่ 1: โหลดเอกสาร
LangChain มี Document Loader หลายสิบตัวสำหรับแหล่งข้อมูลต่างๆ
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Load a PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Load a web page web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Load all .md files from a directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
ขั้นตอนที่ 2: แบ่งเอกสารเป็น Chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Original documents: {len(all_docs)}, Chunks: {len(chunks)}")
พารามิเตอร์ chunk_overlap มีความสำคัญมาก: มันสร้างการทับซ้อนระหว่าง chunks ที่ต่อเนื่องกันเพื่อไม่ให้บริบทสูญหายที่ขอบเขต
ขั้นตอนที่ 3: สร้าง Embeddings และ Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
ขั้นตอนที่ 4: สร้าง Retriever
Retriever คือส่วนประกอบที่เมื่อได้รับคำถาม จะดึง chunks ที่เกี่ยวข้องที่สุดจาก vector store
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("How does authentication work?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
ขั้นตอนที่ 5: สร้าง RAG Chain
ตอนนี้มารวมทุกอย่างเข้าด้วยกันกับ LLM และ prompt template
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Answer the question based only on the provided context. If the context does not contain enough information, say you don't know. Context: {context} Question: {question} Answer: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("How does authentication work in the system?") print(response)
เทคนิค RAG ขั้นสูง
pipeline พื้นฐานทำงานได้ดี แต่มีหลายเทคนิคที่สามารถปรับปรุงคุณภาพคำตอบได้อย่างมีนัยสำคัญ
การดึงข้อมูลแบบหลายคำถาม
บางครั้งคำถามของผู้ใช้คลุมเครือหรือไม่สอดคล้องกับภาษาที่ใช้ในเอกสาร Multi-Query Retriever สร้างรูปแบบต่างๆ ของคำถามเดิมโดยอัตโนมัติเพื่อจับมุมมองที่หลากหลาย
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("What are the security best practices?")
การบีบอัดบริบท
เนื้อหาทั้งหมดใน chunk ไม่ได้เกี่ยวข้องกับคำถาม Contextual Compression Retriever ใช้ LLM เพื่อดึงเฉพาะส่วนที่เกี่ยวข้องจากแต่ละ chunk ที่ดึงมา
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
การค้นหาแบบผสม
การค้นหาเชิงความหมายอย่างเดียวไม่ได้เหมาะสมเสมอไป การค้นหาแบบผสม รวมการค้นหาเชิงความหมาย (embeddings) กับการค้นหาเชิงคำศัพท์ (BM25, การจับคู่คำสำคัญ) เพื่อให้ได้ผลลัพธ์ที่ดีกว่า
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
RAG แบบสนทนา (พร้อมหน่วยความจำ)
เพื่อสร้าง chatbot RAG ที่จำบริบทการสนทนาได้ คุณต้องเพิ่มหน่วยความจำที่จัดรูปแบบคำถามของผู้ใช้ใหม่โดยคำนึงถึงประวัติการสนทนา
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Given the chat history and the user's latest question, " "reformulate the question so it is understandable without the history."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
แนวทางปฏิบัติที่ดีที่สุด
- เลือกขนาด chunk ที่เหมาะสม: ทดลองกับขนาดต่างๆ (500-1500 โทเค็น) chunks เล็กสำหรับคำตอบที่แม่นยำ ใหญ่กว่าสำหรับบริบทที่กว้างขึ้น
- ใช้ metadata ของเอกสาร: เพิ่มแหล่งที่มา วันที่ และหมวดหมู่เป็น metadata ให้กับ chunks ซึ่งช่วยให้กรองผลลัพธ์ระหว่างการดึงข้อมูล
- ประเมินคุณภาพ: ใช้ framework เช่น RAGAS เพื่อวัดเมตริกเช่น faithfulness, relevancy และ context precision
- จัดการการอัปเดตเอกสาร: สร้าง pipeline การนำเข้าซ้ำเพื่อให้ vector store ซิงค์กับแหล่งข้อมูลของคุณ
- เพิ่ม re-ranker: หลังจากการดึงข้อมูลเบื้องต้น ใช้โมเดล re-ranking (เช่น Cohere Rerank) เพื่อจัดเรียงผลลัพธ์ใหม่ตามความเกี่ยวข้องจริง
สรุป
RAG ได้กลายเป็นสถาปัตยกรรมมาตรฐานสำหรับการสร้างแอปพลิเคชัน AI ที่ต้องการเข้าถึงความรู้เฉพาะทางและเป็นปัจจุบัน LangChain ลดความซับซ้อนของการนำไปใช้อย่างมาก โดยให้ abstraction สำหรับทุกส่วนประกอบของ pipeline
ขั้นตอนถัดไป:
- ทดลองในเครื่อง: เริ่มต้นด้วย ChromaDB และเอกสารไม่กี่ชิ้นเพื่อทำความคุ้นเคยกับ pipeline
- สำรวจ LangSmith: ใช้ LangSmith เพื่อตรวจสอบและแก้ไขข้อผิดพลาดของ chain ใน production
- ลองโมเดล embedding ต่างๆ: เปรียบเทียบโมเดลเช่น
text-embedding-3-small,text-embedding-3-largeและโมเดลโอเพนซอร์สจาก Sentence Transformers - ตรวจสอบเอกสาร: เอกสาร LangChain เป็นแหล่งข้อมูลที่ยอดเยี่ยมและอัปเดตอย่างต่อเนื่อง