本手冊會說明如何把三個 demo HTTP 端點(get_schedule、
get_speaker、book_meeting)變成你的 Perxona agent 可以呼叫的工具 — 而且同樣重要的是,如何把它們寫得讓 LLM 真正理解何時、以及如何使用它們。
搭配文件:API(權威 schema)。
1. LLM 如何「看見」一個工具
當 agent 運行時,Perxona 會把一份工具清單交給模型。每個工具本質上就是這樣(與 OpenAI 和 Anthropic 在底層使用的結構相同):
{
"name": "get_schedule",
"description": "List conference sessions. Optionally filter by day, track, or speaker.",
"parameters": { /* the INPUT JSON Schema */ }
}
在每一輪對話中,模型都只憑那段文字問自己:
- 我到底該不該呼叫工具? → 由名稱 + 描述決定。
- 呼叫哪一個? → 由各個描述彼此之間有多明顯不同決定。
- 用什麼參數? → 由輸入 schema決定(欄位名稱、型別、
required、描述、enum、預設值)。 - 我拿回了什麼/要告訴使用者什麼? → 由輸出 schema協助(讓它知道有
event_link存在、count是一個數字等等)。
所以身為建置者,你的工作就是把那四件事寫清楚。這就是整門手藝的全部。
一次呼叫底層實際上發生了什麼
決定呼叫,並產生符合輸入 schema 的參數"] B["Perxona 驗證參數(JSON Schema → Pydantic),
把 $context / $story 範本填入網址與標頭,
注入你的密鑰,然後送出 HTTP 請求"] C["你的伺服器執行並回傳 JSON"] D["Perxona 把 JSON 回饋給 LLM
(輸出 schema 幫助它解讀),
LLM 接著寫出給使用者看的回覆 —— 或呼叫另一個工具"] A --> B --> C --> D
你只需要建置伺服器 + 那四個文字/結構訊號。Perxona 會負責驗證、secret 注入、HTTP 呼叫、用量限制,以及(選擇性的)使用者確認。
2. Perxona 中的兩個建構區塊
設定一個工具一律是兩個物件:
| 物件 | 它是什麼 | 你在裡面放什麼 | LLM 看得到嗎? |
|---|---|---|---|
| OutboundAPI | 連線:你的伺服器在哪 + 如何認證 | Base URL、secret(API key) | 否 — 純粹是基礎設施 |
| CallFunc | agent 可以呼叫的工具 | 名稱、描述、method、path、輸入 schema、輸出 schema、require_confirmation |
是 — 這就是契約 |
- 為 demo 伺服器建立一個 OutboundAPI(一個 base URL + 一個 API key)。
- 建立三個 CallFunc(每個 function 一個),全部指向那個 OutboundAPI。
CallFunc 存在於 Storyboard 的 Panel 內,因此一個工具可以只在它有意義的對話狀態(panel)中提供。
3. 設定逐步教學
步驟 0 — 取得你的 public URL + API key
cd /Users/kilikkuo/Projects/perxona-callfunc-demo
./scripts/run.sh # terminal 1: server on :8090
./scripts/tunnel.sh # terminal 2: prints https://<random>.trycloudflare.com
grep API_KEY .env # your X-API-Key value
對 public URL 做基本檢查(health 不需要 key):
curl -s https://<random>.trycloudflare.com/health
步驟 1 — 建立 OutboundAPI
在後台(Backend Core / Outbound API)中建立一筆項目:
| 欄位 | 值 |
|---|---|
| Base URL | https://<random>.trycloudflare.com |
| Secret type | API_KEY |
| Header name | X-API-Key |
| Header value | (來自 .env 的 API_KEY) |
步驟 2 — 建立 CallFunc #1:get_schedule
| 欄位 | 值 |
|---|---|
| Type | OUTBOUND_API |
| Name | get_schedule |
| Method / Path | GET · /get_schedule |
| Description | List conference sessions. Optionally filter by day ("Day 1"/"Day 2" or a date like 2026-06-11), track (e.g. AI/ML), or speaker name. Use this when the user asks what talks/sessions are happening or when something is scheduled. |
require_confirmation |
false(唯讀) |
輸入 schema(query 參數):
{
"type": "object",
"properties": {
"day": { "type": "string", "description": "Day label ('Day 1') or ISO date ('2026-06-11')." },
"track": { "type": "string", "description": "Track substring, e.g. 'AI/ML'." },
"speaker": { "type": "string", "description": "Speaker name substring." }
}
}
輸出 schema(「Output data」欄位)— 完整區塊請見 API §1:
{
"type": "object",
"properties": {
"count": { "type": "integer", "description": "Number of sessions returned." },
"sessions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type":"string"}, "title": {"type":"string"}, "speaker": {"type":"string"},
"track": {"type":"string"}, "room": {"type":"string"}, "day": {"type":"string"},
"date": {"type":"string"}, "start": {"type":"string"}, "end": {"type":"string"}
}
}
}
}
}
步驟 3 — 建立 CallFunc #2:get_speaker
| 欄位 | 值 |
|---|---|
| Type | OUTBOUND_API |
| Name | get_speaker |
| Method / Path | GET · /get_speaker |
| Description | Look up a conference speaker by name (or exact id). Returns their title, company, bio, topics, and the sessions they present. Use this when the user asks about a speaker, who someone is, or what they're talking about. |
require_confirmation |
false |
輸入 schema:
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Speaker name to search for (substring)." },
"id": { "type": "string", "description": "Exact speaker id, e.g. 'spk_chen'." }
}
}
輸出 schema → 見 API §2:count + speakers[],內含
id, name, title, company, bio, topics[], sessions[]。
步驟 4 — 建立 CallFunc #3:book_meeting
| 欄位 | 值 |
|---|---|
| Type | OUTBOUND_API |
| Name | book_meeting |
| Method / Path | POST · /book_meeting |
| Description | Book a 1:1 meeting with a speaker at a specific date and time. Creates a calendar event and returns a confirmation. Use this only when the user explicitly wants to book/schedule a meeting with a named speaker. |
require_confirmation |
true(這是寫入 — 讓使用者核准) |
輸入 schema(payload):
{
"type": "object",
"required": ["attendee_name", "speaker", "date", "start_time"],
"properties": {
"attendee_name": { "type": "string", "description": "Name of the person requesting the meeting." },
"speaker": { "type": "string", "description": "Name of the speaker to meet." },
"date": { "type": "string", "description": "Meeting date, YYYY-MM-DD." },
"start_time": { "type": "string", "description": "Local start time, 24h HH:MM." },
"duration_minutes": { "type": "integer", "minimum": 5, "maximum": 240, "default": 30 },
"attendee_email": { "type": "string", "description": "Optional attendee email." },
"topic": { "type": "string", "description": "Optional meeting topic." }
}
}
輸出 schema → 見 API §3:booking_id, status, mode, summary, speaker, start,
end, timezone, event_link, message。
步驟 5 — 測試它
在一段 panel 對話中,試試:
- 「Day 1 有哪些 AI/ML 的議程?」 → 應該呼叫
get_schedule(day:"Day 1", track:"AI/ML")。 - 「Evelyn Chen 是誰,她要報告什麼?」 →
get_speaker(name:"chen")。 - 「幫我在 6 月 11 日下午 4 點,跟 Evelyn Chen 約 30 分鐘談架構。」
→
book_meeting(...)→ 確認提示(因為require_confirmation:true)→ 建立 event。
4. 手藝:撰寫 LLM 能理解的描述與 schema
這部分決定了 agent 給人聰明還是笨的感覺。以下是一些經驗法則,並附上前後對照。
4.1 名稱 = 模型用來比對的動詞
使用清楚的 verb_noun:get_schedule、book_meeting。讓三個名稱彼此分明,模型就永遠不必在兩個相似的工具之間猜測。
4.2 描述 = 何時使用它,而不只是它是什麼
描述會在每一輪,當模型決定是否要呼叫時被讀取。要說明它做什麼以及觸發時機(「在……時使用」),再加上任何限制。
| ❌ 含糊 | ✅ 清楚 |
|---|---|
| 「Schedule 端點。」 | 「列出會議議程。可選擇性地依日期、track 或講者篩選。當使用者詢問有哪些演講正在進行、或某件事的排程時使用。」 |
| 「預約一場會議。」 | 「在某個日期/時間與講者預約一對一。只在使用者明確想要預約時使用。會建立行事曆 event 並回傳確認。」 |
訣竅: - 提及它回傳什麼(「回傳 title、company、bio、topics」),讓模型知道這個工具能回答該問題。 - 說明否定範圍(「只在使用者明確想要預約時」)以阻止過早或意外的呼叫 — 對寫入尤其重要。 - 不要傾倒實作細節(DB、framework)。模型不在意這些,而且會浪費模型用來推理的 context。
4.3 輸入 schema = 參數契約
每個 property 的描述都是模型用來填寫該欄位的提示。請對格式具體說明,因為模型會照抄你的措辭。
- 格式:
"Meeting date, YYYY-MM-DD."和"Local start time, 24h HH:MM."會讓模型產生2026-06-11和16:00,而不是"next Tuesday"或"4 PM"。 required:列出沒有就無法運作的欄位(attendee_name, speaker, date, start_time)。模型會持續追問使用者,直到取得它們為止。- 預設值與界限:
duration_minutes有default: 30、minimum: 5、maximum: 240— 所以模型可以省略它,而不良的值會在送達你的伺服器之前被拒絕。 - Enum(本 demo 沒有,但很強大):
{"type":"string","enum":["confirmed","tentative"]}會把模型限制在有效的選項內。 - 保持扁平且精簡。巢狀過深的輸入會讓模型更難可靠地填寫。
4.4 輸出 schema = 模型如何讀取結果(以及為什麼要請你加上它)
呼叫之後,模型會讀取你的 JSON 回應。輸出 schema 告訴它有哪些欄位存在、各代表什麼意思,讓它能:
- 把正確的值帶進它的回覆(例如把
event_link呈現為可點擊的連結,或逐字讀回message); - 理解型別(它知道
count是數字、topics是清單); - 避免幻想出不存在的欄位。
用你描述輸入的相同方式來描述每個輸出欄位。以 book_meeting 為例,註明 event_link 是 string | null(在 mock 模式下為 null),以及 mode 會標示 "google_calendar" 還是 "mock",能讓 agent 誠實地措辭確認訊息。
4.5 黃金法則
5. 範本變數(選讀,了解有益)
Perxona 可以把對話 context 注入請求中,不必向 LLM 索取,做法是在 URL/headers/固定參數中使用 {$context.x} 和 {$story.x} 佔位符。範例:把已登入使用者的 id 以 {$context.user_id} 放進一個 header,這樣 book_meeting 就不需要模型傳遞它。本 demo 不需要這個,但這就是你把 secret 與已知 context 排除在模型之外的方式。
6. 安全與 UX 旋鈕
| 旋鈕 | 它做什麼 | 用於 |
|---|---|---|
require_confirmation: true |
在呼叫執行前,向使用者顯示核准/拒絕提示 | 任何寫入(book_meeting)、任何高成本或不可逆的操作 |
用量限制(call_limit / step_limit) |
限制一個工具每輪/每段對話可執行的次數 | 阻止失控的迴圈與 API 濫用 |
| Secret type(API_KEY / Bearer / None) | 於呼叫時注入認證 header,並以加密方式儲存 | 保持端點私密;絕不要把 key 放進描述中 |
| Panel 範圍限定 | 一個 CallFunc 只存在於你加入它的那些 panel 中 | 在使用者進入「預約」狀態前隱藏預約工具 |
就本 demo 而言:book_meeting → require_confirmation: true;兩個 get_* 讀取 → false。
7. 疑難排解
| 症狀 | 可能原因 | 修正 |
|---|---|---|
工具回傳 401 |
缺少/錯誤的 X-API-Key |
重新檢查 OutboundAPI 的 secret 值 + header 名稱 |
422 |
date/start_time 格式錯誤,或缺少必填欄位 |
收緊欄位描述(格式)與 required 清單 |
502(book_meeting) |
已設定 Google 但 insert 失敗 | 確認行事曆已與 service account 共用 |
| 模型從不呼叫該工具 | 描述太含糊/與另一個工具重疊 | 用清楚的「在……時使用」觸發語重寫描述;讓名稱分明 |
| 模型用錯誤的參數呼叫它 | 欄位描述不清,缺少格式/enum | 加上 description、格式、enum、required、預設值 |
| 模型重複預約或陷入迴圈 | 沒有確認/沒有限制 | 設定 require_confirmation: true 與用量限制 |
| Public URL 404/逾時 | Tunnel 重啟(新 URL)或伺服器停止 | 重新執行 ./scripts/tunnel.sh,更新 OutboundAPI base URL,再重新檢查 /health |
8. Perxona CallFunc 與 MCP 及其他 agentic 工具設計的比較
所有現代工具系統都共用你剛才用到的同一個核心觀念:一個工具就是名稱 + 描述 + JSON Schema,而模型依據那份契約進行推理。它們的差異在於誰來托管工具、如何傳輸,以及平台為你做了什麼。
快速比較
| 面向 | Perxona CallFunc / OutboundAPI | MCP(Model Context Protocol) | 原生 function calling(OpenAI / Anthropic tool use) |
|---|---|---|---|
| 你如何定義工具 | 在管理 UI 中:URL + JSON Schema + 描述(無需程式碼) | 撰寫並執行一個對外提供工具的 MCP 伺服器 | 在你的 API 請求中內嵌宣告工具;由你的應用程式執行它們 |
| 需要的程式碼 | 無(只需要那個 HTTP 端點,可以是任何現有的 API) | 需要 — 實作 MCP 伺服器 | 需要 — 在你的應用程式中實作工具執行器 |
| 傳輸 | 僅 HTTPS 請求/回應 | stdio 或 HTTP/SSE;具狀態的 session | 在你的應用程式內處理;由你發出 provider 呼叫 |
| 工具探索 | 靜態(事先依 panel 設定好) | 動態(tools/list);伺服器在執行時宣告工具 |
靜態(你在每次請求傳入工具清單) |
| 工具以外 | 僅工具 | Resources、prompts、sampling、notifications | 僅工具(provider 特有的額外功能) |
| 串流/長時間執行 | 否(單次往返) | 透過協定支援 | 取決於你的應用程式 |
| 認證與 secret | 內建 — 於呼叫時注入加密的 secret | 你在伺服器中實作 | 你在應用程式中實作 |
| Human-in-the-loop | 內建(require_confirmation) |
取決於 client(例如 Claude Desktop 的提示) | 由你建置 |
| 用量限制/安全 | 內建(call/step 限制、SQL-AST 安全) | 由你建置 | 由你建置 |
| 多租戶/受管理 | 是 — 依組織範圍限定,由平台托管 | 由你營運伺服器 | 由你營運應用程式 |
| 可攜性/重用 | 僅限 Perxona(專有) | 開放標準 — 一個伺服器可跨 Claude Desktop、IDE 和其他 MCP client 重用 | Provider/SDK 特有,但是一種廣為共用的結構 |
| 最適合…… | 你在 Perxona 內建置,並想要無程式碼 HTTP 工具 + 受管理的防護機制 | 你想要跨眾多 AI client 可重用、可探索的工具伺服器,並具備 resources/狀態 | 你正在打造自己的 agent 應用程式,並想要完整掌控 |
Perxona CallFunc 取向的優點
- 無程式碼/低程式碼。任何 HTTP API 只要貼上 URL + JSON Schema 就能變成工具。非常適合 vibe coder,也適合在幾分鐘內把現有後端轉成 agent 工具 — 不必撰寫或托管轉接伺服器(MCP 整合意味著要建置並營運一個伺服器)。
- 功能齊備(batteries included)。加密的 secret、每個工具的用量限制、
require_confirmationhuman-in-the-loop、context 範本化({$context}/{$story})、text-to-SQL 安全、logging 和多租戶範圍限定全都提供。用 MCP 或原生 function calling,這些全都得你自己建。 - 緊密的產品整合。工具存在於 persona/storyboard 流程內,可以做 panel 範圍限定(只在正確的對話狀態才可用),並與 memory、防護機制和對話逐字稿走在同一條 pipeline 上。
- 輸出 schema 作為一級欄位。你宣告回傳什麼,這有助於模型解讀結果並讓回應保持誠實(例如
mock與真實event_link的差別)。
缺點/限制
- 專有且綁定平台。你的工具存在於 Perxona 中;無法移植到其他 host。MCP 最大的優勢就是重用 — 工具伺服器寫一次,就能在 Claude Desktop、IDE 和任何相容 MCP 的 client 上使用。CallFunc 無法在 Perxona 之外重用。
- 僅 HTTP 請求/回應。沒有 local/stdio 工具、沒有串流或長時間執行的工作、沒有具狀態的工具 session,也沒有 MCP 的 resources / prompts / sampling 原語。(在目前的後端中,
DATA_VALIDATIONCallFunc 類型尚未實作,而且沒有工具結果快取、批次處理或 circuit-breaker — 它就是一次乾淨的往返。) - 靜態設定。工具是在 UI 中事先設定好的;沒有像 MCP 的
tools/list那種執行時探索。變更一個工具意味著要在後台中編輯它。 - 你必須托管一個 public 端點。它必須能從 Perxona 連到(因此有 cloudflared tunnel),而你要負責它的 uptime、延遲、逾時和 TLS。MCP 伺服器可以在 client 旁邊本機運行;原生 function 則在你自己的應用程式內運行。
- Schema 方言耦合。輸入/輸出使用 JSON Schema(Draft 2020-12),透過
jambo轉換為 Pydantic;非常罕見的 schema 功能可能無法乾淨地轉換。讓 schema 保持簡單且扁平(反正這對模型也是好做法)。
該如何思考它
- 這些取向在精神上並不互斥 — 它們全都建立在名稱 + 描述 + schema之上。你在這裡學到的東西可以直接轉移到 MCP 和原生 function calling 上。
- 使用 Perxona CallFunc,當你要在 Perxona 內出貨,並想要從「有一個 HTTP API」到「agent 能用它」的最快路徑,而且確認/限制/secret 都已為你處理好 — 正是這個 demo。
- 採用 MCP,當你想要一個可跨眾多 AI client 重用的工具/資料伺服器,需要 local 或具狀態的存取,或想要超越單純 function 呼叫的 resources/prompts。
- 使用原生 function calling,當你正直接在 LLM API 上打造你自己的 agent 應用程式,並想要對執行有端到端的掌控。
9. 一頁回顧
- 兩個物件:一個 OutboundAPI(URL + API key)+ 每個 function 一個 CallFunc (名稱、描述、method、path、輸入 schema、輸出 schema、確認)。
- 模型只看到名稱 + 描述 + 輸入 schema + 輸出 schema — 把它們寫給一位聰明的陌生人看。
- 描述 = 它做什麼 + 何時使用它(+ 它回傳什麼,+ 不該做什麼)。
- 輸入 schema = 欄位名稱、型別、
required、格式、預設值、enum。 - 輸出 schema = 回傳什麼,讓模型正確讀取結果。
- 寫入要加
require_confirmation: true。讀取則不用。 - 這是 HTTP + 受管理的防護機制,無需程式碼 — 在 Perxona 內極利於速度;MCP 在重用/可攜性與更豐富的原語上勝出;原生 function calling 則在你自己應用程式中的完整掌控上勝出。