要监控一个品牌在豆包、DeepSeek 这类 AI 回答里的提及率变化,算法本身不难,难在分母。题库里有的题几乎不产出品牌名单,有的题名单是题面自己带进来的,单天、单平台的数字又跳得厉害。这篇用 39 道中文题在 8 个国内 AI 平台连续 26 天的回答记录,分四步走:建表、筛题、量波动、按周汇总。每一步都给出 SQL 和原样输出,换成自己的数据可以直接跑。
先把结论放在这里:
- 出名单率低于 30% 的题不进提及率的分母。这批 39 道里有 13 道。
- 同一道题在同一平台上,相邻两天的名单只有约四分之一相同(中位数 0.26)。单天、单平台的涨跌不能当信号。
- 按周汇总后,同一个品牌在豆包上的提及率,从按天看的 9%~64%,收窄到每周 29%~44%。
数据长什么样
回答记录覆盖 2026-09-27 到 10-22,共 26 天。平台 8 个:DeepSeek、豆包、Kimi、通义千问、文心、讯飞星火、腾讯元宝、智谱。39 道题每天在每个平台各问一次,去掉请求失败的,成功回答 8,026 条。题目都属于同一个品类,按问法分成推荐、方法、对比、口碑四类(分法见下面脚本里的正则)。
每条记录是一行 JSON,本文用到的字段如下。示例行的格式和真实数据一致,产品名换成了占位符:
| 字段 | 含义 | 示例 |
|---|---|---|
| 查询词 | 问题原文 | 如何监控品牌在不同 AI 平台的提及率变化? |
| 平台 | 平台代号 | doubao |
| 日期 | 提问日期 | 2026-10-08 |
| 状态 | success / failed | success |
| 引用数量 | 回答附带的参考网页数,0 表示没联网 | 23 |
| 提及品牌详情 | 回答里被当作选项提到的产品,每行一个 | 品牌 X (情感:positive, 推荐:是) |
「提及品牌详情」怎么从回答里抽出来不在本文范围。举例时顺带提到的大公司名不算在里面。下文把这一列叫「品牌名单」。
第一步:导进 SQLite
几千到几万条记录,SQLite 足够了。不用起服务,一个文件就能带走,SQL 写错了改完重跑也只要几秒。建三张表:题目表 questions、回答表 answers、名单表 mentions(一条回答里的每个名字一行)。问法分类在入库时用正则打上,规则按顺序匹配,先中先得。几十道题的规模,正则比让模型分类好查错。
import json, re, sqlite3, sys
# 问法:按顺序匹配,先中先得,都不中算方法类
ASK_RULES = [
("推荐", r"哪家好|有没有工具|(有哪些|推荐|选择|什么)[^,?]{0,14}(工具|平台)|(工具|平台)[^,?]{0,4}(推荐|有哪些)"),
("对比", r"差异|对比|比较|区别"),
("口碑", r"口碑|评价|可信|好不好|靠谱"),
]
def ask_type(q):
return next((name for name, pat in ASK_RULES if re.search(pat, q)), "方法")
rows_file, questions_file = sys.argv[1], sys.argv[2]
qs = [l.strip() for l in open(questions_file, encoding="utf-8") if l.strip()]
qid = {
q: i for i, q in enumerate(qs)}
db = sqlite3.connect("answers.db")
db.executescript("""
DROP TABLE IF EXISTS questions; DROP TABLE IF EXISTS answers; DROP TABLE IF EXISTS mentions;
CREATE TABLE questions (qid INT PRIMARY KEY, ask TEXT, text TEXT);
CREATE TABLE answers (id INTEGER PRIMARY KEY, qid INT, platform TEXT, day TEXT,
cites INT, n_brands INT);
CREATE TABLE mentions (answer_id INT, brand TEXT);
""")
db.executemany("INSERT INTO questions VALUES (?, ?, ?)",
[(i, ask_type(q), q) for q, i in qid.items()])
for line in open(rows_file, encoding="utf-8"):
r = json.loads(line)
if r["状态"] != "success" or r["查询词"] not in qid:
continue
names = {
l.split(" (")[0].strip()
for l in (r["提及品牌详情"] or "").splitlines() if l.strip()}
cur = db.execute("INSERT INTO answers (qid, platform, day, cites, n_brands) "
"VALUES (?, ?, ?, ?, ?)",
(qid[r["查询词"]], r["平台"], r["日期"], r["引用数量"], len(names)))
db.executemany("INSERT INTO mentions VALUES (?, ?)",
[(cur.lastrowid, n) for n in names])
db.commit()
for t in ("questions", "answers", "mentions"):
print(t, db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0])
题目清单单独放在 questions.txt,一行一道。不在清单里的题(比如题面直接写了某个产品名的品牌词题)不入库,这类题的回答几乎必然提到那个名字,混进来会把提及率抬高。环境是 Python 3.11、SQLite 3.41。运行 python3 load.py rows.jsonl questions.txt:
questions 39
answers 8026
mentions 37421
第二步:先筛题,算出名单率和联网率
每道题先看两个数:
- 出名单率:品牌名单不为空的回答占比。低了,这道题就不产出要统计的东西,放进分母只会把提及率稀释掉。
- 联网率:回答带引用的比例。不联网的回答靠的是模型自己的知识,网上的内容变了,它也不会跟着变。有的平台整体就很少联网,算之前先剔掉,不然所有题都会被拉低。
-- 每道题:出名单率、联网率(剔掉整体联网率 < 50% 的平台)
WITH web AS (
SELECT platform FROM answers GROUP BY platform HAVING AVG(cites > 0) >= 0.5
), per_q AS (
SELECT a.qid, q.ask,
AVG(a.n_brands > 0) AS yield_rate,
AVG(CASE WHEN a.platform IN (SELECT platform FROM web)
THEN a.cites > 0 END) AS web_rate
FROM answers a JOIN questions q USING (qid)
GROUP BY a.qid
)
SELECT ask AS 问法, COUNT(*) AS 题数,
printf('%.0f%%', MIN(yield_rate) * 100) AS 出名单率最低,
printf('%.0f%%', MAX(yield_rate) * 100) AS 出名单率最高,
SUM(yield_rate < 0.3) AS 低于30的题,
printf('%.0f%%', AVG(web_rate) * 100) AS 平均联网率
FROM per_q GROUP BY ask ORDER BY 题数 DESC;
运行 sqlite3 -header -column answers.db < yield.sql:
问法 题数 出名单率最低 出名单率最高 低于30的题 平均联网率
-- -- ------ ------ ------ -----
方法 23 0% 93% 12 80%
推荐 11 86% 100% 0 96%
对比 3 50% 72% 0 96%
口碑 2 12% 71% 1 91%
被剔掉的平台是 Kimi 和讯飞,它们在这 39 道题上带引用的回答分别只占 20.1% 和 2.6%。
推荐类 11 道题,出名单率最低也有 86%。方法类 23 道两头分化:12 道低于 30%,最高的一道又有 93%。最低的一道是「同一个品牌有不同写法时如何做名称归一?」,207 次成功回答里有 205 次讲的是别名映射,只有 1 次给出了一个产品名,出名单率四舍五入是 0%。出名单率 93% 的那道是「如何监控竞品在 AI 搜索中的提及率?」,同样是方法类,AI 照样答出一串工具。所以光看问法定不了去留,一道题能不能产出名单,得问过才知道。
我们的处理是:出名单率低于 30% 的 13 道题移出提及率的分母;其中联网率高的,可以留在只统计引用来源的题库里。新题先放进试跑池跑两周,出名单率达标再转正。
第三步:量波动,算相邻两天名单重合
要判断一个变化是不是真的,先得知道不变的时候数字会晃多大。做法是:同一道题、同一个平台,拿今天的名单和明天的名单算 Jaccard 系数(交集大小除以并集大小),所有平台、所有日期取平均。两边名单都不为空才算一对,一道题凑够 30 对再给结果。
顺带算一个「名单来自题面」:名单里的名字有多少本来就写在题目里。
-- 同一道题、同一平台,相邻两天名单的 Jaccard;顺带算名单里有多少名字本来就写在题面上
WITH pair AS (
SELECT a.qid, a.id AS a_id, b.id AS b_id, a.n_brands AS na, b.n_brands AS nb
FROM answers a JOIN answers b
ON b.qid = a.qid AND b.platform = a.platform AND b.day = date(a.day, '+1 day')
WHERE a.n_brands > 0 AND b.n_brands > 0
), jac AS (
SELECT p.qid, 1.0 * COUNT(mb.brand) / (p.na + p.nb - COUNT(mb.brand)) AS j
FROM pair p
JOIN mentions ma ON ma.answer_id = p.a_id
LEFT JOIN mentions mb ON mb.answer_id = p.b_id AND mb.brand = ma.brand
GROUP BY p.a_id, p.b_id
), echo AS (
SELECT a.qid, AVG(instr(q.text, m.brand) > 0) AS from_question
FROM mentions m JOIN answers a ON a.id = m.answer_id JOIN questions q USING (qid)
GROUP BY a.qid
)
SELECT q.ask AS 问法, COUNT(*) AS 样本对, printf('%.2f', AVG(j)) AS 隔天重合,
printf('%.0f%%', e.from_question * 100) AS 名单来自题面
FROM jac JOIN questions q USING (qid) JOIN echo e USING (qid)
GROUP BY qid HAVING COUNT(*) >= 30
ORDER BY AVG(j) DESC LIMIT 6;
运行 sqlite3 -header -column answers.db < stability.sql(只显示前 6 行):
问法 样本对 隔天重合 名单来自题面
-- --- ---- ------
对比 54 0.90 89%
方法 30 0.44 0%
方法 70 0.38 0%
推荐 157 0.38 0%
推荐 194 0.35 0%
方法 86 0.33 0%
39 道题里有 28 道凑够了 30 对。
第一行最容易看走眼。它是「豆包、DeepSeek、元宝、Kimi 的品牌表现有什么差异?」,隔天重合 0.90,单看这个数会以为是最稳定的题。可它名单里 89% 的名字本来就写在题面上:回答在讲这几个平台,抽取时它们被当成品牌名记了下来。这种稳定是题目自己带来的,和推荐结果无关,统计前要先剔掉题面里出现的名字,或者把题改写。
第二行那道是「如何提高品牌在 AI 搜索中的被推荐概率?」,只凑够 30 对,出名单率也只有 25%,样本少,这个 0.44 不能当真。
去掉出名单率低于 30% 和名单主要来自题面的题,剩下 25 道,隔天重合的中位数是 0.26。也就是说,同一平台两天的名单合在一起,只有大约四分之一的名字两天都出现。今天在某个平台上没被提到,明天又出现,这在这批数据里是常态。
第四步:按周看同一个品牌的提及率
有了上面两步,就可以只用产出名单的推荐类题,算某个品牌在各平台的提及率,并把按天和按周的结果放在一起比。周从 09-27 起每 7 天一段;某个平台某一周样本不到 50 条就不显示。
-- 某个品牌在推荐类题上的提及率:按天 vs 按周(周从 09-27 起每 7 天一段)
WITH base AS (
SELECT a.platform, a.day,
CAST((julianday(a.day) - julianday('2026-09-27')) / 7 AS INT) AS wk,
EXISTS (SELECT 1 FROM mentions m
WHERE m.answer_id = a.id AND m.brand = :brand) AS hit
FROM answers a JOIN questions q USING (qid)
WHERE q.ask = '推荐'
), daily AS (
SELECT platform, day, AVG(hit) AS r FROM base GROUP BY platform, day
), weekly AS (
SELECT platform, wk, AVG(hit) AS r, COUNT(*) AS n FROM base GROUP BY platform, wk
)
SELECT d.platform AS 平台,
printf('%.0f%%~%.0f%%', MIN(d.r) * 100, MAX(d.r) * 100) AS 按天范围,
(SELECT group_concat(printf('%.0f%%', w.r * 100), ' / ')
FROM weekly w WHERE w.platform = d.platform AND w.n >= 50) AS 按周
FROM daily d GROUP BY d.platform ORDER BY d.platform;
运行时用 -cmd 把品牌名传给 :brand 参数,比如 sqlite3 -header -column -cmd ".parameter set :brand 你的品牌名" answers.db < weekly.sql。下面的输出用的是这批数据里被提及次数最多的一个产品,名字略去:
平台 按天范围 按周
-------- -------- ---------------------
deepseek 18%~83% 32% / 38% / 42%
doubao 9%~64% 35% / 36% / 29% / 44%
kimi 55%~100% 71% / 83% / 83% / 93%
qianwen 18%~73% 53% / 53% / 49% / 64%
wenxin 0%~18% 6% / 6% / 3% / 5%
xunfei 0%~27% 13% / 4% / 6% / 5%
yuanbao 73%~100% 91% / 87% / 87% / 95%
zhipu 82%~100% 94% / 95% / 96%
推荐类题每个平台每天最多 11 条回答,一条回答就是 9 个百分点,按天的范围因此很宽:豆包按天在 9%~64% 之间跳,按周是 35%、36%、29%、44%;千问按天 18%~73%,按周 53%、53%、49%、64%。按天看,几乎每天都能找出一个「大涨」或「大跌」;按周看,豆包前三周在 29%~36% 之间,千问在 49%~53% 之间,第四周两家都往上走了一截。注意第四周只有 10-18 到 10-22 五天,样本少一些。DeepSeek 和智谱的第四周有请求失败,样本分别只有 38 和 49 条,没有显示。
平台之间的差距比时间上的变化大得多:元宝、智谱每周都不低于 87%,文心、讯飞每周都不超过 13%。所以不要把几个平台的回答混在一起算一个总提及率再看涨跌,平台构成稍有变化(比如某个平台某天大量请求失败),总数就会跟着动。
Kimi 是另一种情况:四周是 71%、83%、83%、93%,按周看一路往上。但 Kimi 在这批数据里只有 20.1% 的回答联网,这个变化和网上新出的内容关系大不大,要打个问号。看变化时最好把同期的联网率一起列出来。
照着做的步骤
- 每天定时向各平台提问,把回答存成每行一条的 JSON,至少要有上面表格里的六个字段。
- 把题目清单写进 questions.txt,品牌词题单独放。
python3 load.py rows.jsonl questions.txt建库。- 跑 yield.sql,出名单率低于 30% 的题移出提及率题库。
- 跑 stability.sql,名单主要来自题面的题先处理掉,记下剩余题隔天重合的中位数,作为「不变时会晃多少」的基准。
- 跑 weekly.sql,按平台、按周看提及率,样本不到 50 条的周不下结论。
引用和说明
- Jaccard 系数的定义出自 Paul Jaccard 1901 年的论文《Étude comparative de la distribution florale dans une portion des Alpes et des Jura》,刊于 Bulletin de la Société Vaudoise des Sciences Naturelles 第 37 卷。
date(day, '+1 day')、julianday()的用法见 SQLite 官方文档「Date And Time Functions」一节;.parameter set见官方文档「Command Line Shell For SQLite」。- 回答记录是自己采集的,没有公开。文中只贴了题目原文和统计数字,没有贴回答里的产品名。