前章では、AIエージェントが頭脳(LLM)・道具(ツール)・記憶(メモリ)・ループ(制御)の4要素でできていることを見ました。この章ではいよいよ手を動かします。ゴールはシンプルで、4要素を最小構成で組んだ「動く1体」を作り、実際に走らせて挙動を観察することです。派手な機能はまだ足しません。まずは「LLMに考えさせ、ツールを呼び、結果を戻し、また考えさせ、完了で止める」というエージェントの心臓部を、自分の手で回せるようにします。
「最小のエージェントを1体、自分で組んで動かせる」
最小のエージェントとは
「最小のエージェント」とは、前章の4要素をそれぞれ一番小さい形で組んだものです。豪華なメモリも、たくさんのツールも、複数のエージェントも要りません。次の表のように、まずは削れるだけ削った構成から始めます。ここが理解できれば、あとは各要素を太らせていくだけです。
Claudeを1モデル呼ぶだけ。プロンプトに「あなたは道具を使えるアシスタントです」と役割を1行書く。
ツールは1つだけ。検索でも計算でもよい。まずは動作が確実な単純な関数にする。
会話履歴の配列1本でよい。やりとりを順に積んでいくだけ。永続化はまだ考えない。
while文1つ。「ツール要求があれば実行、なければ完了」で回す。上限回数だけ決めておく。
💡 「1回のAPI呼び出し」との違いはループだけ。 LLMに1回聞いて答えをもらうだけなら、それは単なるAPI利用です。そこに「ツールを実行して結果を戻し、もう一度考えさせる」という繰り返しが加わった瞬間、エージェントになります。増えるのは実質このループ1つ。だからこそ、まずループを完全に理解するのが近道です。
エージェントループの中身
エージェントの心臓部は1本のループです。人間が「調べて・考えて・手を動かして・結果を見て、まだなら繰り返す」のと同じことを、プログラムで回します。まずは1周の流れを図で押さえましょう。
これまでの会話履歴とツール一覧をLLMに渡す。LLMは「答える」か「ツールを使う」かを決める。
LLMが「このツールをこの引数で使いたい」と返したら、こちらが実際にその関数を実行する。
実行結果(ツールの返り値)を会話履歴に追記し、再びLLMに渡す。ここでSTEP 1へ戻る。
LLMがツールを使わず最終回答を返したら、目的達成とみなしてループを抜ける。
ポイントは、STEP 1〜3をぐるぐる回り、STEP 4で初めて抜けるということです。LLMは1周では終わらないかもしれません。「まず検索して、その結果を見てからもう一度検索」のように、何周もして目的に近づきます。擬似コードで書くと驚くほど短いはずです。
# 最小のエージェントループ(擬似コード) messages = [ user("先月の平均気温を調べて要約して") ] while True: reply = llm.think(messages, tools=[search_tool]) # STEP1 考える if reply.wants_tool: # ツール要求あり? result = run_tool(reply.tool_name, reply.args) # STEP2 呼ぶ messages.append(assistant(reply)) messages.append(tool_result(result)) # STEP3 戻す else: return reply.text # STEP4 完了で止める
⚠️ 上限回数を必ず入れる。 上の擬似コードは説明のためwhile Trueにしていますが、本物では必ず「最大N周まで」の歯止めを入れます。LLMが延々とツールを呼び続ける無限ループは、実際によく起きる事故です。詳しくはつまずきポイントで扱います。
ツールを1つ与える
次に、LLMに道具を1つ持たせます。ここで大事なのは、LLMは自分でコードを実行できないということです。LLMができるのは「このツールを、この引数で使いたい」と言葉で要求するところまで。実際に関数を動かして結果を返すのは、いつもこちら(あなたのプログラム)の仕事です。
そのために、まずツール定義をLLMに渡します。ツール定義とは「このツールは何ができ、どんな引数を取るか」の説明書です。LLMはこれを読んで、いつ・どう使うかを判断します。
例:search_weather「地名と月を渡すと平均気温を返す」。説明文がLLMの判断材料になるので丁寧に書く。
どんな引数を、どの型で受け取るかをスキーマ(JSON形式の定義)で指定。例:city(文字列)とmonth(数値)。
実際に動くコード本体。LLMが返した引数を受け取り、結果を返す。ここはただの普通の関数でよい。
この3点セットのうち、①②をLLMに渡し、③は手元に置くのがコツです。LLMが「search_weather(city=\"パリ\", month=6)を使いたい」と要求してきたら、その名前と引数をこちらで受け取り、③の関数を呼び、返り値をツール結果として会話に戻します。擬似コードで見てみましょう。
# ① と ② … LLMに渡すツール定義 search_tool = { "name": "search_weather", "description": "都市と月を渡すと平均気温(℃)を返す", "input_schema": { "city": "string", "month": "number", }, } # ③ … 手元に置く実体。ただの関数 def run_tool(name, args): if name == "search_weather": return weather_db.lookup(args["city"], args["month"]) return "unknown tool"
📊 説明文はプロンプトの一部。 ツールのdescriptionは、LLMが「いつ使うか」を決める唯一の手がかりです。曖昧だと使ってくれなかったり、変な場面で使ったりします。「何を返すか・いつ使うべきか」を人に説明するつもりで書くのがコツ。ツール設計は次章のMCPでさらに深掘りします。
実装イメージ ― 手書きループとSDK
ここまでの部品を組み上げると、最初のエージェントが動きます。実装には大きく2通りあります。仕組みを学ぶなら手書き、実務で楽をするならSDK――どちらも中身は同じループです。まず違いを整理しましょう。
API呼び出しとツール実行を、自分のwhile文で回す。中で何が起きているかが全部見えるので学習に最適。制御も自由自在。
向く場面: 仕組みを理解したい/細かく制御したい。
Claude系SDKなどが用意するツール実行ループに、ツール定義と関数を登録して任せる。定型部分を書かずに済み、堅牢。
向く場面: 早く作りたい/本番の定型処理を任せたい。
A. 手書きループの全体像は、これまでの部品を素直につなぐだけです。会話履歴を持ち、LLMを呼び、ツール要求があれば実行して結果を戻し、なければ抜ける――それだけです。
# A. 手書きループ(擬似コード / Claudeのメッセージ風) messages = [ user(goal) ] for step in range(MAX_STEPS): # ← 上限で歯止め reply = client.messages.create( model="claude-…", messages=messages, tools=[search_tool], ) log(step, reply) # ← 観察のため必ず記録 if reply.stop_reason == "tool_use": out = run_tool(reply.tool_name, reply.tool_args) messages += [ assistant(reply), tool_result(out) ] else: break # 最終回答が出た → 終了
B. SDKのツールランナーを使うと、このforループそのものをライブラリが面倒みてくれます。あなたが書くのは「ツール定義」「実体の関数」「最初のゴール」だけ。ループ・履歴管理・ツール結果の差し戻しは内部で処理されます。
# B. SDKのツールランナーに任せる(擬似コード) runner = ToolRunner( model="claude-…", tools=[search_tool], # 定義 handlers={"search_weather": run_tool}, # 実体 ) answer = runner.run(goal) # ループは内部で回る
💡 正確なメソッド名は公式ドキュメントで。 ここでのclient.messages.createやToolRunnerは雰囲気を伝えるための擬似コードです。実際の引数名・戻り値の形・モデルIDは各社SDKで異なり、更新も頻繁なので、必ず公式ドキュメントで最新版を確認してください。まず概念を掴み、細部はドキュメントで埋める――これが遠回りしないやり方です。手順を追った入門はAIエージェントの作り方(入門)も参考に。
おすすめは「まずAで一度だけ手書きし、以降はBで楽をする」です。一度自分でループを回すと、SDKがブラックボックスでなくなり、詰まったときに中で何が起きているか想像できるようになります。
動かして観察する
組み上がったら走らせます。ここで大切なのは、ただ動かすのではなく「どこで何を考え、どこで失敗したか」を観察することです。エージェント開発は、この観察の質が成果を分けます。だから最初からログを仕込んでおきます。
最低限、1周ごとに次を記録しましょう。手書きループならlog(step, reply)の1行で足ります。
その周でLLMが「ツールを使う」と判断したか、最終回答にしたか。判断の分岐点。
ツール名と引数。ここが変だと結果も狂う。LLMの理解のズレが一番出やすい場所。
ツールの返り値。大きすぎ・空・エラーなど、次の判断を狂わせる材料がここに潜む。
実際に走らせると、面白い(そして厄介な)挙動が見えてきます。たとえば――LLMが必要ないのにツールを呼ぶ。逆に呼ぶべき場面で呼ばず、記憶だけで答えてしまう。引数の都市名を微妙に間違える。1回で済むはずが同じ検索を何度も繰り返す。こうした「判断のクセ」は、ログを見て初めて気づけます。
✅ ログは"高級な機能"ではなく最初から必須。 エージェントは中身が見えにくいぶん、ログがないと「なぜそう動いたか」がまったく分かりません。今はprint1行でも十分。この観察を体系化した評価と可観測性は第5章で本格的に扱います。まずは「見る癖」をここで付けておきましょう。
つまずきポイント
最小構成でも、動かすと必ずいくつかの壁にぶつかります。どれもエージェント開発の定番のつまずきで、これから章を追って解決していくものです。ここでは「正体」と「今できる応急処置」を押さえ、本格対処は先の章に預けます。
同じツールを延々と呼び続け、いつまでも完了しない。ゴールを達成したと判断できずに空回りする。
応急処置: 最大周回数の上限を必ず設ける。同じ引数の連続呼び出しを検知して止める。
ツールが巨大なJSONやページ全文を返し、それを丸ごと会話に戻して費用も遅延も膨らむ。
応急処置: ツール側で必要な項目だけに絞る/要約してから返す。全文を無加工で渡さない。
周を重ねるほど会話履歴が積み上がり、上限に近づいて遅く・高く・不安定になる。
応急処置: 古いやりとりを要約して圧縮する/不要なツール結果を捨てる。次章以降で本格対処。
📊 3つとも「文脈の設計」の問題。 無限ループも戻り値の肥大もコンテキスト膨張も、根っこは「LLMに何を、どれだけ渡すか」という文脈設計に行き着きます。この考え方はコンテキストエンジニアリングとして体系化されており、エージェント開発の背骨になります。まずは「渡しすぎない・溜めすぎない」を合言葉に。
- 最小のエージェント=4要素を一番小さく組んだもの。1回のAPI利用との違いはループだけ。
- ループは「考える→ツールを呼ぶ→結果を戻す→また考える→完了で止める」の1周を繰り返す。上限回数の歯止めは必須。
- ツールは定義(名前・説明・引数)をLLMに渡し、実体の関数は手元で実行する。LLMは要求するだけ。
- 実装は手書きループ(学習向き)とSDKのツールランナー(実務向き)の2通り。中身は同じ。
- 最初からログで観察し、無限ループ・戻り値肥大・コンテキスト膨張という定番のつまずきを見つける。
1体動かせたら、次は道具を本格的に増やす番です。次の第3章「MCPとツール接続」では、ツールを標準的なやり方(MCP)で外部サービスやデータへつなぎ、エージェントの手を一気に広げます。前章の全体像を確認したいときは第1章へ戻ってください。