Ripple

我用 onagent 做了一個 AI 客服,然後發現難的根本不是 AI

一個美髮沙龍的預約助理,從零到能真的幫客人訂位、查詢、取消。 工具寫完只花了十分鐘,提示詞卻改了三小時——這篇是那三小時的筆記。

美髮沙龍預約助理的對話:客人說「我下週想找 Amy 弄頭髮」,助理回覆 Amy 下週的三個空檔(週三下午 2:00、週五上午 11:00、週六下午 4:30);右側並列顯示這一輪實際呼叫的三個工具 get_today_date、check_availability、book_appointment。
客人說「我下週想找 Amy 弄頭髮」,助理先查今天日期,再列出 Amy 的空檔。

先講結論:用 onagent 把一個 AI 客服接上網站,程式碼的部分真的不難。 難的是你得先想清楚「這個客服到底能做什麼」,而且想得比你以為的還要細。

我拿一個美髮沙龍的預約助理當練習題。需求聽起來很單純——讓客人用講的就能查有沒有空位、訂位、看自己訂了什麼、取消。 四件事,聽起來一個下午就能做完。

結果工具十分鐘就推上去了。然後我花了三小時在改提示詞。

onagent 在做的事

先講一下分工,不然後面的坑會看不懂為什麼是坑。

傳統上你要自己做一個會操作網站的 AI,得自己架 LLM agent 迴圈、自己管對話狀態、自己處理工具呼叫的往返。 onagent 把這段包掉了:你只要描述你的網站有哪些操作,它負責把使用者講的自然語言轉成實際的工具呼叫。

所以你的工作變成兩件事:

  1. 定義工具——這個網站能做什麼,每個操作要什麼參數、回什麼資料
  2. 寫提示詞——告訴 LLM 什麼時候該用哪個工具,以及怎麼跟客人講話

第一件事是工程問題,有標準答案。第二件事沒有,而且是真正花時間的地方。

第一步:讓 Claude Code 幫你把工具推上去

工具定義是 YAML,理論上你可以自己寫、自己用 CLI 推。但 onagent 有出一個 Claude Code 的 skill, 直接讓 Claude Code 代勞——這是我推薦的方式,原因等一下講。

先裝 skill:

npx claude-skill-onagent          # 裝到專案的 ./.claude/skills/
npx claude-skill-onagent --user   # 或裝到 ~/.claude/skills/

裝完之後,在 Claude Code 裡直接用講的就行。我當時大概是這樣講的:

幫我在 onagent 建一個 support-app,這是美髮沙龍的預約助理。 需要這些工具:查空位、訂位(含取消)、查自己的預約。查空位要能用日期跟設計師篩選。

然後它就把整套流程跑完了:登入(onagent login --web 會自動開瀏覽器)、 建 app、產生 tool YAML、一個一個推上去。

onagent app create support-app
onagent tool create support-app check_availability.yaml
onagent tool create support-app book_appointment.yaml
onagent tool create support-app get_my_appointments.yaml

為什麼推薦這樣做:不是因為省打字,而是因為 tool 的 description 是寫給 LLM 看的,讓另一個 LLM 來寫它意外地合適。 我自己寫會寫成「查詢可預約時段」,Claude Code 寫出來的是:

description: >
  Looks up which appointment slots are open or already booked.
  Call this whenever the visitor asks about a stylist's
  availability, what times are open, or wants to see the
  schedule — not for questions unrelated to booking.

差別在後面那句「不是給跟預約無關的問題用的」。 我不會想到要寫這種話,但這正是 LLM 判斷「現在該不該呼叫這個工具」時需要的資訊。

當然它也不是每次都對。有幾個地方我還是得回頭改——最重要的是下面這個欄位。

唯一一定要自己確認的:kind

action 是「做完就算了」——例如點一個按鈕、跳轉頁面,LLM 不需要知道結果。 query 則是「我要拿資料回來繼續推理」——查空位這種就是, LLM 拿到有哪些時段之後,才能接著跟客人討論。

這三個工具全都是 query。連「訂位」都是——因為 LLM 需要知道 訂成功了沒,才能跟客人說「好了」還是「抱歉剛被搶走了」。

把 query 寫成 action,LLM 會收到「執行成功」四個字,然後開始編造它以為的查詢結果。

這個坑我踩得很痛,因為它不會報錯。功能看起來是通的,LLM 也很有自信地回答你, 只是內容全是它自己掰的。後面會再提到這件事。

第二步:接上前端

工具定義好之後,前端這邊用 Agent Bridge SDK 把每個工具的實作接上去。 大致長這樣:

import { AgentBridge, defineTool } from '@onagent/bridge'

const bridge = new AgentBridge({
  url: WS_URL,
  appId: APP_ID,
  apiKey: API_KEY,

  onAssistantMessage: (text) => {
    // AI 要跟客人講的話
    appendMessage('assistant', text)
  },

  tools: [
    defineTool(
      'check_availability',
      (raw) => validateArgs(raw),      // 驗證參數
      (args) => findOpenSlots(args),   // 實際查詢
    ),
    // ... 其他工具
  ],
})

defineTool 分成驗證跟執行兩段,這個設計我一開始覺得囉唆, 後來發現很值得——LLM 傳來的參數不保證符合你的 schema, 它可能少傳必填欄位、可能把 enum 傳成別的字串。驗證那段就是你的防線。

到這邊,程式碼的部分就結束了。真的,就這樣。


然後就是三小時的提示詞地獄

功能都通了,我很開心地開始測試,然後遇到一連串完全不在預期內的問題。

問題一:「明天」是哪天?

客人說「明天有空位嗎」,LLM 很自信地去查了一個日期。錯的日期。

因為 LLM 不知道今天幾號。它的訓練資料有截止日期,它會用一個它「覺得」合理的日期去推算, 結果就是查到三個月前的空檔。

解法是補上第四個工具 get_today_date——原本的需求裡根本沒想到要有它—— 然後在提示詞裡明確要求:

If the visitor uses a relative date phrase (e.g. "today,"
"tomorrow," "this Friday," "next week"), call get_today_date
with format: "date" first to resolve it to an actual
YYYY-MM-DD date before calling check_availability.

Don't guess what today's date is, or compute a relative date,
from context alone.

注意最後那句「不要用上下文猜今天幾號」。少了這句,它還是會猜。 你必須把「不要做什麼」也寫清楚,只寫「要做什麼」是不夠的。

問題二:查不到 ≠ 被訂走

我的查詢工具只回傳「有設計師排班的時段」。所以如果週日整天沒人上班,回傳結果就不會有週日。

LLM 看到沒有週日,就跟客人說「週日都被訂滿了喔」。

這完全是合理的誤解——資料本身沒有錯,是資料的語意沒有講清楚。 後來我在提示詞裡補了一段,講明白這兩件事的差別:

check_availability only returns slots where a stylist is
actually scheduled — a time that does not appear in the result
means no stylist works that slot at all in this window, not
that it's booked.

For a slot that DOES appear, check its `available` field:
true means open, false means already booked.

這類問題的共通點是:你以為很明顯的事,對 LLM 一點都不明顯。 你的資料結構背後那些「大家都知道」的商業邏輯,它沒有那個「大家」的脈絡。

問題三:它把內部 ID 唸給客人聽

查詢結果每個時段都有 slotId(給程式用的識別碼)跟 date2026-09-15 這種格式)。

然後 AI 就跟客人說:「您可以選擇 2026-09-15 的 10am,slot ID 是 a7f3c...」

技術上完全正確,體驗上非常荒謬。沒有一個櫃檯人員會這樣講話。 所以提示詞又多了一段,明確區分「哪些欄位是給你內部用的、哪些是可以講出口的」:

use the slot's `day` field (a friendly weekday label) for this,
not its `date` field (the YYYY-MM-DD value, which is only meant
for you to track internally and pass to book_appointment, not
to say aloud).

問題四:取消訂位時它用錯了 ID

這個最危險。客人說「幫我取消」,LLM 從查詢空位的結果裡挑了一個 slotId 就送去取消。

那個 slotId 不是這個客人的預約,是別人的,或根本是個空時段。

正確流程應該是:先查「這個客人自己的預約」,從那裡面拿 slotId,再取消。 但如果不講,LLM 只會看到「我需要一個 slotId」然後從手邊任何有 slotId 的地方拿一個。

If the visitor wants to cancel a booking, first call
get_my_appointments to find the exact slotId of the booking
they mean — never guess or reuse a slotId from
check_availability for this.

Only cancel a slotId that get_my_appointments actually
returned; if it isn't in that list, tell the visitor you can't
find that booking rather than attempting the cancellation
anyway.

這裡的重點是最後那句:明確告訴它失敗時該怎麼辦。 否則 LLM 傾向於「想辦法完成任務」,而不是「承認做不到」。


回頭看,提示詞其實是在寫規格書

三小時之後我的提示詞從五行變成七十行。回頭讀那七十行,我發現它們根本不是「提示詞」, 而是一份寫給非人類同事看的工作規範

  • 什麼情況該用哪個工具(以及不該用)
  • 每個欄位的語意,包括「沒出現」代表什麼
  • 哪些資料是內部的、哪些能講給客人聽
  • 多步驟流程的正確順序
  • 做不到的時候要說什麼,而不是硬幹

如果今天來的是一個真人新員工,你其實也要講這些。差別只在於新人會問「這個 slotId 是什麼意思」, 而 LLM 不會問——它會直接猜,而且猜得很有自信。

LLM 不會告訴你它不懂。它只會用它的理解繼續做下去。

幾個我下次會提早做的事

  1. 讓 Claude Code 產 tool,但自己檢查 kind—— description 它寫得比我好,kind 它會猜。 而這個欄位寫錯不會報錯,只會讓 AI 開始編故事,是最難查的一種 bug。
  2. 先跑一次「回傳什麼」的確認——推完工具之後, 在 Playground 隨便問一句,看 AI 講出來的內容是不是真的來自你的資料。 如果它答得出來但你的 log 裡沒有那筆查詢,那就是 kind 錯了。
  3. 把「不要做什麼」也寫進去——只寫正面指示,它會用預設行為填補空白, 而預設行為通常就是猜。
  4. 用真人的標準測試——不要只測「幫我查週二有沒有空」, 要測「欸我下禮拜想弄頭髮」這種真人講話的方式。

所以值得嗎?

值得。三小時聽起來不多,但這三小時解決的是「這個客服到底該怎麼工作」, 而這件事不管你用什麼技術做都躲不掉。

onagent 幫我省掉的是另外那一大塊——連線管理、對話狀態、工具呼叫的往返、重連、配額。 那些如果自己刻,三小時大概只夠把 WebSocket 的重連寫對。 Claude Code 又再省掉建 app、產 YAML、推工具那些瑣事。

有趣的是,這兩層自動化都不能幫我做那三小時的事。 它們能幫我把想法變成程式碼,但沒辦法幫我決定「查不到空位」跟「被訂走了」是不是同一件事—— 那要有人真的去想沙龍是怎麼運作的。

最後那個沙龍助理現在可以正常接待客人:查空位、訂位、查自己的預約、取消。 客人講「我下週想找 Amy 弄頭髮」,它會先確認今天幾號、算出下週的日期、 查 Amy 的班表、把有空的時段用清單列出來,然後等客人選。

全程沒有一句話是我寫死的。但那七十行規範,每一行都是我用踩坑換來的。

AI 說這樣可以

寫程式、踩坑、然後把坑寫下來。專門記錄那些「照著文件做卻不會動」的瞬間, 以及 AI 很有自信地給我錯答案的每一天。