0Pricing
AI Engineering Academy · บทเรียน

ค้นคำค้น คืนข้อมูล และสร้างคำตอบ

เขียนกระบวนการคำค้นที่สร้างเวกเตอร์ฝังตัวจากคำถามผู้ใช้ ค้นคืนส่วนข้อความอันดับต้น ๆ จัดรูปแบบพรอมต์เสริมบริบท เรียกใช้ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การโหลดเอกสารและการดึงข้อความ
  2. กลยุทธ์การแบ่งส่วน: ขนาดคงที่ เทียบกับประโยค เทียบกับแบบเรียกซ้ำ
  3. การทำดัชนี: การสร้างและจัดเก็บส่วนข้อความ
  4. ค้นคำค้น คืนข้อมูล และสร้างคำตอบ
← กลับไปที่ AI Engineering Academy