AI Engineering Academy · บทเรียน

การพัฒนาความสามารถหลักของ RAG และเอเจนต์

สร้างกระบวนการนำเข้าเอกสาร การทำดัชนีคลังเวกเตอร์ การค้นคืนพร้อมจัดอันดับใหม่ และการผสานรวมเครื่องมือของเอเจนต์ตามรูปแบบที่เรียนรู้มาตลอดหลักสูตร

บทเรียน 2 จาก 413 ขั้นตอน

การพัฒนาความสามารถหลักของ RAG และเอเจนต์ เป็นบทเรียน AI Engineering Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน AI Engineering Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน

ลำดับการพัฒนาและสิ่งที่ต้องพึ่งพา

สร้างระบบจากล่างขึ้นบน: เริ่มจากส่วนประกอบที่ไม่มีสิ่งภายนอกต้องพึ่งพา แล้วจึงเพิ่มชั้นของส่วนประกอบที่พึ่งพาส่วนประกอบเหล่านั้น สำหรับระบบ RAG + เอเจนต์ ลำดับคือ: (1) โครงสร้างสคีมาคลังเวกเตอร์ (2) กระบวนการนำเข้าข้อมูล (3) ตัวค้นคืน (4) ห่วงโซ่ตอบคำถามพื้นฐาน (5) จุดปลายทางแบบสตรีม (6) เอเจนต์พร้อมเครื่องมือ (7) ชั้นแคช (8) การติดตั้งเครื่องมือติดตามร่องรอย ทดสอบแต่ละส่วนประกอบแยกกันก่อนผสานเข้ากับกระบวนการ

# Build order:
IMPL_ORDER = [
    'pgvector_schema',      # prerequisite for everything
    'document_ingestion',   # populate the vector store
    'hybrid_retriever',     # test retrieval in isolation
    'qa_chain_basic',       # integrate LLM with retrieval
    'streaming_endpoint',   # expose via API
    'function_calling',     # add agent tool calls
    'semantic_cache',       # reduce repeat API calls
    'langsmith_tracing',    # add after core works
    'injection_filter',     # harden before load testing
]

การตั้งค่าคลังเวกเตอร์

สร้างตารางพีจีเวกเตอร์ด้วยสคีมาที่ถูกต้องก่อนนำเข้าเอกสาร ใส่คอลัมน์สำหรับเวกเตอร์ ข้อความของชิ้นส่วน ฟิลด์ข้อมูลกำกับทั้งหมด และการประทับเวลา updated_at สำหรับการทำดัชนีใหม่เฉพาะส่วน สร้างดัชนีเวกเตอร์ (HNSW หรือ IVFFlat) บนคอลัมน์เวกเตอร์ฝังทันที เพราะการเพิ่มดัชนีหลังมีแถวหลายล้านแถวนั้นช้ากว่าการเพิ่มตั้งแต่ต้นบนตารางว่างมาก

-- PostgreSQL schema with pgvector
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE document_chunks (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      TEXT NOT NULL,
    chunk_text  TEXT NOT NULL,
    embedding   VECTOR(1536) NOT NULL,
    source_file TEXT,
    page_number INT,
    section     TEXT,
    doc_type    TEXT,
    tenant_id   TEXT NOT NULL,  -- for data isolation
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_chunks_hnsw ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

CREATE INDEX idx_chunks_tenant ON document_chunks(tenant_id);

การสร้างกระบวนการนำเข้าข้อมูล

พัฒนาการนำเข้าข้อมูลเป็นฟังก์ชันแบบอะซิงก์เดียวที่รับเส้นทางไฟล์หรือ URL และคืนค่าจำนวนชิ้นส่วนที่ทำดัชนี ใช้ตัวโหลดเอกสารของ LangChain สำหรับประเภทไฟล์ต่าง ๆ และตัวแบ่งข้อความตามอักขระแบบเรียกซ้ำสำหรับการแบ่งส่วน แบ่งการเรียกสร้างเวกเตอร์ฝังเป็นชุดเพื่อให้อยู่ภายในขีดจำกัดอินพุต 2,048 โทเค็น และลดการเรียก API จากหลักพันครั้งเหลือเพียงหลักสิบครั้ง

from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from openai import AsyncOpenAI

async def ingest_document(file_path: str, doc_id: str, tenant_id: str) -> int:
    loader = PyPDFLoader(file_path)
    pages = loader.load()
    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=100)
    chunks = splitter.split_documents(pages)
    # Batch embed
    texts = [c.page_content for c in chunks]
    client = AsyncOpenAI()
    embeddings_response = await client.embeddings.create(
        model='text-embedding-3-small',
        input=texts
    )
    embeddings = [e.embedding for e in embeddings_response.data]
    await insert_chunks_to_pgvector(chunks, embeddings, doc_id, tenant_id)
    return len(chunks)

การสร้างตัวค้นคืนแบบผสม

ผสานการค้นหาเวกเตอร์แบบหนาแน่นเข้ากับการค้นหาคำสำคัญ BM25 และรวมผลลัพธ์ด้วยการหลอมรวมอันดับแบบผกผัน พัฒนาตัวค้นคืนเป็นคลาสที่มี retrieve(query, tenant_id, top_k) เพียงเมธอดเดียว ภายในนั้นให้เรียกใช้การค้นหาทั้งสองแบบพร้อมกันด้วย asyncio.gather รวมรายการที่จัดอันดับด้วย RRF ลบรายการซ้ำตาม ID ของชิ้นส่วน และคืนผลลัพธ์อันดับต้น ๆ ตาม top_k พร้อมข้อมูลกำกับของแหล่งที่มา

import asyncio
from rank_bm25 import BM25Okapi

class HybridRetriever:
    def __init__(self, pool, k_rrf: int = 60):
        self.pool = pool
        self.k_rrf = k_rrf

    async def retrieve(self, query: str, tenant_id: str, top_k: int = 10) -> list:
        dense_results, sparse_results = await asyncio.gather(
            self._dense_search(query, tenant_id, top_k * 3),
            self._bm25_search(query, tenant_id, top_k * 3)
        )
        merged = self._rrf_merge(dense_results, sparse_results)
        return merged[:top_k]

    def _rrf_score(self, rank: int) -> float:
        return 1.0 / (self.k_rrf + rank + 1)

การพัฒนาห่วงโซ่ตอบคำถามหลัก

สร้างห่วงโซ่ตอบคำถามหลักด้วย LangChain LCEL ห่วงโซ่นี้รับคำถามและชิ้นส่วนที่ค้นคืนมา จัดรูปแบบพรอมต์เสริมพร้อมคำแนะนำให้ใช้เฉพาะบริบทที่ให้มา และสตรีมคำตอบ เพิ่มคำแนะนำอย่างชัดเจนให้โมเดลอ้างอิงแหล่งที่มาตามชื่อเอกสารและหมายเลขหน้า และตอบว่า 'ฉันไม่ทราบ' เมื่อคำตอบไม่มีอยู่ในบริบทที่ค้นคืนมา

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain.schema.output_parser import StrOutputParser

RAG_PROMPT = ChatPromptTemplate.from_messages([
    ('system', 'You are a precise assistant. Answer ONLY using the provided context. '
               'Cite sources as [DocName, p.N]. If the answer is not in the context, say "I don\'t have information about that."'),
    ('user', 'Context:\n{context}\n\nQuestion: {question}')
])

llm = ChatOpenAI(model='gpt-4o', temperature=0, streaming=True)
qa_chain = RAG_PROMPT | llm | StrOutputParser()

async def answer_question(question: str, chunks: list) -> str:
    context = '\n\n'.join(f'[{c["source"]}]\n{c["text"]}' for c in chunks)
    return await qa_chain.ainvoke({'context': context, 'question': question})

การเพิ่มการเรียกใช้ฟังก์ชันให้เอเจนต์

ขยายระบบตอบคำถามพื้นฐานด้วยการเรียกใช้ฟังก์ชัน เพื่อจัดการคำค้นที่ต้องใช้ข้อมูลแบบเรียลไทม์หรือการคำนวณ กำหนดเครื่องมือสำหรับ: ค้นหาข้อมูลปัจจุบันบนเว็บ เรียกใช้คำค้น SQL กับฐานข้อมูลที่ปลอดภัยและอ่านได้อย่างเดียว และค้นหาระเบียนเฉพาะตาม ID เอเจนต์จะตัดสินใจว่าจะเรียกใช้เครื่องมือใดจากคำถาม ดำเนินการกับเครื่องมือเหล่านั้น และนำผลลัพธ์มารวมไว้ในคำตอบสุดท้าย

from openai import AsyncOpenAI
import json

TOOLS = [
    {
        'type': 'function',
        'function': {
            'name': 'search_knowledge_base',
            'description': 'Search the internal document knowledge base for relevant information',
            'parameters': {
                'type': 'object',
                'properties': {
                    'query': {'type': 'string', 'description': 'Search query'},
                    'top_k': {'type': 'integer', 'default': 5}
                },
                'required': ['query']
            }
        }
    }
]

async def agent_with_tools(question: str, tenant_id: str) -> str:
    client = AsyncOpenAI()
    messages = [{'role': 'user', 'content': question}]
    while True:
        resp = await client.chat.completions.create(
            model='gpt-4o', messages=messages, tools=TOOLS)
        if resp.choices[0].finish_reason != 'tool_calls':
            return resp.choices[0].message.content
        tool_call = resp.choices[0].message.tool_calls[0]
        args = json.loads(tool_call.function.arguments)
        result = await dispatch_tool(tool_call.function.name, args, tenant_id)
        messages.append({'role': 'tool', 'tool_call_id': tool_call.id, 'content': result})

การสตรีมคำตอบของเอเจนต์

ห่อหุ้มเอเจนต์ด้วย StreamingResponse ของ FastAPI โดยใช้เหตุการณ์ที่ส่งจากเซิร์ฟเวอร์ เพื่อให้ส่วนหน้าแสดงโทเค็นทันทีที่มาถึง สำหรับการตอบกลับของเอเจนต์ที่มีการเรียกใช้เครื่องมือ ให้สตรีมตัวบ่งชี้ความคืบหน้าขณะที่เครื่องมือกำลังทำงาน เช่น 'กำลังค้นหาฐานความรู้...' จากนั้นจึงสตรีมคำตอบสุดท้ายทีละโทเค็น วิธีนี้ป้องกันไม่ให้หน้าดูเหมือนค้างระหว่างที่เครื่องมือใช้เวลาในการทำงาน

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def event_stream(question: str, tenant_id: str):
    yield 'data: {"type": "start"}\n\n'
    chunks = await retriever.retrieve(question, tenant_id)
    yield 'data: {"type": "retrieving", "count": ' + str(len(chunks)) + '}\n\n'
    async for token in qa_chain.astream({'context': format_context(chunks), 'question': question}):
        yield f'data: {{"type": "token", "content": {repr(token)}}}\n\n'
    yield 'data: {"type": "done"}\n\n'

@app.post('/query/stream')
async def stream_query(question: str, tenant_id: str):
    return StreamingResponse(event_stream(question, tenant_id), media_type='text/event-stream')

เชื่อมต่อ Semantic Cache

เพิ่ม Semantic Cache เป็นขั้นตอนก่อนการค้นคืนในกระบวนการประมวลผลคำค้น ก่อนเรียกใช้ตัวค้นคืนและ LLM ให้นำคำถามของผู้ใช้ไปสร้างเวกเตอร์ฝังตัวและตรวจสอบแคช หากพบรายการในแคชที่มีค่าความคล้ายคลึงสูงกว่าเกณฑ์ ให้ส่งคืนคำตอบที่แคชไว้ทันทีพร้อมแฟล็ก cached: true หากไม่พบรายการ ให้ดำเนินการผ่านกระบวนการทั้งหมด และจัดเก็บคำตอบที่ได้ลงในแคชเพื่อใช้กับคำค้นที่คล้ายกันในอนาคต

async def query_pipeline(question: str, tenant_id: str) -> dict:
    # 1. Check semantic cache
    cache_hit = await semantic_cache.lookup(question, tenant_id, threshold=0.92)
    if cache_hit:
        return {'answer': cache_hit.answer, 'cached': True, 'sources': cache_hit.sources}

    # 2. Retrieve
    chunks = await retriever.retrieve(question, tenant_id, top_k=5)
    chunks = await reranker.rerank(question, chunks, top_n=3)

    # 3. Generate
    answer = await answer_question(question, chunks)
    sources = [c['source'] for c in chunks]

    # 4. Cache result
    await semantic_cache.store(question, tenant_id, answer, sources)

    return {'answer': answer, 'cached': False, 'sources': sources}

เพิ่มการติดตามด้วย LangSmith

เพิ่มการตรวจวัดกระบวนการด้วย LangSmith โดยกำหนดตัวแปรสภาพแวดล้อมสองรายการ การเรียกใช้สายงานของ LangChain ทุกครั้งจะถูกติดตามโดยอัตโนมัติ พร้อมจำนวนโทเค็น เวลาแฝง ข้อมูลนำเข้า ข้อมูลส่งออก และข้อผิดพลาดต่าง ๆ สำหรับโค้ดที่กำหนดเองและไม่ใช่ LangChain เช่น การค้นคืนและการจัดอันดับใหม่ ให้ห่อหุ้มการเรียกใช้ด้วยตัวตกแต่ง @traceable เพื่อรวมไว้ในการติดตาม วิธีนี้ทำให้มองเห็นกระบวนการทุกขั้นตอนได้อย่างครบถ้วนตั้งแต่ต้นจนจบ

import os
from langsmith import traceable

os.environ['LANGCHAIN_TRACING_V2'] = 'true'
os.environ['LANGCHAIN_API_KEY'] = os.environ['LANGSMITH_API_KEY']
os.environ['LANGCHAIN_PROJECT'] = 'document-qa-production'

# Wrap non-LangChain steps with @traceable
@traceable(name='hybrid_retrieval')
async def traced_retrieval(question: str, tenant_id: str, top_k: int) -> list:
    return await retriever.retrieve(question, tenant_id, top_k)

@traceable(name='cohere_reranking')
async def traced_reranking(question: str, chunks: list) -> list:
    return await reranker.rerank(question, chunks)

# LangChain LCEL chains are automatically traced — no extra code needed

เรียกใช้การทดสอบการเชื่อมต่อระบบ

เขียนการทดสอบการเชื่อมต่อระบบที่ตรวจสอบกระบวนการประมวลผลคำค้นทั้งหมด ตั้งแต่รับคำถามจนถึงคำตอบสุดท้าย ใช้คลังเวกเตอร์สำหรับการทดสอบขนาดเล็กที่มีเอกสารเป็นที่รู้จัก เพื่อให้เขียนการตรวจสอบแบบกำหนดผลลัพธ์แน่นอนได้ว่า ควรค้นคืนส่วนใดและคำตอบควรมีเนื้อหาอะไร เรียกใช้การทดสอบการเชื่อมต่อระบบกับสภาพแวดล้อมเตรียมใช้งานที่จำลองโครงสร้างพื้นฐานจริง แต่ใช้คลังเวกเตอร์และคีย์ LLM แยกต่างหาก

import pytest

@pytest.mark.asyncio
async def test_full_pipeline_returns_grounded_answer():
    # Setup: ingest known document
    await ingest_document('tests/fixtures/policy.pdf', 'policy_v1', 'test_tenant')

    # Query with a question that has a known answer in the document
    result = await query_pipeline(
        question='What is the cancellation policy?',
        tenant_id='test_tenant'
    )

    assert result['answer'] is not None
    assert len(result['answer']) > 50
    assert '24 hours' in result['answer']  # known fact in document
    assert 'policy.pdf' in str(result['sources'])
    assert result['cached'] is False  # fresh query

วัดคุณภาพการค้นคืนพื้นฐาน

ก่อนปรับปรุงสิ่งใด ให้กำหนด ค่าพื้นฐานของการค้นคืน เสียก่อน ใช้ชุดข้อมูลสำหรับประเมินเพื่อวัดอัตราการพบผลลัพธ์ที่ต้องการ (เอกสารที่ถูกต้องปรากฏในผลลัพธ์ 5 อันดับแรกหรือไม่) และ MRR (เอกสารนั้นอยู่ในอันดับสูงเพียงใด) เรียกใช้การวัดพื้นฐานนี้หลังตั้งค่าคลังเวกเตอร์เริ่มต้นแล้ว และก่อนเพิ่มการจัดอันดับใหม่หรือการค้นหาแบบผสม ค่าพื้นฐานจะบอกให้ทราบว่าการปรับปรุงแต่ละอย่างให้ผลดีขึ้นจริงเพียงใด ทำให้มีหลักฐานประกอบการตัดสินใจว่าเทคนิคใดควรเก็บไว้

async def measure_retrieval_baseline(test_cases: list) -> dict:
    hits = 0
    reciprocal_ranks = []
    for case in test_cases:
        results = await retriever.retrieve(case['question'], case['tenant_id'], top_k=5)
        result_docs = [r['doc_id'] for r in results]
        if case['relevant_doc'] in result_docs:
            hits += 1
            rank = result_docs.index(case['relevant_doc']) + 1
            reciprocal_ranks.append(1.0 / rank)
        else:
            reciprocal_ranks.append(0.0)
    return {
        'hit_rate_at_5': hits / len(test_cases),
        'mrr': sum(reciprocal_ranks) / len(reciprocal_ranks)
    }

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจเกี่ยวกับการสร้างความสามารถหลักของ RAG และเอเจนต์

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า ลำดับการนำไปใช้จากพื้นฐานขึ้นไป ช่วยให้ทดสอบแต่ละองค์ประกอบแยกกันก่อนการผสานรวมได้ การค้นคืนแบบผสมด้วยการค้นหาแบบหนาแน่นและ BM25 พร้อมกัน ซึ่งรวมผลด้วย RRF ให้คุณภาพการค้นคืนดีที่สุด และ การติดตามด้วย LangSmith พร้อมตัวตกแต่ง @traceable ช่วยให้มองเห็นกระบวนการทั้งหมดได้อย่างครบถ้วน ต่อไปเราจะเสริมความแข็งแกร่งให้ระบบด้วยรูปแบบด้านความปลอดภัย การแคช และความน่าเชื่อถือ

เริ่มต้นได้ฟรี

เรียนรู้ Python ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
30
บทเรียน
120

คำถามที่พบบ่อย

บทเรียน “การพัฒนาความสามารถหลักของ RAG และเอเจนต์” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การพัฒนาความสามารถหลักของ RAG และเอเจนต์” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส AI Engineering Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส AI Engineering Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การพัฒนาความสามารถหลักของ RAG และเอเจนต์”

สร้างกระบวนการนำเข้าเอกสาร การทำดัชนีคลังเวกเตอร์ การค้นคืนพร้อมจัดอันดับใหม่ และการผสานรวมเครื่องมือของเอเจนต์ตามรูปแบบที่เรียนรู้มาตลอดหลักสูตร คุณปฏิบัติ AI Engineering Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน AI Engineering Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน AI Engineering Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “การพัฒนาความสามารถหลักของ RAG และเอเจนต์” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน AI Engineering Academy นี้ได้ไหม

ได้ บทเรียน AI Engineering Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

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

  1. การออกแบบสถาปัตยกรรมสำหรับใช้งานจริง
  2. การพัฒนาความสามารถหลักของ RAG และเอเจนต์
  3. การเสริมความแข็งแกร่ง: ความปลอดภัย การแคช และความน่าเชื่อถือ
  4. การประเมินผล การนำไปใช้งาน และการทบทวนหลังดำเนินการ
← กลับไปที่ AI Engineering Academy