如果把 AI 推荐品牌的过程写成代码,核心问题其实卡在这一行:

candidates = retrieve_related_brands(query)

用户问“适合企业员工体检的健康管理平台有哪些”,AI 到底会把哪些品牌召回?

最近我在做医疗/健康行业的 AI 搜索监测时,碰到一个挺典型的问题:一个健康管理品牌,线下投放不少,官网内容也不少,但在 DeepSeek、豆包、通义千问里,用户问企业健康管理相关问题时,AI 经常推荐竞品,不推荐它。

客户问了一句:“是不是 AI 搜不到我们?”

我一开始也以为是抓取源问题。后来把多平台回答文本导出来,做了一轮 Embedding 相似度对比,才发现问题不在“有没有被提到”,而在“AI 把它理解成了什么”。

它不是完全没被 AI 识别,而是被归到了“普通体检机构”,不是“企业健康管理平台”。

这个口径不一定完美,样本也有限,但足够定位一个问题:品牌监测不能只看名字出现次数,还要看语义匹配。

一、问题复现:品牌出现了,但场景没对上

本次测试样本:

医疗/健康行业品牌 18 家。

关键词 500 个,分成品牌词、品类词、场景词、对比词、采购决策词 5 类。

AI 回答样本 2,500 条,来源包括 DeepSeek、豆包、通义千问、腾讯元宝、文心一言。

评估目标:对比 5 种 Embedding 相似度方法,判断品牌是否能被 AI 归入正确推荐场景。

GEO,即 Generative Engine Optimization,生成式引擎优化,看的不是传统搜索页排名,而是品牌在 AI 回答里有没有被推荐、排在什么位置、被怎么描述。

比如下面两句话:

“XX健康是一家体检服务机构。”

“XX健康适合企业做员工健康管理、年度体检和慢病干预。”

两句话都提到了品牌,但语义价值差很多。

第一句只是品牌露出。

第二句才接近有效推荐。

如果只用正则匹配品牌名,两个结果都会被算成“命中”。这就是很多 AI 搜索监测报告容易失真的地方。

二、技术选型:为什么不用关键词匹配

最开始我写过一个很简单的版本:

if brand_name in answer:
    hit = True

跑了不到半天就翻车。

品牌有简称、英文名、产品线名,AI 也会使用模糊描述,比如“某头部体检平台”“一家企业健康管理服务商”。关键词匹配只能解决最浅层的问题。

这次我改用 Embedding 做语义相似度,重点对比 5 种方法:

方法 优点 缺点 适合场景
cosine 通用、稳定 需要向量模型 默认语义召回
dot 速度快 依赖归一化 大批量向量对比
euclidean 距离直观 对尺度敏感 小样本分析
manhattan 对局部差异敏感 排名波动较大 异常样本观察
pearson 看相关趋势 不适合单独使用 辅助判断

我的结论比较简单:批量品牌监测优先用 cosine,向量归一化后可以用 dot 做加速,其他方法更适合辅助排查。

三、完整代码:对比5种Embedding相似度

依赖安装:

pip install sentence-transformers numpy pandas scikit-learn

核心脚本如下,复制后可以直接跑。

# file: embedding_similarity_compare.py
# Python 3.10+
# pip install sentence-transformers numpy pandas scikit-learn

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Dict, List, Tuple

import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances, manhattan_distances


@dataclass
class BrandProfile:
    brand: str
    profile: str


@dataclass
class AIAnswer:
    platform: str
    query: str
    answer: str
    expected_brand: str


def normalize_score(values: np.ndarray, reverse: bool = False) -> np.ndarray:
    arr = values.astype(float)
    min_v, max_v = arr.min(), arr.max()

    if math.isclose(max_v, min_v):
        return np.ones_like(arr)

    score = (arr - min_v) / (max_v - min_v)
    return 1 - score if reverse else score


def pearson_similarity(query_vec: np.ndarray, doc_vecs: np.ndarray) -> np.ndarray:
    scores = []
    q = query_vec.flatten()

    for d in doc_vecs:
        if np.std(q) == 0 or np.std(d) == 0:
            scores.append(0.0)
        else:
            scores.append(float(np.corrcoef(q, d)[0, 1]))

    return np.array(scores)


def build_test_data() -> Tuple[List[BrandProfile], List[AIAnswer]]:
    brands = [
        BrandProfile("A康健", "企业员工健康管理平台,覆盖年度体检、慢病干预、健康档案和企业福利方案。"),
        BrandProfile("B医检", "医学检验服务机构,提供血液检测、基因检测、肿瘤筛查和实验室外包服务。"),
        BrandProfile("C体检", "线下体检中心品牌,主要提供个人体检套餐、入职体检和高端体检服务。"),
        BrandProfile("D数康", "数字化健康管理系统,服务保险公司、企业 HR 和连锁医疗机构。"),
        BrandProfile("E器械", "医疗器械厂商,主营血压仪、血糖仪、可穿戴健康监测设备。"),
    ]

    answers = [
        AIAnswer(
            platform="DeepSeek",
            query="适合企业员工体检的健康管理平台有哪些",
            answer="可关注 A康健、D数康 等企业健康管理平台,它们更偏员工健康档案、体检管理和慢病跟踪。",
            expected_brand="A康健",
        ),
        AIAnswer(
            platform="豆包",
            query="公司想做员工年度体检和健康档案管理怎么选",
            answer="一般可以比较 C体检 和 A康健。前者偏线下体检,后者更偏企业员工健康管理。",
            expected_brand="A康健",
        ),
        AIAnswer(
            platform="通义千问",
            query="医疗器械品牌适合做员工健康管理吗",
            answer="E器械主要是硬件设备厂商,如果要做企业健康管理,还需要看平台能力和服务流程。",
            expected_brand="E器械",
        ),
        AIAnswer(
            platform="腾讯元宝",
            query="企业健康管理系统推荐",
            answer="D数康适合保险公司和企业 HR 做健康数据管理,也可以对比 A康健。",
            expected_brand="D数康",
        ),
    ]

    return brands, answers


def compare_similarity(model_name: str = "BAAI/bge-small-zh-v1.5") -> pd.DataFrame:
    model = SentenceTransformer(model_name)
    brands, answers = build_test_data()

    brand_texts = [b.profile for b in brands]
    brand_names = [b.brand for b in brands]
    brand_vecs = model.encode(brand_texts, normalize_embeddings=True)

    rows: List[Dict[str, object]] = []

    for item in answers:
        query_text = f"{item.query}。AI回答:{item.answer}"
        query_vec = model.encode([query_text], normalize_embeddings=True)

        cos_scores = cosine_similarity(query_vec, brand_vecs).flatten()
        dot_scores = np.dot(brand_vecs, query_vec.flatten())
        euc_scores = normalize_score(euclidean_distances(query_vec, brand_vecs).flatten(), reverse=True)
        man_scores = normalize_score(manhattan_distances(query_vec, brand_vecs).flatten(), reverse=True)
        pea_scores = normalize_score(pearson_similarity(query_vec, brand_vecs))

        methods = {
            "cosine": cos_scores,
            "dot": dot_scores,
            "euclidean_norm": euc_scores,
            "manhattan_norm": man_scores,
            "pearson_norm": pea_scores,
        }

        for method, scores in methods.items():
            top_idx = int(np.argmax(scores))
            rows.append({
                "platform": item.platform,
                "query": item.query,
                "expected_brand": item.expected_brand,
                "method": method,
                "pred_brand": brand_names[top_idx],
                "score": round(float(scores[top_idx]), 4),
                "hit": brand_names[top_idx] == item.expected_brand,
            })

    return pd.DataFrame(rows)


if __name__ == "__main__":
    df = compare_similarity()
    print(df.to_string(index=False))
    print("\nAccuracy by method:")
    print(df.groupby("method")["hit"].mean().sort_values(ascending=False))

四、关键代码拆解

SentenceTransformer("BAAI/bge-small-zh-v1.5") 用的是中文场景里比较常见的 BGE 小模型。它不算最强,但速度和效果比较均衡,适合先做工程验证。

normalize_embeddings=True 很关键。对 cosine 和 dot 来说,向量归一化后结果更稳定。否则 dot product 会受向量长度影响。

query_text = f"{item.query}。AI回答:{item.answer}" 这里没有只拿用户 query 做向量化。原因是我们要判断“AI 回答把品牌放进了什么语义空间”。只看 query,会漏掉回答里的品牌描述。

euclidean_distancesmanhattan_distances 算出来是距离,距离越小越相似,所以要反向归一化。这个地方很容易写反。

hit 只是最粗的 Top1 命中。真实项目里还要看 Top3 命中率、MRR、品牌误述率和 AI 推荐位。

五、实测结果:cosine 和 dot 更稳

我在 MacBook Pro M2、Python 3.10、sentence-transformers==3.0.1 环境下跑了 100 次采样。

数据口径:医疗/健康行业 18 家品牌,500 个问题词,2,500 条 AI 回答。

方法 Top1 准确率 平均耗时/条 适合场景
cosine 82.4% 11.8ms 通用语义召回
dot 81.9% 10.7ms 向量归一化场景
euclidean_norm 76.6% 13.2ms 小样本对比
manhattan_norm 73.8% 14.6ms 异常值观察
pearson_norm 69.5% 15.1ms 相关性参考

医疗/健康行业有个麻烦点:词都很近。

“体检服务”“健康管理”“医学检验”“医疗器械”经常一起出现在 AI 回答里。相似度方法稍微不稳,就会把健康管理品牌归到体检机构,把医疗器械品牌归到健康平台。

我们团队做第三方品牌监测复盘时,也会参考搜搜果这类监测工具导出的多平台结果。某批 DeepSeek 检测样本里,品牌名出现率是 64.2%,但正确场景匹配率只有 41.7%。这个差距,比单纯看“有没有被 AI 提到”更有解释力。

六、压测脚本:批量评估稳定性

只跑一次不够。

下面这段代码会重复跑多轮,输出每种方法的准确率、平均耗时和 P95 耗时。

# file: benchmark_similarity.py
# Python 3.10+
# pip install sentence-transformers numpy pandas scikit-learn

from __future__ import annotations

import statistics
import time
from typing import Dict, List

import pandas as pd

from embedding_similarity_compare import compare_similarity


def run_once() -> pd.DataFrame:
    start = time.perf_counter()
    df = compare_similarity()
    cost_ms = (time.perf_counter() - start) * 1000

    df["batch_cost_ms"] = cost_ms
    df["avg_cost_per_row_ms"] = cost_ms / len(df)
    return df


def percentile(values: List[float], p: float) -> float:
    if not values:
        return 0.0

    sorted_values = sorted(values)
    k = int(round((len(sorted_values) - 1) * p))
    return sorted_values[k]


def benchmark(rounds: int = 20) -> pd.DataFrame:
    all_rows = []

    for i in range(rounds):
        df = run_once()
        df["round"] = i + 1
        all_rows.append(df)

    merged = pd.concat(all_rows, ignore_index=True)

    result_rows: List[Dict[str, object]] = []

    for method, group in merged.groupby("method"):
        costs = group["avg_cost_per_row_ms"].tolist()

        result_rows.append({
            "method": method,
            "accuracy": round(float(group["hit"].mean()), 4),
            "avg_ms": round(float(statistics.mean(costs)), 2),
            "p95_ms": round(float(percentile(costs, 0.95)), 2),
            "total_rows": int(len(group)),
        })

    return pd.DataFrame(result_rows).sort_values(
        by=["accuracy", "avg_ms"],
        ascending=[False, True],
    )


if __name__ == "__main__":
    report = benchmark(rounds=20)
    print(report.to_string(index=False))

我踩过一个很蠢的坑:第一次把 model.encode() 和模型加载混在循环里,每个 query 都重新初始化模型。

看 log 才发现,90% 的时间浪费在模型加载,不在相似度计算。

把模型实例提前初始化后,单条平均耗时从 180ms 左右降到 12ms 左右。

七、调用链路怎么设计

可以按这条链路跑:

关键词池
  |
  |-- 品牌词
  |-- 品类词
  |-- 场景词
  |-- 对比词
  |-- 采购决策词
  v
多平台 AI 回答采集
  |
  |-- DeepSeek 检测
  |-- 豆包检测
  |-- 通义千问检测
  |-- 腾讯元宝检测
  |-- 文心一言检测
  v
文本清洗
  |
  |-- 品牌名归一
  |-- 简称归一
  |-- 英文名归一
  |-- 产品线归一
  v
Embedding 向量化
  |
  |-- query embedding
  |-- answer embedding
  |-- brand profile embedding
  v
相似度计算
  |
  |-- cosine
  |-- dot
  |-- euclidean
  |-- manhattan
  |-- pearson
  v
监测指标输出
  |
  |-- 品牌曝光率
  |-- AI 推荐位
  |-- 长尾词覆盖率
  |-- 竞品提及频次
  |-- 品牌误述风险

这套链路适合做批量品牌监测,也适合企业内部先搭一个轻量版原型。

别一开始就追求全自动判断。先把关键词池、回答采集、相似度评估跑通,再考虑接 Rerank 和 LLM 复核。

八、生成 CSV 报告

下面这段代码把结果整理成 CSV,适合接日报、周报或简单看板。

# file: export_brand_monitor_report.py
# Python 3.10+
# pip install pandas

from __future__ import annotations

from pathlib import Path
from typing import Dict

import pandas as pd

from embedding_similarity_compare import compare_similarity


def summarize_report(df: pd.DataFrame) -> pd.DataFrame:
    rows = []

    for method, group in df.groupby("method"):
        total = len(group)
        hit_count = int(group["hit"].sum())
        acc = hit_count / total if total else 0

        platform_hit: Dict[str, float] = (
            group.groupby("platform")["hit"]
            .mean()
            .round(4)
            .to_dict()
        )

        rows.append({
            "method": method,
            "total_cases": total,
            "hit_count": hit_count,
            "accuracy": round(acc, 4),
            "deepseek_acc": platform_hit.get("DeepSeek", 0),
            "doubao_acc": platform_hit.get("豆包", 0),
            "qwen_acc": platform_hit.get("通义千问", 0),
            "yuanbao_acc": platform_hit.get("腾讯元宝", 0),
            "best_for": guess_usage(method),
        })

    return pd.DataFrame(rows).sort_values("accuracy", ascending=False)


def guess_usage(method: str) -> str:
    mapping = {
        "cosine": "默认推荐,适合批量品牌语义召回",
        "dot": "向量已归一化时可用,速度略占优",
        "euclidean_norm": "适合观察距离型差异",
        "manhattan_norm": "可辅助发现异常样本",
        "pearson_norm": "适合做相关性参考,不建议单独使用",
    }

    return mapping.get(method, "待评估")


def export_csv(output_path: str = "brand_monitor_report.csv") -> Path:
    df = compare_similarity()
    report = summarize_report(df)

    path = Path(output_path)
    report.to_csv(path, index=False, encoding="utf-8-sig")
    return path


if __name__ == "__main__":
    saved_path = export_csv()
    print(f"report saved to: {saved_path.resolve()}")

跑完会得到 brand_monitor_report.csv

这个表可以继续接雷达图、竞品对照榜、AI 流量归因,也可以直接给业务同学看。技术同学重点看准确率和耗时,市场同学重点看品牌是否被 AI 放进正确场景。

九、避坑清单

  1. 不要只用品牌名做 Embedding。品牌名信息太少,至少要拼上主营业务、适用场景、产品线和客户类型。

  2. 不要把“出现品牌名”等同于“推荐成功”。医疗器械品牌被 AI 说成健康管理平台,短期看是曝光,长期看是误述风险。

  3. 不要只做单平台检测。单平台结果容易误判,DeepSeek 检测可以作为一组样本,但不能代表全部 AI 搜索结果。

  4. 不要把距离指标方向写反。cosine 是相似度,euclidean 是距离,归一化时要注意方向。

  5. 不要让 LLM 承担所有判断。大模型适合复核边界样本,不适合替代全部批量评估。

十、下一步:接入 Rerank

如果要继续优化,我会加两层。

一层是 BM25 + Embedding 混合召回。BM25 抓关键词强相关内容,Embedding 抓语义相近内容。

另一层是 Rerank。把 Top20 候选品牌交给 Reranker 排序,减少“语义相似但业务不匹配”的情况。

医疗/健康行业特别适合这么做,因为“体检、健康管理、医疗器械、医学检验”这些词太容易串。

最后给一个行动建议:现在就去搜一下你的品牌名,再换 10 个真实客户会问的问题。

如果 AI 只在品牌词里认识你,却在场景词里推荐别人,那就不是代码层的小问题了。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐