Запрос → эмбеддинг → top-k: мини-retrieval

Собираем рабочий retrieval-шаг целиком: от текста запроса до списка релевантных чанков.

Retrieval (извлечение) — на запрос пользователя находим top-k наиболее близких по смыслу чанков, чтобы подать их в промпт.

Три действия на каждый запрос

  1. Эмбеддим вопрос той же моделью, что и документы.
  2. Считаем близость вектора вопроса со всеми векторами чанков.
  3. Берём top-k самых близких — это и есть контекст.

Запускаемый мини-retriever

Сделаем игрушечный, но честный retriever без библиотек. «Эмбеддинг» — мешок слов (bag-of-words): вектор по словарю, где число = сколько раз слово встретилось. Метрика — косинус. Это упрощение реальных эмбеддингов, но весь поток retrieval тут настоящий.

import math, re
from collections import Counter

DOCS = [
    "Питон — интерпретируемый язык программирования с динамической типизацией.",
    "Списки в питоне изменяемы, а кортежи неизменяемы.",
    "Кошки спят до шестнадцати часов в сутки.",
    "Для веб-разработки на питоне популярны Django и Flask.",
]

def tokenize(t):
    return re.findall(r"[а-яёa-z]+", t.lower())

vocab = sorted({w for d in DOCS for w in tokenize(d)})
idx = {w: i for i, w in enumerate(vocab)}

def embed(text):
    v = [0.0] * len(vocab)
    for w, c in Counter(tokenize(text)).items():
        if w in idx:
            v[idx[w]] = c
    return v

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a)) or 1e-9
    nb = math.sqrt(sum(x * x for x in b)) or 1e-9
    return dot / (na * nb)

doc_vecs = [embed(d) for d in DOCS]

def retrieve(query, k=2):
    qv = embed(query)
    scored = sorted(
        ((cosine(qv, dv), d) for dv, d in zip(doc_vecs, DOCS)),
        reverse=True,
    )
    return scored[:k]

query = "какие фреймворки для веб на питоне"
print("Запрос:", query)
for score, d in retrieve(query, k=2):
    print(f"  {score:.3f}  {d}")

Вывод:

Запрос: какие фреймворки для веб на питоне
  0.667  Для веб-разработки на питоне популярны Django и Flask.
  0.189  Списки в питоне изменяемы, а кортежи неизменяемы.

Retriever без всякого ИИ нашёл правильный документ первым: у запроса и нужного чанка много общих слов («веб», «питоне»). Второй результат притянулся словом «питоне» — он менее релевантен, и его близость заметно ниже. В настоящем RAG bag-of-words заменяют на эмбеддинги, и тогда найдётся даже документ без общих слов, но про то же по смыслу.

Параметр k

k — сколько чанков забрать. Мало (k=1–2) — рискуем не захватить нужный факт. Много (k=10+) — в промпт попадёт шум, ответ размоется, и вырастет цена. Типичный старт — k от 3 до 5, дальше подбор.

Итог

  • Retrieval: эмбеддинг запроса → близость со всеми → top-k.
  • Запрос эмбеддится той же моделью, что и документы.
  • k балансирует полноту контекста и шум/цену; старт — 3–5.
Проверьте себя
1. Что возвращает retrieval-шаг в RAG?
AГотовый ответ пользователю
Btop-k наиболее близких по смыслу чанков для контекста
CОбновлённую модель
DСписок всех документов базы
2. Чем рискует слишком большой k при поиске?
AНичем, всегда лучше больше
BВ промпт попадёт шум, ответ размоется и вырастет цена
CПоиск перестанет работать
DУменьшится размерность векторов
3. Почему вопрос нужно эмбеддить той же моделью, что и документы?
AЧтобы быстрее
BЧтобы векторы вопроса и чанков были в одном пространстве и сравнимы
CЭто требование лицензии
DИначе вырастет k
Поддержать проект