Agent 智能体:从“一问一答“到“自主干活“的技术全解析
文章目录
一、Agent 到底"智能"在哪?
字面理解——AI 会自己干活。这么理解没问题,但这句大白话遮住了一个更关键的问题:
Agent 和普通的 AI 对话,差别到底在哪里?
答案是:结构不同。
1.1 普通对话:一问一答,任务即终止
你: "帮我写一封邮件"
AI: (输出邮件内容) ← 任务结束
LLM 输出一次,对话终止。它是一个问答机器——问什么答什么,答完即止,没有后续。
1.2 Agent:持续运转的循环结构
你给 Agent 一个任务,它会:
- 拆解任务,决定下一步做什么
- 调用工具,去外部世界获取信息
- 观察结果,判断是继续还是结束
- 循环往复,直到任务完成,或触发退出条件(超出循环次数、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里面有什么:
- llm原生tool_calls:
- 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:
- ReAct 是 Agent 的通用工作框架,不是某个库的专利
- Tool Use 是 Agent 的手和脚,工具范围 = 能力边界
- Message 数组 是 Agent 的"记忆",四种消息类型承载了整个对话流
- LangChain 的价值在于工程抽象——
bindTools、tool()、消息体系,让开发者聚焦业务逻辑
八、核心知识点复盘
| 知识点 | 一句话总结 |
|---|---|
| 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 是工具调用的"说明书",越精准越好。
更多推荐








所有评论(0)