上个月我在做季度品牌巡检的时候照例用同事推荐的“先问一圈AI”的方式去了解自家产品在用户视角里的口碑结果发现一个扎心的事实不同AI平台给的回答差异大得离谱有的平台把我们夸成一朵花有的平台直接没提我们还有一个平台把竞品放在第一位顺便踩了我们一脚。那一刻我意识到品牌方在搜索引擎时代会盯着百度指数和Google排名但到了AI回答时代如果不去主动监控ChatGPT、豆包、Kimi、文心一言这些平台到底怎么说你那基本等于在裸奔。这套“GEO多平台监控系统”就是我从零开始折腾出来的解决方案。GEOGenerative Engine Optimization生成式引擎优化简单说就是让品牌在AI生产的内容里获得更多曝光和更好评价而监控系统则是这一切的数据基础。它能自动向多个AI平台抛出一组固定问题收集完整回答解析出品牌或关键词的提及情况、出现位置、情感倾向最后落库成表并产出日报。这篇博文会把我的整体思路、代码架构、踩坑过程和排障经验全部拆开写清楚适合做品牌运营、SEO/ASO、增长分析以及所有想了解“AI平台都在怎么说我”的开发者参考。1. 内容整体设计与思路拆解1.1 AI平台已经成为新流量入口为什么必须做GEO监控过去十年大家做品牌监测主要盯搜索引擎在百度、Google搜一次品牌词看首页排名、看新闻源、看贴吧评价。但现在越来越多的用户尤其是年轻用户已经习惯直接问AI助手“有什么值得买”“某品牌到底怎么样”“帮我对比几个软件”。这些回答一旦形成用户基本不会再去翻十条搜索结果而是直接采信AI给的结论。问题在于AI回答是生成式的不是固定网页同一个问题隔几天问一次结果可能完全不一样而且不同平台背后的模型、知识库更新策略、安全机制都不同甚至同一个平台换一个模型版本输出口径都会变。这种情况下靠人工每天去每一个平台手动提问、截图、记录根本不现实。GEO监控系统的核心价值就是把这件事自动化定时提问、保存全文、抽取关键信息、计算指标让品牌方知道自己在AI平台的“露出情况”到底是上升还是下降。我最初的需求很简单就是回答三个问题AI平台有没有提到我的品牌、提的时候是正面还是负面、排在回答里的第几个。但如果只用一两个平台做试点数据代表性太差。所以我把目标锁定在四个覆盖不同用户群的大模型产品上——ChatGPT、豆包、Kimi、文心一言做一个多平台统一的监控管道。1.2 监控系统要具备的四个核心能力先别急着写代码动手之前把能力清单想清楚后面可以省很多返工时间。以我要做的这套系统为例拆解下来就是四件套第一多平台问题下发能力。提前配置一批种子问题系统能按计划向四个平台逐个提问。这些问题可以是品牌词直接相关的问题也可以是行业词、竞品词甚至可以混入一些完全无关的对照组问题用于校验回答稳定性。第二回答内容采集能力。这里说的采集不是复制一段文本那么简单而是要拿到完整的结构信息包括平台回复的正文内容、引用的信息来源如果显示的话、回答生成的时间、回答的数量层级等。为了后续分析可靠原始回答需要完整保存最好留原始HTML或Markdown。第三解析归一化能力。不同平台的回答格式五花八门有的带引用链接有的带列表有的会给出“综合来看”的总结段落。解析层要能把它们统一成结构化数据至少包括纯文本正文、分段列表、是否提及目标关键词、关键词首次出现的位置、上下文片段。第四历史存储与指标计算能力。所有采集结果落库后还要按时间维度聚合。每天跑一轮一个月下来就能画出趋势曲线。常见的指标包括提及率、可见度Top3率、情感倾向、平均排名位置等。有了这些数据后续向老板汇报也好跟代理对需求也好都有据可依。1.3 技术选型为什么我选了Python Playwright SQLite技术选型这块我纠结过一阵子最后定的组合是Python 3.10、Playwright、SQLite、APScheduler。理由很直接第一Python做文本解析和数据处理最顺手正则、jieba、huggingface的sentiment pipeline都能直接用第二Playwright是目前浏览器自动化里对反检测支持最好、生态最完善的库之一支持Chromium/Firefox/WebKit还能持久化登录态非常适合需要模拟真人操作采集网页内容的场景第三SQLite够用了每天四个平台每个平台20个问题一个月顶多几千条记录不需要上MySQL或者PostgreSQL单文件部署还省事。依赖和版本方面我列一下方便复现Python 3.10及以上playwright1.40.0以上apscheduler3.10.4pydantic2.5.0httpx0.26.0beautifulsoup44.12.2jieba0.42.1。如果你只需要跑一个最小原型pydantic可以不用直接dict打天下但上了pydantic之后配置校验会省心很多后面加字段也不容易出错。2. 核心架构与平台接入方案2.1 五层架构调度、采集、解析、存储、报表整套系统我按五层来组织每层只做一件事层与层之间通过标准数据结构通信。这样做的好处是将来如果要新增一个AI平台只需要写一个新的适配器其他层都不用动。调度层负责按计划触发采集任务我用的APScheduler的CronTrigger默认每天固定时间跑一次也可以配置成每6小时跑一次。采集层是核心里面按不同平台实现对应的Adapter每个Adapter只负责和某一个具体平台打交道输出统一的原始答案对象。解析层拿到原始答案后做清洗和归一化输出ParsedAnswer。存储层把解析后的数据写入SQLite同时保存原始的HTML快照。报表层从库里聚合数据生成Markdown日报或CSV文件方便同步到飞书、企微或者邮件。我画不出漂亮的架构图但文字描述已经够了实际代码也是一个简单的目录结构scheduler.py、adapter/、parser.py、database.py、reporter.py、config.yaml。项目根目录再放一个main.py作为入口。2.2 适配器模式统一处理ChatGPT、豆包、Kimi、文心一言先说结论这四个平台的接入方式并不一样但通过适配器模式可以把差异封住。我这边有两种接入策略一是走官方API二是走浏览器自动化。官方API适合有稳定配额、能接受按量付费的场景但缺点也很明显不少平台的开放接口并不覆盖“让AI直接回答一个自然语言问题”的业务场景比如有些平台只提供的对话接口或者有较重的审核机制而且每个平台的请求格式、鉴权方式都不同适配成本不低。所以我最后的主方案是浏览器自动化用Playwright模拟真实用户打开网页、输入问题、等待回答、抓取内容。这种方式绕开了各家API的差异唯一的要求就是运行环境得能正常打开目标站点并且能维护一个登录态。至于登录态可以用Playwright的storage_state导出Cookie或者用user_data_dir保持浏览器用户数据实测下来后者更省心一次登录之后长期有效。四个平台我各写了一个Adapter基类长这样class BaseAdapter(ABC): def __init__(self, config: PlatformConfig): self.config config self.browser None self.page None abstractmethod async def ask(self, question: str) - RawAnswer: pass abstractmethod async def close(self): pass每个Adapter的核心就是ask方法输入一个问题返回一个包含平台名、问题、原始回复、来源链接的RawAnswer。对于Web版平台ask内部逻辑基本一致新建页面、打开对话URL、等待输入框加载、输入问题并回车、轮询等待回答停止增长、抓取正文。以豆包为例它的页面结构和Kimi比较接近但元素ID完全不同尤其是输入框的反应速度不稳定经常要重试。我在DoubaoAdapter里专门写了一个wait_for_input_box的方法先等待div[contenteditabletrue]出现再尝试输入输不进去就换一种点击方式这个在后面的坑位里细聊。2.3 提问策略与问题集设计直接影响数据质量监控数据准不准一半取决于问题怎么问。这里有一个容易被忽略的原则问题必须保持固定才能做前后趋势对比。如果你的问题今天问“有哪些好用的笔记软件”明天问“推荐几个笔记软件”虽然意思相近但AI回答的可能是完全不同的内容框架根本没法对比。我在系统里把问题集拆成三类品牌直达问题、行业泛问题、竞品对比问题。品牌直达问题直接包含品牌名或产品名比如“A品牌的口碑怎么样”这类问题是保底项能直接反映品牌有没有被提及。行业泛问题不带品牌词比如“推荐一款适合团队协作的在线文档工具”这类问题能看出模型在无提示状态下会不会主动带出我的品牌。竞品对比问题则是“A和B哪个好”这种能看模型的“站队”倾向。每个问题要配一个元信息包括问题归属的类别、要监测的目标关键词列表。监测关键词列表通常是品牌名加几个核心产品名解析层会拿这些词去正文里做匹配。比如questions: - text: 推荐一款适合团队协作的在线文档工具 category: industry keywords: [文档, 协作, 我们的品牌] - text: A品牌和B品牌哪个更适合中小企业 category: comparison keywords: [A品牌, B品牌]问题数量方面我建议第一批控制在20到30条左右每天跑四个平台也就是100多次对话时间上完全可控。如果问题太多一方面触发风控的概率变大另一方面单轮采集耗时太长容易断线。3. 实操过程与核心环节实现3.1 环境准备装依赖、初始化浏览器、准备目录结构先把基础环境跑起来。如果你本机还没有Python 3.10建议直接用conda建一个干净的虚拟环境避免和系统Python打架。然后安装依赖pip install playwright apscheduler pydantic httpx beautifulsoup4 jieba playwright install chromium这里有一个注意点playwright install chromium会把浏览器下载到用户目录这个步骤如果因为网络原因失败可以多试几次或者换成playwright install chrome使用本机已有的Chrome。实测下来用本机Chrome的兼容性比内置Chromium好因为登录态可以复用已有的浏览器Cookie但缺点是版本漂移时选择器可能会失效。目录结构我按下面的方式组织geo_monitor/ ├── main.py ├── scheduler.py ├── config.yaml ├── adapters/ │ ├── __init__.py │ ├── base.py │ ├── chatgpt_adapter.py │ ├── doubao_adapter.py │ ├── kimi_adapter.py │ └── wenxin_adapter.py ├── parser.py ├── database.py ├── reporter.py └── data/ ├── profiles/ ├── raw_html/ └── geo_monitor.dbdata/profiles是给Playwright保存用户数据用的每个平台一个独立文件夹避免Cookie串掉。data/raw_html专门保存每次采集的页面快照做问题回溯时特别有用。3.2 配置中心管理平台账号、模型和问题集我习惯把配置和代码分离所有可变的东西都放进config.yaml。这不仅仅是“优雅”的问题而是当你需要调整问题集、更换账号、修改采集频率时不应该去改动代码然后重新部署只改配置再重启进程就够了。我的config.yaml结构如下schedule: cron: 0 8 * * * # 每天早上8点跑一次 platforms: - name: chatgpt enabled: true type: web user_data_dir: ./data/profiles/chatgpt headless: false wait_timeout: 30000 - name: doubao enabled: true type: web user_data_dir: ./data/profiles/doubao headless: false wait_timeout: 30000 - name: kimi enabled: true type: web user_data_dir: ./data/profiles/kimi headless: true wait_timeout: 40000 - name: wenxin enabled: true type: web user_data_dir: ./data/profiles/wenxin headless: true wait_timeout: 40000 questions: - text: 推荐一款适合团队协作的在线文档工具 category: industry keywords: [品牌A, 品牌B] # 更多问题写在下面 output: report_dir: ./reports format: markdown用pydantic写个配置模型来加载这个YAML顺手做类型校验配置写错会在启动时立刻暴露而不是跑到一半才报错。这块非常推荐大家做尤其是你会把配置分享给团队其他人维护的时候。3.3 用Playwright打开平台页面并提问完整的ask流程下面以Kimi Adapter为例写一个最小可运行的ask流程。Kimi网页版的输入框是一个contenteditable的div回答是一个流式的渲染区域。核心逻辑是打开聊天页面、输入问题、回车、轮询等待回答稳定然后取出正文。import asyncio from playwright.async_api import async_playwright from .base import BaseAdapter class KimiAdapter(BaseAdapter): async def ask(self, question: str) - RawAnswer: async with async_playwright() as p: ctx await p.chromium.launch_persistent_context( user_data_dirself.config.user_data_dir, headlessself.config.headless, args[--disable-blink-featuresAutomationControlled] ) page ctx.pages[0] if ctx.pages else await ctx.new_page() await page.goto(https://www.kimi.com/, wait_untildomcontentloaded) await page.locator(.chat-input-editor, div[contenteditabletrue]).first.click() await page.keyboard.type(question, delay20) await page.keyboard.press(Enter) # 轮询等待回答稳定连续3秒内容不再变化认为生成完毕 last_text stable_count 0 for _ in range(120): await page.wait_for_timeout(1000) text await page.locator(.chat-container, .markdown-body, main).first.inner_text() if text last_text: stable_count 1 if stable_count 3: break else: stable_count 0 last_text text raw RawAnswer( platformkimi, questionquestion, contentlast_text, htmlawait page.content(), fetched_atdatetime.now() ) await ctx.close() return raw这里面最关键的一步就是“等回答稳定”。AI回答是流式的你不等它完全结束就抓内容抓到的可能是半截话。我用的策略很简单每秒抓一次当前文本连续三次相同就认为生成结束。超时设120秒Kimi长回答有时候会超过这个时间你可以按需加大。ChatGPT、豆包、文心一言的流程基本一样差异只在页面元素和URL入口上。ChatGPT需要保证登录态存在否则会跳到登录页文心一言的页面结构相对重加载慢等待时间要放宽豆包偶尔会有登录弹窗干扰需要在进入页面后先关掉弹窗再操作。3.4 回答解析提取关键字、情感和引用来源原始回答拿到手之后接下来是解析层做的归一化工作。解析层输入RawAnswer输出ParsedAnswer包含五个字段平台、问题、抓取时间、回答全文、关键词命中列表。关键词匹配我建议用两套规则叠加。第一套是“标准匹配”直接判断关键词是否出现在文本中考虑大小写。第二套是“分词召回”用jieba对文本分词后再判断关键词是否以词的形式出现。这两套规则会有差异但作为品牌监控的粗筛来说已经是够用的。举个例子“豆包”这个词可能在“我用豆包优化电脑”和“吃了个豆包”两种语境里出现前者才是品牌相关后者只是食品所以我会在问题集里尽量用品牌全名或者产品全名减少这种歧义。情感倾向我一开始想接一个大型预训练模型做打分后来觉得对监控系统来说太重了。更实用的办法是维护一个正负面词典加上规则判断否定词。比如“好用”“推荐”“领先”“高效”是正面词“难用”“卡顿”“失望”“别买”是负面词如果负面词前面出现“不”“没有”这类否定词要反转标记。这个方法的准确率大概在70%左右对趋势监控来说是够的因为我们要看的不是单条结果的绝对情感而是连续多天的情感分数变化。引用来源的提取也比较重要尤其是ChatGPT的Web版本会给出引用脚标。我通过正则抓取带序号的内容比如[1]、[2]这种配合页面里的链接地址归入citations字段。这块目前做得比较粗糙但已经足够用来回答“AI回答到底有没有引用自家官网”这个问题。3.5 SQLite落库原始表、解析表、指标表数据库设计上我用了三张表。第一张是raw_answers保存每次采集的原始回答和HTML快照用于事后回溯第二张是parsed_answers保存解析后的结构化数据第三张是daily_metrics按天聚合品牌指标相当于物化视图报表层直接查询这张表就行。建表语句大致如下CREATE TABLE IF NOT EXISTS raw_answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, question TEXT NOT NULL, content TEXT, html TEXT, fetched_at DATETIME NOT NULL ); CREATE TABLE IF NOT EXISTS parsed_answers ( id INTEGER PRIMARY KEY AUTOINCREMENT, raw_id INTEGER NOT NULL, platform TEXT NOT NULL, question TEXT, category TEXT, keyword TEXT, mentioned INTEGER, position INTEGER, sentiment TEXT, context TEXT, created_at DATETIME, FOREIGN KEY (raw_id) REFERENCES raw_answers(id) ); CREATE TABLE IF NOT EXISTS daily_metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, stat_date TEXT NOT NULL, platform TEXT NOT NULL, keyword TEXT NOT NULL, question_count INTEGER, mention_count INTEGER, top3_count INTEGER, positive_count INTEGER, negative_count INTEGER, avg_position REAL );落库时有个小细节如果raw_answers已经存在同一个平台的同一个问题和同一天的记录最好先删掉再插入或者用唯一索引做upsert否则重复跑任务时会产生大量重复数据后面的指标计算会被污染。4. 定时调度、异常处理与部署实践4.1 用APScheduler做定时任务兼顾补跑和手动触发调度入口我放在main.py里启动后先加载配置初始化数据库连接然后创建定时任务from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger scheduler AsyncIOScheduler() scheduler.add_job( run_all_platforms, CronTrigger.from_crontab(config.schedule.cron), idgeo_monitor_daily, replace_existingTrue ) scheduler.start() asyncio.get_event_loop().run_forever()我试过用系统crontab来做但问题在于每次任务启动都要重新初始化Playwright、加载各种配置比常驻进程慢很多而且环境变量、路径问题在crontab里很容易踩坑。所以最后选择了常驻进程加APScheduler的方案。常驻进程还有一个额外好处可以用一个简单的HTTP接口手动触发采集任务方便临时补数据。run_all_platforms函数的逻辑是串行遍历平台和问题集每个问题之间随机延迟5到15秒。串行的优点是不容易触发平台的风控缺点是耗时较长20个问题四个平台可能要跑半小时以上。如果对时效性有要求可以改成每个平台一个协程并发跑但对应的风控风险也会上升需要自己权衡。4.2 反风控与登录态维护随机延迟、用户目录、兜底重启做网页自动采集的人都知道最怕的不是平台封号而是悄无声息地限制你的对话能力。我在这套系统里遇到最典型的限制就是Kimi的“当前聊天人数太多”排队提示以及ChatGPT偶尔出现的登录态失效。针对这些情况我做了三层防线。第一层是随机延迟和操作节奏拟人化。输入问题时不要一次性把整句话粘贴进去而是用keyboard.type模拟打字每个字间隔20到50毫秒。提问之间的等待时间也不要固定用random.uniform(5, 15)生成随机数。这些手段虽然看起来不起眼但对降低被识别为机器人的概率很有帮助。第二层是登录态持久化。每个平台单独一个user_data_dir首次运行时手动打开浏览器完成登录后续采集进程启动时Playwright会直接沿用这个用户目录。遇到Cookie过期的情况我会在请求异常时捕获登录跳转特征比如URL变成了/login然后打日志告警必要时把headless临时改成false手动重新登录一次再改回来。第三层是强制兜底重启。每次采集任务开始前我会清理残留的Chromium进程确保没有上一个任务遗留的僵尸浏览器拖慢系统。任务结束后也显式调用ctx.close()关闭上下文避免长时间运行导致的文件句柄泄漏。4.3 异常重试与通知把错误当成正常流程来设计整套系统跑在无人值守的环境里异常处理必须写得足够鲁棒否则一个月之后你会发现中间有一个星期的数据全是空洞。我的做法是给每个平台的ask任务包一层重试机制最多重试3次每次重试之间递增等待时间第一次等30秒第二次等120秒。重试仍然失败的记录到task_logs表里然后继续执行下一个问题而不是中断整个任务。任务全部跑完之后我会统计失败数量。如果失败数量超过总任务数的20%就认为这次采集质量不合格通过一个简单的notify()函数发送告警。通知渠道我用的是飞书机器人Webhook你也可以换成微信、钉钉或者纯邮件。告警内容只要包含日期、平台、失败数量、最后一条错误日志就足够了不要写太多干扰信息。5. 常见问题与排查技巧实录5.1 本地工具链的“幽灵报错”Codex CLI和config.toml这段时间不少朋友在本地折腾OpenAI的Codex CLI然后被两个报错卡住。一个是“ChatGPT failed to start. unable to locate the codex cli binary. set codex_cli_path”另一个是“ChatGPT cant load config.toml, so this thread cant resume. fix config.toml”。虽然这两个问题和GEO监控系统本身没有直接关系但在同一台电脑上折腾本地工具时很容易互相干扰尤其是环境变量串了之后连Playwright都启动不了。先说unable to locate the codex cli binary这是因为ChatGPT桌面端在调用Codex功能时找不到二进制文件需要在环境变量里显式指定CODEX_CLI_PATH或者在配置里填上codex的绝对路径。再说config.toml加载失败常见原因是配置文件编码不对或模型名写成了不存在的值比如报错里提到的“gpt-5.6-sol”。我建议直接删掉损坏的config.toml让工具重新生成一份然后只改必要的字段不要手写多余配置。这类问题给我的教训是本地开发环境中多个AI工具链混用之前一定要把每个工具的配置目录隔离清楚否则一个工具的坏配置可能拖垮另一个工具的正常运行。5.2 豆包输入框定位失败和页面改版问题豆包网页版接入是四个平台里我花时间最多的。它的输入框有时候是div[contenteditabletrue]有时候又变成一个textarea标签而且页面偶尔会有新手引导弹窗遮挡。我在DoubaoAdapter里专门封装了一个“找输入框”的函数依次尝试多个选择器谁出现就用谁。另一个大坑是页面改版。我在项目跑到第三周的时候豆包突然改了一次前端原来的选择器全部失效采集任务全军覆没。从那以后我养成了两个习惯第一每次采集都把原始HTML快照存下来改版后拿快照对比能快速定位新的元素结构第二选择器尽量写稳定属性比如带>## 品牌A GEO监控日报 2025-06-01 | 平台 | 问题数 | 提及率 | Top3率 | 正面数 | 负面数 | 平均情感分 | |------|--------|--------|--------|--------|--------|------------| | ChatGPT | 20 | 35.0% | 20.0% | 3 | 1 | 0.40 | | 豆包 | 20 | 45.0% | 30.0% | 4 | 0 | 0.50 | | Kimi | 20 | 40.0% | 15.0% | 2 | 1 | 0.30 | | 文心一言 | 20 | 30.0% | 25.0% | 2 | 0 | 0.35 |如果你需要把数据同步给不怎么看Markdown的同事可以在reporter.py里加一个CSV导出Excel打开就能直接处理。6.3 这套系统的扩展空间新平台接入和情绪分析升级架构里适配器模式带来的最大好处就是加平台很方便。现在主流的AI平台除了上面四个还有Perplexity、Claude、Gemini、通义千问等。每个新平台要接进来核心工作就是两步写一个Adapter配置问题集。解析层、存储层、报表层都不用动。如果你有更强的自然语言处理能力可以把情感判断从词典规则升级成微调模型或大模型API。我的建议是不要一步到位先用规则跑一个月积累一批人工标注数据再决定值不值得上模型毕竟GEO监控的核心价值在于连续稳定的趋势而不是单条内容的精细化情感。另外如果团队内部有知识库或者CMS系统后续可以把AI回答里引用的链接地址和自家官网页面做关联看看哪些页面成了AI的“答案来源”。这块做出来之后就等于把GEO和传统的网站SEO打通了基本上可以覆盖“从内容生产到AI引用”的整个链路。我个人的体会是做GEO监控系统最难的其实不是技术而是坚持跑数据。第一周的数据可能看不出什么一个月之后趋势就会很明显三个月之后你就能在周会上拿出“AI平台对我们的态度正在从中性转向积极”的结论。这套系统我目前已经稳定跑了两个多月期间经历过豆包改版、Kimi排队、ChatGPT登录态过期但每次修复之后它都能按点爬起来继续干活。如果你也想做一套类似的系统我劝你别一上来就追求功能完整先把“一天四个平台每个平台跑10个问题”的最小闭环跑通再慢慢加指标、加报表、加告警一步步来比一次到位要靠谱得多。