构建 GEO 检测引擎:GetCiteFlow 如何用混合分析架构帮网站被 AI 引用
> **一句话总结:** 传统 SEO 在 AI 搜索时代已经不够用了。本文从技术角度深度拆解什么是 GEO(生成式引擎优化),以及我们如何用 Next.js 15 + Gemini 构建了一个能检测并修复网站 AI 可见性的平台。
> **发布说明:** 掘金/CSDN/SegmentFault 在编辑器页面设置标签和封面图即可,本文不含 frontmatter 专有字段。知乎直接粘贴正文,标题和话题在编辑器设置。
你有没有发现,现在越来越多人不再"谷歌一下",而是直接问 ChatGPT、Perplexity、Claude 或者豆包。这个转变对内容发现的影响是革命性的------但绝大多数站长还没意识到问题所在。
传统 SEO 的指标(外链、域名权重、关键词密度)与 AI 引用率之间只有大约 **0.3 的相关性**。你在 Google 排第一,不代表 ChatGPT 会提到你。这就是 **GEO(Generative Engine Optimization,生成式引擎优化)** 要解决的问题。
这篇文章我会从技术实现角度,带你走进 **GetCiteFlow**------我们构建的 GEO 分析平台------包括架构设计、关键代码和踩过的坑。
1. 什么是 GEO?和 SEO 有什么本质区别?
当 ChatGPT 或 Claude 回答用户问题时,它并不是像 Google 那样"排名"。它更关注那些**容易被引用、总结和归因**的信号。
通过分析数千个网站,我们总结出六个关键维度:
| 维度 | 说明 |
|---|---|
| **AI 可见性** | AI 能否找到并解析你的内容? |
| **FAQ 覆盖率** | 是否有结构化的 FAQ Schema? |
| **实体清晰度** | 页面是否明确说明了"自己是什么"? |
| **权威性** | 有没有原创研究或署名作者? |
| **内容结构** | 是否使用了列表、表格、清晰的标题层级? |
| **摘要优化** | 页面开头是否有 AI 可提取的清晰摘要? |
核心洞察:**AI 搜索引擎不以人类的方式"阅读"页面。** 它们寻找的是机器可读的信号------结构化数据、实体定义、`llms.txt` 文件------而非关键词密度。
2. 混合分析架构:AI 大模型 + 确定性信号检测
GetCiteFlow 采用**混合分析架构**。我们不依赖 LLM 单方面评价(它会胡编乱造),而是结合两层独立的分析:
```
用户输入 URL
|
v
1 爬取网站 → 提取信号(HTML 解析)
|
v
2 格式化信号 → 发送给 AI(Gemini / OpenAI / Deepseek)
|
v
3 AI 返回结构化 JSON(分数、细分、建议)
|
v
4 合并确定性检测结果(列表、meta 长度等)
|
v
5 缓存结果 + 渲染报告
```
核心编排函数出自 `lib/analyze.ts`:
```typescript
export async function analyzeSite(url: string): Promise<Record<string, unknown>> {
const cacheKey = `report:${url}`;
const cached = cacheGet<Record<string, unknown>>(cacheKey);
if (cached) return { ...cached, cached: true };
// 去重:同一 URL 的并发请求只执行一次
if (pendingCache.has(cacheKey)) {
return pendingCache.get(cacheKey)!;
}
const analyze = async () => {
const activeProvider = getProvider();
const fn = providerFnsactiveProvider;
const report = await fn(url);
// 合并 AI 结果 + 确定性信号检测
const siteData = await getSiteData(url);
const deterministicMissing = getDeterministicMissing(siteData);
const aiMissing = (report.missing as string\[\]) || \[\];
const mergedMissing = ...new Set(\[...aiMissing, ...deterministicMissing)];
const result = { ...report, missing: mergedMissing };
cacheSet(cacheKey, result, CACHE_TTL_MS);
return result;
};
const promise = analyze();
pendingCache.set(cacheKey, promise);
return promise;
}
```
为什么要混合?**LLM 擅长定性判断但不擅长"数数"。** AI 可能漏掉一个页面没有任何 `<ul>` 标签这个事实,但简单的正则检查不会。两者结合,各取所长。
3. 爬虫模块:用正则提取确定性信号
爬虫(`lib/scrape.ts`)是一个纯 HTTP 抓取器------不需要无头浏览器。它获取 HTML,用正则解析结构化信号,并检查关键静态文件。
```typescript
async function extractFromHtml(html: string) {
const titleMatch = /<title\^\>*>(\^\<*)<\/title>/i.exec(html);
const hasOpenGraph =
/<meta\^\>+property="'og:(title|description|image)"'/i.test(html);
const hasFaqSchema = /* 解析 JSON-LD <script> 块 */;
const hasOrderedLists = /<ol\\s\>/i.test(html);
const avgParagraphLength = /* 通过 <p> 标签计算 */;
const hasSummarySection =
/\b(key takeaways?|executive summary|tldr|tl;dr)\b/i.test(bodyLower);
return { title, hasOpenGraph, hasFaqSchema, hasOrderedLists, ... };
}
```
三个关键文件的检查是并行进行的:
```typescript
const hasRobotsTxt, hasSitemap, hasLlmstxt = await Promise.all([
checkStaticFile(resolvedOrigin, "/robots.txt"),
checkStaticFile(resolvedOrigin, "/sitemap.xml"),
checkStaticFile(resolvedOrigin, "/llms.txt"),
]);
```
`llms.txt` 的检测尤其重要------这是一个相对较新的标准(由 llmstxt 社区提出),专门为 AI 爬虫创建机器可读的站点索引。拥有 `llms.txt` 文件的网站,AI 引用率显著更高。
4. AI 分析层:用 Gemini 的结构化输出
AI 分析部分,我们使用 Google Gemini 原生支持的结构化输出。这非常关键------没有它,从 LLM 解析自由格式的 JSON 脆弱且容易出错。
```typescript
async function analyzeWithGemini(url: string) {
const siteData = formatSiteData(url, await getSiteData(url));
const response = await getAiClient().models.generateContent({
model: "gemini-3-flash-preview",
contents: ANALYZE_PROMPT(url, siteData),
config: {
temperature: 0, // 确定性输出
responseMimeType: "application/json",
responseSchema: {
type: Type.OBJECT,
required: "score", "breakdown", "missing", "suggestions", "summary",
properties: {
score: { type: Type.NUMBER },
breakdown: {
type: Type.OBJECT,
properties: {
aiVisibility: { type: Type.NUMBER },
faqCoverage: { type: Type.NUMBER },
entityClarity: { type: Type.NUMBER },
authority: { type: Type.NUMBER },
contentStructure: { type: Type.NUMBER },
summaryOptimization: { type: Type.NUMBER },
},
},
missing: { type: Type.ARRAY, items: { type: Type.STRING } },
suggestions: { type: Type.ARRAY, items: { type: Type.STRING } },
summary: { type: Type.STRING },
},
},
},
});
return JSON.parse(response.text || "{}");
}
```
这里的关键设计决策:
-
**`temperature: 0`**------我们需要确定性的、可复现的结果。不需要创造力。
-
**`responseSchema`**------告诉 Gemini 确切需要什么 JSON 形状。没有这个,你会得到解析错误、字段缺失和类型不一致。
-
我们将**实际爬取的信号**注入到提示词中。AI 不是瞎猜------它基于我们找到的数据进行评估。
提示词模板(`lib/ai-provider.ts`):
```typescript
export const ANALYZE_PROMPT = (url: string, siteData?: string) =>
`分析该网站的 AI 可见性(GEO):${url}
{siteData ? \`以下是网站的实际检测信号:\\n{siteData}\n\n请基于这些真实信号进行分析,不要猜测。` : ''}
根据上述信号具体评估以下因素:
-
contentStructure (0-100):内容结构对 AI 解析的友好程度...
-
summaryOptimization (0-100):页面为 AI 摘要所做的优化程度...
仅返回具有以下精确键的 JSON 对象:
{ "score": <数字 0-100>, "breakdown": { ... }, "missing": ..., "suggestions": ..., "summary": "..." }`;
```
我们还支持 OpenAI 和 Deepseek 作为备用提供商,通过 `AI_PROVIDER_DEFAULT` 环境变量切换。架构设计使得添加新提供商非常简单------只需实现相同的函数签名。
5. 缓存策略:内存缓存 + 请求去重
每次分析都要调用 LLM API(成本高)并爬取网站(速度慢),因此缓存至关重要。我们使用简单的内存 `Map`,TTL 为 1 小时。
```typescript
interface CacheEntry<T> {
data: T;
expiresAt: number;
}
const store = new Map<string, CacheEntry<unknown>>();
const CLEAN_INTERVAL = 60_000;
let lastClean = 0;
function clean() {
const now = Date.now();
if (now - lastClean < CLEAN_INTERVAL) return;
lastClean = now;
for (const key, entry of store) {
if (now > entry.expiresAt) store.delete(key);
}
}
export function cacheGet<T>(key: string): T | null {
clean();
const entry = store.get(key);
if (!entry || Date.now() > entry.expiresAt) return null;
return entry.data as T;
}
export function cacheSet<T>(key: string, data: T, ttlMs: number): void {
store.set(key, { data, expiresAt: Date.now() + ttlMs });
}
```
我们还使用 **pending cache**(`analyze.ts` 中的 `pendingCache`)来去重同一 URL 的并发请求------如果两个用户同时提交相同的 URL,只执行一次分析:
```typescript
const pendingCache = new Map<string, Promise<Record<string, unknown>>>();
// ...
if (pendingCache.has(cacheKey)) {
return pendingCache.get(cacheKey)!; // 等待正在进行的请求
}
```
对于生产环境,你肯定需要 Redis 或其他分布式缓存。对于单实例部署(如 Vercel 的 serverless functions 并发场景),这种内存方案够用。
6. 限流:优雅降级
我们使用 Upstash Redis 实现滑动窗口限流。一个关键的设计选择:**当 Redis 不可用时,开放通过,而不是拒绝所有请求。**
```typescript
let ratelimit: Ratelimit | null = null;
try {
const redis = Redis.fromEnv();
ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(max, "1 h"),
analytics: true,
prefix: "@citeflow/ratelimit",
});
} catch {
// Redis 初始化失败------跳过限流
}
export async function checkRateLimit(ip: string): Promise<RateLimitResult> {
if (!ratelimit) {
return { success: true }; // Redis 挂了时允许请求
}
try {
const { success } = await ratelimit.limit(ip);
return success
? { success: true }
: { success: false, reason: 'rate_limited' };
} catch {
return { success: false, reason: 'redis_unavailable' };
}
}
```
为什么选择开放通过?因为免费工具应该是可访问的。因为 Redis 临时故障而阻止所有用户,比少数请求绕过限流更糟糕。
7. 报告页面:SSR + Edge OG 图片
报告页面位于 `app/report/domain/page.tsx`,使用 **SSR(服务端渲染)**,设置了 `maxDuration: 60`(Vercel Pro 计划的超时限制)。这是必要的,因为:
-
需要爬取目标网站(网络 I/O)
-
需要调用 Gemini/OpenAI(API 延迟)
-
渲染前需要完整数据
```typescript
export const maxDuration = 60;
export default async function ReportPage({ params }) {
const { domain } = await params;
const ip = getClientIp(headers());
const result = await getReport(domain, ip);
if (!result.ok) {
// 渲染错误状态:rate_limited, timeout, failed
return <ErrorState reason={result.reason} />;
}
return <ReportView data={result.data} />;
}
```
我们还使用 Edge runtime 为每个报告生成动态 OG 图片:
```typescript
// app/api/og/route.tsx --- 运行在 Vercel Edge
export const runtime = 'edge';
export const dynamic = 'force-dynamic';
```
这意味着每个报告页面都有一个独特的社交预览,显示域名和分数------这对于在 X/Twitter 和 LinkedIn 上分享至关重要。
8. 踩坑总结
8.1 AI 幻觉真实存在------必须给数据做锚定
AI 分析的第一个版本没有将真实爬取信号注入提示词。AI 编造听起来合理但完全错误的评估。**始终在提示词中提供真实数据,并指示模型基于该数据进行分析。**
8.2 结构化输出 > JSON 提示
在使用 `responseSchema` 之前,我们在提示词中使用 `"output valid JSON only"`。成功率大约 70%。使用结构化输出后,接近 99.9%。**只要你的 AI 提供商支持,就用原生结构化输出。**
8.3 积极缓存 + 并发去重
LLM API 调用成本高(每百万 token 约 0.15-3.00)且速度慢(2-5 秒)。内存缓存加上请求去重,消除了所有冗余调用。对于 Vercel 的多并发部署,`pendingCache` 模式至关重要。
8.4 确定性检测能捕获 AI 遗漏的问题
AI 经常遗漏简单的事情,比如"页面上没有列表"或"meta description 太短"。这些用正则检测轻而易举,但 LLM 容易忽略。**混合方案两者都能捕获。**
8.5 GEO 仍处于早期阶段------标准在快速演进
`llms.txt` 是一个提议标准,不是 W3C 规范。AI 搜索中 FAQ Schema 的行为每个月都在变化。构建 GEO 工具意味着随着生态系统的演进而不断迭代。我们将信号检测设计为可插拔层,可以独立于 AI 分析进行更新。
9. 试试看
如果你想检查自己网站的 AI 可见性,前往 getciteflow.ai(https://www.getciteflow.ai) 输入你的网址即可。你会得到一个 0-100 的分数以及具体的、可操作的建议。
整个平台的技术栈:
-
**Next.js 15**(App Router、SSR、Edge Functions)
-
**React 19** + Tailwind CSS 4 + shadcn/ui
-
**Google Gemini** 用于 AI 分析(OpenAI/Deepseek 作为备用)
-
**Upstash Redis** 用于限流
-
**部署在 Vercel**,使用 standalone 输出
*GEO 现在就像是 1998 年的 SEO------还处在非常早期的阶段。今天就开始为 AI 搜索优化的网站,将在 AI 助手成为信息发现主要入口的明天获得复利优势。*
*如果你也在构建这方面的东西,或者对架构有疑问,欢迎在评论区留言交流。*
关于作者:GetCiteFlow 高级工程师,专注于 AI 搜索可见性与生成式引擎优化。