第10章 Function calling で外部の処理を呼び出す

本章は初稿です。

10.1 この章で学ぶこと

これまでの Agent は、渡された言葉から応答していました。ここに、モデルの外で情報を取得する関数を追加します。最初の関数は、架空の社内手順を検索する search_knowledge です。

到達点は、ツールの定義、モデルからの要求、実際の関数実行、結果の返却を区別することです。実習では外部サービスへの接続を必須にせず、ローカル関数で往復の仕組みを学びます。ここでいう「外部」は、まずモデルの外側という意味です。

10.2 要求、実行、結果の往復を追う

Function calling は、モデルが関数名と引数を含む要求を返し、アプリケーションが実行して結果を返す仕組みです。関数の名前を定義しただけでは、検索処理は実行されません。[^function-calling]

社員の質問から検索関数の要求が生成され、アプリケーションが関数と入力を検証する。利用者の閲覧範囲を適用した検索結果をエージェントへ返し、その根拠から回答する順序

図10-1 モデルの要求と、検索の実行を分ける。 時間は上から下へ進み、実線は入力・実行依頼、破線は出力です。図は検索 API に接続する概念例ですが、本章の実習では検索部分をローカル関数へ置き換えます。サービス側で実行されるツールもあり、すべての Foundry ツールがこの方式で動くわけではありません。

図中の「回答案と引用を確認」は、第12章で行う評価に相当します。第13章の学習コードに、回答内容の自動判定まで実装したという意味ではありません。

10.3 ツールの契約を決める

モデルへ渡す定義と、実行するプログラムの契約を対応付けます。今回は検索語だけをモデルの引数とし、利用者の権限は含めません。

項目学習用の設計意味
名前search_knowledge呼び出す関数を識別する
説明閲覧可能な架空の IT 手順を検索するどんな要求で使うかを伝える
引数必須の文字列 queryアプリが検証する検索語
結果文書 ID、タイトル、抜粋、参照先回答に使う根拠
実行側の情報認証済み利用者のグループアプリが保持し、モデルに決めさせない
失敗不正入力や検索障害として明示する文書がないことと混同しない

次は SDK の FunctionTool を使う定義の断片です。第13章で Agent の定義と結び付けます。独自の検索契約であり、Foundry に標準で備わる同名の関数ではありません。

python
from azure.ai.projects.models import FunctionTool

search_tool = FunctionTool(
    name="search_knowledge",
    description=(
        "学習用 IT 手順を検索します。query は VPN Windows のように、"
        "少数のキーワードを空白で区切って指定してください。"
    ),
    parameters={
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "検索キーワード"}
        },
        "required": ["query"],
        "additionalProperties": False,
    },
    strict=True,
)

この例は通常の Prompt エージェントの公式例に沿います。別の harness や API では strict などの対応条件が異なるため、そのまま流用しません。SDK の設定があっても、実行側の入力検証は残します。[^function-calling]

10.4 モデルを呼ばずに検索関数を確かめる

次を、学習用作業ディレクトリの knowledge.py として保存する想定です。Python 標準ライブラリだけを使います。文書と参照先は架空で、.invalid の URL は実在する手順書ではありません。

python
from dataclasses import dataclass


@dataclass(frozen=True)
class Document:
    document_id: str
    title: str
    passage: str
    source: str
    allowed_groups: frozenset[str]


DOCUMENTS = (
    Document(
        "vpn-windows",
        "Windows の VPN 確認手順",
        "学習用手順: Windows では接続先とエラー表示を確認します。",
        "https://kb.example.invalid/vpn-windows",
        frozenset({"employees"}),
    ),
    Document(
        "vpn-macos",
        "macOS の VPN 確認手順",
        "学習用手順: macOS では接続状態とエラー表示を確認します。",
        "https://kb.example.invalid/vpn-macos",
        frozenset({"employees"}),
    ),
    Document(
        "vpn-admin",
        "VPN 管理者向け手順",
        "学習用の管理者専用資料です。一般社員向けには返しません。",
        "https://kb.example.invalid/vpn-admin",
        frozenset({"admins"}),
    ),
)


def search_knowledge(
    query: str, *, user_groups: frozenset[str]
) -> list[dict[str, str]]:
    if not isinstance(query, str) or not query.strip() or len(query) > 200:
        raise ValueError("query must be a non-empty string of at most 200 characters")
    if not user_groups:
        raise PermissionError("Trusted user groups are required")

    terms = query.casefold().split()
    hits = []
    for document in DOCUMENTS:
        if not document.allowed_groups.intersection(user_groups):
            continue
        searchable = f"{document.title} {document.passage}".casefold()
        if all(term in searchable for term in terms):
            hits.append(
                {
                    "document_id": document.document_id,
                    "title": document.title,
                    "passage": document.passage,
                    "source": document.source,
                }
            )
    return hits

検索は、空白区切りの全キーワードが含まれるかを見るだけです。日本語の形態素解析やベクトル検索ではありません。200文字という上限も教材独自の例であり、Foundry の制限値ではありません。

user_groups は、学習中は自分で用意するテスト用の認証済み文脈です。本番では、利用者が入力したグループ名を信頼する仕組みにしてはいけません。例のグループとの交差判定は、実際の組織の認可要件をすべて満たす実装ではありません。

10.5 呼び出しと結果を対応付ける

SDK が返す関数要求には、関数名、JSON 文字列としての引数、call_id などがあります。結果は同じ call_id の function_call_output として返します。[^function-calling] 関数名と結果だけを会話文へ追加すると、要求との対応が失われます。

実行側の処理を疑似コードで表すと、次のようになります。

text
応答を受け取る
関数要求がなければ、最終的な出力を確認する
関数要求があれば、許可された関数名かを確認する
JSON を読み、必須項目・型・余計な項目を検証する
アプリが保持する利用者の文脈で関数を実行する
要求ごとの call_id と結果を、同じ会話へ返す
次の応答を受け取り、上限付きで繰り返す

想定外の関数を任意のコードとして実行してはいけません。関数名からの eval や、引数で任意の URL・認証主体を指定できる仕組みは必要ありません。

10.6 小さな実習と確認課題

まず関数だけを直接呼び、期待結果を確認します。この断片は knowledge.py と同じ作業ディレクトリから試します。

python
from knowledge import search_knowledge

groups = frozenset({"employees"})
hits = search_knowledge("VPN Windows", user_groups=groups)
assert [hit["document_id"] for hit in hits] == ["vpn-windows"]
assert search_knowledge("VPN 管理者", user_groups=groups) == []
assert search_knowledge("存在しない製品", user_groups=groups) == []

続いて、空の検索語、権限文脈なし、想定外の引数を与えた場合を考えます。正しい要求で該当がないことと、検索を実行できないことは別です。検索障害を空の結果へ置き換えないようにしてください。

最後に「モデルが検索を要求したが、アプリが関数を実行しなかった」場合を説明します。その場合、社内文書を取得した証拠はありません。第13章で SDK の往復を組み立てる前に、この区別が説明できれば本章の目標を満たします。

10.7 まとめ

ツールは、定義と実装、実行側の検証、呼び出しに対応した結果の返却で成り立ちます。次章では、検索結果を単に表示するのではなく、回答の根拠として使う RAG を学びます。

10.8 参考情報

[^function-calling]: Use function calling with Microsoft Foundry agents — Microsoft


前章:第9章 会話の文脈と状態を扱う · 教材の目次へ戻る · 次章:第11章 社内文書を検索して根拠付きで回答する