本手冊將示範如何把三個示範用的 HTTP 端點(get_schedule、
get_speaker、book_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 */ }
}
在每一個回合,模型都會僅依據那段文字問自己:
- 我到底該不該呼叫工具? → 由名稱 + 描述驅動。
- 要呼叫哪一個? → 由各個描述彼此之間有多麼明顯不同驅動。
- 要用什麼參數? → 由輸入 schema(欄位名稱、型別、
required、描述、enum、預設值)驅動。 - 我拿回了什麼/我該告訴使用者什麼? → 由輸出 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、輸出 schema、require_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 | (來自 .env 的 API_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 §2:count + 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 §3:booking_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_noun:get_schedule、book_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-24和16:00,而不是"next Tuesday"或"4 PM"。 required:列出沒有就無法運作的欄位(attendee_name, speaker, date, start_time)。模型會持續向使用者追問,直到取得它們為止。- 預設值與邊界:
duration_minutes有default: 30、minimum: 5、maximum: 240— 所以模型可以省略它,而不良的值會在抵達你的伺服器之前就被拒絕。 - Enum(本示範中沒有,但很強大):
{"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",能讓代理誠實地措辭那段確認訊息。
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_meeting → require_confirmation: true;兩個 get_* 讀取 → false。
7. 疑難排解
| 症狀 | 可能原因 | 修正 |
|---|---|---|
工具回傳 401 |
X-API-Key 遺漏/錯誤 |
重新檢查 OutboundAPI 的密鑰值 + header name |
422 |
date/start_time 格式不良,或缺少必填欄位 |
收緊欄位描述(格式)與 required 清單 |
502(book_meeting) |
已設定 Google 但 insert 失敗 | 確認行事曆已分享給 service account |
| 模型從不呼叫該工具 | 描述太含糊/與另一個工具重疊 | 用清楚的「use this when…」觸發語重寫描述;讓名稱鮮明區隔 |
| 模型用錯誤的參數呼叫它 | 欄位描述不清、缺少格式/enum | 加上 description、格式、enum、required、預設值 |
| 模型重複訂位或陷入迴圈 | 沒有確認/沒有限制 | 設定 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_confirmationhuman-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. 一頁式重點回顧
- 兩個物件:一個 OutboundAPI(URL + API key)+ 每個函式一個 CallFunc (名稱、描述、method、path、輸入 schema、輸出 schema、確認)。
- 模型只看得到名稱 + 描述 + 輸入 schema + 輸出 schema — 為一位聰明的陌生人撰寫它們。
- 描述 = 它做什麼 + 何時使用它(+ 它回傳什麼、+ 不該做什麼)。
- 輸入 schema = 欄位名稱、型別、
required、格式、預設值、enum。 - 輸出 schema = 回傳什麼,好讓模型正確讀取結果。
- 寫入操作要設
require_confirmation: true。讀取則不用。 - 它是 HTTP + 代管防護機制,免寫程式 — 在 Perxona 內部追求速度時很棒;MCP 在重用/可攜性與更豐富的原語上勝出;原生 function calling 在你自己的應用程式中追求完全掌控時勝出。