このマニュアルでは、3 つのデモ用 HTTP エンドポイント(get_schedule、
get_speaker、book_meeting)を、Perxona エージェントが呼び出せるツールに変える方法を説明します。そして同じくらい重要なこととして、LLM が「いつ」「どのように」使えばよいのかを実際に理解できるように、それらをどう書くか も解説します。
関連ドキュメント: API(正式なスキーマ)。
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 */ }
}
毎ターン、モデルはそのテキストだけを使って自問します:
- そもそもツールを呼ぶべきか? → 名前 + 説明 によって決まります。
- どれを呼ぶか? → 各説明どうしがどれだけ明確に区別されているか によって決まります。
- どんな引数で呼ぶか? → 入力スキーマ(フィールド名、型、
required、説明、列挙値、デフォルト値)によって決まります。 - 何が返ってきたか/ユーザーに何を伝えるか? → 出力スキーマ が助けになります(
event_linkが存在すること、countが数値であること、などをモデルが理解できます)。
ですから、制作者としてのあなたの仕事は、この 4 つを明確に書くことです。それがこの技術のすべてです。
呼び出し時に実際に起きること(内部の流れ)
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.
あなたが作るのはサーバーと、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. セットアップの手順
Step 0 — 公開 URL + API キーを取得する
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 の動作確認(ヘルスチェックにはキー不要):
curl -s https://<random>.trycloudflare.com/health
Step 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) |
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(読み取り専用) |
入力スキーマ(クエリパラメータ):
{
"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'." }
}
}
出力スキーマ(「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 |
入力スキーマ:
{
"type": "object",
"properties": {
"name": { "type": "string", "description": "Speaker name to search for (substring)." },
"id": { "type": "string", "description": "Exact speaker id, e.g. 'spk_ohtani'." }
}
}
出力スキーマ → 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(書き込み操作 — ユーザーに承認させる) |
入力スキーマ(ペイロード):
{
"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." }
}
}
出力スキーマ → API §3 を参照: booking_id, status, mode, summary, speaker, start,
end, timezone, event_link, message。
Step 5 — テストする
パネルでの会話の中で、次を試してみてください:
- 「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 が理解できる説明とスキーマを書く
ここが、エージェントが賢く感じられるか、間抜けに感じられるかを決める部分です。いくつかの経験則を、ビフォー/アフター付きで紹介します。
4.1 名前 = モデルがマッチングする「動詞」
明確な verb_noun を使ってください: get_schedule、book_meeting。3 つの名前を区別しやすく保ち、モデルが似たツールの間で迷わなくて済むようにします。
4.2 説明 = 「何であるか」だけでなく「いつ使うか」
説明は、モデルが呼び出すかどうかを判断する 毎ターン に読まれます。何をするのかに加えて、トリガー(「use this when…」)と、あらゆる制限を記述してください。
| ❌ 曖昧 | ✅ 明確 |
|---|---|
| “スケジュールのエンドポイント。” | “2026-06-24/25 の日本デモのセッション一覧を返す。日付・トラック・スピーカーで絞り込み可。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-24や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 であること(モックモードでは 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) | 呼び出し時に注入される認証ヘッダー。暗号化して保存される | エンドポイントを非公開に保つ。キーは決して説明文に書かない |
| パネルスコープ | CallFunc は、追加したパネルにのみ存在する | ユーザーが「予約」状態に入るまで予約ツールを隠す |
このデモでは: book_meeting → require_confirmation: true; 2 つの get_* 読み取りは → false。
7. トラブルシューティング
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
ツールから 401 が返る |
X-API-Key が欠落/誤り |
OutboundAPI のシークレット値とヘッダー名を再確認する |
422 |
date/start_time のフォーマット不正、または必須フィールドの欠落 |
フィールドの 説明(フォーマット)と required リストを引き締める |
502(book_meeting) |
Google は設定済みだが挿入に失敗 | カレンダーがサービスアカウントと共有されているか確認する |
| モデルがツールを 一度も呼ばない | 説明が曖昧すぎる/他のツールと重なっている | 明確な「use this when…」のトリガーを入れて説明を書き直す。名前を区別しやすくする |
| モデルが 誤った引数 で呼ぶ | フィールドの説明が不明瞭、フォーマット/列挙値が欠落 | description、フォーマット、enum、required、デフォルト値を追加する |
| モデルが 二重予約 したりループする | 確認も制限もない | require_confirmation: true と利用回数制限を設定する |
| 公開 URL が 404/タイムアウト | トンネルが再起動した(新しい URL)またはサーバーがダウン | ./scripts/run.sh を再実行し、OutboundAPI のベース URL を更新し、/health を再確認する |
8. Perxona CallFunc と MCP およびその他のエージェント型ツール設計の比較
最新のツールシステムはすべて、あなたが今使ったのと同じ中核的な考え方を共有しています。ツールとは、名前 + 説明 + JSON スキーマ であり、モデルはその契約の上で推論します。違いは 誰がツールをホストするか、どう転送するか、プラットフォームが何を肩代わりしてくれるか にあります。
早見比較
| 観点 | Perxona CallFunc / OutboundAPI | MCP (Model Context Protocol) | ネイティブ関数呼び出し(OpenAI / Anthropic tool use) |
|---|---|---|---|
| ツールの定義方法 | 管理 UI で: URL + JSON スキーマ + 説明(コード不要) | ツールを公開する MCP サーバー を書いて実行する | API リクエスト内にインラインでツールを宣言。実行はあなたのアプリが行う |
| 必要なコード | なし(HTTP エンドポイントだけ。既存の任意の API でよい) | あり — MCP サーバーを実装する | あり — アプリ内にツールランナーを実装する |
| 転送 | HTTPS のリクエスト/レスポンスのみ | stdio または HTTP/SSE; ステートフルなセッション | あなたのアプリ内のプロセス内処理; プロバイダ呼び出しは自分で行う |
| ツールの発見 | 静的(パネルごとに事前設定) | 動的(tools/list); サーバーが実行時にツールを公開する |
静的(リクエストごとにツール一覧を渡す) |
| ツール以外 | ツールのみ | リソース、プロンプト、サンプリング、通知 | ツールのみ(プロバイダ固有の追加機能あり) |
| ストリーミング/長時間処理 | なし(単一ラウンドトリップ) | プロトコルでサポート | あなたのアプリ次第 |
| 認証とシークレット | 組み込み — 暗号化されたシークレットを呼び出し時に注入 | サーバー内で自分で実装 | アプリ内で自分で実装 |
| ヒューマン・イン・ザ・ループ | 組み込み(require_confirmation) |
クライアント依存(例: Claude Desktop のプロンプト) | 自分で構築 |
| 利用回数制限/安全性 | 組み込み(call/step の制限、SQL-AST の安全性) | 自分で構築 | 自分で構築 |
| マルチテナント/マネージド | はい — 組織ごとにスコープ化され、プラットフォームがホスト | サーバーは自分で運用 | アプリは自分で運用 |
| 移植性/再利用 | Perxona 専用(プロプライエタリ) | オープン標準 — 1 つのサーバーを Claude Desktop、IDE、その他の MCP クライアントで再利用 | プロバイダ/SDK 固有だが、広く共有された形 |
| 最適な状況… | Perxona の中で 構築し、ノーコードの HTTP ツール + マネージドなガードレールが欲しいとき | 再利用可能で発見可能な ツールサーバーを多数の AI クライアントにわたって、リソース/状態とともに使いたいとき | 自分自身の エージェントアプリを構築し、完全な制御が欲しいとき |
Perxona CallFunc 方式の長所
- ノーコード/ローコード。 URL + JSON スキーマを貼り付けるだけで、任意の HTTP API がツールになります。バイブコーダーにとって、また既存のバックエンドを数分でエージェントツールに変えるのに最適です。書いて運用するアダプターサーバーは不要です(MCP 連携はサーバーを 構築し、かつ運用する ことを意味します)。
- 全部入り。 暗号化されたシークレット、ツールごとの利用回数制限、
require_confirmationによるヒューマン・イン・ザ・ループ、コンテキストのテンプレート化({$context}/{$story})、text-to-SQL の安全性、ロギング、マルチテナントのスコープ化 — これらすべてが提供されます。MCP やネイティブ関数呼び出しでは、これらをすべて自分で構築します。 - 製品との緊密な統合。 ツールはペルソナ/ストーリーボードのフローの内側に存在し、パネルスコープにできます(適切な会話状態でのみ利用可能)。さらに、メモリ、ガードレール、会話トランスクリプトと同じパイプラインに乗ります。
- 出力スキーマがファーストクラスのフィールド。 何が返ってくるかを宣言できるため、モデルが結果を解釈しやすくなり、レスポンスが正直に保たれます(例:
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 に変換します。非常に特殊なスキーマ機能はきれいに変換されない場合があります。スキーマはシンプルかつフラットに保ってください(いずれにせよモデルにとっても良い習慣です)。
考え方の整理
- これらの方式は 精神においては相互排他的ではありません — すべてが 名前 + 説明 + スキーマ に立脚しています。ここで学んだことは、MCP にもネイティブ関数呼び出しにもそのまま転用できます。
- Perxona CallFunc を使う のは、Perxona の中で出荷していて、「HTTP API が存在する」から「エージェントがそれを使える」までの最速の道を、確認/制限/シークレットを肩代わりしてもらいながら進みたいときです — まさにこのデモです。
- MCP に手を伸ばす のは、多数の AI クライアントにわたって再利用可能な ツール/データサーバーが欲しいとき、ローカルまたはステートフルなアクセスが必要なとき、あるいは単なる関数呼び出しを超えたリソース/プロンプトが欲しいときです。
- ネイティブ関数呼び出しを使う のは、LLM API の上に 自分自身の エージェントアプリケーションを直接構築していて、実行のエンドツーエンドの制御が欲しいときです。
9. 1 ページの総まとめ
- 2 つのオブジェクト: OutboundAPI を 1 つ(URL + API キー)+ 関数ごとに CallFunc を 1 つ(名前、説明、メソッド、パス、入力スキーマ、出力スキーマ、確認)。
- モデルが見るのは 名前 + 説明 + 入力スキーマ + 出力スキーマ だけです — 賢い見知らぬ人に向けて書きましょう。
- 説明 = 何をするか + いつ使うか(+ 何を返すか、+ 何をしてはいけないか)。
- 入力スキーマ = フィールド名、型、
required、フォーマット、デフォルト値、列挙値。 - 出力スキーマ = 何が返ってくるか。モデルが結果を正しく読めるようにするため。
- 書き込みには
require_confirmation: trueを付ける。 読み取りには付けない。 - HTTP + マネージドなガードレール、コード不要 — Perxona の中ではスピードに優れます; MCP は再利用性/移植性とより豊富なプリミティブで勝り; ネイティブ関数呼び出し は自分のアプリでの完全な制御で勝ります。