一、 序言:大模型是“缸中之脑”吗?
你是否也曾有过这种无力感:对着 GPT-4 或 Claude 3.5 聊得热火朝天,它能帮你写出完美的 React 组件,但当你问它:“帮我看看当前目录下那个 index.js 里的逻辑有没有 Bug”时,它只能无奈地回答你:“对不起,我无法直接访问您的本地文件。”
这一刻,你会发现,再聪明的大模型,如果没有工具(Tools) ,也只是一个被困在对话框里的“缸中之脑”。它懂天文地理,却没法为你开一下灯,也没法为你读一行代码。
Agent(智能体)的诞生,就是为了给这个大脑安上手脚。 今天,我就带大家拆解一段基于 LangChain 的核心代码,看看我们是如何通过 Tool Calling(工具调用) 机制,让 AI 跨越对话框,直接读取并分析本地文件的。
二、 什么是 Agent?(不仅仅是 Prompt)
在掘金,我们不谈玄学,只谈架构。一个成熟的 AI Agent 遵循以下公式:
Agent = LLM(大脑) + Planning(规划) + Tools(手脚) + Memory(记忆)
- 大脑:负责理解你的意图。
- 规划:把复杂任务拆解成第一步做什么,第二步做什么。
- 工具:去执行那些大模型本身做不到的事(如:读写文件、发邮件、跑 Bash 命令、搜 Google)。
- 记忆:记住之前的操作结果,避免反复横跳。
三、 实战拆解:手把手教你给 AI 装上“手”
我们要实现的功能很简单:让大模型通过调用 Node.js 的异步 I/O,读取本地的 .mjs 文件并进行代码审计。
1. 定义工具:给 AI 一份说明书
在 LangChain 中,定义工具不仅仅是写个函数,更关键的是写好 Description。因为大模型是靠你的描述来判断“什么时候该用这个工具”的。
JavaScript
const readFileTool = tool(
async ({ path }) => {
try {
// 真正的磁盘操作在这里
const content = await fs.readFile(path, 'utf-8');
console.log(`[工具调用] read_file("${path}") 成功`);
return content;
} catch (error) {
return `读取文件失败: ${error.message}`;
}
},
{
name: "read_file",
description: "当用户需要查看代码、分析本地文件内容时,必须调用此工具。",
schema: z.object({
path: z.string().describe("要读取的文件相对路径或绝对路径"),
})
}
);
技术要点: 我们使用了 zod 进行数据校验。这非常重要!AI 有时候会胡乱传参,zod 就像安检员,确保传进来的 path 一定是个字符串,否则直接拦截。
2. 绑定工具:赋予 AI 决策权
如果你只是写了函数,大模型并不知道它的存在。我们需要通过 model.bindTools([readFileTool]) 告诉大模型:“嘿,现在你多了个技能,需要读文件时尽管开口。”
四、 核心逻辑:Tool Calling 的“反复横跳”艺术
这是整篇文章最值钱的地方。很多人不理解:为什么代码里要写一个 while 循环?
真相是:大模型并不会直接运行你的代码。 它只是一个决策者。整个过程像是一场精彩的拉锯战:
- 用户提问:“解释一下
tool-file-read.mjs这个文件。” - LLM 决策:它发现自己没内容,于是返回一个
tool_calls指令,意思是:“我请求执行read_file工具,参数是path: 'tool-file-read.mjs'”。 - 本地执行:我们的代码检测到这个请求,在本地环境运行
fs.readFile,拿到了源码。 - 结果反馈:我们将源码封装成一个
ToolMessage。注意! 必须带上tool_call_id,否则大模型不知道这个结果对应哪个请求。 - LLM 二次分析:我们将包含“原始问题 + 它的请求 + 工具执行结果”的完整对话历史再次发给大模型。
- 最终输出:大模型看完了代码,终于给出了详细的解释。
代码逻辑如下:
JavaScript
while (response.tool_calls && response.tool_calls.length > 0) {
// 1. 遍历 LLM 想调用的所有工具(可能同时调用多个)
const toolResults = await Promise.all(
response.tool_calls.map(async (toolCall) => {
const tool = tools.find(t => t.name === toolCall.name);
return await tool.invoke(toolCall.args); // 在你的服务器上跑逻辑
})
);
// 2. 将结果包装成 ToolMessage,喂回给大模型
response.tool_calls.forEach((toolCall, index) => {
messages.push(new ToolMessage({
content: toolResults[index],
tool_call_id: toolCall.id, // 关键:建立关联
}));
});
// 3. 再次询问,LLM 会根据拿到的 Tool 内容进行思考
response = await modelWithTools.invoke(messages);
messages.push(response);
}
五、 为什么这改变了编程的游戏规则?
你最近一定被 Cursor 刷屏了。为什么 Cursor 这么强?
它的核心就是这套逻辑。当你让 Cursor 修复一个 Bug 时,它会:
- 调用
list_dir看看你有多少文件。 - 调用
read_file读入相关的几个组件。 - 在脑子里模拟运行,找到 Bug。
- 调用
write_to_file直接帮你把代码改了。
这就是 Agentic Workflow(智能体工作流) 。大模型不再仅仅是一个聊天机器人,它变成了一个能够通过工具与现实世界交互的数字员工。
六、 总结与进阶思考
通过今天的学习,我们掌握了让 AI 动起来的核心:Tool Calling。
但这也引出了更深层次的问题:
- 安全性:如果 AI 调用的工具是
rm -rf /怎么办?(提示:永远不要给 Agent 赋予不受限的写权限)。 - 准确性:如何写出让 AI 永远不误判的工具描述?
- 多工具协作:当 AI 需要同时调用搜索工具和绘图工具时,如何保证逻辑不乱?
Agent 的世界才刚刚开启,从简单的文件读取,到自动化的 DevOps 流水线,想象空间巨大。
如果你觉得这篇文章对你有启发,别忘了点赞、收藏加关注!
评论区互动: 你最想给你的 AI 助理加上什么“变态”功能?是自动帮你点外卖,还是自动帮你写日报?欢迎留言,点赞最高的我出一期实战教程。