Skip to content
This page has been auto-translated and may contain errors.View in English

ツール使用

docs.scrimba.com

ツール使用(関数呼び出しとも呼ばれます)は、言語モデルが自分のテキストボックスを超えて機能する方法です。実行可能な関数のリストを提供すると、モデルはそれらを使用する時期を判断します。モデル自体は何も実行しません。呼び出しをリクエストするだけで、コードが実際の作業を行います。

モデルだけでは箱の中に閉じ込められています。今日の天気を確認したり、ユーザーをデータベースで検索したり、メールを送信したりすることはできません。学習時に固定された内容から、テキストを予測するだけです。これはLLMの仕組みの同じ図です。ツール使用は、箱の外に到達させる方法であり、エージェントが構築される基礎です。

覚えておくべき言葉は「要求」です。モデルはどの関数を使用したいのか、どのような引数でかを教えます。コードがそれを実行し、結果を返します。あなたは常にコントロール下にあります。

モデルはテキストのみを生成するため、単独では実時間データを取得したり、データベースにアクセスしたり、アクションをトリガーすることはできません。ツール使用がこのギャップを埋めます。関数のセットを記述し、モデルは構造化されたリクエストを発行し、コードはそれを実行し、結果をフィードバックします。モデルが調整し、コードが実行します。

この章は、構築するすべてのエージェントの基礎となるメカニズムです。ループ、ツールスキーマ、エラーパスをここで正しく理解すれば、エージェントはこの同じパターンを繰り返すだけになります。

ツール使用は、確率的テキスト生成器が決定論的コードと出会う接点であり、本番環境での痛みのほとんどはその接点に存在します。モデルは構造化された呼び出しを予測し、コードはそれを実行し、結果はより多くのコンテキストとしてフィードバックされます。これを難しくするすべてのこと、信頼されていない引数の検証、べき等性、余分なラウンドトリップ、幻想的な呼び出し、可観測性は、その1つのハンドオフから生じます。

ここには新しい数学はありません。これは確率的システムが実システムで実際の副作用をトリガーさせることのエンジニアリング規律であり、エージェント章全体が依存する基板です。

ループ

ツール使用は**行き来**であり、単一の呼び出しではありません。

  1. ユーザーのメッセージとモデルが使用する可能性のあるツールのリストを送信します。
  2. モデルは2つの方法のいずれかで応答します。通常の回答、またはツール呼び出しのリクエスト(必要な引数付き)。
  3. ツール呼び出しを要求した場合、コードはその関数を実行し、結果をモデルに送信します。
  4. モデルはその結果を使用して最終的な回答を書きます。

これがパターン全体です。モデルは要求し、コードは実行します。ステップ2と3は、モデルが複数のツールを必要とする場合に繰り返される可能性があり、これが数章先のエージェントの種です。

Junoループ ツール使用はループです。メッセージとツールのリストを送信し、モデルは回答するか引数を指定してツール呼び出しをリクエストし、コードは関数を実行し、結果をモデルが使用できるように返します。モデルは決してコードを実行しません。呼び出しをリクエストするだけなので、あなたはコントロール下にあります。中間ステップを繰り返すと、エージェントの始まりになります。

ループは4つのビートを持ちます。メッセージとツールリストを送信し、モデルが応答するか呼び出しをリクエストし、コードが関数を実行し、結果が返され、モデルが続行します。初心者が見落としやすい部分は、これはほぼ1ラウンドではないということです。モデルはツールをリクエストし、結果を読み、見たものに基づいて別のツールをリクエストし、以下同様に続くことができます。

つまり、実際の形は**マルチターンループ**です。モデルが呼び出され、ツール呼び出しのない応答が返されるまで、モデルを呼び出し、要求されたツールを実行し続けます。このツール呼び出しのない最終応答がユーザーの答えです。

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # モデルが何を言ったかを記録

    if not msg.tool_calls:            # ツールが望まれていない:これが答え
        return msg.content

    for call in msg.tool_calls:       # リクエストされたすべての呼び出しを実行
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

ループ条件は安全性の問題です。ツールを求め続けるモデルは自動的に終了しないため、本番環境でイテレーションをキャップし、キャップに達した場合は明確なエラーで停止します。ループはモデルがツール呼び出しなしで応答を返すときに終了します。

Junoループ ループはツールを送信し、モデルが応答するか呼び出しをリクエストし、実行して、結果をフィードバックして、繰り返します。コツはここです。モデルがツール呼び出しなしで応答を返すまで続くため、if文ではなく実ループとして記述してください。そしてイテレーションをキャップしてください。ツールを求め続けるモデルは喜んであなたのお金で永遠にスピンするからです。

ループは機械的には小さいです。モデルをツール付きで呼び出し、要求されたものを実行し、結果を追加し、ツール呼び出しなしで応答が返されるまで繰り返します。興味深い部分は、すべてのイテレーションが完全なモデル**ラウンドトリップ**であり、ラウンドトリップはレイテンシーとトークン予算が消費される場所です。

各ターンはメッセージ履歴全体を再送信するため、5つのツール呼び出しが必要なタスクは、プロンプトのコストを5回払い、入力はターンごとに増加します。レバー:

  • ラウンドトリップ数を最小化するには、1回の呼び出しでより多くの作業を行うツールを提供します。
  • ツール結果をコンパクトに保つので、再送信される履歴が膨張しません。
  • 独立したツール呼び出しを同時に実行する(順序ではなく)ため、ウォールクロック遅延はそれらの合計ではなく、最も遅い呼び出しを追跡します。
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # モデルは1ターンで複数の呼び出しをリクエストできます;それらを実行してから続行
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # 検証、ログ、エラーラッピング

raise RuntimeError("ツールループがMAX_TURNS内で収束しませんでした")

ハードキャップはオプションではありません。これがなければ、混乱したモデルは無限にループすることができ、失敗モードはきれいなエラーではなく、遅く、費用のかかるリクエストです。それをバウンドし、バウンドに達したときは大きく失敗し、トレースをログします。モデルが何を追求していたかを確認できるようにです。

Junoループ ループは書くのは簡単で、実行するのは費用がかかります。毎ターンが再送信されたメッセージ履歴全体上の完全なモデル呼び出しなため、5つのツール呼び出しはプロンプトコストを5回払うことを意味します。結果をコンパクトに保ち、独立した呼び出しを同時に実行し、多くのおしゃべりなツールより少ないより太いツールを優先します。そしてターンをハードキャップしてください。モデルがループを無限に続けることを決定した日には、ログにきれいなエラーが欲しいはずですし、5桁の請求書は欲しくないはずです。

実際に何が起こっているのか

ツール使用はモデルが新しい力を獲得したように感じることができ、それはそうではありません。舞台裏では、モデルは常にやっていることを1つやっています。テキストを予測すること。それが生きている箱に関して何も変わっていません。

リクエストにツール定義を含めると、モデルが予測する コンテキストにそれらを追加しています。モデルは、これらのようなツールが与えられた場合、正しい継続は時々「call get_weather with city トウキョウ」を意味する特別にフォーマットされたメッセージであるという例で訓練されました、通常の文ではなく。したがって、質問がツールを役立つように見える場合、最も可能性の高い次の出力はその構造化された**ツール呼び出しメッセージ**であり、APIはそれをtool_callsとして表示します。

モデルが何かに到達して実行していません。ツール呼び出しが正しい動きであることを予測し、コードが読む方法でそれをリクエストしました。

その後、モデルではなく、あなたが関数を実行します。本当の結果を取得し、メッセージに追加のコンテキストとして戻します。全機能は2つの普通の予測で、その間にコードが実際の作業を行います。

あなたはモデルのテキストボックスと現実世界の橋です。この図を保持し、章の残り、エージェントを含む、神秘的に感じるのをやめます。AIが取るすべてのアクションは、コードが実行することを選択するリクエストの予測です。

Juno実際に何が起こっているのか ツール使用はまだ純粋な予測です。モデルはそのコンテキストにツール定義が与えられた場合、出力の確率は時々「この関数をこれらの引数で呼び出す」という構造化されたメッセージであるように訓練されました。APIはそれをtool_callsとして渡します。モデルは決して何も実行しません。呼び出しを予測します。コードは関数を実行し、結果を次の予測のための追加コンテキストとしてフィードバックするため、あなたはモデルと現実世界の橋です。

ツール使用は、LLMの仕組みの同じ次のトークン予測ですが、構造化されたターゲットを対象としています。ツール定義がコンテキストに入り、モデルは、ツールが適切な場合、最も高い確率の継続は散文ではなくフォーマットされた呼び出しになるように訓練されます。APIはその継続を解析し、tool_callsとして渡します。

有用な結果は、ツール呼び出しが**制約付き生成**であることです。モデルが提供したスキーマに一致する出力。これは構造化出力とまったく同じメカニズムで、定義した形状でJSONを返すようモデルに要求します。

それらの間の線は意図です。構造化出力は「この形状でデータをください、停止してください」です。ツール使用は「この形状でデータをください。コードが何かを実行し、結果に対応できるように」です。

モデルから解析されたオブジェクトのみが必要な場合、構造化出力に到達します。モデルが行動し、コードが返すものに対応する必要がある場合、ツール使用に到達します。

呼び出しは予測されるため、実行されるのではなく、コードだけがシステムに実際に触れます。モデルが提案し、コードが処分し、その境界はすべての実際の事柄が起こる前に起きるすべてのチェックを置く場所です。

Juno実際に何が起こっているのか ツール呼び出しは同じ次のトークン予測で、散文ではなくスキーマを対象としています。APIはそれをtool_callsとして表示します。これは構造化出力と同じメカニズムである制約付き生成です。違いは意図です。構造化出力はデータを提供し、停止します。ツール使用はデータを提供するため、何かを実行して対応できます。どちらの場合でも、モデルは提案するだけで、コードは本当に何かに触れるだけです。

ツール呼び出しは予測されたテキストであり、実行ではなく、それを直線的に保つことはあなたを保護するものです。モデルはコンテキストのツールが与えられた場合、最も確率の高い継続を発行し、APIはそれを構造化された呼び出しにデコードし、コードはそれを実行するかどうかを決定します。機械的には、それは構造化出力と副作用が添付されたものです。

このフレーミングは信頼境界を正確に設定します。ツール呼び出しの引数はモデル出力です。つまり、それらは**信頼されていない入力**です。確率的システムが生成したテキスト。ユーザーがフォームに入力したものと同じ立場です。そのように扱ってください。

モデルはループの途中で読むコンテンツに基づいてステアリングされることができるため、ウェブページまたはドキュメントからのテキストを返すツールがある場合、そのテキストに埋め込まれたプロンプトインジェクションは、攻撃者が選んだ引数で異なるツールをリクエストするようにモデルに話しかけることができます。防御は構造的であり、システムプロンプトの丁寧な指示ではありません。厳格なスキーマに対して引数を検証し、各ツールを最も狭い許可にスコープし、ツールの権限がその入力に影響を与えることができる最も信頼されていない当事者に付与するものを超えるようにしないでください。

メンタルモデルはリクエストルーターであり、同僚ではありません。モデルは提案された引数を持つ関数にルーティングします。コードは認証、許可、検証を行い、その後のみ実行します。「それだけ」より下流はこの章が価値を得る場所です。

Juno実際に何が起こっているのか ツール呼び出しはAPIがデコードする予測されたテキスト。副作用を持つ構造化出力。モデルが決して実行していません。引数は信頼されていない入力、フォーム欄に見知らぬ人が入力したのと同じ立場です。ツールが返すテキストに隠されたプロンプトインジェクションは次の呼び出しをリダイレクトできます。検証、スコープ、境界で認可してください。モデルはリクエストをルーティングし、コードだけがレバーに手をつけています。

ツールの定義

各ツールをモデルに記述します。その名前、その機能、およびそれが取る引数。説明はあなたのためのドキュメントではなく、ツール使用のためのモデルへの指示であり、プロンプトのように書いてください。

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "指定された都市の現在の天気を取得します。ユーザーが天気について尋ねる場合に使用してください。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "都市名(例:'東京')"},
                },
                "required": ["city"],
            },
        },
    },
]

parametersブロックは**スキーマ**であり、構造化出力と同じ考えです。モデルが生成する必要がある引数を定義します。モデルがget_weatherを呼び出すことを決定すると、このスキーマに一致するcity引数を返します。曖昧な説明(「天気を取得します」)はモデルが間違った時点でツールを使用するようにリードします。明確なもの(「ユーザーが天気について尋ねる場合に使用します」)はそれをうまく導きます。

Junoツールの定義 ツール定義には、名前、説明、および引数のparametersスキーマがあります。説明は本当にプロンプトです。モデルがツールをいつ使用するかを判断するために使用するため、注意深く記述してください。パラメータスキーマは、構造化出力と同じメカニズムであり、モデルが送信する引数を制約します。

ツール定義には、モデルが読む3つの部分があります。名前、説明、およびparametersスキーマ。説明は人々が引き受ける部分です。これはチームメイトのコメントではなく、モデルがツールをいつ呼び出すかを決定するために使用する**プロンプト**です。そのように記述してください。ツールが何をするか、いつそれを使用するか、いつそれを使用しないかを言います。

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "顧客のアカウントメールで注文を検索します。 "
                "ユーザーが既存の注文について尋ねる場合にのみ使用してください。 "
                "製品の質問または払い戻しには使用しないでください。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "検索するアカウントメール"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "オプションのステータスフィルター",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

スキーマを厳しくすると、モデルも厳しくなります。

  • requiredはツールが実行できない引数を強制します。
  • enumはフィールドを固定セットに制限するため、モデルは5番目のステータスを発明できません。
  • additionalProperties: falseは定義されていないフィールドを拒否します。

スキーマはあなたの最初の防衛線です。狭いほど、モデルがツールを呼び出すのに間違える方法が少なくなります。

このツール形状はOpenAIスタイルです。正確なキーはプロバイダーによって異なります(Anthropicおよび他の人はスキーマを異なる方法でネストしています)が、3つの部分(名前、説明、スキーマ)はどこでも同じです。

Junoツールの定義 名前、説明、およびparametersスキーマ。それがツールです。説明はdocstringではなくプロンプトです。それをいつ呼び出すか、いつ呼び出さないかモデルに伝えるか、それは間違った時点で呼び出すでしょう。requiredenum、追加のプロパティなし、スキーマを厳しくしてください。狭いスキーマはモデルがツールを呼び出すのに間違える方法が少なくなります。正確なJSONキーはプロバイダーによってシフトしますが、それらの3つの部分は変わりません。

ツール定義は1つの帽子を被った2つのプロンプトです。説明はモデルがツールを呼び出すいつを操作し、スキーマは送信可能な引数を制約し、両方ともモデルに向いた指示であり、内部ドキュメントではありません。スキーマはあなたが厳格になれる場所であり、厳格さはここであなたが持っている最も安い幻想制御です。

python
{
    "name": "issue_refund",
    "description": (
        "特定の注文行の払い戻しを発行します。 "
        "注文IDと金額をユーザーに確認した後にのみ使用してください。 "
        "注文総額を超える金額に対しては決して呼び出さないでください。"
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

制約されたフィールドenumpatternminimum/maximum、またはrequiredセットを持つもの)はモデルが発行できるものを狭めます。ただし、制限を明確に読んでください。多くのプロバイダーはスキーマの構造を検証しますが、値はまだ予測から来ているため、厳格なスキーマは形式が悪い呼び出しを減らしますが、呼び出しが正しいまたは安全であることを証明しません。order_idパターンに一致することは、その注文が存在するか、このユーザーに属することを意味しません。つまり、スキーマはフィルターであり、認可チェックではありません。

あなたはまだ実際の状態に対して、注文が存在し、金額が総額を超えないことを確認し、呼び出し元がそれを所有していることをコード内で副作用を実行する前に検証します。スキーマ検証は間違った形を捕捉します。あなたのコードだけが間違ったアクションをキャッチします。(スキーマキーと各プロバイダーがそれぞれの制約をどの程度厳格に実行するかは異なるため、仮定するのではなくプロバイダーの動作を確認してください。)

もう1つのレバー。ツール数。約十数個のツール過去、選択精度は低下し、モデルは間違ったものに到達し始め、すべての定義はまたコンテキストに座ってすべての呼び出しのトークンを消費します。アクティブなツールセットを小さく、タスクに関連したものにして、API表面全体を一度に公開するのではなく保ってください。

Junoツールの定義 説明はモデルがいつ呼び出すかを制御し、スキーマはそれが送信できるものを制御し、両方ともプロンプトです。enumpattern、境界でロックダウンしてください。ただし、有効な形状を有効なアクションと間違えないでください。整形式のorder_idはまだ信頼されていません。副作用を実行する前に実際の状態に対して再度検証してください。スキーマはフィルター処理し、コードは認可します。ツールセットを小さく保ってください。12を過ぎるとモデルは間違った選択をし、すべての定義はコンテキストを消費しています。

呼び出しの処理

モデルがツールを望む場合、応答には最終的な回答の代わりにtool_callsが含まれます。リクエストされた関数と引数を読み、実際の関数を実行し、結果をtoolメッセージとして返します。その後、コードを再度呼び出して、完了できるようにします。

python
import json

# ツールの実装
def get_weather(city):
    # 実際のアプリではこれは気象APIを呼び出しますが、ここでは偽造します
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "東京の天気は?"}]

# 1. 最初の呼び出し:ツールを提供
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. リクエストされた関数をモデルの引数で実行
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. モデルのリクエストと結果を返す
    messages.append(response.choices[0].message)         # アシスタントのツールリクエスト
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. 再度呼び出して、モデルが結果を使用して回答できるように
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "現在、東京は18度でくもりです。"

引数は**JSON文字列**として到着するため、実際のオブジェクトにjson.loadsしてから、使用する前に確認します。2つのメッセージをhistoryにプッシュします。モデルのツールリクエストとtool result。tool_call_idで接続されます。最終的な呼び出しは、生の気象データを自然な文に変えます。

Juno呼び出しの処理 モデルがツールを望む場合、応答はテキストの代わりにtool_callsを持ちます。JSON文字列から引数を解析し、実際の関数を実行し、2つのメッセージをプッシュバックします。モデルのリクエストと結果はtool_call_idでリンクされています。最終的なモデル呼び出しは、生の結果を自然な回答に変えます。

モデルがツールを望む場合、応答はcontentの代わりにtool_callsを持ちます。各呼び出しは関数名、id、JSON文字列として引数を提供します。解析、実行、マッチングtool_call_idでタグ付けされたtoolメッセージを追加するため、モデルはどの結果がどのリクエストに答えるかを知っています。

正しく取得する価値がある部分は失敗です。ツールがスロー:悪い引数、404、タイムアウト。本能は例外をバブルアップさせることですが、それは全ターンを終わらせます。より良い動きは**エラーをデータとして返す**です。エラーをキャッチしてツール結果としてハンドバックするため、モデルは何が悪かったかを読み、回復、修正された引数で再試行、またはユーザーにそれができなかったことを伝えることができます。

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # 例外ではなくデータとして失敗をハンドバック
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

エラーをツールメッセージとして返すことは、ツールループを脆弱ではなく復元力のあるものにするものです。search_ordersを形式が悪いメールで呼び出すモデルは{"error": "invalid email"}を返し、ユーザーにそれを修正するよう求めることができます。全リクエストが500-ingする代わりに。レスポンス形状(tool_callstool_call_idtoolロール)はOpenAIスタイルであり、プロバイダーによって異なりますが、パターン(解析、実行、結果またはエラーを返す)はすべてにおいて同じです。

Juno呼び出しの処理 応答はtool_callsを持ちます。JSON引数を解析し、関数を実行し、マッチングtool_call_idを持つtoolメッセージを追加します。ここで失敗の動き:例外をバブルアップさせず、ターンを終わらせるのではなく、それをキャッチして{"error": ...}をツール結果としてハンドバックするため、モデルは回復または再試行できます。正確なフィールド名はプロバイダーによって異なりますが、解析、実行、結果またはエラーを返すはすべてにおいて同じです。

解析とディスパッチはつまらない部分です。危険な部分は引数の受信と関数の実行の間のすべてです。なぜなら、引数はモデル出力で、関数は何か本当のことをするからです。

デモから本番環境で実行できるツールを分けるのは3つの習慣です。

  • まず、スキーマではなく、実際の状態に対して実行前に検証してください。注文が存在し、ユーザーはそれを所有し、金額は範囲内です。
  • 次に、**べき等性**のために構築します。同じ操作を2回実行することは、1回実行することと同じ効果をもつプロパティ。モデルが再試行し、ループが再発火し、ネットワークが複製するため、ライトツールはtool_call_idから導出されるべき等性キーが必要です。リピートが二重請求または二重送信しないように。
  • 3番目に、データとしてエラーを返す。モデルが行動できるほど構造化されているため、回復可能な失敗は再試行になり、回復不可能なものはユーザーへのきれいなメッセージになります。
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # 可観測性

    try:
        validate(name, args)                       # 実際の状態チェック、悪い入力で上昇
        result = TOOLS[name](args, idem_key=call.id)  # リトライで べき等
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # モデルは再試行できます
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # モデルに内部を漏らさない

    return {"role": "tool", "tool_call_id": call.id, "content": content}

本番環境のもう2つの現実:

  • 可観測性。どのツールが実行されたか、どの引数でログして(シークレットを赤字する)、何を返したか、なぜなら、エージェントが不正動作するとき、ツール呼び出しのトレースは実際に何をしたかを再構成する唯一の方法だからです。
  • 破壊的なツール。削除、支払い、メール送信などの取り消し不可能な何かに対して、モデルの呼び出しを最終的な権限にしないでください。確認、実行されるかのドライラン、またはヒューマンの承認ステップの後ろにゲートしてください。資格情報をスコープして、モデルが間違っていたり乗っ取られたりしても影響の半径が制限されるようにしてください。

スキーマ検証呼び出しはまだ、コードが認可するまで信頼されていません。(フィールド名とリトライエンベロープはOpenAIスタイル;規律はどのプロバイダーにも実行されます。)

Juno呼び出しの処理 解析はつまらない部分。引数と副作用の間:実際の状態に対して検証してください(スキーマだけではなく)、`tool_call_id`オフでライトをべき等にしてください(リトライは2回発火するため)、モデルが行動できるようにエラーをデータとして返してください(内部を漏らさないで)。すべての呼び出しと引数をログして、エージェントが実際にしたことを再構成できるようにしてください。削除、支払い、メール送信など、破壊的な何かの後ろにゲートしてください(確認またはスコープ付き資格情報)。スキーマ検証呼び出しはまだ見知らぬ人のリクエストです。コードがはい、と言うまで。

実際に

再利用可能な関数と同じフロー。モデルと間にあるのはあなたが書いてコントロール下にあるコードだけであることに注意してください。

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # モデルは直接回答

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

これは1つのツール呼び出しを処理します。モデルはまた、複数を一度にリクエストすることができます。これは**平行ツール呼び出し**と呼ばれます。これはtool_callsの各エントリをループして処理します。そして、モデルがツールを呼び出し続けるループをさせることができます。それを決める独自のステップを毎回、エージェントを取得します。これは、次の章が正確に進む場所です。

Juno実際に 関数でラップされて、ツール使用はツール、コード実行、結果を回答に変えるための2番目の呼び出しを選択するために1つのモデル呼び出しです。モデルとシステム間の唯一のものはあなたが書いたコードなので、あなたはコントロール下にあります。モデルが完了するまでこれをループしてください。エージェントがあります。

実際の**ツールハンドラ**では、マルチターンループ、平行呼び出し、エラーリターンを1つの関数に組み合わせます。モデルは単一ターンで複数のツールをリクエストし、ループは質問を停止するまで実行されます。

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # すべての平行呼び出しを処理
            messages.append(run_tool(call))  # 解析、実行、結果またはエラーを返す

    return "申し訳ございません。それを完了できませんでした。"  # ターンキャップに到達

3つの決定は、デモではなく本番環境対応になります。すべてtool_calls[0]だけではなくループします。呼び出しのみを要求したモデルを暗黙的にドロップするか、またはループしないでください。MAX_TURNSをキャップするため、モデルは永遠にスピンできません。エラーをrun_toolを通じてエラーメッセージにルーティングして、単一の悪い呼び出しが交換全体をクラッシュしないようにしてください。

ステップをハードコードするのをやめ、モデルが毎ターン独自の次のステップを決定させるとき、このループが正確にエージェントになります。違いは自律性,アーキテクチャではありません。

Juno実際に 本番環境ハンドラーは3つのものが組み込まれたマルチターンループです。tool_callsの各エントリをループしますが、最初のエントリのみではなく、永遠にスピンできないようにターンをキャップしてください、悪い呼び出しが実行全体をクラッシュしないようにエラーをツールメッセージとしてハンドバックしてください。同じループ、より多くの自律性、エージェントを持っています。アーキテクチャは変わりません、モデルは独自の次のステップを決定するだけです。

本番環境ハンドラーはすべてを合算します。バウンドされたマルチターンループ、平行呼び出しは同時実行し、検証およびログ実行、エラーはデータとして返されます。残っている質問は、ほとんどのチームがスキップするもの。モデルにツールをまったく与えないとき

ツールは、アクションが実際に自然言語に対するモデルの判断に依存するとき、正しい答えです。どの注文がユーザーを意味するか、これが払い戻しケースをカウントするかどうか、何を検索するか。これは、パス**決定論的**であるとき、間違った答えです。同じ入力は常に同じ呼び出しを生成します。リクエストが常に同じ導出可能な引数で同じ呼び出しをトリガーする場合、コード内でそれをワイヤーし、ラウンドトリップをスキップします。幻想サーフェスを削除し、レイテンシーホップ、トークンコストを削除しても、損失ゼロです。モデルにツールをハンドラックすると、柔軟性が得られ、非決定性で支払います。柔軟性が要点である場所でのみ使用してください。

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # 独立呼び出しは平行
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

ツール呼び出し幻想を制約するとき使用する場合、2つのレバー:

  • ツール呼び出し幻想を制約する。モデルは定義していないツール呼び出しや作成引数を発明できるため、不明なツール名をすぐに却下し、tool_choice(ツール使用を強制、禁止、または解放するパラメーター)を使用して、1つが必要だと知っているとき、または必要な1つもないと知っているときはツールを禁止してください。毎ターンをチャンスに残す代わりに。
  • 可観測性トレース、ツール、引数、結果、ターン数を保ってください。自律ループは、実際にしたことを再現できた場合のみデバッグ可能です。

このハンドラーはエージェントの文字通りの種です。エージェントは、このループであり、より広いツールセットと目標に向けて呼び出しをチェーンする余地があり、これは失敗モードがここでスケールする理由でもあります。(SDKサーフェスはOpenAIスタイル;tool_choiceとメッセージエンベロープはプロバイダーによって異なります。)

Juno実際に 完全なハンドラーは、バウンドされたループであり、並行、検証、ログ実行ツールを実行し、エラーはデータです。スキップされた質問は、ツールをまったく与えないとき。決定論的なら、コード内でワイヤーし、ラウンドトリップを削除してください。tool_choiceを使用してツールを強制または禁止してください(希望ではなく)、定義していないツールへの呼び出しを拒否してください、トレースを保ってください。これはトレーニングホイールがまだ付いているエージェントです。失敗モードはここからのみ大きくなります。