之前已经学过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进行定义一个工具,第一个参数就是要执行的函数
第二个参数就是一个配置对象,配置对象包含三个必需属性:
| 属性 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
| name | string | 必需 | 工具名称(AI 通过此名称识别和调用工具) |
| description | string | 必需 | 工具描述(AI 会读取此描述) |
| schema | ZodSchema | 必需 | 参数定义(使用 zod) |
model.bindTools(tools);是将工具数组绑定到 ChatOpenAI 模型实例上,创建一个新的模型实例,这个新实例"知道"有哪些工具可以使用。
根据打印的结果可以看到:当 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,创建文件并且写入代码
可以看到大模型成功创建todolist.html文件,并将代码写入,并且代码可以正常运行