The Prompt Architect

A fabricated citation is worse than no citation

This is the rule I hold most strictly when building anything that generates claims: a made-up source is worse than an absent one. Not equally bad. Worse.

An uncited number invites scrutiny. You see "commission: 20–40%" with nothing behind it and you know to go check. A cited number closes the question. It converts a guess into apparent evidence, and it spends a kind of credibility the system has not earned. The reader stops looking precisely when they should start.

Models will cite anything you ask them to

If you add a sources field to your output schema and ask a model to fill it, it will. It will produce URLs that are well-formed, plausibly named, on real domains, and pointing at pages that may not exist. This is not the model malfunctioning. You asked for a list of URLs; it produced the most probable such list. Nothing in that process involves having read anything.

So the instinct to fix this with prompt engineering — only cite real sources, do not invent URLs, be accurate — misunderstands the failure. The model is not disobeying. It has no mechanism to distinguish a URL it retrieved from a URL that merely sounds right, unless you build one.

The pattern: whitelist against what you actually retrieved

If your system does retrieval, you already possess ground truth: the exact set of URLs that came back. That set is the whitelist. Everything else is discarded, unconditionally, no matter how convincing.

function groundCitations(products, retrievedSources) {
  const retrieved = new Set(retrievedSources.map(s => s.url));
  return products.map(p => {
    const cited = (p.sourceUrls ?? []).filter(url => retrieved.has(url));
    return { ...p, sourceUrls: cited, claimsSourced: cited.length > 0 };
  });
}

Three properties matter here, and each one was a decision:

Derive the flag from what survived, not what was claimed

claimsSourced is computed from the filtered list, never from whether the model said it had sources. If the model cites three URLs and all three are invented, the correct output is unsourced — not "sourced, citations unavailable." The flag must describe reality after filtering.

Exact matching, deliberately

It is tempting to normalise before comparing — strip www., ignore trailing slashes, compare domains only. I decided against it. In practice the near-miss is the signal: a model that returns flexoffers.com/x when the retrieved URL was www.flexoffers.com/x is reconstructing from memory rather than copying what it was given. Loosening the match hides exactly the behaviour you want to detect.

The cost is occasionally dropping a citation that was morally correct. I will take that trade every time. A dropped good citation shows as "unverified," which is a mild, honest understatement. A retained bad one is a lie.

Say so in the interface

The filtering is worthless if the UI renders sourced and unsourced claims identically. In my system an unsourced product now says, in place of its citation line:

Unverified estimate — no retrieved source backs this commission or trend figure. Check the network's own rate card before spending.

Note the last sentence. A warning that only says "unverified" tells the reader they have a problem. Telling them where the authoritative answer lives tells them what to do about it.

The precondition everyone misses

All of the above assumes the model can cite correctly when it wants to. That requires the retrieved URLs to be in the prompt, as an explicit list. I shipped this whole mechanism once with the URLs missing from the prompt — the filter worked flawlessly and marked every single item unsourced, because there was nothing to match.

If you take one operational detail from this: put the source list in the prompt explicitly, labelled as the only permissible citations, and separately from the prose summary of what those sources said. Then verify against the live model that citations actually come back, because a filter with an empty input is indistinguishable from a filter working correctly.

Where this generalises

The pattern is not really about citations. It is: when a model produces a reference to something in the real world, validate the reference against the real world rather than trusting the reference.

It applies to file paths a model claims exist, function names it says are defined, product identifiers, dates, prices, statute numbers, and anything else where being wrong is invisible in the shape of the answer. If you can check it cheaply, check it. If you cannot check it, say you cannot — and make the interface say it too.