To stop an AI research or RAG agent from presenting its own inferences as retrieved facts, split the work so the LLM never decides what is a fact: let the LLM only extract and summarize, and let a deterministic, non-LLM pipeline do all scoring, cross-checking, and labeling. Tag a claim FACT only when a rule is satisfied — corroboration by ≥2 independent sources, or one official API — and downgrade everything else to INFERENCE. Because labeling is rule-based, the agent can't launder a guess into a fact, and the same query produces the same labels every run.
An AI agent that gathers information has two kinds of output tangled together: things it retrieved and things it concluded. A web page said the market was 1.2 trillion won (retrieved); the agent inferred the market is "growing fast" (concluded). Both come out in the same confident prose. For anything you'll act on, that blend is the problem — you can't tell which sentence is grounded and which is the model filling a gap.
The fix isn't a better prompt ("only state facts you can cite"). Prompts are probabilistic; under pressure the model reverts. The fix is structural: take the fact/inference decision away from the model entirely and put it in code.
Draw a hard line through the pipeline:
| The LLM does | Deterministic code does |
|---|---|
| Extract claims from a fetched page; summarize a passage | Score, cross-check, sort, deduplicate, label FACT/INFERENCE, decide freshness |
The LLM is excellent at reading messy text and pulling out a structured claim. It is unreliable at judging that claim — ask it to "rate confidence 0–1" and it will turn a guess into 0.85, and give a different number next run. So nothing downstream of extraction is allowed to be an LLM call. Scores are token matches, source counts, and recency math. Labels are rule outputs. This buys two things at once: reproducibility (same query → same labels, which you can unit-test) and no laundering (the model can't promote its own inference to a fact, because it never holds the pen on labeling).
Make the stages explicit so each is testable in isolation:
PLAN → HARVEST → NORMALIZE → CORROBORATE → SCORE → RENDER
FACT is not a default; it's a status a claim must earn, enforced as a type invariant:
# A claim constructed as FACT without evidence is a bug, not a soft warning.
Claim(provenance=FACT, evidence_ids=[]) # -> raises
# The corroboration rule (the knob is the count; the principle is independence)
def label(claim):
independent = count_independent_sources(claim) # distinct domains, not pages
if independent >= 2 or claim.from_official_api:
return FACT # carries the evidence_ids that corroborated it
return INFERENCE # single-source or model-derived
"Independent" is doing real work: one blog quoting another blog is one source, not two. Two different domains, or a single authoritative API (a government dataset, an exchange's own endpoint), clear the bar. Everything else is rendered as INFERENCE — visible to the reader as exactly that.
Diversity of sources is what makes corroboration meaningful, but firing every source at once is wasteful and noisy. Use escalation, not broadcast: try a primary search, and only escalate to the next path when the first is insufficient.
| Path | Order |
|---|---|
| Web search | primary → escalate to a news-grade engine (ad/spam pollution) → escalate to a semantic engine (papers, near-duplicates) |
| Official API | a government/first-party dataset; one official source may stand alone as FACT |
Never send the same query to three engines simultaneously — read the first result, then decide whether to escalate. And when a source fails or is rate-limited, log the failure and the escalation; never substitute a guess for a missing fetch.
Two more rules complete the provenance picture. Freshness: every datum carries a confirmation date, and a rule marks it stale when it ages past a threshold — a fact true last quarter is labeled as such, not silently presented as current. Gaps: the render step emits an explicit list of what was asked but not found or not corroborated. A silent gap reads as completeness and is the most dangerous output a research agent can produce; surfacing it is what makes the FACT list trustworthy.
The payoff is a research output a reader (or a downstream AI) can trust per-claim: every FACT points at the independent sources that earned it, every INFERENCE is flagged as the agent's own leap, stale data says so, and the gaps are named. The model still does what it's good at — reading and extracting — but it never gets to decide what's true. In an era where AI answers are increasingly cited as sources themselves, the agents worth citing are the ones that label their own confidence honestly, by rule, and reproducibly.
Q. How do I make an AI agent distinguish facts from its own inferences?
Let the LLM only extract/summarize; a deterministic pipeline scores and labels. FACT requires ≥2 independent sources or one official API; everything else is INFERENCE. Rule-based labeling means the model can't promote a guess, and runs are reproducible.
Q. How do I stop a RAG agent presenting inferences as facts?
Make FACT carry evidence IDs and require them in code — a FACT constructed without evidence should raise. Claims failing the corroboration rule auto-downgrade to INFERENCE.
Q. Why deterministic scoring instead of LLM scoring?
LLM scores drift (same query, different numbers) and can launder a guess into a high confidence. A function — token match, source count, recency — is reproducible and unit-testable.
Q. How should the agent handle what it couldn't find?
Report gaps loudly as an explicit list. A silent omission reads as completeness and is the most dangerous failure. Log failed/rate-limited sources and escalations too.