LangChain.js 快速上手指南:Tool的使用,给大模型安上了双手

0 阅读5分钟

之前已经学过ChatOpenAI 、invoke和Messages,完成了与大模型(LLM)的对话,可以查看这篇文章:LangChain.js 基础

接下来我们将学习如何让大模型与外界交互。

虽然大模型(LLM)具备强大的语言理解和生成能力,但它本身不具备直接与外部API、数据库交互的能力,如果想让大模型与外界交互就需要用到Tool

Function Calling 和 Tool

函数调用(Function Calling) 是LangChain.js 框架提供的一种强大机制,它允许LLM根据用户指令,请求调用外部定义的函数(工具),并将函数的执行结果反馈给LLM,从而让LLM能够完成更复杂、更贴近实际应用的任务。

在真正的进行开发前我们需要先搞清楚一些基础概念:什么是Function Calling(函数调用)?什么是Tool(工具)?

Function Calling 最早是 OpenAI 在其 API 中引入的一项功能,允许开发者将大语言模型(如 GPT-4)与外部函数或工具集成。通过 Function Calling,模型可以理解用户请求并生成调用外部函数所需的参数,从而实现更复杂、更动态的任务处理。

让大语言模型拥有了调用外部接口的能力,使用这种能力,大模型能做一些比如实时获取天气信息、发送邮件等和现实世界交互的事情,执行相应的本地/远程函数,然后将函数的执行结果返回给LLM,LLM再基于这个结果生成最终的用户回复。

Tool 是 Function Calling 机制中的 具体实现单元 ,是可被模型调用的外部能力封装。

在LangChain中,“工具”是指LLM可以调用的任何函数或功能。这些工具可以是:

  • API调用:获取天气、股票价格、搜索信息等。
  • 数据库查询:从数据库中检索或修改数据。
  • 代码执行:运行Python脚本或任何其他代码。
  • 自定义逻辑:任何你希望LLM能够触发的特定业务逻辑。

开发一个Tool

大模型不能直接读取本地文件,但是我们可以开发一个读取本地文件的tool,并让大模型讲解一下内容

我们创建一个读取文件内容的工具,让大模型调用

需要安装@langchain/openai、@langchain/core 、这几个包

npm install @langchain/openai @langchain/core zod
  • @langchain/openai 是提供与 OpenAI API 交互的核心功能,是 LangChain 框架中调用 OpenAI 模型的入口。
  • @langchain/core 是 LangChain 的核心库,提供基础的数据结构、工具定义、消息类型等。
  • zod 是TypeScript 优先的模式验证库,用于定义和验证数据结构。
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
import fs from 'node:fs/promises';
import { z } from 'zod';

const model = new ChatOpenAI({
    modelName: process.env.MODEL_NAME,
    apiKey: process.env.API_KEY,
    configuration: {
        baseURL: process.env.BASE_URL,
    },
});

const readFileTool = tool(
    async ({ filePath }) => {
        const content = await fs.readFile(filePath, 'utf-8');
        console.log(`  [工具调用] read_file("${filePath}") - 成功读取 ${content.length} 字节`);
        return `文件内容:\n${content}`;
    },
    {
        name: 'read_file',
        description: '用此工具来读取文件内容。当用户要求读取文件、查看代码、分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)。',
        schema: z.object({
            filePath: z.string().describe('要读取的文件路径'),
        }),
    }
);

const tools = [
    readFileTool
];

const modelWithTools = model.bindTools(tools);

const messages = [
    new SystemMessage(
        `你是一个代码助手,可以使用工具读取文件并解释代码。

工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具
2. 等待工具返回文件内容
3. 基于文件内容进行分析和解释

可用工具:
- read_file: 读取文件内容(使用此工具来获取文件内容)
`),
    new HumanMessage('请读取 src/index.js 文件内容并解释代码')
];

let response = await modelWithTools.invoke(messages);
console.log(response);


messages.push(response);

while (response.tool_calls && response.tool_calls.length > 0) {
    console.log(`\n[检测到 ${response.tool_calls.length} 个工具调用]`);

    for (const toolCall of response.tool_calls) {
        const tool = tools.find(t => t.name === toolCall.name);
        if (tool) {
            console.log(`  [执行工具] ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
            try {
                const result = await tool.invoke(toolCall.args);
                messages.push(
                    new ToolMessage({
                        content: result,
                        tool_call_id: toolCall.id,
                    })
                );
            } catch (error) {
                console.log(`  [错误] ${error.message}`);
                messages.push(
                    new ToolMessage({
                        content: `错误: ${error.message}`,
                        tool_call_id: toolCall.id,
                    })
                );
            }
        }
    }

    response = await modelWithTools.invoke(messages);
}

console.log('\n[最终回复]');
console.log(response.content);

使用 tool进行定义一个工具,第一个参数就是要执行的函数

第二个参数就是一个配置对象,配置对象包含三个必需属性:

属性类型是否必需说明
namestring必需工具名称(AI 通过此名称识别和调用工具)
descriptionstring必需工具描述(AI 会读取此描述)
schemaZodSchema必需参数定义(使用 zod)

model.bindTools(tools);是将工具数组绑定到 ChatOpenAI 模型实例上,创建一个新的模型实例,这个新实例"知道"有哪些工具可以使用。

image.png

根据打印的结果可以看到:当 AI 判断需要调用工具时,它不会生成文本回答,而是返回工具调用请求,所以此时content为空,而tool_calls有值

此时我们把AI返回的消息放到messages对话记录里,然后 从tools 数组里找到对应的工具,取出来进行 invoke,并且传入大模型解析出的参数,最后把工具调用结果作为 ToolMessage 传给大模型,让它继续回答

根据这个简单的读取工具调用的案例,我们可以总结出这个流程:

LangChain 已经封装了完整的工具调用全流程,无需手动处理每一步,他会自动把 Tool 定义注入 Prompt→解析模型输出的调用指令→执行工具→把结果回传给模型→生成最终回复。我们只需要定义好 Tool 并绑定到模型,用已经学过的invoke方法,就能完成完整的工具调用,和之前的学习内容无缝衔接。

可以让大模型给我们写一个todolist的代码,他给出了代码,但是不能直接代码写入html文件,我们就可以写一个tool,让大模型通过执行tool将代码写入html文件


const writeFileTool = tool(
    async ({ filePath, content }) => {
        try {
            const dir = path.dirname(filePath);
            await fs.mkdir(dir, { recursive: true });
            await fs.writeFile(filePath, content, 'utf-8');
            console.log(`[工具调用] write_file("${filePath}") - 成功写入 ${content.length} 字节`);
            return `文件写入成功: ${filePath}`;
        } catch (error) {
            console.log(`[工具调用] write_file("${filePath}") - 错误: ${error.message}`);
            return `写入文件失败: ${error.message}`;
        }
    },
    {
        name: 'write_file',
        description: '向指定路径写入文件内容,自动创建目录',
        schema: z.object({
            filePath: z.string().describe('文件路径'),
            content: z.string().describe('要写入的文件内容'),
        }),
    }
);

const tools = [
    readFileTool,
    writeFileTool,
];

const modelWithTools = model.bindTools(tools);

// Agent 执行函数
async function runAgentWithTools(query, maxIterations = 30) {
    const messages = [
        new SystemMessage(
`你是一个经验丰富的程序员,可以使用工具完成任务。

工具:
1. read_file: 读取文件
2. write_file: 写入文件
`), 
        new HumanMessage(query)
    ];

        console.log(`⏳ 正在等待 AI 思考...`);
        const response = await modelWithTools.invoke(messages);
        console.log("🚀 ~ runAgentWithTools ~ response:", response)
        messages.push(response);

        // 检查是否有工具调用
        if (!response.tool_calls || response.tool_calls.length === 0) {
            console.log(`\n✨ AI 最终回复:\n${response.content}\n`);
            return response.content;
        }

        // 执行工具调用
        for (const toolCall of response.tool_calls) {
            const foundTool = tools.find(t => t.name === toolCall.name);
            if (foundTool) {
                const toolResult = await foundTool.invoke(toolCall.args);
                messages.push(new ToolMessage({
                    content: toolResult,
                    tool_call_id: toolCall.id,
                }));
            }
        }

}

const prompt = `实现一个功能丰富的 TodoList 应用:只需要使用html实现,不需要使用任何框架或库,并且将代码写入到todolist.html文件中`;

try {
    await runAgentWithTools(prompt);
} catch (error) {
    console.error(`\n❌ 错误: ${error.message}\n`);
}

写一个writeFileTool工具方法,让大模型生成代码后调用这个tool,创建文件并且写入代码 image.png

image.png

可以看到大模型成功创建todolist.html文件,并将代码写入,并且代码可以正常运行