Agent 7小时通关计划 - Day1: 最小 IO 核心 —— 让 LLM 开口说话

6 阅读1分钟

导读

这一天,我们只做一件事:构建 AI Agent 的最小 IO 循环

听起来简单?但这个循环是所有后续能力的基石——Day 2 的工具调用、Day 3 的自主推理、Day 6 的多代理协作,全都是在这个 IO 循环上叠加的。Claude Code、Cursor、Windsurf…所有 AI 编码工具的核心都是同一个循环:

用户输入 → LLM 思考 → 流式输出 → 等待下一轮

学完这一天,你会理解:

  • 为什么 Agent 本质是一个 while(true) 循环
  • 流式输出和非流式输出的技术选型
  • messages 数组如何承载多轮对话的上下文

1.1 核心概念

Agent 的本质是 IO

很多人觉得 AI Agent 很神秘,但剥开所有抽象,它就是一个输入→处理→输出的循环:

┌──────────┐    prompt    ┌──────────┐    stream    ┌──────────┐
│  user    │─────────────→│  LLM API │────────────→ │ console  │
│  (stdin) │              │ (OpenAI) │              │ (stdout) │
└──────────┘              └──────────┘              └──────────┘
       ↑                                                │
       └────────────────────────────────────────────────┘
                         下一轮输入

这和一个聊天机器人有什么区别?现在——没有区别。 但从 Day 2 开始,我们会在这个循环里插入工具调用,让 LLM 从"只会说话"变成"能做事"。

Streaming vs Non-Streaming

对比维度

非流式 (stream: false)

流式 (stream: true)

用户体验

等几秒后一次性输出

逐字打出(打字机效果)

首 Token 延迟

高(需等全部生成完)

低(第一个 token 即输出)

适用场景

工具调用解析 JSON

自然语言回复

错误处理

简单(一次性成功或失败)

复杂(流中断需处理)

Day 1 用流式,Day 2 工具调用切非流式——这是 Claude Code 的真实做法。

1.2 环境搭建

初始化项目

mkdir bun-agent-tutorial && cd bun-agent-tutorial
bun init -y
bun add openai dotenv

配置 .env

# 支持任何 OpenAI 兼容接口:GLM、Gemini、DeepSeek...
OPENAI_API_KEY=your-api-key-here
OPENAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4
MODEL_NAME=glm-4-flash

💡 提示:我们使用 OpenAI SDK 的 baseURL 参数连接不同厂商的 API。只要它兼容 OpenAI Chat Completions 格式,就能直接用。

1.3 核心代码解析

初始化客户端

import OpenAI from "openai";

// 从环境变量加载配置
const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL,
});

const MODEL = process.env.MODEL_NAME || "glm-4-flash";

流式对话函数

这是今天代码的核心——一个带流式输出的对话函数:

async function chat(messages: OpenAI.Chat.ChatCompletionMessageParam[]) {
  const stream = await client.chat.completions.create({
    model: MODEL,
    messages,
    stream: true, // ← 开启流式
  });

  let fullContent = "";

  // 逐 chunk 读取,实现打字机效果
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta?.content || "";
    process.stdout.write(delta); // 不换行,逐字输出
    fullContent += delta;
  }

  console.log(); // 最后换行
  return fullContent;
}

逐行解析:

作用

stream: true

告诉 API 不要等全部生成完再返回,而是边生成边推送

for await (const chunk of stream)

异步迭代器,每收到一个 chunk 就执行一次循环体

chunk.choices[0]?.delta?.content

每个 chunk 只包含"增量"内容(几个字或一个词)

process.stdout.write(delta)

不换行输出,让文字看起来像在"打字"

fullContent += delta

累积完整内容,后面推入 messages 数组用于多轮对话

多轮对话循环

import readline from "readline";

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

function askQuestion(prompt: string): Promise<string> {
  return new Promise((resolve) => rl.question(prompt, resolve));
}

// --- 主循环 ---
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
  {
    role: "system",
    content: "你是一个友好的 AI 助手,专注于帮助用户解决编程问题。",
  },
];

console.log('🤖 AI 助手已启动(输入 "exit" 退出)\n');

while (true) {
  const userInput = await askQuestion("You: ");
  if (userInput.toLowerCase() === "exit") break;

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

  process.stdout.write("AI: ");
  const reply = await chat(messages);

  messages.push({ role: "assistant", content: reply });
}

rl.close();

关键机制——messages 数组

第 1 轮:
  messages = [system, user1]
  → LLM 回复 assistant1
  → messages = [system, user1, assistant1]

第 2 轮:
  messages = [system, user1, assistant1, user2]
  → LLM 回复 assistant2(它能看到之前的对话)
  → messages = [system, user1, assistant1, user2, assistant2]

每一轮都把完整历史发给 LLM——这就是多轮对话的秘密。代价是 messages 会越来越长,这个问题 Day 5 会解决。

1.4 运行

bun run day1_init/index.ts


🤖 AI 助手已启动(输入 "exit" 退出)

You: 你好,帮我解释一下什么是 Agent
AI: Agent(智能代理)是一个能够自主感知环境、做出决策并执行动作的...

You: 那它和普通聊天机器人有什么区别?
AI: 好问题。普通聊天机器人只能被动回复,而 Agent 能...

1.5 常问问题

Q: 流式输出和非流式有什么区别?什么时候用哪个?

参考回答

流式适合自然语言输出——降低首 Token 延迟,提升用户体验。非流式适合需要解析完整 JSON 的场景(比如 Function Calling),因为流式的增量 chunk 无法直接 JSON.parse

Claude Code 的做法是:文本回复用流式,工具调用用非流式(或流式收集完再解析)。

Q: messages 数组无限增长怎么办?

参考回答

这是上下文管理的核心问题。简单方案:保留 system prompt + 最近 N 条。进阶方案:用 LLM 摘要旧消息(Day 5)。生产方案:多层压缩策略。