- バックエンド
- PdM
- フロントエンドエンジニア
- Other occupations (23)
- Development
- Business
こんにちは。ウォンテッドリーの Enabling チームでバックエンドエンジニアをしている市古(@sora_ichigo_x)です。
現在、Enabling チームでは技術的な取り組みを社外にも発信すべく、メンバーが週替わりで技術ブログをリレー形式で執筆しています。前回は冨永さんによる「【計画編】 スカウト機能のマイクロサービス化を辞める判断について」でした。今回はAIエージェントを実装する話をします。
2025年に入り、「AIエージェント」という言葉が急速に浸透してきています。
そしてこれは単に利用ユーザーの関心が高まっているだけではなく、サービスを提供する側にとっても無視できない話です。主要ベンダーのSDKやAPIが次々と整備され、技術的には誰もがAIエージェントを社会実装できる環境が整いつつあります。つまり、AIが当たり前になる世界で、AI機能を持たないプロダクトは競争優位を維持できなくなるリスクが高まっていると言えるでしょう。
エンジニアとしては、AIエージェントを活用するだけでなく、その動作原理を理解し、自分たちで機能を実装できるようになっておくことが重要だと考えています。
目次
AIエージェントとは
厳密な定義はない
「LLMとツール群を組み合わせて、目標達成まで自律的に複数のタスクを実行する仕組み」と考える
RubyでAIエージェントを動かしてみる
ライブラリ選定
Claude 3 Haiku と 自作 Calculator Tool だけの簡単なAIエージェントを作ってみる
動作原理
AIエージェントの本質はLLMとツールによるループである
ループの内部構造
まとめ
AIエージェントとは
厳密な定義はない
「AIエージェント」という言葉には、実は厳密で統一的な定義がありません。研究分野やプロダクト開発の文脈によって、その指す範囲はさまざまです。
有名な論文 Intelligent Agents: Theory and Practice(Michael Wooldridge, Nicholas Jennings, 1995) でも「そもそもエージェントとは何か」という問いは、AI研究者にとっても曖昧なまま議論されてきたと述べられています。
この論文では「弱い意味でのエージェント」として、以下の4つの特徴を持つものが広く認識されていると説明されています。
- 自立性(Autonomy)
- 他の主体の直接的な介入なしに、自分で行動や内部状態をある程度コントロールする
- 社会性(Social Ability)
- 他のエージェントや人間とコミュニケーションする
- 反応性(Reactivity)
- 環境の変化を感知し、タイムリーに反応する
- 積極性(Pro-activeness)
- 周囲の変化に受動的に応じるだけではなく、目標を持って主体的に行動する
また、より「強い意味でのエージェント」としては、知識や意図、信念など、人間的な概念を内部的に持つものと定義する立場もあります。
「LLMとツール群を組み合わせて、目標達成まで自律的に複数のタスクを実行する仕組み」と考える
このブログでは、厳密な哲学的定義を深掘りするのではなく、上記の論文に倣った「弱い意味でのエージェント」を前提に話を進めます。特に大規模言語モデル(LLM)とツール群を組み合わせ、目標達成まで、自律的に複数のタスクを遂行する仕組みを「AIエージェント」と呼ぶことにします。
RubyでAIエージェントを動かしてみる
ライブラリ選定
この分野でのエコシステムは Python が最も発達していますが、Ruby でもAIエージェントを作ることはできます。
実際に Ruby で AI エージェントを作ろうと思ったとき、主に4つの選択肢があります(ここではLLMはすでにAPIキーなどを用意済み」という前提です)。
- ruby_llm を使う
シンプルなインターフェースでLLM呼び出しやツール実行をラップする - langchainrb を使う
LangChainの概念をRubyで利用できる - activeagent を使う
Railsに統合する形でLLM呼び出しやツール実行をラップする - 自作する
LLM呼び出しやメッセージ管理、ツール実行などを全て自前で実装する
自作は不可能ではありませんが、トークン管理やツール呼び出しのステート管理など、思った以上にやることが多くなります。そのため、現実的には 1~3 のライブラリを使うのが良いと思います。
今回は最も star 数が多い ruby_llm を例に使います。しかし基本的な動作原理はどのライブラリでも共通です。
Claude 3 Haiku と 自作 Calculator Tool だけの簡単なAIエージェントを作ってみる
ここからは実際にコードを動かしてみます。
今回は、LLM として Claude 3 Haiku を使い、さらに「四則演算できるシンプルなツール(Calculator)」を組み合わせた簡単なAIエージェントを考えてみます。
まずは事前準備として LLM のセットアップを行います。今回は Amazon Bedrock を使いましたが、別のベンダーを使う場合は設定項目が異なるため公式ドキュメントを参考に調整してください。
require "ruby_llm"
RubyLLM.configure do |config|
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
config.bedrock_region = ENV.fetch('AWS_REGION', nil) # e.g., 'us-west-2'
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil) # For temporary credentials
config.default_model = "anthropic.claude-3-haiku-20240307-v1:0"
end
続いてAIエージェントの実装を行います。今回はテストしやすいように Agent クラスを作りました。
class Agent
def initialize
@chat = RubyLLM.chat
@chat.with_tools(Calculator)
@chat.on_end_message do |message|
if message.tool_call?
if message.content && !message.content.empty?
puts "Agent: #{message.content}"
end
puts ""
puts "ツールを実行します"
message.tool_calls.each do |tool_call|
puts "ツール名: #{tool_call.last.name}"
puts "引数: #{tool_call.last.arguments}"
end
puts ""
end
end
end
def run
puts "Chat with Agent. Type 'exit' to quit."
loop do
print "You: "
user_input = ""
while line = gets
user_input += line
break if line.chomp.empty?
end
break if user_input.downcase == 'exit'
response = @chat.ask user_input
puts "Agent: #{response.content}"
end
end
end
続いてツールの実装を行います。今回は簡単な四則演算のみサポートした Calculator クラスを作りました。
#frozen_string_literal: true
require "ruby_llm"
class Calculator < RubyLLM::Tool
description "基本的な四則演算を実行するツール"
param :expression, desc: "計算式(例:2+3、10-5、4*6、20/4)"
def execute(expression:)
tokens = tokenize(expression)
# 長くなるので割愛
# https://github.com/sora-ichigo/code-editing-agent/blob/2efb1da93d6548a75d703f5cb16ce7f357680123/calculator.rb に公開しています
"#{expression} = #{result}"
end
end
最後に Agent#run
を呼び出すエントリーポイントを実装しましょう。
if __FILE__ == $0
agent = Agent.new
agent.run
end
実際に動かすと、以下のような対話ができます。この例では簡単な算数問題を解いてもらいました。
$ bundle e ruby ./run.rb
Chat with Agent. Type 'exit' to quit.
You: たろうくんはキャンディを24個持っています。
1. まず、そのうち半分を友だちにあげました。
2. 残りを4つずつ袋に分けました。
3. 1つの袋にはキャンディが何個入っていますか。
Agent: では、この問題を順に解いていきましょう。
ツールを実行します
ツール名: calculator
引数: {"expression"=>"24 / 2"}
Agent: たろうくんが最初に友達にあげたキャンディは12個です。
ツールを実行します
ツール名: calculator
引数: {"expression"=>"12 / 4"}
Agent: 残りのキャンディを4つずつ袋に分けたので、1つの袋にはキャンディが3個入っています。
したがって、1つの袋にはキャンディが3個入っています。
このように、複数のステップを含む問いかけに対して、LLMが自動で計算ツールを呼び出しながら解答を組み立ててくれます。
とてもシンプルな例ですが「自律的に複数のタスクを遂行する」というAIエージェントの基本動作が確認できると思います。
動作原理
AIエージェントというと、巷では何か魔法のようなイメージを持たれていることもありますが、しかし先ほど見た通り実際の仕組みはとてもシンプルで次のような仕組みになっているだけです。
- LLMが出したタスクをツールが実行し、その結果をまたLLMに返す
- すべてのツール実行が終わるまでこのループを繰り返す
単発のツール実行で終わらず、目標達成まで自律的に複数のタスクを遂行できるのは 2 のループのおかげです。
つまりAIエージェントの本質はLLMとツールによるループであると言えます。
AIエージェントの本質はLLMとツールによるループである
このループがどのようにして生まれたのか、イメージしやすいように進化の流れを図にまとめました。
A. 従来のLLMチャット
まず、従来のLLMチャットはユーザーとLLMが1対1でやりとるするだけの構造でした。
ユーザーが質問を長えると、LLMが回答を返す、この往復のみです。
この形ではツールを呼ぶことはできないため、知識と行動の範囲はLLMの訓練データや補助のプロンプトに限定されます。
B. 単発のツール呼び出し
次の進化が、LLMが単発でツールを呼び出せる仕組みです。
例えば「天気予報を取得するツール」を登録すると、LLMはユーザーのリクエストをツールに振り分け、その結果をユーザーに返せるようになります。
この仕組みだけでも便利ですが、あくまで「1回だけツールを呼ぶ」にとどまります。
C. 複雑なタスクを遂行するループ
AIエージェントと呼ばれる仕組みでは、このツール呼び出しを複数回繰り返すループが可能になります。
例えば「旅行プランを作る」というタスクでは、
- LLMがツールを呼んで宿泊先を検索して、
- ツールを呼んで移動手段を確認して、
- ツールを呼んで料金を比較して、
- 最終的にユーザーへおすすめのプランを返す
といった複数のステップを自律的に進めます。
このループして考え続ける能力こそが、従来のチャットボットとAIエージェントの最大の違いです。
実際に先ほど作った Claude 3 Haiku と Calculator の例でも「Calculator ツールを2回呼んで結果を組み立てる」というループを自動的に回しています。
ループの内部構造
先ほどの「AIエージェントのループ」では、ユーザーからの入力が一度LLMに入り、その後は「LLM ⇄ ツール」の往復が自律的に続くという話をしました。
この動きの内部は、大きく以下の3ステップに分けられます。
- インテントの認識とプランニング
- ツール呼び出し
- 結果の取り込みと再プランニング
順番に見ていきます。
1. インテントの認識とプランニング
まず、ユーザーからテキストで指示が与えられると、LLMは何をやるべきかを自然言語で理解し、タスクをプランに分解します。
例えば「数字を半分にして袋に分ける」なら
- 「半分にする」という計算
- 「残りを袋に分ける」という計算
の2ステップに分ける必要があると推論します。
このフェーズでは「今何をすべきか?」を決めるだけで、実際の行動はまだ起こしません。
You: たろうくんはキャンディを24個持っています。
1. まず、そのうち半分を友だちにあげました。
2. 残りを4つずつ袋に分けました。
3. 1つの袋にはキャンディが何個入っていますか。
Agent: では、この問題を順に解いていきましょう。
2. ツール呼び出し
次に、LLMはプランに基づきどのツールを呼ぶか、どんな引数を渡すかを決めます。
例として「24 / 2 の計算を実行する」と判断した場合、次のような指示が生成されます。
※分かりやすくするため日本語で書いていますが、内部的にはJSON構造で指示が生成されます。
ツールを実行します
ツール名: calculator
引数: {"expression"=>"24 / 2"}
この指示を受けて、アプリケーション側では Calculator クラス(あるいはMCP、APIなど)を呼び出し、結果を取得します。
3. 結果の取り込みと再プランニング
ツールから帰ってきた結果は、再び LLM に入力として戻ります。
LLMは
- すでに得られた情報
- 次にやるべきこと
を踏まえ、プランの進捗を更新します。
必要なら次のツール呼び出しを決め、なければ最終回答を組み立ててユーザーに返します。
Agent: たろうくんが最初に友達にあげたキャンディは12個です。
ツールを実行します
ツール名: calculator
引数: {"expression"=>"12 / 4"}
Agent: 残りのキャンディを4つずつ袋に分けたので、1つの袋にはキャンディが3個入っています。
したがって、1つの袋にはキャンディが3個入っています。
以上のように「1. インテントの認識とプランニング → 2. ツール呼び出し → 3. 結果の取り込みと再プランニング」のサイクルが目的をタッセウするまで繰り返される仕組みです。
仕組みとしてはシンプルですが、実際に本格的なAIエージェントを作ってみるとツールの戻り値の形式が違ったり、LLMが引数やツールを誤解したり、途中でプロンプトが破綻するなど、色々な落とし穴があります。この辺りの内容については、また別のブログで解説します。
まとめ
今回は、AIエージェントの基本的な考え方と、Rubyで実際に動かす例を紹介しました。
- AIエージェントは「LLM+ツールのループ」で動く仕組み
- Rubyでも既存ライブラリを使えば簡単に試せる
- 仕組みを理解することでプロダクト実装の幅が広がる
次回のブログでは、実際にもう少し複雑なワークフローを組む例や、失敗しやすいポイントについても触れる予定です。