Два прозореца със страницата за търсене на pacod-kb, светъл и тъмен, един върху друг. И в двата е зададен въпросът „How does the gateway keep provider keys?“ с top K 5; тъмният показва първия откъс, от раздела „How it is put together“ на pacod-demo, с RRF 0.0164, идентификатора на документа и байтове 16916–17985, и следващия, от pacod-gw, с RRF 0.0161.
Търсене през деня и през нощта. Обикновен въпрос към седемте README файла на PACOD връща подредени откъси, всеки с пътя от заглавия, идентификатора на документа и байтовия диапазон, нужни, за да бъде цитиран; оценките 0.0164 и 0.0161 (1/61 и 1/62) показват, че редът идва само от векторното търсене.

pacod-kb зарежда файлове и уеб страници, преобразува ги в каноничен Markdown, индексира ги за хибридно семантично търсене и търсене по ключови думи и пази всяка версия. Проверява всеки документ за prompt injection, преди нещо да бъде индексирано, и подава на моделите надолу по веригата фрагменти, които те могат да цитират, по документиран договор за метаданните. Работи на CPU, а търсенията остават отзивчиви, докато се зареждат документи.

RAG база знания

Възможности

Зарежда всичко

PDF, DOCX, HTML, Markdown, текст и JSON — качени, пуснати в папка, изтеглени от URL или наблюдавани за промени.

Каноничен Markdown

Заглавията се разпознават в правни текстове на осем езика, в научни публикации и в статии; номерата на страници, колонтитулите и шаблонният текст се филтрират.

Сигурност на съдържанието

Многостепенна проверка за prompt injection и спам блокира документа или го поставя под карантина, преди да бъде индексиран; резултатите от търсенето се ограждат като недоверени данни.

Хибридно търсене

Плътни вектори от BGE-M3 в Qdrant плюс индекс по ключови думи, обединени чрез reciprocal rank fusion, в проекти, които никога не виждат документите един на друг.

Всяка версия се пази

Новата версия заменя старата, която остава достъпна за търсене по дата; наблюдавана страница, която се върне към предишно състояние, възстановява по-ранната версия.

Калибрирана релевантност

Незадължителен cross-encoder reranker дава оценка на релевантността от 0 до 1, сравнима между заявките, така че min_score може да отсее резултатите, които не са отговор.

Договор за потребителите

Всеки фрагмент носи стабилен идентификатор, пътя си от заглавия и байтови отмествания в съхранения Markdown, документирани поле по поле.

Създаден за CPU

Заявките минават пред работата по зареждането и чакат най-много един малък пакет; ембедингите могат да идват и от обща услуга.

Уеб интерфейс и CLI

Уеб интерфейс с общия облик на pacod, светъл или тъмен, за документи, проекти, задачи, използване и разходи за AI и членове; CLI kb за зареждане, търсене, наблюдение, повторно изграждане и експорт.

Диаграмата на платформата с осветен pacod-kb и приглушени останали проекти: сива линия от хората до pacod-kb, зелена пунктирна линия от pacod-kb до pacod-gw, под който са доставчиците на модели, и златиста пунктирна линия до pacod-id; пунктирна рамка с pacod-demo обхваща всички.
Мястото на pacod-kb. Хората го използват пряко (сиво), незадължителните му обръщения към LLM минават през pacod-gw, който държи ключовете на доставчиците (зелено), а входът минава през pacod-id, където се дават ролите в kb (златисто). pacod-demo го пуска заедно с останалите.

Екранни снимки

Светлата страница с документите на pacod-kb за проекта kb_chunks: поле за филтриране с All, Active, Superseded и Errors, бутони за експорт и таблица със седем ръководства на английски, pacod-kb, pacod-id, pacod-gw, pacod-demo, pacod-cms, pacod-benchmark и pacod-ap, с по 4 до 28 части, всички активни и приети на 2026-10-28.
Документите на един проект: седемте README файла на PACOD, качени като Markdown, всеки активен и с броя на частите си. Идентификаторът на документа е SHA-256 на байтовете му, а всеки може да бъде разгледан, изтеглен като източник или като съхранения Markdown, или индексиран наново.
Светлото табло на pacod-kb: плочки със 7 документа общо, 7 активни, 0 заменени и 0 грешки; панел за работа, начело със зелен бутон Ingest document; панел за поддръжка със Scan dropzone, Run watch, Regenerate from DB, Convert no-index и червен Rebuild full index; и редове с настройки, където проверката на съдържанието е включена, а преобразуването е детерминистично.
Таблото: документите на проекта по състояние, ежедневната работа, поддръжката (папката за пускане, наблюдаваните уеб източници, пълното преизграждане) и превключвателите, които действат без рестарт. Проверката на съдържанието е включена; преобразуването е детерминистично, така че никакъв модел не докосва тези документи.
Тъмната страница с колекциите в Qdrant на pacod-kb: форма за нова колекция с име, 1024 измерения и косинусово разстояние и таблица с kb_chunks, 87 точки, бутон Export all и връзка Delete.
Колекциите на kb в Qdrant, през нощта. kb_chunks съдържа 87 точки, по една за всяка част на седемте документа, като 1024-мерни вектори на BGE-M3, сравнявани по косинус; в Qdrant, споделен с други продукти, страницата показва само собствените колекции на kb.

Защо съществува

По-голямата част от работата в retrieval-augmented generation се случва, преди моделът да прочете и дума: да вкараш документите, да ги поддържаш чисти, да намериш правилния пасаж и да кажеш на модела откъде идва. pacod-kb е точно тази част, изградена като самостоятелна услуга с HTTP API, уеб интерфейс и CLI.

Три изисквания са я оформили:

  • Отговорите трябва да могат да се цитират. Всеки фрагмент носи пътя си от заглавия и байтови отмествания в стабилен Markdown текст, така че потребителят може да го цитира, да постави връзка към него и да извлече контекста му.
  • Източниците се променят. Нищо не се презаписва: новата версия заменя старата, която остава достъпна за търсене по дата.
  • Зареденият текст е недоверен вход за модела. Той се проверява за prompt injection, преди да бъде индексиран, и се огражда като данни на изхода.

Документ, стъпка по стъпка

Документът минава по един и същ път, независимо дали е качен, пуснат в папката dropzone, изтеглен от URL или изтеглен отново от наблюдаван уеб източник:

  1. Приемане. SHA-256 на байтовете на източника е идентификаторът на документа; познат идентификатор се пропуска, така че един и същ файл никога не се зарежда два пъти.
  2. Парсинг. PDF през docling, с незадължително OCR за сканирани страници (ако е инсталиран tesseract, два OCR двигателя се сравняват страница по страница), DOCX, HTML, Markdown, текст и JSON, чиито ключове стават заглавия. Резултатът е Markdown с YAML front matter.
  3. Проверка на езика. Приемат се английски, немски, български, руски, сръбски, македонски, хърватски и словенски; другите езици се отказват, преди нещо да бъде записано. Текст, чийто език не може да бъде разпознат, се приема като unknown, с предупреждение.
  4. Проверка на детерминистичния парсинг за prompt injection и спам. Тя става, преди какъвто и да е модел да види текста: преобразувателят би могъл да изпълни вмъкнати инструкции, а преобразуването би премахнало символите с нулева ширина и скритата маркировка, които проверката търси.
  5. Преобразуване, по желание. LLM обработка през pacod-gw подрежда Markdown-а; PDF до 20 MiB стига до модела като файл, всичко останало — като извлечен текст. Документ под карантина никога не се изпраща. Прекъснат, отказан или празен отговор запазва детерминистичния парсинг, а преобразуваният текст се проверява втори път.
  6. Почистване и структуриране. Филтър за шум премахва номера на страници, колонтитули, известия за бисквитки и блокове от навигационни връзки. Заглавията се повишават в правни кодекси, регламенти и директиви (осем езика) и в научни публикации; статия от URL получава и своя автор, дата и издание.
  7. Съхраняване на document.md, манифест с дневника на жизнения цикъл и моментно копие на източника: в папки под KB_ROOT или в PostgreSQL.
  8. Индексиране, само когато статусът на документа е ok. Фрагменти до 500 токена, съобразени със заглавията, се вграждат с BGE-M3, всеки с пътя от заглавия [Title > Heading > Subheading] отпред (таблицата получава и абзаца преди нея), в колекцията на проекта в Qdrant и в неговия индекс по ключови думи. Фрагментите, които не са се променили от по-ранна версия, запазват векторите си.

Зарежданията от интерфейса или от API-то се изпълняват като задачи, една след друга, в собствен worker, никога в нишките, които отговарят на търсенията. В режим с база данни задачите се пазят в PostgreSQL, а рестартът маркира прекъснатите от него като неуспешни.

Архитектура

pacod-kb се състои от две услуги, kb-pipeline и kb-ui, както и от моделите и хранилищата, които те ползват:

Част Роля
Pipeline API (kb-pipeline, :8000) Зареждане, индексиране, търсене, задачи, настройки; единствената част, която вика AI шлюза
Уеб интерфейс (kb-ui, :8080) FastAPI и HTMX; вход през pacod-id, сесии, членове, страниците; вика конвейера с API ключ
BGE-M3 Ембединги с 1024 измерения, в собствения процес на конвейера или от услугата за ембединги на pacod-ap
Qdrant Векторите: вграден в конвейера или сървър, който могат да споделят и други продукти
Индекс по ключови думи SQLite FTS5 (BM25) при съхранение във файловата система, пълнотекстово търсене на PostgreSQL при съхранение в база данни
Документи и регистър Папки под KB_ROOT или PostgreSQL; регистърът пази настройките, промптовете, използването и разходите за AI

Разделението е умишлено. Конвейерът парсва недоверени документи и изтегля произволни URL адреси, затова не получава нито една от тайните на pacod-id; интерфейсът пък никога не вижда ключа за шлюза. Всеки контейнер получава изричен списък от променливи на средата, а един тест проверява този списък.

Всеки контейнер се включва в общата Docker мрежа pacod-net, където всеки може да достигне останалите. Изключение е вътрешният слушател на pacod-id (токени, ключове за подписване, потокът за оторизация, административното API): той отговаря само в pacod-admin-net, вътрешна мрежа без изход навън, като pacod-id-internal:8071. kb-ui се включва в тази мрежа; конвейерът не е в нея и не може да го достигне.

Кой какво е направил. Всяко извикване от интерфейса носи X-KB-Actor — идентификатора на влезлия човек. Конвейерът му се доверява само заедно с API ключа, записва го към задачите, промените в настройките и използването и го предава на шлюза. Когато pacod-id изтрие акаунт, kb прекратява сесиите му, заменя този идентификатор с deleted-user навсякъде и едва тогава потвърждава изтриването.

Търсене и какво получава моделът

Заявката минава през Qdrant и през индекса по ключови думи, а двете класирания се обединяват чрез reciprocal rank fusion: всеки списък добавя 1/(60 + ранг) за всеки фрагмент, който е намерил. Филтрите по дата и история се прилагат вътре в двете търсения, така че сто заменени фрагмента не могат да изместят единствения валиден.

Оценката по RRF подрежда един списък с резултати и не означава нищо между различни заявки. Оценка 0,0164 е 1/61: първо място в единия списък и отсъствие от другия. Точно това получават по подразбиране повечето естествено формулирани въпроси, защото половината с ключови думи (KEYWORD_QUERY_MODE=legacy) изисква съвпадение на всяка дума от заявката. Анализиращите режими (or, and_or) вместо това премахват стоп думите и свеждат думите до основната им форма; те остават изключени, докато оценка не покаже, че помагат.

Незадължителен cross-encoder, BAAI/bge-reranker-v2-m3, пренарежда най-добрите десет кандидата и дава на всеки релевантност между 0 и 1, сравнима между заявките, така че min_score може да отсее резултатите, които не са отговор. Той се зарежда във фонов режим, когато бъде включен, и никоя заявка не го зарежда.

POST /query приема въпроса и филтрите му:

{"query": "maximum fine for a data breach", "top_k": 5, "as_of": "2025-12-30"}

Отговорът е {"results": [...], "reranked": false, "score_kind": "rrf"}. Всеки резултат съдържа:

Поле За какво служи
chunk_id Стабилен идентификатор, който кодира структурата от заглавия, а не пореден номер
source_id Идентификаторът на документа: SHA-256 на байтовете на източника
heading_path Път от заглавия като ["Chapter 1", "§ 5"], за цитиране и филтри по раздел
byte_start, byte_end UTF-8 отмествания в съхранения Markdown текст
valid_until Кога тази версия е заменена, или null, докато е актуална
text Вграденият текст, ограден като недоверени данни
score, rrf_score, rerank_score Оценката за подреждане и нейните съставки

Отместванията са в байтове, а не в символи, което има значение за текст на кирилица:

body_bytes = body.encode("utf-8")
passage = body_bytes[result["byte_start"]:result["byte_end"]].decode("utf-8")

Всеки документ има и манифест: заглавие, източник, тип документ, статус, проект, парсерът или моделът през шлюза, създал текста, и дневник на жизнения цикъл (ingested, superseded, reinstated, security_flagged, review_flagged, llm_usage).

Версии и проекти

Нова версия, заредена с --supersedes, или променен наблюдаван източник маркира старата с valid_until едновременно на четири места: във front matter, в манифеста, в индекса по ключови думи и във векторните данни. Търсенията връщат актуалните фрагменти по подразбиране; as_of връща валидното на даден ден; include_historical връща всичко. Ако наблюдавана страница се върне към предишно състояние, по-ранната версия се възстановява. В режим с файлова система, с GIT_AUTOCOMMIT=true, всяко зареждане се записва и като commit в git хранилище в KB_ROOT.

Проектът е изолирана колекция: собствена колекция в Qdrant и собствени редове в индекса по ключови думи. Търсенето вижда един проект, повторното изграждане изтрива и попълва наново индекса на един проект (--all-projects изгражда наново всеки в собствената му колекция), а смяната на активния проект се отказва, докато има задача на опашката или в изпълнение.

Търсене, подкрепено с измервания

Стойностите по подразбиране идват от инструмент за оценка: 14 документа на шест езика (GDPR, Директивата за защита на данните в правоприлагането, четири конституции, статии от Wikipedia) и 92 ръчно написани заявки — перифрази, точни термини, междуезикови, на роден език и такива без отговор. Резултатът се брои, когато идва от раздела, посочен в етикета, така че етикетите остават валидни при промени в разделянето на фрагменти. Числата са измерени на процесора на машина за разработка (Apple M5, четири нишки) и документацията подчертава, че това не са продукционни числа:

Измерване Резултат
Правилният раздел е сред обединените кандидати при всяка заявка с отговор
Правилният раздел е сред първите пет, без reranker 98,7 % (R@1 0,83, около 50 ms)
С reranker, десет кандидата R@1 0,91, около 2 s на заявка
Търсене, докато се зареждат документи (p50) 24,3 s преди; 0,41 s сега, със задачи, изпълнявани една след друга
Вграждане при зареждане, пакети, сортирани по дължина около 2,2× по-бързо, векторите са непроменени

Оценката на reranker-а разделя отговорите от резултатите, които не са отговор: min_score 0.1 отсява 14 от 15 заявки без отговор и 2 от 70 верни първи отговора, докато оценките по RRF изобщо не можеха да ги разделят. Въпреки това той е изключен по подразбиране: подобрението му остана малко под прага при отделената за проверка половина, а латентността му тепърва трябва да се потвърди на продукционния процесор.

Анализиращите режими за ключови думи помогнаха при заявките на роден език и с точни термини, но загубите при междуезиковите и перифразираните бяха по-големи, а при отделената за проверка половина загубиха: legacy остава по подразбиране.

Модел на сигурност

Сигурност на съдържанието. Проверката работи върху нормализирано копие на текста (нормализиран по Unicode, с малки букви, с отново слети думи, изписани с разредени букви) и прилага таблици с правила: фрази за injection на английски, немски, руски и български, трикове с обръщане на посоката на писане и със символи с нулева ширина, base64 и шестнадесетични блокове (декодирани и проверени отново), скрита маркировка и мерки за спам като плътността на връзките и повтарящите се редове. Всяко правило има тежест. Висока е само за недвусмислена подмяна на инструкциите („ignore previous instructions“), за знак за обръщане на посоката на писане и за инструкция, скрита в base64; фрази, които се срещат и в технически текстове, като „enable developer mode“, са с ниска тежест, така че едно-единствено съвпадение не може да блокира документ. Тестове проверяват, че дълги закони, сред тях GDPR, минават като чисти. Присъдата е една от три:

  • block: находка с висока тежест или висока оценка на заплахата. Зареждането се проваля и нищо не се съхранява.
  • quarantine: документът се пази със статус review, никога не се преобразува и никога не се индексира. Само спам никога не води до блокиране.
  • clean: документът се индексира.

Незадължително второ мнение от LLM през шлюза (по подразбиране изключено) се иска само когато правилата не са блокирали; то има 15 секунди и при грешка не блокира документа. Материал, за който се знае, че е безопасен, може да прескочи проверката с kb ingest --trusted. На изхода /query огражда всеки фрагмент като недоверени данни.

Изтегляне. Зарежданията от URL приемат само http и https, следват най-много пет пренасочвания и проверяват всяка стъпка, а спират при 50 MB или 180 секунди; зареждане от URL никога не чете локален файл. Две стойности по подразбиране са подходящи за лаптоп, а не за сървър: задайте WEB_FETCH_ALLOW_PRIVATE=false, за да се отказват адреси, които водят до частни мрежи (защита срещу server-side request forgery), и ALLOW_INSECURE_TLS=false, за да спре повторният опит без проверка на сертификата след TLS грешка.

Хора. Интерфейсът вписва хората чрез pacod-id с OIDC и PKCE, на собствените страници на pacod-id, оформени с марката на kb. Той проверява токена за достъп, пази токените в криптирана сесия на сървъра и дава на браузъра бисквитка, която носи само случайна тайна. Ролите на kb са манифест, който интерфейсът изпраща на pacod-id при всяко стартиране; след това ролята на човека идва от потока за оторизация на pacod-id. Без роля в kb човек вижда страница „няма достъп“; ако копието на потока в интерфейса е по-старо от 60 секунди, страниците отговарят с 503, вместо да гадаят. Излизането прекратява сесията в pacod-id, а сесия, отменена там, приключва и в kb.

Роля Може
viewer Четене и търсене
editor Зареждане, повторно индексиране и изтриване на документи; изпълнение на задачи; управление на проекти
admin AI Management, промптове, превключвателите по време на работа, членове

Администраторите канят хора и сменят роли на страницата с членове, през административното API на pacod-id: показват се само ролите на kb, а последният администратор не може да бъде премахнат.

API-то на конвейера сравнява ключа си за постоянно време и при грешка отказва достъп: без PIPELINE_API_SECRET всеки маршрут освен /health отговаря с 503.

Внедряване и експлоатация

docker-compose.yml съдържа собствените услуги на kb, kb-pipeline и kb-ui. docker-compose.standalone.yml съдържа копията на kb от общите компоненти, които се включват с профили на Compose: Qdrant, PostgreSQL, pacod-id с база данни до тази на kb, прихващач на поща и шлюз за разработка — pacod-gw-lite пред фиктивните доставчици на pacod-gw, така че нищо не напуска машината. make up зарежда и двата файла; нужни са му клонирани хранилища на pacod-id и pacod-gw до това на kb, а на една машина може да работи само по едно копие от всеки общ компонент. Фиктивният доставчик повтаря промпта, затова стекът за разработка оставя преобразуването изключено; AI Management → Test model все пак минава целия път, включително цената.

В продукция насочвате kb към истински шлюз (pacod-gw с организация, работно пространство и ключ за kb или pacod-gw-lite с ключовете на доставчиците), поставяте pacod-id зад TLS или използвате вече съществуващ и премахвате профилите за разработка.

pacod-demo, достъпен на demo.pacod.io, пуска kb заедно с останалите проекти на pacod. Той зарежда само файла на приложението на kb: kb държи базата си данни в общия PostgreSQL и колекциите си kb_ в общия Qdrant, вгражда през общата услуга за ембединги, а интерфейсът му се обслужва зад общия входен Caddy. Демото също оставя преобразуването изключено.

  • Хранилище. KB_STORAGE_BACKEND е filesystem или database; смяната не мигрира документите.
  • Данни. Директориите с данни са променливи (KB_ROOT, KB_MODEL_CACHE, KB_QDRANT_DATA, KB_POSTGRES_DATA). Име без наклонена черта е Docker том; под macOS make setup използва томове.
  • Процесор. TORCH_NUM_THREADS=0 следва квотата на контейнера за процесор. EMBED_BATCH_TOKENS (1024) ограничава колко дълго една заявка може да чака зад зареждане.
  • Модели офлайн. Пуснете веднъж с достъп до мрежата, за да се напълни кешът на моделите, след това задайте HF_HUB_OFFLINE=1.
  • Общи модели. С EMBEDDINGS_URL конвейерът вгражда през общата услуга за ембединги, вместо сам да зарежда BGE-M3: векторите са същите, така че нищо не се вгражда наново, а конвейерът има нужда от около 2 GB по-малко памет. RERANKER_URL прави същото за reranker-а. Ако моделът на тази услуга някога бъде сменен, индексът на kb трябва да се изгради наново.
  • Общ Qdrant. На Qdrant, който ползват и други продукти, страниците за Qdrant в интерфейса показват и засягат само колекциите на kb (KB_QDRANT_COLLECTION_PREFIX, по подразбиране kb_).
  • Превключватели по време на работа. Таблото включва и изключва проверката на съдържанието, AI преобразуването и проверката на езика без рестарт; те не се запазват и при следващото стартиране отново важат променливите на средата.
  • AI. Администраторите избират модели и редактират промптове в AI Management, където се виждат и използването и разходите, отчетени от шлюза. Всяко извикване носи непрозрачна атрибуция (функция, задача, проект, човек); при 429 извикването се повтаря веднъж, след изтичане на Retry-After. Ако няма конфигуриран шлюз, AI е изключен и нищо друго не се чупи.
  • Логове. По едно събитие на ред в общия формат на pacod, JSON или текст (LOG_FORMAT), на ниво LOG_LEVEL, без параметрите на заявките в URL. Docker пази пет файла по 10 MB за всеки контейнер.

Резервните копия обхващат KB_ROOT или базата данни на kb в PostgreSQL, хранилището на Qdrant (или го изградете наново с kb rebuild --all-projects), базата данни на pacod-id заедно с ключа ѝ за криптиране на ключове и .env.

Какво е тествано и какви са ограниченията

make check пуска ruff, mypy --strict и тестовете, които не се нуждаят от мрежа, от изтегляне на модели, нито от работещ pacod-id или шлюз: тестовете на интерфейса влизат през истинския процес срещу фалшив pacod-id. Моментни снимки на таблиците с маршрути на двете API-та улавят нежелани промени, а CI пуска набора за оценка със заместващ модел за ембединги при всеки push, за да улови отклонения в корпуса или в разделянето на фрагменти.

  • Числата за търсенето са от една машина за разработка; make eval-docker ги повтаря в образа на конвейера, ограничен до зададен брой процесори, както на продукционния хост.
  • Конвейерът и интерфейсът работят с по един worker: вграденият Qdrant държи заключване на файл, а управлението на сесиите в интерфейса предполага един процес.
  • Reranker в собствения процес на конвейера приблизително удвоява паметта му; с RERANKER_URL конвейерът не зарежда такъв.
  • Сръбският се приема, но детекторът на езици няма профил за сръбски, затова сръбски текст може да бъде разпознат грешно или отказан.
  • kb няма крайна точка за метрики. Графиката за използването на AI зарежда Chart.js от CDN — единственият външен скрипт в интерфейса.
  • Документирани за следващото повторно индексиране, но не включени: разредени вектори от BGE-M3 за половината с ключови думи, почистване на разделянето на фрагменти и изграждане в колекция-сянка преди превключване.

Повече подробности:

Бърз старт

Стартиране с Docker

make setup
make up
open http://localhost:8080/kb

Разработка и оценка

uv sync
make check
make eval

Работи с

  • pacod-id: Хората влизат през pacod-id; ролите viewer, editor и admin се предоставят там, а страницата с членове на kb използва неговото административно API.
  • pacod-gw: Незадължителното LLM почистване и второто мнение при проверката, също незадължително, минават през шлюза, който държи ключовете на доставчиците, остойностява всяко извикване и го отчита.
  • pacod-ap: Когато работи заедно с pacod-ap, kb може да вгражда и пренарежда през неговата обща услуга за ембединги, със същите вектори от BGE-M3.
  • pacod-demo: Пуска kb заедно с останалите продукти, върху общите Qdrant, PostgreSQL и услуга за ембединги, зад общия входен слой.