問題の概要:ローカルLLMでAgent機能を実装する際の課題
大規模言語モデル(LLM)を単なるチャットボットではなく、思考と行動を繰り返す自律的な「Agent」として活用したいと考えている開発者は増えています。特に、OpenAIのAPIに依存せず、ローカル環境で動作するLLM(Llama 3, Gemma, Mistralなど)を用いて、ReAct(Reasoning and Acting)やTool Use(関数呼び出し)機能を実装しようとすると、いくつかの典型的な問題に直面します。
具体的には、以下のようなエラーや期待しない動作が発生します:
- フォーマットエラー: LLMの出力が期待したJSONや特定の形式(例:
Thought:,Action:,Action Input:)に従わない。 - 無限ループ: Agentが同じ思考と行動を繰り返し、問題解決に進まない。
- ツール選択の誤り: 利用可能なツールの中から不適切なものを選択する、またはツールを全く使用しない。
- コンテキスト長超過: 思考、行動、観察のサイクルが長くなり、モデルのコンテキストウィンドウを超えてしまう。
- 性能不足: 小規模なローカルLLMでは、複雑な推論やツール使用の指示に従う能力が不足している。
# よくあるエラーメッセージ例
Error: LLM output is not a valid JSON. Received: "まず、天気を調べてみようと思います。"
Error: Maximum iteration (10) reached. Agent stuck in a loop.
Error: Tool 'get_stock_price' is not available. Available tools are: ['search_web', 'calculate'].
Warning: Context length exceeded. Truncating the oldest messages.
原因の解説:なぜローカルLLMでのAgent実装は難しいのか
これらの問題の根本原因は、主に以下の3点に集約されます。
1. 指示追従(Instruction Following)能力の差
GPT-4などの高性能なモデルは、複雑なプロンプト構造(「思考し、行動を選択し、特定のJSON形式で出力せよ」)を厳密に守ることができます。しかし、パラメータ数が少ないローカルLLMは、このような構造化された出力を生成するように微調整(Fine-tuning)されていない場合が多く、自由な形式の文章を出力してしまいます。
2. プロンプト設計の難しさ
Agentの動作はプロンプトに大きく依存します。利用可能なツールの説明、出力形式の指定、思考プロセスの促しなど、すべてを明確かつ過不足なく記述する必要があります。情報が不足すればモデルは迷い、冗長すぎればコンテキストを浪費し、重要な部分が押し出されてしまいます。
3. ツールと環境の統合
LLM自体は計算やAPI呼び出しができません。Python関数として実装されたツールを、LLMの出力に基づいて安全に実行し、その結果(Observation)を再度LLMにフィードバックするエンジン(Agent実行ループ)が必要です。この統合部分の実装不備がエラーを引き起こします。
解決方法:ステップバイステップ実装ガイド
ここでは、Pythonと言語モデルライブラリ(langchain、llama-index、または純粋なtransformers)を用いた実装手順を説明します。今回はシンプルさのために、langchain と Ollama(ローカルLLM実行環境)を使用する方法を紹介します。
ステップ1: 環境構築と必要なライブラリのインストール
まず、ベース環境を整えます。Ollamaをインストールし、使用するモデル(例: llama3)をダウンロードします。
# Ollamaのインストール(Macの場合。他OSは公式サイト参照)
brew install ollama
# モデルのダウンロードと起動
ollama pull llama3
ollama run llama3 & # バックグラウンドで実行
# Pythonライブラリのインストール
pip install langchain langchain-community langchain-core
ステップ2: ツールの定義
Agentが使用できるツールをPython関数として定義し、LangChainのツール形式にラップします。
from langchain.tools import tool
import requests
import json
@tool
def search_web(query: str) -> str:
"""ウェブ検索を行い、結果の要約を返します。実際の実装ではSerpAPI等を使用します。"""
# デモ用のモック応答
return f"「{query}」の検索結果: 今日の東京の天気は晴れ、最高気温25度です。"
@tool
def calculate(expression: str) -> str:
"""数式を計算して結果を返します。"""
try:
# 注意: evalはセキュリティリスクがあるため、実際のプロダクトではより安全な方法(ast.literal_eval等)を検討してください。
result = eval(expression)
return f"計算結果: {expression} = {result}"
except Exception as e:
return f"計算エラー: {e}"
# ツールのリストを作成
tools = [search_web, calculate]
ステップ3: プロンプトテンプレートの作成
ReAct形式を遵守するようモデルに指示する強力なプロンプトを作成します。ツールの説明と出力形式を明確に記載することが成功の鍵です。
from langchain.prompts import PromptTemplate
# ReActスタイルのプロンプトテンプレート
prompt_template = PromptTemplate.from_template(
"""あなたは思考と行動を繰り返してタスクを解決するアシスタントです。
以下のツールのみを使用できます:
{tools}
出力形式は以下のJSON形式で**厳密に**従ってください:
{{
"thought": "ここに現在の状況分析と次のステップの思考を書く",
"action": "ツール名({tool_names}のいずれか)",
"action_input": "ツールへの入力(該当する場合)"
}}
現在の質問: {input}
これまでの対話履歴: {agent_scratchpad}
では、始めてください。"""
)
ステップ4: LLMとAgentの初期化
Ollamaで動作するローカルLLMをLangChainに接続し、ツールとプロンプトを組み合わせてAgentを作成します。
from langchain.llms import Ollama
from langchain.agents import AgentExecutor, create_react_agent
# ローカルLLM(Ollama経由)の初期化
llm = Ollama(model="llama3", temperature=0) # temperatureは低めに設定して出力を安定させる
# ReAct Agentの作成
agent = create_react_agent(llm, tools, prompt_template)
# Agent実行エンジンの作成(最大反復回数を制限して無限ループを防止)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=5, handle_parsing_errors=True)
handle_parsing_errors=True は、モデルの出力のパース(JSONデコードなど)に失敗した際のエラーをキャッチし、モデルに再試行を促すための重要な設定です。
ステップ5: Agentの実行とデバッグ
実際にAgentを実行し、その思考過程を観察します。verbose=True に設定しているので、詳細なログが出力されます。
try:
response = agent_executor.invoke({
"input": "東京の今日の天気を調べて、その気温に15を足した結果を教えて。"
})
print("n=== 最終回答 ===")
print(response["output"])
except Exception as e:
print(f"エラーが発生しました: {type(e).__name__}: {e}")
成功した場合、以下のようなログ(思考過程)が出力されるはずです。
> Entering new AgentExecutor chain...
{
"thought": "ユーザーは東京の今日の天気と、その気温に15を足した値を求めている。まず天気情報を取得する必要がある。利用可能なツールでは 'search_web' が適切だ。",
"action": "search_web",
"action_input": "東京 今日 天気 気温"
}
観察: 「東京 今日 天気 気温」の検索結果: 今日の東京の天気は晴れ、最高気温25度です。
{
"thought": "観察から、最高気温は25度であることがわかった。次に、この25に15を足す計算を行う。利用可能なツールの 'calculate' が使える。",
"action": "calculate",
"action_input": "25 + 15"
}
観察: 計算結果: 25 + 15 = 40
{
"thought": "必要な情報が全て揃った。天気は晴れで最高気温25度、それに15を足すと40度になる。これをユーザーに伝えよう。",
"action": "Final Answer", # LangChainが最終段階で自動的に追加するアクション
"action_input": "東京の今日の天気は晴れ、最高気温は25度です。この気温に15を足すと40度になります。"
}
> Finished chain.
=== 最終回答 ===
東京の今日の天気は晴れ、最高気温は25度です。この気温に15を足すと40度になります。
コード例・コマンド例:トラブルシューティングの具体策
エラー1: 出力形式が不正(JSONパースエラー)
対策: プロンプトを強化し、出力例を示す。または、より指示追従能力の高いモデル(例: llama3:instruct, mistral, gemma:instruct)を試す。パースエラーハンドリングを有効にする。
# プロンプト改良例(出力例を追加)
prompt_template = PromptTemplate.from_template(
"""... 前述のプロンプト ...
出力形式は以下のJSON形式で**厳密に**従ってください。例:
{{"thought": "ユーザーの要求を分析する", "action": "search_web", "action_input": "検索クエリ"}}
..."""
)
# AgentExecutorのパースエラーハンドリングを確実に有効化
agent_executor = AgentExecutor(..., handle_parsing_errors=True)
エラー2: 無限ループ
対策: max_iterations パラメータで明確に上限を設定する(例: 5〜10回)。プロンプトに「最終答えが得られたら、Final Answerアクションを使用せよ」と明記する。
agent_executor = AgentExecutor(..., max_iterations=5, early_stopping_method="generate")
エラー3: ツールを選択しない/誤選択
対策: ツールの説明(docstring)をより具体的で区別しやすいものにする。プロンプトで「必ず提供されたツールのいずれかを使用すること」と強調する。
@tool
def search_web(query: str) -> str:
"""**リアルタイム情報や一般的な知識が必要な時に使用**。例: 天気、ニュース、事実確認。"""
...
@tool
def calculate(expression: str) -> str:
"""**数値計算が必要な時にのみ使用**。例: 足し算、掛け算、数式の評価。"""
...
まとめ・補足情報
ローカルLLMで安定したAgent機能を実装するには、「モデル選定」「プロンプトエンジニアリング」「実行エンジンの堅牢な実装」の3つが重要です。小規模モデルを使用する場合は、複雑なタスクではなく、限定的で明確なツール使用に特化させることで成功率が上がります。
次のステップとして推奨される取り組み:
- モデルのファインチューニング: ReAct形式の出力を大量に生成したデータセットでモデルを微調整する。これが最も根本的な解決策ですが、コストがかかります。
- フレームワークの活用:
LangGraph(LangChainの新機能)やMicrosoft Autogenのように、より高度なエージェントワークフローを構築できるフレームワークを検討する。 - 出力の正規化: LLMの生出力を、正規表現や小規模なParser LLMを用いて強制的に所定の形式に変換する「出力パーサー」を導入する。
ローカル環境でのAgent開発は、APIコストがかからずデータプライバシーも守られるという大きな利点があります。今回紹介した基本構造とトラブルシューティングの知見を活かし、実用的なAIアシスタントの開発に挑戦してみてください。