讓 AI 助理「動手」但不越權:動作卡、確認閘、風險分級怎麼設計
目錄
站內 AI 助理從「回答問題」走到「替你動手」,最怕的不是它聽不懂,是它聽錯了還照做。我們這週在即將上線的 MindThread 2.0 預覽站,讓小麥開始會動手:排一篇文、改發文時段、改人設語氣、加海巡關鍵字、開關雲端海巡、暫停帳號。做法一句話:模型碰不到任何工具。規則判斷你想做什麼,模型只填卡片欄位,伺服器存卡,你按確認才執行,高風險要按兩次,執行前重讀現況,每張卡從建立到結果都留紀錄。預覽站實走 33 項全過。
上一篇我們寫過小麥怎麼只講真數字,那是「讀」。這篇是「寫」,風險高一個等級。
為什麼不直接給模型 function calling?
讀錯了是答錯一題,寫錯了是帳號停發、海巡關掉,而且會員可能好幾天都沒發現。
另一個原因是輸入不可信。會員打的話是一種輸入,小麥能讀到的資料裡還有別人寫的貼文和留言。讓模型自己決定呼叫哪個寫入工具,等於讓這些文字有機會影響寫入。
還有一點很實際:規則可以測。我們的意圖路由加卡片欄位、確認閘、風險等級,一共寫了 179 項單元測試,接在 build 之前跑。模型的工具選擇很難這樣測。
代價是彈性變小。對不到規則的句子,小麥就當成問題回答,不會動手。我們接受這個代價。
第一關:規則判斷意圖,模型只填欄位
六種動作和風險等級:
- 排一篇:低
- 加海巡關鍵字:低;雲端海巡正開著時算中
- 改發文時段:中
- 改人設語氣:中(只在人設最後加一行,原文不動)
- 開雲端海巡:中;關雲端海巡:高
- 暫停帳號:高
判斷順序有講究。先把「不是要動手」的句子排掉:建議句(「要不要開海巡」)、疑問句、否定句(「不要關」)、過去式(「我昨天已經關了」)。這些交給只讀的問答流程。規則難免漏判,萬一把一句話誤判成動作,也只是多出一張要你按確認的卡,這正是後面確認閘存在的理由。
接著先判排程。「排一篇關於海巡的文」裡有「海巡」兩個字,如果先判開關,就會被當成關海巡。然後判不支援的動作,最後才判其他五類。英文句子要以動詞開頭才算。
刪文、改方案、回留言、立即發文,這四件事小麥不做,只回「這件事小麥還不能替你做」並附那一頁的連結。恢復帳號也不做,小麥會請你到帳號設定自己打開。恢復帳號不做成卡,是因為把帳號從停用改回啟用,在我們的護欄系統裡會被當成「放寬」,跳出「AI 助手放寬了護欄」的提醒,反而誤導會員。
模型拿到的工作只有填欄位:哪個帳號、什麼時間、全文、哪些關鍵字。填完程式再驗一次。帳號只從這位會員自己的帳號清單裡找,找不到就請會員說清楚;排程時間要在至少 5 分鐘之後。排一篇的內文會吃帳號人設和輸出語言,再跑一次捏造檢查,抓到捏造重寫一次,還是捏造就不出卡。
風險等級也由程式決定,不給模型判斷:
export function riskOf(kind, params) {
if (kind === 'schedule_post') return 'low';
if (kind === 'add_keywords') return params.patrolOn ? 'medium' : 'low';
if (kind === 'patrol_toggle' || kind === 'account_pause') return params.enabled === false ? 'high' : 'medium';
return 'medium';
}
第二關:每張卡都要按確認
卡片在伺服器建立、存在資料庫。前端按確認時只送卡片編號,參數一律用伺服器存的那份,所以在瀏覽器改請求也改不了要執行的內容。
按下確認,伺服器在同一筆交易裡跑確認閘。以下是簡化版,省略了工作區成員和操作者身分的檢查:
// 純函式,跟「把狀態改成 executing」放在同一筆交易裡
export function confirmGate(card, ctx) {
if (!card) return { ok: false, code: 'not_found' };
if (card.ownerUid !== ctx.uid) return { ok: false, code: 'not_owner' };
if (card.status !== 'pending') return { ok: false, code: 'not_pending' }; // 連按兩次,第二次停在這
if (card.id !== ctx.latestCardId) return { ok: false, code: 'not_latest' }; // 只有最新那張能執行
if (ctx.nowMs > card.createdAtMs + 15 * 60 * 1000) return { ok: false, code: 'expired' };
if (card.risk === 'high') {
const first = card.firstConfirmedAtMs;
if (!(ctx.second === true && first && ctx.nowMs - first >= 1000)) {
return { ok: false, code: 'need_second', markFirst: !first }; // 伺服器記住第一次
}
}
return { ok: true };
}
幾個細節:
- 一次一張卡。建新卡時,同一筆交易把上一張還沒按的卡改成作廢,並更新「最新那張」的編號。
- 15 分鐘過期。時間一久,設定可能已經變了,過期就請會員再說一次。
- 「改一下」。卡片作廢,原話放回輸入框,改完重送。
- 只有本人。工作區成員一律擋下,只能問不能動手。
第三關:高風險要按兩次,由伺服器擋
關掉海巡、暫停帳號,是我們定為高風險的兩件事。我們的考量是停下來的東西沒有聲音:開海巡錯了,你會看到多出來的留言;關錯了,什麼都不會發生,要等你發現成效掉了才知道。
第一次按,伺服器只記下第一次確認的時間,回「需要第二次確認」,畫面出一段警告和「我了解,確定執行」。第二次要帶 second=true,而且距離第一次至少 1 秒,伺服器才放行。
老實說,第一版的二次確認只做在前端。我們自己跑 critic 審查時被抓出來:直接送 second=true 就能跳過。現在改成伺服器記第一次、比時間。
第四關:執行前重讀,狀態變了就不做
假設小麥建了一張「發文時段 09:30、21:30 改成 09:00、21:00」的卡,你還沒按,先去設定頁把時段改成 10:00。這時候按確認,卡片會把整欄覆寫回 09:00、21:00,你剛改的 10:00 就不見了。
所以執行前重讀帳號現況,跟卡片記下的「原本」比對。人設、時段、關鍵字、開關只要不同,就不執行:
export function staleAgainst(card, account) {
if (!account) return true;
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
switch (card.kind) {
case 'set_times': return !same(account.scheduleTimes, card.params.before);
case 'set_tone': return account.persona !== card.params.personaBefore;
case 'add_keywords': return !same(account.keywords, card.params.before);
case 'patrol_toggle': return account.patrolEnabled === card.params.enabled;
case 'account_pause': return account.enabled === card.params.enabled;
default: return false;
}
}
執行本身沒有新增任何寫入路徑。小麥呼叫的是網頁本來就在用的那幾條 API,那些 API 原本的檢查照跑:帳號是不是你的、格式、內容規則、全站暫停開關、方案限制。小麥能做的事,不會比你自己在網頁上按的多。
稽核紀錄怎麼留?
每張卡從建立、第一次確認時間、確認、執行結果到取消,都寫在伺服器的卡片集合裡。資料庫規則沒有開放這個集合,前端讀寫一律被拒,只有伺服器能寫,等於一份唯讀稽核。
執行結果回到對話,同時寫一筆通知,連到對應的設定頁。如果動到的是護欄相關設定(例如暫停帳號),護欄變更紀錄照記,執行者標成小麥加會員編號,事後查得到是誰透過什麼做的。
額度也算清楚:產生動作卡計入 AI 用量,跟問答同一套每日上限,生成失敗退回;按確認執行不另外計。
實測結果與還沒做好的
預覽站用測試帳號實走六種動作,加上「改一下」和英文介面取消,33 項全過。資料庫對照:7 張卡執行成功、2 張取消,兩張高風險卡都有兩段確認的紀錄,console 沒有錯誤。實走抓到 3 個文字和版面問題,修完用 build 和單元測試驗過,沒有重跑整套實走。
還沒做好的,照實列:
- 高風險是再按一次,不是打字確認。要更嚴可以改成輸入帳號名。
- 海外帳號在夏令時間切換那一小時,排程時間可能差一小時。台灣不受影響。
- 預覽站沒有排程器,「排的那篇到點真的發出」沒在預覽站驗,那段走的是跟手動排程同一條既有管線。
想自己做,照這個順序
- 列出要開放的動作,每一種寫清楚會改哪個欄位、走哪條既有 API。
- 用規則判斷意圖,先排掉建議句、疑問、否定、過去式。
- 模型只輸出欄位,程式驗欄位,風險等級由程式給。
- 伺服器存卡,前端只送編號;確認閘放在交易裡,過了就改狀態。
- 高風險的第二次確認由伺服器記時間。
- 執行前重讀現況,跟卡片的「原本」比對。
- 卡片集合只給伺服器寫,當稽核紀錄。
這套東西不需要很聰明的模型,需要的是你先想清楚哪些事寧可不做。即將上線的 MindThread 2.0,小麥就是照這七步做的。
常見問題
讓 AI 助理替使用者執行動作,怎麼避免它做錯事?
別讓模型決定要呼叫什麼。我們的做法是規則先判斷使用者想做哪一種動作,模型只負責把卡片欄位填好,程式再驗一次欄位,伺服器把卡存起來。使用者按確認時只送卡片編號,參數一律用伺服器存的版本,經過確認閘檢查才執行。建議句、疑問句、否定句和過去式會先被規則擋掉,只回答;萬一誤判,也只是多一張要你按確認的卡,不會直接執行。
高風險動作為什麼要按兩次,而且要由伺服器檢查?
因為關掉海巡或暫停帳號之後,系統就安靜了,不會有東西提醒你它停了。第一次按,伺服器只記下時間並回「需要第二次確認」;第二次要帶 second=true,而且距第一次至少 1 秒才放行。如果只在前端擋,有人直接送 second=true 就能跳過,我們第一版就是這樣,自審時才改成伺服器擋。
建好卡到按確認之間,設定被別處改了怎麼辦?
執行前重讀一次帳號現況,跟建卡時記下的「原本」比對。人設、發文時段、關鍵字或開關只要不一樣,就不執行,回「狀態變了,請重新說一次」。不然卡片上的整份覆寫,會把你剛在設定頁改好的東西蓋掉。