【31-Ai-Agent】ai-agent的核心实现细节-bysking

一、文章目的

帮助学习了解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... 基础原理,其实这篇文章看完也应该能理解大部分了,剩下的就交给大家自行探索了。

参考:github1s.com/minorcell/m...

相关推荐
从文处安2 小时前
「前端何去何从」(React教程)React 状态管理:从局部 State 到可扩展架构
前端·react.js
一拳不是超人2 小时前
Three.js一起学-如何通过官方例子高效学习 Three.js?手把手带你“抄”出一个3D动画
前端·webgl·three.js
椰子皮啊2 小时前
400行Node.js搞定mediasoup信令转换:一次跨语言"表白"实录
前端·架构
果然_2 小时前
告别混淆!Git 多账号按域名/目录自动切换身份的终极指南
前端
Wect2 小时前
React Scheduler & Lane 详解
前端·react.js·面试
myNameGL2 小时前
ArkTs核心语法
前端·javascript·vue.js
重庆穿山甲2 小时前
从零到精通:OpenClaw完整生命周期指南
前端·后端·架构
浏览器API调用工程师_Taylor2 小时前
web逆向之小红书无水印图片提取工具
前端·javascript·逆向
程序员阿峰2 小时前
【JavaScript面试题-作用域与闭包】什么是闭包?闭包在实际开发中有什么应用和潜在问题(如内存泄漏)?
前端·面试