はじめに
背景
2025年になり「AIエージェント」という言葉を多く聞くようになりました。一般的には、人間が毎回細かく指示しなくても、AIがある程度自律的に判断して作業を進める仕組みを指すことが多いようです。
今回は、自分の研究分野に合う助成金を探してくれるAIエージェントを作ることにトライし、実際にプロトタイプを開発しました。以下では、その実装プロセスと得られた知見を紹介します。
テーマ選定:助成金検索
企業・研究機関問わず、研究をしていると助成金や公募の申請は必須業務です。
特に、助成金への依存度が高いニッチな分野ほど、応募できそうな助成金を探すのにかなりの労力を費やすことになります。
助成金情報が一つのプラットフォームにまとまっていれば良いものの、
バラバラと乱立しており、個別で探し出さないといけないことも珍しくあり
加えて、詳細がwordだったり、xlsxだったり、PDFだったり...
良さげな助成金を見つけた時には締め切りが1週間後なんてことも...
そこで、「自分に合う助成金を自動で調べて、必要情報を抜き出してまとめてくれたら便利」という発想が今回のプロジェクトの出発点になりました。
この記事執筆時点でのプロトタイプは近日中に公開予定ですが、まずは開発の流れを共有します。
実装のあゆみ
1. CrewAIで作ってみる
CrewAIを選んだ理由
AIエージェント開発のライブラリは多々ありますが、比較的シンプルな設定で拡張もしやすいという理由からCrewAIを最初に採用しました。
CrewAIの実装ポイント
CrewAIでは、以下のようにエージェントに「役割(role)」「タスク(task)」「ゴール(goal)」を設定し、必要に応じて外部ツールを使ってもらいます。たとえば「Web検索のためのツール」「PDFリーダーのツール」を登録しておくと、エージェントがプロンプト経由でツールを呼び出します。
最初は2つのエージェント構成でスタートしました。
- url_searcher
ユーザー情報を読んで助成金を検索し、URLリストを作る - extractor
- URLリストから助成金情報を読み出し、必要項目(概要、締切、金額など)を抽出
設定ファイル(yaml)は下記のような形です。
url_searcher:
role: >
助成金検索エージェント
goal: >
ユーザーの目的やキーワードに合致する公募・助成金のURLを多数見つけ出す。
backstory: >
あなたはウェブ検索に精通しており、関連性のある助成金を探すのが得意です。
必要に応じて検索ツールを使い、最新かつ適切なURLをリスト化してください。
extractor:
role: >
助成金情報抽出エージェント
goal: >
URLリストを1件ずつ精読し、必要な情報(概要・申請期限・応募要件・金額など)を抽出する。
backstory: >
あなたはウェブページを読み込み、ユーザーにとって本当に必要な助成金情報のみを抽出するスペシャリストです。
不要だと判断したページは破棄し、有用だと判断した情報だけを保持してください。
タスク設定ファイル例
url_search_task:
description: |
ユーザーのキーワードや希望条件に基づいて、関連性の高い助成金のURLリストを作成してください。
まずは検索ツールなどを使い、可能な限り有益そうなサイトを見つけてください。
ユーザーが指定したurlがある場合は、そのURLを含めてください。
[ユーザーが指定したurl]
{url_list}
expected_output: |
found_urls という変数に最大20件程度のURLを入れてください。
agent: url_searcher
extract_info_task:
description: |
以下のステップで情報を抽出してください:
1. url_search_task で得られた found_urls を順に読み込み、ページをスクレイピング。
2. 必要な情報(概要・申請期限・応募要件・金額など)が足りない場合は、再度検索ツールを使って
他のサイトや公募要項のソースを探すなど追加調査を行い、補足URLを得る。
3. 情報が見つかったら抽出内容を更新する。まだ不足していればさらに再検索・再スクレイピングを試みる。
4. すべて必要な情報が揃えば、それ以上は検索しない。
最終的に有益な助成金情報だけを extracted_grants という変数にまとめてください。
expected_output: |
extracted_grants に、1件ごとに「概要」「申請期限」「応募要件」「金額」を含む形でまとめる。
agent: extractor
reporting_task:
description: |
extract_info_task で作成された extracted_grants をもとに、
日本語でMarkdown形式のレポートを作成してください。
不足があれば再度検索に行く必要はありません。
expected_output: |
レポートを最終的に report.md に出力し、読みやすい構成にしてください。
agent: extractor
output_file: report.md
CrewAI本体のコード例
# src/my_project/crew.py
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
from dev_grant.tools import PDFReaderTool
@CrewBase
class PublicFundingCrew():
"""
- url_searcher エージェント:
URL検索タスクを担当 (url_search_task)
- extractor エージェント:
URLを1件ずつ読んで情報抽出 (extract_info_task)
必要に応じてレポート作成 (reporting_task)
"""
@agent
def url_searcher(self) -> Agent:
return Agent(
config=self.agents_config['url_searcher'],
verbose=True,
tools=[SerperDevTool(),ScrapeWebsiteTool(), PDFReaderTool()]
)
@agent
def extractor(self) -> Agent:
return Agent(
config=self.agents_config['extractor'],
verbose=True,
tools=[SerperDevTool(),ScrapeWebsiteTool(), PDFReaderTool()]
)
@task
def url_search_task(self) -> Task:
return Task(
config=self.tasks_config['url_search_task'],
)
@task
def extract_info_task(self) -> Task:
return Task(
config=self.tasks_config['extract_info_task'],
)
@task
def reporting_task(self) -> Task:
return Task(
config=self.tasks_config['reporting_task'],
output_file='report.md'
)
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential, # url_search_task -> extract_info_task -> reporting_task
verbose=True,
)
ベースとなるモデルには、一定枠まで無料で使えるgoogleのgeminiAPIを使用しました。
動かしてみた結果
- 一応動くが、検索してくる助成金が的外れ + 情報不足
- 「必要情報が揃うまで検索を続ける」という指示がうまく機能しない
プロンプト調整を色々試しましたが改善せず、「もう少し細かく行動制御できる仕組みが欲しい」との思いから、別のライブラリに移行しました。
2. LangChainで作ってみる
LangChainを使った理由
- ツールの豊富さ(Webブラウザ操作、検索API、PDF解析など)
- エージェントの挙動をある程度細かく制御することができる
- LangGraphほど学習ハードルが高くない
CrewAIよりも「if文やループで行動を制御すること」を重視する形にシフトしました。
LangChainの実装ポイント
- Searchエージェント & Extractorエージェントに加え、PDFからユーザー情報を抽出するエージェントなどを追加
- Playwrightを使った独自ツール(playwright_tools.py)を実装して、Web上を動的に遷移可能に
ファイル構成
main.py # 全体フロー制御やCLI処理、結果出力
create_user_preference.py # PDF読み取り → ユーザ設定ファイル生成
auto_url_selector.py # Googleカスタム検索APIを叩いてURL候補取得
grant_extractor.py # 助成金情報抽出クラス (LangChainエージェント)
playwright_tools.py # Playwrightラッパ (リンクたどりなど)
pdf_summarizer.py # Gemini 2.0 APIでPDFを要約
search_utils.py # Googleカスタム検索APIラッパ
config.py # APIキーや設定値管理
エージェントの抜粋例
class GrantInfoExtractor:
def __init__(self):
"""ツール・LLM・メモリを初期化し、LangChain エージェントを構築"""
self.llm = ChatGoogleGenerativeAI(
model="gemini-2.0-flash-lite-preview-02-05",
temperature=0.2,
google_api_key=os.environ["GEMINI_API_KEY"],
)
self.browser_tool = PlaywrightBrowserTool()
self.link_follower = LinkFollowerTool()
self.tools = [self.browser_tool, self.link_follower]
self.memory = ConversationBufferMemory(memory_key="chat_history")
self.agent = initialize_agent(
tools=self.tools,
llm=self.llm,
agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
memory=self.memory,
verbose=True,
)
self.visited_urls: set[str] = set()
self.source_urls: dict[str, str] = {}
def extract_grant_info(self, url: str, max_follow_links: int = 3) -> dict:
"""
1. メインページをスクレイピング
2. _extract_initial_data で初期情報を構造化抽出
3. update_missing_fields_for_grant_data で「要確認」項目を補完
4. _identify_relevant_links で不足情報のありそうなリンクを抽出
5. _extract_additional_info でリンク先から追加情報を取得
6. 最終結果を整形して返却
"""
def update_missing_fields_for_grant_data(self, data: dict, context: str) -> dict:
"""
data 内の値が '要確認' / '情報なし' のフィールドを補完する。
- Google 検索で候補 URL を取得
- PlaywrightBrowserTool でページ取得
- extract_field_info で該当項目だけを抽出
"""
def extract_field_info(self, field: str, content: str, url: str) -> str:
"""LLM に単一フィールド抽出を依頼し、テキストで返す"""
def _extract_initial_data(self, content: str, url: str) -> dict:
"""初期ページの全文を LLM に渡し、必要 8 項目を JSON 構造で取得"""
def _identify_relevant_links(self, links: list[dict], initial_data: dict) -> list[dict]:
"""不足項目に関連するリンクを LLM に選定させ、上位 N 件を返す"""
def _extract_additional_info(self, content: str, current_data: dict, url: str) -> dict:
"""リンク先ページから不足項目だけを抽出し、差分 JSON を返す"""
def summarize_grant_info(self, grant_info: dict) -> str:
"""抽出した助成金情報を Markdown 形式で整形し、要約文を生成"""
動かしてみた結果
- 以前より助成金情報は充実したが、クエリキーワードを変えてもらう必要があるケースで行き詰まる
- 細かい制御(if文やループ)で再検索の条件を組んだため、「自律的」というより従来の手続き型プログラム感が出てきた
ここで一度チームでディスカッションし、エージェントの構成から考え直しました。
助成金カテゴリの仮説を立てるエージェントを追加
LangChainで試行錯誤するうちに気づいたのが、「研究者は検索して見つからなければ、自分の研究分野を言い換えて再検索する」という動きです。
例:「生態系 助成金」→「生物多様性 助成金」
自分の研究領域を別の角度で説明したり、関連キーワードを試したりするのは、「世の中にどういう分野の助成金が存在するか」をある程度把握しているからこそできる動きです。
Geminiのような大規模モデルなら、さまざまな研究領域・助成金カテゴリを学習しているはずです。
そこで、「ユーザー情報から直接検索クエリを作る」のではなく、まず「ユーザーに当てはまりそうな助成金カテゴリの仮説」をAIに立ててもらい、そのカテゴリごとにクエリを生成→検索する方式に変更しました。
仮説構築エージェント例
def hypothesize_categories(user_info: str) -> list:
"""
LLM1: ユーザー情報をもとに、検索すべき公募・助成金カテゴリの仮説を立てる。
出力は、各行に1つずつカテゴリが記載されたリスト。
"""
llm = ChatGoogleGenerativeAI(
model="gemini-2.0-flash-thinking-exp-01-21",
temperature=0.2,
google_api_key=os.getenv("GEMINI_API_KEY"),
)
prompt = f"""
### 指示
以下のユーザー情報に基づいて、ユーザーが応募できる可能性がある公募・助成金のカテゴリについて仮説を5個立ててください。
カテゴリは具体的になりすぎないように注意してください。
例えば、衛星データ解析の研究者であれば、「気候変動」「GX」「災害対策」などが考えられますし、クジラの研究者であれば、「海洋生態系」「SDGs」などが考えられます。
### 入力
ユーザー情報: {user_info}
### 出力形式
出力は下記の形式で出力してください
```
気候変動
GX
災害対策
```
"""
logging.debug("LLM1: 仮説生成プロンプトを送信します。")
response = llm.invoke(prompt)
output = response.content.strip()
logging.debug(f"LLM1 出力:\n{output}")
# 改行ごとにカテゴリを抽出
categories = [line.strip() for line in output.splitlines() if line.strip()]
return categories
仮説立ては検索プロセスの中で一度だけしか実行しないので、reasoningモデル(gemini-2.0-flash-thinking)を使用し、ユーザー情報をしっかりと分析して仮説立ててもらうようにしました。
このステップを入れた結果、無関係な助成金を拾うケースが大幅に減少しました。
それでも残る課題
- それぞれの助成金を深掘りしていく際の「検索ループ」がルールベースで、無関係なサイトに飛んだり、情報が足りないまま止まったりする
- 細かく制御すればするほど「これって本当にエージェント? ただの自動化スクリプトでは?」という疑問が増す
この経験から、「自律判断に任せたいのに、制御を頑張ると自律判断成分が削られてしまう」というジレンマが強まりました。
とはいえ、LangChainでの実装で得た知見(「カテゴリ仮説→再検索」など)は有用だったので、再びCrewAIに戻してタスク定義を拡張してみました。
3. CrewAIに再挑戦
LangChainで細分化したプロセスをCrewAIの「タスク」単位に落とし込み、エージェントにある程度任せる形を再構成しました。
「ユーザー情報分析 → 助成金カテゴリ仮説 → 検索クエリ生成 → 助成金検索 → 情報抽出」という流れをタスクとして定義して、それぞれのタスクに最適なツールセットや役割を割り当てます。
安定性向上に向けた取り組み
しかし、実際に動かすと以下のような問題が発生していました:
- 検索結果に大きなばらつきがある
有用なリストを作成してくれるときもあれば、まったく不適切な助成金ばかり集めてしまうケースもある - 軽量モデルを使う検索エージェントの判断精度が低い
検索の呼び出し回数は多いため、計算コストを抑えて軽量モデルを割り当てたいが、結果として情報の充足度チェックが甘くなる
ユーザー代理エージェントの追加
そこでまず、「一度に詳細をすべて集める」のではなく、まずは簡易リストアップを行い、その後“調査すべき助成金”を取捨選択する工程を挟むようにしました。具体的には「ユーザー代理」エージェントを追加して、候補の助成金リストを見ながら「この助成金は詳細調査が必要」「これは不要」とジャッジを行う流れです。
このエージェントは、あたかもユーザーが中間チェックをしているような役割を持たせるイメージ。役割(role)設定には「研究分野やユーザーの希望を理解し、関連性の高い助成金を選ぶこと」を明示的に記載しました。これにより、検索エージェントが生成する大量の候補から無関係そうなものを早期に排除できるようになり、無駄な追加検索を抑制できます。
CrewAIの階層プロセス(Process.hierarchical)の利用
次に、「マネージャーエージェント」が進捗を見ながら「まだ情報が足りないから再検索」「十分集まったので報告に進む」といったフロー制御を行うことを試みました。CrewAIには Process.hierarchical を使う方法があるのですが、コミュニティでも議論されている通り、まだ開発途上な部分がありエラーに遭遇しがちでした。
レビュー + ルールベースによる品質担保
最終的には、ハードコードで2択分岐を設けて、マネージャー役のエージェントが「調査完了 or 継続」を返すというシンプルな制御に留め、それを見て次のタスクに進むかどうかを決定する形をとりました。こうした工夫で以前よりは動作の安定度が増し、無駄なループや誤判定をある程度抑制できました。
これにより、比較的関連性の高い助成金リストを作成することができるようになりました。
このタイミングでstreamlitを使って簡単なUIを作成し、プロトタイプの形が見えてきました。
4. Google-ADK登場
形にはなったものの、そもそもCrewAIは順次実行が基本で、ループや分岐を実現するにはハードコードが増える傾向がありました。実際にコードベースでif ~ else ~や再帰呼び出しに近い書き方をする必要があり、文脈の受け渡しや結果の管理、API接続エラー時の再試行などで不安定な部分が多々あったのです。
そんな中、2025年4月9日に発表されたのが、GoogleのAgent Development Kit (google-adk) です。基本的な思想はCrewAIと同様で、「AIに役割・タスク・ツールを与えて実行させる」方式ですが、以下のような特徴が大きな魅力でした。
- SequentialAgent / LoopAgent / ParallelAgent のビルトイン
順次実行だけでなく、「終了条件を満たすまで繰り返すエージェント(LoopAgent)」や、「複数タスクを並列実行するエージェント(ParallelAgent)」が標準で用意されており、CrewAIでハードコードしていたループや分岐が数行で書ける - エージェントとサブエージェント
ワークフローを入れ子にしていける設計で、たとえば「全体を監督するマネージャーエージェント」と「個別の検索・抽出を行うサブエージェント」をシンプルに定義できる - 既存ツールとの互換性
CrewAIやLangChainのツールをラップするだけでそのまま利用できる
なぜCrewAIからGoogle-ADKへ移行したのか
- コードの可読性・保守性
CrewAIを拡張する形でループや分岐をハードコードしていると、コードが複雑化しやすくメンテナンスも大変でした。Google-ADKではLoopAgentなどが最初から用意されており、「何を繰り返すのか」「何を基準に終了するのか」を数行の宣言的なコードで記述できます。
- 容易な階層構造の実装
CrewAIのProcess.hierarchicalは使いこなすのがやや難しく、コミュニティでもバグ報告が多い状況でした。一方Google-ADKでは、親エージェントの下にサブエージェントを何層もネストしやすく設計されており、「上位エージェントが下位エージェントをいつ呼び出し、いつ終了するか」を定義しやすいです。
- ツール資産がそのまま使える
既存のCrewAIツールやLangChainのユーティリティをADK向けにラップするだけで流用できるので、大半のコードを大きく書き直す必要がありませんでした。これは移行のハードルを大幅に下げる要素でした。
ADKのワークフロー
# --- Workflow Definition ---
initial_phase_agent = SequentialAgent(name="InitialGathering",
sub_agents=[profile_analyzer, hypotheses_generator, query_generator, search_expert_initial])
detailed_investigation_loop_agent = LoopAgent(name="DetailedInvestigationLoop",
sub_agents=[search_expert_investigation, investigation_evaluator,quality_checker, CheckStatusAndEscalate(name="StopChecker")],
max_iterations=10 )
second_phase_agent = SequentialAgent(name="SecondPhase", sub_agents=[user_proxy, detailed_investigation_loop_agent,report_generator])
CrewAIでハードコードしていた「情報がまだ足りないなら再検索ループを回す」「条件を満たしたら抜ける」といった制御を、ADKではスムーズに実現できました。
実際に検索してみた結果も、きちんと必要な情報が埋まったリストを作成できています。
実装してみてわかったこと
1. タスク切り分けが命
AIに委ねる範囲が広すぎると、どのように判断すべきか混乱しやすいため、精度が落ちやすいです。反対に、一連の作業をすべて手動で制御する形にするとAIの自律性が薄れ、エージェントらしさが失われてしまいます。
モデルの性能が上がっていくと、より複雑なタスクに対応できるようになっていくと考えられますが、gemini-2.0-flashベースの場合、新人アルバイトが迷わずこなせる程度に設計する必要がある印象でした。
2. エージェントという定義は幅広い
タスクを細かく分割し、行動を手動で逐一制御すると、実質的には従来の自動化スクリプトに近い仕組みになりがちです。これは必ずしも悪いことではありませんが、エージェントのキモである「自律的に動く」醍醐味は減ります。
現在エージェントと呼ばれているシステムの中には、AIが主体的に判断や計画立案を行うものから、ほとんど判断要素を含まずに生成AIを使って定型プロセスを自動化するだけのものまで、自律性に幅広いグラデーションがあります。前者では「どんな検索クエリを再生成するか」「どんなステップを踏むか」といった高度な意思決定をAIに委ねますが、後者では人間が決めたフローに沿ってAIがテキストを生成しているだけの仕組みになります。
「エージェント」という単語はこの両端をひと括りにして語られがちですが、どこまで自律性を持たせるかによって必要な設計や検証も大きく変わるため、プロジェクトの要件に合わせて慎重に選択することが重要です。
3. 人間の判断プロセスをAIに落とし込む
たとえば助成金検索でも「全体のカテゴリを把握→自分の研究領域に合うものを洗い出し→要件の詳細を確認」といったステップを人間は自然に行っています。こうした明確化がないと、エージェントが途中で迷走したり、取りこぼしが発生しやすくなります。
一方で、そのうちAIが人間には思いもつかないようなプロセスで同等以上のアウトプットに辿り着く時代がくるような気もしています。
今後の展望
現在はローカル環境でStreamlitのUIを使い、その都度手動でエージェントを実行していますが、今後は定期的な自動実行を目指したいと考えています。新しい助成金情報が見つかった際にユーザーへ通知を送れるようにすれば、最新の公募情報を逃さずチェックできる仕組みが整うはずです。
また、ユーザーのフィードバックをリアルタイムに取り入れ、さらに助成金の応募テーマをAIが提案できるようにすることで、「研究資金獲得コンシェルジュ」のようなシステムを実現したいと考えています。研究者が資金情報収集に費やす時間を減らし、本来の研究に集中できるようなサポートツールを目指して、今後も機能を拡充していく予定です。
おわりに
助成金検索エージェント構築の試行錯誤として、CrewAI → LangChain → CrewAI → Google-ADKと渡り歩きました。
「自律判断をどこまでAIに任せるか」と「細かい制御をどこまでハードコードするか」が常にトレードオフで、「エージェントとは何か」を考えるきっかけになりました。
まだ各ライブラリとも発展途上で、今後も頻繁なアップデートが予想されますが、エージェント開発は可能性に満ちた面白い分野だと思います。
今回作ったプロトタイプは、近日中に公開予定です。公開後はぜひ触ってみていただけると幸いです。
参考