RAG + tool-use агент, реализованный без фреймворков (без LangChain/LlamaIndex) — hybrid retrieval, реранкинг, tool-calling цикл и eval-харнес написаны напрямую поверх Postgres/pgvector, Groq и Cohere.
Демо: 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Тот же агент доступен не только по 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.
- Neon — Postgres с pgvector, провизионится через Vercel Marketplace
(
vercel integration add neon) или напрямую на neon.tech. - Render — подключить GitHub-репозиторий, он подхватит
render.yaml. Секреты задаются в дашборде:GROQ_API_KEY,COHERE_API_KEY,DATABASE_URL(из Neon),ALLOWED_ORIGIN(домен фронта). python -m src.ingestодин раз противDATABASE_URLот Neon — наполняет базу.- Vercel —
web/деплоится отдельным проектом, envRAG_AGENT_API_URLуказывает на Render-сервис.
Прогон eval.py против прод-базы: retrieval hit rate 100%, faithfulness 75%
(4 вопроса; единственный "провал" — ложное срабатывание судьи, см. "Известные ограничения").
- Суммаризация старой истории вместо hard truncation в
memory.py - Golden set 30–50 вопросов вместо 4
- Streaming ответа агента
- Авторизация API
- Multi-tenant (
tenant_id, изоляция документов между организациями)