「RAGを導入したのに、なぜかLLMが的外れな回答をする」「外部APIと連携させたら、ハルシネーションが頻発して困った」
多くのエンジニアがAIエージェント開発で直面するこの問題は、RAGと外部API連携の組み合わせ方に本質的な原因があります。単に情報を検索して渡すだけでは、LLMの持つ自律的な判断力と外部リソースを活用する力が引き出せません。
この記事では、RAGと外部API連携を組み合わせ、自律的にタスクを実行するAIエージェントの具体的な構築手順を解説します。LangChainやLangGraphを活用したハンズオン形式で、実用的なAIアプリケーション開発の第一歩を踏み出しましょう。
AIエージェント開発の現状とRAG・API連携の重要性
このセクションでは、AIエージェントの概念と、その能力を最大化するRAG・API連携の役割について解説します。
AIエージェントは、与えられた目標に対し、自律的に計画を立て、ツール(関数やAPI)を呼び出し、タスクを実行するシステムです。従来のRPAが定型業務の自動化に特化していたのに対し、AIエージェントは非定型かつ複雑なタスクにも対応できる点で大きく異なります。
しかし、LLM単体では「学習データにない最新情報」や「リアルタイムな外部データ」にアクセスできません。そこで重要になるのが、以下の2つの技術です。
- RAG (Retrieval-Augmented Generation): 外部のナレッジベースから関連情報を検索し、LLMに提示することで、ハルシネーション(誤情報生成)を抑制し、回答の正確性を向上させます。
- API連携: 外部サービス(天気予報、ECサイト、社内システムなど)のAPIを呼び出し、リアルタイムデータ取得やアクション実行を可能にします。これにより、AIエージェントの適用範囲が飛躍的に広がります。
この2つの技術を組み合わせることで、AIエージェントは「最新の情報を参照し、かつ外部システムを操作できる」ようになり、真に自律的なタスク実行が可能になるのです。
AIエージェントを構築するための主要フレームワークとLLM
このセクションでは、AIエージェント開発で利用される主要なフレームワークと、その基盤となるLLMについて解説します。
AIエージェントフレームワークの選択肢
AIエージェントの構築には、様々なフレームワークが存在します。それぞれの特徴を理解し、プロジェクトの要件に合ったものを選びましょう。
- LangGraph: LangChainの機能を拡張し、マルチエージェントワークフローを効率的に構築できるフレームワークです。状態管理や条件分岐を少ないコードで記述できるため、複雑なエージェント連携に向いています。
- Microsoft Agent Framework: Microsoftが提唱するエージェント開発フレームワークで、Semantic KernelやAutoGenの思想を取り入れています。Workflowベースの設計により、エージェント間の連携や実行状況のモニタリングが容易です。
- AutoGen (Microsoft): 複数のエージェントがそれぞれの役割(例:プログラマー、テスター)を分担し、協調してタスクを解決する「マルチエージェントシステム」の構築に優れています。
- CrewAI: AutoGenと同様にマルチエージェントシステムに特化しており、より直感的なAPIでエージェント間の連携を定義できます。
本記事では、柔軟なワークフロー構築が可能なLangGraphを基盤としたAIエージェント構築の考え方を中心に解説します。
基盤となる大規模言語モデル (LLM)
AIエージェントの推論能力は、基盤となるLLMの性能に大きく依存します。
- GPT-4o (OpenAI): 高い推論能力とマルチモーダル対応が特徴です。複雑なタスクや多様なデータ形式に対応する場合に適しています。
- Claude 3.5 Sonnet (Anthropic): OpenAIのモデルと並び、非常に高い推論能力を持つモデルです。特に長文読解や複雑な指示の理解に優れています。
- Gemini 1.5 Pro (Google): 大規模なコンテキストウィンドウと高い推論能力を併せ持ちます。動画や音声といったマルチモーダル入力にも対応可能です。
- Anthropic Claude 3 Haiku: 高速な応答性とコスト効率が求められる場合に適しています。
これらのモデルは、それぞれ得意な領域やコストが異なります。開発するAIエージェントの要件(応答速度、精度、予算など)に合わせて最適なモデルを選定することが重要です。
RAG(検索拡張生成)の仕組みと実装例
このセクションでは、RAGがどのように機能し、LLMの回答精度を向上させるのか、そしてその最小構成の実装例について解説します。
RAGの基本原理
RAGは、「検索 (Retrieval)」と「生成 (Generation)」を組み合わせることで、LLMが持つ既存の知識に加えて、外部の最新情報や専門知識を参照して回答を生成する技術です。
- 検索フェーズ: ユーザーのクエリが入力されると、まず外部のナレッジベース(ドキュメント、データベースなど)から関連性の高い情報を検索します。このとき、ベクトルデータベースや全文検索エンジン(例: Amazon Kendra)が利用されます。
- 生成フェーズ: 検索された情報(コンテキスト)がユーザーのクエリとともにLLMにプロンプトとして渡されます。LLMはこのコンテキストを基に回答を生成するため、ハルシネーションが抑制され、より正確で根拠のある回答が期待できます。
RAGの最小構成実装
LangChainとベクトルDB(例: Chroma DB)を用いることで、RAGの基本的なパイプラインを構築できます。
前提準備
pip install langchain_community langchain_openai chromadb tiktoken python-dotenv
.envファイルにOpenAI APIキーを設定します。
OPENAI_API_KEY="your_openai_api_key_here"
RAGのサンプルコード
以下のPythonコードは、LangChainとChromaDBを使ったRAGの最小構成です。
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
# 1. ドキュメントの読み込み
# 実際のアプリケーションでは、S3やDBなどからデータをロードします
# ここでは簡単なテキストファイルを作成して使用します
document_content = """
AIエージェントは、自律的にタスクを計画・実行するシステムです。
RAGは、検索拡張生成の略で、LLMが外部情報を参照して回答を生成する技術です。
API連携は、外部サービスとAIエージェントを繋ぎ、リアルタイムデータ取得やアクション実行を可能にします。
LangChainは、LLMアプリケーション開発のためのオーケストレーションフレームワークです。
LangGraphは、LangChainを拡張し、状態を持つマルチエージェントワークフローを構築できます。
"""
with open("agent_docs.txt", "w") as f:
f.write(document_content)
loader = TextLoader("agent_docs.txt")
documents = loader.load()
# 2. ドキュメントのチャンク分割
# LLMのコンテキストウィンドウに収まるように、ドキュメントを小さなチャンクに分割します
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = text_splitter.split_documents(documents)
# 3. 埋め込みベクトルの生成とベクトルストアへの保存
# OpenAIEmbeddingsを使って各チャンクのベクトル表現を生成し、ChromaDBに保存します
embedding_function = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(chunks, embedding_function)
# 4. リトリーバーの作成
# ベクトルストアから関連ドキュメントを検索するためのリトリーバーを設定します
retriever = vectorstore.as_retriever()
# 5. LLMの初期化
llm = ChatOpenAI(model="gpt-4o", temperature=0) # temperature=0で安定した出力を目指す
# 6. プロンプトテンプレートの定義
# 検索結果をLLMに渡すためのプロンプトを定義します
prompt = ChatPromptTemplate.from_messages([
("system", "あなたは役立つAIアシスタントです。以下の検索結果のみを用いて質問に答えてください。検索結果にない場合は「わかりません」と答えてください。\n\n検索結果:\n{context}"),
("user", "{question}")
])
# 7. RAGチェーンの構築
# LCEL (LangChain Expression Language) を使ってチェーンを構築します
def rag_chain(question: str):
# 検索フェーズ
retrieved_docs = retriever.invoke(question)
context = "\n".join([doc.page_content for doc in retrieved_docs])
# 生成フェーズ
formatted_prompt = prompt.format(context=context, question=question)
response = llm.invoke(formatted_prompt)
return response.content
# 8. 質問と回答の実行
question1 = "RAGとは何ですか?"
print(f"質問: {question1}")
print(f"回答: {rag_chain(question1)}\n")
question2 = "LangGraphは何のために使われますか?"
print(f"質問: {question2}")
print(f"回答: {rag_chain(question2)}\n")
question3 = "今日の天気は?" # 検索結果にない質問
print(f"質問: {question3}")
print(f"回答: {rag_chain(question3)}\n")
# 不要になったテキストファイルを削除
os.remove("agent_docs.txt")
このコードでは、以下のステップでRAGを実装しています。
- ドキュメントの読み込み: 外部情報源からテキストデータをロードします。
-
チャンク分割: 長いドキュメントを、LLMのコンテキストウィンドウに収まる適切なサイズのチャンクに分割します。
RecursiveCharacterTextSplitterは、意味のある区切りを保ちつつ分割するのに役立ちます。 - 埋め込みベクトルの生成: 各チャンクを数値ベクトル(埋め込み)に変換します。これにより、意味的に似たチャンクがベクトル空間上で近くに配置されます。
- ベクトルストアへの保存: 生成された埋め込みベクトルをベクトルデータベース(ChromaDB)に保存します。
- リトリーバーの作成: ユーザーの質問と最も関連性の高いチャンクをベクトルストアから検索するためのコンポーネントを準備します。
- プロンプトテンプレートの定義: LLMに渡すプロンプトを定義し、検索結果を埋め込むプレースホルダーを設定します。「検索結果のみを使って回答する」といった制約を設けることで、ハルシネーションを抑制します。
RAGのよくある落とし穴と回避策
RAGを導入しても、期待する精度が出ないことがあります。主な原因と回避策は以下の通りです。
-
検索精度の低さ:
- 落とし穴: 埋め込みモデルの選定ミス、検索アルゴリズムの最適化不足、データのフォーマット不備。
-
回避策:
-
埋め込みモデルの選択:
text-embedding-ada-002(OpenAI)などの高性能モデルや、タスクに特化したモデルを選定します。 - クエリ拡張: ユーザーの質問をLLMで拡張し、より多くの関連情報を検索できるようにします。
- 再ランキング: 検索で得られた上位N件のドキュメントを、別のLLMやモデルで再度スコアリングし、最も関連性の高いものを選択します。
-
チャンクの適切な分割: LangChainの
TextSplittersツールなどを活用し、意味のあるまとまりでチャンクを分割します。チャンクが長すぎるとノイズが多くなり、短すぎると文脈が失われます。
-
埋め込みモデルの選択:
-
ハルシネーションの発生:
- 落とし穴: 検索結果が不十分な場合や、LLMが検索結果以外の情報を補完しようとする。
-
回避策:
- プロンプトによる制約: 「検索結果のみを使って回答する」「わからない場合は「わかりません」と答える」といった明確な指示をプロンプトに含めます。
- データの品質向上: ナレッジベースの情報を常に最新かつ正確に保ち、網羅性を高めます。
外部API連携によるAIエージェントの拡張
このセクションでは、AIエージェントが外部APIを利用して、リアルタイムな情報取得やアクション実行を行う方法について解説します。
API連携の基本とPythonでの実装
AIエージェントが外部APIを利用するには、HTTPリクエストを送信する機能が必要です。Pythonではrequestsライブラリが標準的に使われます。
Python requestsライブラリを使ったAPI呼び出し
import requests
import os
from dotenv import load_dotenv
load_dotenv()
# APIキーは環境変数から取得することを推奨
# .envファイルに YOUR_API_KEY="your_actual_api_key" のように記述
API_KEY = os.getenv("YOUR_API_KEY")
BASE_URL = "https://api.example.com" # 連携したいAPIのベースURLを設定
# GETリクエストの例
# 例: ユーザー情報を取得するAPI
try:
# paramsにAPIキーを含める場合 (APIによって異なる)
response = requests.get(f"{BASE_URL}/users/123", params={"api_key": API_KEY})
response.raise_for_status() # HTTPエラーが発生した場合に例外を発生させる
user_data = response.json()
print(f"GET Status Code: {response.status_code}")
print(f"GET User Data: {user_data}")
except requests.exceptions.RequestException as e:
print(f"GET Request Error: {e}")
# POSTリクエストの例 (bodyにJSONデータを設定)
# 例: 新しいリソースを作成するAPI
try:
headers = {'Content-Type': 'application/json'}
# APIキーをヘッダーに含める場合 (例: Authorizationヘッダー)
# headers['Authorization'] = f'Bearer {API_KEY}'
payload = {'name': 'New Item', 'description': 'This is a new item created by AI.'}
response = requests.post(f"{BASE_URL}/items", json=payload, headers=headers)
response.raise_for_status() # HTTPエラーが発生した場合に例外を発生させる
print(f"POST Status Code: {response.status_code}")
print(f"POST Response Data: {response.json()}")
except requests.exceptions.RequestException as e:
print(f"POST Request Error: {e}")
# requestsライブラリは `pip install requests python-dotenv` でインストールできます。
このコードでは、以下の点に注意してください。
-
APIキーの管理: APIキーは環境変数(
.envファイルとpython-dotenv)で管理し、コードに直書きしないようにします。 -
エラーハンドリング:
response.raise_for_status()を使ってHTTPステータスコードを確認し、エラーが発生した場合は適切に例外処理を行います。 -
ヘッダーとペイロード: APIの仕様に応じて、
headersやjson(POSTの場合)を設定します。
AIエージェントにおけるAPI利用の設計
AIエージェントがAPIを「ツール」として利用する場合、LLMがどのAPIを、どのような引数で呼び出すべきかを判断できるようにする必要があります。これは、LangChainのToolやFunction Callingの機能を使って実現できます。
LangChain Toolsの例
from langchain.tools import tool
import requests
# 外部APIを呼び出す関数をツールとして定義
@tool
def get_current_weather(location: str) -> str:
"""指定された都市の現在の天気情報を取得します。"""
try:
# 実際にはAPIキーやエラーハンドリングを適切に実装
response = requests.get(f"http://api.weatherapi.com/v1/current.json?key=YOUR_WEATHER_API_KEY&q={location}")
response.raise_for_status()
data = response.json()
return f"{location}の現在の天気は{data['current']['condition']['text']}、気温は{data['current']['temp_c']}℃です。"
except requests.exceptions.RequestException as e:
return f"天気情報の取得に失敗しました: {e}"
# ツールリストに登録
tools = [get_current_weather]
# LLMにツールを使わせるプロンプトの例(概念)
# from langchain_openai import ChatOpenAI
# from langchain.agents import create_tool_calling_agent, AgentExecutor
#
# llm = ChatOpenAI(model="gpt-4o", temperature=0)
# prompt = ChatPromptTemplate.from_messages([
# ("system", "あなたはツールを使ってユーザーの質問に答えるAIアシスタントです。"),
# ("user", "{input}"),
# MessagesPlaceholder(variable_name="agent_scratchpad"),
# ])
#
# agent = create_tool_calling_agent(llm, tools, prompt)
# agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
#
# # 実行例
# # agent_executor.invoke({"input": "東京の現在の天気は?"})
この例では、@toolデコレータを使ってPython関数をLangChainのツールとして登録しています。LLMはプロンプトと利用可能なツールリストを与えられることで、ユーザーの質問に応じて適切なツールを選択し、引数を生成して呼び出すことができます。
API連携のよくある落とし穴と回避策
API連携は、外部サービスに依存するため、特有の課題があります。
-
外部サービス依存とトラブル:
- 落とし穴: 連携先のサーバー障害、サービス停止、仕様変更が自社システムに影響を与える。
-
回避策:
- 利用条件の事前把握: 料金体系、リクエスト制限、データ転送量を確認します。
- エラーハンドリングとリトライ処理: ネットワークエラーやAPI側のエラーに備え、適切なエラー処理とリトライメカニズムを実装します。
- キャッシュの利用: 頻繁に変わらないデータはキャッシュすることで、API呼び出し回数を減らし、可用性を向上させます。
- 信頼できる提供元の選択: Google, Amazon, Microsoftなど、信頼性の高い企業が提供するAPIを利用するのが望ましいです。
-
セキュリティリスク:
- 落とし穴: APIキーの漏洩、不正アクセス。
-
回避策:
- APIキーの厳重な管理: 環境変数やシークレット管理サービス(AWS Secrets Manager, Azure Key Vaultなど)を利用し、コードに直書きしない。
- OAuth 2.0などの認証・認可: 適切な認証・認可フローを導入し、最小権限の原則に従います。
- API Gatewayの活用: API Gatewayで認証、認可、レートリミットなどを一元的に管理し、セキュリティを強化します。
-
メンテナンス工数の考慮:
- 落とし穴: 連携先APIのアップデートに合わせて、接続プログラムの改修が必要になる。
-
回避策:
- バージョニング戦略の策定: 連携先APIのバージョニングポリシーを理解し、自社システムもそれに合わせてバージョニング戦略を立てます。
- 自動テストの導入: API連携部分のテストを自動化し、仕様変更時の影響を早期に検知できるようにします。
RAGとAPI連携を組み合わせたAIエージェントの構築手順
このセクションでは、RAGとAPI連携を統合したAIエージェントの具体的な構築手順を、LangGraphの概念を交えて解説します。
AIエージェントのワークフロー設計
RAGとAPI連携を組み合わせる場合、AIエージェントは以下のようなワークフローで動作します。
- ユーザーからの質問受付: AIエージェントがユーザーからの質問を受け取ります。
-
タスク計画: LLMが質問の内容を分析し、RAGによる情報検索が必要か、外部API呼び出しが必要か、あるいは両方が必要かを判断します。
- 例:「〇〇について教えて」→ RAGで情報検索
- 例:「〇〇の現在の天気は?」→ 天気APIを呼び出し
- 例:「〇〇の現在の天気と、その歴史について教えて」→ 天気APIとRAGを両方利用
-
ツール実行(RAGまたはAPI呼び出し):
- RAGの場合: ナレッジベースから関連情報を検索し、結果を取得します。
- API連携の場合: 適切なAPIを呼び出し、結果を取得します。
- 結果の統合と回答生成: 検索結果やAPIの実行結果をLLMに渡し、ユーザーへの最終的な回答を生成します。
- フィードバックと改善: ユーザーからのフィードバックや、エージェントの実行ログを分析し、エージェントの性能を継続的に改善します。
LangGraphによるワークフロー定義の概念
LangGraphは、LLMアプリケーションを「グラフ」として定義することで、複雑な状態遷移やエージェント間の連携を管理しやすくします。
LangGraphの主要な概念
- ノード (Node): グラフの各ステップを表します。これはLLM呼び出し、RAG検索、API呼び出しなどの処理単位です。
- エッジ (Edge): ノード間の接続を表します。条件付きエッジを使うことで、前のノードの出力に基づいて次に実行するノードを動的に決定できます。
- 状態 (State): ワークフロー全体で共有されるデータです。ユーザーの質問、LLMの思考過程、ツールの実行結果などが含まれます。
LangGraphのコード例(概念的な説明)
# from langgraph.graph import StateGraph, END
# from langchain_core.messages import HumanMessage
# from langchain_openai import ChatOpenAI
# from langchain.tools import tool
#
# # ツール定義 (上記参照)
# @tool
# def get_current_weather(location: str) -> str:
# """指定された都市の現在の天気情報を取得します。"""
# return f"天気情報 for {location}" # 簡略化
#
# # RAG検索ツール (簡略化)
# @tool
# def search_knowledge_base(query: str) -> str:
# """ナレッジベースから関連情報を検索します。"""
# return f"RAG検索結果 for {query}" # 簡略化
#
# tools = [get_current_weather, search_knowledge_base]
# llm = ChatOpenAI(model="gpt-4o", temperature=0)
#
# # 状態定義
# class AgentState(TypedDict):
# messages: Annotated[list, operator.add]
#
# # ノード定義
# def call_llm(state: AgentState):
# # LLMが次に何をするか (ツール呼び出し、または最終回答) を判断
# # Function Callingを使ってツールを呼び出すか、直接回答を生成
# messages = state["messages"]
# response = llm.invoke(messages + tools) # ツール情報もLLMに渡す
# return {"messages": [response]}
#
# def call_tool(state: AgentState):
# # LLMが選択したツールを実行
# last_message = state["messages"][-1]
# tool_calls = last_message.tool_calls
# tool_output = []
# for tool_call in tool_calls:
# tool_name = tool_call["name"]
# tool_args = tool_call["args"]
# # ツールを実行し、結果を収集
# if tool_name == "get_current_weather":
# output = get_current_weather.invoke(tool_args)
# elif tool_name == "search_knowledge_base":
# output = search_knowledge_base.invoke(tool_args)
# tool_output.append(ToolMessage(content=output, tool_call_id=tool_call["id"]))
# return {"messages": tool_output}
#
# # 条件付きエッジの関数
# def should_continue(state: AgentState):
# last_message = state["messages"][-1]
# if last_message.tool_calls:
# return "continue" # ツール呼び出しがあれば続行
# else:
# return "end" # なければ終了 (最終回答)
#
# # グラフの構築
# workflow = StateGraph(AgentState)
# workflow.add_node("llm", call_llm)
# workflow.add_node("tool", call_tool)
#
# workflow.set_entry_point("llm")
# workflow.add_conditional_edges(
# "llm",
# should_continue,
# {
# "continue": "tool",
# "end": END
# }
# )
# workflow.add_edge("tool", "llm") # ツール実行後、再度LLMに判断させる
#
# app = workflow.compile()
#
# # 実行例 (概念)
# # app.invoke({"messages": [HumanMessage(content="東京の天気とRAGについて教えて")]})
このLangGraphの概念的なコードは、LLMが「思考」し(call_llmノード)、必要に応じて「ツールを実行」し(call_toolノード)、その結果を基に再度「思考」するという、エージェントの自律的なループを表しています。should_continue関数で、LLMがツールを呼び出すか、それとも最終的な回答を生成するかを判断し、グラフの実行パスを決定します。
設計上のトレードオフとベストプラクティス
AIエージェント開発のトレードオフ:
- 賢さ(精度・判断力) vs 速さ(応答速度) vs コスパ(コスト): 高性能なLLMは賢いですが、応答が遅くコストも高くなりがちです。ビジネス要件に合わせて、これらのバランスを最適化する必要があります。
- 完全自動化の難しさ: エージェント間の連携で誤りが連鎖的に増加するリスクや、重要なタスクには人間のチェックが必要な現状を理解し、適切なヒューマン・イン・ザ・ループの仕組みを導入します。
RAGのベストプラクティス:
- データの品質と構造化: ナレッジベースの品質と明確さを向上させ、ドキュメントを見出しやサブ見出しで適切に構造化することで、LLMが関連情報にアクセスしやすくなります。
- 検索クエリの最適化: クエリ拡張や再ランキングなどを用いて、検索精度を最大化します。
- 継続的なテストと調整: 小さく試して改善を重ねることで、より正確な回答につながります。
API設計のベストプラクティス:
- RESTful APIの原則: 一貫性のあるAPI設計を心がけ、OpenAPI/Swaggerでドキュメントを明確に記述します。
- セキュリティバイデザイン: 認証、認可、アクセス制御、暗号化など、すべてのAPIに求めるセキュリティ要件を明確にし、設計段階から組み込みます。
- API Gatewayの活用: APIのエンドポイントを一元管理し、セキュリティ、パフォーマンス、モニタリングを向上させます。
- SDKの提供: 開発者がAPIを簡単に利用できるよう、SDK(ソフトウェア開発キット)を提供することも検討します。
まとめと次の一歩
この記事では、RAGと外部API連携を組み合わせたAIエージェントの構築について解説しました。
- AIエージェントは、RAGによる情報検索とAPI連携による外部システム操作を組み合わせることで、自律的なタスク実行能力を向上させます。
- RAGの実装にはLangChainとベクトルデータベースが、API連携にはPythonの
requestsライブラリやLangChainのTool機能が有効です。 - LangGraphのようなフレームワークは、複雑なAIエージェントのワークフローを効率的に定義し、管理するのに役立ちます。
- 開発においては、検索精度の低さ、ハルシネーション、外部サービス依存といった課題に対し、適切な回避策と設計上のベストプラクティスを適用することが成功の鍵となります。
AIエージェントは、2025年以降もマルチエージェントシステムやAgentic RAG、GUI操作連携など、さらなる進化が期待される分野です。本記事で紹介した内容を足がかりに、ぜひご自身のAIエージェント開発プロジェクトに挑戦してみてください。
次の一歩として、LangGraphの公式ドキュメントを参照し、より複雑なエージェントワークフローの構築に挑戦することをお勧めします。