一、文章目的
帮助学习了解agent的核心原理
二、原理拆解
2.1 解决用户输入&输出的交互
在 Node.js 中,你可以使用内置的 readline 模块来实现不断读取用户命令行输入并执行不同逻辑的功能。以下是一个完整的实现示例。(当然还可以使用 commander 这个流行的库来实现,咱们就先简单实现)
js
const readline = require('readline');
// 创建 readline 接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: '> ' // 命令提示符
});
// 显示欢迎信息
console.log('欢迎使用命令行交互工具!');
console.log('可用命令:');
console.log(' hello - 显示问候信息');
console.log(' time - 显示当前时间');
console.log(' info - 显示系统信息');
console.log(' exit - 退出程序');
console.log('');
// 开始提示符
rl.prompt();
// 监听用户输入
rl.on('line', (input) => {
// 去除首尾空白字符
const command = input.trim().toLowerCase();
// 根据不同命令执行不同逻辑
switch (command) {
case 'hello':
console.log('你好!欢迎使用命令行工具。');
break;
case 'time':
console.log(`当前时间:${new Date().toString()}`);
break;
case 'info':
console.log('系统信息:');
console.log(` 节点版本: ${process.version}`);
console.log(` 平台: ${process.platform}`);
console.log(` 架构: ${process.arch}`);
break;
case 'exit':
console.log('再见!');
rl.close();
return; // 退出当前回调
default:
console.log(`未知命令: ${command}`);
console.log('可用命令: hello, time, info, exit');
}
// 重新显示提示符,等待下一次输入
console.log('');
rl.prompt();
});
// 监听关闭事件
rl.on('close', () => {
console.log('\n程序已退出。');
process.exit(0);
});
2.2 实现和AI大模型的一问一答交互
这里没什么特别的,就是普通的请求调用逻辑,我们基于deepseek实现一个简单演示代码,如下:
js
const DEEPSEEK_API_KEY = 'xxxxxx'; // 这里需要替换成你自己的api,一般不要定义在项目里面,有泄漏风险
/**
* 调用 DeepSeek API 获取模型回复
* @param messages 对话消息列表,包含 system、user 和 assistant 角色的消息
* @returns 模型生成的回复文本
* @throws 如果 API 请求失败或返回格式不正确,将抛出错误
*/
async function callLLMs(messages) {
const res = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${DEEPSEEK_API_KEY}`,
},
body: JSON.stringify({
model: 'deepseek-chat',
messages,
temperature: 0.35,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`DeepSeek API 错误: ${res.status} ${text}`);
}
const data = await res.json();
const content = data.choices?.[0]?.message?.content;
if (typeof content !== 'string') {
throw new Error('DeepSeek 返回内容为空');
}
return content;
}
module.exports = { callLLMs };
我们可以编写测试代码来执行一下
js
const { callLLMs } = require('./agent');
// 测试代码
async function run(input) {
const messages = [
{ role: 'system', content: '你是一个专业的助手' },
{ role: 'user', content: input },
];
const response = await callLLMs(messages);
return response;
}
// 测试代码
run('你好').then(console.log);
2.3 增加工具调用
做完上面的两步,那么将他们组合一下,你就能得到一个命令行的AI对话工具,当然,agent不止如此,我们接着往下走,思考一下,我们期望大模型在合适的时间调用我们提供的tools, 当然大模型并不知道我们有哪些工具,所以我们需要告诉大模型,我们有哪些工具
我们可以通过提示词构造来告诉大模型当前有哪些可用的工具列表:
js
const { callLLMs } = require('./agent');
// 通过系统提示词来告诉大模型角色定位,可用工具列表,工具参数,返回结构等。
const systemPrompt = `
你是天气查询的工具型助手,回答要简洁。
可用工具列表如下(action 的 tool 属性需与下列名称一致):
- getTime: 返回当前 time 字符串,参数为空。
- getWeather: 返回模拟天气信息字符串,参数为 JSON,如 {"city":"上海","time":"2026-02-27 10:00"}。
回复格式(严格使用 XML,小写标签): 对问题的简短思考 工具输入 等待 后再继续思考。 如果已可直接回答,则输出: 最终回答(中文,必要时引用数据来源)
规则:
每次仅调用一个工具;工具输入要尽量具体,根据用户输入,识别意图,如果需要调用工具,必须提供必要的参数,不允许使用大模型的默认工具。
如果拿到 observation 后有了答案,应输出 而不是重复调用。
未知工具时要说明,但仍用 XML 格式。
避免幻觉,不确定时请说明。请用中文回答。
`;
const question = `现在几点`;
const history = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question },
];
async function run() {
const response = await callLLMs(history);
console.log(response);
}
run();
我们看一下返回的是啥
js
<think>
用户问现在几点,我需要获取当前时间。我可以使用 getTime 工具来获取当前时间字符串。
</think>
<action>
<tool>getTime</tool>
<input></input>
</action>
我们需要做啥:通过正则解析返回,发现需要进行工具调用,则解析出工具名,参数,然后本地调用得到结果后,再回给大模型作为上下文,继续后面的逻辑
- 实现解析函数
js
const parseAssistantResponse = (content) => {
const parsed = {
actions: [], // 存储多个 action 的数组
final: null,
};
// 1. 提取所有 <action> 标签
const actionRegex = /<action>([\s\S]*?)<\/action>/gi;
let actionMatch;
while ((actionMatch = actionRegex.exec(content)) !== null) {
const actionContent = actionMatch[1];
// 2. 解析当前 action 中的 tool 和 input
const toolMatch = actionContent.match(/<tool>([\s\S]*?)<\/tool>/i);
const inputMatch = actionContent.match(/<input>([\s\S]*?)<\/input>/i);
if (toolMatch) {
const actionItem = {
tool: toolMatch[1].trim(),
input: inputMatch ? inputMatch[1].trim() : '',
};
parsed.actions.push(actionItem);
}
}
// 3. 提取 <final> 标签
const finalMatch = content.match(/<final>([\s\S]*?)<\/final>/i);
if (finalMatch) {
parsed.final = finalMatch[1].trim();
}
return parsed;
};
我们构造一个大模型的返回结果,进行一个工具解析测试:
js
const response = `
<think>用户问现在几点,我需要获取当前时间。我可以使用 getTime 工具来获取当前时间字符串。</think>
<action>
<tool>getTime</tool>
<input></input>
</action>
<action>
<tool>getTime2</tool>
<input></input>
</action>
<final>当前时间是 10:00。</final>
`;
const parseRes = parseAssistantResponse(response);
console.log(parseRes);
查看打印的测试结果
js
{
actions: [ { tool: 'getTime', input: '' }, { tool: 'getTime2', input: '' } ],
final: '当前时间是 10:00。'
}
那下一步,思路就清晰很多了,我们针对大模型的返回,判断是否已经得到了最终结果(判断条件是:final有值),有结果,直接返回, 如果没有最终结果,说明还是只是中间过程(final没有值),中间过程需要继续解析工具得到结果,然后继续进行大模型处理
- 接下来,我们就在本地执行大模型解析后需要调用的工具函数,得到返回结果,然后拼接到大模型的输入上下文里面,继续进行用户的提问处理流程。
2.4 增加agent的工具处理循环
上一个步骤我们能注意到,只是处理了单次对话,单次工具调用,实际场景下,我们几乎会遇到多个工具的调用,我们需要一个循环来不断处理这些工具的调用结果。
js
const { callLLMs } = require('./agent');
const systemPrompt = `
你是天气查询的工具型助手,回答要简洁。
可用工具列表如下(action 的 tool 属性需与下列名称一致):
- getTime: 返回当前 time 字符串,参数为空。
- getWeather: 返回模拟天气信息字符串,参数为 JSON,如 {"city":"上海","time":"2026-02-27 10:00"}。
回复格式(严格使用 XML,小写标签): 时间获取,必须使用本地工具,对问题的简短思考 工具输入 等待 后再继续思考。
如果已可直接回答,则输出: 最终回答,必须使用xml格式,使用final标签包裹(中文,必要时引用数据来源)
规则:
每次仅调用一个工具;工具输入要尽量具体,根据用户输入,识别意图,如果需要调用工具,必须提供必要的参数,不允许使用大模型的默认工具。
如果拿到 observation 后有了答案,应输出 而不是重复调用。
未知工具时要说明,但仍用 XML 格式。
避免幻觉,不确定时请说明。请用中文回答。
`;
const parseAssistantResponse = (content) => {
const parsed = {
actions: [], // 存储多个 action 的数组
final: null,
};
// 1. 提取所有 <action> 标签
const actionRegex = /<action>([\s\S]*?)<\/action>/gi;
let actionMatch;
while ((actionMatch = actionRegex.exec(content)) !== null) {
const actionContent = actionMatch[1];
// 2. 解析当前 action 中的 tool 和 input
const toolMatch = actionContent.match(/<tool>([\s\S]*?)<\/tool>/i);
const inputMatch = actionContent.match(/<input>([\s\S]*?)<\/input>/i);
if (toolMatch) {
const actionItem = {
tool: toolMatch[1].trim(),
input: inputMatch ? inputMatch[1].trim() : '',
};
parsed.actions.push(actionItem);
}
}
// 3. 提取 <final> 标签
const finalMatch = content.match(/<final>([\s\S]*?)<\/final>/gi);
if (finalMatch) {
parsed.final = finalMatch[0].trim();
}
return parsed;
};
const TOOLKIT = {
getTime: () => '2026-03-16 15:15',
getWeather: (input) => {
const { city, time } = JSON.parse(input);
return `模拟天气信息:${city} ${time} 晴转多云,温度 25°C,湿度 60%`;
},
};
/**
* Agent 主循环,负责与 LLM 交互、解析回复、调用工具并更新对话历史
* @param question
* @returns 最终回答字符串,或错误提示
*/
async function AgentLoop(question) {
const maxStep = 10;
const history = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question },
];
for (let step = 0; step < maxStep; step++) {
const assistantText = await callLLMs(history);
console.log(`\n[LLM 第 ${step + 1} 轮输出]\n${assistantText}\n`);
history.push({ role: 'assistant', content: assistantText });
const parsed = parseAssistantResponse(assistantText);
console.log(parsed, '本轮解析转化结果');
// 如果已经获得了处理结果,则直接返回结果
if (parsed.final) {
return parsed.final;
}
// 如果有 action,则调用工具并添加到对话历史中
if (parsed.actions?.length) {
const actionName = parsed.actions[0]?.tool;
const actionParams = parsed.actions[0]?.input;
const toolFn = TOOLKIT[actionName];
let observation;
observation = toolFn ? await toolFn(actionParams) : `未知工具: ${actionName}`;
history.push({
role: 'user',
content: `<observation>${observation}</observation>`,
});
continue;
}
break; // 未产生 action 或 final
}
return '未能生成最终回答,请重试或调整问题。';
}
async function run() {
const question = `现在几点`;
const response = await AgentLoop(question);
console.log(response);
}
// 启动测试
run();
三、下一步Todo
现在,组合上面的代码,监听用户输入输出,调用agent解析循环,就能实现一个迷你版本的获取时间,和天气的agent,后续我们需要进行工程化版本的生产级别的agent应用,还需要更多的封装,和支持网络能力,文件读写能力,上下文token监控和上下文压缩等等 我们还可以使用开源的框架进行项目搭建,比如:ai-sdk.dev/docs/agents... 基础原理,其实这篇文章看完也应该能理解大部分了,剩下的就交给大家自行探索了。