AI Engineering Academy · Lekcja

Implementowanie podstawowych funkcji RAG i agenta

Zbuduj potok pozyskiwania dokumentów, indeksowanie w magazynie wektorowym, wyszukiwanie z ponownym rankingiem oraz integracje narzędzi agenta, stosując wzorce poznane w całej ścieżce.

Lekcja 2 z 413 kroki

Implementowanie podstawowych funkcji RAG i agenta to bezpłatna lekcja AI Engineering Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Engineering Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Kolejność implementacji i zależności

System należy budować od dołu: rozpocząć od komponentów, które nie mają zewnętrznych zależności, a następnie dodawać warstwy komponentów zależnych od nich. W przypadku systemu RAG + agent kolejność jest następująca: (1) schemat bazy wektorowej, (2) potok pozyskiwania danych, (3) retriever, (4) podstawowy łańcuch pytań i odpowiedzi, (5) endpoint strumieniowy, (6) agent z narzędziami, (7) warstwa buforowania, (8) instrumentacja śledzenia. Każdy komponent należy przetestować osobno przed zintegrowaniem go z potokiem.

# 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
]

Konfigurowanie bazy wektorowej

Tabelę pgvector z odpowiednim schematem należy utworzyć przed pozyskaniem dokumentów. Należy uwzględnić kolumny na wektor, tekst fragmentu, wszystkie pola metadanych oraz znacznik czasu updated_at do selektywnego ponownego indeksowania. Indeks wektorowy (HNSW lub IVFFlat) należy utworzyć na kolumnie embeddingów od razu — dodanie indeksu po wstawieniu milionów wierszy jest znacznie wolniejsze niż utworzenie go wcześniej na pustej tabeli.

-- 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);

Budowanie potoku pozyskiwania danych

Pozyskiwanie danych należy zaimplementować jako pojedynczą funkcję asynchroniczną, która przyjmuje ścieżkę pliku lub URL i zwraca liczbę zindeksowanych fragmentów. Należy użyć loaderów dokumentów LangChain dla różnych typów plików oraz rekurencyjnego dzielnika tekstu na podstawie znaków do dzielenia na fragmenty. Wywołania tworzenia embeddingów należy wykonywać partiami, aby mieścić się w limicie wejściowym 2048 tokenów i ograniczyć liczbę wywołań API z tysięcy do kilkudziesięciu.

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)

Budowanie hybrydowego retrievera

Należy połączyć wyszukiwanie za pomocą gęstych wektorów z wyszukiwaniem słów kluczowych BM25, a następnie połączyć wyniki za pomocą reciprocal rank fusion. Retriever należy zaimplementować jako klasę z jedną metodą retrieve(query, tenant_id, top_k). W jej wnętrzu należy uruchomić oba wyszukiwania współbieżnie za pomocą asyncio.gather, połączyć uporządkowane listy z użyciem RRF, usunąć duplikaty według identyfikatora fragmentu i zwrócić wyniki top_k wraz z metadanymi źródłowymi.

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)

Implementowanie podstawowego łańcucha QA

Podstawowy łańcuch pytań i odpowiedzi należy zbudować za pomocą LangChain LCEL. Łańcuch przyjmuje pytanie i pobrane fragmenty, formatuje rozszerzony prompt z instrukcjami korzystania wyłącznie z dostarczonego kontekstu oraz przesyła odpowiedź strumieniowo. Należy dodać wyraźne instrukcje, aby model cytował źródła, podając nazwę dokumentu i numer strony, a także mówił „I don't know”, gdy odpowiedzi nie ma w pobranym kontekście.

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})

Dodawanie wywoływania funkcji do agenta

Podstawowy system pytań i odpowiedzi należy rozszerzyć o wywoływanie funkcji, aby obsługiwać zapytania wymagające danych w czasie rzeczywistym lub obliczeń. Należy zdefiniować narzędzia do: wyszukiwania aktualnych informacji w internecie, wykonywania zapytań SQL w bezpiecznej bazie danych tylko do odczytu oraz wyszukiwania konkretnych rekordów według identyfikatora. Agent na podstawie pytania decyduje, które narzędzia wywołać, wykonuje te wywołania i uwzględnia wyniki w odpowiedzi końcowej.

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})

Przesyłanie odpowiedzi agenta strumieniowo

Opakuj agenta w obiekt FastAPI StreamingResponse wykorzystujący server-sent events, aby frontend wyświetlał tokeny w miarę ich napływania. W przypadku odpowiedzi agenta wywołujących narzędzia przesyłaj wskaźnik postępu podczas wykonywania narzędzia („Searching knowledge base...”), a następnie przesyłaj końcową odpowiedź token po tokenie. Dzięki temu strona nie sprawia wrażenia zawieszonej podczas oczekiwania na wykonanie narzędzia.

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')

Podłączenie Semantic Cache

Dodaj semantic cache jako krok poprzedzający wyszukiwanie w potoku zapytań. Przed użyciem retrievera i LLM utwórz embedding pytania użytkownika i sprawdź pamięć podręczną. Jeśli znaleziono pasujący wpis o podobieństwie przekraczającym próg, natychmiast zwróć zapisaną odpowiedź z flagą cached: true. W przypadku braku trafienia zapytanie przechodzi przez cały potok, a wynikowa odpowiedź zostaje zapisana w pamięci podręcznej na potrzeby przyszłych podobnych zapytań.

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}

Dodawanie śledzenia LangSmith

Oprzyrządkuj potok za pomocą LangSmith, ustawiając dwie zmienne środowiskowe. Każde wywołanie łańcucha LangChain jest automatycznie śledzone wraz z liczbą tokenów, opóźnieniem, danymi wejściowymi, wynikami i ewentualnymi błędami. W przypadku niestandardowego kodu niezwiązanego z LangChain (wyszukiwanie, reranking) opakuj wywołania dekoratorami @traceable, aby uwzględnić je w śladzie. Zapewnia to pełny wgląd w każdy krok potoku, od początku do końca.

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

Uruchamianie testów integracyjnych

Napisz testy integracyjne obejmujące cały potok zapytań — od wprowadzenia pytania do uzyskania końcowej odpowiedzi. Użyj niewielkiego testowego magazynu wektorowego ze znanymi dokumentami, aby móc tworzyć deterministyczne asercje dotyczące tego, które fragmenty powinny zostać znalezione i co powinna zawierać odpowiedź. Uruchamiaj testy integracyjne w środowisku stagingowym odwzorowującym infrastrukturę produkcyjną, ale korzystającym z osobnego magazynu wektorowego i klucza 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

Pomiar bazowej jakości wyszukiwania

Zanim zaczniesz cokolwiek optymalizować, ustal bazową jakość wyszukiwania. Użyj zestawu testowego do ewaluacji, aby zmierzyć współczynnik trafień (czy właściwy dokument pojawia się wśród 5 najlepszych wyników?) oraz MRR (jak wysoko jest w rankingu?). Uruchom ten pomiar po skonfigurowaniu początkowego magazynu wektorowego, a przed dodaniem rerankingu lub wyszukiwania hybrydowego. Wartość bazowa pokaże, jakie rzeczywiste korzyści przynosi każda optymalizacja, dzięki czemu będziesz mieć podstawy do oceny, które techniki warto zachować.

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)
    }

Szybkie sprawdzenie

Sprawdź swoją wiedzę na temat tworzenia podstawowych funkcji RAG i agentów.

Podsumowanie lekcji

W tej lekcji poznano następujące zagadnienia: oddolna kolejność implementacji gwarantuje przetestowanie każdego komponentu w izolacji przed integracją, wyszukiwanie hybrydowe z równoległym wyszukiwaniem gęstym i BM25, połączone za pomocą RRF, zapewnia najlepszą jakość wyszukiwania, a śledzenie LangSmith z dekoratorami @traceable daje pełny wgląd w działanie potoku. W następnej części zwiększymy odporność systemu, korzystając z wzorców bezpieczeństwa, buforowania i niezawodności.

Bezpłatny start

Ucz się Python dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
30
Lekcje
120

Często zadawane pytania

Czy lekcja „Implementowanie podstawowych funkcji RAG i agenta” jest bezpłatna?

Tak — pełny tekst „Implementowanie podstawowych funkcji RAG i agenta” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Engineering Academy, przejdź na CoddyKit PRO. Kurs AI Engineering Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Implementowanie podstawowych funkcji RAG i agenta”?

Zbuduj potok pozyskiwania dokumentów, indeksowanie w magazynie wektorowym, wyszukiwanie z ponownym rankingiem oraz integracje narzędzi agenta, stosując wzorce poznane w całej ścieżce. Ćwiczysz AI Engineering Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Engineering Academy?

Nie wymagamy żadnego doświadczenia. AI Engineering Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Implementowanie podstawowych funkcji RAG i agenta”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Engineering Academy?

Tak. Każda lekcja AI Engineering Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Projektowanie architektury produkcyjnej
  2. Implementowanie podstawowych funkcji RAG i agenta
  3. Wzmacnianie: bezpieczeństwo, buforowanie i niezawodność
  4. Ewaluacja, wdrożenie i retrospektywa
← Powrót do AI Engineering Academy