216 lines
11 KiB
Markdown
216 lines
11 KiB
Markdown
# wiki — jak to funguje
|
||
|
||
Hybrid hledání ve tvých vlastních poznámkách: dva git repozitáře (`index`, `travel`) plus živý
|
||
workspace nanobota. Skill je **výhradně čtecí** — do tvých repů nikdy nezapisuje a klony drží
|
||
jen jako pracovní kopii. Index je derived artifact: kdykoli se dá smazat a postavit od nuly
|
||
(u dnešního objemu jsou to desítky sekund).
|
||
|
||
## Tři vrstvy, od nejlevnější
|
||
|
||
```text
|
||
grep ripgrep přes soubory na disku žádný index, nikdy zastaralé, vidí i kód
|
||
↓
|
||
toc katalog `files` z databáze adresář → soubor → titulek + tagy
|
||
↓
|
||
search FTS5 (BM25) + vec0 (KNN) → RRF parafráze, když neznáš slova
|
||
```
|
||
|
||
Není to fallback řetěz, kde se jde dolů, když horní vrstva zklame — je to volba podle **tvaru
|
||
otázky**: přesný string, hostname nebo identifikátor patří do `grep`, orientace („co o tom
|
||
vůbec mám?") do `toc`, parafráze do `search`.
|
||
|
||
`grep` je tedy základ, ne poslední záchrana. Soubory na disku beztak leží, takže nestojí nic
|
||
navíc, a nad kódem je exact match to hlavní — proto se zdrojáky vůbec neindexují (viz níž).
|
||
|
||
## Index nikdy nevzniká v tahu agenta
|
||
|
||
Indexuje cron každou minutu. Agent v tahu jen **čte** hotový index.
|
||
|
||
Proč takhle: `exec` tool má timeout 60 s a plný index je minuty. Indexace v odpovědi by
|
||
navíc při nedostupné Ollamě dělala z čerstvého dokumentu FTS-only výsledek.
|
||
|
||
Drtivá většina tiků neudělá nic a **nezapíše ani řádek**. Když je `log/wiki_sync_cron.log`
|
||
prázdný, je to správný stav, ne že cron neběží.
|
||
|
||
## Co se v tom tiku vlastně děje
|
||
|
||
1. Vezme lock `wiki/.sync.lock`. Když ho drží živý běh, skončí bez výpisu. Lock po mrtvém
|
||
procesu (`pid` neexistuje nebo je starší než 30 min) si vezme zpátky a zaloguje
|
||
`WARN stale lock, reclaiming`.
|
||
2. **Levná detekce změn, bez indexace.** U git zdrojů `git ls-remote` — zjistí remote `HEAD`
|
||
**bez** `fetch`, což je na minutovou kadenci ten správný nástroj; `fetch` teprve když se
|
||
revize liší. U workspace zdroje walk a porovnání `path` + `size` + `mtime`; sha256 se
|
||
počítá jen při neshodě.
|
||
3. Nic se nezměnilo a nic nečeká na vektor → konec.
|
||
4. Změněné soubory se rozřežou na chunky a pošlou do Ollamy po 32. Pak se **doberou všechny
|
||
chunky bez vektoru**, i když se žádný soubor nezměnil — tohle je cesta zpátky
|
||
z degradovaného režimu, když byla Ollama chvíli mimo.
|
||
5. Zapíše `indexed_rev` a `last_sync_at`, přidá coverage řádek a shrnutí do
|
||
`log/wiki_sync.log`.
|
||
|
||
Selhání sítě je **per zdroj**: timeout nebo chyba gitu zaloguje `WARN`, ten zdroj přeskočí
|
||
s nezměněnou `indexed_rev` a ostatní dojedou. Bez timeoutů by visící `ls-remote` držel lock
|
||
a zablokoval i workspace zdroj, který se sítí nemá nic společného.
|
||
|
||
## Proč hybrid
|
||
|
||
Každá polovina umí něco jiného a ani jedna neumí obojí:
|
||
|
||
| | Umí | Neumí |
|
||
|---|---|---|
|
||
| **BM25** (FTS5) | exact match, čísla, jména, zkratky | parafrázi, kde se slova nepřekrývají |
|
||
| **vektory** (`vec0`) | „elektroodpad ze stavebnic" → „Mindstorms po ukončení podpory" | přesné řetězce a čísla |
|
||
|
||
Výsledky slučuje **RRF** — sečte převrácené ranky z obou seznamů. Aby to mělo definovaný
|
||
význam, musí obě poloviny řadit **tutéž množinu**: proto je jedinou retrieval jednotkou chunk,
|
||
ne soubor. Kdyby BM25 řadil soubory a vektory chunky, merge by nešlo interpretovat.
|
||
|
||
Naměřeno na reálných poznámkách: RRF dává MRR 0,367 proti 0,257 (BM25 sám) a 0,261 (vektory
|
||
samy), takže merge opravdu vyhrává. Plná čísla, včetně srovnání čtyř modelů, jsou
|
||
v `.claude/tracking/plans/final-wiki-hybrid-rag-result.md` v trackovacím repu.
|
||
|
||
Ve výstupu `search` u každého hitu stojí, která polovina ho našla (`bm25 #2, vec #1`). Hit,
|
||
na kterém se poloviny **shodnou**, má výrazně větší cenu než ten, který vytáhly jen vektory.
|
||
|
||
## Česká flexe — na co narazíš
|
||
|
||
Dotazová vrstva lepí `*` na slova od tří znaků, aby pokryla české koncovky. **Funguje to jen
|
||
napůl** a je dobré vědět jak: wildcard je **prefixový**, takže pomůže jen tehdy, když je tvoje
|
||
slovo prefixem tvaru v dokumentu. Česká flexe ale mění koncovku, ne začátek.
|
||
|
||
Naměřeno nad `travel/packaging-list.md`:
|
||
|
||
| Dotaz | Chunků v tom souboru |
|
||
|---|---|
|
||
| `cestu*` | **0** |
|
||
| `cesty*` | 3 |
|
||
| `cest*` | 4 |
|
||
|
||
Prakticky: na *„co si vzít na cestu do zahraničí"* se doslovný seznam věcí na cestu nedostal
|
||
ani do top 10. Když víš, že něco máš, a `search` to nenajde, zkus **kmen slova** (`cest`,
|
||
`záloh`) nebo rovnou `grep`. Rozhodnutí, jestli to řešit systémově, je otevřená položka
|
||
v `todo.md` trackovacího repa — každá varianta rozšiřuje kandidátní pool, takže to není
|
||
zdarma.
|
||
|
||
## Index je nápověda, ne odpověď
|
||
|
||
Dvě věci, které se snadno přehlédnou:
|
||
|
||
- **Výřez v `search` není zdroj pravdy.** Je zkrácený a index může být o minutu starší než
|
||
disk. Než se z nálezu odpovídá, má se ten soubor přečíst čerstvý — `SKILL.md` to agentovi
|
||
říká, ale platí to i pro tebe.
|
||
- **Vektorová polovina vždycky něco vrátí.** KNN nemá dolní mez podobnosti, takže na jakýkoli
|
||
dotaz nabídne svých k nejbližších chunků. Pro retrieval je to záměr (posoudit relevanci je
|
||
na čtenáři), ale znamená to, že prázdný výsledek nikdy neznamená „nic nesedí" — znamená
|
||
prázdný index.
|
||
|
||
## Co se indexuje a co ne
|
||
|
||
Rozsah řídí `wiki/config.yaml` ve třech krocích: `paths` (whitelist — co v něm není, pro index
|
||
neexistuje) → `include` (whitelist přípon, `*.md`) → `exclude` (skalpel, vyhrává nad oběma).
|
||
|
||
Proč whitelist a ne blacklist: rozhoduje **směr selhání**. U blacklistu nový adresář *tiše
|
||
vstoupí* do indexu, u whitelistu *tiše chybí*. Index je komprimovaná kopie obsahu včetně
|
||
osobních věcí, takže „tiše zaindexováno" je horší porucha — a workspace se mění i bez tebe,
|
||
Dream do něj zapisuje sám.
|
||
|
||
Proti té jediné slabině whitelistu stojí záchranná síť: sync na konci zaloguje řádek
|
||
`coverage <zdroj>: markdown outside paths in …` s top-level adresáři, které markdown mají
|
||
a žádný zdroj je nepokrývá. Adresáře, které tam vidíš pořád (`backup`, `tmp`, `skills`, …),
|
||
jsou vědomě vynechané; zajímavý je **nový** přírůstek v tom seznamu.
|
||
|
||
**Indexuje se výhradně markdown**, a to bez jakékoli detekce typu obsahu — `main.py` se
|
||
netrefí do `include: ["*.md"]` a k chunkeru se nedostane. Důvod je měřený: markdown parser
|
||
dělá z Python komentáře `# TODO: …` nadpis a `unicode61` neumí identifikátory (`send` uvnitř
|
||
`SendAsync` je miss). Kód pokrývá `grep` — jede přes celý klon včetně `.txt` a `.mhtml`.
|
||
|
||
## Kde co leží
|
||
|
||
Relativně ke workspace (`~/.nanobot/workspace`):
|
||
|
||
| Cesta | Role |
|
||
|---|---|
|
||
| `wiki/config.yaml` | zdroje a model — **jediný verzovaný soubor** pod `wiki/` |
|
||
| `wiki/index.sqlite` | `chunks` + FTS5 + vektory; derived, gitignorovaný |
|
||
| `wiki/remote/<id>/` | non-bare klony git zdrojů (working tree, aby měl `grep` co číst) |
|
||
| `wiki/.sync.lock` | JSON `{pid, started_at}`, jen když sync běží |
|
||
| `log/wiki_sync.log` | jeden řádek na událost — co se naindexovalo, `WARN`, coverage |
|
||
| `log/wiki_sync_cron.log` | stdout cronu; **prázdný je správně** |
|
||
| `skills/wiki/scripts/` | `wiki_sync.py` (indexace) a `wiki_search.py` (dotazy) |
|
||
|
||
Klon je záměrně non-bare: `--mirror` nemá working tree, takže by `grep` neměl co číst.
|
||
Cena je dvojnásobek místa na disku, což je na těchhle repech nic.
|
||
|
||
## Ruční spuštění
|
||
|
||
```bash
|
||
cd ~/.nanobot/workspace
|
||
|
||
# to, co dělá cron
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py
|
||
|
||
# jen jeden zdroj
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --source travel
|
||
|
||
# smazat index a postavit od nuly
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_sync.py --full
|
||
|
||
# dotazy
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zálohování" --limit 5
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --source travel
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py toc --tag caj
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py grep "WireGuard" --source index
|
||
```
|
||
|
||
`--full` potřebuješ jen po změně embedding modelu, jeho dimenzí, `query_prefix` nebo chunkeru
|
||
— tedy přesně tehdy, když dotaz začne hlásit `reindex needed`. Přeembeduje všechno, takže to
|
||
nepouštěj v tahu, kde na to někdo čeká.
|
||
|
||
Plná cesta k `uv` je tu proto, že v neinteraktivním SSH není v `PATH`. Crontab si `PATH`
|
||
nastavuje sám, takže tam stačí `uv run …`.
|
||
|
||
## Když něco nefunguje
|
||
|
||
| Hláška | Co to je |
|
||
|---|---|
|
||
| `note: embeddings unavailable … FTS-only results` | Ollama na `nvidia.hell` neodpovídá. Sync mezitím indexuje dál a vektory dosadí sám, až se vrátí. |
|
||
| `note: N chunks still awaiting vectors` | Sync je rozjezdu nebo byl degradovaný. Sémantická polovina je zatím neúplná. |
|
||
| `reindex needed: …` (exit 1) | Index byl postavený pod jiným embedding kontraktem než má config. Poznámky jsou v pořádku, řeší to `--full`. |
|
||
| `WARN <zdroj>: git … failed` | Ten zdroj se přeskočil, ostatní dojely. `indexed_rev` zůstala, takže příští tik to zkusí znovu. |
|
||
| `(no matches)` | Prázdný index — ne „nic nesedí" (viz výš). |
|
||
|
||
Všechno ostatní hledej v `log/wiki_sync.log`. Když je prázdný i po změně poznámek, problém je
|
||
v cronu, ne v hledání.
|
||
|
||
## Jak ověřit, že to funguje
|
||
|
||
**Smoke loop v konzoli** — projde celou smyčku od zápisu po nalezení:
|
||
|
||
```bash
|
||
cd ~/.nanobot/workspace
|
||
printf '# Smoke\n\nZrzavý jednorožec kontroluje wiki index.\n' > notes/_smoke.md
|
||
sleep 90
|
||
~/.local/bin/uv run skills/wiki/scripts/wiki_search.py search "zrzavý jednorožec" --limit 3
|
||
rm notes/_smoke.md # cron ho z indexu odklidí sám do minuty
|
||
```
|
||
|
||
**Dotaz do WebUI na každý zdroj.** Princip: vyber fakt, který model **nemůže** znát
|
||
z obecných znalostí — pak je konkrétní správná odpověď sama důkazem, že přišla z poznámek.
|
||
Když odpoví obecně a čísla vynechá nebo si je vymyslí, neprošel.
|
||
|
||
Příklady platné k 2026-09-09 (poznámky se mění, princip ne):
|
||
|
||
| Zdroj | Dotaz | Musí padnout |
|
||
|---|---|---|
|
||
| `workspace` | Kolik vody a čaje mám v receptu na Teh Tarik? | 700–800 ml, dvě polévkové lžíce čaje |
|
||
| `travel` | Co si mám podle poznámek vzít na Sněžku? | rozdvojka do zásuvky, power banka, Sony ANC sluchátka |
|
||
| `index` | Co je projekt Sauter Bus? | RS485 proxy s odposlechem a modifikací provozu, micro:bit |
|
||
|
||
**Negativní kontrola** je nejcennější z celé sady: zeptej se na téma, které v poznámkách
|
||
prokazatelně není (např. pěstování bonsají). Správná odpověď je „nic o tom nemáš". Když
|
||
začne vyprávět, doplňuje si výsledky z obecných znalostí — a to je zrádnější vada než
|
||
nefunkční index, protože se tváří jako odpověď z poznámek.
|
||
|
||
Když si nejsi jistý, odkud odpověď je, dopiš **„ze kterého souboru to je?"**. Má přijít
|
||
konkrétní cesta, ne „z tvých poznámek".
|