AI代理的最小可用循环

在本系列中,我们将从最简单的骨架开始,逐步构建一个小型代理系统

AI代理的最小可用循环
AI模型价格对比 | AI工具导航 | ONNX模型库 | Vibe Coding教程 | Tripo 3D | Meshy AI | ElevenLabs | KlingAI | ArtSpace | Phot.AI | InVideo

我喜欢学习新事物,我最喜欢的学习方式是从具体和小的地方开始。我不想从大型框架或复杂的架构图开始。我想亲眼看到某个东西在工作,即使第一个版本非常有限。

这就是为什么我要在这个通讯中开始一个名为从零开始构建AI代理的新系列。

在接下来的几期中,我将逐步介绍如何从零开始构建一个小的代理系统。我们将从一个非常简单的REPL开始,然后逐步添加工具,引入简单的插件模式,用嵌入和RAG构建记忆系统,以及后来探索路由和规划。

最终的结果将是一个小型个人知识助手。你可能会发现它直接对你的工作流有用,但更大的目标是理解代理系统背后的架构。一旦活动部件清晰了,你可以将同样的想法应用到自己的项目中。

使用TensorBoard Projector的嵌入可视化

当前的AI趋势中,有很多资源、教程和演示。但我仍然很难找到实用的指南,一步步展示底层到底发生了什么。我希望这个系列能帮助填补这个空白。

在本期中,我们将从骨架开始:AI代理的最小有用形态。

我还为本期录制了一个YouTube版本,在其中我逐步构建了第一个版本。如果你更喜欢观看代码的构建过程,可以在这里查看视频:YouTube视频

如果你一直在听人们谈论AI代理,你可能想知道底层到底发生了什么。这个术语可能听起来很模糊,特别是当人们用它来描述从编码助手到工作流自动化工具的一切时。但在核心,这个想法出人意料地简单。

**代理是一个带循环的程序。**它读取输入,决定下一步做什么,采取行动,观察结果,然后重复。

那个循环就是基础。一旦你能清楚地看到它,许多代理系统就会变得更容易理解。

这里的目标不是构建一个生产就绪的框架。目标是让活动部件可见。我们将从一个微小的程序开始,将它连接到本地语言模型,然后给它一个真正的工具来获取实时天气数据。

最后,我们将拥有代理的基本骨架。

1、为什么从小处开始?

当人们谈论AI代理时,例子往往很快变得复杂。可能涉及多个工具、记忆、规划、文件访问、浏览器自动化、后台任务或代码执行。

这些是有用的功能,但它们也可能隐藏下面简单的想法。

所以,不要从一个完整的助手开始,让我们从最小的可能版本开始:一个持续询问输入的命令行程序。

async function main() {
  const rl = createInterface({ input, output });

  while (true) {
    // 1) 读取
    let line = await rl.question("You> ");

    // 2) 解析/分支(命令 vs 普通输入)
    if (shouldExit(line)) break;

    // 3) 行动 — 打印出该行
    output.write(`Assistant> ${line}\n\n`);
  }

  rl.close();
}

你输入一些东西。程序读取它。它用那个输入做一些事情。然后等待下一行。

这已经是一个循环了。

在这个阶段,还没有智能参与。程序只是将输入回显给你。如果你输入:

hello

它会回复:

hello

显然,这还没有用。但结构很重要。程序已经在做我们需要的基本事情了:

读取输入
决定做什么
采取行动
重复

这就是我们将不断改进的骨架。"决定"步骤目前很简单,"行动"步骤也很简单。但系统的形状已经存在了。

2、用模型调用替换回显

下一步是用语言模型调用替换回显行为。程序不再将用户输入直接发送回终端,而是将其发送给模型。

async function main() {
  const rl = createInterface({ input, output });

  /** @type {{ role: string, content: string }[]} */
  const messages = [];

  while (true) {
    let line = await rl.question("You> ");
    // 省略了检查

    messages.push({ role: "user", content: trimmed });

    const data = await ollamaChat(messages);
    const assistant = data?.message;
    const reply =
      assistant?.content?.trim?.() ??
      "(no text from model — check model / Ollama logs)";

    // 保留转录,以便模型在下一轮有上下文
    messages.push({ role: "assistant", content: reply });

    output.write(`\nAssistant> ${reply}\n\n`);
  }
}

在这个例子中,我使用Ollama。如果你之前没用过Ollama,可以把它想象成一个本地模型服务器。它在你的机器上运行一个模型,给你的应用程序一个可以与之对话的API。

这意味着我的Node.js程序不需要包含模型本身。它只需要向Ollama发送请求。Ollama在本地运行模型并将响应发送回来。

循环本身没有改变。只有行动改变了。

之前,程序将输入回显到终端:

用户输入 -> 回显

现在,程序将输入发送给模型并打印模型响应:

用户输入 -> 发送给模型 -> 打印模型响应

此时,程序变成了一个简单的本地聊天应用。它可以接收输入,将其发送到本地模型,并打印响应。

为了让对话正常工作,我们还需要保留一个消息列表。这个消息列表就是对话的转录。每条消息通常有一个角色和一些内容:

system     -> 指令或规则
user       -> 人类说的
assistant  -> 模型回复的

转录很重要,因为模型需要上下文。如果我们只发送最新的用户输入,模型不会知道对话中之前发生了什么。通过保留消息历史,我们给模型足够的上下文来自然地继续。

到目前为止,我们已经构建了有用的东西,但它仍然有一个重要的限制。模型只能根据它已经知道的东西或我们在提示中提供的信息来响应。

如果我们问:

墨尔本现在的天气怎么样?

模型无法可靠地自己回答。它可能会猜测,给出一个通用的答案,或者说它无法访问实时信息。

这就是工具变得重要的地方。

3、为什么工具重要

语言模型不会自动获取实时数据。它不会自动调用API、读取你的日历、检查天气或更新数据库。

它能做的是决定需要使用一个工具。然后你的应用程序执行那个工具。

这个区别很重要。模型不直接运行工具。它产生一个结构化请求,实际上是说:

我需要用这些参数调用这个工具。

然后应用程序决定如何处理那个请求。

例如,如果用户询问当前天气,模型可能会请求一个带有位置的 get_weather 工具:

tool: get_weather
arguments: { location: "Melbourne" }

然后应用程序运行实际的工具,获取天气数据,并将结果发送回模型。只有在那之后,模型才会为用户产生最终答案。

这就是一个简单的LLM聊天程序开始变成代理的地方。不是因为模型突然变得神奇,而是因为程序现在有了一个围绕模型的循环,可以在外部世界采取行动。

4、添加第一个工具

让我们添加一个真正的工具:get_weather

在我的例子中,我已经有一个小型的本地命令行天气工具。它接收一个位置并打印天气信息。从代理的角度来看,这个命令行工具变成了一个名为get_weather的能力。

但模型需要知道这个工具存在。所以当程序向Ollama发送请求时,它会在消息旁边包含一个工具定义。

概念上,程序在说:

你可以正常回答。

但如果你需要实时天气信息,
你可以调用get_weather并传入一个位置。

现在模型有了另一个选择。它不仅可以返回普通文本,还可以返回一个工具调用。

例如,用户可能会问:

墨尔本现在的天气怎么样?

模型可能会返回一个结构化请求:

调用get_weather,位置 = "Melbourne"

同样,模型不是在运行天气命令。Node.js程序在运行。在这个例子中,这个Node.js程序就是我们正在构建的代理运行时。

运行时接收工具调用,运行天气CLI,捕获结果,并将该结果作为工具消息添加回对话中。然后程序再次调用模型。

这一次,模型在转录中有天气数据,所以可以为用户生成一个有根据的答案。

流程看起来像这样:

用户提出问题
    ↓
模型决定是否需要工具
    ↓
应用程序执行请求的工具
    ↓
工具返回结果
    ↓
应用程序将结果发回模型
    ↓
模型产生最终答案

这就是为什么代理通常不只是一个单一的模型请求。它是一个循环。

模型决定下一步应该发生。运行时执行行动。结果被反馈给模型。然后模型再次决定。

5、代理循环

如果我们简化整个系统,代理循环看起来像这样:

当对话处于活跃状态时:
  读取用户输入
  将消息和工具发送给模型
  检查模型响应

  如果模型请求工具:
    执行该工具
    将工具结果添加到消息中
    再次调用模型

  否则:
    向用户显示最终响应

这就是核心思想。

LLM本身主要是文本输入和文本输出。代理是围绕LLM的循环。那个循环允许系统决定何时使用工具、执行行动、观察结果并继续。

一旦你理解了这个循环,许多代理系统就会变得不那么神秘。它们可能有更多工具。它们可能有记忆。它们可能规划多个步骤。它们可能与文件、浏览器、API或代码库交互。

但基本形状仍然相同:

决定
行动
观察
重复

6、要点

主要的要点是:LLM本身主要是文本输入和文本输出,而代理是围绕LLM的循环,可以决定何时使用工具并采取行动。

这个例子故意很小。它不是一个完整的助手或生产就绪的框架。它的目的是让活动部件可见。

一旦骨架清晰了,我们可以开始添加更实际的功能。

例如,不仅仅是获取天气数据,我们可以添加一个Google日历工具。然后代理不仅能回答问题,还能检查事件、找到空闲时间或创建日历条目。

这就是这个系列的方向。

我们将从这个小循环开始,然后逐步添加更多工具和设计决策,使代理系统逐步增长而不是变成一个黑盒。

如果你正在构建自己的代理系统,我认为这是最好的起点:不是从框架开始,而是从循环开始。

一旦你理解了循环,框架就变得更容易评估。你可以问出更好的问题:

模型调用在哪里?
工具在哪里定义的?
谁执行工具?
结果如何传回?
循环如何停止?

这些问题帮助你更清楚地看到系统。而这种清晰度比任何看起来很神奇的演示更有用。

在下一期中,我们将把这个骨架再推进一步,看看如何让工具更容易添加和管理。这是系统开始感觉不再像演示,而更像一个你可以扩展的小框架的地方。

如果你觉得这个主题对你有用,请留个评论让我知道你想看到什么样的代理工具被添加。天气是一个简单的起点,但日历、笔记、搜索和个人知识工具都打开了有趣的设计问题。


原文链接:Building an AI Agent from Scratch: The Smallest Useful Loop

汇智网翻译整理,转载请标明出处