Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ data/processed/*.jsonl
*.swp
.DS_Store
Thumbs.db
data/raw/pdf/
data/index/
data/raw/pdf/
data/index/
70 changes: 70 additions & 0 deletions IMPROVEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# IMPROVEMENTS — направления улучшений

Документ фиксирует улучшения RAG-конвейера: **реализованные** и **запланированные**.

---

## Реализовано: относительный критерий отказа

### Проблема

В исходном MVP решение «ответить или отказать» принималось по абсолютному порогу: если score лучшего чанка `>= MIN_SCORE` — отвечаем, иначе отказ. На банковских жалобах образца этого хватало, но на учебнике порог перестал разделять классы.

Причина — в природе данных. Учебник разбит на ~150 документов, нарезанных на ~1300 коротких чанков. В методическом тексте часто встречаются общие слова («как», «урок», «учитель», «приготовить»). Поэтому почти **любой** короткий вопрос со словом «как» получает ненулевой score, даже если по смыслу ответа в учебнике нет.

Замеры на реальном TF-IDF подтвердили перекрытие классов:

| Вопрос | top-1 score | Ожидание |
|--------|:-----------:|----------|
| Как ввести новую лексику на уроке? | 0.248 | ответ |
| Как приготовить борщ? | 0.217 | **отказ** |
| Как поменять масло в двигателе? | 0.217 | **отказ** |

Видно: «борщ» и «масло» (мусорные вопросы) набирают столько же, сколько хороший вопрос. Поднять порог нельзя — отсечём и валидные вопросы. Один абсолютный порог здесь не работает.

### Решение

Добавлен **относительный критерий**: вопрос считается релевантным, только если лучший фрагмент одновременно

1. набрал `score >= MIN_SCORE` (абсолютный минимум, отсекает совсем слабые совпадения), и
2. **заметно выделяется** на фоне среднего по top-k: `top1 - mean(top_k) >= MIN_GAP`.

Идея: у настоящего ответа один фрагмент явно релевантнее остальных (большой отрыв). У шумового вопроса все найденные чанки имеют близкий низкий score — отрыва нет, значит совпадение случайное.

Реализация — `app/generator.py`, функция `is_relevant()`; пороги вынесены в `app/prompts.py` (`MIN_SCORE = 0.18`, `MIN_GAP = 0.03`).

```python
def is_relevant(hits):
if not hits:
return False
scores = [h["score"] for h in hits]
top1 = max(scores)
mean_k = sum(scores) / len(scores)
return top1 >= MIN_SCORE and (top1 - mean_k) >= MIN_GAP
```

### Результат

На наборе demo + negative критерий разделяет классы корректно: три demo-вопроса отвечают, мусорные («борщ», «масло в двигателе», «квадратные уравнения») и тематически чужие («Столица Австралии») — отказывают. Показательный пример: «Что делать в начале урока?» имеет высокий top-1 (0.285), но малый отрыв (gap 0.026) → отказ; это и есть случай, который абсолютный порог пропустил бы.

Поведение зафиксировано тестами в `tests/test_refusal.py` (5 кейсов).

### Честное ограничение

Относительный критерий — улучшение, но не панацея. На TF-IDF идеального разделения не достичь в принципе: метод сравнивает пересечение слов, а не смысл. Отдельные вопросы с реальным лексическим пересечением всё ещё могут проскочить. Полное решение — переход к семантическому поиску (ниже).

---

## Запланировано

### 1. Семантический поиск (embeddings)

Заменить TF-IDF на векторные представления (sentence-transformers, многоязычная модель для русского). Это снимет корневое ограничение: поиск пойдёт по смыслу, синонимы и перефразировки начнут находиться, а отделение релевантных вопросов от шума станет надёжнее. Pipeline остаётся тем же — меняется только слой retrieval.

### 2. Несколько учебников в корпусе

Скрипт `pdf_to_datasets.py` уже принимает несколько PDF. Логичное расширение — собрать корпус из учебника, рабочей тетради и книги для учителя, чтобы покрыть и методику, и предметный контент для ребёнка.

### 3. Гибридная выдача

Комбинировать TF-IDF/embeddings с reranking, чтобы поднимать наиболее релевантный фрагмент в топ и усиливать сигнал отрыва для критерия отказа.
221 changes: 127 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,127 @@
# RAG Tutorial

Учебный RAG на текстовых описаниях: TF-IDF + demo-ответ с источниками.
Pipeline: данные → чанки → индекс → поиск → ответ.

**Документы разработки:** [doc/tasklist.md](doc/tasklist.md) · **Данные:** [doc/DATA.md](doc/DATA.md) · **Домашнее задание:** [homework/README.md](homework/README.md) · **Слайды:** [Seminar_Big_data.pdf](Seminar_Big_data.pdf) · **Kaggle:** [Consumer Complaint Database](https://www.kaggle.com/datasets/datasnaek/consumer-complaint-database)

## Требования

- Python 3.10+
- [uv](https://docs.astral.sh/uv/)

## Быстрый старт

```bash
# 1. Окружение
uv venv
uv sync

# 2. Сборка индекса (ingest + chunk + TF-IDF)
uv run python scripts/build_index.py

# 3. Запуск UI
uv run streamlit run app/main.py
```

Откройте в браузере: http://localhost:8501

## Demo-вопросы

В sidebar приложения или в поле ввода:

| Вопрос | Ожидание |
|--------|----------|
| **Ипотека - закрытие ипотечной сделки** | ответ, doc_id=2, score > 0.4 |
| Какие переменные в датасете про безработицу? | отказ (нет таких данных) |
| За какой период данные об инфляции? | отказ |
| Как приготовить борщ? | отказ |

Другие рабочие запросы: `студенческий кредит`, `Capital One`, `Wells Fargo закрытие счёта`.

## Проверка из консоли

```bash
# Тесты
uv run pytest tests/ -v

# Поиск (итерация 5)
uv run python scripts/check_retrieval.py

# Demo-ответ (итерация 6)
uv run python scripts/check_generator.py
```

## Структура проекта

```
rag-tutorial/
├── app/
│ ├── config.py # пути, top_k, размер чанка
│ ├── chunker.py # нарезка текста
│ ├── retriever.py # TF-IDF + cosine top-k
│ ├── generator.py # demo-ответ
│ ├── prompts.py # правила и отказы
│ └── main.py # Streamlit UI
├── scripts/
│ ├── ingest.py
│ ├── build_index.py
│ ├── check_retrieval.py
│ └── check_generator.py
├── data/
│ ├── raw/datasets.json
│ ├── processed/ # documents.jsonl, chunks.jsonl (генерируются)
│ └── index/ # vectorizer.pkl, matrix.npz (генерируются)
├── tests/
└── doc/
```

## Пересборка индекса

После изменения `data/raw/datasets.json`:

```bash
uv run python scripts/build_index.py
```

## Ограничения MVP

- Поиск по **словам** (TF-IDF), не по смыслу — синонимы могут не находиться.
- Demo-режим: ответ из найденных чанков, без внешней LLM.
- Индексируется только текст описаний, CSV не используется.

## Контакты
Подписывайтесь на канал: @Marat_notes
# RAG Tutorial — Умняш

Учебный RAG на материалах школьного учебника: TF-IDF + demo-ответ с источниками.
Pipeline: PDF учебника → документы → чанки → индекс → поиск → ответ.

Прототип контентного ядра для **«Умняша»** — AI-помощника по домашним заданиям для младшей школы. Идея: помощник отвечает **строго по методическим материалам учебника**, а не из «головы» модели, и всегда показывает источник.

**Документы:** [doc/DATA.md](doc/DATA.md) — данные · [doc/00_project_idea.md](doc/00_project_idea.md) — идея · [IMPROVEMENTS.md](IMPROVEMENTS.md) — улучшения · [homework/SUBMISSION.md](homework/SUBMISSION.md) — сдача ДЗ

## Требования

- Python 3.10+
- [uv](https://docs.astral.sh/uv/)

## Быстрый старт

```bash
# 1. Окружение
uv venv
uv sync

# 2. (опционально) Пересборка датасета из PDF учебника
# PDF не входит в репозиторий — положить локально в data/raw/pdf/
uv run python scripts/pdf_to_datasets.py data/raw/pdf/Spotlight_2_TB.pdf

# 3. Сборка индекса (ingest + chunk + TF-IDF)
uv run python scripts/build_index.py

# 4. Запуск UI
uv run streamlit run app/main.py
```

Откройте в браузере: http://localhost:8501

> Готовый `data/raw/datasets.json` уже в репозитории, поэтому шаг 2 можно пропустить и сразу собирать индекс.

## Данные

Источник — книга для учителя УМК **«Английский в фокусе» (Spotlight 2)**: планы уроков, лексика, фонетика, методические указания по пяти модулям. Из PDF извлекается текст, сегментируется по урокам и разделам, очищается от артефактов извлечения.

- 154 документа в `datasets.json`
- ~1317 чанков после нарезки

Сам PDF учебника **не включён в репозиторий** (авторское право); хранится только производный обработанный текст. Подробно — в [doc/DATA.md](doc/DATA.md).

## Demo-вопросы

В sidebar приложения или в поле ввода:

| Вопрос | Ожидание |
|--------|----------|
| **Что такое УМК Английский в фокусе?** | ответ + источник |
| **Как работать с диалогом на уроке?** | ответ + источник |
| **Как используются картинки на уроке?** | ответ + источник |
| Столица Австралии | отказ (нет таких данных в учебнике) |

## Скриншоты

**Ответ с источниками:**

![Ответ с источниками](doc/screenshots/answer.png)

**Отказ на вопрос не по теме:**

![Отказ](doc/screenshots/refusal.png)

> Положите скриншоты в `doc/screenshots/` под именами `answer.png` и `refusal.png`.

## Проверка из консоли

```bash
# Тесты (16 шт.: chunking, retrieval, отказ)
uv run pytest tests/ -v
```

## Структура проекта

```
rag-tutorial/
├── app/
│ ├── config.py # пути, top_k, размер чанка
│ ├── chunker.py # нарезка текста на чанки
│ ├── retriever.py # TF-IDF + cosine top-k
│ ├── generator.py # demo-ответ + относительный критерий отказа
│ ├── prompts.py # правила, отказы, пороги (MIN_SCORE, MIN_GAP)
│ └── main.py # Streamlit UI
├── scripts/
│ ├── pdf_to_datasets.py # PDF учебника → datasets.json
│ ├── ingest.py
│ └── build_index.py
├── data/
│ ├── raw/datasets.json # подготовленный корпус (коммитится)
│ ├── raw/pdf/ # PDF учебника (НЕ коммитится)
│ ├── processed/ # documents.jsonl, chunks.jsonl (генерируются)
│ └── index/ # vectorizer.pkl, matrix.npz (генерируются)
├── tests/
│ ├── test_chunking.py
│ ├── test_retrieval.py
│ └── test_refusal.py # тесты относительного критерия отказа
├── doc/
└── IMPROVEMENTS.md
```

## Пересборка индекса

После изменения `data/raw/datasets.json`:

```bash
uv run python scripts/build_index.py
```

Чтобы пересобрать датасет из другого/дополнительного PDF (скрипт принимает несколько файлов):

```bash
uv run python scripts/pdf_to_datasets.py data/raw/pdf/file1.pdf data/raw/pdf/file2.pdf
uv run python scripts/build_index.py
```

## Реализованное улучшение

**Относительный критерий отказа.** Вместо одного абсолютного порога score решение «ответить или отказать» учитывает и отрыв лучшего фрагмента от среднего по top-k. На длинном методическом тексте частые слова дают ненулевой score почти на любой вопрос, поэтому абсолютного порога недостаточно. Подробно — в [IMPROVEMENTS.md](IMPROVEMENTS.md).

## Ограничения MVP

- Поиск по **словам** (TF-IDF), не по смыслу — синонимы могут не находиться.
- Demo-режим: ответ из найденных чанков, без внешней LLM.
- Индексируется только извлечённый текст учебника.
Loading