Mark Ku's Blog
在 ChatGPT 開啟在 Claude 開啟

利用 Langchain AgentExecutor 整合自訂工具與 SerpAPI,並透過 Redis 實現 AI Agent 的記憶功能,打造具狀態的對話式 Bot。此方法能讓 AI 動態選擇工具並回應,同時儲存對話歷程。

Podcast 對話本文 AI 對話朗讀版
本文音訊由 VoAI 提供技術支援VoAI 絕好聲創

前言

先前有試著將 Python Langchain 換成 Node 版,但 Langchain node 版的文件真的有點少,因此,轉換起來有點辛苦,因為常常就算有文件,還是漏了參數,程式就炸了,最後還是得對照Python 版本。

預先準備

  • Redis server
  • Ollama
  • NextJS 專案

代理組件

首先,我們來看代理組件(AgentExecutor),代理組件在Langchain 框架中蠻核心的組件,代理能根據用戶輸入的內容,由AI 動態決定要使用那個工具,並給出最終的答案。

初始化代理組件

// 初化模式
const llm = new ChatOllama({
            model: 'llama3.2',
            temperature: 0,
            maxRetries: 2,
            baseUrl: 'http://localhost:11434',
        });

const tools = []; // 工具清單

// 初始化聊天範本
const prompt = ChatPromptTemplate.fromMessages([
            ['system', 'You are a helpful assistant'], // default role
            ['placeholder', '{chat_history}'], // default role
            ['human', '{input}'],
            ['placeholder', '{agent_scratchpad}'],
        ]);

const agent = createToolCallingAgent({
            llm,
            tools,
            prompt,
        });  
        
const result = await agentExecutor.invoke({ input: 'your question ' });        

工具 ( 動態路由 )

前面提到,當代理組件,接收使用者問題 > 會依據工具的描述,並讓 AI 決定使用那個工具 > 執行工具 > 最終回應結果給用戶。

客製化工具

import { tool } from '@langchain/core/tools';
...
 const greetingTool = tool(
            async ({ input }: { input: string }) => {
                return input + ' ==';
            },
            {
                name: 'greetingTool',
                description: 'if some one say hello',
                schema: z.object({
                    input: z.string(),
                })                
            }
        );
...        

參數說明

  • name:工具的名稱。
  • description:描述該工具的功能,供 AI 會依據描述,決定使用那個工具。
  • func:該工具的核心功能,這裡是異步函數,根據輸入值返回
  • schema:使用 zod 定義工具輸入的參數格式。

使用第三方工具 - SerpAPI

SerpAPI 是一個專門設計用來與搜尋引擎互動的 API,特別是 Google 搜尋,SerpAPI 自動化了網頁抓取的過程,讓開發者無需自行管理搜尋引擎的網頁解析和資料提取,簡化了搜尋結果的獲取過程。

import { SerpAPI } from '@langchain/community/tools/serpapi';

 let serpApiTool = new SerpAPI('your key ', {
           location: 'Austin,Texas,United States', // ,代表你要模擬的搜尋位置。在這裡,它指定了搜索應基於美國德州奧斯汀的位置,這可以影響搜尋結果的區域相關性。
           hl: 'en', //  語言參數,代表要以哪種語言來顯示搜尋結果。'en' 表示結果會以英文顯示。
           gl: 'us', // 國家/地區代碼,用來設定你希望搜尋結果的區域依據。'us' 表示結果會根據美國的地區來產生。
           }),

擴充 AI Agent 的記憶功能

大型語言模型,本身是不具備記憶功能,之所謂能讀懂上下文的問題,因為每次將討論的過程,再傳遞給大型語言模型,去詢問。

Langchain 的momory ,也是透過這個機制,因此將每次用戶問的問題存在 Redis ,讓AI Bot 也具備記憶功能。

安裝相關的套件 - 參考文件

npm i @langchain/redis @langchain/core redis @langchain/openai --save

使用 redis memory

import { RedisChatMessageHistory } from '@langchain/community/stores/message/ioredis';
import { BufferMemory } from 'langchain/memory';
...
 const client = new Redis('redis://localhost:30001');

        const memory = new BufferMemory({
            chatHistory: new RedisChatMessageHistory({
                sessionId: 'sessionId:' + new Date().toISOString(),
                sessionTTL: 300,
                client,
            }),
            aiPrefix: 'ollama',
            outputKey: 'output', // 沒有會爆錯
            memoryKey: 'chat_history', // 沒有會爆錯
            inputKey: 'input', // 沒有會爆錯
            returnMessages: true,
            
            
const agentExecutor = new AgentExecutor({
            agent,
            tools,
            verbose: true, // enable debug
            handleParsingErrors: true,
            memory, // redis 記憶功能
            returnIntermediateSteps: true,
        });
...

完整的程式碼 ( Next.JS API)

import { RedisChatMessageHistory } from '@langchain/community/stores/message/ioredis';
import { Calculator } from '@langchain/community/tools/calculator';
import { SerpAPI } from '@langchain/community/tools/serpapi';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { tool } from '@langchain/core/tools';
import { ChatOllama } from '@langchain/ollama';
import Redis from 'ioredis';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
import { BufferMemory } from 'langchain/memory';
import { NextApiRequest, NextApiResponse } from 'next/types';
import { z } from 'zod';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    if (req.method !== 'GET') {
        return res.status(405).json({ message: 'Only GET requests are allowed' });
    }

    try {
        const magicTool = tool(
            async ({ input }: { input: number }) => {
                return `${input + 2}`;
            },
            {
                name: 'magic_function',
                description: 'Applies a magic function to an input.',
                schema: z.object({
                    input: z.number(),
                }),
            }
        );

        const llm = new ChatOllama({
            model: 'llama3.2',
            temperature: 0,
            maxRetries: 2,
            baseUrl: 'http://localhost:11434',
            // other params...
        });

        const tools = [
            magicTool, // custom tool
            new Calculator(),
            new SerpAPI('your serp key', {
                location: 'Austin,Texas,United States',
                hl: 'en',
                gl: 'us',
            }),
        ];

        const prompt = ChatPromptTemplate.fromMessages([
            ['system', 'You are a helpful assistant'], // default role
            ['placeholder', '{chat_history}'], // default role
            ['human', '{input}'],
            ['placeholder', '{agent_scratchpad}'],
        ]);

        const agent = createToolCallingAgent({
            llm,
            tools,
            prompt,
        });

        const client = new Redis('redis://localhost:30001');

        const memory = new BufferMemory({
            chatHistory: new RedisChatMessageHistory({
                sessionId: 'sessionId:' + new Date().toISOString(),
                sessionTTL: 300,
                client,
            }),
            aiPrefix: 'ollama',
            outputKey: 'output', // 沒有會爆錯
            memoryKey: 'chat_history', // 沒有會爆錯
            inputKey: 'input', // 沒有會爆錯
            returnMessages: true,
        });

        const agentExecutor = new AgentExecutor({
            agent,
            tools,
            verbose: true, // enable debug
            handleParsingErrors: true,
            memory, // redis 記憶功能
            returnIntermediateSteps: true,
        });

        const result = await agentExecutor.invoke({ input: 'what is the value of magic_function(3)?' });
        const serpResult2 = await agentExecutor.invoke({ input: 'what is the Pokomon?' });
        const result3 = await agentExecutor.invoke({ input: 'what is 5 + 2 *5 =' });
        const result4 = await agentExecutor.invoke({ input: 'hello', outputKey: 'key1' });
        const result5 = await agentExecutor.invoke({ input: '幫我推薦電腦' });

        return res.status(200).json({ result, serpResult2, result3, result4, result5 });
    } catch (error) {
        return res.status(500).json({ message: 'Internal server error' });
    }
}

參考資料

此系列相關文章

作者

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。閱讀更多

覺得這篇有幫助?

作者做的免費工具、每日 Podcast 與電子報,都在這裡。

Mark Ku · 本文採用 CC BY 4.0 授權,轉載請註明作者並附上原文連結。

留言

訂閱電子報

訂閱後即時收到新文章通知,不錯過任何技術分享。

提交即表示同意接收電子報,隨時可

熱門文章

View all
Mark Ku
··640

Oracle Cloud 永久免費方案 Linux 主機及固定 IP :0 元打造雲端解決方案

Oracle Cloud 永久免費方案 Linux 主機及固定 IP :0 元打造雲端解決方案
Mark Ku
··549

告別 Postman 收費陷阱!開源 Git 原生 API 測試神器 Bruno 實戰指南

告別 Postman 收費陷阱!開源 Git 原生 API 測試神器 Bruno 實戰指南
Mark Ku
··329

一款免費開源類似於 Notion 類知識庫系統 — Outline Wiki 佈署與備份全攻略

一款免費開源類似於 Notion 類知識庫系統 — Outline Wiki 佈署與備份全攻略
Mark Ku
··264

訓練自己的 AI 語音:硬體門檻、開源模型比較與 LoRA 微調

訓練自己的 AI 語音:硬體門檻、開源模型比較與 LoRA 微調
Mark Ku
··230

打造高效 API 管理平台:從 0 開始部署 Kong Gateway - Part 1

打造高效 API 管理平台:從 0 開始部署 Kong Gateway - Part 1
Mark Ku
··220

在 Ubuntu 上設置 Samba 來共享資料夾,讓 Windows 11 用戶可以存取

在 Ubuntu 上設置 Samba 來共享資料夾,讓 Windows 11 用戶可以存取
使用 Langchain 和開源 Llama AI 在 Next.js 打造 AI Bot API Part 2 - 打造 AI 具有記憶功能的 AI Agent - Mark Ku's Blog