問題の概要:ローカルLLMでAgent機能を実装する際の課題
大規模言語モデル(LLM)を単なるチャットボットではなく、外部ツール(検索、計算、API呼び出しなど)を活用して自律的に問題解決を行う「Agent」として活用したいと考えている開発者は多いでしょう。特に、クラウドAPIに依存しないローカル環境(Ollama, llama.cpp, vLLMなど)でAgent機能を実装しようとすると、以下のような典型的な課題に直面します。
- エラー例1: 構造化出力のパース失敗
Error: Failed to parse LLM output. Expected JSON with 'thought', 'action', 'action_input' keys, but got: "まずユーザーの質問を理解します。東京の天気を知りたいようです。天気APIを呼び出す必要があります。" - エラー例2: 無限ループまたは無意味なアクションの繰り返し
Agent entered infinite loop: [Thought] 計算します -> [Action] calculator -> [Action Input] 1+1 -> [Observation] 2 -> [Thought] 計算します -> ... - エラー例3: ツール選択の誤り
「最新のニュースを検索して」という質問に対して、計算機ツールを選択してしまう。 - 課題: ReActフレームワークのプロンプト設計が難しい
思考(Thought)、行動(Action)、観察(Observation)のサイクルを適切に回すための指示(プロンプト)をLLMに理解させられない。
原因の解説:なぜローカルLLMでのAgent実装は難しいのか
これらの問題の根本原因は、主に以下の3点に集約されます。
1. 出力フォーマットの制御難易度
GPT-4などの高性能なクラウドLLMは、細かな指示に従って厳密なJSON形式で出力することが比較的得意です。しかし、パラメータ数が少ないローカルLLM(7B, 13Bモデルなど)は、プロンプトで指定した出力形式を無視し、自由な自然言語で応答してしまう傾向が強く、プログラムで次のアクションをパース(解析)できなくなります。
2. 推論能力(Reasoning)の限界
ReAct(Reasoning + Acting)の核心は、モデルが自身の「思考過程」を言語化することです。小規模なローカルLLMは、複数ステップにわたる推論や、過去の観察結果を次の思考に活かす「メタ認知」が苦手な場合があり、浅い思考で不適切なツールを選択したり、同じ行動を繰り返したりします。
3. ツール定義とその理解の齟齬
開発者が提供するツールの説明(名前、機能、引数の形式)と、LLMが持つ世界知識との間にギャップがあります。例えば「web_search(query)」というツールを定義しても、モデルが「ネットで調べる」という概念とこの関数呼び出しを結びつけられないことがあります。
解決方法:ステップバイステップ実装ガイド
ここでは、Pythonと人気のローカルLLM実行環境Ollamaを使用した、実践的なAgent実装の手順を説明します。ライブラリにはシンプルな構成のLangChainを利用します。
ステップ1: 環境構築と必要ライブラリのインストール
# 必要なパッケージのインストール
pip install langchain langchain-community ollama
# Ollamaのセットアップとモデルダウンロード(例: Llama 3.1 8B)
# ターミナルで別途実行
ollama pull llama3.1:8b
ステップ2: カスタムツールの定義
LangChainの@toolデコレータを使用して、Agentが使えるツールを定義します。説明文(docstring)は非常に重要です。
from langchain.tools import tool
import requests
import json
@tool
def get_weather(city: str) -> str:
"""指定された都市の現在の天気予報を取得します。引数は都市名(日本語または英語)です。"""
# 注: これはダミーAPIです。実際にはOpenWeatherMap等のAPIキーが必要です。
try:
# 模擬応答
return f"{city}の天気は晴れ、気温は22度です。"
except Exception as e:
return f"天気情報の取得に失敗しました: {e}"
@tool
def calculator(expression: str) -> str:
"""数式を計算します。例: '1 + 2 * 3'。"""
try:
# 安全のため、evalの代わりに限定的な評価を実装すべきです。
result = eval(expression, {"__builtins__": None}, {})
return f"計算結果: {result}"
except Exception as e:
return f"計算に失敗しました: {e}"
# ツールをリスト化
tools = [get_weather, calculator]
ステップ3: ローカルLLMのセットアップとプロンプトテンプレートの強化
キモとなるプロンプトテンプレートを作成し、出力形式を強く指示します。ChatPromptTemplateとMessagesPlaceholderを活用します。
from langchain.llms import Ollama
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import AgentExecutor, create_react_agent
llm = Ollama(model="llama3.1:8b", temperature=0) # temperatureは低めに設定
# ReAct形式を強制するための強力なプロンプトテンプレート
prompt = ChatPromptTemplate.from_messages([
("system", """あなたは優秀なアシスタントです。以下のルールを厳守してください:
1. ユーザーの質問を解決するために、**必ず**以下のフォーマットで考えて答えてください:
Thought: ここに次の行動を決定するための理由を書く
Action: 使用するツール名({tool_names}のいずれか)
Action Input: ツールへの入力(シンプルな文字列)
2. 最終答えがわかったら、必ず以下のフォーマットで終了してください:
Thought: これでユーザーの質問に答えられる
Final Answer: ここに最終的な答えを書く
3. ツールの説明をよく読み、正しいツールを選択してください。
利用可能なツール:
{tools}
"""),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
ステップ4: AgentとExecutorの作成、実行
# ReActエージェントの作成
agent = create_react_agent(llm, tools, prompt)
# エージェント実行器の作成(最大ループ数を制限して無限ループを防止)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 思考過程を表示
handle_parsing_errors=True, # パースエラーをハンドリング
max_iterations=5, # 最大反復回数を制限
early_stopping_method="generate" # 最終答えが出たら停止
)
# エージェントの実行例
try:
result = agent_executor.invoke({
"input": "東京の天気を教えて、それに今日の日付(2024年8月10日)を足したら何日になるかも計算して。"
})
print(result["output"])
except Exception as e:
print(f"エージェント実行中にエラーが発生しました: {e}")
コード例・コマンド例:よくあるエラーとその対処法
ケース1: パースエラーへの対応
OutputParserExceptionが発生した場合、handle_parsing_errorsパラメータでフォールバック処理を行います。より強固な方法として、カスタム出力パーサーを実装することも可能です。
# AgentExecutorの設定でパースエラーをキャッチ
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
handle_parsing_errors=True, # これをTrueに設定
max_iterations=5,
verbose=True
)
# あるいは、カスタムのエラーハンドリング関数を定義
def parsing_error_handler(error):
return f"エージェントの出力を解析できませんでした。もう一度考え直してください。エラー内容: {error}"
# プロンプトのシステムメッセージに、より具体的な指示を追加
# "出力は必ず'Thought:', 'Action:', 'Action Input:'という**正確なラベル**で始めてください"
ケース2: モデル性能による制限と対策
小規模モデル(7B/8B)で動作が不安定な場合、以下の対策が有効です。
# 1. より指示追従に特化したモデルを選択
ollama pull mistral:7b-instruct
ollama pull llama3.1:8b-instruct # instruct版は指示に従いやすい
# 2. LLMの呼び出しパラメータを調整
llm = Ollama(
model="llama3.1:8b",
temperature=0.1, # 創造性を低くし、一貫性を向上
top_p=0.9,
num_predict=512, # 出力トークン数を制限
repeat_penalty=1.1 # 繰り返しを抑制
)
# 3. ツールの説明を極限までシンプルかつ具体的にする
@tool
def search(query: str):
"""ウェブ検索。キーワードを入力。例: 'query=OpenAI最新発表'"""
まとめ・補足情報
ローカルLLMで安定したAgent機能を実装するには、「モデルの能力を過信しないプロンプト設計」と「堅牢なエラーハンドリング」が最も重要です。高性能なクラウドAPIのような挙動は期待せず、モデルの限界を理解した上で、以下のベストプラクティスを実践しましょう。
- プロンプトは具体的に、繰り返し指示する: 出力形式は1箇所ではなく複数箇所で指示し、例を示す。
- ツールは少なく、説明は簡潔に始める: 最初は2〜3個のツールから実装し、モデルが扱えることを確認してから増やす。
- 反復回数に厳格な上限を設ける:
max_iterationsは必須設定です。5〜10回程度が目安。 - より高機能なフレームワークの検討: LangChain以外にも、AutoGen(マルチエージェントに強い)や、LlamaIndex(データ連携に強い)などの選択肢があります。実装が進んだらこれらの利用も検討すると良いでしょう。
- モデル選定: Agent用途では、
llama3.1:8b-instruct,mistral:7b-instruct,qwen2.5:7b-instructなど、指示追従(Instruction Following)に優れたモデルを選ぶことが近道です。
ローカル環境でのAgent開発は、クラウド依存からの脱却とデータプライバシーの確保という大きなメリットがあります。初めは思うように動作せず挫折しそうになるかもしれませんが、モデルとツール、プロンプトを少しずつ調整していくことで、実用的な自律型アシスタントを構築できるでしょう。