2026年7月3日 3 分钟阅读

从零搭建自定义 AI Agent 循环:Open Agent Loops 实战教程

tinyash 0 条评论

如果你写过使用 Claude Code、Codex 或 Cursor 的自动化脚本,可能已经发现一个尴尬的事实:每个 AI 编码工具都有自己的 Agent 循环实现——有的用 LangChain、有的自己写 while 循环、有的封装在 SDK 里。当你需要自己搭建一个轻量、可控的 Agent 循环时,往往要重新造轮子。

Open Agent Loops 是一个极简的、与模型无关(provider-agnostic)的 Agent 循环 SDK。它的核心理念是”插拔式 seam 接口”——模型客户端、记忆、工具、停止条件,每一个组件都放在一个可替换的接口后面。你不需要因为换模型就重写整个循环,也不需要因为增加一个工具就修改主逻辑。

本文将带你通过三个实战场景,从零搭建自己的 Agent 循环。

安装与环境

Open Agent Loops 以 npm 包形式发布,需要 Node.js 20.6+(支持 --env-file 参数)和兼容 OpenAI 格式的 API 端点。

npm install @open-agent-loops/core openai zod
npm install -D tsx

创建 .env 文件,配置你的模型连接信息:

LLM_API_KEY=***            # API key for the endpoint
LLM_MODEL=zai-org/GLM-5.2  # model id to call

tsx 用于直接运行 TypeScript 文件,不需要额外的编译步骤。

实战场景 1:单轮天气查询 Agent

最简单的用例:一个单次对话的 Agent,用户输入城市名,Agent 调用天气工具返回结果。

以下是完整的 single-turn-loop.ts

import { AgentEventType, defineTool, runAgent, SessionMemoryStore } from "@open-agent-loops/core";
import type { AgentEvent } from "@open-agent-loops/core";
import { OpenAICompatibleModel } from "@open-agent-loops/core/providers/openai";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { z } from "zod";

const weather = defineTool({
  name: "weather",
  description: "Get the current weather for a city.",
  parameters: z.object({ city: z.string().describe("City to look up.") }),
  execute: async ({ city }) => {
    return { content: `Sunny in ${city}` };
  },
});

const model = new OpenAICompatibleModel({
  apiKey: process.env.LLM_API_KEY,
  baseURL: process.env.LLM_BASE_URL ?? "https://api.featherless.ai/v1",
  model: process.env.LLM_MODEL ?? "zai-org/GLM-5.2",
  thinking: "on",
});

function render(e: AgentEvent) {
  switch (e.type) {
    case AgentEventType.ToolStart:
      console.log(`→ ${e.toolName}(${JSON.stringify(e.args)})`);
      break;
    case AgentEventType.ToolEnd:
      console.log(`← ${e.toolName} [${e.isError ? "error" : "ok"}]: ${e.result}`);
      break;
    case AgentEventType.TextDelta:
      process.stdout.write(e.text);
      break;
  }
}

const rl = createInterface({ input, output });
const prompt = await rl.question("you › ");
rl.close();

const result = await runAgent({
  model,
  memory: new SessionMemoryStore(),
  sessionId: "single-turn-demo",
  prompt,
  tools: [weather],
  onEvent: render,
});

运行命令:

npx tsx --env-file=.env single-turn-loop.ts

输入 What's the weather in Paris?,你会看到 Agent 自动识别需要调用天气工具,返回结果后终止对话。

这个场景展示了 Agent 循环的最小工作单元:一个工具(defineTool)、一个模型客户端(OpenAICompatibleModel)、一个事件渲染器(onEvent)和一次对话(runAgent)。所有 seam 都可以单独替换——比如把 SessionMemoryStore 换成 Redis,或者把 weather 工具换成真实的 API 调用。

实战场景 2:多轮对话聊天机器人

单轮对话有用,但大多数场景需要 Agent 记住上下文。第二个场景建立一个持续对话的聊天机器人,复用同一个记忆存储(memory)和会话 ID(sessionId),让每一轮对话都能看到之前的内容。

import { AgentEventType, defineTool, runAgent, SessionMemoryStore } from "@open-agent-loops/core";
import type { AgentEvent } from "@open-agent-loops/core";
import { OpenAICompatibleModel } from "@open-agent-loops/core/providers/openai";
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { z } from "zod";

const weather = defineTool({
  name: "weather",
  description: "Get the current weather for a city.",
  parameters: z.object({ city: z.string().describe("City to look up.") }),
  execute: async ({ city }) => {
    return { content: `Sunny in ${city}` };
  },
});

const model = new OpenAICompatibleModel({
  apiKey: process.env.LLM_API_KEY,
  model: process.env.LLM_MODEL,
  baseURL: process.env.LLM_BASE_URL ?? "https://api.featherless.ai/v1",
  thinking: "on",
});

function render(e: AgentEvent) {
  switch (e.type) {
    case AgentEventType.AgentStart:
      console.log(`▶ start · session ${e.sessionId}`);
      break;
    case AgentEventType.TurnStart:
      console.log(`\n— turn ${e.step} —`);
      break;
    case AgentEventType.ReasoningDelta:
      process.stdout.write(`\x1b[2m${e.text}\x1b[22m`);
      break;
    case AgentEventType.TextDelta:
      process.stdout.write(e.text);
      break;
    case AgentEventType.ToolStart:
      console.log(`→ ${e.toolName}(${JSON.stringify(e.args)})`);
      break;
    case AgentEventType.ToolEnd:
      console.log(`← ${e.toolName} [${e.isError ? "error" : "ok"}]: ${e.result}`);
      break;
    case AgentEventType.AgentEnd:
      console.log(`\n■ done · ${e.steps} steps`);
      break;
  }
}

const memory = new SessionMemoryStore();
const sessionId = "chat";
const rl = createInterface({ input, output });

while (true) {
  const prompt = (await rl.question("\nyou › ")).trim();
  if (prompt === "" || prompt === "exit") break;

  process.stdout.write("bot › ");
  await runAgent({ model, memory, sessionId, prompt, tools: [weather], onEvent: render });
}
rl.close();

运行后,你可以先问 “What’s the weather in Paris?”,然后接着问 “And in London?”——Agent 会理解你指的是天气,并返回伦敦的结果。这种跨轮次的上下文能力来自 SessionMemoryStore + 固定的 sessionId 组合。

关键细节SessionMemoryStore 将整段对话加载到模型上下文中,因此不用额外配置向量数据库。如果对话太长,你可以换用外部存储(Redis、SQLite),实现自己的 Memory 接口即可。

实战场景 3:自定义事件渲染 + 工具调用

第三个场景展示如何利用 Agent 事件流的细粒度控制,实现更好的用户体验。Open Agent Loops 的事件系统包含多种事件类型:

  • AgentStart/AgentEnd:循环开始/结束,携带步骤数和会话 ID
  • TurnStart:每轮对话开始,包含步骤编号
  • ReasoningDelta:模型的推理思维链路(reasoning channel)
  • TextDelta:模型输出的文本流片段
  • ToolStart/ToolEnd:工具调用的开始和结束,含参数和结果
  • Message:完整的消息完成事件

你可以为每种事件类型定制渲染逻辑。例如,在工具调用开始时打印 -> 箭头和参数,结束时打印 <- 箭头和结果;推理链用灰色显示以区分于最终答案;每轮对话用 — turn N — 分隔。

这种细粒度的事件流让同一个 Agent 循环既能驱动 CLI 工具,也能驱动 Web 页面或 TUI 界面——只需换一个 onEvent 回调函数,不需要修改循环的逻辑。

最佳实践

  1. 使用 Zod 校验工具参数defineToolparameters 字段使用 Zod schema,不仅提供类型安全,还能让模型更准确地理解参数结构。总是给字段加上 .describe() 描述。
  1. 善用 sessionId 管理对话:固定 ID 创建持续对话,每次生成新 ID 创建独立会话。如果需要持久化记忆,将 SessionMemoryStore 替换为外部存储实现。
  1. 模型无关性是真优势:将模型配置放在环境变量中,可以在 Featherless、vLLM、Together、Groq 等兼容 OpenAI 格式的提供商之间无缝切换——修改 .env 即可,不需要改代码。
  1. 事件驱动渲染避免阻塞runAgent 通过 onEvent 回调异步推送事件,你的渲染函数可以处理 UI 更新而不阻塞循环。如果渲染处理耗时较长,考虑在回调中使用缓冲或节流策略。
  1. 停止条件可以自定义:除了 maxStepswhenToolCalled 等内置停止条件,你可以通过实现 StopCondition 接口添加自定义逻辑——比如基于 Token 计数、运行时长度或特定的模型响应模式。

总结

Open Agent Loops 以一个包、三个 seam(模型/记忆/工具)和一条 runAgent() 调用,提供了一套极简可扩展的 Agent 循环方案。如果你厌倦了在 LangChain 的重磅框架和手写 while 循环之间反复横跳,它值得你试试。

相关链接

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。