přestaňte tvrdit, že je to náhoda
O halucinacích se často mluví jako o nějakém podivném chování modelu, jako by se systém špatně vyspal a z ničeho nic rozhodl, že vaše faktura má hodnotu 17 430 EUR místo 1 743,00 EUR. Tenhle framing je líný a týmy kvůli němu končí u špatné architektury. Jakmile si namluvíte, že je chyba záhadná, začnete přidávat delší prompty, víc příkladů, víc retry a další drahé nesmysly, místo abyste měřili místa, kde vám model úplně zjevně říká, že si není jistý.
Selhání v produkci obvykle mají nějaký tvar. Vidíme to v document pipelines, CRM asistentech i AI sales workflow, které ve Steezru stavíme. Nebezpečné výstupy se většinou točí kolem stejných vzorců: nepodložené propojování entit, vymyšlené citace, přehnaně sebejistá normalizace špinavého vstupu a action requesty, které nenápadně podsouvají předpoklady, jež retriever vůbec nenašel. Nic z toho není náhoda. Model generuje oblasti s nízkou confidence na úrovni tokenů, chvíli začne opatrně kličkovat a pak se vrátí k sebejistému textu, nebo odkazuje na chunky, které v context window nikdy nebyly. Tyhle signály umíte chytat levně, když přestanete odpověď brát jako posvátný blob textu a začnete generování brát jako telemetry.
Praktická teze je jednoduchá: logujte confidence na úrovni tokenů tam, kde to API umí, ukládejte provenance pro každý retrieved chunk, nad finální sadou claimů pusťte malý verifier nebo re-ranker a pro úlohy, které LLM nikdy nepotřebovaly, nechte deterministické cesty. Jestli jen taháte datum faktury z fixního PDF template, použijte pdfplumber, pymupdf, regexy a validaci vůči schématu. Jestli odpovídáte na fuzzy dotaz nad deseti policy dokumenty, pak ano, použijte model, ale nenechte ho nic udělat bez levného checkpointu.
Většina týmů checkpoint přeskočí. Pak jsou překvapené, když si asistent vymyslí renewal clause.
token stream je telemetry
Jestli už dnes streamujete výstup modelu a používáte to jen proto, aby UI působilo živěji, zahazujete užitečný signál. Stream generování vám může říct, kde model začal hádat, kde se JSON objekt odchýlil od schématu a kde je vysoká šance, že citace byla slepená z okolního kontextu místo toho, aby byla převzatá z evidence. Nejviditelnější feature jsou token-level log probabilities, pokud je provider pro daný model vůbec nabízí. Podpora se liší, takže pipeline nestavte na tvrdé závislosti, ale na volitelném enrichmentu.
Praktický pattern s OpenAI Python SDK je streamovat odpověď kvůli latenci a pak si, kde to jde, na stejné completion cestě vyžádat detaily o tokenech. Když endpoint logprobs v režimu, který potřebujete, nevrací, udělejte levný sekundární scoring pass nad vygenerovanou odpovědí. Konkrétní API surface se mění, o důvod víc schovat to za vlastní adapter. Logika zůstává stejná. Agregujte logprob po tokenech, počítejte rolling windows a označujte spany, kde průměr spadne pod threshold a text zároveň obsahuje riskantní struktury, třeba částky, data, právní klauzule, databázové identifikátory nebo citace.
Zjednodušený nástřel může vypadat takhle:
1from openai import OpenAI2client = OpenAI()34resp = client.responses.create(5 model="gpt-4.1-mini",6 input=[7 {"role": "system", "content": "Answer using provided evidence only."},8 {"role": "user", "content": question}9 ],10 stream=False,11 logprobs=True12)1314text = resp.output_text15tokens = []16for item in resp.output:17 if getattr(item, "type", None) == "output_text":18 for t in getattr(item, "logprobs", []) or []:19 tokens.append({"token": t.token, "logprob": t.logprob})2021def suspicious_spans(tokens, threshold=-2.5, window=6):22 out = []23 for i in range(len(tokens) - window + 1):24 avg = sum(t["logprob"] for t in tokens[i:i+window]) / window25 if avg < threshold:26 out.append((i, i + window, avg))27 return out
Thresholdy musíte kalibrovat na vlastních datech. -2.5 může být u jednoho modelu příliš hlučné a u jiného moc benevolentní. Udělejte si označený dataset dobrých a špatných odpovědí a pak si vykreslete false positives proti ceně incidentu. Nejde o akademickou jistotu. Jde o to zachytit ošklivé případy dřív, než z nich budou support tickety nebo tichá korupce dat v databázi.
Ještě jedna věc: ukládejte raw token trace spolu s finální odpovědí, verzí promptu, verzí modelu a retrieval ids. Pokud si neumíte špatné generování později přehrát, nemáte observability. Jen pocit, že to snad nějak funguje.
bez provenance se nic nestalo
Retrieval-augmented generation se rozpadne ve chvíli, kdy provenance evidence splácnete do jednoho context stringu. Viděl jsem týmy, které vezmou deset chunků, slepí je přes \n\n---\n\n, pošlou do promptu a pak po modelu chtějí citace. To je divadlo. Jakmile smažete identitu chunků, čísla stránek, section ids a retrieval scores, downstream už neověříte nic, leda že celou pipeline pustíte znovu a budete doufat, že retriever vrátí stejné pořadí.
Každou jednotku evidence držte jako first-class object. To znamená chunk_id, id zdrojového dokumentu, locator stránky nebo řádku, retrieval score, verzi embedding modelu, checksum podkladového textu a ideálně i stabilní offset spanu, pokud zdrojový dokument ovládáte. Generátor odpovědi by měl vracet strukturované claimy navázané na evidence ids, ne volný text s okrasnými citacemi. Donuťte model emitovat něco jako:
1{2 "claims": [3 {4 "text": "The contract renews automatically for 12 months.",5 "evidence_ids": ["doc_184:p12:c03"],6 "risk": "high"7 }8 ]9}
Pak ověřte, že každé evidence_id existuje, že citovaný chunk opravdu obsahuje lexikální překryv nebo sémantickou oporu pro claim a že chunk pochází z aktuální retrieval sady, ne ze zastaralé cache z včerejšího indexu. Je to bolestivě neatraktivní práce. Současně je to přesně ten rozdíl mezi demem a produkčním systémem.
Pro document workflows mám rád přístup se dvěma tabulkami v PostgreSQL. Jedna tabulka drží kanonické chunky se source metadaty a hashem textu. Druhá ukládá retrieval eventy pro každý request, včetně pořadí, score, query rewrite a verze promptu. Když se uživatel zeptá, proč asistent tvrdil, že vendor má net 30 terms, odpovíte přesným audit trailem místo toho, abyste čtyřicet minut zírali do LangSmith traces.
Tahleta provenance vrstva navíc odemyká deterministické kontroly. Když model tvrdí, že total na faktuře pochází z invoice_882:p1:c2 a ten chunk neobsahuje pattern měny odpovídající r"\b(?:EUR|USD|CZK)\s?\d+[\d.,]*\b", odmítněte to. Levné, nudné, účinné.
přidejte levného soudce
Hlavní model nemá být poslední autorita v tom, jestli je jeho vlastní odpověď podložená. Self-grading funguje právě tak dobře, aby lidi zmátl v benchmarcích, a právě tak špatně, aby vás spálil v produkci. Přidejte druhý krok. Contrastive re-ranker, malý verifier model, nebo obojí.
Contrastive re-ranking se hodí na zúžení evidence před generováním nebo před schválením sady citací, které model tvrdí. Cross-encodery jako bge-reranker-large nebo ms-marco-MiniLM-L-6-v2 jsou pořád užitečné, zvlášť když jedete na vlastním hardwaru a chcete předvídatelné náklady. Pro každý claim ohodnoťte kandidátní chunky proti textu claimu a pak porovnejte nejlepší podpůrný chunk s chunkem, který model citoval. Když citovaný chunk není nahoře nebo aspoň blízko, označte claim jako slabý. Je to jeden pass, dost levný na spuštění pro každou odpověď, a podstatně spolehlivější než věřit tomu, že generátor správně naformátoval citaci.
Pak přijde na řadu lehký verifier model. Může to být další LLM call, držený úzce a strukturovaně, nebo fine-tuned classifier, pokud máte stabilní doménu. Ptejte se jen na jednu věc: plyne claim X z evidence Y? Použijte labely jako supported, contradicted, insufficient. Teplotu držte na 0, vyžadujte JSON a při chybě parsování failněte natvrdo. Prompt pro verifier má být brutálně konkrétní:
1{2 "task": "Assess support for a claim from evidence.",3 "labels": ["supported", "contradicted", "insufficient"],4 "claim": "The SLA guarantees 99.95% uptime.",5 "evidence": "Section 4.2: Service availability target is 99.9% per calendar month.",6 "rules": [7 "Do not use outside knowledge.",8 "Numeric mismatches are contradictions.",9 "If evidence is incomplete, return insufficient."10 ]11}
Jestli chcete praktické pravidlo pro thresholding, začněte třeba takhle: zablokujte jakoukoliv akci, pokud je některý high-risk claim označený jako contradicted nebo pokud je víc než 20 % claimů insufficient. Doladíte to později. První den nepotřebujete eleganci. Potřebujete gate, která zastaví očividný odpad.
Týmy se bojí, že tenhle extra pass zhorší latenci. Ve skutečnosti to obvykle přidá pár stovek milisekund, což je pořád levnější než poslat špatný refund email, přepsat špatné pole v CRM nebo zákazníkovi tvrdit, že mu smlouva končí příští týden, když se ve skutečnosti automaticky prodloužila o rok.
deterministické cesty často vyhrávají
Depresivně velká část návrhu LLM workflow je ve skutečnosti jen neochota přiznat si, že potřebujete parser. Když data už leží v PostgreSQL, neptejte se modelu na quota attainment obchodníka, napište SQL. Když zákaznický portál potřebuje z uploadů vytáhnout čísla pojistek, definujte schéma a nejdřív použijte deterministickou extrakci. Do modelu pošlete až nejednoznačná pole. Dostanete nižší náklady, míň incidentů a čistší debugging.
Tohle rozdělená architektura zvládá dobře. Dejte před to orchestrator, klasifikujte úlohu a routujte ji do jedné ze tří cest: deterministický dotaz, deterministický parser plus validátor, nebo retrieval plus generation plus verification. Tohle děláme často s Django backendy a Next.js frontendy, protože většina uživatelských requestů v interních nástrojích je dost repetitivní na to, aby šla spolehlivě klasifikovat. Dotaz jako „ukaž neuhrazené faktury za únor pro Acme“ se má změnit na parametrizovaný query nad známým view. Request typu „shrň riziko sporů v těchto šesti smlouvách s dodavateli“ patří na LLM cestu.
Guardrails jsou důležité i na hraně, kde se dělá akce. Když model navrhne SQL query, spouštějte ho jen proti omezené read-only roli, parsujte AST přes sqlglot nebo sqlparse a write operace rovnou odmítejte. Když vrací JSON pro automatizační krok, validujte ho pomocí Pydantic v2 a ukažte přesnou chybu. Konkrétní error pomáhá. Tohle je užitečné:
11 validation error for LeadUpdate2next_contact_date3 Input should be a valid date or datetime, input_value='next Thursday-ish', input_type=str
Tohle ne:
1Something went wrong processing your request.
Deterministické fallbacky navíc drží incident response při smyslech. Když se po update providera začne rozpadat modelová cesta, můžete rozumně degradovat na search results, raw evidence snippets nebo parser-only režim, místo abyste celý feature vypnuli. Uživatelé snesou hrubší odpověď. Vymyšlenou nesnesou.
zapojte to jako produkční systém
Pipeline, kterou bych dnes shipnul pro produkčního asistenta nebo document workflow, je přímočará. A ano, přímočaré řešení skoro vždycky porazí chytráctví.
Začněte klasifikací requestu. Rozhodněte, jestli jde o čistý retrieval, extrakci, transformaci, nebo action request. Kde to jde, pusťte nejdřív deterministické handlery. U requestů vhodných pro LLM nejdřív vyhledejte evidence se stabilními chunk ids a top sadu pak přerovnejte pomocí cross-encoderu. Generujte strukturovanou odpověď, ne nejdřív prózu, s claim objekty navázanými na evidence ids. Během generování sbírejte token-level signály tam, kde jsou podporované, a pak nad spany s rizikovými entitami spusťte anomaly checks. Každý claim a jeho citovanou evidenci pošlete do verifieru. Když verification projde, vyrenderujte odpověď pro uživatele nebo proveďte omezenou akci. Když neprojde, degradujte na citovanou evidenci, zeptejte se na upřesnění, nebo přepněte na deterministickou cestu.
Logování potřebuje stejnou disciplínu jako payment kód. Ukládejte request_id, user id, jméno modelu, id odpovědi od providera, verzi prompt template, ids retrieved chunků, score z re-rankeru, labely verifieru, metriky tokenových anomálií a finální disposition. Všechno držte queryovatelné. Jednoduché schéma v PostgreSQL stačí, pokud už nejste hluboko v nějakém observability stacku. Na odpověď na otázku „která verze promptu začala po změně velikosti chunku z 600 na 1200 tokenů vyrábět vymyšlená data zrušení?“ opravdu nepotřebujete deset vendorů.
Jeden provozní detail lidem často uteče: verzujte každý prompt i každé schéma. Pokud generátor ve v12 emituje claims[].evidence_ids[] a ve v13 claims[].citations[], verifier i dashboardy musí vědět, co zrovna čtou. My jsme se to naučili tou otravnou cestou na document processing pipeline, kde drobná změna schématu na dva dny tiše obešla citation checker. Nic nevybuchlo, protože deterministická action gate zablokovala updaty bez evidence, což je přesně ten druh nudné pojistky, který chcete mít před launchí hotový.
Berte halucinace jako měřitelná selhání, stejně jako timeouty, deadlocky nebo cache stampedes. Měřte je, stavte před ně gate a podle potřeby je obcházejte jinou cestou. Týmy, které tohle dělají, shipují rychleji. Hlavně proto, že nemusí každý sprint vést stejnou debatu o tom, jestli se modelu dá věřit.
