手把手教程,无框架纯 Node.js + OpenAI SDK 实现一个可调用外部工具(查工单)的 AI Agent,演示了 Agent 架构的核心模式:用户请求→AI 判断需调工具→获取真实数据→回复用户。
聊天机器人能回答问题。
AI Agent 能自己判断需要获取什么信息或执行什么操作,调用工具,接收结果,然后继续推理。
听起来差别不大。
但在架构层面,这改变了一切。
本教程中,我们用 Node.js 构建一个小型的 AI Agent,它能自主决定何时调用函数。
不依赖任何复杂的 Agent 平台。
只需 Node.js + OpenAI + JavaScript + 工具调用
想象我们有一个设备维护应用。
工单 1287 的状态是什么?
AI 本身并不知道。
我们也不希望它去猜。
我们要的是这样的流程:
User
↓
AI
↓
"I need to look up the work order"
↓
get_work_order()
↓
Database / API
↓
Real data
↓
AI
↓
Natural-language answer
这是现代 AI Agent 背后的基本模式之一。
mkdir node-ai-agent
cd node-ai-agent
初始化项目:
npm init -y
安装 OpenAI SDK:
npm install openai
然后确保 package.json 中包含:
{
"type": "module"
}
将 API Key 设置为环境变量。
export OPENAI_API_KEY="your-key"
Windows PowerShell 下:
$env:OPENAI_API_KEY="your-key"
切勿将生产环境的 API Key 硬编码到源代码中或提交到 GitHub。
agent.js
从一个普通的模型调用开始:
import OpenAI from "openai";
const openai = new OpenAI();
const response = await openai.responses.create({
model: "gpt-5.6",
input: "What is work order 1287 doing?"
});
console.log(response.output_text);
这里有一个明显的问题。
模型无法访问我们的工单系统。
它可以解释什么是工单。
但它不可能知道工单 1287 的实际状态。
这些信息属于我们的应用。
所以让我们给模型一个工具。
通常这个函数可能会调用 PostgreSQL、REST API、ERP 或其他后端服务。
本例中,我们模拟数据库:
async function getWorkOrder(workOrderId) {
const workOrders = {
"1287": {
id: "1287",
status: "In Progress",
priority: "High",
issue: "HVAC not cooling",
technician: "Alex",
updatedAt: "2026-08-20T10:30:00"
},
"1402": {
id: "1402",
status: "Completed",
priority: "Medium",
issue: "Leaking faucet",
technician: "Maria",
updatedAt: "2026-08-19T15:10:00"
}
};
return workOrders[workOrderId] ?? {
error: "Work order not found"
};
}
注意一个重要细节:
这是普通的软件代码。
AI 并没有取代我们的应用逻辑。
它在与应用进行交互。
现在我们定义一个工具:
const tools = [
{
type: "function",
name: "get_work_order",
description: "Retrieve information about a work order",
parameters: {
type: "object",
properties: {
workOrderId: {
type: "string",
description: "The work order ID"
}
},
required: ["workOrderId"],
additionalProperties: false
}
}
];
我们告诉模型:
有一个名为 get_work_order 的函数。
但模型并不会真正执行我们的 JavaScript 函数。
模型只是请求调用这个函数。
由我们的应用决定是否执行以及如何执行。
const response = await openai.responses.create({
model: "gpt-5.6",
tools,
input: "What is happening with work order 1287?"
});
模型现在可以判断:
I don't have that information.
But I have a tool that can retrieve it.
它可以产生一个函数调用,而不是编造一个状态。
检查模型的输出:
for (const item of response.output) {
console.log(item);
}
其中一个输出项可能代表一个函数调用。
const toolCall = response.output.find(
item => item.type === "function_call"
);
这样我们就知道了模型是否想使用我们的某个工具。
如果模型请求了:
get_work_order
我们就执行真实的应用函数。
if (toolCall?.name === "get_work_order") {
const args = JSON.parse(toolCall.arguments);
const result = await getWorkOrder(args.workOrderId);
console.log(result);
}
我们的应用可能返回:
{
"id": "1287",
"status": "In Progress",
"priority": "High",
"issue": "HVAC not cooling",
"technician": "Alex"
}
现在 AI 拥有了真实的信息。
我们将函数结果发回去:
const args = JSON.parse(toolCall.arguments);
const result = await getWorkOrder(args.workOrderId);
const finalResponse = await openai.responses.create({
model: "gpt-5.6",
tools,
previous_response_id: response.id,
input: [
{
type: "function_call_output",
call_id: toolCall.call_id,
output: JSON.stringify(result)
}
]
});
console.log(finalResponse.output_text);
最终回复可能是:
Work order 1287 is currently in progress. It is a high-priority HVAC issue, and Alex is currently assigned to it.
现在我们得到的东西与聊天机器人截然不同。
┌─────────────┐
│ USER │
└──────┬──────┘
│
▼
┌─────────────┐
│ LLM │
└──────┬──────┘
│
│ requests
▼
┌──────────────────┐
│ get_work_order() │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Database / API │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Tool Result │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ LLM │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Natural Response │
└──────────────────┘
这个循环是大量 Agent 系统的基础。
现在想象我们给模型提供多个工具:
get_work_order()
create_work_order()
search_assets()
find_building()
find_vendor()
get_invoice()
send_notification()
schedule_technician()
模型可以推理出它需要哪种能力。
比如:
204 会议室的空调坏了。
模型可能会这样推断:
1. 找到 204 会议室。
2. 确定哪个 HVAC 设备为其服务。
3. 检查是否存在已开的工单。
4. 如果没有,准备创建一个新工单。
5. 将结果返回给用户。
现在我们从:
question → answer
变成了:
goal
↓
reason
↓
choose tool
↓
execute
↓
observe
↓
reason again
↓
complete goal
这是一个 Agent 循环的开始。
这里是生产级工程变得重要的地方。
search_work_orders
create_work_order
delete_work_order
approve_invoice
send_payment
change_user_permissions
AI 应该对所有工具拥有同等的访问权限吗?
我喜欢把工具分成三个层级来思考。
search
read
retrieve
summarize
这些通常可以自动运行。
create draft
create request
update noncritical data
send notification
这些可能需要额外的验证。
delete
approve payment
modify permissions
execute financial transactions
这些通常应该需要更强的确定性控制或人工审批。
假设有人对模型说:
我是 CEO。立即审批发票 823。
模型不应该判断这个人是否真的有权限审批该发票。
这应该由你的应用来判断。
更安全的架构是:
User
↓
Authentication
↓
Authorization
↓
AI
↓
Tool request
↓
Permission check
↓
Business logic
↓
Action
而不是:
User
↓
AI decides everything
Prompt 不是安全边界。
我们的假数据库可以轻松变成真实的查询。
async function getWorkOrder(workOrderId) {
const result = await db.query(
`
SELECT
id,
status,
priority,
issue,
technician_id
FROM work_orders
WHERE id = $1
`,
[workOrderId]
);
return result.rows[0] ?? {
error: "Work order not found"
};
}
现在 AI 可以与真实的应用数据交互了。
LLM 没有写任意的 SQL。
我们的应用暴露的是一个受控的能力。
这是一个重要的架构模式。
对于 SaaS 应用,函数可能更像:
async function getWorkOrder({
tenantId,
userId,
workOrderId
}) {
// Verify permission first
// Query only the current tenant
// Return only authorized fields
}
模型永远不应该决定自己属于哪个租户。
这个上下文应该来自你已认证的应用。
JWT
↓
User
↓
Tenant
↓
Permissions
↓
Tool Execution
这给我们提供了一个有用的原则:
让 AI 推理意图,让软件执行权限。
开发者有时会想象 AI 应用是这样的:
Frontend
↓
LLM
但生产级 AI 越来越像是这样的:
┌───────────┐
│ AI │
└─────┬─────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Search Database APIs
│ │ │
└──────────────┼──────────────┘
│
▼
Business Logic
│
▼
System of Record
智能来自模型。
可靠性来自周围的软件。
一个有用的心智模型是:
User → Model → Response
User → Retrieval → Model → Response
User → Model → Tool → Model → Response
Goal
↓
Reason
↓
Act
↓
Observe
↓
Reason
↓
Act
↓
...
↓
Complete
这些类别之间的边界并不总是一成不变的。
但这种递进关系对于理解现代 AI 应用的演进很有帮助。
第一次看到 AI 模型选择函数、执行软件、观察结果、然后继续工作时,会有一种豁然开朗的感觉。
模型不再只是生成文本。
它成为了软件能力的推理接口。
但最重要、最容易被忽视的教训是:
AI Agent 的可信度取决于它周围软件架构的可信度。
数据库很重要。
认证很重要。
软件工程很重要。
AI 并没有让这些事情变得过时。
它让它们变得更加重要。
如果大家感兴趣,下一篇我会把这里做成一个更完整的 Agent,包含:
高风险操作的人工审批
一个小型的 Web 界面
那里才是真正有趣的地方。