การพัฒนาเครื่องมือค้นหาที่เข้าใจภาษามนุษย์เป็นความท้าทายที่นักพัฒนาหลายคนต้องเผชิญ ในบทความนี้เราจะมาเรียนรู้วิธีสร้างระบบค้นหาภาษาธรรมชาติโดยใช้ Large Language Model หรือ LLM เพื่อให้ผู้ใช้สามารถค้นหาข้อมูลได้อย่างเป็นธรรมชาติเหมือนกับการถามคนจริงๆ
เปรียบเทียบบริการ AI API ยอดนิยม
ก่อนจะเริ่มพัฒนา เรามาดูการเปรียบเทียบค่าใช้จ่ายและความสามารถของบริการต่างๆ กัน โดยเฉพาะ HolySheep AI ที่มีความคุ้มค่าสูงสุดสำหรับนักพัฒนาไทย
| บริการ | ราคา ($/MTok) | ความหน่วง (ms) | วิธีการชำระเงิน | เครดิตฟรี |
|---|---|---|---|---|
| HolySheep AI | $0.42 - $15 | <50 | WeChat, Alipay, บัตร | ✓ มีเมื่อลงทะเบียน |
| API อย่างเป็นทางการ | $2.50 - $60 | 100-300 | บัตรเครดิตเท่านั้น | ✗ ไม่มี |
| บริการรีเลย์อื่นๆ | $1.50 - $30 | 80-200 | หลากหลาย | ขึ้นอยู่กับผู้ให้บริการ |
จะเห็นได้ว่า HolySheep AI มีความได้เปรียบเรื่องราคาที่ประหยัดได้ถึง 85% เมื่อเทียบกับ API อย่างเป็นทางการ พร้อมทั้งความหน่วงที่ต่ำกว่า 50 มิลลิวินาที ทำให้เหมาะสำหรับการพัฒนาเครื่องมือค้นหาที่ต้องการความเร็วสูง
หลักการทำงานของ Semantic Search
การค้นหาภาษาธรรมชาติแตกต่างจากการค้นหาแบบดั้งเดิมที่ใช้ Keyword Matching โดยระบบจะทำความเข้าใจความหมายของคำถาม แทนที่จะแค่จับคู่คำที่ตรงกันเท่านั้น ซึ่งทำให้ผู้ใช้สามารถถามคำถามได้หลากหลายรูปแบบและยังคงได้ผลลัพธ์ที่ถูกต้อง
การตั้งค่าโปรเจกต์และติดตั้ง Dependencies
เริ่มต้นด้วยการสร้างโปรเจกต์ Python และติดตั้งไลบรารีที่จำเป็นสำหรับการพัฒนาระบบค้นหา
pip install openai tiktoken numpy scikit-learn fastapi uvicorn
การสร้าง Client สำหรับเชื่อมต่อกับ LLM API
ส่วนสำคัญที่สุดคือการตั้งค่า Client สำหรับเชื่อมต่อกับ API ซึ่งเราจะใช้ HolySheep AI เป็นตัวอย่างเนื่องจากมีราคาที่คุ้มค่าและความหน่วงต่ำ
import openai
from typing import List, Dict, Optional
class LLMClient:
"""Client สำหรับเชื่อมต่อกับ LLM API ผ่าน HolySheep"""
def __init__(self, api_key: str, base_url: str = "https://api.holysheep.ai/v1"):
self.client = openai.OpenAI(
api_key=api_key,
base_url=base_url
)
def create_embedding(self, text: str, model: str = "text-embedding-3-small") -> List[float]:
"""สร้าง Embedding จากข้อความ"""
response = self.client.embeddings.create(
model=model,
input=text
)
return response.data[0].embedding
def chat_completion(
self,
messages: List[Dict],
model: str = "gpt-4o",
temperature: float = 0.7
) -> str:
"""ส่งข้อความไปยัง LLM และรับคำตอบ"""
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature
)
return response.choices[0].message.content
ตัวอย่างการใช้งาน
api_key = "YOUR_HOLYSHEEP_API_KEY" # แทนที่ด้วย API Key ของคุณ
llm_client = LLMClient(api_key=api_key)
การสร้างระบบ Semantic Search Engine
ต่อไปจะเป็นการสร้างคลาสหลักสำหรับระบบค้นหา โดยจะประกอบด้วยฟังก์ชันสำหรับสร้าง Embedding ของเอกสาร การค้นหาด้วย Cosine Similarity และการใช้ LLM ตอบคำถาม
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class SemanticSearchEngine:
"""ระบบค้นหาภาษาธรรมชาติโดยใช้ LLM"""
def __init__(self, llm_client: LLMClient):
self.llm_client = llm_client
self.documents: List[Dict] = []
self.embeddings: Optional[np.ndarray] = None
def index_documents(self, docs: List[Dict[str, str]]):
"""ทำดัชนีเอกสารทั้งหมด"""
self.documents = docs
texts = [doc["content"] for doc in docs]
# สร้าง Embedding สำหรับแต่ละเอกสาร
self.embeddings = np.array([
self.llm_client.create_embedding(text) for text in texts
])
print(f"✓ ทำดัชนีเอกสาร {len(docs)} ฉบับเสร็จสิ้น")
def search(self, query: str, top_k: int = 5) -> List[Dict]:
"""ค้นหาเอกสารที่เกี่ยวข้องที่สุด"""
# สร้าง Embedding ของคำถาม
query_embedding = np.array([
self.llm_client.create_embedding(query)
])
# คำนวณ Cosine Similarity
similarities = cosine_similarity(query_embedding, self.embeddings)[0]
# เรียงลำดับตามความคล้ายคลึง
top_indices = np.argsort(similarities)[::-1][:top_k]
results = []
for idx in top_indices:
results.append({
"document": self.documents[idx],
"score": float(similarities[idx])
})
return results
def answer_question(self, question: str) -> str:
"""ใช้ LLM ตอบคำถามโดยอ้างอิงจากเอกสารที่ค้นหาเจอ"""
# ค้นหาเอกสารที่เกี่ยวข้อง
relevant_docs = self.search(question, top_k=3)
# สร้าง Context จากเอกสารที่พบ
context = "\n\n".join([
f"[เอกสาร {i+1}]: {doc['document']['content']}"
for i, doc in enumerate(relevant_docs)
])
# สร้าง Prompt สำหรับ LLM
messages = [
{
"role": "system",
"content": "คุณเป็นผู้ช่วยตอบคำถามโดยอ้างอิงจากเอกสารที่ให้มา หากไม่แน่ใจให้ตอบว่าไม่ทราบ"
},
{
"role": "user",
"content": f"เอกสารที่เกี่ยวข้อง:\n{context}\n\nคำถาม: {question}"
}
]
return self.llm_client.chat_completion(messages, model="gpt-4o")
ตัวอย่างการใช้งาน
engine = SemanticSearchEngine(llm_client)
เพิ่มเอกสารตัวอย่าง
sample_docs = [
{"title": "การติดตั้ง Python", "content": "Python สามารถติดตั้งได้จาก python.org โดยดาวน์โหลดตัวติดตั้งและรัน"},
{"title": "ตัวแปรใน Python", "content": "ตัวแปรใน Python ไม่ต้องประกาศชนิดข้อมูล เช่น x = 10"},
{"title": "ฟังก์ชันใน Python", "content": "ฟังก์ชันสร้างด้วย def เช่น def hello(): print('สวัสดี')"}
]
engine.index_documents(sample_docs)
ค้นหา
result = engine.answer_question("วิธีสร้างตัวแปรใน Python เป็นอย่างไร?")
print(result)
การปรับปรุงประสิทธิภาพด้วย Batching
สำหรับการทำดัชนีเอกสารจำนวนมาก การใช้ Batching จะช่วยลดเวลาและค่าใช้จ่ายได้อย่างมาก โดย HolySheep AI รองรับการสร้าง Embedding หลายตัวพร้อมกันในคำขอเดียว
from concurrent.futures import ThreadPoolExecutor
import time
class OptimizedSearchEngine(SemanticSearchEngine):
"""ระบบค้นหาที่ปรับปรุงประสิทธิภาพแล้ว"""
def index_documents_batched(self, docs: List[Dict[str, str]], batch_size: int = 100):
"""ทำดัชนีเอกสารแบบ Batch เพื่อประหยัดเวลาและค่าใช้จ่าย"""
self.documents = docs
texts = [doc["content"] for doc in docs]
all_embeddings = []
total_batches = (len(texts) + batch_size - 1) // batch_size
start_time = time.time()
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
batch_num = i // batch_size + 1
# สร้าง Embedding หลายตัวในคำขอเดียว (Batching)
response = self.llm_client.client.embeddings.create(
model="text-embedding-3-small",
input=batch
)
batch_embeddings = [item.embedding for item in response.data]
all_embeddings.extend(batch_embeddings)
elapsed = time.time() - start_time
print(f"✓ Batch {batch_num}/{total_batches} เสร็จสิ้น (ใช้เวลา {elapsed:.1f}s)")
self.embeddings = np.array(all_embeddings)
print(f"✓ ทำดัชนี {len(docs)} ฉบับ เสร็จสิ้นใน {time.time() - start_time:.1f}s")
def index_parallel(self, docs: List[Dict[str, str]], max_workers: int = 5):
"""ทำดัชนีเอกสารแบบขนานเพื่อความเร็วสูงสุด"""
self.documents = docs
def get_embedding(text: str) -> np.ndarray:
return np.array(self.llm_client.create_embedding(text))
start_time = time.time()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
self.embeddings = np.array(
list(executor.map(get_embedding, [doc["content"] for doc in docs]))
)
print(f"✓ ทำดัชนีแบบขนาน {len(docs)} ฉบับ เสร็จสิ้นใน {time.time() - start_time:.2f}s")
ทดสอบกับเอกสาร 1000 ฉบับ
large_dataset = [
{"title": f"เอกสาร {i}", "content": f"เนื้อหาตัวอย่าง {i} " * 50}
for i in range(1000)
]
optimized_engine = OptimizedSearchEngine(llm_client)
optimized_engine.index_documents_batched(large_dataset, batch_size=50)
การสร้าง API Server สำหรับ Search Engine
เพื่อให้ Search Engine พร้อมใช้งานจริง เราจะสร้าง API Server โดยใช้ FastAPI ซึ่งจะรองรับการค้นหาและการตอบคำถามผ่าน HTTP Endpoint
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
app = FastAPI(title="AI Semantic Search API")
สร้าง Search Engine Instance (ใช้ Singleton Pattern)
search_engine: Optional[SemanticSearchEngine] = None
class Document(BaseModel):
title: str
content: str
class SearchRequest(BaseModel):
query: str
top_k: int = 5
class QuestionRequest(BaseModel):
question: str
model: str = "gpt-4o"
@app.on_event("startup")
async def startup_event():
global search_engine
api_key = "YOUR_HOLYSHEEP_API_KEY"
llm_client = LLMClient(api_key=api_key)
search_engine = SemanticSearchEngine(llm_client)
# โหลดเอกสารตัวอย่าง
sample_docs = [
{"title": "HolySheep AI", "content": "บริการ AI API ราคาประหยัด ความหน่วงต่ำ"},
{"title": "การใช้งาน API", "content": "สมัครบัญชีแล้วนำ API Key มาใช้งาน"}
]
search_engine.index_documents(sample_docs)
print("✓ Search Engine พร้อมใช้งาน")
@app.post("/search")
async def search(request: SearchRequest):
if search_engine is None:
raise HTTPException(status_code=503, detail="Search Engine ยังไม่พร้อมใช้งาน")
results = search_engine.search(request.query, request.top_k)
return {"results": results}
@app.post("/answer")
async def answer_question(request: QuestionRequest):
if search_engine is None:
raise HTTPException(status_code=503, detail="Search Engine ยังไม่พร้อมใช้งาน")
answer = search_engine.answer_question(
request.question,
model=request.model
)
return {"answer": answer}
@app.post("/index")
async def index_documents(documents: List[Document]):
if search_engine is None:
raise HTTPException(status_code=503, detail="Search Engine ยังไม่พร้อมใช้งาน")
docs = [{"title": d.title, "content": d.content} for d in documents]
search_engine.index_documents(docs)
return {"message": f"ทำดัชนีเอกสาร {len(documents)} ฉบับเสร็จสิ้น"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. ข้อผิดพลาด AuthenticationError: Invalid API Key
สาเหตุ: API Key ไม่ถูกต้องหรือหมดอายุ
# ❌ วิธีที่ผิด - Key ไม่ถูกต้อง
client = openai.OpenAI(
api_key="sk-wrong-key",
base_url="https://api.holysheep.ai/v1"
)
✅ วิธีที่ถูกต้อง - ตรวจสอบ Key และใช้ Environment Variable
import os
from dotenv import load_dotenv
load_dotenv() # โหลด .env file
api_key = os.getenv("HOLYSHEEP_API_KEY")
if not api_key:
raise ValueError("กรุณาตั้งค่า HOLYSHEEP_API_KEY ในไฟล์ .env")
client = openai.OpenAI(
api_key=api_key,
base_url="https://api.holysheep.ai/v1"
)
วิธีแก้ไข: ตรวจสอบว่าได้สมัครสมาชิกและคัดลอก API Key ที่ถูกต้องจาก หน้าผู้ใช้งาน พร้อมทั้งเก็บ Key ไว้ใน Environment Variable แทนการเขียนตรงในโค้ด
2. ข้อผิดพลาด RateLimitError: Too Many Requests
สาเหตุ: ส่งคำขอมากเกินกว่าขีดจำกัดที่กำหนด
import time
from functools import wraps
def rate_limit(max_requests: int = 60, period: int = 60):
"""Decorator สำหรับจำกัดจำนวนคำขอ"""
min_interval = period / max_requests
last_called = [0.0]
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
class RateLimitedClient(LLMClient):
"""Client ที่มีการจำกัดอัตราคำขออัตโนมัติ"""
@rate_limit(max_requests=50, period=60) # สูงสุด 50 คำขอต่อนาที
def create_embedding(self, text: str):
return super().create_embedding(text)
@rate_limit(max_requests=20, period=60)
def chat_completion(self, messages, model="gpt-4o", temperature=0.7):
return super().chat_completion(messages, model, temperature)
หรือใช้ Exponential Backoff สำหรับ Retry
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
def safe_embedding_call(client, text):
"""เรียก API พร้อม Retry อัตโนมัติเมื่อเกิด Rate Limit"""
return client.create_embedding(text)
วิธีแก้ไข: ใช้ Rate Limiting หรือ Exponential Backoff เพื่อหลีกเลี่ยงการถูกจำกัด รวมถึงพิจารณาใช้ Batching เพื่อรวมคำขอหลายคำให้เป็นคำขอเดียว
3. ข้อผิดพลาด BadRequestError: Invalid Input - Text Too Long
สาเหตุ: ข้อความที่ส่งไปยาวเกินขีดจำกัดของ Model
import tiktoken
class TextProcessor:
"""ประมวลผลข้อความให้เหมาะสมก่อนส่งไปยัง API"""
def __init__(self, model: str = "gpt-4o"):
self.encoding = tiktoken.encoding_for_model(model)
self.max_tokens = {
"gpt-4o": 128000,
"gpt-4-turbo": 128000,
"gpt-3.5-turbo": 16385
}
self.model = model
def count_tokens(self, text: str) -> int:
"""นับจำนวน Token ในข้อความ"""
return len(self.encoding.encode(text))
def truncate_text(self, text: str, max_tokens: int) -> str:
"""ตัดข้อความให้เหลือตามจำนวน Token ที่กำหนด"""
tokens = self.encoding.encode(text)
if len(tokens) <= max_tokens:
return text
truncated_tokens = tokens[:max_tokens]
return self.encoding.decode(truncated_tokens)
def prepare_for_embedding(self, text: str, max_chars: int = 8000) -> str:
"""เตรียมข้อความสำหรับสร้าง Embedding"""
# ตัดข้อความถ้ายาวเกินไป (ประมาณ 8000 ตัวอักษร ≈ 10000 tokens)
if len(text) > max_chars:
text = text[:max_chars] + "..."
return text
วิธีใช้งาน
processor = TextProcessor(model="gpt-4o")
ตรวจสอบก่อนส่ง
long_text = "ข้อความยาวมาก..." * 1000
safe_text = processor.prepare_for_embedding(long_text)
embedding = llm_client.create_embedding(safe_text)
print(f"✓ Token ที่ใช้: {processor.count_tokens(safe_text)}")
วิธีแก้ไข: ใช้ TextProcessor เพื่อตรวจสอบและตัดข้อความให้เหมาะสมก่อนส่งไปยัง API รวมถึงแบ่งเอกสารยาวเป็นส่วนๆ แล้วค่อยสร้าง Embedding แต่ละส่วน
4. ข้อผิดพลาด TimeoutError: Connection Timeout
สาเหตุ: เครือข่ายช้าหรือ API ไม่ตอบสนอง
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session_with_retry(retries: int = 3, backoff_factor: float = 0.5):
"""สร้าง Session ที่มีการ Retry อัตโนมัติ"""
session = requests.Session()
retry_strategy = Retry(
total=retries,
backoff_factor=backoff_factor,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST", "GET"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
class RobustLLMClient(LLMClient):
"""Client ที่มีความทนทานต่อข้อผิดพลาดเครือข่าย"""
def __init__(self, api_key: str, timeout: int = 60):
super().__init__(api_key)
self.timeout = timeout
self.session = create_session_with_retry()
def create_embedding(self, text: str, model: str = "text-embedding-3-small"):
try:
response = self.session.post(
f"{self.client.base_url}embeddings",
headers={"Authorization": f"Bearer {self.client.api_key}"},
json={"model": model, "input": text},
timeout=self.timeout
)
response.raise_for_status()
return response.json()["data"][0]["embedding"]
except requests.exceptions.Timeout:
print(f"⚠️ Connection Timeout - ลองใช้ Embedding Cache แทน")
return self._get_cached_embedding(text)
ใช้งาน
robust_client = RobustLLMClient(
api_key="YOUR_HOLYSHEEP_API_KEY",
timeout=120 # เพิ่ม timeout เป็น 120 วินาที
)
วิธีแก้ไข: เพิ่มค่า Timeout และใช้การ Retry อัตโนมัติ รวมถึงพิจารณาใช้ Caching สำหรับข้อความที่ถูกถามบ่อยๆ เพื่อลดการเรียก API ซ้ำ
สรุป
การพัฒนาเครื่องมือค