ค้นคำค้น คืนข้อมูล และสร้างคำตอบ
เขียนกระบวนการคำค้นที่สร้างเวกเตอร์ฝังตัวจากคำถามผู้ใช้ ค้นคืนส่วนข้อความอันดับต้น ๆ จัดรูปแบบพรอมต์เสริมบริบท เรียกใช้ LLM และส่งคืนคำตอบพร้อมการอ้างอิง
ค้นคำค้น คืนข้อมูล และสร้างคำตอบ เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
ขั้นตอนการค้นหา: ตั้งแต่ต้นจนจบ
ขั้นตอนการค้นหาคือ ส่วนออนไลน์ของ RAG ซึ่งเป็นโค้ดที่ทำงานแบบเรียลไทม์เมื่อผู้ใช้ถามคำถาม โดยเชื่อมต่อองค์ประกอบทั้งหมดที่สร้างขึ้นระหว่างการทำดัชนี ได้แก่ โมเดลเวกเตอร์ฝังตัว คลังเวกเตอร์ แม่แบบคำสั่ง และ LLM ขั้นตอนการค้นหาที่พัฒนาอย่างดีจะทำงานเสร็จภายใน 500 มิลลิวินาทีสำหรับงานส่วนใหญ่ และสร้างคำตอบที่มีข้อมูลรองรับพร้อมการอ้างอิงแหล่งที่มา ในบทเรียนนี้ เราจะสร้างแต่ละขั้นตอนขึ้นใหม่ตั้งแต่ต้น
ขั้นตอนที่ 1: สร้างเวกเตอร์ฝังตัวจากคำค้นของผู้ใช้
ขั้นตอนแรกคือการแปลงคำถามภาษาธรรมชาติของผู้ใช้ให้เป็นเวกเตอร์ฝังตัวโดยใช้ โมเดลเดียวกันกับที่ใช้ระหว่างการสร้างดัชนี เวกเตอร์ฝังตัวนี้เข้ารหัสความหมายเชิงนามธรรมของคำถาม และจะนำไปเปรียบเทียบกับเวกเตอร์ฝังตัวของส่วนย่อยเอกสารในคลังเวกเตอร์ ควรทำขั้นตอนนี้ให้รวดเร็ว โดยใช้โมเดลขนาดเล็ก เช่น text-embedding-3-small และแคชเวกเตอร์ฝังตัวสำหรับคำค้นที่เหมือนกันทุกประการและเกิดซ้ำ
from openai import OpenAI
client = OpenAI()
def embed_query(question: str) -> list:
response = client.embeddings.create(
model='text-embedding-3-small',
input=[question]
)
return response.data[0].embedding
user_question = 'What is our remote work policy?'
query_vector = embed_query(user_question)
print(f'Query embedded: {len(query_vector)}-dim vector')ขั้นตอนที่ 2: ดึงส่วนย่อย K อันดับแรก
ส่งเวกเตอร์คำค้นไปยังคลังเวกเตอร์เพื่อค้นหา ส่วนย่อยที่มีความหมายใกล้เคียงที่สุดจำนวน K รายการ ผลลัพธ์ที่ได้จะจัดอันดับตามคะแนนความคล้ายคลึงแบบโคไซน์ (โดยทั่วไปอยู่ระหว่าง 0.0 ถึง 1.0 ค่ายิ่งสูงยิ่งดี) ค่า K ที่เหมาะสมต้องสร้างสมดุลระหว่างความสมบูรณ์ของบริบทกับต้นทุนของหน้าต่างบริบท โดย K=5 เป็นจุดเริ่มต้นที่ใช้กันทั่วไป นอกจากนี้ คุณยังใช้ตัวกรองข้อมูลเมตาในขั้นตอนนี้เพื่อจำกัดการดึงข้อมูลให้อยู่ในแผนก ประเภทเอกสาร หรือช่วงวันที่ที่กำหนดได้
def retrieve_chunks(query_vector, index, top_k=5, filters=None):
query_params = {
'vector': query_vector,
'top_k': top_k,
'include_metadata': True
}
if filters:
query_params['filter'] = filters
results = index.query(**query_params)
chunks = []
for match in results.matches:
chunks.append({
'score': match.score,
'text': match.metadata['text'],
'source': match.metadata.get('source', ''),
'page': match.metadata.get('page', '')
})
return chunksขั้นตอนที่ 3: กรองตามเกณฑ์คะแนน
ส่วนย่อยที่ดึงมาไม่ได้เกี่ยวข้องอย่างแท้จริงทั้งหมด บางส่วนอาจมีคะแนนความคล้ายคลึงต่ำ แต่ยังติดอยู่ใน K อันดับแรก เพราะคำค้นอยู่นอกขอบเขตข้อมูลที่ดัชนีครอบคลุม ให้ใช้ เกณฑ์คะแนนขั้นต่ำ เพื่อกรองผลลัพธ์ที่มีความมั่นใจต่ำออก หากส่วนย่อยที่ดึงมาทั้งหมดมีคะแนนต่ำกว่าเกณฑ์ ให้ส่งคำตอบว่า “ไม่พบข้อมูล” แทนการส่งบริบทที่ไม่เกี่ยวข้องไปยัง LLM เพราะจะทำให้ได้คำตอบแย่กว่าการปฏิเสธอย่างเหมาะสม
MIN_SCORE_THRESHOLD = 0.75
def filter_by_score(chunks, threshold=MIN_SCORE_THRESHOLD):
relevant = [c for c in chunks if c['score'] >= threshold]
if not relevant:
print(f'No chunks above threshold {threshold}. Scores: {[c["score"] for c in chunks]}')
return relevant
retrieved = retrieve_chunks(query_vector, index, top_k=5)
filtered = filter_by_score(retrieved)
if not filtered:
print('Responding: no relevant information found')ขั้นตอนที่ 4: จัดรูปแบบบล็อกบริบท
รวบรวมส่วนย่อยที่ดึงมาเป็น บล็อกบริบทที่มีโครงสร้าง ซึ่ง LLM จะอ่าน กำกับป้ายกำกับแหล่งที่มาของแต่ละส่วนย่อย เพื่อให้โมเดลอ้างอิงได้อย่างถูกต้อง เพิ่มตัวคั่นระหว่างส่วนย่อยเพื่อให้อ่านได้ชัดเจน ควบคุมบริบททั้งหมดให้อยู่ภายในงบประมาณโทเค็น โดยนับโทเค็นด้วย tiktoken และตัดหรือลบส่วนย่อยที่มีคะแนนต่ำกว่าออกหากเกินขีดจำกัด บล็อกบริบทจะถูกแทรกลงในคำสั่งระหว่างคำสั่งของระบบกับคำถามของผู้ใช้
def format_context(chunks):
parts = []
for i, chunk in enumerate(chunks, start=1):
source_label = chunk['source']
if chunk.get('page'):
source_label += f", page {chunk['page']}"
parts.append(
f'[Document {i} | Source: {source_label}]\n{chunk["text"]}'
)
return '\n\n---\n\n'.join(parts)
context = format_context(filtered)
print(f'Context block: {len(context)} characters')ขั้นตอนที่ 5: สร้างคำสั่งเสริมบริบท
รวมบล็อกบริบท คำสั่งของระบบ และคำถามของผู้ใช้เข้าด้วยกันเป็น คำสั่งสุดท้าย ข้อความของระบบจะบอกโมเดลให้ใช้เฉพาะบริบทที่ให้ไว้และอ้างอิงแหล่งที่มา ส่วนข้อความของผู้ใช้จะประกอบด้วยบริบทที่จัดรูปแบบแล้วตามด้วยคำถาม การแยกส่วนอย่างชัดเจนนี้ช่วยป้องกันไม่ให้โมเดลปะปนเนื้อหาบริบทเข้ากับคำถาม และทำให้ขอบเขตระหว่างข้อมูลที่ดึงมากับข้อมูลป้อนเข้าของผู้ใช้ชัดเจน
def build_prompt(question, context):
system_message = (
'You are a helpful assistant. Answer the question using ONLY '
'the information in the provided documents. '
'Cite the document number(s) used, like [Doc 1]. '
'If the documents do not contain the answer, say so.'
)
user_message = (
f'Documents:\n\n{context}\n\n'
f'Question: {question}'
)
return system_message, user_messageขั้นตอนที่ 6: เรียกใช้ LLM และรับคำตอบ
ส่งคำสั่งที่ประกอบเสร็จแล้วไปยัง LLM โดยใช้ Chat Completions API สำหรับการถามตอบข้อเท็จจริง ให้ใช้ อุณหภูมิต่ำ (0.0 ถึง 0.3) เพื่อให้ได้คำตอบที่สม่ำเสมอและมีข้อมูลรองรับ อุณหภูมิที่สูงขึ้นจะทำให้คำตอบสร้างสรรค์มากขึ้น แต่เพิ่มความเสี่ยงที่โมเดลจะเติมข้อมูลนอกเหนือจากบริบท แยกวิเคราะห์คำตอบและส่งคืนทั้งข้อความคำตอบกับแหล่งที่มาที่ดึงมา เพื่อให้แอปพลิเคชันของคุณแสดงการอ้างอิงแก่ผู้ใช้ได้
def generate_answer(question, context, sources):
system_msg, user_msg = build_prompt(question, context)
response = client.chat.completions.create(
model='gpt-4o',
temperature=0.1, # low temperature for factual Q&A
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
answer = response.choices[0].message.content
return {
'answer': answer,
'sources': sources,
'tokens_used': response.usage.total_tokens
}นำทุกอย่างมาประกอบกัน
ขั้นตอนการค้นหาที่สมบูรณ์จะเรียกใช้ขั้นตอนเหล่านี้ตามลำดับ แต่ละขั้นตอนเป็นฟังก์ชันบริสุทธิ์ที่คุณสามารถทดสอบแยกกันได้ และข้อมูลจะไหลจากขั้นตอนหนึ่งไปยังขั้นตอนถัดไปอย่างเป็นระเบียบ การเพิ่มการบันทึกเหตุการณ์ในแต่ละขั้นตอนทำให้สามารถสังเกตการทำงานของขั้นตอนได้ คุณจะเห็นได้อย่างชัดเจนว่าส่วนย่อยใดถูกดึงมา มีคะแนนเท่าใด ประกอบบริบทอย่างไร และใช้โทเค็นไปกี่รายการ การมองเห็นนี้จำเป็นอย่างยิ่งต่อการแก้ไขข้อบกพร่องและการปรับปรุงคุณภาพการดึงข้อมูล
def answer_question(user_question, vector_index):
# Step 1: Embed query
q_vector = embed_query(user_question)
# Step 2: Retrieve
chunks = retrieve_chunks(q_vector, vector_index, top_k=5)
# Step 3: Filter low-confidence matches
chunks = filter_by_score(chunks, threshold=0.70)
if not chunks:
return {'answer': 'I do not have information about that topic.', 'sources': []}
# Step 4 & 5: Format and build prompt
context = format_context(chunks)
sources = [c['source'] for c in chunks]
# Step 6: Generate
return generate_answer(user_question, context, sources)การเพิ่มประสิทธิภาพเวลาแฝง
ขั้นตอนการค้นหามีสองขั้นตอนที่ถูกจำกัดด้วยการรับส่งข้อมูลเข้าออก ได้แก่ การเรียกใช้การสร้างเวกเตอร์ฝังตัวและการเรียกใช้ LLM ควรทำงานทั้งสองขั้นตอนโดย ไม่รอโดยไม่จำเป็น การเรียกใช้การสร้างเวกเตอร์ฝังตัวทำได้รวดเร็ว (<100ms) ส่วนการเรียกใช้ LLM ใช้เวลานาน (500ms-3s) เพื่อลดเวลาแฝงที่ผู้ใช้รับรู้ ให้ ส่งคำตอบจาก LLM แบบต่อเนื่อง เพื่อให้โทเค็นปรากฏทันทีที่สร้างขึ้น แทนที่จะรอคำตอบทั้งหมด และควรแคชเวกเตอร์ฝังตัวของคำค้นที่เหมือนกันทุกประการและเกิดซ้ำ เพื่อหลีกเลี่ยงการเรียกใช้ API ซ้ำซ้อน
async def answer_question_streaming(question, index):
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
if not chunks:
yield 'I do not have information about that topic.'
return
context = format_context(chunks)
system_msg, user_msg = build_prompt(question, context)
stream = await client.chat.completions.create(
model='gpt-4o',
stream=True,
messages=[
{'role': 'system', 'content': system_msg},
{'role': 'user', 'content': user_msg}
]
)
async for chunk in stream:
delta = chunk.choices[0].delta.content or ''
yield deltaการบันทึกเหตุการณ์เพื่อการสังเกตการทำงาน
ขั้นตอน RAG ในระบบจริงจำเป็นต้องมีการบันทึกเหตุการณ์แบบมีโครงสร้าง เพื่อให้คุณวินิจฉัยได้ว่าเกิดข้อผิดพลาดในการดึงข้อมูลเมื่อใด หรือ LLM ให้คำตอบที่ไม่ดีเมื่อใด ให้บันทึก คำค้น ID และคะแนนของส่วนย่อยที่ดึงมา จำนวนโทเค็นของบริบท คำตอบ และเวลาแฝงสำหรับทุกคำขอ จัดเก็บบันทึกเหล่านี้ในฐานข้อมูลหรือแพลตฟอร์มสำหรับสังเกตการทำงาน เมื่อผู้ใช้รายงานว่าได้รับคำตอบไม่ดี คุณจะสามารถเรียกใช้คำค้นเดิมซ้ำและตรวจสอบได้ว่าส่วนย่อยใดถูกดึงมาและเหตุใดจึงไม่เพียงพอ
import time
import logging
import json
def answer_question_with_logging(question, index):
start = time.time()
q_vector = embed_query(question)
chunks = retrieve_chunks(q_vector, index, top_k=5)
chunks = filter_by_score(chunks)
context = format_context(chunks)
result = generate_answer(question, context, [c['source'] for c in chunks])
latency_ms = (time.time() - start) * 1000
log_entry = {
'question': question,
'num_chunks_retrieved': len(chunks),
'chunk_scores': [c['score'] for c in chunks],
'tokens_used': result.get('tokens_used'),
'latency_ms': round(latency_ms)
}
logging.info(json.dumps(log_entry))
return resultการแคชเวกเตอร์ฝังตัวของคำค้น
หากแอปพลิเคชันของคุณได้รับคำค้นที่ซ้ำกันหรือเกือบเหมือนกันจำนวนมาก เช่น บอต FAQ ที่ผู้ใช้มักถามคำถามเดิม การแคชเวกเตอร์ฝังตัวของคำค้นเป็นการเพิ่มประสิทธิภาพที่ทำได้ง่ายและให้ผลสูง ให้แฮชสตริงคำค้น ตรวจสอบแคช Redis เพื่อหาเวกเตอร์ฝังตัวที่ตรงกัน และเรียกใช้ API สำหรับสร้างเวกเตอร์ฝังตัวเฉพาะเมื่อไม่พบข้อมูลในแคช อัตราการพบข้อมูลเวกเตอร์ฝังตัวในแคชที่ 30-60% เป็นเรื่องปกติในบอต FAQ และบอตสนทนาสำหรับการสนับสนุนในระบบจริง ช่วยลดค่าใช้จ่าย API ได้มากและลดเวลาแฝงลง 50-100 มิลลิวินาทีต่อคำค้นที่พบในแคช
import hashlib
import json
import redis
r = redis.Redis(host='localhost', port=6379)
EMBED_CACHE_TTL = 86400 # 24 hours
def embed_query_cached(question):
cache_key = 'embed:' + hashlib.sha256(question.encode()).hexdigest()
cached = r.get(cache_key)
if cached:
return json.loads(cached) # cache hit
# Cache miss: call the API
vector = embed_query(question)
r.setex(cache_key, EMBED_CACHE_TTL, json.dumps(vector))
return vectorตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดวิศวกรรม AI จากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้เกี่ยวกับ ขั้นตอนการค้นหา 6 ขั้น (สร้างเวกเตอร์ฝังตัวจากคำค้น ดึงส่วนย่อย กรองตามคะแนน จัดรูปแบบบริบท สร้างคำสั่ง และสร้างคำตอบ) การกรองตามเกณฑ์คะแนนเพื่อจัดการกับคำค้นที่อยู่นอกขอบเขตของดัชนี และ การปรับปรุงสำหรับระบบจริง ซึ่งรวมถึงการส่งคำตอบแบบต่อเนื่อง การบันทึกเหตุการณ์แบบมีโครงสร้าง และการเพิ่มประสิทธิภาพเวลาแฝง บทถัดไป เราจะเรียนรู้วิธีประเมินว่าระบบ RAG ที่สมบูรณ์ของคุณทำงานได้ถูกต้องจริงหรือไม่
คำถามที่พบบ่อย
บทเรียน “ค้นคำค้น คืนข้อมูล และสร้างคำตอบ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ค้นคำค้น คืนข้อมูล และสร้างคำตอบ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ค้นคำค้น คืนข้อมูล และสร้างคำตอบ”
เขียนกระบวนการคำค้นที่สร้างเวกเตอร์ฝังตัวจากคำถามผู้ใช้ ค้นคืนส่วนข้อความอันดับต้น ๆ จัดรูปแบบพรอมต์เสริมบริบท เรียกใช้ LLM และส่งคืนคำตอบพร้อมการอ้างอิง คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ค้นคำค้น คืนข้อมูล และสร้างคำตอบ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม
ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การโหลดเอกสารและการดึงข้อความ
- กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ
- การทำดัชนี: การสร้างและจัดเก็บส่วนข้อความ
- ค้นคำค้น คืนข้อมูล และสร้างคำตอบ