DEV Community

John
John

Posted on • Originally published at hexisteme.github.io

How to make an AI research agent label facts vs inferences — a deterministic provenance pipeline

Originally published on hexisteme notes, part of a series on building and running an AI agent fleet.

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.

The split: LLM extracts, code judges

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).

Reproducibility is the tell. If your research agent gives different confidence on the same question across runs, an LLM is scoring somewhere in the pipeline. Find it and replace it with a function. The goal is: re-run the exact query, get the exact same FACT/INFERENCE split.

A six-phase pipeline

Make the stages explicit so each is testable in isolation:

PLAN → HARVEST → NORMALIZE → CORROBORATE → SCORE → RENDER
Enter fullscreen mode Exit fullscreen mode
  • PLAN — turn the question into concrete sub-queries and the sources to try.
  • HARVEST — fetch from multiple paths (see below). LLM-free; just collection.
  • NORMALIZE — LLM extracts structured claims from each fetched item. This is the only place the model touches the data.
  • CORROBORATE — group claims; count independent sources per claim.
  • SCORE — assign labels and scores by rule.
  • RENDER — emit FACTs, INFERENCEs, and an explicit gap list.

The FACT gate: earn the label

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
Enter fullscreen mode Exit fullscreen mode

"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.

⚠️ Watch for order-dependence. An early version of this scored a cross-corroborated FACT lower than a single-source INFERENCE because the score depended on processing order. That silently breaks reproducibility. Scores must be a pure function of the claim and its evidence, independent of the order claims were processed.

Multi-path harvest, without redundancy

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.

Freshness and gaps are first-class

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.

Why this is worth the structure

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.


More notes on building an AI agent fleet — falsifier-driven AI decisions, reusable decision units, a file-based agent work-bus — at hexisteme.github.io/notes.

Top comments (0)