Browser AutomationPlaywrightMCPReliabilityPythonNotebookLM

瀏覽器自動化怎麼避免抓錯答案:NotebookLM 整合的可靠性檢查

··閱讀約 7 分鐘

本文記錄 kof-notebooklm-mcp 操作 NotebookLM 網頁的做法。2026-09-21 核對 Google Cloud 文件,企業服務已提供筆記本管理 API,因此原文「沒有公開 API、只有瀏覽器自動化」的概括不成立。是否可替代這套工具,要對照帳戶、授權及實際需要的操作。

所以我做了一個 MCP server,用 Playwright 驅動真的 NotebookLM 頁面,把「加來源」「提問」「撈答案」包成程式可以呼叫的工具,發上 PyPI。

第一版跑通只花一個週末。然後接下來兩個版本,我都在修同一類問題:它會回傳「看起來完全合理、但其實是錯的」的東西。

為什麼瀏覽器自動化的錯特別毒

一般 API 整合可回報 4xx、5xx 或 timeout,但同樣可能回傳格式正確、內容錯誤的結果。瀏覽器整合還多了一層要驗證:取得的 DOM 節點是否代表這次操作的結果。

瀏覽器自動化不一樣。你面對的是一個為人類設計的動態頁面,它的失敗模式是「抓到了東西,但不是你要的東西」:

失敗症狀下游後果
抓到舊答案問第二題,拿回第一題的回覆答非所問,但格式完全正確,很難發現
抓到半截答案串流還在打字就收割結論被切在句子中間,引用缺失
抓到 UI 本身把「AI 簡報」「心智圖」等功能按鈕的文字當成回覆一段完全不知所云的「答案」
三類「成功回傳錯誤內容」的失敗

這三種情況都可能被工具包裝成成功回傳;不論傳輸使用 HTTP 或 MCP,呼叫端都不能只看成功旗標。若另一個 AI agent 繼續使用錯誤內容,後續步驟也會受影響。

失敗一的解法:答案要「連續穩定」才算數

AI 回答是串流的,DOM 裡的答案節點會持續變長。什麼時候才算「寫完了」?

固定等幾秒最直覺,問題也最多:短答案白等,長答案又等不夠。比時間更可靠的訊號是內容是否穩定:

python
# 輪詢答案節點:內容連續 3 次檢查都相同,才視為完成
if text == last_text:
    stable_answer_checks += 1
else:
    stable_answer_checks = 0    # 還在變,重新計數
    last_text = text

if has_changed_answer and stable_answer_checks >= 3:
    return text                 # 新的、而且穩定的答案

「內容變新」與「短暫穩定」可以排除部分舊答案與過早擷取,但不是完成保證。串流可能停頓後繼續;不同問題也可能得到相同文字。上面的三次檢查是啟發式條件,還應搭配本次回覆節點、生成狀態、引用與總逾時;缺少完成訊號時,應標記不確定而非假裝完整。

失敗二的解法:偵測 UI 污染

最陰險的是第三類。NotebookLM 的工作區裡到處是功能入口——AI 簡報、學習卡、心智圖、Audio Overview——當選擇器在某次改版後開始抓到過寬的節點,這些按鈕文字會被串成一段「回答」。

它不是亂碼,而是一串真實卻不成語意的中文詞。人看一眼就知道不對,程式卻看不出來。

我的解法是建一個特徵庫:把工作區的控制元素文字和功能名稱列成標記清單,再對抓到的內容計數:

python
def detect_answer_ui_pollution(text: str) -> list[str]:
    """辨識誤把 NotebookLM 工作區介面當成回答正文的情況。"""
    normalized = re.sub(r"\s+", " ", text).strip().lower()
    control_hits = [m for m in _ANSWER_UI_CONTROL_MARKERS if m in normalized]
    feature_hits = [m for m in _ANSWER_UI_FEATURE_MARKERS if m in normalized]

    # 控制元素命中 ≥2,或功能名稱命中 ≥4 → 這不是答案,是介面
    if len(control_hits) >= 2 or len(feature_hits) >= 4:
        return [*control_hits, *feature_hits]
    return []

污染門檻會有誤判。如果使用者問的是 NotebookLM 功能,正常答案也可能同時提到心智圖、學習卡等名稱。因此特徵計數適合觸發進一步檢查,不能證明文字一定來自 UI。保留命中的規則與目標節點資訊,才能調整門檻而不靠猜。

判定污染之後,我選擇讓這次抽取明確失敗。

工具會回傳 ANSWER_EXTRACTION_FAILED,不會把抓到的東西照樣交出去。寧可讓呼叫端知道「這次沒拿到」,也不要給它一段會被當真的錯誤內容。尤其 AI agent 幾乎無法抵抗「格式正確的錯誤內容」。

失敗三的解法:選擇器的分層防禦

沒有 API 合約時,DOM 就是合約,而對方隨時可以改。選擇器不只要在今天「寫對」,還得考慮它日後怎麼老化。

我的做法是替每個目標元素準備一個 fallback 陣列,從最特定排到最通用:

python
"sources_panel": [
    "section.source-panel",          # 當前版本的實際結構,最準
    '[data-testid="sources-panel"]', # 測試屬性,次穩定
    '[aria-label*="Sources"]',       # 無障礙標籤,跨改版存活率高
    '[aria-label*="來源"]',           # 中文介面的同一層
    '[class*="sources"]',            # 最寬的網,最後手段
],

排序本身就是策略:上面的準但脆,改版就可能失效;下面的活得久卻較寬,也更容易誤抓。分層防禦必須和污染偵測一起用:寬選擇器在改版後撐住基本功能,污染偵測則把它抓錯的內容攔下來。

另外還有兩個配套。所有「可有可無」的元素讀取都設短逾時,避免元素消失時拖慢整條流程;「頁面還在載入」也要有明確狀態,否則載入中的筆記本可能被讀成「零來源的未命名筆記本」,又是一筆看似合理的錯誤資料。

離線測試與登入後驗收分開記錄

這類專案的測試有個特殊困難:你 mock 不了 NotebookLM。單元測試蓋得住解析邏輯(污染偵測、穩定性判斷、錯誤分類),但「選擇器還抓不抓得到真的頁面」只有一種驗法——用真的帳號、真的筆記本、真的長串流答案跑過。

原稿提到 133 個測試,但本文沒有附當次版本與完整輸出,不再把這個數字當成目前服務可用的證據。解析邏輯可以做離線測試;登入狀態、頁面選擇器、長答案與引用仍需在實際服務驗收。本次修訂沒有替讀者登入或重新驗證 NotebookLM。

如果你也要包一個沒有 API 的產品

五條原則,按重要性排:

一、預設不信任抓到的東西。每一段內容過完整性檢查(穩定?新的?不是 UI?)才交出去。

二、寧可明確失敗。回錯誤碼永遠好過回「看起來合理的錯誤內容」,尤其當呼叫端是 AI。

三、等待用穩定性訊號,不用固定秒數。

四、選擇器分層,並接受寬選擇器需要污染偵測來配平。

五、真環境驗證是發版流程的一部分,不是出事後的補救。

若官方 API 已涵蓋所需操作,優先比較其權限與維護成本。保留瀏覽器整合時,應能說清楚哪個操作仍需要它,以及 UI 改版後要如何發現失敗。

相關文章:從零到 PyPI——讓 NotebookLM 可以被程式控制

用一份小型來源測出錯誤類型

可自行建立一份測試文件,只寫「專案代號:青鳥;交付日:2026-10-15」,再問交付日與代號。第二題不應拿回第一題的答案;引用應能回到該文件。接著問文件沒有寫的負責人,確認系統沒有把猜測包成來源事實。這是建議的驗收案例,不是本文已完成的線上測試。

另外測試回答中途切斷連線、來源仍在處理,以及登入失效。分別記錄「等待完成」「明確失敗」或「結果可採用」,而不是只記工具有沒有回傳文字。測試資料不要包含正式客戶文件;這樣重跑時比較容易看清楚差異。

參考資料