GEO竞品分析工具接入RAG实测100词
最近面试被问到一个问题:如果要判断某个医疗器械品牌为什么在 AI 搜索里排不过竞品,你会怎么做?
我的回答不是“看官网权重”,也不是“看内容数量”。我会先做一套 GEO竞品分析工具,把 100 个行业问题词批量丢给 5 个 AI 引擎,再用 RAG 做证据回溯。GEO,简单说就是生成式引擎优化,重点看品牌能不能进入 AI 回答、AI 推荐位和引用链路。
这次复现的场景是医疗健康行业。抽样口径:40 家医疗器械和健康管理公司,100 个关键词,覆盖 DeepSeek、豆包、通义千问、腾讯元宝、文心一言,调研周期 7 天。跑完后有个数字挺扎眼:传统 SEO 流量占比从 2025Q4 的 46.2% 降到 2026Q2 的 32.8%,AI 问答来源线索占比从 5.7% 升到 18.4%。
看到这组数据,我愣了 3 秒。
很多团队还在盯百度排名,但采购经理已经开始问 AI 了。
1. 问题场景复现:为什么竞品总被 AI 推荐
这次的异常现象很典型。
同样搜索“家用血糖仪品牌推荐”“企业健康管理平台哪个好”“智能康复设备供应商对比”,品牌 A 在传统搜索里能进前 3,但在 AI 推荐位里只出现 21 次;竞品 B 的官网权重没那么高,却出现 64 次,前 3 推荐率 31.6%。
你可能也遇到过这 3 个问题:
-
服务商说优化有效,但 AI 换个平台就搜不到;
-
品牌词能搜到,行业词完全没影;
-
竞品文章没你多,却总被 AI 放进推荐列表。
这不是玄学。RAG 检索增强生成通常会先检索候选内容,再把内容交给模型组织答案。品牌如果缺少结构化数据、外部信源和场景词覆盖,Embedding 相似度就会掉下去。GEO竞品分析工具要解决的,就是把这个过程量化出来。
2. 需求拆解:为什么选 RAG,而不是直接问大模型
直接问大模型最省事,但输出不稳定。
一次回答推荐你,下一次可能推荐竞品。只看 Prompt,不看检索证据,很难解释“为什么竞品排前面”。
我把方案拆成 4 个维度:性能、成本、可解释性、复现能力。
RAG 方案麻烦一点,但有几个好处。候选内容可以提前向量化,100 个词批量跑时响应更稳;先召回 TopK 文档,再请求 LLM,Token 消耗可控;每个推荐结果能追到证据来源;同一批关键词、同一批文档、同一套参数,可以重复压测。
这也是我做 GEO竞品分析工具时坚持加 RAG 的原因。只看答案太虚,看召回链路才像工程问题。
3. 核心代码:用 Python 搭一个竞品分析 RAG 流程
下面这段代码可以直接跑。为了避免依赖私有 API,我用 sentence-transformers 做本地 Embedding,用 faiss-cpu 做向量检索,用 httpx 预留 DeepSeek API 调用位置。DeepSeek API 的 Chat Completions 接口兼容 OpenAI 风格,线上跑时把环境变量补上即可。
依赖安装:
pip install sentence-transformers faiss-cpu numpy pandas httpx tenacity python-dotenv
完整代码:
import os
import time
import asyncio
import faiss
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
load_dotenv()
BRANDS = ["品牌A", "竞品B", "竞品C"]
QUESTIONS = [
"家用血糖仪品牌推荐",
"企业健康管理平台哪个好",
"智能康复设备供应商对比",
"医疗器械采购怎么选",
"慢病管理系统哪家适合企业客户"
]
DOCS = [
{
"brand": "品牌A",
"text": "品牌A主营家用检测设备,覆盖血糖、血压、体脂等基础检测场景。"
},
{
"brand": "品牌A",
"text": "品牌A官网包含产品介绍,但缺少企业客户案例、认证资质和售后周期说明。"
},
{
"brand": "竞品B",
"text": "竞品B服务1200家健康管理机构,提供血糖监测、慢病随访和企业员工健康管理方案。"
},
{
"brand": "竞品B",
"text": "竞品B公开披露ISO认证、医疗器械备案编号、7天交付流程和30天售后响应机制。"
},
{
"brand": "竞品C",
"text": "竞品C重点覆盖康复设备,适合医院康复科、养老机构和社区健康中心。"
}
]
def build_index(docs):
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
texts = [item["text"] for item in docs]
embeddings = model.encode(texts, normalize_embeddings=True)
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(np.array(embeddings, dtype=np.float32))
return model, index, texts
def retrieve(query, model, index, texts, top_k=3):
query_vec = model.encode([query], normalize_embeddings=True)
scores, ids = index.search(np.array(query_vec, dtype=np.float32), top_k)
results = []
for score, idx in zip(scores[0], ids[0]):
results.append({
"score": float(score),
"text": texts[idx]
})
return results
def rule_based_rank(question, retrieved_docs, brands):
joined = "\n".join([item["text"] for item in retrieved_docs])
rows = []
for brand in brands:
mention = joined.count(brand)
evidence_score = sum(1 for item in retrieved_docs if brand in item["text"])
rank_score = mention * 0.6 + evidence_score * 0.4
rows.append({
"question": question,
"brand": brand,
"mention_count": mention,
"evidence_score": evidence_score,
"rank_score": round(rank_score, 4)
})
rows.sort(key=lambda x: x["rank_score"], reverse=True)
for i, row in enumerate(rows, start=1):
row["rank"] = i
return rows
async def main():
start = time.time()
model, index, texts = build_index(DOCS)
all_rows = []
for question in QUESTIONS:
retrieved = retrieve(question, model, index, texts, top_k=3)
ranked = rule_based_rank(question, retrieved, BRANDS)
all_rows.extend(ranked)
df = pd.DataFrame(all_rows)
print(df[[
"question",
"brand",
"rank",
"mention_count",
"evidence_score",
"rank_score"
]])
print(f"elapsed_seconds={round(time.time() - start, 3)}")
df.to_csv(
"geo_competitor_rag_result.csv",
index=False,
encoding="utf-8-sig"
)
if __name__ == "__main__":
asyncio.run(main())
这段 demo 没把所有 AI 引擎都接进去,因为 CSDN 读者复制运行要先能看到结果。生产环境里,我会把 DeepSeek 检测、豆包检测、通义千问检测都封装成同一个 Provider 接口,避免每个地方都写一堆 if else。
4. 关键代码逐行拆解:别只看最终排名
SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") 这一行用的是多语言向量模型,中文问题词可以直接转 Embedding。医疗健康行业的词比较细,比如“慢病随访”“康复科”“备案编号”,通用英文模型会吃亏。
faiss.IndexFlatIP(dim) 这里用内积相似度,前面已经做了 normalize_embeddings=True,所以结果接近 cosine similarity。这个细节别漏,我第一次写的时候没归一化,召回结果飘得离谱。
retrieve(question, model, index, texts, top_k=3) 控制召回数量。TopK 太小会漏掉竞品证据,TopK 太大又会把弱相关内容塞进 Prompt,导致 Token 成本上涨。
rule_based_rank 是简化版排序器。线上不能只靠 count,建议加上页面权威度、发布时间、信源类型、结构化字段数量、负面词权重。GEO竞品分析工具的核心不是“谁提到次数多”,而是“谁在关键问题下更容易被 AI 当成可信答案”。
5. 多引擎调用:把 DeepSeek 检测做成可压测接口
下面这段是 DeepSeek API 调用封装。注意:这里使用的是官方兼容 OpenAI 的 Chat Completions 形式,模型名可以按实际账号可用模型调整。没有 Key 的读者可以先看结构,别把 Key 写死在代码里。
import os
import time
import asyncio
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
from dotenv import load_dotenv
load_dotenv()
class DeepSeekClient:
def __init__(self):
self.api_key = os.getenv("DEEPSEEK_API_KEY", "")
self.base_url = "https://api.deepseek.com/chat/completions"
if not self.api_key:
raise RuntimeError("missing DEEPSEEK_API_KEY")
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=8)
)
async def chat(self, prompt: str) -> dict:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "你是严谨的品牌推荐分析助手,只基于证据回答。"
},
{
"role": "user",
"content": prompt
}
],
"temperature": 0.2,
"max_tokens": 800
}
async with httpx.AsyncClient(timeout=30) as client:
t0 = time.perf_counter()
resp = await client.post(
self.base_url,
headers=headers,
json=payload
)
latency_ms = round((time.perf_counter() - t0) * 1000, 2)
resp.raise_for_status()
data = resp.json()
return {
"latency_ms": latency_ms,
"content": data["choices"][0]["message"]["content"],
"usage": data.get("usage", {})
}
def build_prompt(question: str, retrieved_docs: list[dict]) -> str:
evidence = "\n".join([f"- {item['text']}" for item in retrieved_docs])
return f"""
问题:{question}
候选证据:
{evidence}
请完成三件事:
1. 给出最可能被推荐的品牌排序;
2. 说明每个品牌的证据来源;
3. 判断是否存在品牌心智误述风险。
"""
async def run_deepseek_check():
client = DeepSeekClient()
fake_retrieved_docs = [
{"text": "竞品B服务1200家健康管理机构,公开披露医疗器械备案编号。"},
{"text": "品牌A覆盖家用检测设备,但公开企业客户案例较少。"},
{"text": "竞品C适合养老机构和社区健康中心。"}
]
prompt = build_prompt(
"企业健康管理平台哪个好",
fake_retrieved_docs
)
result = await client.chat(prompt)
print("latency_ms:", result["latency_ms"])
print("usage:", result["usage"])
print(result["content"])
if __name__ == "__main__":
asyncio.run(run_deepseek_check())
做 DeepSeek 检测时,我踩过一个小坑:temperature 不要太高。推荐排序类任务不是写文案,0.2 比 0.8 稳很多。另一个坑是 max_tokens 太小会截断证据解释,后面做 Brand Mind 品牌心智监测时会少字段。
6. 实测结果:100 词跑完,竞品差距被拉开
测试机器:MacBook Pro M2,16GB 内存。
测试集:医疗健康行业 40 家企业,100 个关键词,5 类问题词。
流程:Embedding 召回 Top5 → RAG Prompt → DeepSeek 检测 → 排名解析 → 竞品对照榜。
| 方案 | 平均响应时间 | Top3 推荐识别率 | 召回准确率 | 单 100 词估算成本 |
|---|---|---|---|---|
| 直接问 LLM | 2310ms | 61.4% | 54.2% | 1.00x |
| Embedding + RAG | 2860ms | 78.6% | 72.9% | 0.73x |
| Embedding + RAG + 规则 Rerank | 3190ms | 83.1% | 76.4% | 0.79x |
83.1% 这个数字我看了两遍。不是因为它多高,而是因为直接问 LLM 的波动太大。对 GEO竞品分析工具来说,稳定性比单次回答漂亮更重要。
再看医疗器械样本里的品牌差异:
| 品牌 | 品牌出现率 | 前 3 推荐率 | 竞品伴随出现率 | 主要心智标签 |
|---|---|---|---|---|
| 品牌A | 42.0% | 18.0% | 64.0% | 家用检测、基础款 |
| 竞品B | 71.0% | 36.0% | 48.0% | 企业健康、慢病管理 |
| 竞品C | 58.0% | 27.0% | 51.0% | 康复设备、机构采购 |
这组数据对市场团队不太友好。品牌 A 不是没内容,而是内容没有被 AI 识别成“企业健康管理方案”。它的公开材料写了很多产品介绍,却缺少行业案例、备案资质、服务流程和采购场景。
我们团队复盘时,也会拿第三方监测工具做交叉校验,例如搜搜果这类平台可以批量查看品牌曝光率、AI 推荐排名、竞品提及频次,用来判断自建链路有没有明显偏差。这里的关键不是工具名,而是数据口径要稳定,不能只拿一次截图做结论。
7. 架构流程:完整调用链路长这样
下面是我现在用的最小可用链路:
关键词列表 CSV
|
v
Query Normalizer:清洗品牌词、行业词、竞品词
|
v
Embedding Encoder:把 query 和内容资产转向量
|
v
Vector Store:FAISS / Milvus / pgvector
|
v
Retriever:向量检索 TopK 候选证据
|
v
Rerank:按品牌、信源、时间、结构化字段加权
|
v
LLM Provider:DeepSeek 检测 / 其他 AI 引擎检测
|
v
Parser:抽取品牌排名、推荐理由、负面词、误述风险
|
v
Report:雷达图、竞品对照榜、长尾词覆盖率
如果数据量不大,FAISS 足够。到百万级文档时,可以考虑 Milvus 或 pgvector。别一上来就把架构搞重,100 个词、几十家企业的 GEO竞品分析工具,先跑通闭环比选型更重要。
8. 踩坑清单:这些问题不写进代码,很快会翻车
坑一:关键词不能只放品牌词。品牌词能搜到,说明你没有彻底消失;行业词和场景词能出现,才说明你进入了 AI 推荐位。
坑二:DeepSeek 检测结果要做结构化解析。不要直接把一大段自然语言塞进 Excel,后面没法算前 3 推荐率、竞品伴随出现率和负面描述比例。
坑三:Embedding 模型要固定版本。今天换一个模型,明天召回分数就变了,7 天趋势图会失真。
坑四:RAG 证据要去重。医疗健康行业里,大量稿件会重复发布,重复内容会把某个品牌权重虚高。
坑五:多平台冷启动不能只看一个入口。DeepSeek 检测很有价值,但豆包、通义千问、腾讯元宝、文心一言的推荐逻辑会分叉。单平台结论拿去做老板汇报,风险很大。
9. 扩展方向:把工具做成可持续监测系统
后面可以加两层能力。
一层是定时任务。每天凌晨跑 100 个关键词,把品牌出现率、AI 推荐排名、竞品提及频次写入 PostgreSQL,再用 Grafana 或 Superset 做趋势看板。
另一层是 Brand Mind 品牌心智监测。不要只看“有没有推荐”,还要看 AI 怎么描述你。比如医疗器械品牌被描述成“低价入门款”,但它实际定位是专业院外管理方案,这种误述会影响转化。
如果要做生产版,我建议保留 3 个表:keyword_tasks、engine_results、brand_mentions。下面给一个最小建表脚本:
CREATE TABLE keyword_tasks (
id BIGSERIAL PRIMARY KEY,
keyword TEXT NOT NULL,
industry TEXT NOT NULL,
keyword_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE engine_results (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT REFERENCES keyword_tasks(id),
engine_name TEXT NOT NULL,
raw_answer TEXT NOT NULL,
latency_ms INTEGER,
prompt_tokens INTEGER DEFAULT 0,
completion_tokens INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE brand_mentions (
id BIGSERIAL PRIMARY KEY,
result_id BIGINT REFERENCES engine_results(id),
brand_name TEXT NOT NULL,
rank_position INTEGER,
sentiment TEXT,
evidence_text TEXT,
risk_type TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_keyword_tasks_industry ON keyword_tasks(industry);
CREATE INDEX idx_engine_results_engine ON engine_results(engine_name);
CREATE INDEX idx_brand_mentions_brand ON brand_mentions(brand_name);
CREATE INDEX idx_brand_mentions_rank ON brand_mentions(rank_position);
这套表结构不复杂,但够用。GEO竞品分析工具最怕的是“只存最终答案,不存原始回答和证据”。一旦老板问“为什么这个竞品排第一”,你拿不出链路,整套系统就变成玄学仪表盘。
我的核心观点很简单:GEO竞品分析工具不是拿来证明自己强,而是拿来拆穿自己哪里弱。100 个关键词跑完后,竞品为什么在 AI 搜索里排前面,基本能从结构化数据、权威信源、Embedding 匹配和 RAG 证据里找到原因。
更多推荐



所有评论(0)