最近面试被问到一个问题:如果要判断某个医疗器械品牌为什么在 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 个问题:

  1. 服务商说优化有效,但 AI 换个平台就搜不到;

  2. 品牌词能搜到,行业词完全没影;

  3. 竞品文章没你多,却总被 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_tasksengine_resultsbrand_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 证据里找到原因。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐