以下の内容はhttps://enakai00.hatenablog.com/entry/2025/04/21/114332より取得しました。


Agent Development Kit のコード解析メモ

参考資料

zenn.dev

zenn.dev

zenn.dev

zenn.dev

基本事項

LlmAgent クラス

  • LLM による応答を得るための基本となるクラス
  • セッション管理機能はなく、与えられた InvocationContext の情報を用いてワンショットで応答を返す
  • InvocationContext に含まれる session_service からセッション情報(過去の会話履歴など)を取得して利用する
  • InvocationContext は LlmAgent を呼び出すごとに新しく生成する前提

Runner クラス

  • ローカルで LlmAgent を対話的に利用する際に利用する
  • agent と session_service を保持しており、session 情報を含んだ InvocationContext を作って agent を呼び出す
  • Runner クラスを利用した簡易的な会話 App の例:
class LocalApp:
    def __init__(self, agent):
        self._agent = agent
        self._user_id = 'local_app'
        self._runner = Runner(
            app_name=self._agent.name,
            agent=self._agent,
            artifact_service=InMemoryArtifactService(),
            session_service=InMemorySessionService(),
            memory_service=InMemoryMemoryService(),
        )
        self._session = self._runner.session_service.create_session(
            app_name=self._agent.name,
            user_id=self._user_id,
            state={},
            session_id=uuid.uuid1().hex,
        )
        
    async def stream(self, query):
        content = UserContent(parts=[Part.from_text(text=query)])
        async_events = self._runner.run_async(
            user_id=self._user_id,
            session_id=self._session.id,
            new_message=content,
        )
        result = []
        async for event in async_events:
            if DEBUG:
                print(f'----\n{event}\n----')
            if (event.content and event.content.parts):
                response = '\n'.join([p.text for p in event.content.parts if p.text])
                if response:
                    print(response)
                    result.append(response)
        return result
  • LocalApp クラスのインスタンスからセッション内のイベント情報(Event クラスのリスト)を取り出した例:
session = client._runner.session_service.sessions\
    [client._session.app_name][client._session.user_id][client._session.id] 
session.events

[出力結果]

[Event(content=UserContent(parts=[Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=None, inline_data=None, text='\nこんにちは!おすすめのコーヒーはありますか?\n')], role='user'), grounding_metadata=None, partial=None, turn_complete=None, error_code=None, error_message=None, interrupted=None, invocation_id='e-2a8d30d4-e6fb-4056-8cce-3b2dd13e1dd1', author='user', actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}), long_running_tool_ids=None, branch=None, id='X7a8bStM', timestamp=1745202464.531635),
 Event(content=Content(parts=[Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=None, inline_data=None, text='とばりちゃんが答えるよ!おすすめのコーヒーですか?夜の帳ブレンドは、深煎りでコクがあり、ほんのりビターな大人の味わいで、疲れた心に染み渡りますよ。それから、月光の浅煎りは、フルーティーな香りが特徴で、すっきりとした味わいなので、リフレッシュしたい時におすすめです!どちらも、その日の気分に合わせてお選びいただけます。\n')], role='model'), grounding_metadata=None, partial=None, turn_complete=None, error_code=None, error_message=None, interrupted=None, invocation_id='e-2a8d30d4-e6fb-4056-8cce-3b2dd13e1dd1', author='TobariChan_agent', actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}), long_running_tool_ids=None, branch=None, id='M2sVH857', timestamp=1745202464.532078),
 Event(content=UserContent(parts=[Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=None, inline_data=None, text='\n夜の帳ブレンドは大人の味わいなんですね!\n')], role='user'), grounding_metadata=None, partial=None, turn_complete=None, error_code=None, error_message=None, interrupted=None, invocation_id='e-6c99af33-75d0-41ea-8b34-5b98670884f5', author='user', actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}), long_running_tool_ids=None, branch=None, id='N2gl4EQp', timestamp=1745202491.251667),
 Event(content=Content(parts=[Part(video_metadata=None, thought=None, code_execution_result=None, executable_code=None, file_data=None, function_call=None, function_response=None, inline_data=None, text='とばりちゃんが答えるよ!そうなんです!夜の帳ブレンドは深煎りなので、少しビターで香ばしい、落ち着いた味わいが特徴です。一日の終わりにゆっくりと味わって、くつろいでいただけたら嬉しいです。もちろん、甘いデザートとの相性も抜群ですよ!\n')], role='model'), grounding_metadata=None, partial=None, turn_complete=None, error_code=None, error_message=None, interrupted=None, invocation_id='e-6c99af33-75d0-41ea-8b34-5b98670884f5', author='TobariChan_agent', actions=EventActions(skip_summarization=None, state_delta={}, artifact_delta={}, transfer_to_agent=None, escalate=None, requested_auth_configs={}), long_running_tool_ids=None, branch=None, id='aPw30kDB', timestamp=1745202491.252363)]
  • 特に、Event 内の author 要素が応答を生成したエージェント名を表す

サブエージェントを持つエージェントにおけるルーティング

  • ユーザーの入力を処理するエージェントは、最後のイベントの author を選択する というシンプルなルール。Runner が InvocationContext を作る際に決定する。
  • つまり、session_service に強制的に最後のイベントを追加すれば、次に応答するエージェントを強制変更できる。
  • エージェントが自発的にルーティングする際は、transfer_to_agent ツールを Function calling で呼び出す
  • ルーティングの判断指示は、プロンプトに記載されている。基本的には、description で判断するように指示しているので、適切な description の設定が重要と思われる。
If you are the best to answer the question according to your description, you
can answer it.
  • 転送可能なサブエージェントをツール登録するところ

エージェントが受け取るシステムインストラクションを確認する方法

AgentTool の構造

  • AgentTool() で生成されたツールのインスタンスは、呼び出しごとに新しいテンポラリーセッションを生成するので、過去のセッション情報を引き継がずにワンショットの関数として実行される。
  • ただし、アーティファクトとステート(エージェント間で共有される任意の Key-value データ)は呼び出し元のエージェントと共有される

ToolContext について

  • ToolContext で app_name, session_id, user_id を取得する方法

Session 関連

ToolContext で変更した state が session_service 管理の session 本体に反映される流れ

※ LlmAgent と Session は独立したもので、これらをまとめる責任は Runner にある点に注意。LlmAgent はあくまで state_delta をイベントして記録するだけで、それを Session に反映するのは、Runner 側で実施する。

Agent Engine 関係

デプロイしたエージェントの正体

  • 下記で取得される remote_agent は、デプロイしたエージェント(LlmAgent クラスのオブジェクト)がクラウド上での Runner (的なもの)にラップされている
remote_agent = vertexai.agent_engines.get(
    'projects/[Project Number]/locations/us-central1/reasoningEngines/[App Name]')
  • reasoning_engines.AdkApp クラスが「クラウド上での Runner(的なもの)」に相当する。実際にラップする処理はここで行われる。

  • 公式ドキュメントでエージェント(LlmAgent クラスのオブジェクト)を reasoning_engines.AdkApp でラップして動かす手順が紹介されているが、これはあくまで、クラウド上の動作をローカルでエミュレーションしているだけ。

  • 非公式ではあるが、ユーザー側で先に AdkApp でラップしたものを agent_engines.create() でデプロイしても構わない。

デフォルトの Artifact service

  • Agent Engine にデプロイする際は、インスタンス間で情報共有されない InMemoryArtifactService は使えないが、実際にはこれがデフォルトになっており、これを変更する公式オプションが説明されていない。

    • When deploying my_agent (LlmAgent instance) with agent_engine.create(agent_engine=my_agent), it's wrapped by reasoning_engines.AdkApp as agent_engine = reasoning_engines.AdkApp(agent=agent_engine) [1]

    • Then reasoning_engines.AdkApp uses artifact_service_builder option to setup its artifact service [2] and the default is InMemoryArtifactService [3].

    • So the problem is that agent_engine.create() doesn't have an option to modify this flow to change the artifact service from InMemoryArtifactService to other ones such as GcsArtifactService.

[1] https://github.com/googleapis/python-aiplatform/blob/main/vertexai/agent_engines/_agent_engines.py#L699

[2] https://github.com/googleapis/python-aiplatform/blob/main/vertexai/preview/reasoning_engines/templates/adk.py#L255

[3] https://github.com/googleapis/python-aiplatform/blob/main/vertexai/preview/reasoning_engines/templates/adk.py#L405

  • GcsArtifactService に変更する非公式手順はこちら

remote_agent と会話する簡易アプリの実装例

class RemoteApp:
    def __init__(self, remote_agent, user_id='default_user'):
        self._remote_agent = remote_agent
        self._user_id = user_id
        self._session = remote_agent.create_session(user_id=self._user_id)
    
    def _stream(self, query):
        events = self._remote_agent.stream_query(
            user_id=self._user_id,
            session_id=self._session['id'],
            message=query,
        )
        result = []
        for event in events:
            if DEBUG:
                print(f'----\n{event}\n----')
            if ('content' in event and 'parts' in event['content']):
                response = '\n'.join(
                    [p['text'] for p in event['content']['parts'] if 'text' in p]
                )
                if response:
                    print(response)
                    result.append(response)
        return result

    def stream(self, query):
        # TODO: avoid infinite loop in case of permanent error
        while True:
            result = self._stream(query)
            if result:
                break
            if DEBUG:
                print('----\nRetrying...\n----')
            time.sleep(3)
        return result
  • セッションは remote_agent.create_session(user_id=self._user_id) で生成しているが、この実体は、VertexAiSessionService なので後述の方法で操作できる。
  • remote_agent._session には、初期化時点でのセッション情報が入っており、app_name, user_id, session_id が参照できる。

セッション情報の操作

  • remote_client から app_name, user_id, session_id を取得する
app_name = remote_client._session['app_name']
user_id = remote_client._session['user_id']
session_id = remote_client._session['id']
  • VertexAiSessionService のインスタンスを利用して操作する
from google.adk.sessions import VertexAiSessionService

session_service = VertexAiSessionService(
    project = PROJECT_ID,
    location = LOCATION,
)
session = session_service.get_session(
    app_name=app_name,
    user_id=user_id,
    session_id=session_id,
)
  • 次は、マルチエージェント(サブエージェント)構成の際に、セッションに最後のイベントを強制追加して、エージェントを強制転送する関数の例
from google.adk.sessions import VertexAiSessionService
    
def force_transfer_agent(remote_client, next_agent):
    session_service = VertexAiSessionService(
        project = PROJECT_ID,
        location = LOCATION,
    )
    session = session_service.get_session(
        app_name=remote_client._session['app_name'],
        user_id=remote_client._session['user_id'],
        session_id=remote_client._session['id'],
    )
    last_event = copy.deepcopy(session.events[-1])
    last_event.author = new_agent
    last_event.content.parts = [Part.from_text(text='I got transferred.')]
    last_event.timestamp = time.time()
    return session_service.append_event(session, last_event)

1 回の LLM 呼び出し

  async def _run_one_step_async(
      self,
      invocation_context: InvocationContext,
  ) -> AsyncGenerator[Event, None]:
    """One step means one LLM call."""
    llm_request = LlmRequest() # LLM API に渡す内容

    # ここで llm_request を用意する
    # Preprocess before calling the LLM.
    async for event in self._preprocess_async(invocation_context, llm_request): 
      yield event
    if invocation_context.end_invocation:
      return

    # Calls the LLM.
    model_response_event = Event(
        id=Event.new_id(),
        invocation_id=invocation_context.invocation_id,
        author=invocation_context.agent.name,
        branch=invocation_context.branch,
    )
    async for llm_response in self._call_llm_async(
        invocation_context, llm_request, model_response_event # LLM API 呼び出し
    ):
      # Postprocess after calling the LLM.
      async for event in self._postprocess_async(
          invocation_context, llm_request, llm_response, model_response_event
      ):
        yield event
  • llm_request (特に system instruction)に詰め込む情報の定義



以上の内容はhttps://enakai00.hatenablog.com/entry/2025/04/21/114332より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14