
はじめに
こんにちは。データサイエンティストの閔(みん)です。普段は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の自律性にも度合いがある
一口で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を使うことで、自動的に必要なソースコードを読み込んだり、ソースコードを書いたり、必要に応じてテストを実行したりすることができてとても便利です。しかし、決まった手順で作業を行わないといけない場合は、手順が確率的に変わるようでは、かえって使いにくくなってしまいます。
Agentが力を発揮しそうなケース
- ある程度臨機応変が必要な場合: コーディングのように、動的にタスクが変わる場合などにはとても便利です。また、Toolを使うかどうかの判断を動的に行いたい場合にも有効です
- メモリを使いたい場合: Agentは、チャット履歴を短期メモリとして使いますので、以前言ったことを繰り返す手間が省かれます。また、Agentによっては長期メモリを持つこともあるので、重要な内容はいちいち言及しなくてもさくさくとやりとりを進めることができます
Agentが要らないかもしれないケース
- 手順が決まっている場合: 手順が決まっていて、仕様レベルでそれをきちんと定め、管理したい場合は、Agentを使うよりは、必要に応じてLLMを使った方が良いかもしれません
- メモリを使わなくて良い場合: 手順が固定されており、入力データが毎回変わる場合なら、メモリがむしろ邪魔になることもあるでしょう。この場合は、無理にAgentを導入する必要はないと思います
Agentの導入に慎重になっていただきたいケース
- コストの予測が最重要な場合: この場合は、Agentの導入をより慎重に検討すべきでしょう。Agent開発用のライブラリには、Tool呼び出し部やその他LLMによって制御される部分のプロンプトが隠蔽されている場合もあり、消費トークン数の予測が難しくなります。このような場合は、すべてのプロンプトが読める状態となっているAgentを選ぶか、自前のAgentライブラリを開発するか、もしくはAgentの要否を慎重に検討する必要があるかもしれません。
- 再現性のあるテストや評価が必要な場合: LLMを単体で使う場合に比べ、Agentを使った時は、ユーザーとAgentの1やりとりの間でも、Agent間のやり取りやToolの呼び出しなど、どうしても制御できないLLMの呼び出しが何回も発生します。その中で、ユーザーの入力が何回も加工されるため、再現性のある評価が困難な場合もありますので、再現性が問われるケースでは、Agentの導入をより慎重に検討すべきでしょう。
Agent不確実性との向き合い方:Google ADKの例を中心に
Agentは、その自律性のため、ケースによってはとても便利かもしれませんが、仕様が作成しにくくなったり、評価がしづらくなるケースもあります。 フローがある程度固定はされているけれども、一部機能を臨機応変に発火させたい場合や、メモリを使いたい場合は、以下のような方法でバランスをとることができるかと思います。 ここからは、Google ADK(Agent Development Kit)の例を中心に解説を進めていきます。
Callback関数をうまく使う
例えば、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_json
フローを固定しつつ、SubAgentとして自律度の高いAgentを使う
Google 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などによる実装を検討しても良いかもしれません。
さいごに: 実現したいことに応じてAgentを使い分けよう
2025年は、Agentの年といえる一年でした。様々な企業で、様々なユースケースでAgentを導入しようとしているようですが、自律性故にかえって制御しづらい部分が出ているのも確かだと思います。Agentには自律性がありますが、万能のツールではありません。時には、すべてをAgentに任せるのではなく、ある程度制御を行わないと、本当に提供したいサービスややりたいことが実現できないこともあります。Agentを作る前に、制御すべき部分がどこで、自律性を与える部分がどこかをしっかり洗い出すことが大事です。
