核心指南

CallFunc 操作手冊

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

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

本手冊將示範如何把三個示範用的 HTTP 端點(get_scheduleget_speakerbook_meeting)變成你的 Perxona 代理可以呼叫的工具 — 同樣重要的是,如何撰寫它們,好讓 LLM 真正理解何時以及如何使用它們。

相關文件:API(具權威性的 schema)。


1. LLM 如何「看見」一個工具

當代理執行時,Perxona 會把一份工具清單交給模型。每個工具基本上就是這樣(與 OpenAI 和 Anthropic 在底層使用的結構相同):

{
  "name": "get_schedule",
  "description": "List conference sessions for the 2026-06-24/25 Japan demo. Optionally filter by day, track, or speaker.",
  "parameters": { /* the INPUT JSON Schema */ }
}

在每一個回合,模型都會僅依據那段文字問自己:

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

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

一次呼叫實際上會發生什麼(底層運作)

LLM reads name + description + input schema
        │  decides to call, emits arguments matching the input schema
        ▼
Perxona validates args (JSON Schema → Pydantic), renders any {$context}/{$story}
templates into the URL/headers, injects your secret, and makes the HTTP request
        │
        ▼
Your server runs and returns JSON
        │
        ▼
Perxona feeds the JSON back to the LLM (the output schema helps it interpret),
which then writes the user-facing reply — or calls another tool.

你只需要建立伺服器 + 那四個文字/結構訊號。Perxona 負責驗證、密鑰注入、HTTP 呼叫、用量限制,以及(選用的)使用者確認。


2. Perxona 中的兩個建構元件

設定一個工具永遠都是兩個物件

物件 它是什麼 你在裡面放什麼 LLM 看得到它嗎?
OutboundAPI 連線:你的伺服器在哪裡 + 如何驗證 Base URL、密鑰(API key) 不 — 僅屬於基礎設施
CallFunc 代理可呼叫的工具 名稱、描述、method、path、輸入 schema輸出 schemarequire_confirmation — 這就是合約
  • 為示範伺服器建立一個 OutboundAPI(一個 base URL + 一把 API key)。
  • 建立三個 CallFunc(每個函式一個),全部指向那個 OutboundAPI。

CallFunc 存在於 Storyboard 的 Panel 之中,所以一個工具可以只在合理的對話狀態(panel)中可用。


3. 設定逐步教學

Step 0 — 取得你的公開 URL + API key

cd /Users/kilikkuo/Projects/perxona-callfunc-demo
brew install cloudflared  # one-time
./scripts/run.sh        # server on :8090 + prints https://<random>.trycloudflare.com
grep API_KEY .env       # your X-API-Key value

健全性檢查公開 URL(health 不需要 key):

curl -s https://<random>.trycloudflare.com/health

Step 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

Step 2 — 建立 CallFunc #1:get_schedule

欄位
Type OUTBOUND_API
Name get_schedule
Method / Path GET · /get_schedule
Description List conference sessions for the 2026-06-24/25 Japan demo. Optionally filter by day ("Day 1"/"Day 2" or a date like 2026-06-24), track (e.g. Performance or Workshop), 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-24')." },
    "track":   { "type": "string", "description": "Track substring, e.g. 'Performance' or 'Workshop'." },
    "speaker": { "type": "string", "description": "Speaker name substring, e.g. 'Ohtani'." }
  }
}

輸出 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"}
        }
      }
    }
  }
}

Step 3 — 建立 CallFunc #2:get_speaker

欄位
Type OUTBOUND_API
Name get_speaker
Method / Path GET · /get_speaker
Description Look up one of the demo speakers 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_ohtani'." }
  }
}

輸出 schema → 見 API §2count + speakers[],包含 id, name, title, company, bio, topics[], sessions[]

Step 4 — 建立 CallFunc #3:book_meeting

欄位
Type OUTBOUND_API
Name book_meeting
Method / Path POST · /book_meeting
Description Book a 1:1 meeting with one of the demo speakers 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, e.g. 'Shohei Ohtani'." },
    "date":             { "type": "string", "description": "Meeting date, YYYY-MM-DD (for the demo, usually 2026-06-24 or 2026-06-25)." },
    "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

Step 5 — 測試它

在一個 panel 對話中,試試:

  • 「Day 1 有哪些 Performance 場次?」 → 應呼叫 get_schedule(day:"Day 1", track:"Performance")
  • 「Shohei Ohtani 是誰,他要講什麼?」get_speaker(name:"ohtani")
  • 「幫我在 6 月 24 日下午 2 點和 Tanjiro Kamado 約 30 分鐘,談事件處理。」book_meeting(...) → 確認提示(因為 require_confirmation:true)→ 事件建立。

4. 手藝:撰寫 LLM 能理解的描述與 schema

這部分決定了代理感覺起來是聰明還是笨。以下是一些經驗法則,附上前/後對照。

4.1 名稱 = 模型用來比對的動詞

使用清楚的 verb_nounget_schedulebook_meeting。讓這三個名稱保持鮮明區隔,好讓模型永遠不需要在兩個相似的工具之間猜測。

4.2 描述 = 何時使用它,而不只是它是什麼

描述在每一個回合模型決定是否呼叫時都會被讀取。說明它做什麼以及觸發時機(「use this when…」),加上任何限制。

❌ 含糊 ✅ 清楚
“排程端點。” “列出 2026-06-24/25 日本示範場次的議程。可依日期、軌道或講者篩選。Use this when 使用者詢問有哪些議程、或某件事排在什麼時候。”
“預約一場會議。” “在指定日期/時間預約與講者的一對一。Use only when 使用者明確表示要預約時。 會建立日曆事件並回傳確認。”

訣竅: - 提及它回傳什麼(「returns title, company, bio, topics」),讓模型知道這個工具能回答這個問題。 - 標明負面空間(「only when the user explicitly wants to book」),以防止過早或意外的呼叫 — 特別是對寫入操作而言。 - 不要傾倒實作細節(DB、framework)。模型不在乎這些,而且會浪費掉模型用來推理的 context。

4.3 輸入 schema = 參數合約

每個 property 描述都是模型用來填寫該欄位的提示。請對格式具體說明,因為模型會照抄你的用語。

  • 格式:"Meeting date, YYYY-MM-DD.""Local start time, 24h HH:MM." 會讓模型輸出 2026-06-2416:00,而不是 "next Tuesday""4 PM"
  • required列出沒有就無法運作的欄位(attendee_name, speaker, date, start_time)。模型會持續向使用者追問,直到取得它們為止。
  • 預設值與邊界:duration_minutesdefault: 30minimum: 5maximum: 240 — 所以模型可以省略它,而不良的值會在抵達你的伺服器之前就被拒絕。
  • Enum(本示範中沒有,但很強大):{"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",能讓代理誠實地措辭那段確認訊息。

4.5 黃金法則


5. 範本變數(選用,值得知道)

Perxona 可以把對話 context 注入到請求中,而不需要向 LLM 索取,做法是在 URL/headers/固定參數中使用 {$context.x}{$story.x} 佔位符。例如:把已登入使用者的 id 以 {$context.user_id} 放進 header,讓 book_meeting 就不需要模型去傳遞它。本示範不需要這個,但它正是你把密鑰和已知 context 排除在模型掌控之外的方法。


6. 安全性與 UX 旋鈕

旋鈕 它做什麼 用於
require_confirmation: true 在呼叫執行前向使用者顯示一個批准/拒絕提示 任何寫入book_meeting)、任何高成本或不可逆的操作
用量限制(call_limit / step_limit 限制一個工具每回合/每次對話能執行幾次 阻止失控迴圈與 API 濫用
Secret type(API_KEY / Bearer / None) 驗證 header 在呼叫時注入,並以加密形式儲存 讓你的端點保持私密;切勿把 key 放進描述裡
Panel 範圍限定 一個 CallFunc 只存在於你加入它的那些 panel 中 在使用者進入「booking」狀態之前隱藏訂位工具

對本示範而言:book_meetingrequire_confirmation: true;兩個 get_* 讀取 → false。


7. 疑難排解

症狀 可能原因 修正
工具回傳 401 X-API-Key 遺漏/錯誤 重新檢查 OutboundAPI 的密鑰值 + header name
422 datestart_time 格式不良,或缺少必填欄位 收緊欄位描述(格式)與 required 清單
502(book_meeting) 已設定 Google 但 insert 失敗 確認行事曆已分享給 service account
模型從不呼叫該工具 描述太含糊/與另一個工具重疊 用清楚的「use this when…」觸發語重寫描述;讓名稱鮮明區隔
模型用錯誤的參數呼叫它 欄位描述不清、缺少格式/enum 加上 description、格式、enumrequired、預設值
模型重複訂位或陷入迴圈 沒有確認/沒有限制 設定 require_confirmation: true 與用量限制
公開 URL 404/逾時 Tunnel 已重啟(新的 URL)或伺服器掛掉 重新執行 ./scripts/run.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 server 在你的 API 請求中內嵌宣告工具;由你的應用程式執行它們
是否需要程式碼 無需(只需 HTTP 端點,它可以是任何既有的 API) 需要 — 實作 MCP server 需要 — 在你的應用程式中實作工具執行器
傳輸 僅 HTTPS request/response stdio 或 HTTP/SSE;有狀態的 session 在你的應用程式內行程中;由你發出 provider 呼叫
工具發現 靜態(每個 panel 事先設定好) 動態tools/list);server 在執行期間公告工具 靜態(你每次請求都傳入工具清單)
工具以外的功能 僅工具 Resources、prompts、sampling、notifications 僅工具(provider 特定的額外功能)
串流/長時間執行 無(單次來回) 透過協定支援 取決於你的應用程式
驗證與密鑰 內建 — 在呼叫時注入加密過的密鑰 由你在 server 中實作 由你在應用程式中實作
Human-in-the-loop 內建require_confirmation 取決於 client(例如 Claude Desktop 的提示) 由你建構
用量限制/安全性 內建(call/step 限制、SQL-AST 安全性) 由你建構 由你建構
多租戶/代管 — 依 org 範圍限定,由平台託管 由你營運 server 由你營運應用程式
可攜性/重用 僅限 Perxona(專有) 開放標準 — 同一個 server 可跨 Claude Desktop、IDE、其他 MCP client 重用 provider/SDK 特定,但屬於廣為共享的結構
最適合在… 你在 Perxona 內部開發,並想要免寫程式的 HTTP 工具 + 代管的防護機制 你想要在許多 AI client 之間可重用、可發現的工具 server,並具備 resources/狀態 你正在建構自己的代理應用程式並想要完全掌控

Perxona CallFunc 做法的優點

  • 免寫程式/低程式碼。貼上一個 URL + JSON Schema,任何 HTTP API 就變成工具。對 vibe coder 非常理想,也適合在數分鐘內把既有後端轉成代理工具 — 無需撰寫或託管 adapter server(一個 MCP 整合意味著要建構並營運一個 server)。
  • 內建一切。加密密鑰、每工具用量限制、require_confirmation human-in-the-loop、context 範本化({$context}/{$story})、text-to-SQL 安全性、logging 以及多租戶範圍限定全都提供好了。換成 MCP 或原生 function calling,這些全都要你自己建構。
  • 緊密的產品整合。工具存在於 persona/storyboard 流程之中,並能以 panel 範圍限定(只在正確的對話狀態中可用),而且它們與記憶、防護機制和對話逐字稿走在同一條管線上。
  • 輸出 schema 是一級欄位。你宣告回傳的內容,這能幫助模型詮釋結果並讓回應保持誠實(例如 mock 與真實的 event_link)。

缺點/限制

  • 專有且綁定平台。你的工具存在於 Perxona 之中;它們無法移植到其他 host。MCP 最大的優勢是重用 — 撰寫一個工具 server 一次,就能從 Claude Desktop、IDE 以及任何相容 MCP 的 client 使用它。一個 CallFunc 無法在 Perxona 以外重用。
  • 僅 HTTP request/response。沒有 local/stdio 工具、沒有串流或長時間執行的工作、沒有有狀態的工具 session,也沒有 MCP 的 resources / prompts / sampling 原語。(在目前的後端中,DATA_VALIDATION 這個 CallFunc 型別尚未實作,也沒有工具結果快取、batching 或 circuit-breaker — 它就是一次乾淨的來回。)
  • 靜態設定。工具是事先在 UI 中設定好的;沒有像 MCP 的 tools/list 那樣的執行期發現。更改一個工具意味著要在後台中編輯它。
  • 你必須託管一個公開端點。它必須能從 Perxona 連到(因此才有 cloudflared tunnel),而且 uptime、延遲、逾時與 TLS 都由你負責。MCP server 可以在 client 旁邊本地執行;原生函式則在你自己的應用程式內執行。
  • Schema 方言耦合。輸入/輸出使用 JSON Schema(Draft 2020-12),透過 jambo 轉換成 Pydantic;非常奇特的 schema 功能可能無法乾淨地轉換。讓 schema 保持簡單且扁平(對模型而言這本來就是好的實踐)。

該如何看待它

  • 這些做法在精神上並不互斥 — 它們全都立基於 名稱 + 描述 + schema。你在這裡學到的東西可以直接轉移到 MCP 和原生 function calling。
  • 使用 Perxona CallFunc,當你要在 Perxona 內部出貨,並想要從「一個 HTTP API 存在」到「代理能使用它」的最快路徑,且把確認/限制/密鑰都替你處理好 — 正是這個示範。
  • 採用 MCP,當你想要一個可跨許多 AI client 重用的工具/資料 server、需要 local 或有狀態的存取,或想要超越單純函式呼叫的 resources/prompts。
  • 使用原生 function calling,當你正直接在一個 LLM API 上建構自己的代理應用程式並想要對執行有端到端的掌控。

9. 一頁式重點回顧

  1. 兩個物件:一個 OutboundAPI(URL + API key)+ 每個函式一個 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 在你自己的應用程式中追求完全掌控時勝出。