AI-LECTURER 速報課|2026-09-20|約 25 分鐘

Stagehand:讓 AI 幫你操作網頁,密碼卻不外流的瀏覽器自動化

取材:We made Playwright 2x faster and 80% more token efficient(Hacker News(AI 高人氣))
📍 真實場景
小雯,高雄一家網拍賣家的兼職會計,每個月要對十幾家廠商的帳

月初她得一家一家登入廠商的後台網站,把發票號碼、金額、是否已付款抄進試算表,一次要花掉大半個下午

😖 卡住的地方:她試過錄製操作的腳本,但廠商只要改版一次,按鈕位置跑掉,整支腳本就壞;想改用 AI 代勞,又怕把後台密碼交給 AI 模型
💡 這課會帶她看懂 Stagehand 怎麼用一句話叫瀏覽器做事,並且讓密碼只留在自己電腦上;最後你也能跑出第一支能抓資料的小專案

🧭 這到底是什麼(白話版)

Stagehand 是一套專門讓 AI agent能自己看情況、決定下一步並動手做事的 AI 程式 操作網頁的 SDK軟體開發套件,別人先寫好、讓你直接呼叫的工具箱。你用一句白話(素材的範例是英文句子)告訴它「找出 email 輸入框」「點登入按鈕」「把表格裡的發票全部抓出來」,它就替你操作瀏覽器完成。它目前有 TypeScript、Python、Go 三種語言版本。

它的對照組是 Playwright。Playwright 是一套 瀏覽器自動化用程式代替人手去點擊、輸入、翻頁的技術 工具,出發點是「測試網站」,測試人員會事先寫死每個按鈕的位置。Stagehand 的自我介紹很直接:Playwright 為測試而生,Stagehand 為 agent 而生。這則 Hacker News 熱門文章的標題則宣稱:在 agent 的使用情境下,速度快 2 倍,tokenAI 模型計費與運算的文字單位,大約一個字或半個詞,送得越多越貴越慢 用量少 80%。

它的核心只有三個動詞。observe() 回答「這個東西在頁面的哪裡」,回傳的是真正的 選擇器selector,網頁元素的定位地址,例如某個輸入框的定位字串;act() 負責「做一個動作」,例如點按鈕、打開帳單頁;extract() 負責「把資料撈出來」,並且用 schema資料格式的規格表,規定每個欄位的名稱與型別,例如 amount 必須是數字 驗收,格式不對就不放行。

登入這件事也被設計進去了。設定 userDataDir 後,cookies網站存在瀏覽器裡的小紀錄,用來記住你已經登入 會留在你電腦的資料夾裡,下次啟動一開始就是登入狀態。密碼則由你自己的程式從 環境變數作業系統替程式保管的設定值,不寫在程式檔裡,適合放金鑰與密碼 讀出來親手填進去,素材特別強調:因為 observe() 只回傳欄位的位置,帳號密碼不會送到 AI 模型那邊。

🎯 為什麼值得你花時間

網站改版,不必重寫腳本傳統做法把「按鈕在哪」寫死在程式裡,廠商一改版就壞。Stagehand 的 act() 用一句話描述目標,素材標榜它在網站改版表單時會自我修復(self-heals)。對小雯這種要串很多家、又管不到對方網站的人,維護成本差很多。
密碼留在自己手上把整個登入交給 AI,等於把密碼寫進提示詞送給模型。素材的做法是拆開:AI 只負責找出輸入框的位置,填密碼這一步由你的程式用環境變數完成。這是用架構保護機密,而不是靠一句「請模型不要洩漏」的口頭承諾。
省 token 就是省錢也省時間agent 每做一個動作都要呼叫一次模型,token 越少,帳單越低、回應越快。標題主張快 2 倍、少 80% token。這數字值得驚喜,但也值得驗證:自動化一跑就是幾百上千次,差距會直接反映在帳單上,所以你最終要用自己的任務實測。

⚙️ 它是怎麼運作的

1
開瀏覽器,並記住登入用 localBrowser.launch 開一個本機瀏覽器,指定 userDataDir 為 ./browser-data。登入後的 cookies 存在這個資料夾,下次執行一開始就已登入。
2
observe:問「它在哪」呼叫 stagehand.observe('find the email input'),模型看著頁面,回傳輸入框真正的選擇器。這一步的輸出是位置,不是你的帳號密碼。
3
用一般程式親手填入機密拿到選擇器後,用 page.locator(選擇器).fill(環境變數裡的帳號密碼) 填入。機密只在你的電腦與目標網站之間流動。
4
act:用一句話下動作指令stagehand.act('click the sign in button')、stagehand.act('open the billing page')。你描述的是目的而不是位置,所以按鈕被搬家時,它還有機會自己找到新位置。
5
extract:帶著 schema 把資料撈出來把「發票號碼是字串、金額是數字、是否已付款是真假值」這類規格寫成 zod(TypeScript)或 pydantic(Python)的 schema,交給 extract()。拿回來的資料已通過驗證,可以直接餵給下游程式。
6
收工:關閉 Stagehand 與瀏覽器最後依序呼叫 stagehand.close() 與 browser.close(),把資源還回去。實務上用 try/finally 包起來,即使中途出錯也會關閉。
Playwright 與 Stagehand 的設計取向對照(依素材 README 整理)
面向傳統 Playwright 寫法Stagehand 寫法
設計初衷為測試網站而生為 AI agent 而生,提供 TypeScript/Python/Go
找到元素由人手寫選擇器用一句話描述,observe() 回傳真的選擇器
網站改版手寫的選擇器一改版就可能失效act() 標榜會自我修復
取得資料自己寫程式解析頁面extract() 搭配 schema,回傳驗證過的資料
登入機密由程式填入同樣由程式填入;observe() 只回傳位置,密碼不進模型

🔍 程式碼漫遊(點有 ● 的行看白話講解)

這是素材 README 裡「登入一次、保存登入狀態、再把資料抓出來」的 TypeScript 範例(略作精簡)。請特別留意哪幾行交給 AI、哪幾行由你自己的程式精準執行。

import { localBrowser, Stagehand } from '@browserbasehq/stagehand';
💬 匯入兩樣東西:localBrowser 負責開本機瀏覽器,Stagehand 是主角。
import { z } from 'zod/v4';
💬 zod 是用來寫 schema 的工具,稍後 extract() 要用它驗收資料。
const browser = await localBrowser.launch({ userDataDir: './browser-data' });
💬 重點:登入資料(cookies)會存進 browser-data 資料夾,下次執行一開始就是登入狀態。
const stagehand = await Stagehand.create({
💬 建立 Stagehand,並把剛剛的瀏覽器交給它。
browser,
💬 告訴 Stagehand 要操作哪一個瀏覽器。
model: { modelName: 'openai/gpt-5.4-mini', apiKey: process.env.OPENAI_API_KEY },
💬 指定背後的 AI 模型;金鑰從環境變數讀,不寫死在檔案裡,避免不小心外流。
});
const [page] = await browser.context.pages();
💬 取出瀏覽器目前開著的第一個分頁,後面的操作都在它身上進行。
await page.goto('https://app.example.com/login');
💬 前往登入頁。這是一般的 Playwright 式操作,沒有用到 AI。
const { data: email } = await stagehand.observe('find the email input');
💬 重點:請 AI 找出 email 輸入框,回傳的是位置(選擇器)。此時你的帳號還沒出場。
const { data: password } = await stagehand.observe('find the password input');
💬 同樣的做法找密碼欄,AI 只知道欄位在哪。
await page.locator(email[0].selector).fill(process.env.APP_EMAIL);
💬 重點:用剛拿到的選擇器,由你自己的程式把帳號填進去,帳號從環境變數來。
await page.locator(password[0].selector).fill(process.env.APP_PASSWORD);
💬 密碼也一樣由程式填入。素材強調的「憑證不會到達模型」,關鍵就在這兩行。
await stagehand.act('click the sign in button');
💬 改回交給 AI:用一句話點登入鈕。網站改版時,它比寫死的選擇器更有機會自我修復。
await stagehand.act('open the billing page');
💬 同樣的方式打開帳單頁,你只描述目標,不必知道按鈕長什麼樣子。
const { data } = await stagehand.extract(
💬 開始抓資料,extract() 接兩個參數:一句需求,以及資料規格。
'extract every invoice in the table',
💬 需求:把表格裡每一張發票都抓出來。
z.object({ invoices: z.array(z.object({ number: z.string(), amount: z.number(), paid: z.boolean() })) }),
💬 重點:schema 規定每張發票必有號碼(字串)、金額(數字)、是否已付(真假值)。拿回來的資料已通過這套規格的驗證。
);
console.log(data.invoices);
💬 印出發票清單。實際使用時,這裡通常改成寫進試算表或資料庫。

🛠️ 動手做:動手跑跑看:用 Stagehand 抓 Hacker News 前 5 則,再練習不外洩密碼的登入

這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。

📄 package.json ⬇ 下載
{
  "name": "stagehand-lab",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@browserbasehq/stagehand": "latest",
    "zod": "latest"
  }
}
📄 extract_top_stories.mjs ⬇ 下載
import { localBrowser, Stagehand } from '@browserbasehq/stagehand';
import { z } from 'zod/v4';

// 為什麼先定義 schema:它規定 AI 要回傳的欄位與型別,拿回來的資料也靠它驗收
const Stories = z.object({
  stories: z.array(z.object({ title: z.string(), points: z.number() })),
});

if (!process.env.OPENAI_API_KEY) {
  console.error('找不到 OPENAI_API_KEY:請先在本視窗設定環境變數後再執行');
  process.exit(1);
}

// userDataDir 讓 cookies 留在本機資料夾,下次執行不必重新登入
const browser = await localBrowser.launch({ userDataDir: './browser-data' });
try {
  const stagehand = await Stagehand.create({
    browser,
    model: { modelName: 'openai/gpt-5.4-mini', apiKey: process.env.OPENAI_API_KEY },
  });
  try {
    const [page] = await browser.context.pages();
    await page.goto('https://news.ycombinator.com/');
    const { data } = await stagehand.extract('extract the top 5 stories on the front page', Stories);
    data.stories.forEach((s, i) => console.log(`${i + 1}. ${s.title}(${s.points} 分)`));
  } finally {
    await stagehand.close();
  }
} finally {
  await browser.close();
}
📄 login_safely.mjs ⬇ 下載
import { localBrowser, Stagehand } from '@browserbasehq/stagehand';

// 機密只從環境變數讀,不寫進程式碼,也不放進任何交給模型的指令字串
const { OPENAI_API_KEY, LOGIN_URL, APP_EMAIL, APP_PASSWORD } = process.env;
if (!OPENAI_API_KEY || !LOGIN_URL || !APP_EMAIL || !APP_PASSWORD) {
  console.error('缺少環境變數:需要 OPENAI_API_KEY、LOGIN_URL、APP_EMAIL、APP_PASSWORD');
  process.exit(1);
}

const browser = await localBrowser.launch({ userDataDir: './browser-data' });
try {
  const stagehand = await Stagehand.create({
    browser,
    model: { modelName: 'openai/gpt-5.4-mini', apiKey: OPENAI_API_KEY },
  });
  try {
    const [page] = await browser.context.pages();
    await page.goto(LOGIN_URL);
    // observe 只回傳「輸入框在哪」(選擇器),模型看不到我們接著要填的內容
    const { data: email } = await stagehand.observe('find the email input');
    const { data: password } = await stagehand.observe('find the password input');
    if (email.length === 0 || password.length === 0) {
      console.log('找不到登入輸入框:可能上次的登入狀態被保留在 browser-data,或這個網址不是登入頁。');
    } else {
      await page.locator(email[0].selector).fill(APP_EMAIL);
      await page.locator(password[0].selector).fill(APP_PASSWORD);
      await stagehand.act('click the sign in button');
      console.log('登入動作已送出,登入狀態會保存在 ./browser-data');
    }
  } finally {
    await stagehand.close();
  }
} finally {
  await browser.close();
}
📄 .gitignore ⬇ 下載
# browser-data 裡是登入用的 cookies,等同你的登入憑證,絕對不能上傳到版本庫
node_modules/
browser-data/
.env
  1. 確認電腦有 Node.js:開啟 PowerShell,輸入 node -v。如果顯示找不到指令,執行 winget install OpenJS.NodeJS.LTS,裝完後把 PowerShell 關掉再重開。
  2. 建立專案資料夾:依序執行 mkdir D:\stagehand-lab 與 cd D:\stagehand-lab,然後把上面四個檔案(package.json、extract_top_stories.mjs、login_safely.mjs、.gitignore)用記事本存成 UTF-8 格式放進去,注意副檔名不要被記事本加上 .txt。
  3. 安裝套件:在該資料夾的 PowerShell 執行 npm install,第一次需要下載一些套件,請等它跑完。
  4. 設定模型金鑰(只存在這個 PowerShell 視窗的記憶體,不會寫進任何檔案):執行 $env:OPENAI_API_KEY = '你的金鑰'。範例裡的模型名稱 openai/gpt-5.4-mini 取自素材 README;如果你的帳號沒有這個模型,請把三個檔案裡的 modelName 換成你有權限使用的模型。
  5. 執行第一支:node extract_top_stories.mjs。瀏覽器會自動開啟並前往 Hacker News,終端機應印出 5 行「編號、標題、分數」。若提示找不到瀏覽器,先安裝 Google Chrome 再重跑。
  6. (選做)練習不外洩密碼的登入:改成你自己有帳號、可以自動化的網站,在同一視窗依序設定 $env:LOGIN_URL = '登入頁網址'、$env:APP_EMAIL = '帳號'、$env:APP_PASSWORD = '密碼',再執行 node login_safely.mjs。跑第二次時如果印出「找不到登入輸入框」,通常代表上次的登入狀態被 browser-data 保留了。
  7. 驗證「省 token」的宣稱:到你的模型供應商用量頁面,記下跑之前與跑之後的 token 用量,並用電腦計時記下耗時。自己的任務、自己的數字,才是你能拿來做決定的依據。若日後版本的 API 寫法與範例不同,以官方 GitHub(github.com/browserbase/stagehand)文件為準。

🧠 工程思維透鏡(資深工程師看到的是什麼)

🔭 為什麼登入不直接叫 AI「幫我登入」,而要拆成 observe() 找位置、程式自己 fill()?
這是把兩種能力分開放:AI 擅長模糊判斷(頁面上哪一格是 email 欄位),程式擅長精確且可控的執行(把密碼一字不差填進去)。密碼只在你的程式與目標網站之間走,模型只看到頁面結構,這是最小授權原則:只給對方完成工作所需的最少資訊。資深工程師偏好用架構來保證安全,而不是依賴模型的口頭承諾。但要留意一個邊界(這是依範例的合理推論,不是素材明說的):extract() 需要讀頁面內容,登入後的頁面資料(例如發票金額)仍可能被送給模型。所以機密不外流只涵蓋你親手填的那一段,敏感頁面要另外評估。
🔭 「快 2 倍、省 80% token」這種標題數字,工程師會怎麼看?
先分清楚它是行銷主張還是實測結果。素材節錄只有標題,沒有交代比較的任務類型、模型與測試方式。工程師會自己建基準:同一個任務跑 20 次,記錄 token 用量、耗時、成功率三個數字。一定要把成功率放進去,只比速度和成本,很容易選到「又快又省但常做錯」的方案。token 少通常也代表延遲低,因為少送、少算,這兩個指標多半會一起變好,但是否一起變好,要靠實測,不靠推理。
🔭 act() 的自我修復很方便,代價是什麼?該全部都用它嗎?
固定選擇器的成本近乎零、結果可預測,但改版就壞;act() 用一句話描述目標,換來抗改版的能力,代價是每次都要呼叫模型,有費用、有延遲,也帶一點不確定性。成熟的做法是混合:穩定、要跑很多次的步驟用精準的選擇器,容易改版或每次長得不太一樣的步驟才交給 AI。素材自己的登入範例就是這種混合,填帳密用 Playwright 的 fill() 精準完成,只有點按鈕、換頁交給 act()。
🔭 extract() 為什麼一定要帶 schema,而不是直接回傳文字?
模型的自由輸出是不穩定的:金額可能帶著貨幣符號,是否已付可能寫成「已付」或「paid」。schema(zod 或 pydantic)把這些自由文字變成有型別、可驗證的資料:金額必須是數字、是否已付必須是真假值,格式不對就在系統邊界被擋下。好處是壞資料不會悄悄流進你的帳務試算表,出錯時也能在第一時間發現,而不是月底對帳才發現。

📝 隨堂考(點選答案,立即回饋)

Q1. 素材的登入範例用 observe() 取得選擇器,再用 page.locator().fill() 填入帳密,而不是叫 act() 去輸入密碼。主要原因是什麼?
✅ observe() 回傳的是欄位的位置(選擇器),密碼從環境變數讀出後由你的程式直接 fill(),整個過程不經過模型,這就是「憑證不會到達模型」的設計。
Q2. 設定 userDataDir: './browser-data' 的作用是什麼?
✅ userDataDir 就是瀏覽器的資料夾,登入後的 cookies 會留在裡面。也因此這個資料夾等同登入憑證,不可以上傳到 Git 或分享給別人。
Q3. extract() 搭配 zod 或 pydantic 的 schema,最主要的價值是什麼?
✅ schema 是資料的規格表,把模型的自由輸出變成有型別、通過驗證的資料,壞資料在邊界就被擋下,不會流進下游系統。
Q4. 看到標題「快 2 倍、省 80% token」,最合理的做法是什麼?
✅ 標題數字是別人的任務、別人的條件下的結果。你的網站、你的模型可能差很多,用自己的任務量測三個指標(含成功率)才是可靠的決策依據。
Q5. 哪一種情況最適合用固定的選擇器,而不是每次都呼叫 act()?
✅ 固定選擇器幾乎沒有成本、結果可預測,適合穩定且高頻的步驟。改版頻繁或不熟悉的頁面,才值得付出模型呼叫的成本換取彈性。

🃏 翻牌記憶卡(先想答案,再點開對答)

observe() 回傳什麼?為什麼重要?點我翻面
回傳元素真正的選擇器(位置)。程式再用它精準操作,帳號密碼因此不必經過模型。
act() 的用途與特色點我翻面
用一句話下動作指令(點按鈕、開頁面);素材標榜網站改版表單時它會自我修復。
extract() 搭配 schema 的意義點我翻面
回傳通過 zod(TypeScript)或 pydantic(Python)驗證的結構化資料,格式不對就攔下。
userDataDir 的用途與風險點我翻面
保存 cookies,下次啟動直接是登入狀態;但資料夾等同登入憑證,不可進版本庫或外傳。
Stagehand 與 Playwright 一句話差別點我翻面
Playwright 為測試而生,Stagehand 為 AI agent 而生(TypeScript/Python/Go)。

✅ 離開前自測(全勾=這課真的學會了)

0%