导读
这一天,我们只做一件事:构建 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)。生产方案:多层压缩策略。