惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
Security Archives - TechRepublic
Security Archives - TechRepublic
V
Vulnerabilities – Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Proofpoint News Feed
G
GRAHAM CLULEY
P
Privacy International News Feed
The Hacker News
The Hacker News
Forbes - Security
Forbes - Security
U
Unit 42
N
News and Events Feed by Topic
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
A
About on SuperTechFans
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
D
Docker
I
Intezer
Spread Privacy
Spread Privacy
The Last Watchdog
The Last Watchdog
V2EX - 技术
V2EX - 技术
S
Security @ Cisco Blogs
F
Full Disclosure
S
Secure Thoughts
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
W
WeLiveSecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Project Zero
Project Zero
Recorded Future
Recorded Future
Cyberwarzone
Cyberwarzone
S
Security Affairs
AWS News Blog
AWS News Blog
H
Help Net Security
The GitHub Blog
The GitHub Blog
Hacker News: Ask HN
Hacker News: Ask HN
Vercel News
Vercel News
P
Proofpoint News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Register - Security
The Register - Security
S
Schneier on Security
F
Fortinet All Blogs
C
CERT Recently Published Vulnerability Notes
L
LINUX DO - 最新话题
T
Tor Project blog
T
The Exploit Database - CXSecurity.com
MongoDB | Blog
MongoDB | Blog
Webroot Blog
Webroot Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Где заканчивается вызов LLM и начинается backend система: локальный RAG на FastAPI и Ollama
Андрей · 2026-06-23 · via Все публикации подряд на Хабре

На практике хотел понять где заканчивается простой вызов локальной LLM и начинается backend система: с API контрактом, логированием, request_id, источниками, индексом документов, диагностикой и честными ограничениями.

Сначала проект выглядел просто: frontend отправляет вопрос, FastAPI принимает POST /ask, backend вызывает локальную модель через Ollama и возвращает ответ. Это уже работало, но стало понятно такой вариант ещё нельзя назвать системой по документации. Модель отвечает, но непонятно на что она опирается, откуда взяла ответ, сколько времени занял каждый этап и что делать если документы изменились.

В статье описисоваю не "как вообще устроен RAG" а как постепенно превращал простой вызов локальной LLM в небольшой backend/RAG проект. Без претензии на production ready, но с инженерными акцентами: API, наблюдаемость, источники, индекс, ошибки и ограничения.

1. Зачем делал локального AI помощника

Идея проекта была простой, сделать локального помощника по документации. Документы лежат в папке documents/, пользователь задаёт вопрос, backend ищет релевантные фрагменты и передаёт их в локальную LLM через Ollama.

Стек получился таким:

  • frontend на HTML/CSS/JS;

  • backend на FastAPI;

  • Ollama для локального запуска моделей;

  • gpt-oss:20b для генерации ответа;

  • embeddinggemma для embeddings;

  • Markdown/TXT документы в локальной папке;

  • in memory vector store для первого варианта RAG.

Начал с backend: явного API, валидации запроса, request_id, логирования и измерения времени. Frontend нужен чтобы удобно отправлять вопросы и смотреть результат, но основная инженерная часть находится на стороне backend.

Первая схема была такой:

Пользователь задаёт вопрос
          |
          v
Frontend отправляет POST /ask
          |
          v
FastAPI принимает запрос
          |
          v
Создаётся request_id
          |
          v
Запрос логируется
          |
          v
Backend вызывает локальную LLM через Ollama
          |
          v
Ответ возвращается во frontend
          |
          v
Результат логируется

На этом этапе помощник уже отвечал, но по сути это всё ещё был обычный "чат с моделью". Документы в процессе не участвовали.

2. Почему простой вызов LLM это ещё не система

Простой вызов LLM выглядит заманчиво отправил prompt, получил answer. Но для помощника по документации этого недостаточно.

Проблемы быстро становятся очевидными:

  • модель не знает локальные документы, если не передать их в контекст;

  • ответ нельзя проверить по источникам;

  • непонятно, что попало в prompt;

  • сложно связать ответ пользователя с логами backend;

  • если ответ медленный, непонятно где именно тратится время;

  • если документ изменился, непонятно обновился ли индекс;

  • если модели нет ответа, она всё равно может попытаться ответить.

Поэтому решил двигаться не сразу к "полноценному RAG" а небольшими шагами. Сначала документы -> потом чанки -> потом embeddings -> потом vector store -> потом поиск -> потом prompt с найденным контекстом -> потом sources -> потом диагностика и тесты.

Это оказалось полезнее чем сразу прикручивать всё в /ask. Когда каждый слой можно проверить отдельным endpoint, проще искать ошибки.

3. Архитектура проекта

В итоге проект стал состоять из нескольких слоёв:

Frontend
    |
    v
FastAPI backend
    |
    v
Document loader
    |
    v
Chunking layer
    |
    v
Embedding service
    |
    v
In memory vector store
    |
    v
Retriever
    |
    v
Prompt assembly
    |
    v
Ollama generation
    |
    v
Answer + sources + timings + request_id

Документы лежат в папке documents/. Поддерживаются .md и .txt. При rebuild index backend перечитывает документы, режет их на чанки, считает embeddings и кладёт результат в in memory vector store.

Основные endpoint:

POST /ask
GET  /documents
GET  /documents/chunks
GET  /documents/embeddings
POST /documents/index/rebuild
GET  /documents/index/status
POST /documents/search
POST /rag/debug

/ask главный endpoint для пользователя. Остальные endpoint появились не для красоты а для диагностики. Было важно видеть, что backend действительно читает документы, режет их на чанки, строит embeddings и ищет похожие фрагменты.

4. Как backend обрабатывает /ask

У /ask задача не просто "передать вопрос в модель", он управляет всем pipeline.

Упрощённо обработка выглядит так:

принять вопрос
      |
      v
создать request_id
      |
      v
записать начало запроса в лог
      |
      v
найти релевантные чанки
      |
      v
отфильтровать sources по score
      |
      v
проверить exact terms
      |
      v
собрать prompt
      |
      v
вызвать Ollama
      |
      v
вернуть answer, sources, timings, request_id
      |
      v
записать результат в лог

Для ответа использую явную модель. В статье не буду приводить весь код проекта, но смысл контракта такой:

class Source(BaseModel):
    document: str
    path: str
    chunk_index: int
    score: float
    chunk: str


class TimingBreakdown(BaseModel):
    retrieval_ms: int = 0
    filtering_ms: int = 0
    prompt_ms: int = 0
    generation_ms: int = 0
    total_ms: int = 0


class AskResponse(BaseModel):
    request_id: str
    answer: str
    sources: list[Source]
    duration_ms: int
    timings: TimingBreakdown

Мне было важно чтобы ответ API был не просто строкой answer нужен пользователю а sources, score, chunk_index, duration_ms и request_id нужны для проверки и диагностики.

Пример ответа:

{
  "request_id": "8b7a8be9-4bfd-4b89-ac9c-5a26f01de229",
  "answer": "Rebuild index нужно выполнять после добавления, изменения или удаления документов...",
  "duration_ms": 60440,
  "timings": {
    "retrieval_ms": 2163,
    "filtering_ms": 0,
    "prompt_ms": 0,
    "generation_ms": 58276,
    "total_ms": 60440
  },
  "sources": [
    {
      "document": "operations.md",
      "path": "documents/operations.md",
      "chunk_index": 5,
      "score": 0.6547,
      "chunk": "...Пятый случай смена embedding модели..."
    }
  ]
}

Такой JSON уже похож не на игрушечный чат а на backend API: у него есть контракт, диагностика и связь с логами.

5. Как появился RAG: документы -> чанки -> embeddings -> vector store

Не стал сразу менять /ask. Сначала добавил папку documents/, загрузку .md и .txt, потом отдельный endpoint который показывает какие документы backend видит.

Следующим шагом появилась нарезка документов на чанки. Это ещё не RAG, но уже подготовка корпуса. Документ целиком редко удобно передавать в prompt, лучше работать с небольшими фрагментами.

После чанкинга добавил embeddings через Ollama и модель embeddinggemma, потом появился простой in memory vector store, список чанков с embeddings и метаданными.

На этом этапе схема стала такой:

documents/
    |
    v
document_loader
    |
    v
document_chunker
    |
    v
embedding service
    |
    v
in memory vector store
    |
    v
search_similar_chunks()

Поиск похожих фрагментов делается через cosine similarity: embedding вопроса сравнивается с embeddings чанков, после чего выбираются top-k результатов.

Важно что на каждом этапе был отдельный способ проверки:

  • /documents backend видит файлы;

  • /documents/chunks документы режутся на чанки;

  • /documents/embeddings embeddings создаются;

  • /documents/search semantic search возвращает похожие chunks;

  • /documents/index/status видно состояние индекса.

Так проект проще отлаживать. Если /ask ответил плохо, то можно проверить не только prompt, но и весь путь до него.

6. Sources, score и request_id

sources стали одним из главных отличий RAG от обычного вызова LLM.

Без sources пользователь видит только сгенерированный текст, с sources он может проверить на каких фрагментах был построен ответ. Для backend разработчика sources ещё важнее, они показывают качество retrieval.

Если вопрос был:

"Когда нужно выполнять rebuild index?"

то хорошим источником должен быть operations.md, а не случайный обзорный документ. В тестах так и получилось: главным source стал operations.md, chunk_index: 5, score: 0.6547.

Кроме sources сразу добавил request_id. Это маленькая деталь, но в отладке она быстро окупилась. Он создаётся на каждый запрос, возвращается во frontend и пишется в логи. Если пользователь говорит "ответ был странный" можно взять request_id и найти всю цепочку в логах.

Пример логической цепочки:

request_started
rag_context_filtered
request_finished

С request_id можно связать вопрос, найденные chunks, scores, timings и ошибку если она была. Даже в локальном проекте это сильно упрощает диагностику.

7. Manual rebuild index и stale index

Когда появился vector store стала заметна новая сущность индекс документов как отдельное состояние системы.

Документ можно изменить на диске, но in memory vector store сам об этом не узнает. Embeddings уже посчитаны по старому тексту поэтому добавил ручной rebuild index из frontend.

Сценарий такой:

добавил или изменил документ
          |
          v
нажал rebuild index
          |
          v
backend перечитал documents/
          |
          v
создал новые chunks
          |
          v
пересчитал embeddings
          |
          v
заменил in memory vector store

Для проверки добавил три документа разного размера:

  1. overview.md короткий обзор проекта.

  2. architecture.md средний документ про архитектуру и поток /ask.

  3. operations.md большой документ про эксплуатацию, rebuild index, request_id, duration_ms, логи, ошибки и диагностику RAG.

После rebuild получилось:

Документов: 3
Чанков: 63
Embedding model: embeddinggemma
Embedding dimensions: 768

Здесь хорошо видно, что chunks_count зависит не от количества файлов а от объёма текста и настроек нарезки.

Потом проверил stale index, в конец operations.md добавил контрольную строку:

CONTROL_EXACT_TERM_12345

Если не делать rebuild а новый текст уже есть на диске, но его ещё нет в vector store, то RAG не читает документы магически на каждый вопрос, он работает с тем состоянием индекса которое было построено.

После rebuild вопрос про CONTROL_EXACT_TERM_12345 начал находить operations.md, поздний chunk_index и вызывать generation. Это подтвердило что индекс действительно обновился.

8. Timings: где реально тратится время

Сначала возвращал только общий duration_ms, потом стало понятно что этого мало. Если запрос занимает 60 секунд важно знать где именно тратится время.

Добавил breakdown:

timings = TimingBreakdown()

retrieval_started_at = perf_counter()
search_results = search_similar_chunks(
    question=request.question,
    top_k=RAG_TOP_K,
)
timings.retrieval_ms = int((perf_counter() - retrieval_started_at) * 1000)

# filtering_ms, prompt_ms, generation_ms считаются отдельно

timings.total_ms = int((perf_counter() - started_at) * 1000)

Результаты тестов показали интересную картину. Retrieval обычно занимал около 2.1-2.2 секунды. Основное время уходило на generation.

Несколько примеров из локального запуска:

Вопрос

Total

Retrieval

Generation

Вывод

Когда нужно выполнять rebuild index?

60440 мс

2163 мс

58276 мс

Основное время в генерации

Опиши поток обработки /ask

74810 мс

2184 мс

72624 мс

Длинный ответ, долгая генерация

Можно ли использовать проект в production?

37499 мс

2173 мс

35325 мс

Ответ короче, быстрее

Какая лицензия у проекта?

2179 мс

2179 мс

0 мс

LLM не вызывалась

Самый важный результат negative test по лицензии. В документах нет информации о лицензии поэтому backend не стал вызывать LLM. generation_ms = 0.

Думаю это хороший подход: если retrieval не нашёл достаточный контекст, то не надо надеяться что prompt заставит модель быть честной. Лучше вообще не вызывать generation.

Проект запускался с такими настройками:

OLLAMA_OPTIONS = {
    "num_ctx": 32768,
    "num_gpu": 20,
    "num_thread": 8,
    "num_batch": 512,
    "temperature": 0.2,
    "top_p": 0.9,
    "num_predict": 1024,
}

RAG_TOP_K = 5
MAX_CONTEXT_CHARS = 4000

Эти параметры важны для интерпретации timings: num_ctx, MAX_CONTEXT_CHARS и num_predict влияют на размер контекста и потенциальную длину ответа, а RAG_TOP_K на количество найденных фрагментов которые backend рассматривает перед сборкой prompt.

Проект проверял на конфигурации:

Компонент

Значение

Ноутбук

ASUS Vivobook S 16 M3607HA

CPU

AMD Ryzen 7 260, 8 ядер / 16 потоков

GPU

AMD Radeon 780M Graphics, встроенная графика, shared memory

RAM

32 GB DDR5 5600

Доступная память Windows

31,3 GB

SSD

NVMe 512 GB

OS

Windows 11

Режим питания Windows

Оптимальная производительность

Питание

От сети

Отдельно смотрел нагрузку на систему во время генерации ответов. В среднем картина была примерно такой:

CPU: около 67%
RAM: около 23.2 / 31.3 ГБ
Disk: около 1%
GPU: около 6%

Это не бенчмарк, а просто наблюдение из моего локального запуска. Оно хорошо показывает ограничение локальной LLM: данные остаются на машине, зато скорость сильно зависит от железа, модели, параметров Ollama, размера prompt и насколько вычисления реально уходят на GPU.

Перед exact-term guard добавил ещё один слой двухуровневую фильтрацию retrieval.

Vector search почти всегда может вернуть какие то chunks. Но "самый похожий" не всегда означает "достаточный для ответа", поэтому разделил результаты на strong и borderline.

В текущей версии пороги выглядят так:

RAG_MIN_SCORE = 0.32
RAG_BORDERLINE_SCORE = 0.25
RAG_MIN_CONTEXT_SOURCES = 2

strong это source с score >= RAG_MIN_SCORE, borderline это source со score между RAG_BORDERLINE_SCORE и RAG_MIN_SCORE.

Логика получилась такой:

  • если нет ни одного strong source, то не отвечаем и не вызываем LLM;

  • если strong source есть, можно добавить немного borderline контекста;

  • borderline фрагменты не должны сами по себе запускать generation.

Это помогло не подгонять threshold под один удачный вопрос. Например, вопрос про production должен получить осторожный ответ если в документах есть подходящий контекст. Но вопрос "Какая лицензия у проекта?" не должен доходить до LLM если лицензия в документах не описана.

Для меня это был важный момент: решение "можно ли отвечать" принимает не только prompt а сам backend до вызова модели.

9. Exact-term guard для технических токенов

После настройки retrieval появилась интересная проблема.

Semantic search хорошо ищет смысловые совпадения. Но для технических идентификаторов этого недостаточно. Если пользователь спрашивает про конкретную строку, endpoint, имя файла, класс или ошибку, важно найти именно буквальное совпадение.

Столкнулся с этим на stale index тесте. Вопрос был про контрольную строку которой ещё не было в индексе. Semantic search находил похожие chunks про rebuild index, потому что вопрос содержал слова INDEX и REBUILD. Модель получала нерелевантный контекст и тратила время на generation хотя точного токена в sources не было.

Попытка просто снизить retrieval threshold решила один пограничный вопрос, но ухудшила negative tests. Вопрос про production начал проходить, но вопрос про лицензию тоже получил нерелевантный source и дошёл до LLM. Модель честно ответила что информации нет, но backend уже потратил время на generation и передал в prompt неподходящий контекст.

Вывод: пороги нельзя подбирать только под один успешный кейс. Нужно проверять и positive и negative tests.

Поэтому добавил exact-term guard и exact-match boost.

Идея такая:

def extract_exact_terms(text: str) -> list[str]:
    # endpoint, имена файлов, CamelCase классы,
    # CONSTANT_CASE и строки вида CONTROL_EXACT_TERM_12345
    ...


def find_missing_exact_terms(terms: list[str], texts: list[str]) -> list[str]:
    joined_text = "\n".join(texts).lower()

    return [
        term
        for term in terms
        if term.lower() not in joined_text
    ]

Если вопрос содержит технический токен, backend дополнительно проверяет буквальное совпадение в найденных chunks. Если токена нет в контексте LLM не вызывается. Если токен есть в индексе соответствующий chunk добавляется в sources как exact-match результат со score = 1.0.

Это позволило не снижать общий retrieval threshold не ломать negative tests и при этом отвечать на вопросы про точные технические токены.

Проверка после изменения:

  • вопрос "Опиши поток обработки запроса /ask". снова работает, sources идут из architecture.md, generation_ms > 0;

  • вопрос "Какая лицензия у проекта?" возвращает no context, sources пустые, generation_ms = 0;

  • вопрос "Что такое CONTROL_EXACT_TERM_12345?" после rebuild находит operations.md, поздний chunk_index, generation_ms > 0.

После этого проверял систему не только на happy path.

Минимальный набор тестов получился таким:

  • релевантный вопрос должен поднимать ожидаемый документ;

  • вопрос про /ask должен находить архитектурный документ;

  • вопрос про production должен давать осторожный ответ, а не "да, можно";

  • вопрос про лицензию должен возвращать "в документах нет достаточной информации" и не вызывать LLM;

  • stale index сценарий должен показывать, что новый текст на диске не появляется в RAG, пока не выполнен rebuild index;

  • вопрос с точным техническим токеном должен проверяться не только semantic search, но и exact-match логикой.

Эти тесты оказались полезнее чем просто смотреть на красивый ответ во frontend. Они показывают где RAG действительно работает а где backend должен остановить pipeline и честно сказать, что контекста недостаточно.

10. Ограничения текущей версии

Текущая версия проекта это локальный прототип а не production ready система. Для меня было важно не скрывать ограничения, потому что именно они показывают где заканчивается демо и начинается engineering.

In memory vector store

Индекс хранится в памяти процесса backend. Это удобно для обучения и демонстрации, не нужно поднимать отдельную vector database. Но после перезапуска backend индекс исчезает и rebuild нужно выполнять заново.

Для production лучше использовать persistent vector store: Qdrant, Chroma, pgvector или другое хранилище.

Ручной rebuild index

Сейчас backend не отслеживает изменения файлов автоматически. Если документ изменился нужно вручную нажать rebuild index. Хотел что бы было видно, что индекс отдельное состояние. Для реальной эксплуатации лучше добавить автоматическое отслеживание изменений, хэши документов, scheduled rebuild или фоновую индексацию.

Простая нарезка на чанки

Чанки создаются простым способом: по размеру текста и overlap. Это понятно но не идеально. Chunk может начинаться в середине раздела, заголовок может отделиться от текста а один фрагмент может смешивать несколько тем.

Следующий уровень markdown aware chunking: учитывать заголовки, списки, абзацы и смысловые блоки.

Качество embeddings

Retrieval зависит от embedding модели. Semantic search может найти тематически похожий chunk, но не тот который нужен для ответа. Особенно это заметно на технических токенах. Exact-term guard частично решает проблему, но не заменяет качественную retrieval стратегию.

Скорость локальной LLM

Локальная генерация может быть медленной. В тестах retrieval был относительно стабильным а основное время занимал generation. Это нормально для локальной модели, но важно показывать пользователю duration_ms и timings.

Ограниченный prompt context

В prompt нельзя добавлять все документы. Нужно выбирать top-k sources и ограничивать общий размер контекста это компромисс между полнотой ответа, скоростью и качеством.

Sources не гарантируют идеальный ответ

sources, score и chunk_index делают ответ проверяемым, но не гарантируют абсолютную правильность. Модель всё ещё может ответить слишком общо или добавить разумное, но не подтверждённое источниками обобщение. Поэтому sources это диагностический инструмент а не магическая гарантия.

11. Итоги

Начал с простого вызова локальной LLM через FastAPI а в конце получился небольшой backend/RAG пайплайн:

API контракт
request_id
логирование
documents/
chunking
embeddings
in memory vector store
retrieval
sources
score
chunk_index
manual rebuild index
timings breakdown
negative tests
stale index tests
exact-term guard

Главный вывод для меня: LLM приложение становится системой не в момент когда модель начинает отвечать, оно становится системой когда вокруг модели появляются API, источники, диагностика, управляемый индекс, понятные ошибки и честные ограничения.

RAG это не один prompt это pipeline. И качество ответа зависит не только от генеративной модели, но и от документов, чанкинга, embeddings, retrieval thresholds, exact-match логики, prompt assembly, скорости локальной машины и наблюдаемости.

Понравился именно backend взгляд на задачу. Не "давайте прикрутим AI", а "Давайте построим понятный сервис вокруг LLM". Пусть локальный, учебный и не production ready, но с теми инженерными деталями, которые потом можно развивать.

Поэтому ценность проекта не в том, что он отвечает на вопросы по документам. Ценность в том, что у ответа появляется объяснимая цепочка: какие документы были загружены, какие chunks найдены, какие sources попали в prompt, сколько занял каждый этап и почему backend решил вызывать или не вызывать LLM.

Исходный код проекта доступен на GitHub: local-rag-assistant. В репозитории есть backend на FastAPI, простой frontend, RAG pipeline, ручной rebuild index, timings breakdown и exact-term guard для технических токенов.

Что можно улучшить дальше

Не стал добавлять всё сразу, но направления понятны:

  • заменить in memory vector store на persistent хранилище;

  • сделать markdown aware chunking;

  • добавить автоматический rebuild index по изменению документов;

  • улучшить /rag/debug чтобы явно показывать strong, borderline, rejected и exact-match sources;

  • добавить hybrid search: semantic + keyword;

  • добавить тесты качества retrieval;

  • сократить prompt и подобрать параметры генерации;

  • добавить мониторинг и отдельные метрики latency;

  • аккуратно вынести конфигурацию моделей и thresholds.

Но это уже следующий этап. Для проекта мне было важно пройти путь от простого вызова локальной LLM до понятного backend/RAG приложения и увидеть какие инженерные вопросы появляются по дороге.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

0%Качество поиска по документам0

0%Скорость локальной LLM0

0%Подбор chunking и embeddings0

0%Диагностика: sources, score, request_id0

0%Поддержка индекса в актуальном состоянии0

0%Пока не пробовал локальный RAG0

Никто еще не голосовал. Воздержавшихся нет.