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
Top comments (2)
That supporting-texture failure is the one that makes me nervous too. A fake citation at least looks suspicious. A fake privacy page, exact date, or minor product detail looks too boring to verify, so it slips past review. I would probably make the harness separate core claims from supporting texture and sample-check both.
Your framing sent me back to the logs and it holds up, with a sharper split than I expected.
The specific case: three engines assert the fake domain is the official channel (GLM 6 times, Doubao 3, DeepSeek 1). Two correctly deny it exists (Kimi 3, Doubao 1). Doubao does both — it contradicts itself across runs.
But the part that proves your point is what surrounds those assertions. Take the Kimi answers, which get the domain question right. In the same responses they also state that Airtable has no dedicated Chinese site, that the product UI does support Simplified Chinese, that the official case studies are all Western companies, that the main China user base is cross-border e-commerce teams and China subsidiaries of foreign firms. Core claim correct, and the texture around it is a mix of verifiable-true, verifiable-false, and unverifiable-by-anyone.
The GLM ones are the inverse and worse: correct core claim in some answers (no independent mainland entity), fabricated texture attached to it (RMB pricing displayed with tax notation, a China privacy policy, a localized terms of service). The headline was right. The evidence supporting the headline was invented.
That's exactly your point and I hadn't separated the two axes until you named them.
I measured how much texture there is to check, across all 4,023 answers:
a domain or URL 39.5%
a certification name 23.5% (等保, ISO 27001, SOC 2, ICP filing)
a specific year or date 7.5%
a percentage 5.2%
a price or amount 0.9%
a named Chinese entity 0.3%
≥3 of those in one answer 2.7%
The 0.3% named-entity number is the one I'd watch. It's rare, and every instance is maximally checkable and maximally damaging if wrong — a fabricated subsidiary name is the kind of detail a procurement team would repeat in a document.
On implementing it: I'm building the extraction pass you describe, but with one change from "sample-check both." Sampling answers means reading whole responses and hoping the wrong atom is in the ones you drew. Extracting the atoms first turns it into checking claims — 40 specific assertions rather than 20 essays — and the atoms are individually verifiable in a way an answer isn't. A domain resolves or doesn't. A certification appears in a registry or doesn't. An entity exists in the business registry or doesn't.
The core claim still needs a human because it's usually a judgment ("is this brand credible in China"). But the texture is mostly mechanically checkable, which inverts my assumption — I'd been treating texture as the part too expensive to verify.
The failure mode I'm now worried about with my own fix: extracting atoms and checking them mechanically will make the texture layer clean, which will make the core-claim layer feel safer than it is. Green texture on a wrong headline is a new way to be confidently misled, and I don't have an answer for that one yet beyond keeping the hand-read.