Skip to content

About

RAG + tool-use agent from scratch — pgvector hybrid search, cross-encoder reranking, faithfulness eval, MCP server. No LangChain.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

rag-mcp-agent

RAG + tool-use агент, реализованный без фреймворков (без LangChain/LlamaIndex) — hybrid retrieval, реранкинг, tool-calling цикл и eval-харнес написаны напрямую поверх Postgres/pgvector, Groq и Cohere.

Python FastAPI Next.js pgvector Groq Cohere No LangChain

Демо: https://rag-agent-liard-two.vercel.app Стек: Next.js (Vercel) → FastAPI (Render) → Postgres/pgvector (Neon), LLM и реранкинг через Groq/Cohere

Что делает

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

Пример: на вопрос "Сколько времени занимает откат деплоя и сколько будет 12×7?" main-агент вызывает search_documents для первой части и calculate для второй, и собирает оба результата в один ответ.

Архитектура

   вопрос
     │
     ▼
  Guardrail Agent ──BLOCK──▶ вежливый отказ, дальше не идём
     │ ALLOW
     ▼
  Query Rewriter Agent   (сырой вопрос → чистая формулировка)
     │
     ▼
  ┌──────────────────┐
  │    MAIN AGENT     │◀── история tool-вызовов внутри одного запроса
  │ (Groq, gpt-oss-20b)│
  └────────┬──────────┘
           │ модель решает: нужен инструмент?
   ┌───────┴────────┐
   ▼                ▼
search_documents  calculate
   │
┌──┴───┐
▼      ▼
vector  bm25
search  search
   └──┬───┘
      ▼
  union по id → rerank (cross-encoder, top-20→top-4)
      │
      ▼
  ответ + trace (что искал, что нашёл)
      │
      ▼
  Critic Agent ──REVISE──▶ Main Agent пробует снова (макс. 1 раз)
      │ OK
      ▼
  ответ пользователю

Агенты — что делает каждый и зачем он отдельный

Четыре агента, и ни один не дублирует роль другого — это то, что делает систему настоящим multi-agent, а не косметикой ради модного слова.

Агент Файл Что проверяет/делает Почему это отдельный агент, а не часть другого
Guardrail guardrail.py Вход: не является ли вопрос попыткой prompt injection ("забудь инструкции", "покажи системный промпт"). Блокирует до того, как запущены остальные агенты. Безопасность на входе — своя, узкая ответственность. Смешивать с проверкой качества ответа (критик) нельзя: разная цена ошибки — здесь ложное срабатывание блокирует легитимный вопрос, у критика — просто лишняя переделка.
Query Rewriter query_rewriter.py Чистит сырую формулировку вопроса (опечатки, сокращения, двусмысленность) перед тем, как её увидит retrieval. Не трогает вопрос, если переформулировка не нужна (например, арифметику). Retrieval (эмбеддинги + BM25) чувствителен к формулировке — реальный приём в продакшн RAG (query rewriting/expansion), а не выдумка ради количества агентов.
Main Agent agent.py Решает, какой инструмент вызвать (search_documents / calculate), сколько раз, и формулирует ответ. Единственный агент с доступом к tool-calling — вся "работа" происходит тут, остальные три — обвязка вокруг него.
Critic critic.py Выход: сверяет ответ Main Agent с тем, что он реально нашёл (trace). Если ответ не обоснован найденным — просит Main Agent переделать (1 попытка), с конкретной причиной. Проверка ПОСЛЕ генерации — тот же принцип, что офлайн-судья в eval.py, только встроен в живой запрос, а не считается отдельно на golden set.

orchestrator.py — не агент, а пайплайн, который вызывает всех четырёх по порядку и передаёт результат от одного к другому.

Честная цена: до 5 вызовов LLM на один вопрос в худшем случае (guardrail + rewriter + agent + critic + 1 повтор). Для демо это избыточно; для системы, которая не должна врать и не должна вестись на инъекции, — осознанный trade-off, не бесплатная фича. /agent/ask-raw в API отдаёт путь без критика — для сравнения задержки и честного разговора про эту цену.

Ключевые решения

  • pgvector вместо отдельной vector DB. Postgres с HNSW-индексом достаточен вплоть до миллионов векторов; отдельная инфраструктура не нужна для этого масштаба.
  • Hybrid search (vector + BM25). Эмбеддинги ловят смысл, но плохо ловят точные токены — версии, коды, имена. BM25 закрывает этот случай. Кандидаты из обоих источников объединяются и фильтруются реранкингом.
  • Двухэтапный retrieval (bi-encoder → cross-encoder). Bi-encoder кодирует вопрос и документ независимо — быстро, подходит для recall по всей базе. Cross-encoder читает пару вместе — точнее, но дорого считать на всей базе, поэтому применяется только к уже отобранным top-20 кандидатам.
  • RAG как инструмент агента, а не отдельный pipeline. rag.py — фиксированный retrieve→generate; agent.py — модель сама решает, вызывать ли search_documents, calculate, оба или ни одного.
  • Калькулятор через ast, не eval(). LLM генерирует выражение как строку; прямой eval — инъекция. ast-парсер разрешает только арифметику.
  • Два бэкенда эмбеддингов/реранкинга (local / Cohere), переключаемые USE_COHERE. Self-hosted sentence-transformers держат в памяти ~1.3GB (обе модели) — не помещается в 512MB на Render free/starter tier. Cohere Embed+Rerank API переносит инференс наружу, локальный процесс почти ничего не занимает. Остальной код (retrieval.py, ingest.py) не знает, какой бэкенд активен.
  • Четыре агента с непересекающимися ролями (guardrail/rewriter/main/critic), а не один агент с большим промптом. У каждого своя, узкая ответственность и своя цена ошибки (см. таблицу "Агенты" выше) — это позволяет менять/тестировать каждого независимо и явно называть, за что отвечает конкретно этот вызов LLM.
  • Память между сообщениями — hard truncation, не суммаризация. Фронт шлёт всю историю чата на каждый запрос, memory.py обрезает её до последних 10 сообщений перед тем, как она попадёт в промпт. Простая обрезка теряет старый контекст безвозвратно — осознанный выбор для этого масштаба, не недосмотр (суммаризация вместо отбрасывания — следующий шаг, см. roadmap).

Известные ограничения

  • Eval-набор маленький (4 вопроса) — показывает метод, не даёт статистической значимости.
  • LLM-судья в eval неидеален: на прогоне против Neon судья иногда засчитывает FAIL честному отказу ("не знаю") на out-of-scope вопрос, хотя собственный промпт судьи требует засчитывать это как PASS — сам судья требует отдельной валидации.
  • Без авторизации — API открыт всем, кто знает URL.

Структура

rag-agent/
├── data/*.txt              # документы для индексации
├── db/schema.sql            # справочная структура таблицы chunks (реально создаётся динамически, db.py)
├── render.yaml               # Render Blueprint
├── web/                        # Next.js чат-фронт (отдельный деплой на Vercel)
├── src/
│   ├── config.py         # параметры, переключатель local/Cohere бэкенда
│   ├── db.py              # слой поверх psycopg, без ORM
│   ├── chunking.py        # текст -> чанки с overlap
│   ├── embeddings.py      # эмбеддинги/реранкинг: local (sentence-transformers) или Cohere API
│   ├── ingest.py           # data/*.txt -> чанки -> эмбеддинги -> Postgres
│   ├── retrieval.py        # hybrid search + rerank
│   ├── rag.py               # retrieve + generate, без агента
│   ├── tools.py              # search_documents, calculate
│   ├── agent.py               # tool-use цикл поверх Groq (main agent)
│   ├── guardrail.py            # агент: проверка входа на prompt injection
│   ├── query_rewriter.py        # агент: чистит формулировку вопроса перед retrieval
│   ├── critic.py                 # агент: проверяет ответ на обоснованность (reflection)
│   ├── memory.py                  # обрезка истории диалога под контекстное окно
│   ├── orchestrator.py             # пайплайн: guardrail → rewriter → agent(+history) → critic
│   ├── eval.py                     # retrieval hit rate + faithfulness (LLM-judge)
│   ├── api.py                       # FastAPI: /agent/ask (с критиком), /agent/ask-raw, /rag/ask
│   └── mcp_server.py                 # тот же агент как MCP-сервер (stdio)
├── requirements.txt         # база, без torch (то, что ставится на Render)
├── requirements-local.txt    # + sentence-transformers, для self-hosted режима
└── requirements-mcp.txt       # + mcp SDK, для запуска mcp_server.py

Локальный запуск

cp .env.example .env   # заполнить GROQ_API_KEY; USE_COHERE=false по умолчанию

python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements-local.txt

python -m src.ingest              # индексация data/*.txt

python -m src.rag                 # RAG без агента
python -m src.agent               # агент с инструментами
python -m src.eval                # eval-метрики

uvicorn src.api:app --reload      # HTTP API

MCP-сервер

Тот же агент доступен не только по HTTP, но и как MCP-сервер — src/mcp_server.py тонкой обвязкой выставляет ask_agent, ask_agent_fast, search_documents и rag_answer как MCP tools, дёргая ровно те же функции, что и api.py (никакой отдельной логики, два транспорта поверх одного движка).

pip install -r requirements-mcp.txt
claude mcp add --transport stdio rag-agent -- python -m src.mcp_server

После этого любой MCP-клиент (Claude Code, Claude Desktop, Cursor) может спрашивать базу знаний этого проекта напрямую, тем же самым пайплайном (guardrail → query rewriter → main agent → critic), что работает на проде.

Деплой

Прод использует Cohere вместо self-hosted моделей и Neon вместо локального Postgres.

  1. Neon — Postgres с pgvector, провизионится через Vercel Marketplace (vercel integration add neon) или напрямую на neon.tech.
  2. Render — подключить GitHub-репозиторий, он подхватит render.yaml. Секреты задаются в дашборде: GROQ_API_KEY, COHERE_API_KEY, DATABASE_URL (из Neon), ALLOWED_ORIGIN (домен фронта).
  3. python -m src.ingest один раз против DATABASE_URL от Neon — наполняет базу.
  4. Vercel — web/ деплоится отдельным проектом, env RAG_AGENT_API_URL указывает на Render-сервис.

Метрики

Прогон eval.py против прод-базы: retrieval hit rate 100%, faithfulness 75% (4 вопроса; единственный "провал" — ложное срабатывание судьи, см. "Известные ограничения").

Roadmap

  • Суммаризация старой истории вместо hard truncation в memory.py
  • Golden set 30–50 вопросов вместо 4
  • Streaming ответа агента
  • Авторизация API
  • Multi-tenant (tenant_id, изоляция документов между организациями)

About

RAG + tool-use agent from scratch — pgvector hybrid search, cross-encoder reranking, faithfulness eval, MCP server. No LangChain.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages