
こんにちは。データサイエンティストの閔(みん)です。普段はAIレストラン検索アプリ「UMAME!」の開発に携わるほか、社内のデータ管理、AIを用いた業務改善などに関わっています。
本記事では、今年1年間、AIの話題として最もホットだったであろう AI Agent(以降Agentとする)を触ったり、作ってみたりしながら感じた、Agentの「自律性」との向き合い方について話します。Agentについては、過去の記事をご参照ください。
Vertex AI Agent Engine Memory Bankを使ってみた - ぐるなびをちょっと良くするエンジニアブログ
AIレストラン検索アプリ『UMAME!』 の舞台裏 ~Google AI Agent Summit ‘25 Spring登壇レポ~ - ぐるなびをちょっと良くするエンジニアブログ
一口でAgentといっても、その自律度(Agency)には差があります。様々な企業や研究者が自律度を定義していますが、その中でもわかりやすい例として、Hugging Face社の定める自律度の目安があります(参照:What is an Agent? - Hugging Face Agents Course)。次の表に、その内容をまとめておきます。
| レベル | 権限委譲の度合い | Agentに許される自由度と判断 | 開発・制御の難易度 |
|---|---|---|---|
| レベル 0 | 自由度なし(No Agency) | LLMは単なるテキスト生成器。ユーザーからの指示にのみ応答する。 | 低 |
| レベル 1 | 条件分岐の実施 | If-Else 文などの定型的なロジックをAgentが実行する(処理の順番は開発者が決定) | 中 |
| レベル 2 | Toolの自律的な利用 | Tool(外部機能)を利用するか否か、どのToolを使うかをAgent自身が判断し決定する。 | 高 |
| レベル 3 | 高度な計画と実行 | 複数のAgentを実行させたり、ToolそのものをAgentが自ら作成したりする。 | 極めて高 |
自律度が高いから必ず良い、というものではなく、実現したいものが何かに応じて使い分けをしていかないといけないと思います。レベル2以降は、自律性が増し、挙動の予測が難しくなってしまいます。ここで初めて、「自律」と「制御」のトレードオフが発生します。
Agentは、与えられた課題に対し、何をすべきかを自ら考える機能を備えているため、ユースケースによっては大変便利なツールとなります。例えば、コーディングAgentを使うことで、自動的に必要なソースコードを読み込んだり、ソースコードを書いたり、必要に応じてテストを実行したりすることができてとても便利です。しかし、決まった手順で作業を行わないといけない場合は、手順が確率的に変わるようでは、かえって使いにくくなってしまいます。
Agentは、その自律性のため、ケースによってはとても便利かもしれませんが、仕様が作成しにくくなったり、評価がしづらくなるケースもあります。フローがある程度固定はされているけれども、一部機能を臨機応変に発火させたい場合や、メモリを使いたい場合は、以下のような方法でバランスをとることができるかと思います。ここからは、Google ADK(Agent Development Kit)の例を中心に解説を進めていきます。
例えば、Google ADKには、「Callback」という機能が用意されており、Agent呼び出しの前後や、モデル呼び出しの前後、そしてTool呼び出しの前後で入出力をコントロールすることができます。Callbackに具体的なreturnを設定すると、Agentの戻り値を書き換えることができます。
こちらに、Agentが呼び出され、すべての処理が行われた後に、出力の型をCallbackを使って調整する例を挙げておきます。Callbackを使って検索結果を呼び出せるように、検索結果をstateとして保持して必要があることが注意点です。
# after_agent_callback の例async def after_agent_callback(callback_context: CallbackContext): """ ホテル検索agentの結果と、stateに保持されている検索情報をまとめて返す """ # ホテル検索agentの結果(ホテルリスト) hotel_list = callback_context.state.get("hotel_list", []) # ユーザーのクエリ query = callback_context.state.get("query", "") # ユーザーの現在地 location = callback_context.state.get("location", {}) # ホテル検索パラメータ hotel_search_params = { "date": callback_context.state.get("date"), "nights": callback_context.state.get("nights"), "adults": callback_context.state.get("adults"), "children": callback_context.state.get("children"), "smoking": callback_context.state.get("smoking"), "breakfast": callback_context.state.get("breakfast"), } # 出力形式を整形 result = { "query": query, "location": location, "hotel_search_params": hotel_search_params, "hotels": hotel_list, } # 必要に応じてJSON文字列で返す result_json = json.dumps(result, ensure_ascii=False, indent=2) return result_jsonGoogle ADKの場合は、「並列」「ループ」「シーケンシャル」の3つの基本フローが用意されています。このケースに当てはまらない場合は、「カスタムエージェント」を作り、自前のフローを作成することもできます。あるSubAgentの結果に応じてagentの実行フローを制御しなければならない場合は、callback、state、カスタムエージェントを組み合わせることでフローを自由に制御することができます。前述のように、途中のSubAgentの結果をstateに保持しておき、callbackで後続処理を行う方法です。
次の例では、ユーザーの入力からユーザーの意図を分類し、その結果に応じて起動すべきAgentを決めています。例えば、サンプルAgentは、観光ガイドのAgentで、観光地検索Agent(sightseeing_agent)、経路探索Agent(route_agent)、航空券検索Agent(flight_agent)、ホテル検索Agent(hotel_agent)の4つSubAgentを持っているとします。そして、ユーザーの意図を分類するAgent(classification_agent)があってユーザーが何を検索したいかを分類するとします。
class RootAgent(BaseAgent): classification_agent: LlmAgent sightseeing_agent: LlmAgent route_agent: LlmAgent flight_agent: LlmAgent hotel_agent: LlmAgent def __init__( self, name: str, classification_agent: LlmAgent, sightseeing_agent: LlmAgent, route_agent: LlmAgent, flight_agent: LlmAgent, hotel_agent: LlmAgent, after_agent_callback: Callable[[CallbackContext], Awaitable[types.Content | None]] | None = None, ) -> None: super().__init__( name=name, classification_agent=classification_agent, sightseeing_agent=sightseeing_agent, route_agent=route_agent, flight_agent=flight_agent, hotel_agent=hotel_agent, after_agent_callback=after_agent_callback, ) async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]: # まず判別用Agentでagent_typeを決定 async for event in self.classification_agent.run_async(ctx): yield event agent_type = ctx.session.state.get("agent_type", None) # agent_typeに応じてサブエージェントを起動 if agent_type == "sightseeing": async for event in self.sightseeing_agent.run_async(ctx): yield event elif agent_type == "route": async for event in self.route_agent.run_async(ctx): yield event elif agent_type == "flight": async for event in self.flight_agent.run_async(ctx): yield event elif agent_type == "hotel": async for event in self.hotel_agent.run_async(ctx): yield event else: # agent_typeが不明な場合は何もしない or エラーイベントを返す passここではGoogle ADKの例を中心に解説していますが、LangGraphなど、フローの作成に特化したライブラリもありますので、カスタムフローの作成が必要な場合はLangGraphなどによる実装を検討しても良いかもしれません。
2025年は、Agentの年といえる一年でした。様々な企業で、様々なユースケースでAgentを導入しようとしているようですが、自律性故にかえって制御しづらい部分が出ているのも確かだと思います。Agentには自律性がありますが、万能のツールではありません。時には、すべてをAgentに任せるのではなく、ある程度制御を行わないと、本当に提供したいサービスややりたいことが実現できないこともあります。Agentを作る前に、制御すべき部分がどこで、自律性を与える部分がどこかをしっかり洗い出すことが大事です。
