我做 GEO 监测时,为什么更愿意用 API 去模拟真实搜索回答
最近在整理 GEO 监测能力时,我重新看了一遍几种采集方式。表面上看,GEO 监测只是问大模型一个问题,然后判断回答里有没有出现目标品牌。真正做起来会发现,它比普通关键词排名监控麻烦得多:同一个问题在不同模型里会有不同检索策略,不同时间会引用不同网页,回答里还可能把品牌、公司、产品和竞品混在一起。这篇只写我这次做技术接入时踩到的点。重点放在两类路线:一类是网页版监测,一类是基于厂商 API 去模拟
最近在整理 GEO 监测能力时,我重新看了一遍几种采集方式。表面上看,GEO 监测只是问大模型一个问题,然后判断回答里有没有出现目标品牌。真正做起来会发现,它比普通关键词排名监控麻烦得多:同一个问题在不同模型里会有不同检索策略,不同时间会引用不同网页,回答里还可能把品牌、公司、产品和竞品混在一起。
这篇只写我这次做技术接入时踩到的点。重点放在两类路线:一类是网页版监测,一类是基于厂商 API 去模拟模型的联网回答。后面用豆包的模拟实现举例,讲接口怎么调、返回怎么解析、哪些字段值得保存。业务提示词不在这里展开,示例代码里也不会放。
网页版监测更接近用户,但问题也更复杂
最直观的做法是打开各个大模型的网页版,像真实用户一样提问,再把回答抓回来。这个方式有一个明显好处:它看到的确实是用户正在使用的产品形态,包含网页端自己的搜索、推荐、账号状态和前端策略。
但它的问题也很明显。
首先是合规边界。网页版通常面向人机交互,不是稳定的数据采集接口。用自动化浏览器、账号池、Cookie 复用、绕过风控去批量采集,都很容易碰到服务条款、账号安全和访问频率问题。这个方向我一般只把它当成“现象观察”或“低频人工校验”,不把它写成可规模化的技术路径。
其次是稳定性。网页端页面结构会变,登录态会过期,验证码和限流也会突然出现。即使采到了回答,结果里哪些是答案正文、哪些是引用卡片、哪些是推荐追问,也需要跟着前端 DOM 改。它适合做接近真实体验的抽样,但不适合承担每天定时、批量、可复盘的监控任务。
还有一个容易被忽略的点:网页版监测虽然“真实”,但也不等于完全可解释。一次截图只能说明那一刻看到了什么,很难稳定回答这些问题:引用来源有哪些、品牌是否被正面推荐、竞品排在第几、同一个问题在五个模型里谁更容易提到你。
API 模拟不是简单把问题发给模型
另一条路是基于 API 模拟。很多团队会先写一个最小调用:把用户问题发给模型,拿回文本,判断里面有没有品牌词。这个 demo 很快,但离 GEO 监测还差一截。
GEO 监测要模拟的不是“模型能不能回答”,而是“模型在联网检索后会如何组织答案”。所以 API 调用至少要处理几个细节。
第一,必须让模型真的走搜索。不同厂商的开关不一样,有的是 enable_search,有的是工具调用,有的是专门的智能搜索接口。只开普通聊天接口,模型可能凭参数记忆回答,结果和真实联网问答差很多。
第二,要把搜索结果拿出来。只保存最终回答不够,因为 GEO 优化很关心引用来源。一个品牌被提到,可能是因为官网被引用,也可能是因为第三方媒体、百科、论坛或竞品文章里顺带出现。没有来源 URL,就很难判断后续该补哪类内容。
第三,要做归一化。厂商返回的字段差异很大,有的叫 site_name,有的叫 sitename,有的摘要在 summary,有的在 snippet。如果不先归一化,后面的“来源排行”“系统是否已收录”“引用域名覆盖”都会很乱。
第四,要接受波动。GEO 监测不是一次性判断题,更像采样统计。一次任务里品牌没有出现,不代表模型永远不会提;一次出现,也不代表可见性已经稳定。所以我更愿意把 API 模拟结果落成任务、来源、主体分析和报告,而不是只在页面上显示一个“命中 / 未命中”。
豆包模拟:我用的是 Responses API 加豆包助手
豆包这一支,我用的是火山方舟的 Responses API:
POST https://ark.cn-beijing.volces.com/api/v3/responses
模型用的是:
doubao-seed-2-0-pro-260215
关键不是这个接口能生成文本,而是它可以启用 doubao_app 工具里的 ai_search。这样返回里除了最终回答,还能拿到搜索过程里的结果卡片。请求头里除了常规的鉴权和 JSON 类型,还需要带上 beta 开关:
Authorization: Bearer <ARK_API_KEY>
Content-Type: application/json
ark-beta-doubao-app: true
下面是一个保留关键结构的 Node.js 示例。注意这里没有放业务提示词,也没有放真实的 role_description,我会把这部分放到配置里,避免代码示例被直接复制成线上策略。
const ENDPOINT = "https://ark.cn-beijing.volces.com/api/v3/responses";
const MODEL = "doubao-seed-2-0-pro-260215";
function buildDoubaoGeoRequest(question) {
const roleDescription = process.env.DOUBAO_APP_ROLE_DESCRIPTION;
if (!roleDescription) {
throw new Error("Missing DOUBAO_APP_ROLE_DESCRIPTION");
}
return {
model: MODEL,
stream: true,
input: [
{
type: "message",
role: "user",
content: [
{
type: "input_text",
text: question,
},
],
},
],
tools: [
{
type: "doubao_app",
feature: {
ai_search: {
type: "enabled",
role_description: roleDescription,
},
chat: { type: "disabled" },
deep_chat: { type: "disabled" },
reasoning_search: { type: "disabled" },
},
},
],
};
}
async function callDoubaoGeo(question) {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
authorization: `Bearer ${process.env.ARK_API_KEY}`,
"content-type": "application/json",
"ark-beta-doubao-app": "true",
},
body: JSON.stringify(buildDoubaoGeoRequest(question)),
});
const rawText = await response.text();
if (!response.ok) {
throw new Error(`Doubao request failed: ${response.status} ${rawText}`);
}
return rawText.trimStart().startsWith("{")
? parseDoubaoJson(rawText)
: parseDoubaoSse(rawText);
}
我这里把 chat、deep_chat、reasoning_search 都关掉,只保留 ai_search。原因很简单:GEO 监测要的是尽量稳定的联网搜索回答,不希望一次任务里混入太多不可控的产品形态。真实项目里还要给请求加超时,比如 180 秒;否则上游卡住时,队列 worker 会被占住。
SSE 解析比请求本身更容易出错
豆包这个接口可以流式返回。流式响应里会出现多类事件:有的是回答增量,有的是最终完成事件,有的是搜索完成事件。我的处理方式是先把 SSE 拆成 payload,再从 payload 里分别抽取三类数据:
- 回答文本:优先取完成事件里的完整文本,缺失时再用 delta 拼接。
- 请求 ID 和 usage:通常在完成事件或 response 对象里。
- 搜索结果:递归扫描 payload 中的
results,重点读text_card。
精简后的解析代码大概是这样:
function parseSsePayloads(rawText) {
return rawText
.replace(/\r\n/g, "\n")
.split(/\n{2,}/)
.flatMap((block) => {
const event = block
.split("\n")
.find((line) => line.startsWith("event:"))
?.slice("event:".length)
.trim();
const data = block
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice("data:".length).trimStart())
.join("\n")
.trim();
if (!data || data === "[DONE]") return [];
return [{ event, payload: JSON.parse(data) }];
});
}
function collectSearchResults(value, output = []) {
if (Array.isArray(value)) {
for (const item of value) collectSearchResults(item, output);
return output;
}
if (!value || typeof value !== "object") return output;
if (Array.isArray(value.results)) {
for (const item of value.results) {
const card = item?.text_card || item;
if (card?.url) {
output.push({
title: card.title || "",
site_name: card.sitename || card.site_name || "",
summary: card.summary || card.snippet || "",
url: card.url,
published_at:
card.published_at || card.date || card.datePublished || "",
});
}
}
}
for (const child of Object.values(value)) {
collectSearchResults(child, output);
}
return output;
}
这里最容易踩坑的是“字段位置不固定”。有时搜索结果在 response.doubao_app_call_search.completed 一类事件里,有时会嵌在更深的对象中。如果解析代码只按一层路径取值,很容易在线上遇到空来源。我的习惯是对搜索结果做递归扫描,再按 url + title + site_name 去重。
最后落库时,我不会只存最终文本,而是把结果拆成几层:
assistant_content: 模型最终回答。request_id: 上游请求 ID,方便排查。usage: 上游 token 或计量信息。search_results: 标准化后的引用来源。matched: 回答正文或引用来源里是否命中目标品牌。content_matched_citation_indexes: 品牌出现在回答里时,附近引用了哪些来源序号。
这一步做完,GEO 监测才开始有分析价值。否则只靠一个 includes(brandName),很容易把“被推荐”“被负面提到”“只在引用标题里出现”混成同一种结果。
我在系统里怎么把它变成可用的监测能力
在 Dcoding Max 盾码无界系统里,我没有把豆包模拟做成一个孤立按钮。它被放进“大模型监控任务”的统一模型里,和通义、DeepSeek、元宝、文心等入口放在同一套口径下比较。
这个设计里有两个概念比较重要。
channel 表示执行路线。web_platform 走外部网页版监测接口,适合做接近真实网页产品的抽样;api_platform 走系统内置的厂商 API runner,适合做更可控、可记录、可重试的模拟。mode 才表示具体模型入口,比如 doubao、qwen、deepseek。
这样拆开之后,同一个业务问题可以同时跑多种组合。任务成功后,系统会统一写入回答文本、来源列表和品牌命中结果,再异步做品牌主体分析。品牌主体分析不是简单看有没有目标品牌,而是从回答里识别企业、品牌、产品和竞品,给出排名、态度和标签。
实际看效果时,我最关注的不是“某一次豆包有没有提到我”,而是这些指标能不能稳定生成:
- 品牌提及率:成功任务里有多少回答提到了目标品牌。
- 最佳排名和平均排名:目标品牌在主体分析里排第几。
- 情绪分布:正面、中性、负面回答占比。
- 引用来源:哪些域名和 URL 更容易被模型引用。
- 分模型表现:豆包、元宝、文心、DeepSeek、通义之间的差异。
- 系统收录命中:模型引用的 URL 是否已经存在于自己的内容发布记录里。
品牌诊断报告会再往前走一步。系统会围绕品牌和关键词生成 3 个核心查询词,然后按默认的 5 个模型入口创建监控任务,也就是一次诊断形成最多 15 个采样点。等任务和品牌主体分析都完成后,再汇总成报告数据。
这个报告不是把回答堆在一起,而是按模型拆开看:每个模型的任务成功率、提及率、排名、态度、引用域名、引用 URL、竞品和代表样本都会单独列出来。评分也有明确拆分,提及率占 40 分,排名占 25 分,态度占 20 分,来源覆盖占 10 分,成功率占 5 分。这样看起来就很清楚:问题到底出在品牌没被提到,还是被提到了但排名靠后,或者模型引用的来源根本不是自己的内容。
我觉得这类系统真正有用的地方也在这里。它不能保证每一个用户在每一次提问时都看到同样答案,但它能把原来很虚的 GEO 问题变成可追踪的数据:哪类查询词弱,哪个模型弱,哪些引用来源缺,哪些竞品经常被推荐。后续做内容发布、媒体补充、官网结构调整时,就有了比较具体的依据。
小结
GEO 监测难在它不是传统搜索排名,也不是普通模型调用。网页版监测接近真实产品,但批量化和合规边界都要谨慎;API 模拟更适合工程化,但必须精巧构造请求,让模型进入联网搜索状态,并且把回答、引用、命中、排名和态度都拆开保存。
豆包这条路线的关键点是 Responses API、doubao_app、ai_search、SSE 解析和搜索结果归一化。真正放进业务系统时,代码的重点不只是调通接口,而是让每次监测都能被复盘、被聚合、被比较。只有做到这一步,GEO 监测才不只是“问一下 AI”,而是能变成后续内容优化和品牌诊断的工程基础。
参考资料
更多推荐



所有评论(0)