Closed4

OpenAI「Agents SDK」④コンテキスト管理

kun432kun432

コンテキスト管理

https://openai.github.io/openai-agents-python/ja/context/

一口に「コンテキスト」と言っても色んな意味がある。ここでは主に以下の2つ。

  1. コードからローカルに利用できるコンテキスト
    • ツール関数の実行時に、on_handoff のようなコールバック、ライフサイクルフックなどで必要になるデータや依存関係
  2. LLMに利用できるコンテキスト
    • 応答を生成する際にLLMが参照できるデータ

それぞれの観点で見ていく。


ローカルコンテキスト

RunContextWrapper暮らさうとその中のcontextプロパティで表される。以下のような動作となる。

  1. 任意のPythonオブジェクトを作成する。dataclassやPydanticオブジェクトなど
  2. そのオブジェクトを実行メソッド(Runner.run(..., context=XXXX))に渡す。
  3. すべてのツール呼び出しやライフサイクルフックに、ラッパーオブジェクト RunContextWrapper[T]が渡され、wrapper.contextでアクセスできる(Tはコンテキストオブジェクトの型。)

重要なのは、特定のエージェント実行においては、全てのエージェント、ツール関数、ライフサイクルなどは、同じ方のコンテキストを使用しなければならない、という点。

コンテキストの用途は以下。

  • 実行のための状況/状態データ(例: ユーザ名、uid、その他ユーザに関する情報など)
  • 依存関係(例: loggerオブジェクト、データ取得コンポーネントなど)
  • ヘルパー関数

なお、コンテキストオブジェクトはLLMには送信されず、ローカル内でのみ使用可能なオブジェクトであり、読み書き・メソッドでの呼び出しなどが行える。

サンプル

import asyncio
from dataclasses import dataclass
from agents import Agent, RunContextWrapper, Runner, function_tool

@dataclass
class UserInfo:  
    name: str
    uid: int

@function_tool
async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str:  
    """ユーザの年齢を取得する。ユーザの年齢に関する情報を取得する場合はこのツールを呼び出す。"""
    return f"ユーザ {wrapper.context.name} は、47歳です。"

async def main():
    # ユーザのコンテキストを定義
    user_info = UserInfo(name="太郎", uid=123)

    # エージェントを定義
    agent = Agent[UserInfo](  
        name="Assistant",
        tools=[fetch_user_age],
    )

    # コンテキストを渡して、エージェントを実行
    result = await Runner.run(  
        agent,
        input="ユーザについて知っていることを全部教えて。",
        context=user_info,
    )

    print(result.final_output)  

if __name__ == "__main__":
    asyncio.run(main())
出力
ユーザは「太郎」さんで、47歳です。それ以外の情報は持っていません。

コンテキストでユーザ名とUIDを渡す→ツール内からコンテキストにアクセスして結果を返す→LLMが回答を返す、という感じ。

コンテキストオブジェクトは LLM に 送信されません。ローカル専用のオブジェクトであり、読み書きやメソッド呼び出しができます。

というところについても、例えば上記の例だとコンテキストとしてUIDが渡されているが、あくまでもこれにアクセスできるのはツールであって、エージェント(LLM)ではない。なので、何かしらそれを読み出すようなことがなければ、エージェントは知り得ない、ということになるのだと思う。


エージェント / LLM コンテキスト

LLMは基本的に会話履歴にしかアクセスできない。これ以外の新しいデータをLLMに参照させる場合には、その履歴を経由して提供する必要がある。例えば以下。

  1. エージェントのinstructions
    • 「システムプロンプト」または「開発者メッセージ」
    • スタティックな文字列、または、コンテキストを受け取って動的に文字列を返すような関数
    • 常に有用な情報に最適(ユーザ名や現在の日付)
  2. Runner.run()実行時の入力
    • instructionsに追加するのと似ている
    • ただし、モデルスペックにある "chain of command" の下位にあるメッセージをもたせる事が可能
    • つまり「ユーザープロンプト」「ユーザメッセージ」
  3. 関数ツールで公開
    • オンデマンドのコンテキスト向け
    • LLMが必要と判断した際に必要な情報を取得する
  4. リトリーバルやWeb検索
    • ファイルやデータベース、Webから関連するデータを取得する
    • 関連情報を使って**応答の内容を「グラウンディング」**するのに有用

余談だが、このあたりの使い分け、みたいなのがやりやすい、ってのはフレームワークにも求められるところだと思う。先日、とあるフルスタックフレームワークをいじっていたのだけど、コンテキストをいろんなやり方で注入しようとしたら、仕様上難しかったことがある(そのフレームワーク的にはコンテキストをいろんなやり方で注入する、というよりはある程度一つのやり方でまるっと全部いれる、みたいな思想だった)

このスクラップは1ヶ月前にクローズされました