| app | ||
| evaluation | ||
| scripts | ||
| test | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
ZP Agent
Backend pre indexovanie, hybridné vyhľadávanie a RAG nad repozitárom
záverečných prác zpwiki.
Projekt načítava Markdown dokumenty, spracuje YAML metadata, rozdelí obsah na tokenové chunky a vytvorí SQLite index kombinujúci FTS5 fulltextové vyhľadávanie a embeddingy. Nad hybridným vyhľadávaním je vytvorená RAG vrstva, ktorá pripravuje kontext, zdroje a pravidlá pre odpoveď jazykového modelu. API je integrované so školským OpenWebUI ako OpenAPI Tool Server.
Súčasná hlavná retrieval vetva používa klasický hybridný RAG: FTS5/BM25 + embeddingy + RRF. GraphRAG zatiaľ nie je implementovaný; je preň pripravený samostatný evaluačný dataset a bude sa dopĺňať ako ďalšia experimentálna vetva.
Implementované
- načítanie Markdown súborov a YAML front matter,
- normalizácia názvov, autorov, tagov, kategórií a
published, - tokenové chunkovanie pomocou
tiktoken, - konfigurovateľná maximálna veľkosť chunku, overlap a minimálna veľkosť chunku,
- zachovanie názvu dokumentu a hierarchie nadpisov v chunku,
- SQLite databáza a FTS5 fulltextový index,
- BM25 vyhľadávanie s podporou diakritiky a prefixových výrazov,
- vyhľadávacie stratégie
all_terms,prefix_termsaany_term, - embeddingy pre každý chunk pomocou modelu
intfloat/multilingual-e5-small, - uloženie embeddingov priamo v SQLite,
- vektorové vyhľadávanie pomocou cosine similarity,
- hybridné vyhľadávanie FTS5 + embeddings pomocou RRF,
- nižšia váha pre slabú
any_termFTS stratégiu, - lexikálne zvýhodnenie presných alebo veľmi silných zhôd,
- obmedzenie počtu výsledkov z jedného dokumentu pomocou
max_per_document, - filtrovanie publikovaných dokumentov,
- generovanie
source_urlpre dohľadateľnosť výsledkov, - RAG vrstva s pripraveným kontextom, zdrojmi a pravidlami pre grounded odpoveď,
- rozšírenie RAG kontextu o začiatok rovnakej sekcie
(
section-lead expansion) bez globálneho zvýšeniamax_per_document, - pravidlá proti používaniu neoverených informácií a zamieňaniu rôznych typov údajov,
- no-answer správanie pri chýbajúcej alebo nedostatočne podloženej informácii,
- FastAPI endpointy
/health,/rag,/search,/synca/webhook/gitea, - striktná validácia API vstupov,
- autorizácia vyhľadávania pomocou
X-API-Keyalebo Bearer tokenu, - autorizácia
/syncpomocou samostatného API kľúča, - CORS konfigurácia pre OpenWebUI,
- bezpečnostné HTTP hlavičky,
- integrácia s OpenWebUI cez OpenAPI Tool Server,
- Gitea webhook s HMAC-SHA256 podpisom, limitom veľkosti payloadu a kontrolou udalosti a repozitára,
- zámok proti súbežnému reindexovaniu,
- atomická výmena databázy po úspešnom reindexovaní,
- persistentná Hugging Face cache v Docker volume,
- warm-up embeddingového modelu,
- retrieval evaluácia pre FTS, embeddings a hybridný retrieval,
- answer-level RAG evaluácia cez OpenWebUI model a RAG tool,
- oddelené answer-level overrides bez úpravy frozen retrieval benchmarku,
- príprava rozšírených datasetov pre extended RAG, GraphRAG, robustness a performance experimenty.
Architektúra
zpwiki
↓
Markdown + YAML
↓
scan_zpwiki.py
↓
normalizované dokumenty
↓
build_chunks.py
↓
tokenové chunky
↓
build_sqlite_index.py
↓
SQLite
├── dokumenty
├── metadata
├── chunky
├── FTS5
└── embeddingy
↓
search_core.py / search_utils.py
↓
hybridné vyhľadávanie
├── FTS5 / BM25
├── embeddingové vyhľadávanie
└── RRF fusion + lexical anchor
↓
rag_utils.py
├── výber zdrojov
├── section-lead expansion
├── RAG kontext
└── grounding pravidlá
↓
FastAPI /rag
↓
OpenWebUI / ZP Agent
↓
jazykový model
↓
odpoveď + source_url
Synchronizačná vetva:
Gitea push
↓
POST /webhook/gitea
↓
HMAC + repository + event validácia
↓
voliteľný git pull --ff-only
↓
rebuild_index.py
↓
nový SQLite index
↓
atomická výmena databázy
Evaluačná vetva:
evaluation/json_files/*.json
↓
retrieval evaluator / RAG answer evaluator
↓
runner
↓
metriky
↓
evaluation/results/
evaluation/results/ nie je v nižšie uvedenom stromčeku, pretože
obsahuje generované výstupy experimentov.
Štruktúra projektu
Nižšie je funkčná štruktúra projektu. Testovacie súbory, generované
výsledky, cache, __pycache__, Git interné súbory a podobné pomocné
artefakty sú zámerne vynechané.
zp-agent/
├── app/
│ ├── main.py
│ ├── routes.py
│ └── security.py
│
├── scripts/
│ ├── common.py
│ ├── scan_zpwiki.py
│ ├── build_chunks.py
│ ├── build_sqlite_index.py
│ ├── embedding_utils.py
│ ├── rag_utils.py
│ ├── rebuild_index.py
│ ├── search_db.py
│ ├── search_core.py
│ └── search_utils.py
│
├── evaluation/
│ ├── evaluate_retrieval.py
│ ├── retrieval_runner.py
│ ├── metrics.py
│ ├── evaluate_rag_answers.py
│ ├── rag_runner.py
│ ├── rag_metrics.py
│ └── json_files/
│ ├── questions.json
│ ├── questions_before_ambiguity_cleanup.json
│ ├── questions_before_validation.json
│ ├── rag_answer_overrides.json
│ ├── questions_extended_2000.json
│ ├── graphrag_questions.json
│ ├── robustness_questions.json
│ └── performance_queries.json
│
├── data/
│ ├── documents.json
│ ├── chunks.json
│ └── zp_index.sqlite
│
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── requirements-dev.txt
└── README.md
app/
app/main.py
- vytvára FastAPI aplikáciu,
- nastavuje lifespan aplikácie,
- validuje bezpečnostnú konfiguráciu pri štarte,
- vykonáva warm-up embeddingovej vrstvy,
- nastavuje CORS a bezpečnostné hlavičky,
- pripája API routery,
- ponecháva OpenAPI schému dostupnú pre OpenWebUI.
Interaktívne Swagger/Redoc rozhranie môže byť v produkčnej konfigurácii vypnuté. Pre integráciu OpenWebUI je dôležitý endpoint:
/openapi.json
app/routes.py
- definuje
/health, - definuje verejne publikovaný RAG nástroj
/rag, - obsahuje interné/zabezpečené
/searcha/sync, - obsluhuje
/webhook/gitea, - vykonáva validáciu requestov a sanitizáciu chýb,
- používa reindexovací zámok,
- kontroluje webhook payload pred spracovaním.
app/security.py
- spracúva
SEARCH_API_KEY, - spracúva
SYNC_API_KEY, - spracúva
WEBHOOK_SECRET, - vyžaduje dostatočne dlhé a navzájom odlišné tajomstvá,
- používa bezpečné porovnávanie autentifikačných hodnôt,
- validuje očakávaný Gitea repozitár a súvisiacu konfiguráciu.
scripts/
common.py
Spoločné utility a konfiguračné funkcie používané indexovacou a vyhľadávacou vrstvou.
scan_zpwiki.py
- prechádza repozitár
zpwiki, - načítava Markdown a YAML front matter,
- normalizuje dokumentové metadata,
- pripravuje dokumenty na ďalšie spracovanie.
build_chunks.py
- rozdeľuje dokumenty na tokenové chunky,
- používa
tiktoken, - zachováva nadpisy, kódové bloky a tabuľky,
- podporuje tokenový overlap,
- eviduje token count a hash obsahu chunku.
build_sqlite_index.py
- vytvára SQLite databázu,
- ukladá dokumenty, chunky a metadata,
- vytvára FTS5 index,
- ukladá embeddingy do SQLite,
- pripravuje databázu pre hybridný retrieval.
embedding_utils.py
- načítava embeddingový model,
- vytvára embeddingy pre dokumenty a query,
- normalizuje vektory,
- poskytuje utility pre vektorové vyhľadávanie.
search_core.py
Nízkoúrovňové jadro vyhľadávania:
- normalizácia query,
- FTS5 vyhľadávanie,
- embeddingové vyhľadávanie,
- scoring,
- RRF fusion,
- lexical anchor,
- metadata,
- diverzifikácia výsledkov.
search_utils.py
Vyššia orchestration vrstva nad search_core.py a kompatibilné
vyhľadávacie utility používané ostatnými časťami projektu.
search_db.py
CLI rozhranie na ručné vyhľadávanie nad lokálnym SQLite indexom.
rag_utils.py
- zostavuje RAG kontext,
- pripravuje zdrojové metadata,
- pridáva
source_url, - aplikuje grounding pravidlá,
- podporuje no-answer správanie,
- rozširuje primárny chunk o začiatok rovnakej sekcie,
- zachováva väzbu medzi primárnym chunkom a section-lead chunkom.
rebuild_index.py
Orchestruje kompletný rebuild:
scan
→ chunking
→ SQLite/FTS
→ embeddings
→ validácia
→ atomická výmena DB
evaluation/
evaluate_retrieval.py
CLI vstup pre retrieval benchmark. Umožňuje spúšťať DEV, TEST alebo celý dataset a používa strict dataset validáciu.
retrieval_runner.py
Spúšťa jednotlivé retrieval stratégie nad evaluačnými otázkami.
metrics.py
Počíta retrieval metriky, napríklad:
Hit@1
Hit@3
Hit@5
MRR
Recall@5
evaluate_rag_answers.py
CLI vstup pre answer-level RAG evaluáciu. Hodnotí celý tok:
otázka
→ OpenWebUI model
→ ZP Agent tool
→ /rag
→ finálna odpoveď
Podporuje answer-level overrides, aby nebolo potrebné meniť frozen retrieval benchmark pri otázkach, ktoré sú pre generatívnu evaluáciu nejednoznačné.
rag_runner.py
Spúšťa jednotlivé answer-level prípady a komunikuje s OpenWebUI/API.
rag_metrics.py
Počíta answer-level metriky, napríklad:
- prítomnosť očakávaných faktov,
- správnosť
source_url, should_answer,- použitie toolu,
- strict pass,
- latency a tokenové údaje.
evaluation/json_files/
questions.json
Hlavný frozen benchmark pre klasický retrieval a RAG. Po zmrazení benchmarku sa nemá meniť len preto, aby sa zlepšil výsledok answer-level evaluácie.
rag_answer_overrides.json
Obsahuje iba answer-level úpravy formulácie alebo očakávaní pri nejednoznačných otázkach. Retrieval benchmark tým ostáva nezmenený.
questions_before_ambiguity_cleanup.json
Historická záloha datasetu pred úpravami nejednoznačností.
questions_before_validation.json
Historická záloha datasetu pred validačnými úpravami.
questions_extended_2000.json
Rozšírený benchmark s presne 2000 otázkami. Je pripravený pre neskoršie rozsiahlejšie experimenty nad klasickým retrievalom a RAG.
Obsahuje kombináciu:
- faktických otázok,
- parafráz,
- otázok bez diakritiky,
- preklepov,
- krátkych query,
- no-answer prípadov,
- negatívnej verifikácie,
- multi-document otázok,
- porovnávania,
- citation-oriented prípadov.
Tento dataset je zatiaľ pripravený, ale nemá sa používať na priebežné ladenie frozen benchmarku.
graphrag_questions.json
Samostatný benchmark pripravený pre budúcu GraphRAG vetvu.
Obsahuje 500 otázok zameraných na:
- multi-hop vzťahy,
- spoločné témy,
- spoločné roky,
- person → title → year,
- person → topic → title,
- person → category → author,
- agregácie,
- konjunktívnu disambiguáciu,
- multi-document reasoning.
Dataset obsahuje aj GraphRAG metadata ako:
graph.task
graph.hop_count
graph.start_entities
graph.expected_entities
graph.expected_relations
graph.expected_paths
Dôležité: existencia tohto datasetu neznamená, že je GraphRAG už implementovaný. Dataset je pripravený na neskorší vývoj a porovnanie.
robustness_questions.json
Samostatný dataset s 200 otázkami pre testovanie robustnosti.
Pokrýva napríklad:
- chýbajúcu diakritiku,
- preklepy,
- nekonzistentnú kapitalizáciu a interpunkciu,
- nerelevantný šum,
- prompt injection,
- konfliktné tvrdenia používateľa,
- tlak na halucinovanie,
- no-answer grounding,
- ambiguity/disambiguation,
- integritu citácií,
- kombinovaný vstupný šum.
Každá položka obsahuje aj robustness metadata s typom útoku,
perturbáciou, očakávaným správaním a závažnosťou.
performance_queries.json
Samostatný workload s 200 query pre budúce performance a stress experimenty.
Profily zahŕňajú:
- baseline single-document query,
- krátke query,
- noisy query,
- multi-document query,
- no-answer query,
- query s väčším retrieval limitom,
- context-heavy query,
- opakované hot query.
Súbor samotný performance nemeria. Je to vstup pre budúci samostatný
performance runner, ktorý bude volať /rag a merať napríklad:
mean latency
median
P50
P95
P99
requests/second
error rate
timeout rate
cold vs warm latency
Stress režim bude nad rovnakým workloadom zvyšovať paralelizmus a sledovať správanie systému pri rastúcej záťaži.
Konfigurácia
V koreňovom priečinku vytvor .env:
WEBHOOK_SECRET=<náhodná hodnota s minimálne 32 znakmi>
SYNC_API_KEY=<iná náhodná hodnota s minimálne 32 znakmi>
SEARCH_API_KEY=<ďalšia náhodná hodnota s minimálne 32 znakmi>
EXPECTED_GITEA_REPOSITORY=KEMT/zpwiki
WEBHOOK_PULL_GIT=false
# Voliteľné
EMBEDDING_MODEL=intfloat/multilingual-e5-small
EMBEDDING_BATCH_SIZE=32
Tajomstvá je možné vygenerovať príkazom:
openssl rand -hex 32
Súbor .env sa nesmie commitovať.
Chunkovanie je možné konfigurovať pomocou premenných prostredia:
CHUNK_MAX_TOKENS=450
CHUNK_OVERLAP_TOKENS=70
CHUNK_MIN_TOKENS=80
CHUNK_TOKEN_ENCODING=cl100k_base
Spustenie cez Docker
docker compose up -d --build
Kontrola služby:
curl http://127.0.0.1:8000/health
OpenAPI schéma:
http://127.0.0.1:8000/openapi.json
Logy:
docker compose logs -f zp-agent-api
Zastavenie:
docker compose down
Hugging Face cache je uložená v persistentnom Docker volume, takže sa embeddingový model pri bežnom reštarte kontajnera nemusí znova sťahovať.
Reindexovanie
Celý proces načíta dokumenty, vytvorí chunky, obnoví FTS5 index a vytvorí embedding pre každý chunk:
docker compose run --rm zp-agent-api python scripts/rebuild_index.py
Vzniknú súbory:
data/documents.json
data/chunks.json
data/zp_index.sqlite
Databáza obsahuje dokumenty, chunky, FTS5 index, metadata a embeddingy.
Pri automatickom reindexovaní sa používa zámok proti súbežnému spusteniu a atomická výmena databázy po úspešnom vytvorení nového indexu.
Hybridné vyhľadávanie
Vyhľadávanie kombinuje:
dotaz
├── FTS5 / BM25
└── embeddingové vyhľadávanie
↓
RRF fusion
↓
lexical anchor
↓
výsledky
FTS5 používa stratégie:
all_terms
prefix_terms
any_term
Presné lexikálne výsledky majú prednosť pred slabšími sémantickými
zhodami. Pri slabšej any_term stratégii sa znižuje jej váha a väčší
význam môže dostať embeddingové vyhľadávanie.
Test z terminálu:
docker compose run --rm zp-agent-api \
python scripts/search_db.py "rag agent" --limit 5
Pred volaním API načítaj premenné z .env:
set -a
source .env
set +a
Vyhľadávanie cez zabezpečené API:
curl -X POST http://127.0.0.1:8000/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SEARCH_API_KEY" \
-d '{
"query": "strojový preklad",
"limit": 3,
"published_only": false,
"max_per_document": 1
}'
API používa hybridný engine:
hybrid_fts5_embeddings
max_per_document=1 zabezpečí, že sa pri požiadavke na viac výsledkov
preferujú rôzne dokumenty namiesto viacerých chunkov z rovnakého
dokumentu.
RAG
Nad hybridným retrievalom je implementovaná RAG vrstva v
scripts/rag_utils.py.
Endpoint /rag vyhľadá relevantné dokumenty a pripraví:
- textový kontext pre jazykový model,
- metadata použitých zdrojov,
- názov a autora dokumentu,
source_url,- interné retrieval informácie,
- pravidlá pre grounded odpoveď.
RAG pravidlá požadujú, aby model odpovedal iba podľa poskytnutých zdrojov. Zároveň rozlišujú údaje, ktoré sa môžu ľahko zameniť, napríklad autora dokumentu, osobu, o ktorej dokument pojednáva, rok začiatku štúdia a rok záverečnej práce.
Ak sa relevantná informácia nenachádza priamo v primárnom chunke,
RAG vrstva môže k rovnakému zdroju doplniť začiatok rovnakej sekcie.
Táto section-lead expanzia umožňuje zachovať retrieval
max_per_document=1, ale zároveň doplniť názov témy, rok alebo inú
informáciu umiestnenú na začiatku sekcie.
Pri nedostatočnej podpore má model odmietnuť informáciu domýšľať.
Príklad:
curl -X POST http://127.0.0.1:8000/rag \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SEARCH_API_KEY" \
-d '{
"query": "V akom roku robil Ján Holp diplomovú prácu?",
"limit": 5,
"published_only": false,
"max_per_document": 1
}'
OpenWebUI
ZP Agent je pripojený do OpenWebUI ako OpenAPI Tool Server.
OpenWebUI používa OpenAPI schému ZP Agent API a Bearer autentifikáciu pre vyhľadávací/RAG nástroj.
Tok požiadavky:
používateľ
↓
OpenWebUI
↓
jazykový model
↓
ZP Agent /rag
↓
hybrid retrieval
↓
RAG kontext + zdroje
↓
jazykový model
↓
odpoveď
Výsledná odpoveď má byť grounded v ZP Wiki a má používať relevantný
source_url.
Retrieval evaluácia
Frozen retrieval benchmark sa spúšťa napríklad:
python evaluation/evaluate_retrieval.py \
--split dev \
--strict-dataset
Podporované režimy:
dev
test
all
Vyhodnocujú sa samostatne:
FTS
vector
hybrid
a metriky:
Hit@1
Hit@3
Hit@5
MRR
Recall@5
TEST split sa nemá používať na priebežné ladenie retrieval konfigurácie.
RAG answer evaluácia
Answer-level evaluácia používa:
python evaluation/evaluate_rag_answers.py --split dev
Evaluuje kompletný model/tool/RAG tok a od retrieval benchmarku je oddelená.
Ak je pôvodná otázka vhodná pre retrieval, ale nejednoznačná pre generatívnu odpoveď, jej answer-level formulácia sa upraví iba cez:
evaluation/json_files/rag_answer_overrides.json
Pôvodný frozen questions.json tým ostáva nezmenený.
Manuálna synchronizácia
Manuálne reindexovanie cez zabezpečený endpoint:
curl -X POST http://127.0.0.1:8000/sync \
-H "Content-Type: application/json" \
-H "X-API-Key: $SYNC_API_KEY" \
-d '{"pull_git": false}'
Gitea webhook
Endpoint:
POST /webhook/gitea
Webhook overuje:
- HMAC-SHA256 podpis,
- typ Gitea udalosti,
- očakávaný repozitár,
- maximálnu veľkosť payloadu,
- stav reindexovacieho zámku.
Očakávaný repozitár:
EXPECTED_GITEA_REPOSITORY=KEMT/zpwiki
Ak je povolené:
WEBHOOK_PULL_GIT=true
pred reindexovaním sa vykoná:
git pull --ff-only
Bezpečnosť
Vyhľadávanie podporuje API key:
X-API-Key: <SEARCH_API_KEY>
aj Bearer autentifikáciu:
Authorization: Bearer <SEARCH_API_KEY>
Endpoint /sync používa samostatný SYNC_API_KEY a webhook samostatný
WEBHOOK_SECRET.
Tajomstvá sa ukladajú do .env, ktorý nesmie byť súčasťou Git
repozitára.
Bezpečnostná vrstva zároveň kontroluje minimálnu dĺžku tajomstiev, odlišnosť jednotlivých secretov a používa bezpečné porovnávanie hodnôt.
Testy
Inštalácia testovacích závislostí:
pip install -r requirements-dev.txt
Bežné automatizované testy:
pytest -q
Kontrola syntaxe najdôležitejších upravovaných modulov:
python -m py_compile \
scripts/rag_utils.py \
evaluation/evaluate_rag_answers.py \
evaluation/rag_runner.py \
evaluation/rag_metrics.py
Pred checkpointom je vhodné použiť aj:
git diff --check
git status
Pevný počet pytest výsledkov nie je v README uvádzaný, pretože sa
s vývojom mení. Aktuálny stav sa má vždy potvrdiť novým spustením testov
na konkrétnom commite.
Pripravené, ale zatiaľ neaktívne experimenty
Nasledujúce datasety sú pripravené, ale nemajú sa teraz používať na priebežné ladenie aktuálneho frozen benchmarku:
questions_extended_2000.json
graphrag_questions.json
robustness_questions.json
performance_queries.json
Odporúčané poradie ich neskoršieho použitia:
stabilný klasický RAG
↓
extended benchmark
↓
robustness benchmark
↓
GraphRAG implementácia
↓
GraphRAG benchmark
↓
performance benchmark
↓
stress test
↓
finálne experimenty
Najbližší postup
Najbližšia vývojová etapa je zámerne menšia a má uzavrieť existujúci klasický RAG pred otvorením ďalších experimentov.
1. Uzavretie retrieval/regresie
Najprv sa má potvrdiť, že aktuálne zmeny neovplyvnili frozen retrieval baseline.
Postup:
py_compile
↓
pytest
↓
frozen retrieval DEV strict
↓
porovnanie s uloženým baseline
↓
git diff --check
↓
retrieval lock
Do retrieval scoringu sa potom nemá zasahovať bez nového experimentálneho dôvodu.
2. Regresia section-lead RAG kontextu
Treba potvrdiť, že section-lead expanzia:
- opravuje prípady, kde primárny chunk chýba o názov/tému/rok,
- nepridáva duplicitný chunk,
- zostáva v rovnakom dokumente a sekcii,
- nemení retrieval
max_per_document=1, - nevytvára zbytočne veľký kontext.
3. retry + backoff + resume pre answer evaluator
Pred spustením veľkého DEV answer benchmarku sa má evaluator doplniť tak, aby dlhý beh nebol znehodnotený jedným timeoutom alebo dočasnou chybou API.
Plánované správanie:
request
↓
úspech ───────────────→ uložiť výsledok
│
└─ timeout / 429 / 5xx
↓
retry
↓
exponential backoff
↓
retry limit
Resume mechanizmus má:
- priebežne ukladať
.partial.json, - pri novom spustení načítať existujúci partial výsledok,
- overiť kompatibilitu modelu/datasetu/splitu,
- preskočiť už úspešne dokončené otázky,
- pokračovať od ďalšej otázky,
- neprepisovať hotové výsledky bez explicitnej voľby,
- po úspešnom dokončení vytvoriť finálny JSON/CSV výstup.
Zároveň je vhodné:
- rozlišovať retryable a permanentné chyby,
- logovať číslo pokusu a dôvod retry,
- mať konfigurovateľný maximálny počet pokusov,
- mať konfigurovateľný počiatočný backoff,
- používať mierny delay medzi otázkami,
- validovať typy answer-level datasetových polí,
- nenačítavať osobný OpenWebUI API kľúč z fallback súboru, ak má byť podľa bezpečnostnej politiky dostupný iba cez environment.
Až po tejto etape má zmysel spustiť väčší DEV answer experiment a robiť systematickú error analysis.