Gemini grounded search returning incorrect source names with Vertex AI RAG
Building a news pipeline on Gemini grounded search (googleSearch tool -> grounded text + groundingChunks/groundingSupports, then a second structured-extraction call that emits one story per item with a source_name and a resolved article URL). Rendering [source_name](url) ships attributions where the URL is correct and the figures verify against the linked page, but the outlet NAME is not the publisher. Recent production sample: "U.S. Census Bureau" -> a private bank's markets blog, "CME FedWatch Tool" -> cbsnews.com, "U.S. Treasury" -> bankrate.com, "U.S. Strategic Petroleum Reserve" -> a public radio station, "Energy market reports" -> latimes.com. Every link resolved 200 and every cited figure was present in the linked article, so link-integrity checks and figure verification both pass clean. Tried the obvious guard: reject source_name when it is not a recognized news outlet and fall back to the domain. That does not fire here — unlike a garbled affiliate name, these are real, famous institutions, so a known-outlet allowlist or an LLM 'is this a real outlet?' check both answer yes. Also tried treating it as a prompt problem ('name the publisher, not the data source') — the extraction step never sees the resolved URL, so it has nothing to name the publisher FROM. The bug is invisible in any check that only validates links and numbers, and it stays latent whenever a downstream writer happens to substitute the real outlet on its own.
Root cause: source_name is generated in the wrong phase, from text that does not contain the publisher.
The extraction call reads only the grounded text. That text says things like "The CME FedWatch Tool shows a 35% probability of a hike" or "The U.S. Census Bureau released its June durable goods report" — the model is faithfully naming the entity the text attributes the DATA to. The publisher exists only in groundingChunks[].web.uri (and its resolved redirect target), which the extraction call never sees. So source_name is not a hallucination to be validated away; it is a correct answer to a different question.
Do not derive the outlet name in the LLM phase at all. Derive it after attribution, from the resolved URL:
_OUTLETS = { # only where the domain is not self-describing
'apnews.com': 'AP News', 'cbsnews.com': 'CBS News',
'latimes.com': 'Los Angeles Times', 'wsj.com': 'The Wall Street Journal',
'bbh.com': 'Brown Brothers Harriman',
}
def outlet_for(url: str) -> str:
host = urlsplit(url).hostname or ''
host = host[4:] if host.startswith('www.') else host
if host in _OUTLETS:
return _OUTLETS[host]
# local TV/radio affiliates: bare call letters are the real name
label = host.rsplit('.', 2)[0]
return label.upper() if len(label) <= 5 else label.title()Keep the model's source_name in your trace as the data origin — it is genuinely useful ('per the CME FedWatch Tool, via CBS News'), just never as the link label.
Why an allowlist guard on the LLM output does not work. The failure emits real institution names, so source_name in KNOWN_OUTLETS is the wrong predicate — the string is a legitimate proper noun, it is just not the publisher of that URL. The only predicate that catches it is agreement between the name and the resolved host, which means you already have the host, which means you should have used the host in the first place.
Detection in existing output. Join each story's source_name against its final URL's registrable domain and flag disagreement. Two classes both matter: a garbled/invented name (obvious), and a real-institution name that is not the host's publisher (silent). The second class is the common one and passes every naive check.
This is a near-miss detector, not a paragraph-only bug. If a downstream generation step rewrites citations, it may quietly substitute the correct outlet and hide the defect for weeks — audit the structured intermediate, not just the final prose. Two consecutive production editions showed the wrong name in the intermediate while the published text read correctly.
Related garbage-in worth stripping in the same pass: grounded summaries of nonprofit/public-media pages absorb the site's fundraising banner into the story body (e.g. an oil-price story whose 'what this means for you' text asked the reader to start a monthly donation to replace eliminated funding). Filter donation/subscribe/newsletter boilerplate out of grounded text before extraction.