Skip to content
使用 Node.js 开发 Agent:概述
一个由 LLM 驱动的聊天程序,能做的事情非常有限:它只能生成文本。当你需要它查数据库、调用内部 API、读写文件时,对话接口本身没有执行能力。这正是 Agent 要解决的问题。
为什么需要 Agent
直接调用 LLM 对话 API 拿到的是模型根据上下文推理出的文字。模型不知道当前服务器时间,不知道你的业务库里有几条订单,也无法帮你发送邮件。传统的做法是,在调用 LLM 前人为拼好 prompt,把外部数据以文本方式注入,再让模型回答。这个链条一旦变长,逻辑就散落在应用代码各处——数据取用、结果拼装、多步推理全都靠开发者写死。
Agent 把这种“推理—行动”的循环规范化了。Agent 仍然由 LLM 驱动,但它不再只输出给用户看的内容,还会输出结构化的工具调用意图。宿主程序拿到这个意图去执行真实的函数,把返回值送回给模型,模型再决定下一步是继续调用工具还是直接给出最终回答。这就把 LLM 从纯对话引擎变成了一个可以操作外部世界的调度中心(参见参考链接 Red Hat 博客)。
你可以把 Agent 理解为:带着一组可以随时取用的工具,并且知道什么时候该用哪个工具的程序。
Agent 与普通 LLM 对话接口的区别
从调用方的角度看,普通对话接口是一个简单的输入-输出:
js
// 普通对话
const answer = await llm.chat({
messages: [...history, { role: "user", content: "北京今天天气" }]
});
// answer 直接是模型返回的文本
console.log(answer); // "抱歉,我无法获取实时天气"即使模型猜测你想问天气,它也没有获取天气的能力,只能编造或拒绝。要让它能回答,你需要事先把天气数据拼进 prompt,但模型本身不知道这个数据是外部来的——它只是看到一段文本。
Agent 接口改变了这个交互模式。模型不再直接返回纯文本,而是返回一类特殊消息——工具调用。典型的结构类似:
json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}
]
}宿主程序检测到 tool_calls 后,在本地执行 get_weather({ city: "北京" }),将返回值封装成一条 tool 消息追加到对话历史中,再次请求模型。模型拿到真实的天气数据后,才用自然语言总结返回给用户。这个循环被称为 Agent Loop:模型思考 → 输出工具调用 → 宿主执行工具 → 结果返回模型 → 模型输出最终回答。
一个完整的示例流程如下(以 Red Hat 示例中的 FavoriteColorTool 为例)。
- 用户输入:
My city is Montreal and my country is Canada - 模型输出 thought:“用户想知道最喜欢的颜色,我需要调用 FavoriteColorTool”,并生成
tool_name: "FavoriteColorTool",tool_input: { city: "Montreal", country: "Canada" } - 宿主执行工具,得到
tool_output: "red" - 模型拿到
tool_output,生成final_answer: "Your favorite color is red."
这种模式的关键差异在于:模型不再只是生成文本,而是变成了能够调用外部工具的执行者。它不需要把所有知识压缩进参数,而是在推理过程中动态获取外部信息。
下面是一段极简的 Agent 循环骨架(伪代码),展示工具调用的基本流转:
js
async function agentLoop(userInput) {
const messages = [
{ role: "system", content: "You are a helpful assistant with tools." },
{ role: "user", content: userInput }
];
while (true) {
const response = await llm.chat({ messages });
if (response.tool_calls) {
// 追加模型产生的工具调用消息
messages.push({ role: "assistant", tool_calls: response.tool_calls });
for (const call of response.tool_calls) {
const toolResult = await executeTool(
call.function.name,
JSON.parse(call.function.arguments)
);
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(toolResult)
});
}
// 继续循环,让模型基于工具结果再次推理
} else {
return response.content; // 模型认为可以给出最终回答
}
}
}实际实现还需要处理错误、截断以及防止无限循环,但整体骨架正是如此。
Node.js 技术栈概览
为什么选择 Node.js
用 Node.js 构建 Agent,有几个现实原因:
- 生态对齐:LLM 生态的主流库(LangChain、LlamaIndex、Bee Agent Framework 等)都把 TypeScript/JavaScript 作为第一或第二支持语言,很多模型提供者也直接发布 Node.js SDK。
- 异步天然匹配 Agent Loop:Agent Loop 的多轮请求-执行-返回模式,本质上是一个异步流程。Node.js 的事件驱动模型和
async/await语法,在处理“请求模型 → 等待结果 → 执行工具 → 再次请求”这类链路时几乎没有心智负担。 - 全栈同构:如果 Agent 最终需要嵌入到 Web 服务、边缘函数或 Electron 应用中,一套语言就能跑通。
常用库与工具
开发 Agent 不会从零拼 HTTP 请求。目前有几类工具会反复出现:
LLM 客户端与框架:openai 官方包提供了完整的 chat completion 接口,也支持 function calling。更上层的 Agent 框架,如 LangChain.js、Bee Agent Framework,帮你封装了 Agent Loop、工具注册、消息管理等机制。
HTTP 客户端:Node.js 18+ 内置的 fetch 或独立的 undici,在调用远程 LLM、中间件或第三方 Tool 时都足够用。
运行时与依赖管理:Node.js LTS 版本,配合 npm 或 yarn,是当前默认的组合。如果需要类型检查,TypeScript 几乎是必备的。
环境准备
Node.js 版本与 npm
Agent 项目依赖 fetch、AbortController、crypto 等较新的内置模块,建议使用 Node.js 18 LTS 或 20 LTS。检查版本:
bash
node -v # 预期 v18.x 或 v20.x
npm -v # 预期 9.x 或 10.x包管理方面,package.json 中会声明 type: "module" 以使用 ES Modules,这是主流 LLM 库默认支持的模块格式。
LLM 服务访问方式
Agent 需要一个能理解工具调用格式的 LLM。常见途径:
- 云端 API:直接使用 OpenAI、Anthropic 等服务的 API Key,通过 HTTPS 请求调用。这种方式免运维,但需要网络和账户。
- 本地或私有部署:通过 Ollama、vLLM 等工具,在本地或内网启动兼容 OpenAI API 格式的服务,Agent 连接
http://localhost:11434/v1这样的地址即可。数据不出本机,适合原型阶段或对数据敏感的场景。
无论哪种方式,最终在代码里都是一个 baseURL 和 apiKey 的组合,例如:
js
const client = new OpenAI({
baseURL: process.env.LLM_BASE_URL || "http://localhost:11434/v1",
apiKey: process.env.LLM_API_KEY || "ollama"
});这种方式可以无缝切换云端和本地模型,而不用修改 Agent 的核心逻辑。
学习路线图
整个系列的目标是从零开始,带你用 Node.js 构建一个可扩展的 Agent 系统。七篇文章的递进关系如下:
- 概述(本篇):建立对 Agent 的整体认知,确认技术环境。
- Agent 核心概念与工作原理:深入 Agent Loop、ReAct 范式、工具调用生命周期。
- 工具定义与注册:如何用 Node.js 编写和注册 Tool,包括参数校验、错误处理和异步工具。
- LLM 接入与 Function Calling:对接 OpenAI / 本地模型的 function calling,解析工具调用并处理流式响应。
- Agent 循环实现:从零写一个可运行的 Agent Loop,处理多轮工具调用、停止条件与异常。
- Memory 与会话管理:为 Agent 加上记忆,覆盖对话历史、摘要和向量索引三种常见策略。
- 部署与服务化:将 Agent 打包为 HTTP 服务或 CLI 工具,加上流式输出、日志、配置管理。
每一篇都会在前一篇的基础上增加一个功能模块,最终形成一个功能完备的脚手架项目。如果对 Node.js 本身还不熟悉,可以参考 Nodejs-Roadmap 项目补充基础模块知识。
注意点
- 工具返回错误时的处理:Agent 不能假定工具一定成功。如果用户输入信息不足,工具会收到空参数并返回错误。此时模型需要根据错误信息,转而向用户追问缺失的信息,而不是凭空编造(参见 Red Hat 示例中的错误处理流程)。在设计工具时,应当让错误信息足够明确,比如“缺少 city 参数”,而不是直接丢出一个
Error对象。 - 工具调用的限制:LLM 的单次推理受 context window 限制。当工具返回的结果非常大(例如一次数据库查询返回上千条记录),需要做截断或摘要,否则可能超出 token 上限。
- 版本兼容性:function calling 的具体 JSON 结构在不同模型和库版本间略有差异(例如 OpenAI 的
tool_calls和 Anthropic 的tool_use),封装llm.js时建议对标准化结构做一层适配。 - 本地模型的能力差异:使用 Ollama 等本地模型时,务必确认模型支持 tool calling 格式。部分小模型可能仅支持文本生成,无法可靠地输出结构化工具调用,此时需要切换模型或使用 prompt 工程模拟,但模拟的稳定性远低于原生支持。
