我用 onagent 做了一個 AI 客服,然後發現難的根本不是 AI
一個美髮沙龍的預約助理,從零到能真的幫客人訂位、查詢、取消。 工具寫完只花了十分鐘,提示詞卻改了三小時——這篇是那三小時的筆記。
先講結論:用 onagent 把一個 AI 客服接上網站,程式碼的部分真的不難。 難的是你得先想清楚「這個客服到底能做什麼」,而且想得比你以為的還要細。
我拿一個美髮沙龍的預約助理當練習題。需求聽起來很單純——讓客人用講的就能查有沒有空位、訂位、看自己訂了什麼、取消。 四件事,聽起來一個下午就能做完。
結果工具十分鐘就推上去了。然後我花了三小時在改提示詞。
onagent 在做的事
先講一下分工,不然後面的坑會看不懂為什麼是坑。
傳統上你要自己做一個會操作網站的 AI,得自己架 LLM agent 迴圈、自己管對話狀態、自己處理工具呼叫的往返。 onagent 把這段包掉了:你只要描述你的網站有哪些操作,它負責把使用者講的自然語言轉成實際的工具呼叫。
所以你的工作變成兩件事:
- 定義工具——這個網站能做什麼,每個操作要什麼參數、回什麼資料
- 寫提示詞——告訴 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(給程式用的識別碼)跟 date(2026-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 不會告訴你它不懂。它只會用它的理解繼續做下去。
幾個我下次會提早做的事
-
讓 Claude Code 產 tool,但自己檢查 kind——
description 它寫得比我好,
kind它會猜。 而這個欄位寫錯不會報錯,只會讓 AI 開始編故事,是最難查的一種 bug。 -
先跑一次「回傳什麼」的確認——推完工具之後,
在 Playground 隨便問一句,看 AI 講出來的內容是不是真的來自你的資料。
如果它答得出來但你的 log 裡沒有那筆查詢,那就是
kind錯了。 - 把「不要做什麼」也寫進去——只寫正面指示,它會用預設行為填補空白, 而預設行為通常就是猜。
- 用真人的標準測試——不要只測「幫我查週二有沒有空」, 要測「欸我下禮拜想弄頭髮」這種真人講話的方式。
所以值得嗎?
值得。三小時聽起來不多,但這三小時解決的是「這個客服到底該怎麼工作」, 而這件事不管你用什麼技術做都躲不掉。
onagent 幫我省掉的是另外那一大塊——連線管理、對話狀態、工具呼叫的往返、重連、配額。 那些如果自己刻,三小時大概只夠把 WebSocket 的重連寫對。 Claude Code 又再省掉建 app、產 YAML、推工具那些瑣事。
有趣的是,這兩層自動化都不能幫我做那三小時的事。 它們能幫我把想法變成程式碼,但沒辦法幫我決定「查不到空位」跟「被訂走了」是不是同一件事—— 那要有人真的去想沙龍是怎麼運作的。
最後那個沙龍助理現在可以正常接待客人:查空位、訂位、查自己的預約、取消。 客人講「我下週想找 Amy 弄頭髮」,它會先確認今天幾號、算出下週的日期、 查 Amy 的班表、把有空的時段用清單列出來,然後等客人選。
全程沒有一句話是我寫死的。但那七十行規範,每一行都是我用踩坑換來的。