An LLM described a company's official website to me in detail: complete Simplified Chinese interface, pricing displayed in RMB marked "tax not included", China-specific terms of service, a localized privacy policy. It told me this site was "the most important source" for...
An LLM described a company's official website to me in detail: complete Simplified Chinese interface, pricing displayed in RMB marked "tax not included", China-specific terms of service, a localized privacy policy. It told me this site was "the most important source" for verifying the company's credibility.
The domain has been registered to a private individual since 2016. It returns a 502. None of those pages have ever existed.
This is the hallucination shape that worries me most — not "the model made something up," which everyone expects, but the model made up specific, checkable, mundane details that a human would never think to check. Nobody verifies a privacy policy's existence. You verify the big claim and assume the supporting texture came from somewhere.
Here's how I caught it, why my own pipeline sat on it for a week, and the check that generalizes.
The setup
I run a measurement harness against six Chinese LLM APIs — DeepSeek, Doubao, Qwen, Kimi, ERNIE, GLM — asking buyer-style questions about international software brands and logging every answer. 4,023 valid responses, retrieval off, everything stored as JSONL.
One question type asks, in Chinese and English, some version of "what are this brand's official channels, and how would you verify them?"
GLM's answer for one brand:
Airtable China Official Website (Airtable中国官网)
URL:https://www.airtable.cn/
What to look for: This is the most important source. Its existence signals a formal commitment to the Chinese market.
And in a separate answer, in Chinese:
域名
.cn是中国的国家顶级域名,由 Airtable 官方运营,这本身就是一种官方身份的声明
(The .cn domain is China's country-code TLD, operated officially by Airtable — this is itself a declaration of official identity.)
Confident, structured, and it reasons about why the evidence counts. That last part is what makes it dangerous.
Why my pipeline missed it
My extractor pulled URLs with a regex and recorded the domains. airtable.cn went into the citation column as a cited source, indistinguishable from a real one.
Every quality check I had was a rate: error count, empty-answer rate, answer-length distribution, language distribution. All of them were green, because nothing about this row was anomalous. One URL among 1,416, in a well-formed answer of normal length in the expected language.
Rates catch a class of rows that changes size between runs. They cannot catch a class that was wrong from the first run and stayed wrong at a stable size. A reviewer put it better than I did: rates catch a class that shrinks, asserts catch a class that was never right.
The check
Three lookups per domain. No tools, no API, about ten minutes for eight brands.
# 1. Does anything answer for it?
dig +short airtable.cn A
# → 223.26.56.104 (someone registered it and pointed it somewhere)
# 2. Who holds it?
whois airtable.cn | grep -iE "^(Registrant|Registration Time|Sponsoring)"
# → Registrant: (a private individual)
# → Registrant Contact Email: (a free QQ mail address)
# → Registration Time: 2016-02-08
# 3. What does it actually serve?
curl -s -o /dev/null -w "%{http_code}\n" http://www.airtable.cn/
# → 502
For contrast, the one brand in my sample that does own its .cn:
Registrant: BRIAN TYLER EVANS
Registrant Contact Email: help@clickup.com
Sponsoring Registrar: GoDaddy.com, LLC
That's what ownership looks like in a registration record: a company contact, at the company's own domain. It takes one line to tell the two cases apart, and my pipeline had never looked.
Running all eight brands from the study:
| Domain | Held by | Serves |
|---|---|---|
| clickup.cn | the brand | nothing (parked) |
| airtable.cn | private individual | 502 |
| wrike.cn | private individual | 502 |
| asana.cn | private individual | "domain for sale" |
| smartsheet.cn | private individual, registered 2025 | "域名转让 — The domain is on sale!" |
| notion.cn | a domain-holding company | 403 |
| monday.cn | the same domain-holding company | 403 |
| basecamp.cn | private individual, registered through 2034 | a bicycle apparel manufacturer in Dongguan |
Seven of eight belong to someone other than the brand. I'm not publishing registrant names — registering an available domain is legal and these are private individuals. The interesting part is on the other side.
The tell: the model contradicted itself
Same engine, same collection window, a differently-worded question:
You do not access a separate
airtable.cnwebsite. Instead, your Airtable China account is configured to use the China-hosted infrastructure.
Two incompatible accounts of the same fact, days apart, neither hedged.
That's the generalizable detection signal, and it's cheap: ask the same factual question several ways and diff the answers. A model that knows something answers consistently. A model that is constructing something plausible constructs differently each time, because there's no underlying fact constraining it.
In my open-question data, 18.8% of question-pairs changed outcome between two runs on the same day. If you're evaluating an LLM's factual output and you only ask once, you have no way to distinguish knowledge from confabulation.
What I changed
A predicate check on extracted URLs. A URL inside a clause that denies its existence is not a citation. My extractor was matching tokens without reading the sentence around them:
const URL_NEGATION_CUES =
/没有|不存在|并无|未(设立|开设|推出|建立)|无(独立|专门|官方)|不提供|尚未|(?:does not|doesn't|no)\s+(?:have|exist|operate)/i;
export function urlIsNegated(text, index) {
return URL_NEGATION_CUES.test(clauseAround(text, index));
}
One implementation note that cost me a wrong result: URL predicates need tighter clause boundaries than entity mentions do. Split on sentence punctuation only, and "并没有推出中文官网,其主要官网是 https://basecamp.com" flags that URL as negated — but the negation targets the Chinese site and the URL is being affirmed, one comma later. Splitting on commas as well fixed it: zero false flags across 1,416 URLs, and 74 genuine anti-citations in the bare-domain form my original regex never captured at all.
Assertions at the joins. Anywhere two vocabularies meet, assert a hit that must be there or refuse to run. In my harness there were three such seams and all three were quietly broken:
- Competitor list vs answer text — my list was romanized, the answers name competitors in Chinese. The join found almost nothing and reported it as "no substitution." Fixed by refusing to start if a Chinese-language panel has no Chinese-script competitor names.
- Category label vs prompt template — an English label injected into a Chinese question changed what was being asked. Fixed by round-tripping the rendered prompt through a model: "what category does this question ask about?" Compare to what you meant. One call per template.
- URL vs surrounding clause — the predicate check above.
Provenance on every row, so a number can be reconstructed later rather than silently changing when the scorer improves: scoring_version, finish_reason, completion and reasoning token counts, a response hash, and a validity enum decided before any content scoring runs.
The part I'd want you to take away
The failure wasn't that a model hallucinated. It's that the hallucination was operationally indistinguishable from a fact at every layer of my pipeline, and every quality metric I had was green while it sat there.
If you're building anything that treats LLM output as evidence — extraction, enrichment, research automation, RAG evaluation — the questions worth asking are:
- If a whole class of your rows were wrong from run one, which metric would move? (If the answer is "none," you're where I was.)
- When your extractor pulls an entity, does anything check the predicate of the sentence it came from?
- Do you ask the same factual question more than once, in more than one phrasing?
- Can you reconstruct last month's number, or would re-running today silently produce a different one?
I've published corrections to my own numbers four times in three weeks doing this. Every single one was found either by reading raw output by hand or by a stranger asking a question I couldn't answer. Neither is a metric you can add to a dashboard, which I think is the actual lesson.
Harness, labelled validation samples, and the re-scoring scripts are public under CC BY 4.0: github.com/David88666/china-ai-visibility-benchmark