一、Agent 到底"智能"在哪?

字面理解——AI 会自己干活。这么理解没问题,但这句大白话遮住了一个更关键的问题:

Agent 和普通的 AI 对话,差别到底在哪里?

答案是:结构不同

1.1 普通对话:一问一答,任务即终止

你: "帮我写一封邮件"
AI: (输出邮件内容)  ← 任务结束

LLM 输出一次,对话终止。它是一个问答机器——问什么答什么,答完即止,没有后续。

1.2 Agent:持续运转的循环结构

你给 Agent 一个任务,它会:

  1. 拆解任务,决定下一步做什么
  2. 调用工具,去外部世界获取信息
  3. 观察结果,判断是继续还是结束
  4. 循环往复,直到任务完成,或触发退出条件(超出循环次数、Token 上限、连续相同结果、任务失败)

这不是"一问一答",而是一个自主运转的执行循环在这里插入图片描述


二、Agent 工作方式:ReAct 三步循环

Agent 每一轮循环只有三个核心动作:

动作 英文 含义
思考 Reason 分析当前状态,决定下一步做什么
行动 Act 调用工具,执行具体操作
观察 Observe 接收工具返回的结果,判断是否继续

一轮结束后,观察 → 再回到思考 → 再行动 → 再观察,循环往复。

这个循环就叫 ReAct(Reasoning + Act + Observe)。它不是 LangChain 那种大型开发框架,而是 Agent 领域通用的循环工作标准,是一种思维范式

🔑 面试重点:ReAct 是 Agent 执行流程的行业共识,几乎所有主流 Agent 框架(LangChain、AutoGPT、CrewAI)底层都遵循这个循环模型。

2.1 一个场景走通 ReAct

任务:“帮我分析三家竞品,写一份报告”

┌─────────────────────────────────────────────────┐
│ 第一轮                                          │
│ Reason: "需要搜索竞品信息"                       │
│ Act:    调用搜索工具,查三家竞品最新动态          │
│ Observe: "信息量挺大,但缺少财务数据"             │
├─────────────────────────────────────────────────┤
│ 第二轮                                          │
│ Reason: "需要财务数据,可以去官网或股市API"       │
│ Act:    调用 API 抓取财报数据                    │
│ Observe: "财务数据拿到了,可以开始写报告"         │
├─────────────────────────────────────────────────┤
│ 第三轮...第 N 轮                                 │
│ Reason: "信息充足,写报告"                        │
│ Act:    生成完整竞品分析报告                      │
│ Observe: "报告完成,交给用户"                     │
└─────────────────────────────────────────────────┘

每一轮都在Reason → Act → Observe的循环中推进,直到任务完成。

在这里插入图片描述
在这里插入图片描述


三、Tool Use:Agent 的手和脚

🔑 核心认知:工具是 Agent 的能力边界。没有工具,Agent 只能在"脑子里转",转完还是只有文字。

3.1 常见工具类型

工具类型 能力 代表产品
搜索工具 上网查实时信息 Perplexity
代码执行器 运行代码、看结果、自验证 Claude(工程化验收机制最完善)
文件 I/O 读写本地文件 Cursor、Copilot
浏览器操控 打开网页、点击、提交表单 Manus
API 调用 连接外部服务 各类 SaaS Agent

📌 行业观察:“文无第一,武无第二”——Claude 在代码执行和工程化验收方面建立了事实标准。Anthropic 的 Agent 自测试机制,是目前最成熟的 AI 工程质量验证方案。

3.2 工具数量 = 能力边界

工具的覆盖范围,直接决定 Agent 的能力上限。选型 Agent 产品时,先看它集成了多少工具、工具质量如何——这是最核心的评估维度。


四、Message:Agent 的"记忆"载体

Agent 的整个对话历史和工具调用结果,全部存储在 messages 数组中。四种核心消息类型:

类型 角色 用途
SystemMessage 系统 设定 AI 身份、能力边界、行为规范
HumanMessage 用户 用户的输入
AIMessage AI LLM 的回复(可能包含工具调用)
ToolMessage 工具 工具执行结果的回传,必须带 tool_call_id

⚠️ 易错点ToolMessage 必须携带对应的 tool_call_id,否则 LLM 无法将工具结果与调用请求关联,会产生"幻觉"或忽略结果。

4.1 原生 OpenAI vs LangChain 的返回差异

原生 OpenAI 返回工具调用时,数据结构在 additional_kwargs.tools 中。LangChain 在 invoke 后会原样保留这个结构,同时贴心地准备了 tool_calls 属性——这就是 LangChain 的工程价值:提升 LLM 开发的便捷性和可读性
在这里插入图片描述


五、AI 工程:从零构建编程助手 Agent

5.1 工程目录

hello-langchain/
├── package.json
├── node_modules/
├── .env                  # API Key 配置
└── src/
    └── index.mjs         # Agent 主程序

5.2 前置知识:async 函数

// async 函数本身就是 Promise 实例
// return 等同于 Promise.resolve()
// return 的结果就是 resolve 的参数
async function getData() {
    return "hello";  // 等价于 Promise.resolve("hello")
}
// getData() 返回一个 Promise

六、完整实现:编程助手 Agent

下面是一个可运行的完整 Agent,逐段拆解。

6.1 环境与依赖

import dotenv from 'dotenv';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import {
    HumanMessage,
    SystemMessage,
    ToolMessage,
    // AIMessage  // invoke 返回的 response 本身就是 AIMessage
} from '@langchain/core/messages';
import fs from 'node:fs/promises';
import { z } from 'zod';

// 加载 .env 中的环境变量
dotenv.config();

6.2 初始化 LLM

const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',           // 可替换为任意兼容 OpenAI 协议的模型
    apiKey: process.env.DEEPSEEK_API_KEY,
    temperature: 0,                           // Agent 场景建议 0,保证输出稳定性
    configuration: {
        baseURL: 'https://api.deepseek.com/v1',  // 自定义 API 地址
    },
});

💡 经验之谈:Agent 场景 temperature 设 0,因为工具调用的参数需要精确,创造性在这里是负资产。

6.3 定义工具:文件读取

const readFileTool = tool(
    async ({ filePath }) => {
        // ===== 功能函数:真正干活的代码 =====
        const content = await fs.readFile(filePath, 'utf-8');

        // 给用户实时反馈(Agent 任务可能很耗时,用户需要感知进度)
        console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`);
        return content;
    },
    {
        // ===== 工具元信息:LLM 用它来决定何时调用、传什么参数 =====
        name: 'read_file',
        description: `用此工具来读取文件内容。当用户要求读取文件、查看代码、
分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)`,
        schema: z.object({
            filePath: z.string().describe('要读取的文件路径'),
        }),
    }
);

// 工具注册表
const tools = [readFileTool];

🔑 设计要点description 是 LLM 决定"何时调用"的唯一依据,写得越精确,调用越准确。schema 用 Zod 定义参数约束,LLM 会严格按 Schema 生成参数。

6.4 绑定工具到模型

// LangChain 的核心抽象:将 LLM 和工具注册到一起
const modelWithTools = model.bindTools(tools);

bindTools 做的事情:告诉 LLM"你有这些工具可用,需要时在回复中带上 tool_calls"。

6.5 初始化消息

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

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

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

6.6 核心循环:ReAct 的代码实现

前置回顾:tool_calls里面有什么:

  1. llm原生tool_calls:在这里插入图片描述
  2. LangChain额外返回的tool_calls:在这里插入图片描述
// 第一轮:让 LLM 思考
let response = await modelWithTools.invoke(messages);
console.log(JSON.stringify(response, null, 2));
messages.push(response);  // 将 AI 回复加入历史

// ===== ReAct 循环:有工具调用就执行,没有就结束 =====
while (response.tool_calls && response.tool_calls.length > 0) {
    console.log(`\n[检测到 ${response.tool_calls.length} 个工具调用]`);

    // ---- Act:并行执行所有工具调用 ----
    const toolResults = await Promise.all(
        response.tool_calls.map(async (toolCall) => {
            // 查找对应的工具定义
            const matchedTool = tools.find(t => t.name === toolCall.name);
            if (!matchedTool) {
                return `错误:找不到工具 ${toolCall.name}`;
            }

            console.log(`[执行工具] ${toolCall.name}(${JSON.stringify(toolCall.args)})`);

            try {
                // LangChain 的 tool.invoke() 方法,内部调用你写的功能函数
                const result = await matchedTool.invoke(JSON.parse(toolCall.args));
                return result;
            } catch (err) {
                return `错误:工具 ${toolCall.name} 执行失败 —— ${err.message}`;
            }
        })
    );

    // ---- Observe:将工具结果打包为 ToolMessage 加入历史 ----
    response.tool_calls.forEach((toolCall, index) => {
        messages.push(new ToolMessage({
            content: toolResults[index],
            tool_call_id: toolCall.id,  // 必须!LLM 靠这个 ID 关联请求和结果
        }));
    });

    // ---- 下一轮 Reason:把所有消息喂给 LLM,让它继续思考 ----
    response = await modelWithTools.invoke(messages);
    messages.push(response);
}

// 循环结束:LLM 不再要求调用工具,说明任务完成
console.log('\n===== 最终输出 =====');
console.log(response.content);

6.7 执行流程可视化

messages = [SystemMessage, HumanMessage]
    │
    ▼
┌──────────────────────────────┐
│ modelWithTools.invoke()      │  ← Reason: LLM 思考,决定是否调工具
│ 返回 response (AIMessage)    │
└──────────┬───────────────────┘
           │
    有 tool_calls? ──No──▶ 输出 response.content(任务完成)
           │
          Yes
           │
           ▼
┌──────────────────────────────┐
│ Promise.all(tool.invoke())   │  ← Act: 并行执行所有工具
│ 返回 toolResults[]           │
└──────────┬───────────────────┘
           │
           ▼
┌──────────────────────────────┐
│ messages.push(ToolMessage)   │  ← Observe: 结果带 tool_call_id 回传
└──────────┬───────────────────┘
           │
           ▼
       (回到 Reason,继续循环)

七、全文总结

Agent 的核心差异在于结构——从"一问一答"升级为"思考→行动→观察"的自主循环。理解这个循环,就理解了 Agent:

  1. ReAct 是 Agent 的通用工作框架,不是某个库的专利
  2. Tool Use 是 Agent 的手和脚,工具范围 = 能力边界
  3. Message 数组 是 Agent 的"记忆",四种消息类型承载了整个对话流
  4. LangChain 的价值在于工程抽象——bindToolstool()、消息体系,让开发者聚焦业务逻辑

八、核心知识点复盘

知识点 一句话总结
ReAct 循环 Reason → Act → Observe → 循环,直到任务完成或触发退出条件
Tool Use Agent 操作外部世界的能力,工具的覆盖范围决定 Agent 的能力上限
SystemMessage 定义 Agent 的身份、能力、行为规范
ToolMessage 工具执行结果,必须带 tool_call_id 才能被 LLM 正确关联
model.bindTools() LangChain 的核心抽象,将工具注册到 LLM
tool() 定义工具的元信息(name/description/schema)+ 功能函数
temperature: 0 Agent 场景建议设为 0,保证工具调用参数的精确性
Promise.all 多个工具调用应并行执行,而非串行

九、避坑指南

❌ 坑 1:ToolMessage 忘了传 tool_call_id

// 错误写法
messages.push(new ToolMessage({ content: result }));
// LLM 无法关联,可能导致"幻觉"

// 正确写法
messages.push(new ToolMessage({
    content: result,
    tool_call_id: toolCall.id,  // 必须!
}));

❌ 坑 2:串行调用多个工具

当 LLM 返回多个 tool_calls 时,它们通常是互相独立的,应该用 Promise.all 并行执行,而不是 for 循环串行等。

❌ 坑 3:LLM 的"无状态"特性

LLM 本身不记忆任何东西。每一轮调用,你必须把完整的 messages 数组传给它。少传一条历史消息,Agent 就"失忆"了。Agent 的记忆全靠你手动维护 messages 数组。

❌ 坑 4:无限循环

Agent 可能陷入"调工具→不满意→再调→还不满意"的死循环。生产环境必须设置硬限制:

  • 最大循环次数(如 20 轮)
  • Token 总量上限
  • 连续相同结果检测(防重复调用)

❌ 坑 5:工具 description 写得太模糊

LLM 靠 description 判断何时调用工具。写"用来处理文件"和写"当用户要求读取文件内容、查看代码、分析文件时调用此工具",效果天差地别。description 是工具调用的"说明书",越精准越好。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐