Normal path: execute â tool â observe.
Problem (aus der Praxis)
Dein Agent liefert eine âsauber belegteâ Antwort.
Dann klickt jemand die Quellen.
Ein Link ist 404. Ein anderer ist thematisch daneben. Der dritte ist ein PDF mit 120 Seiten â aber die Antwort kam in 6 Sekunden zurĂŒck.
GlĂŒckwunsch: du hast einen Credibility Bug shippt.
In Production ist das nicht nur peinlich. Es ist teuer:
- Support/Vertrauen bekommen einen Schlag (âeure Doku ist fakeâ).
- Legal/Compliance ist plötzlich im Raum, wenn du Policies oder Regs zitierst.
- Du verbringst Stunden mit âCitation Archaeologyâ in Logs, die es nicht gibt.
Diese Failure Mode taucht auf, sobald du das Modell nach âQuellenâ fragst, ohne eine harte Definition zu geben, was als Quelle zĂ€hlt.
Warum das in Production bricht
Halluzinierte Citations sind kein Zauber. Es ist die erwartbare Folge von Agent-Architektur.
1) Das Modell ist optimiert, hilfreich auszusehen â nicht auditierbar zu sein
Wenn der Prompt sagt âinclude sourcesâ, liefert das Modell Sources. Auch wenn es keine hat. Es erfindet plausible Deko:
- eine Domain, die ârichtig klingtâ
- einen URL-Pfad, der real aussieht
- einen Titel, der existieren sollte
Es âlĂŒgtâ nicht absichtlich. Es fĂŒllt die Form, die du verlangst.
2) âSearch Resultsâ sind keine Evidence
Viele Agents machen:
search.read("x")- Titles + URLs
- Antwort mit Citations
Aber: der Agent hat die Seiten nicht gefetcht. Er kennt den Inhalt nicht. Er kennt nur, was der Snippet behauptet.
Wenn du das als Evidence akzeptierst, zitierst du Dinge, die du nie gelesen hast. Weil du sie nie gelesen hast.
3) Evidence geht zwischen Steps verloren
Selbst wenn du fetcht, verlierst du Evidence oft:
- Tool Output wird nicht gespeichert, nur zusammengefasst
- Kontext wird truncatât
- Retries Àndern Reihenfolge
- spĂ€tere Steps ĂŒberschreiben Sources
Wenn du nicht traceân kannst âdiese Aussage kommt aus Snapshot Xâ, hast du keine Citations. Du hast Dekoration.
4) âCite sourcesâ ist Policy. Policy enforceât sich nicht.
Du kannst dich nicht in Auditability âpromptenâ. Du brauchst Enforcement in Code:
- Sources mĂŒssen aus Tool Outputs kommen, die dein System gespeichert hat
- Citations mĂŒssen auf diese Snapshots referenzieren
- Outputs ohne valide Citations mĂŒssen failen (oder degradieren)
So soll die Pipeline aussehen:
Implementierungsbeispiel (echter Code)
Der sicherste Pattern, den wir gefunden haben:
- Citations sind IDs, nicht URLs
- nur Citations auf snapshottete Tool Outputs
- optional: Excerpt-Hash pro Citation
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import time
from typing import Any
@dataclass(frozen=True)
class Evidence:
source_id: str
url: str
fetched_at: float
title: str
text_sha256: str
class EvidenceStore:
def __init__(self) -> None:
self._items: dict[str, Evidence] = {}
def add(self, *, url: str, title: str, text: str) -> str:
sha = hashlib.sha256(text.encode("utf-8")).hexdigest()
source_id = f"src_{len(self._items)+1:03d}"
self._items[source_id] = Evidence(
source_id=source_id,
url=url,
fetched_at=time.time(),
title=title,
text_sha256=sha,
)
return source_id
def has(self, source_id: str) -> bool:
return source_id in self._items
def meta(self, source_id: str) -> Evidence:
return self._items[source_id]
def verify_citations(*, cited_source_ids: list[str], store: EvidenceStore) -> None:
missing = [s for s in cited_source_ids if not store.has(s)]
if missing:
raise ValueError(f"invalid citations (unknown source_ids): {missing}")
def answer_with_citations(task: str, *, store: EvidenceStore) -> dict[str, Any]:
# In real code: the model returns structured output.
# Example shape:
# { "answer": "...", "citations": ["src_001", "src_002"] }
out = llm_answer(task) # (pseudo)
verify_citations(cited_source_ids=out["citations"], store=store)
return out
def render_sources(cited_ids: list[str], store: EvidenceStore) -> list[dict[str, str]]:
sources: list[dict[str, str]] = []
for sid in cited_ids:
ev = store.meta(sid)
sources.append(
{
"source_id": sid,
"title": ev.title,
"url": ev.url,
"sha256": ev.text_sha256[:12],
}
)
return sourcesimport crypto from "node:crypto";
export class EvidenceStore {
constructor() {
this.items = new Map();
}
add({ url, title, text }) {
const sha = crypto.createHash("sha256").update(text, "utf8").digest("hex");
const sourceId = "src_" + String(this.items.size + 1).padStart(3, "0");
this.items.set(sourceId, { sourceId, url, title, fetchedAt: Date.now(), textSha256: sha });
return sourceId;
}
has(sourceId) {
return this.items.has(sourceId);
}
meta(sourceId) {
const ev = this.items.get(sourceId);
if (!ev) throw new Error("unknown source_id: " + sourceId);
return ev;
}
}
export function verifyCitations({ citedSourceIds, store }) {
const missing = citedSourceIds.filter((s) => !store.has(s));
if (missing.length) throw new Error("invalid citations (unknown source_ids): " + missing.join(", "));
}
export function renderSources(citedIds, store) {
return citedIds.map((sid) => {
const ev = store.meta(sid);
return { source_id: sid, title: ev.title, url: ev.url, sha256: ev.textSha256.slice(0, 12) };
});
}Was du dadurch bekommst:
- Citations können nicht auf erfundene URLs zeigen
- du kannst Answers spĂ€ter reproduzieren (âhier ist der Snapshot-Hashâ)
- du kannst fail-closed, wenn Citations nicht passen
Wenn duâs hĂ€rter willst: Excerpt-Hash (oder Quote) pro Claim. Langsamer, aber schwerer zu faken.
Echter Incident (mit Zahlen)
Wir hatten einen âinternal research agentâ, der wöchentlich Competitive Summaries schrieb. Er sollte âinclude sourcesâ.
Was wirklich passierte:
- credible-looking URLs
- diese URLs wurden nie gefetcht
- zwei Links waren tot
- einer war eine komplett unpassende Pressemitteilung
Impact:
- ein PM forwardete das Doc an einen Partner (aua)
- ~6 Engineer-Stunden fĂŒr Reconstruction, was ĂŒberhaupt passiert ist
- Vertrauen war weg (âcool demo, aber ich kannâs nicht nutzenâ)
Fix:
- Sources wurden
source_ids, gebunden an Tool Snapshots - âSearch Resultsâ zĂ€hlen nicht mehr als Evidence
- Antworten ohne verifizierte Citations degradieren zu: âkann ich nicht sauber belegenâ
Trockene Lesson: ohne gespeicherte Evidence gibtâs keine Citations.
AbwÀgungen
- Snapshots kosten Storage und Zeit.
- Fail-closed reduziert kurzfristig Answer Rate.
- FĂŒr manche Tasks sind Citations Overhead (nicht ĂŒberall erzwingen).
Wann du es NICHT nutzen solltest
- Wennâs intern ist und keine Citations braucht: nicht aus Prinzip erzwingen.
- Wenn du Evidence nicht sicher fetchen/speichern kannst (PII/Secrets): keine âverlĂ€sslichenâ Citations vorspielen.
- Wenn du deterministische Lookups aus einer Source of Truth hast: link die Source direkt.
Checkliste (Copy/Paste)
- [ ] Citations als
source_ids, nicht URLs - [ ] Tool Output Snapshots speichern (URL + hash + timestamp)
- [ ] Citations auf unfetched URLs verbieten
- [ ] âSearch Resultsâ von âEvidenceâ trennen
- [ ] Citations validieren (fail closed oder degrade)
- [ ]
run_id+source_ids + Snapshot-Hashes loggen - [ ] Retention Policy fĂŒr Snapshots
- [ ] Safe-mode: âanswer without sourcesâ wenn Evidence fehlt
Sicheres Default-Config-Snippet (JSON/YAML)
citations:
required: true
evidence_sources: ["http.get", "kb.read"]
allow_search_results_as_evidence: false
fail_closed: true
attach_snapshot_hash: true
retention_days: 14
FAQ (3â5)
Von Patterns genutzt
Verwandte Failures
Q: Reicht es nicht, das Modell einfach nach Quellen zu fragen?
A: Du kannst fragen, aber du kannst es nicht enforceân. Ohne Verifier, der an Tool Snapshots hĂ€ngt, sind Citations nur Deko.
Q: Muss ich den kompletten Seiten-Text speichern?
A: Nicht unbedingt. Starte mit URL + Title + Content-Hash + Timestamp. Full text brauchst du fĂŒr Quotes/Replay.
Q: Sind Search Results jemals akzeptable Evidence?
A: Nur wenn du ok damit bist, Dinge zu zitieren, die du nicht gelesen hast. In Prod: meist nein.
Q: Und private Docs?
A: Gleiches Pattern: source_ids an kb.read Snapshots. Kein Raw Text in Logs.
Verwandte Seiten (3â6 Links)
- Foundations: Wie Agents Tools nutzen · Wie LLM-Limits Agents beeinflussen
- Failure: Prompt Injection · Infinite Loop
- Governance: Tool Permissions
- Production stack: Production Stack