開發者配套

CallFunc — 開發者參考手冊

在 Perxona 中設定工具 — 給建置者的實作指南

易讀的 HTML 版面 適用對象:接線即時示範後端的技術讀者
藍色區塊 在 console 中要執行的具體操作。
琥珀色區塊 常見陷阱、限制與注意事項。
綠色區塊 用來確認運作正常的檢查。
紫色區塊 可交給 AI 工具的提示片段。

本手冊會說明如何把三個 demo HTTP 端點(get_scheduleget_speakerbook_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 */ }
}

在每一輪對話中,模型都只憑那段文字問自己:

  1. 我到底該不該呼叫工具? → 由名稱 + 描述決定。
  2. 呼叫哪一個? → 由各個描述彼此之間有多明顯不同決定。
  3. 用什麼參數? → 由輸入 schema決定(欄位名稱、型別、required、描述、enum、預設值)。
  4. 我拿回了什麼/要告訴使用者什麼? → 由輸出 schema協助(讓它知道有 event_link 存在、count 是一個數字等等)。

所以身為建置者,你的工作就是把那四件事寫清楚。這就是整門手藝的全部。

一次呼叫底層實際上發生了什麼

flowchart TB A["LLM 讀取 name + description + 輸入 schema
決定呼叫,並產生符合輸入 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輸出 schemarequire_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 (來自 .envAPI_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 §2count + 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 §3booking_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_nounget_schedulebook_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-1116:00,而不是 "next Tuesday""4 PM"
  • required列出沒有就無法運作的欄位(attendee_name, speaker, date, start_time)。模型會持續追問使用者,直到取得它們為止。
  • 預設值與界限:duration_minutesdefault: 30minimum: 5maximum: 240 — 所以模型可以省略它,而不良的值會在送達你的伺服器之前被拒絕。
  • Enum(本 demo 沒有,但很強大):{"type":"string","enum":["confirmed","tentative"]} 會把模型限制在有效的選項內。
  • 保持扁平且精簡。巢狀過深的輸入會讓模型更難可靠地填寫。

4.4 輸出 schema = 模型如何讀取結果(以及為什麼要請你加上它)

呼叫之後,模型會讀取你的 JSON 回應。輸出 schema 告訴它有哪些欄位存在、各代表什麼意思,讓它能:

  • 把正確的值帶進它的回覆(例如把 event_link 呈現為可點擊的連結,或逐字讀回 message);
  • 理解型別(它知道 count 是數字、topics 是清單);
  • 避免幻想出不存在的欄位。

用你描述輸入的相同方式來描述每個輸出欄位。以 book_meeting 為例,註明 event_linkstring | 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_meetingrequire_confirmation: true;兩個 get_* 讀取 → false。


7. 疑難排解

症狀 可能原因 修正
工具回傳 401 缺少/錯誤的 X-API-Key 重新檢查 OutboundAPI 的 secret 值 + header 名稱
422 datestart_time 格式錯誤,或缺少必填欄位 收緊欄位描述(格式)與 required 清單
502(book_meeting) 已設定 Google 但 insert 失敗 確認行事曆已與 service account 共用
模型從不呼叫該工具 描述太含糊/與另一個工具重疊 用清楚的「在……時使用」觸發語重寫描述;讓名稱分明
模型用錯誤的參數呼叫它 欄位描述不清,缺少格式/enum 加上 description、格式、enumrequired、預設值
模型重複預約或陷入迴圈 沒有確認/沒有限制 設定 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_confirmation human-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_VALIDATION CallFunc 類型尚未實作,而且沒有工具結果快取、批次處理或 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. 一頁回顧

  1. 兩個物件:一個 OutboundAPI(URL + API key)+ 每個 function 一個 CallFunc (名稱、描述、method、path、輸入 schema、輸出 schema、確認)。
  2. 模型只看到名稱 + 描述 + 輸入 schema + 輸出 schema — 把它們寫給一位聰明的陌生人看。
  3. 描述 = 它做什麼 + 何時使用它(+ 它回傳什麼,+ 不該做什麼)。
  4. 輸入 schema = 欄位名稱、型別、required、格式、預設值、enum。
  5. 輸出 schema = 回傳什麼,讓模型正確讀取結果。
  6. 寫入要加 require_confirmation: true讀取則不用。
  7. 這是 HTTP + 受管理的防護機制,無需程式碼 — 在 Perxona 內極利於速度;MCP 在重用/可攜性與更豐富的原語上勝出;原生 function calling 則在你自己應用程式中的完整掌控上勝出。