Halluzinierte Quellen bei AI Agents (Failure Mode + Fixes + Code)

  • Erkenne den Fehler frĂŒh, bevor die Rechnung steigt.
  • Verstehe, was in Prod bricht – und warum.
  • Guardrails kopieren: Budgets, Stop-Reasons, Validation.
  • Wissen, wann das nicht die Root Cause ist.
Erkennungs-Signale
  • Tool-Calls pro Run steigen (oder wiederholen sich mit args-hash).
  • Kosten/Tokens pro Request steigen ohne bessere Ergebnisse.
  • Retries kippen von selten zu konstant (429/5xx).
Agents zitieren URLs, die sie nie gefetcht haben. Warum das in Prod passiert und wie du evidence-backed Citations erzwingst.
Auf dieser Seite
  1. Problem (aus der Praxis)
  2. Warum das in Production bricht
  3. 1) Das Modell ist optimiert, hilfreich auszusehen — nicht auditierbar zu sein
  4. 2) “Search Results” sind keine Evidence
  5. 3) Evidence geht zwischen Steps verloren
  6. 4) “Cite sources” ist Policy. Policy enforce’t sich nicht.
  7. Implementierungsbeispiel (echter Code)
  8. Echter Incident (mit Zahlen)
  9. AbwÀgungen
  10. Wann du es NICHT nutzen solltest
  11. Checkliste (Copy/Paste)
  12. Sicheres Default-Config-Snippet (JSON/YAML)
  13. FAQ (3–5)
  14. Verwandte Seiten (3–6 Links)
Interaktiver Ablauf
Szenario:
Schritt 1/2: Execution

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:

  1. search.read("x")
  2. Titles + URLs
  3. 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
PYTHON
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 sources
JAVASCRIPT
import 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:

  1. Sources wurden source_ids, gebunden an Tool Snapshots
  2. “Search Results” zĂ€hlen nicht mehr als Evidence
  3. 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)

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)

Reicht es nicht, das Modell einfach nach Quellen zu fragen?
Du kannst fragen, aber du kannst es nicht enforce’n. Ohne Verifier, der an Tool Snapshots hĂ€ngt, sind Citations nur Deko.
Muss ich den kompletten Seiten-Text speichern?
Nicht unbedingt. Starte mit URL + Title + Content-Hash + Timestamp. Full text brauchst du fĂŒr Quotes/Replay.
Sind Search Results jemals akzeptable Evidence?
Nur wenn du ok damit bist, Dinge zu zitieren, die du nicht gelesen hast. In Prod: meist nein.
Und private Docs?
Gleiches Pattern: `source_id`s an `kb.read` Snapshots. Kein Raw Text in Logs.

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.

Nicht sicher, ob das dein Fall ist?

Agent gestalten ->
⏱ 7 Min. Lesezeit ‱ Aktualisiert MĂ€r, 2026Schwierigkeit: ★★☆
In OnceOnly umsetzen
Guardrails for loops, retries, and spend escalation.
In OnceOnly nutzen
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
controls:
  loop_detection:
    enabled: true
    dedupe_by: [tool, args_hash]
  retries:
    max: 2
    backoff_ms: [200, 800]
stop_reasons:
  enabled: true
logging:
  tool_calls: { enabled: true, store_args: false, store_args_hash: true }
Integriert: Production ControlOnceOnly
Guardrails fĂŒr Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Kill switch & Incident Stop
  • Audit logs & Nachvollziehbarkeit
  • Idempotenz & Dedupe
  • Tool-Permissions (Allowlist / Blocklist)
Integrierter Hinweis: OnceOnly ist eine Control-Layer fĂŒr Production-Agent-Systeme.
Beispiel-Policy (Konzept)
# Example (Python — conceptual)
policy = {
  "budgets": {"steps": 20, "seconds": 60, "usd": 1.0},
  "controls": {"kill_switch": True, "audit": True},
}
Autor

Diese Dokumentation wird von Engineers kuratiert und gepflegt, die AI-Agenten in der Produktion betreiben.

Die Inhalte sind KI-gestĂŒtzt, mit menschlicher redaktioneller Verantwortung fĂŒr Genauigkeit, Klarheit und Produktionsrelevanz.

Patterns und Empfehlungen basieren auf Post-Mortems, Failure-Modes und operativen Incidents in produktiven Systemen, auch bei der Entwicklung und dem Betrieb von Governance-Infrastruktur fĂŒr Agenten bei OnceOnly.