使用JavaScript从零构建AI代理

使用大型语言模型(LLMs)和网络搜索构建并理解AI代理。

使用JavaScript从零构建AI代理

自主系统允许软件通过必要时做出决策并使用外部工具来执行复杂任务。在本文中,我们将从头开始用JavaScript构建一个自主系统,不依赖任何框架。这将使我们深入了解AI代理在底层的工作原理。

我们将实现一个系统,该系统:

  • 通过Replicate API使用大型语言模型(LLM)
  • 集成由SerpAPI支持的网络搜索工具(前100次请求免费)
  • 根据查询动态注册并使用工具

最终,你将拥有一个功能齐全的代理,可以搜索网络、处理信息并生成见解。让我们开始吧!

1、项目搭建

确保已安装Node.js,并且如果尚未安装,请全局安装ts-node

npm install -g ts-node

接下来,初始化一个TypeScript项目:

mkdir agentic-system && cd agentic-system  
npm init -y  
npm install typescript ts-node axios dotenv cheerio  
npx tsc — init

创建一个**.env**文件并添加您的SerpAPI密钥:

SERPAPI_KEY=your-serpapi-key

现在,让我们创建我们的TypeScript文件。

本教程的完整代码可在此处获取。

2、理解AI代理

AI代理是一个能够根据用户输入进行推理、决定何时使用工具并基于外部信息生成响应的系统。在我们的案例中,我们需要:

  • 接受用户的查询
  • 决定是否可以直接响应或需要外部工具(如网络搜索)
  • 如果需要,检索额外数据
  • 生成结构化的响应

2.1 创建AI代理(Agent.ts)

此代理将管理工具并与LLM交互。Agent类维护一组可用工具,并确定是调用LLM还是使用工具。

import { LLMProvider } from "../llm/LLMProvider";  
import { createSystemPrompt } from "../utils/createSystemPrompt";  
import { Tool } from "./Tool";  
  
export class Agent {  
    tools: Record<string, Tool>;  
    private llmProvider: LLMProvider;  
  
    constructor(llmProvider: LLMProvider) {  
        this.tools = {};  
        this.llmProvider = llmProvider;  
    }  
  
    addTool(tool: Tool): void {  
        this.tools[tool.name] = tool;  
    }  
  
    getAvailableTools(): string[] {  
        return Object.values(this.tools).map(  
            (tool) => `${tool.name}: ${tool.description}`  
        );  
    }  
  
    async useTool(toolName: string, args: Record<string, any>): Promise<string> {  
        const tool = this.tools[toolName];  
        if (!tool) {  
            throw new Error(  
                `Tool '${toolName}' not found. Available: ${Object.keys(this.tools)}`  
            );  
        }  
        const orderedKeys = Object.keys(tool.parameters);  
        const argValues = orderedKeys.map(key => args.hasOwnProperty(key) ? args[key] : undefined);  
  
        return await tool.call(...argValues);  
    }  
  
  
    createSystemPrompt(): string {  
        const toolsArray = Object.values(this.tools);  
        return JSON.stringify(createSystemPrompt(toolsArray), null, 2);  
    }  
  
    /**  
     * 使用提供的LLMProvider调用LLM。  
     *  
     * 它构建输入对象(使用系统提示、用户提示和其他参数),然后传递给LLMProvider。  
     */  
    async callLLM(userPrompt: string): Promise<any> {  
        const systemPrompt = this.createSystemPrompt();  
        const promptTemplate = "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n{system_prompt}<|eot_id|><|start_header_id|>user<|end_header_id|>\n\n{prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"  
        const input = {  
            prompt: userPrompt,  
            system_prompt: systemPrompt,  
            max_new_tokens: 10000,  
            prompt_template: promptTemplate,  
        };  
  
        return await this.llmProvider.callLLM(input);  
    }  
}

解释:

  1. Agent类存储一组可用工具。
  2. addTool() 注册新工具,允许代理稍后使用它。
  3. useTool() 检查工具是否已注册,并使用给定参数执行它。
  4. callLLM() 查询语言模型,提供在需要时使用工具的指令。

2.2 创建系统提示(createSystemPrompt.ts)

import {Tool} from "../agent/Tool";  
import {SystemPrompt} from "../interfaces/SystemPrompt";  
  
/**  
 * 根据可用工具生成系统提示。  
 *  
 * @param tools 注册的工具数组。  
 * @returns 系统提示对象。  
 */  
export function createSystemPrompt(tools: Tool[]): SystemPrompt {  
    return {  
        role: "AI Assistant",  
        capabilities: [  
            "在需要时使用提供的工具回答用户查询",  
            "在不需要工具时直接提供响应",  
            "计划有效的工具使用顺序"  
        ],  
        instructions: [  
            "仅在必要时使用工具",  
            "如果可以直接提供答案,则不要使用工具",  
            "如果需要工具,请计划解决问题所需的步骤",  
            "仅响应JSON,其他内容不得包含"  
        ],  
        tools: tools.map(tool => ({  
            name: tool.name,  
            description: tool.description,  
            parameters: tool.parameters,  
        })),  
        response_format: {  
            type: "json",  
            schema: {  
                requires_tools: {  
                    type: "boolean",  
                    description: "此查询是否需要工具"  
                },  
                direct_response: {  
                    type: "string",  
                    description: "无需工具时的响应",  
                    optional: true  
                },  
                thought: {  
                    type: "string",  
                    description: "如何解决查询的推理(当需要工具时)",  
                    optional: true  
                },  
                plan: {  
                    type: "array",  
                    items: { type: "string" },  
                    description: "解决问题的步骤(当需要工具时)",  
                    optional: true  
                },  
                tool_calls: {  
                    type: "array",  
                    items: {  
                        type: "object",  
                        properties: {  
                            tool: { type: "string", description: "工具名称" },  
                            args: { type: "object", description: "工具参数" }  
                        }  
                    },  
                    description: "按顺序调用的工具(如果需要)",  
                    optional: true  
                }  
            }  
        }  
    };  
}

解释

  1. 此函数根据可用工具生成结构化系统提示。
  2. capabilitiesinstructions 定义了代理的行为方式。
  3. response_format 确保响应始终为JSON格式。
  4. 示例工具使用场景帮助指导LLM的决策过程。

2.3 实现LLM调用(ReplicateLLMProvider.ts)

此模块与外部LLM API交互,模拟对Replicate的Llama 3模型的调用。

import Replicate from "replicate";  
import { LLMProvider } from "./LLMProvider";  
import {writeToLog} from "../utils/writeToLog";  
import {isValidJson} from "../utils/isValidJson";  
  
export class ReplicateLLMProvider implements LLMProvider {  
    private replicate: Replicate;  
    private readonly modelName: `${string}/${string}` | `${string}/${string}:${string}`;  
  
    constructor(modelName: `${string}/${string}` | `${string}/${string}:${string}`) {  
        this.replicate = new Replicate();  
        this.modelName = modelName;  
    }  
  
    async callLLM(input: any): Promise<any> {  
        await writeToLog('llm_input.log', JSON.stringify(input))  
  
        let fullResponse = "";  
        for await (const event of this.replicate.stream(this.modelName, { input })) {  
            fullResponse += event;  
        }  
  
        await writeToLog('llm_response.log', fullResponse)  
  
        if (isValidJson(fullResponse)) {  
            return JSON.parse(fullResponse);  
        } else {  
            return fullResponse;  
        }  
    }  
}

解释:

  1. ReplicateLLMProvider 包装了Replicate的API,允许我们的代理生成响应。
  2. 它存储了一个模型名称(meta/meta-llama-3–8b-instruct)。
  3. callLLM() 向Replicate发送查询并在响应中记录事件流。生成响应。

2.4 使用SerpAPI的Web搜索工具(WebSearchTool.ts)

  
import { ToolDecorator } from "../decorators/ToolDecorator";  
import axios from "axios";  
import * as cheerio from "cheerio";  
import 'dotenv/config';  
  
// 确保在环境变量中设置了API密钥  
const SERPAPI_KEY = process.env.SERPAPI_KEY!;  
const SERPAPI_URL = "https://serpapi.com/search";  
  
export class WebSearchTool {  
    @ToolDecorator({  
        docstring: `使用Google进行网络搜索并总结前5个结果。  
  
参数:  
- query: 查找信息的搜索查询。`,  
        parameters: {  
            query: { type: "string", description: "用于网络搜索的查询" }  
        },  
    })  
    async searchWeb(query: string): Promise<string> {  
        try {  
            const searchResponse = await axios.get(SERPAPI_URL, {  
                params: {  
                    q: query,  
                    api_key: SERPAPI_KEY,  
                    num: 5, // 获取前5个结果  
                },  
            });  
  
            const results = searchResponse.data.organic_results;  
            if (!results || results.length === 0) {  
                return "未找到相关结果。";  
            }  
  
            // 获取所有5个搜索结果的摘要  
            let summaries: string[] = [];  
            for (const result of results.slice(0, 5)) { // 限制为前5个结果  
                if (!result.link) continue; // 如果没有链接则跳过  
  
                console.log(`正在获取来自: ${result.link} 的内容`);  
  
                const summary = await this.summarizeWebPage(result.link);  
                summaries.push(`**${result.title}**\n${summary}\n[阅读更多](${result.link})\n`);  
            }  
  
            // 将所有摘要合并为结构化响应  
            return `### 网络搜索摘要:\n\n` + summaries.join("\n---\n");  
        } catch (error) {  
            console.error('错误:', error);  
            return "检索搜索结果时出错。";  
        }  
    }  
  
    /**  
     * 获取并总结网页内容。  
     * 使用Cheerio提取可读文本并限制其长度。  
     */  
    private async summarizeWebPage(url: string): Promise<string> {  
        try {  
            const response = await axios.get(url, { timeout: 8000 }); // 获取网页内容  
            const html = response.data; // 获取原始HTML字符串  
            const $ = cheerio.load(html); // 将HTML加载到Cheerio中  
  
            // 从<p>元素中提取可读文本  
            let pageTextArray: string[] = [];  
            $("p").each((index, element) => {  
                const paragraphText = $(element).text().trim();  
                if (paragraphText.length > 50) { // 忽略非常短的段落(例如广告、导航)  
                    pageTextArray.push(paragraphText);  
                }  
            });  
  
            // 将提取的文本合并为单个摘要  
            let pageText = pageTextArray.join(" ");  
  
            if (!pageText || pageText.length < 100) {  
                return "未发现实质性内容。";  
            }  
  
            // 限制输出为前1000个字符  
            return pageText.substring(0, 1000) + "...";  
        } catch (error) {  
            console.error(`获取/总结页面失败: ${url}`, error);  
            return "检索页面摘要时出错。";  
        }  
    }  
}

解释:

  1. searchWeb() 使用SerpAPI查询Google并获取前5个结果。
  2. 它调用summarizeWebPage(),该方法使用Cheerio从每个页面抓取和提取有意义的文本。

2.5 将所有内容整合在一起(index.ts)

import { WebSearchTool } from "./providers/WebSearchTool";  // 导入新工具  
import { ToolsManager } from "./agent/ToolsManager";  
import { Agent } from "./agent/Agent";  
import { ReplicateLLMProvider } from "./llm/ReplicateLLMProvider";  
  
(async () => {  
    const searchProvider = new WebSearchTool();  
  
    const searchTools = ToolsManager.registerToolsFrom(searchProvider);  
  
    const llmProvider = new ReplicateLLMProvider("meta/meta-llama-3-8b-instruct");  
    const agent = new Agent(llmProvider);  
  
    // 向代理注册所有工具  
    [...searchTools].forEach(tool => agent.addTool(tool));  
  
    const userPrompt = "查找2025年与大型语言模型相关的最新AI趋势并总结它们。仅返回Json,不要附加任何文本。";  
    const initialLLMResponse = await agent.callLLM(userPrompt);  
  
    if (!initialLLMResponse.requires_tools) {  
        console.log('无需工具')  
        console.log(initialLLMResponse.direct_response);  
    } else {  
        const toolResults: Record<string, string> = {};  
        for (const toolCall of initialLLMResponse.tool_calls) {  
            toolResults[toolCall.tool] = await agent.useTool(toolCall.tool, toolCall.args);  
        }  
  
        const finalPrompt = `  
            用户查询: ${userPrompt}  
              
            工具输出:  
            ${JSON.stringify(toolResults, null, 2)}  
              
            指令:  
            1. 阅读并分析“工具输出”部分中的前5个网站的内容摘要。  
            2. 对于每个网站,生成一个**简短的摘要(2-3句话)**,捕捉该来源的最重要见解。  
            3. 然后提供一个**最终摘要**,将所有5个来源的关键要点综合成一个**简洁且结构良好的概述**。  
            4. 使用清晰、有信息量的语言。除了所需的摘要外,不要生成任何其他文本。  
            不需要工具使用。  
            仅返回Json,不要附加任何文本。  
              
            格式:  
            ### 来源摘要:  
            1. **[来源1标题]** - (来源1摘要)  
            2. **[来源2标题]** - (来源2摘要)  
            3. **[来源3标题]** - (来源3摘要)  
            4. **[来源4标题]** - (来源4摘要)  
            5. **[来源5标题]** - (来源5摘要)  
              
            ### 最终摘要:  
            (综合所有来源关键见解的简洁概述)  
        `;  
  
        const finalLLMResponse = await agent.callLLM(finalPrompt);  
        console.log("最终AI摘要:");  
        console.log(finalLLMResponse.direct_response);  
    }  
})();

解释

工具注册: 我们实例化WebSearchTool,将其与ToolsManager注册,并将其添加到Agent实例中。

用户查询处理: 代理接收一个提示,要求查找最新的AI趋势。

决策制定:

  • 如果代理确定不需要外部工具,则提供直接响应。
  • 否则,它会使用SerpAPI调用searchWeb以获取相关结果。

总结: LLM被指示:

  • 分析前5个搜索结果
  • 分别总结每个结果
  • 创建一个最终的、结构化的总结

最终输出: AI代理输出一个JSON格式的摘要,包含结构化的见解。

3、结束语

通过本教程,你已经从头开始构建了一个AI代理,它:

  • 使用LLM API
  • 执行网络搜索
  • 决定是否使用外部工具
  • 生成结构化响应

这为你提供了对底层工作原理的深入了解。你现在可以扩展此功能,通过集成更多工具、添加记忆或改进决策过程。🚀


原文链接:AI Agents: Build an Agent from Scratch using JavaScript

汇智网翻译整理,转载请标明出处