1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI Agent入門 第3部:ツール統合とWebUI - 高度なAI Agentシステム構築

Posted at

概要

AI Agent開発シリーズの最終章として、計算、検索、分析を自動実行し、プロフェッショナルなWebUIで操作できる本格システムを構築します。

従来のチャットボットの限界を超え、「考えて、ツールを使って、問題を解決する」真の自律型エージェントの実装方法を解説します。

この記事で実装するもの

  • ✅ 高度な計算ツール(安全性考慮)
  • ✅ Web検索ツール(SerpAPI + フォールバック)
  • ✅ OpenAI Function Callingによるツール統合
  • ✅ Streamlit製プロフェッショナルWebUI

🛠️ ツール統合エージェントの実装

🧮 高度な計算ツールの実装

セキュリティを重視した安全な計算ツールを実装します:

# tools/calculator.py
# import文は省略(完全版はpontanuki.comで公開)

class CalculatorInput(BaseModel):
    """計算ツールの入力スキーマ"""
    expression: str = Field(
        description="数学式(例:2 + 3 * 4, sin(30), sqrt(16), log(100))"
    )

class AdvancedCalculator(BaseTool):
    """高度な計算機能を提供するツール
    
    基本的な四則演算から三角関数、対数、統計関数まで
    幅広い数学的計算をサポートします。
    セキュリティ対策として、安全な関数のみを許可します。
    """
    
    name = "calculator"
    description = """数学的計算を安全に実行します。

対応している計算:
- 基本演算: +, -, *, /, %, **(べき乗)
- 三角関数: sin, cos, tan(度数で入力)
- 逆三角関数: asin, acos, atan
- 対数: log(自然対数), log10(常用対数)
- 平方根: sqrt
- 指数関数: exp
- 絶対値: abs
- 円周率: pi
- ネイピア数: e

使用例:
- 基本計算: "2 + 3 * 4"
- 三角関数: "sin(30)" (30度のサイン)
- 対数: "log10(100)"
- 平方根: "sqrt(16)"
- 複合計算: "sin(30) + cos(60)"
"""
    
    args_schema: Type[BaseModel] = CalculatorInput
    
    def __init__(self):
        super().__init__()
        
        # 安全な数学関数のマッピング
        self.safe_functions = {
            # 基本数学関数
            'sin': lambda x: math.sin(math.radians(x)),  # 度数入力
            'cos': lambda x: math.cos(math.radians(x)),
            'tan': lambda x: math.tan(math.radians(x)),
            'asin': lambda x: math.degrees(math.asin(x)),  # 度数出力
            'acos': lambda x: math.degrees(math.acos(x)),
            'atan': lambda x: math.degrees(math.atan(x)),
            
            # 対数・指数関数
            'sqrt': math.sqrt,
            'log': math.log,      # 自然対数
            'log10': math.log10,  # 常用対数
            'log2': math.log2,    # 二進対数
            'exp': math.exp,
            
            # その他の関数
            'abs': abs,
            'round': round,
            'ceil': math.ceil,
            'floor': math.floor,
            'pow': pow,
            
            # 定数
            'pi': math.pi,
            'e': math.e,
        }
    
    def _run(self, expression: str) -> str:
        """計算を安全に実行"""
        try:
            # 入力の前処理
            expression = expression.strip()
            
            # 危険な文字列をチェック
            dangerous_patterns = [
                '__', 'import', 'exec', 'eval', 'open', 'file',
                'input', 'raw_input', 'compile', 'reload'
            ]
            
            for pattern in dangerous_patterns:
                if pattern in expression.lower():
                    return "エラー: 安全でない式が検出されました"
            
            # 数学関数の名前空間を設定
            namespace = {
                "__builtins__": {},  # 組み込み関数を無効化
                **self.safe_functions
            }
            
            # 安全な評価の実行
            result = eval(expression, namespace)
            
            # 結果の型チェックと整形
            if isinstance(result, (int, float)):
                # 無限大・NaNのチェック
                if math.isinf(result):
                    return "エラー: 結果が無限大です"
                elif math.isnan(result):
                    return "エラー: 結果が数値ではありません(NaN)"
                
                # 整数の場合は小数点を表示しない
                if isinstance(result, float) and result.is_integer():
                    return str(int(result))
                elif isinstance(result, float):
                    # 小数点以下10桁まで表示、末尾のゼロは削除
                    return f"{result:.10f}".rstrip('0').rstrip('.')
                else:
                    return str(result)
            else:
                return str(result)
                
        except ZeroDivisionError:
            return "エラー: ゼロ除算は実行できません"
        except ValueError as e:
            return f"エラー: 数学的エラー - {str(e)}"
        except OverflowError:
            return "エラー: 数値が大きすぎます"
        except SyntaxError:
            return "エラー: 数式の構文が正しくありません"
        except NameError as e:
            return f"エラー: 未定義の関数または変数 - {str(e)}"
        except Exception as e:
            return f"エラー: 計算できませんでした - {str(e)}"
    
    async def _arun(self, expression: str) -> str:
        """非同期版計算実行"""
        return self._run(expression)

🌐 Web検索ツールの実装

リアルタイム情報検索機能を提供するツールを実装します:

# tools/web_search.py
# import文は省略(完全版はpontanuki.comで公開)

class WebSearchInput(BaseModel):
    """Web検索ツールの入力スキーマ"""
    query: str = Field(description="検索したいキーワードや質問")
    num_results: int = Field(
        default=5, 
        description="取得する検索結果数(1-10)",
        ge=1,
        le=10
    )

class WebSearchTool(BaseTool):
    """Web検索機能を提供するツール
    
    SerpAPIまたはフォールバック手法を使用して
    インターネット上の最新情報を検索します。
    """
    
    name = "web_search"
    description = """インターネット上の最新情報を検索して取得します。

使用場面:
- 最新のニュースや情報
- 特定のトピックについての詳細情報
- 統計データや事実確認
- 技術的な情報やチュートリアル

使用例:
- "2024年 Python 最新トレンド"
- "ChatGPT 最新アップデート"
- "東京オリンピック 結果"
- "機械学習 入門 チュートリアル"

注意: 正確性は情報源に依存するため、重要な情報は複数のソースで確認することをお勧めします。
"""
    
    args_schema: Type[BaseModel] = WebSearchInput
    
    def __init__(self):
        super().__init__()
        self.serpapi_key = os.getenv("SERPAPI_API_KEY")
        self.fallback_enabled = True
        
        # リクエスト間の待機時間(レート制限対策)
        self.request_delay = 1.0
        self.last_request_time = 0
    
    def _wait_for_rate_limit(self):
        """レート制限のための待機"""
        current_time = time.time()
        time_since_last = current_time - self.last_request_time
        
        if time_since_last < self.request_delay:
            time.sleep(self.request_delay - time_since_last)
        
        self.last_request_time = time.time()
    
    def _run(self, query: str, num_results: int = 5) -> str:
        """Web検索を実行"""
        try:
            # レート制限対策
            self._wait_for_rate_limit()
            
            if self.serpapi_key:
                return self._search_with_serpapi(query, num_results)
            elif self.fallback_enabled:
                return self._search_with_fallback(query, num_results)
            else:
                return "エラー: 検索APIが設定されていません。SERPAPI_API_KEYを設定してください。"
                
        except Exception as e:
            return f"検索エラー: {str(e)}"
    
    def _search_with_serpapi(self, query: str, num_results: int) -> str:
        """SerpAPIを使用した本格的な検索"""
        try:
            url = "https://serpapi.com/search.json"
            params = {
                "engine": "google",
                "q": query,
                "api_key": self.serpapi_key,
                "num": min(num_results, 10),
                "hl": "ja",  # 日本語
                "gl": "jp",  # 日本の検索結果
                "safe": "active"  # セーフサーチ
            }
            
            response = requests.get(url, params=params, timeout=15)
            response.raise_for_status()
            
            data = response.json()
            
            # エラーチェック
            if "error" in data:
                return f"検索APIエラー: {data['error']}"
            
            organic_results = data.get("organic_results", [])
            
            if not organic_results:
                return f"{query}」に関する検索結果が見つかりませんでした。"
            
            # 結果の整形
            results = []
            for i, result in enumerate(organic_results[:num_results], 1):
                title = result.get("title", "タイトルなし")
                link = result.get("link", "")
                snippet = result.get("snippet", "説明なし")
                
                # 長すぎるスニペットは切り詰め
                if len(snippet) > 200:
                    snippet = snippet[:200] + "..."
                
                results.append(f"""
🔗 **{i}. {title}**
   URL: {link}
   📄 概要: {snippet}
""")
            
            search_info = data.get("search_information", {})
            total_results = search_info.get("total_results", "不明")
            
            header = f"{query}」の検索結果 (総件数: {total_results}):\n"
            return header + "\n".join(results)
            
        except requests.exceptions.Timeout:
            return "検索がタイムアウトしました。しばらくしてから再試行してください。"
        except requests.exceptions.RequestException as e:
            return f"検索リクエストエラー: {str(e)}"
        except Exception as e:
            return f"SerpAPI検索エラー: {str(e)}"

🤖 ツール統合エージェントの実装

計算ツールと検索ツールを統合した高度なエージェントを作成します:

# agents/tool_enhanced_agent.py
# import文は省略(完全版はpontanuki.comで公開)

class ToolEnhancedAgent:
    """ツール統合エージェント
    
    このクラスは複数のツールを統合し、問題解決に必要な
    ツールを自動選択して実行する高度なエージェントです。
    """
    
    def __init__(self, model_name: str = None, memory_window: int = 10):
        """ツール統合エージェントを初期化
        
        Args:
            model_name: 使用するOpenAIモデル名
            memory_window: メモリに保持する会話数
        """
        # 設定を取得
        model_config = settings.get_model_config(model_name)
        
        # LLMの初期化(ツール使用時は低Temperatureを推奨)
        self.llm = ChatOpenAI(
            model=model_config["model_name"],
            temperature=0.1,  # ツール使用時は一貫性を重視
            api_key=model_config["api_key"]
        )
        
        # ツールの初期化
        self.tools = self._initialize_tools()
        
        # メモリの設定
        self.memory = ConversationBufferWindowMemory(
            k=memory_window,
            return_messages=True,
            memory_key="chat_history"
        )
        
        # システムプロンプトの定義
        self.system_prompt = """あなたはツールを使いこなす高度なAIアシスタントです。
ユーザーの問題を解決するために、適切なツールを選択し、組み合わせて使用してください。

利用可能なツール:
1. calculator: 数学的計算(基本演算、三角関数、対数など)
2. web_search: インターネット検索(リアルタイム情報取得)

ツール使用のガイドライン:
- 計算が必要な場合は calculator を使用
- 最新情報や具体的な事実確認が必要な場合は web_search を使用
- 複数のツールが必要な場合は、論理的な順序で実行
- ツールの結果を基に、分かりやすい回答を提供
- ツールを使用しない場合でも、質の高い回答を心がける

重要な指針:
1. ユーザーの意図を正確に理解する
2. 必要に応じて複数のツールを組み合わせる
3. ツールの実行結果を適切に解釈・説明する
4. エラーが発生した場合は代替手段を提案する
5. 常に正確で有用な情報提供を目指す"""
        
        # プロンプトテンプレートの作成
        self.prompt = ChatPromptTemplate.from_messages([
            SystemMessage(content=self.system_prompt),
            MessagesPlaceholder(variable_name="chat_history"),
            ("user", "{input}"),
            MessagesPlaceholder(variable_name="agent_scratchpad")
        ])
        
        # OpenAI Tools Agentの作成
        self.agent = create_openai_tools_agent(
            llm=self.llm,
            tools=self.tools,
            prompt=self.prompt
        )
        
        # AgentExecutorの作成(メモリ付き)
        self.agent_executor = AgentExecutor(
            agent=self.agent,
            tools=self.tools,
            memory=self.memory,
            verbose=True,  # デバッグ用
            max_iterations=5,  # 無限ループ防止
            early_stopping_method="generate",
            handle_parsing_errors=True
        )
    
    def _initialize_tools(self) -> List:
        """利用可能なツールを初期化"""
        tools = []
        
        # 計算ツール
        calculator = AdvancedCalculator()
        tools.append(calculator)
        
        # Web検索ツール
        web_search = WebSearchTool()
        tools.append(web_search)
        
        return tools
    
    def chat_with_tools(self, user_input: str) -> str:
        """ツール統合チャット
        
        Args:
            user_input: ユーザーからの入力
            
        Returns:
            AI生成の回答(ツール実行結果を含む)
        """
        try:
            # エージェント実行
            response = self.agent_executor.invoke({
                "input": user_input,
                "chat_history": self.memory.chat_memory.messages
            })
            
            return response["output"]
            
        except Exception as e:
            error_msg = f"申し訳ありません。処理中にエラーが発生しました: {str(e)}"
            # エラーもメモリに記録
            self.memory.chat_memory.add_user_message(user_input)
            self.memory.chat_memory.add_ai_message(error_msg)
            return error_msg

🎨 StreamlitによるWebUI実装

作成したエージェントを使いやすいWebインターフェースで操作できるようにしましょう:

# ui/streamlit_app.py
# import文は省略(完全版はpontanuki.comで公開)

class StreamlitChatApp:
    """Streamlit チャットアプリケーション"""
    
    def __init__(self):
        self.setup_page_config()
        self.initialize_session_state()
    
    def setup_page_config(self):
        """ページ設定"""
        st.set_page_config(
            page_title="AI Agent Chat Interface",
            page_icon="🤖",
            layout="wide",
            initial_sidebar_state="expanded"
        )
        
        # カスタムCSS
        st.markdown("""
        <style>
        .main-header {
            font-size: 3rem;
            color: #1f77b4;
            text-align: center;
            margin-bottom: 2rem;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
        }
        
        .agent-info {
            background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 1rem;
            border-radius: 10px;
            margin-bottom: 1rem;
        }
        
        .chat-message {
            padding: 1rem;
            border-radius: 10px;
            margin: 0.5rem 0;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .user-message {
            background-color: #e3f2fd;
            border-left: 4px solid #2196f3;
            margin-left: 20px;
        }
        
        .ai-message {
            background-color: #f3e5f5;
            border-left: 4px solid #9c27b0;
            margin-right: 20px;
        }
        </style>
        """, unsafe_allow_html=True)
    
    def initialize_session_state(self):
        """セッション状態の初期化"""
        if "messages" not in st.session_state:
            st.session_state.messages = []
        
        if "agent_type" not in st.session_state:
            st.session_state.agent_type = "basic"
        
        if "agent" not in st.session_state:
            st.session_state.agent = None
        
        if "session_id" not in st.session_state:
            st.session_state.session_id = f"streamlit_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        
        if "settings_validated" not in st.session_state:
            st.session_state.settings_validated = False
    
    def render_sidebar(self):
        """サイドバーの描画"""
        with st.sidebar:
            st.title("🤖 AI Agent 設定")
            
            # エージェントタイプ選択
            st.subheader("エージェントタイプ")
            agent_type = st.selectbox(
                "使用するエージェント",
                ["basic", "memory", "tool_enhanced"],
                format_func=lambda x: {
                    "basic": "🤖 基本チャット",
                    "memory": "🧠 メモリ付きチャット", 
                    "tool_enhanced": "🔧 ツール統合チャット"
                }[x],
                key="agent_selector"
            )
            
            # エージェントタイプが変更された場合
            if agent_type != st.session_state.agent_type:
                st.session_state.agent_type = agent_type
                st.session_state.agent = None  # エージェントをリセット
                st.rerun()
            
            # モデル設定
            st.subheader("モデル設定")
            model_name = st.selectbox(
                "使用モデル",
                ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo-preview"],
                index=0
            )
            
            temperature = st.slider(
                "Temperature(創造性)",
                min_value=0.0,
                max_value=1.0,
                value=0.7,
                step=0.1
            )
            
            # メモリ設定(メモリエージェント用)
            if agent_type in ["memory", "tool_enhanced"]:
                st.subheader("メモリ設定")
                memory_window = st.slider(
                    "メモリウィンドウ",
                    min_value=5,
                    max_value=50,
                    value=10,
                    help="保持する会話数"
                )
            else:
                memory_window = 10
            
            # セッション管理
            st.subheader("セッション管理")
            st.text(f"セッションID: {st.session_state.session_id}")
            
            if st.button("🧹 チャット履歴をクリア"):
                st.session_state.messages = []
                if st.session_state.agent and hasattr(st.session_state.agent, 'clear_memory'):
                    st.session_state.agent.clear_memory()
                st.rerun()
            
            # ツール情報(ツール統合エージェント用)
            if agent_type == "tool_enhanced" and st.session_state.agent:
                st.subheader("🔧 利用可能なツール")
                tools = st.session_state.agent.get_available_tools()
                for tool in tools:
                    with st.expander(f"📋 {tool['name']}"):
                        st.write(tool['description'])
            
            return {
                "agent_type": agent_type,
                "model_name": model_name,
                "temperature": temperature,
                "memory_window": memory_window
            }
    
    def render_chat_interface(self, config: Dict[str, Any]):
        """チャットインターフェースの描画"""
        st.markdown('<h1 class="main-header">🤖 AI Agent Chat Interface</h1>', 
                   unsafe_allow_html=True)
        
        # エージェント情報表示
        agent_info = {
            "basic": "🤖 基本的な質問応答機能",
            "memory": "🧠 会話履歴を記憶して継続的な対話が可能",
            "tool_enhanced": "🔧 計算とWeb検索ツールを使用可能"
        }
        
        st.markdown(f'''
        <div class="agent-info">
            <h3>現在のエージェント</h3>
            <p>{agent_info[config["agent_type"]]}</p>
        </div>
        ''', unsafe_allow_html=True)
        
        # チャット履歴表示
        for message in st.session_state.messages:
            with st.chat_message(message["role"]):
                st.write(message["content"])
        
        # チャット入力
        if prompt := st.chat_input("メッセージを入力してください..."):
            # ユーザーメッセージを追加
            st.session_state.messages.append({"role": "user", "content": prompt})
            with st.chat_message("user"):
                st.write(prompt)
            
            # AI応答を生成
            with st.chat_message("assistant"):
                with st.spinner("考え中..."):
                    try:
                        if config["agent_type"] == "basic":
                            response = st.session_state.agent.chat(prompt)
                        elif config["agent_type"] == "memory":
                            response = st.session_state.agent.chat_with_memory(
                                prompt, 
                                st.session_state.session_id
                            )
                        elif config["agent_type"] == "tool_enhanced":
                            response = st.session_state.agent.chat_with_tools(prompt)
                        
                        st.write(response)
                        
                        # レスポンスを履歴に追加
                        st.session_state.messages.append({
                            "role": "assistant", 
                            "content": response
                        })
                        
                    except Exception as e:
                        error_msg = f"エラーが発生しました: {str(e)}"
                        st.error(error_msg)
                        st.session_state.messages.append({
                            "role": "assistant",
                            "content": error_msg
                        })
    
    def run(self):
        """アプリケーションの実行"""
        # 環境設定の検証
        if not self.validate_environment_ui():
            st.stop()
        
        # サイドバー設定
        config = self.render_sidebar()
        
        # エージェントの初期化
        if not self.create_agent(config):
            st.stop()
        
        # メインのチャットインターフェース
        self.render_chat_interface(config)
        
        # サンプルプロンプト
        self.render_sample_prompts()
        
        # フッター
        st.markdown("---")
        st.markdown("**AI Agent Chat Interface** - Built with Streamlit & LangChain")

🚀 実行とテスト

実際の使用例

ツール統合エージェント:

あなた: 2の10乗を計算してください
AI: 計算ツールを使用して2の10乗を求めます。

> Entering new AgentExecutor chain...
I need to calculate 2 to the power of 10.

Action: calculator
Action Input: 2 ** 10

Observation: 1024
Thought: I now know the final answer.

Final Answer: 2の10乗は1024です。

あなた: ChatGPTの最新情報を調べて
AI: Web検索ツールを使用してChatGPTの最新情報を調べます。

> Entering new AgentExecutor chain...
I need to search for the latest information about ChatGPT.

Action: web_search
Action Input: ChatGPT 最新情報 2024

Observation: 「ChatGPT 最新情報 2024」の検索結果 (総件数: 約2,340,000件):

🔗 **1. ChatGPT最新アップデート - 2024年3月版**
   URL: https://example.com/chatgpt-update-2024
   📄 概要: 2024年3月にリリースされたChatGPTの新機能について...

Thought: I have found recent information about ChatGPT updates.

Final Answer: ChatGPTの最新情報をお調べしました。2024年3月に大きなアップデートがあり、新機能として...

🌐 WebUIでの操作

StreamlitアプリケーションをWebブラウザで開くと:

  1. エージェント選択: サイドバーで3種類のエージェントから選択
  2. モデル設定: GPTモデルとTemperatureの調整
  3. チャットインターフェース: 直感的な対話画面
  4. ツール情報: 利用可能なツールの詳細表示
  5. サンプルプロンプト: ワンクリックでテスト可能

📈 次のステップと応用

🔮 さらなる機能拡張

1. カスタムツールの追加

# tools/custom_tools.py
class WeatherTool(BaseTool):
    name = "weather"
    description = "天気情報を取得"
    # 実装...

class EmailTool(BaseTool):
    name = "email_sender"
    description = "メール送信"
    # 実装...

2. より高度なメモリ

# 長期記憶、ベクターサーチなど
from langchain.memory import ConversationSummaryBufferMemory
from langchain.vectorstores import Chroma

3. マルチモーダル対応

# 画像、音声の処理
from langchain.schema import HumanMessage, AIMessage
from langchain.schema.messages import ImageMessage

🏢 実用的なビジネス応用

1. カスタマーサポート

  • FAQ検索機能
  • チケット管理連携
  • エスカレーション判定

2. データ分析アシスタント

  • データベース連携
  • グラフ生成
  • レポート自動作成

3. コンテンツ生成

  • SEO最適化
  • 多言語対応
  • ブランドガイドライン準拠

🎯 第3部のまとめ

📚 この記事で学んだこと

ツール開発面:

  • ✅ 安全な計算ツールの実装
  • ✅ Web検索APIの統合
  • ✅ セキュリティを考慮したコード実行

エージェント統合面:

  • ✅ OpenAI Function Callingの活用
  • ✅ 複数ツールの動的選択
  • ✅ エラーハンドリングと回復機能

WebUI開発面:

  • ✅ Streamlitによる本格的なWebアプリ開発
  • ✅ リアルタイム対話インターフェース
  • ✅ プロダクションレディな機能実装

システム設計面:

  • ✅ スケーラブルなアーキテクチャ
  • ✅ 設定管理とデプロイ対応
  • ✅ ユーザビリティとパフォーマンス最適化

🚀 シリーズ全体の成果物

AI Agent開発シリーズを通じて、以下が完成しました:

  1. 基本エージェント: 質問応答機能
  2. メモリエージェント: 会話履歴記憶機能
  3. ツール統合エージェント: 計算・検索機能
  4. WebUIシステム: プロフェッショナルなインターフェース
  5. データベース管理: 効率的なデータ永続化
  6. 設定管理システム: 環境変数の安全な管理

💡 重要なポイント

  1. 段階的な構築: 基礎→応用→実践の体系的アプローチ
  2. 実用性重視: 企業でも使える本格的な実装
  3. 拡張可能性: カスタマイズと機能追加の容易さ
  4. セキュリティ考慮: 安全性を重視した設計

このAI Agentシステムは、単なる学習教材ではなく、実際のビジネスで活用できる本格的なツールです。


🔗 完全なソースコードはpontanuki.comで公開中!

完全なソースコードと詳細な実装解説は、pontanuki.comで公開しています!

この記事では主要な実装部分のみを紹介しましたが、pontanuki.comでは以下も含めて公開中:

  • 完全なimport文とセットアップ手順
  • エラーハンドリングの詳細実装
  • プロジェクト構造と設定ファイル
  • テスト関数とデモコード
  • requirements.txtと環境設定
  • その他のユーティリティ関数

📖 AI Agent開発シリーズ(完結)

AI Agentの世界はまだ始まったばかりです。このシステムを基盤に、さらに高度な自動化と効率化を実現していきましょう!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?