基于 RAG 的 AI GEO 内容优化引擎架构
RAG(检索增强生成)不仅是问答系统的标配,更是 AI GEO 内容优化的核心引擎。本文讲解如何利用 RAG 架构分析内容在 AI 搜索中的表现,并自动生成优化建议。
一、为什么 AI GEO 需要 RAG?
AI GEO 的核心目标:让 AI 搜索引擎在回答用户问题时引用你的内容。
这需要回答三个问题:
- 当前内容覆盖了什么? → 需要理解现有内容的语义
- AI 搜索在回答什么? → 需要理解 AI 回答的语义
- 差距在哪里? → 需要语义对比和差距分析
传统关键词匹配无法完成语义层面的对比,而 RAG 架构天然适合:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 内容库 │────▶│ 向量索引 │────▶│ 语义检索 │────▶│ 差距分析 │
│ (网页/文章)│ │ (Embedding)│ │ (Top-K) │ │ (优化建议) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
▲
│
┌─────┴─────┐
│ AI 搜索回答 │
│ (Query) │
└───────────┘
二、RAG 内容优化引擎架构
┌─────────────────────────────────────────────────────────┐
│ RAG 内容优化引擎 │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 内容入库 │ │ 向量化 │ │ 索引管理 │ │
│ │ Pipeline │─▶│ Pipeline │─▶│ Service │ │
│ └───────────┘ └───────────┘ └─────┬─────┘ │
│ │ │
│ ┌───────────┐ ┌───────────┐ │ │
│ │ 差距检测 │◀─┤ 语义检索 │◀───────┘ │
│ │ Engine │ │ Service │ │
│ └─────┬─────┘ └───────────┘ │
│ │ │
│ ┌─────┴─────┐ ┌───────────┐ ┌───────────┐ │
│ │ 优化建议 │─▶│ 建议生成 │─▶│ 效果追踪 │ │
│ │ 生成器 │ │ (LLM) │ │ Service │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────┘
2.1 内容入库 Pipeline
将企业已有内容(官网、博客、产品文档)入库:
python
from dataclasses import dataclass, field
from typing import List, Optional
from enum import Enum
class ContentType(Enum):
WEBPAGE = "webpage"
BLOG = "blog"
PRODUCT_DOC = "product_doc"
FAQ = "faq"
PRESS_RELEASE = "press_release"
@dataclass
class ContentItem:
"""内容项"""
content_id: str
url: str
title: str
body: str
content_type: ContentType
published_at: Optional[str] = None
tags: List[str] = field(default_factory=list)
word_count: int = 0
class ContentIngestionPipeline:
"""内容入库 Pipeline"""
def __init__(self, chunker, embedder, vector_store, postgres):
self.chunker = chunker # 文本分块器
self.embedder = embedder # 向量化器
self.vector_store = vector_store # 向量存储
self.postgres = postgres # 元数据存储
async def ingest(self, content: ContentItem) -> str:
"""入库流程:分块 → 向量化 → 存储"""
# 1. 文本分块
chunks = self.chunker.chunk(content.body, content.title)
# 2. 批量向量化
embeddings = await self.embedder.embed_batch(
[chunk.text for chunk in chunks]
)
# 3. 存储向量
vectors = []
for chunk, embedding in zip(chunks, embeddings):
vectors.append({
"id": f"{content.content_id}_{chunk.index}",
"embedding": embedding,
"content_id": content.content_id,
"url": content.url,
"title": content.title,
"chunk_text": chunk.text,
"chunk_index": chunk.index,
"content_type": content.content_type.value,
"published_at": content.published_at,
})
await self.vector_store.insert_batch("content_vectors", vectors)
# 4. 存储元数据
await self.postgres.execute(
"""
INSERT INTO content_items
(content_id, url, title, content_type, word_count,
chunk_count, ingested_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (content_id) DO UPDATE SET
title = EXCLUDED.title,
word_count = EXCLUDED.word_count,
chunk_count = EXCLUDED.chunk_count,
updated_at = NOW()
""",
content.content_id, content.url, content.title,
content.content_type.value, content.word_count,
len(chunks), datetime.now()
)
return content.content_id
2.2 文本分块策略
python
from dataclasses import dataclass
@dataclass
class TextChunk:
index: int
text: str
start_pos: int
end_pos: int
class SemanticChunker:
"""语义分块器 - 按语义边界分块"""
def __init__(
self,
chunk_size: int = 512,
overlap: int = 50,
min_chunk_size: int = 100
):
self.chunk_size = chunk_size
self.overlap = overlap
self.min_chunk_size = min_chunk_size
def chunk(self, text: str, title: str = "") -> List[TextChunk]:
"""分块主逻辑"""
# 在标题前加上上下文
full_text = f"{title}\n\n{text}" if title else text
# 按段落分割
paragraphs = self._split_paragraphs(full_text)
# 合并段落到目标大小
chunks = []
current = []
current_len = 0
for para in paragraphs:
if current_len + len(para) > self.chunk_size and current_len >= self.min_chunk_size:
# 当前块已满,保存并开始新块
chunk_text = "\n\n".join(current)
chunks.append(chunk_text)
# 保留 overlap
overlap_text = current[-1][-self.overlap:] if self.overlap > 0 else ""
current = [overlap_text] if overlap_text else []
current_len = len(overlap_text)
current.append(para)
current_len += len(para)
# 最后一块
if current:
chunks.append("\n\n".join(current))
return [
TextChunk(index=i, text=text, start_pos=0, end_pos=len(text))
for i, text in enumerate(chunks)
]
def _split_paragraphs(self, text: str) -> List[str]:
"""按段落分割"""
# 中文文本按换行分割
paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
# 如果段落过长,进一步按句子分割
result = []
for para in paragraphs:
if len(para) > self.chunk_size:
sentences = self._split_sentences(para)
result.extend(sentences)
else:
result.append(para)
return result
def _split_sentences(self, text: str) -> List[str]:
"""按句子分割(中英文)"""
import re
# 匹配中英文句子结束符
sentences = re.split(r'(?<=[。!?.!?])\s*', text)
return [s.strip() for s in sentences if s.strip()]
2.3 向量化服务
python
from typing import List
import httpx
class EmbeddingService:
"""向量化服务"""
def __init__(self, api_key: str, model: str = "text-embedding-3-large"):
self.api_key = api_key
self.model = model
self.client = httpx.AsyncClient(timeout=30.0)
self._cache = {} # 生产环境用 Redis
async def embed(self, text: str) -> List[float]:
"""单文本向量化"""
# 缓存检查
cache_key = hash(text)
if cache_key in self._cache:
return self._cache[cache_key]
response = await self.client.post(
"https://api.openai.com/v1/embeddings",
json={
"model": self.model,
"input": text,
"dimensions": 1536
},
headers={"Authorization": f"Bearer {self.api_key}"}
)
response.raise_for_status()
embedding = response.json()["data"][0]["embedding"]
self._cache[cache_key] = embedding
return embedding
async def embed_batch(self, texts: List[str], batch_size: int = 64) -> List[List[float]]:
"""批量向量化"""
results = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
response = await self.client.post(
"https://api.openai.com/v1/embeddings",
json={
"model": self.model,
"input": batch,
"dimensions": 1536
},
headers={"Authorization": f"Bearer {self.api_key}"}
)
response.raise_for_status()
data = response.json()["data"]
for item in sorted(data, key=lambda x: x["index"]):
results.append(item["embedding"])
return results
三、差距分析引擎
3.1 核心逻辑
差距分析是 RAG 内容优化引擎的核心:
python
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class GapAnalysisResult:
"""差距分析结果"""
keyword: str
platform: str
ai_answer: str # AI 搜索的回答
covered_topics: List[str] # 你的内容已覆盖的主题
missing_topics: List[str] # AI 回答中提到但你未覆盖的主题
coverage_score: float # 覆盖率 0-1
relevant_content: List[dict] # 相关内容块
suggestions: List[str] # 优化建议
class GapAnalysisEngine:
"""差距分析引擎"""
def __init__(self, vector_store, embedder, llm_client):
self.vector_store = vector_store
self.embedder = embedder
self.llm = llm_client
async def analyze(
self,
keyword: str,
platform: str,
ai_answer: str,
top_k: int = 5
) -> GapAnalysisResult:
"""分析内容差距"""
# 1. 将 AI 回答向量化
answer_embedding = await self.embedder.embed(ai_answer)
# 2. 在内容库中检索相关内容
search_results = await self.vector_store.search(
collection="content_vectors",
query_vector=answer_embedding,
top_k=top_k,
filter_expr=f'tenant_id == "{tenant_id}"'
)
# 3. 提取 AI 回答中的主题
ai_topics = await self._extract_topics(ai_answer)
# 4. 提取已有内容覆盖的主题
covered_topics = await self._extract_topics_from_results(search_results)
# 5. 计算差距
missing_topics = [t for t in ai_topics if t not in covered_topics]
coverage_score = 1 - (len(missing_topics) / max(len(ai_topics), 1))
# 6. 生成优化建议
suggestions = await self._generate_suggestions(
keyword, ai_answer, covered_topics, missing_topics
)
return GapAnalysisResult(
keyword=keyword,
platform=platform,
ai_answer=ai_answer,
covered_topics=covered_topics,
missing_topics=missing_topics,
coverage_score=coverage_score,
relevant_content=[
{"url": r["url"], "title": r["title"], "score": r["score"]}
for r in search_results
],
suggestions=suggestions
)
async def _extract_topics(self, text: str) -> List[str]:
"""从文本中提取主题(使用 LLM)"""
prompt = f"""分析以下文本,提取其中的核心主题(每个主题用短语表示)。
只返回主题列表,每行一个,不要额外解释。
文本:
{text}
主题:"""
response = await self.llm.chat(
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
topics = [t.strip() for t in response.split("\n") if t.strip()]
return topics
async def _extract_topics_from_results(self, results: List[dict]) -> List[str]:
"""从检索结果中提取主题"""
all_topics = []
for r in results:
text = r.get("chunk_text", "")
topics = await self._extract_topics(text)
all_topics.extend(topics)
return list(set(all_topics))
async def _generate_suggestions(
self, keyword, ai_answer, covered, missing
) -> List[str]:
"""生成优化建议"""
prompt = f"""你是 AI GEO 内容优化专家。分析以下差距,给出具体的优化建议。
关键词: {keyword}
AI 搜索回答中涉及的主题: {', '.join(missing)}
已有内容覆盖的主题: {', '.join(covered)}
请给出 3-5 条具体可执行的优化建议,包括:
1. 需要补充的内容主题
2. 内容结构调整建议
3. 提升被 AI 引用概率的具体方法
建议:"""
response = await self.llm.chat(
messages=[{"role": "user", "content": prompt}],
temperature=0.7
)
return [s.strip() for s in response.split("\n") if s.strip()]
3.2 语义相似度计算
python
import numpy as np
from typing import List
class SemanticSimilarity:
"""语义相似度计算"""
@staticmethod
def cosine_similarity(vec_a: List[float], vec_b: List[float]) -> float:
"""余弦相似度"""
a = np.array(vec_a)
b = np.array(vec_b)
dot = np.dot(a, b)
norm = np.linalg.norm(a) * np.linalg.norm(b)
return float(dot / norm) if norm > 0 else 0.0
@staticmethod
def max_similarity(query_vec: List[float], doc_vecs: List[List[float]]) -> float:
"""查询向量与文档向量集的最大相似度"""
return max(
SemanticSimilarity.cosine_similarity(query_vec, dv)
for dv in doc_vecs
)
@staticmethod
def average_similarity(query_vec: List[float], doc_vecs: List[List[float]], top_k: int = 5) -> float:
"""Top-K 平均相似度"""
scores = [
SemanticSimilarity.cosine_similarity(query_vec, dv)
for dv in doc_vecs
]
scores.sort(reverse=True)
return np.mean(scores[:top_k]) if scores else 0.0
四、优化建议生成器
4.1 建议模型
python
from enum import Enum
class SuggestionType(Enum):
ADD_TOPIC = "add_topic" # 新增主题内容
ENHANCE_STRUCTURE = "enhance_structure" # 增强内容结构
ADD_FAQ = "add_faq" # 添加 FAQ
UPDATE_STATS = "update_stats" # 更新数据
ADD_CITATION = "add_citation" # 增加权威引用
RESTRUCTURE = "restructure" # 重新组织内容
@dataclass
class OptimizationSuggestion:
suggestion_id: str
keyword: str
platform: str
type: SuggestionType
title: str
description: str
priority: int # 1-5, 5最高
expected_impact: str
action_items: List[str]
created_at: str
4.2 建议生成器
python
class SuggestionGenerator:
"""优化建议生成器"""
def __init__(self, llm_client):
self.llm = llm_client
async def generate(
self,
gap_result: GapAnalysisResult,
brand_name: str
) -> List[OptimizationSuggestion]:
"""基于差距分析生成优化建议"""
suggestions = []
# 1. 缺失主题建议
for topic in gap_result.missing_topics:
suggestion = await self._generate_topic_suggestion(
topic, gap_result, brand_name
)
suggestions.append(suggestion)
# 2. 结构优化建议
if gap_result.coverage_score < 0.5:
suggestions.append(await self._generate_structure_suggestion(gap_result))
# 3. FAQ 建议
suggestions.append(await self._generate_faq_suggestion(gap_result, brand_name))
# 按优先级排序
suggestions.sort(key=lambda x: x.priority, reverse=True)
return suggestions
async def _generate_topic_suggestion(
self, topic: str, gap: GapAnalysisResult, brand: str
) -> OptimizationSuggestion:
prompt = f"""为品牌「{brand}」生成一条 AI GEO 内容优化建议。
关键词: {gap.keyword}
缺失主题: {topic}
当前覆盖率: {gap.coverage_score:.0%}
请生成:
1. 标题(简短描述优化方向)
2. 详细描述(为什么需要这个内容,如何影响 AI 搜索引用)
3. 3-5 个具体行动项
4. 预期影响
格式:JSON"""
response = await self.llm.chat(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
response_format={"type": "json_object"}
)
import json
data = json.loads(response)
return OptimizationSuggestion(
suggestion_id=f"sug_{hash(topic) % 100000}",
keyword=gap.keyword,
platform=gap.platform,
type=SuggestionType.ADD_TOPIC,
title=data["title"],
description=data["description"],
priority=4,
expected_impact=data.get("expected_impact", ""),
action_items=data.get("action_items", []),
created_at=datetime.now().isoformat()
)
五、效果追踪闭环
python
class OptimizationTracker:
"""优化效果追踪器"""
def __init__(self, postgres):
self.postgres = postgres
async def track_optimization(
self,
suggestion_id: str,
keyword: str,
platform: str,
before_score: float
):
"""记录优化前的基线"""
await self.postgres.execute(
"""
INSERT INTO optimization_tracking
(suggestion_id, keyword, platform, before_score, status, created_at)
VALUES ($1, $2, $3, $4, 'pending', NOW())
""",
suggestion_id, keyword, platform, before_score
)
async def update_after_score(
self, suggestion_id: str, after_score: float
):
"""更新优化后的分数"""
await self.postgres.execute(
"""
UPDATE optimization_tracking
SET after_score = $1, status = 'completed', updated_at = NOW()
WHERE suggestion_id = $2
""",
after_score, suggestion_id
)
async def get_optimization_report(self, tenant_id: str) -> dict:
"""获取优化效果报告"""
rows = await self.postgres.fetch(
"""
SELECT
COUNT(*) as total,
COUNT(*) FILTER (WHERE after_score > before_score) as improved,
AVG(after_score - before_score) as avg_improvement
FROM optimization_tracking ot
JOIN collection_tasks ct ON ot.keyword = ct.keyword
WHERE ct.tenant_id = $1
""",
tenant_id
)
if rows:
r = rows[0]
return {
"total_optimizations": r["total"],
"improved_count": r["improved"],
"success_rate": r["improved"] / r["total"] if r["total"] > 0 else 0,
"avg_score_improvement": float(r["avg_improvement"] or 0)
}
return {}
六、架构决策记录
ADR-005: 选择语义分块而非固定长度分块
背景:内容入库需要分块,固定长度会割裂语义。
决策:采用语义分块(按段落 + 句子边界),设置 overlap。
后果:
- ✅ 语义完整性更好,检索质量更高
- ✅ overlap 减少边界信息丢失
- ❌ 分块大小不均匀,需要额外处理
- ❌ 处理速度略慢于固定分块
ADR-006: 主题提取使用 LLM 而非规则
背景:差距分析需要从 AI 回答和内容中提取主题。
决策:使用 LLM 提取主题,而非关键词提取算法。
后果:
- ✅ 主题理解更准确,能捕捉隐含主题
- ✅ 可处理多语言、多领域内容
- ❌ 成本较高,延迟较高
- ❌ LLM 输出不稳定,需要后处理
七、总结
RAG 内容优化引擎是 AI GEO 的核心差异化能力:
- 内容入库:语义分块 + 向量化 + 多维索引
- 差距分析:AI 回答 vs 自有内容的语义对比
- 建议生成:LLM 驱动的可执行优化建议
- 效果追踪:优化前后评分对比闭环
- 语义检索:向量相似度计算覆盖度
本文为 AI GEO 系统架构系列第 4 篇,共 15 篇。