0Pricing
AI Engineering Academy · Урок

Реализация основных возможностей RAG и агента

Создайте конвейер загрузки документов, индексацию в векторном хранилище, поиск с повторным ранжированием и интеграцию инструментов агента, применяя изученные в курсе подходы.

«Реализация основных возможностей RAG и агента» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 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
]

Настройка векторного хранилища

Создайте таблицу pgvector с правильной схемой до загрузки документов. Включите столбцы для вектора, текста фрагмента, всех полей метаданных и временной метки 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 для разных типов файлов и рекурсивное разбиение текста по символам на фрагменты. Объединяйте вызовы создания векторных представлений в пакеты, чтобы не превышать ограничение в 2048 токенов на вход и сократить количество вызовов 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

Измерение базового качества извлечения

Прежде чем что-либо оптимизировать, установите базовый уровень извлечения. Используйте свой набор тестовых данных для оценки доли попаданий (появляется ли правильный документ в первых пяти результатах?) и 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 обеспечивает полную видимость конвейера. Далее мы усилим систему с помощью методов защиты, кэширования и повышения надежности.

Часто задаваемые вопросы

Урок «Реализация основных возможностей RAG и агента» бесплатный?

Да — полный текст урока «Реализация основных возможностей RAG и агента» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.

Чему я научусь в уроке «Реализация основных возможностей RAG и агента»?

Создайте конвейер загрузки документов, индексацию в векторном хранилище, поиск с повторным ранжированием и интеграцию инструментов агента, применяя изученные в курсе подходы. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 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