このマニュアルでは、3 つのデモ用 HTTP エンドポイント(get_schedule、
get_speaker、book_meeting)を、Perxona エージェントが呼び出せるツールに変える方法を示します。そして同じくらい重要なこととして、LLM がいつ・どのように使うべきかを実際に理解できるように、それらをどう書くか を解説します。
関連ドキュメント:API(正式なスキーマ)。
1. LLM はツールをどう「見る」か
エージェントが動作するとき、Perxona はモデルにツールの一覧を渡します。各ツールは本質的に次のような形をしています(OpenAI や Anthropic が内部で使っているのと同じ形です)。
{
"name": "get_schedule",
"description": "List conference sessions. Optionally filter by day, track, or speaker.",
"parameters": { /* the INPUT JSON Schema */ }
}
毎ターン、モデルはそのテキストだけを使って自問します。
- そもそもツールを呼び出すべきか? → name + description によって決まります。
- どれを? → 各 description が互いにどれだけ明確に区別されているか によって決まります。
- どんな引数で? → 入力スキーマ(フィールド名、型、
required、説明、enum、デフォルト)によって決まります。 - 何が返ってきたか/ユーザーに何を伝えるか? → 出力スキーマ が助けになります(これにより
event_linkが存在すること、countが数値であることなどがわかります)。
つまり、制作者としてのあなたの仕事は、その 4 つを明確に書くことです。それがこの技芸のすべてです。
呼び出し時に実際に起こること(内部の動き)
呼び出すと判断し、入力スキーマに沿った引数を生成"] B["Perxona が引数を検証し(JSON Schema → Pydantic)、
$context / $story テンプレートを URL・ヘッダーに展開し、
シークレットを注入して HTTP リクエストを送信"] C["あなたのサーバーが処理し JSON を返す"] D["Perxona が JSON を LLM に戻し
(出力スキーマが解釈を助ける)、
LLM がユーザー向けの返答を書く — あるいは別のツールを呼ぶ"] A --> B --> C --> D
あなたが作るのは、サーバーと 4 つのテキスト/形のシグナルだけです。検証、シークレットの注入、HTTP 呼び出し、利用回数の制限、そして(任意で)ユーザー確認は Perxona が行います。
2. Perxona における 2 つの構成要素
ツールのセットアップは、必ず 2 つのオブジェクト で構成されます。
| オブジェクト | その正体 | そこに入れるもの | LLM はそれを見るか? |
|---|---|---|---|
| OutboundAPI | 接続。サーバーの所在 + 認証方法 | ベース URL、シークレット(API キー) | いいえ — インフラのみ |
| CallFunc | エージェントが呼び出せる ツール | 名前、説明、メソッド、パス、入力スキーマ、出力スキーマ、require_confirmation |
はい — これが契約です |
- デモサーバー用に OutboundAPI を 1 つ 作成します(1 つのベース URL + 1 つの API キー)。
- CallFunc を 3 つ 作成します(関数ごとに 1 つ)。いずれもその OutboundAPI を指すようにします。
CallFunc は Storyboard の Panel の中 に存在するため、ツールはそれが意味をなす会話状態(パネル)でのみ利用可能にできます。
3. セットアップの手順
ステップ 0 — 公開 URL と API キーを取得する
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
公開 URL の動作確認をします(health にはキー不要)。
curl -s https://<random>.trycloudflare.com/health
ステップ 1 — OutboundAPI を作成する
管理画面(Backend Core / Outbound API)で、エントリを 1 つ作成します。
| フィールド | 値 |
|---|---|
| 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(読み取り専用) |
入力スキーマ(クエリパラメータ)。
{
"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." }
}
}
出力スキーマ(「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 |
入力スキーマ。
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Speaker name to search for (substring)." },
"id": { "type": "string", "description": "Exact speaker id, e.g. 'spk_chen'." }
}
}
出力スキーマ → 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(書き込みなので、ユーザーに承認させる) |
入力スキーマ(ペイロード)。
{
"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." }
}
}
出力スキーマ → API §3 を参照。booking_id, status, mode, summary, speaker, start,
end, timezone, event_link, message。
ステップ 5 — テストする
パネルの会話の中で、次を試してみてください。
- 「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のため)→ イベント作成。
4. 技芸: LLM が理解できる説明とスキーマを書く
ここが、エージェントが賢く感じられるか、それとも間抜けに感じられるかを左右する部分です。いくつかの経験則を、ビフォー/アフターつきで示します。
4.1 Name = モデルが照合する動詞
明確な verb_noun を使いましょう。get_schedule、book_meeting。3 つの名前を区別できるように保ち、モデルが似た 2 つのツールの間で迷わなくて済むようにします。
4.2 Description = 何であるか だけでなく いつ使うか
説明は、モデルが呼び出すかどうかを判断するたびに、毎ターン 読まれます。何をするかと、その引き金(「次のときに使う…」)、加えて制約があればそれを書きましょう。
| ❌ 曖昧 | ✅ 明確 |
|---|---|
| 「スケジュールのエンドポイント。」 | 「カンファレンスのセッション一覧を返す。日付・トラック・スピーカーで絞り込み可。Use this when ユーザーがどんな講演があるか、または何がいつ予定されているかを尋ねたとき。」 |
| 「会議を予約する。」 | 「指定した日時でスピーカーとの 1:1 を予約する。Use only when ユーザーが明確に予約を希望したときのみ。 カレンダー予定を作成し、確認を返す。」 |
コツ。 - 何を返すか(「returns title, company, bio, topics」)を書きましょう。そうすればモデルは、そのツールで質問に答えられることを把握できます。 - 否定の領域(「only when the user explicitly wants to book」)を明示し、早すぎる呼び出しや偶発的な呼び出しを止めましょう — 特に書き込みでは重要です。 - 実装の詳細(DB、フレームワーク)を書き連ねないこと。モデルはそれを気にしませんし、モデルが推論に使うコンテキストを無駄に消費します。
4.3 入力スキーマ = 引数の契約
各プロパティの説明は、モデルがそのフィールドを埋めるために使うヒントです。モデルはあなたの言い回しを真似るので、フォーマット については具体的に書きましょう。
- フォーマット。
"Meeting date, YYYY-MM-DD."や"Local start time, 24h HH:MM."と書けば、モデルは"next Tuesday"や"4 PM"ではなく2026-06-11や16:00を出力するようになります。 required。 呼び出しに不可欠なフィールドを列挙します(attendee_name, speaker, date, start_time)。モデルはそれらが揃うまでユーザーに尋ね続けます。- デフォルトと範囲。
duration_minutesにはdefault: 30、minimum: 5、maximum: 240があります — だからモデルはそれを省略でき、不正な値はサーバーに届く前に拒否されます。 - Enum(このデモにはありませんが、強力です)。
{"type":"string","enum":["confirmed","tentative"]}はモデルを妥当な選択肢に制約します。 - フラットで小さく保つこと。 深くネストした入力は、モデルが確実に埋めるのが難しくなります。
4.4 出力スキーマ = モデルが結果をどう読むか(そして、なぜ追加を求められたか)
呼び出しのあと、モデルはあなたの JSON レスポンスを読みます。出力スキーマは、どんなフィールドが存在し、それらが何を意味するか をモデルに伝えるので、モデルは次のことができます。
- 正しい値を返答に引き込む(例:
event_linkをクリック可能なリンクとして提示する、またはmessageをそのまま読み返す)。 - 型を理解する(
countが数値で、topicsがリストであることを把握する)。 - 存在しないフィールドをでっち上げない。
各出力フィールドも、入力を説明するのと同じ要領で説明しましょう。book_meeting では、
event_link が string | null であること(mock モードでは null)、mode が "google_calendar" か
"mock" かを示すことを書いておくと、エージェントは確認メッセージを正直に表現できます。
4.5 黄金律
5. テンプレート変数(任意、知っておくとよい)
Perxona は、URL/ヘッダー/固定パラメータの中で {$context.x} や {$story.x} のプレースホルダーを使い、会話コンテキストを LLM に尋ねることなく リクエストに注入できます。例。ログイン中ユーザーの id を、ヘッダーに {$context.user_id} として入れておけば、book_meeting はそれをモデルに渡してもらう必要がなくなります。このデモでは必須ではありませんが、これがシークレットや既知のコンテキストをモデルの手の届かないところに保つやり方です。
6. 安全性と UX のつまみ
| つまみ | 何をするか | 用途 |
|---|---|---|
require_confirmation: true |
呼び出しが実行される前に、承認/拒否のプロンプトをユーザーに表示する | あらゆる書き込み(book_meeting)、コストの高いものや取り消せないもの |
利用回数の制限(call_limit / step_limit) |
ツールがターン/会話ごとに実行できる回数の上限を設ける | 暴走ループや API の濫用を止める |
| Secret type(API_KEY / Bearer / None) | 呼び出し時に注入される認証ヘッダー。暗号化して保存される | エンドポイントを非公開に保つ。キーを description に決して入れないこと |
| パネルスコープ | CallFunc は、追加したパネルの中にのみ存在する | ユーザーが「予約」状態になるまで予約ツールを隠す |
このデモでは。book_meeting → require_confirmation: true。2 つの get_* の読み取り → false。
7. トラブルシューティング
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
ツールから 401 |
X-API-Key の欠落/誤り |
OutboundAPI のシークレット値とヘッダー名を再確認する |
422 |
date/start_time のフォーマットが不正、または必須フィールドの欠落 |
フィールドの description(フォーマット)と required リストを引き締める |
502(book_meeting) |
Google は設定済みだが、挿入に失敗した | カレンダーがサービスアカウントと共有されているか確認する |
| モデルがツールを 決して呼ばない | 説明が曖昧すぎる/別のツールと重複している | 明確な「use this when…」の引き金を入れて説明を書き直す。名前を区別する |
| モデルが 誤った引数 で呼び出す | フィールドの説明が不明確、フォーマット/enum の欠如 | description、フォーマット、enum、required、デフォルトを追加する |
| モデルが 二重予約 したりループしたりする | 確認なし/制限なし | require_confirmation: true と利用回数の制限を設定する |
| 公開 URL が 404/タイムアウト | トンネルが再起動した(新しい URL)、またはサーバーが落ちている | ./scripts/tunnel.sh を再実行し、OutboundAPI のベース URL を更新し、/health を再確認する |
8. Perxona CallFunc 対 MCP およびその他のエージェント型ツール設計
現代のツールシステムはすべて、あなたが今使ったのと同じ中核的な考え方を共有しています。ツールとは name + description + 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。ステートフルなセッション | あなたのアプリ内のプロセス内。プロバイダ呼び出しはあなたが行う |
| ツールの発見 | 静的(パネルごとに事前設定) | 動的(tools/list)。サーバーが実行時にツールを通知する |
静的(リクエストごとにツール一覧を渡す) |
| ツール以外 | ツールのみ | リソース、プロンプト、サンプリング、通知 | ツールのみ(プロバイダ固有の追加機能あり) |
| ストリーミング/長時間実行 | 不可(単一のラウンドトリップ) | プロトコルを通じてサポート | あなたのアプリ次第 |
| 認証とシークレット | 組み込み — 呼び出し時に暗号化されたシークレットを注入 | サーバー内で自分で実装する | アプリ内で自分で実装する |
| Human-in-the-loop | 組み込み(require_confirmation) |
クライアント依存(例: Claude Desktop のプロンプト) | 自分で作る |
| 利用回数の制限/安全性 | 組み込み(call/step の制限、SQL-AST の安全性) | 自分で作る | 自分で作る |
| マルチテナント/マネージド | はい — 組織ごとにスコープされ、プラットフォームがホストする | サーバーは自分で運用する | アプリは自分で運用する |
| 移植性/再利用 | Perxona 専用(プロプライエタリ) | オープン標準 — 1 つのサーバーを Claude Desktop、IDE、その他の MCP クライアントで再利用できる | プロバイダ/SDK 固有だが、広く共有された形 |
| 最適な場面… | Perxona の中で 構築し、ノーコードの HTTP ツールとマネージドのガードレールが欲しいとき | 多くの AI クライアントにまたがって 再利用でき、発見可能な ツールサーバーが欲しく、リソース/状態も必要なとき | 自分自身の エージェントアプリを構築していて、完全な制御が欲しいとき |
Perxona CallFunc アプローチの長所
- ノーコード/ローコード。 URL + JSON Schema を貼り付けるだけで、任意の HTTP API がツールになります。バイブコーダーにとっても、既存のバックエンドを数分でエージェントのツールに変えるのにも最適です — 書いたりホストしたりするアダプターサーバーは不要です(MCP 統合はサーバーを構築 かつ運用 することを意味します)。
- 全部入り。 暗号化されたシークレット、ツールごとの利用回数制限、
require_confirmationによる human-in-the-loop、コンテキストのテンプレート化({$context}/{$story})、text-to-SQL の安全性、ロギング、マルチテナントのスコープがすべて提供されます。MCP やネイティブの function calling では、それらすべてを自分で作ることになります。 - 緊密な製品統合。 ツールはペルソナ/ストーリーボードのフローの中に存在し、パネルスコープ化できます(適切な会話状態でのみ利用可能にできます)。そしてメモリ、ガードレール、会話トランスクリプトと同じパイプラインに乗ります。
- 第一級フィールドとしての出力スキーマ。 何が返ってくるかを宣言でき、それがモデルの結果解釈を助け、返答を正直に保ちます(例:
mockか本物のevent_linkか)。
短所/制約
- プロプライエタリでプラットフォームに縛られる。 あなたのツールは Perxona の中に存在し、他のホストには移植できません。MCP の最大の利点は再利用 です — ツールサーバーを一度書けば、Claude Desktop、IDE、あらゆる MCP 互換クライアントから使えます。CallFunc は Perxona の外では再利用できません。
- HTTP のリクエスト/レスポンスのみ。 ローカル/stdio のツールはなく、ストリーミングや長時間実行ジョブもなく、ステートフルなツールセッションもなく、MCP の リソース/プロンプト/サンプリング といったプリミティブもありません。(現行のバックエンドでは、
DATA_VALIDATIONの CallFunc 型は実装されておらず、ツール結果のキャッシュ、バッチ処理、サーキットブレーカーもありません — クリーンなラウンドトリップが 1 回あるだけです。) - 静的な設定。 ツールは UI 上で事前にセットアップされます。MCP の
tools/listのような実行時の発見はありません。ツールの変更は、管理画面でそれを編集することを意味します。 - 公開エンドポイントを自分でホストしなければならない。 それは Perxona から到達可能でなければならず(だからこその cloudflared トンネルです)、その稼働時間、レイテンシ、タイムアウト、TLS はあなたの責任です。 MCP サーバーはクライアントのすぐ隣でローカルに動かせますし、ネイティブの関数はあなた自身のアプリ内で動きます。
- スキーマ方言の結合。 入出力は JSON Schema(Draft 2020-12)を使い、
jambo経由で Pydantic に変換されます。非常に風変わりなスキーマ機能はきれいに変換されないことがあります。スキーマはシンプルでフラットに保ちましょう(いずれにせよ、それはモデルにとっても良い習慣です)。
どう考えるべきか
- これらのアプローチは 精神において相互排他的ではありません — いずれも name + description + schema の上に成り立っています。ここで学んだことは、MCP やネイティブの function calling にそのまま応用できます。
- Perxona CallFunc を使う のは、Perxona の中で出荷していて、「HTTP API が存在する」から「エージェントがそれを使える」までを最速の経路でつなぎ、確認/制限/シークレットを代わりに扱ってほしいとき — まさにこのデモのようなケースです。
- MCP に手を伸ばす のは、多くの AI クライアントにまたがって再利用できる ツール/データサーバーが欲しい、ローカルまたはステートフルなアクセスが必要、あるいは単なる関数呼び出しを超えてリソース/プロンプトが欲しいときです。
- ネイティブの function calling を使う のは、LLM API の上に直接 自分自身の エージェントアプリケーションを構築していて、実行をエンドツーエンドで制御したいときです。
9. 1 ページの総まとめ
- 2 つのオブジェクト。 OutboundAPI を 1 つ(URL + API キー)+ 関数ごとに CallFunc を 1 つ (name、description、メソッド、パス、入力スキーマ、出力スキーマ、確認)。
- モデルが見るのは name + description + 入力スキーマ + 出力スキーマ だけ — 賢い見知らぬ人に向けて書きましょう。
- Description = 何をするか + いつ使うか(+ 何を返すか、+ してはいけないこと)。
- 入力スキーマ = フィールド名、型、
required、フォーマット、デフォルト、enum。 - 出力スキーマ = 何が返ってくるか。これによりモデルが結果を正しく読みます。
- 書き込みには
require_confirmation: trueをつける。 読み取りにはつけません。 - これは HTTP + マネージドのガードレールで、コード不要 — Perxona の中でのスピードに最適です。MCP は再利用/移植性とより豊かなプリミティブで勝り、ネイティブの function calling は自分自身のアプリ内での完全な制御で勝ります。