基于 main.ts — 约 70 行,一个最小可运行的 AI Agent CLI。
1. 整体架构
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ user input │ ──→ │ Agent Loop │ ──→ │ DeepSeek API │
│ (readline) │ ←── │ (while true) │ ←── │ (streamText) │
└──────────────┘ └────────┬─────────┘ └─────────────────┘
│
│ tool-call
▼
┌──────────────────┐
│ readFile tool │
│ (fs.readFile) │
└──────────────────┘程序 = 模型 + 工具 + 多轮对话循环,三个要素缺一不可。
2. 依赖图谱
3. 逐段拆解
3.1 环境变量加载(第 8 行)
import "dotenv/config";副作用导入——执行时把 .env 中的 DEEPSEEK_API_KEY 注入 process.env。后续 @ai-sdk/deepseek 自动从环境变量读取,无需显式传参。
3.2 工具定义(第 16–24 行)
const readFile = tool({
description: "读取一个文本文件,返回完整内容",
inputSchema: z.object({
path: z.string().describe("要读取的文件路径"),
}),
execute: async ({ path }) => {
return await fs.readFile(path, "utf-8");
},
});Vercel AI SDK 的 tool() 接收三个核心字段:
这就是 function calling 的完整实现:描述 → 声明参数 → 执行函数 → 结果回传。
3.3 对话基础设施(第 30–38 行)
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q: string) => new Promise<string>((r) => rl.question(q, r));
const messages: Array<{ role: "user" | "assistant"; content: any }> = [];rl/ask:把 Node.js 回调式的readline包装成 Promise,方便在async函数里awaitmessages:对话历史数组,agent 的"记忆"。每轮把用户输入 + 模型的所有回复(文本、工具调用、工具结果)追加进去
3.4 Agent 主循环(第 40–67 行)
for (;;) {
const input = (await ask("\n > : ")).trim();
if (!input || input === "exit") break;
messages.push({ role: "user", content: input });
const result = streamText({
model: deepseek("deepseek-chat"),
messages,
tools: { readFile },
stopWhen: stepCountIs(10),
});
// 流式消费...
const { messages: newMessages } = await result.response;
messages.push(...(newMessages as any));
}每一步做了什么:
┌─ ① 读用户输入
│ 空输入 / "exit" → 退出循环
│
├─ ② 追加到 messages(role: "user")
│
├─ ③ 调用 streamText
│ 把 messages + tools 发给 DeepSeek
│ SDK 内部自动跑 multi-step:
│ model 输出 text ──→ model 输出 tool-call ──→ execute 工具 ──→ tool-result 回传 model ──→ model 输出 text ──→ ...
│ 最多跑 10 步(stepCountIs(10) 兜底)
│
├─ ④ 流式消费 fullStream
│ 把 text-delta / tool-call / tool-result 实时打印到终端
│
└─ ⑤ 拿 SDK 累积的新消息,追加进 messages
下一轮对话时模型就能"记住"之前发生了什么3.5 流式输出处理(第 52–63 行)
process.stdout.write("助手: ");
for await (const chunk of result.fullStream) {
if (chunk.type === "text-delta") process.stdout.write(chunk.text);
else if (chunk.type === "tool-call")
process.stdout.write(`\n [调用 ${chunk.toolName}(${JSON.stringify(chunk.input)})]`);
else if (chunk.type === "tool-result")
process.stdout.write(`\n [返回 ${String(chunk.output).length} 字节]\n助手: `);
}fullStream 是一个异步可迭代流,产出三种 chunk:
4. 数据流
用户: "帮我看看 package.json 里有哪些依赖"
messages = [
{ role: "user", content: "帮我看看 package.json 里有哪些依赖" }
]
│
▼
┌────────────────────────────────────────────────┐
│ streamText() │
│ │
│ Step 1: model → tool-call │
│ chunk: { type: "tool-call", │
│ toolName: "readFile", │
│ input: { path: "package.json" } } │
│ │
│ SDK: execute readFile({ path }) │
│ → 返回文件内容 (487 字节) │
│ │
│ chunk: { type: "tool-result", │
│ output: "...文件内容..." } │
│ │
│ Step 2: model → text-delta │
│ chunk: { type: "text-delta", │
│ text: "你的 package.json..." } │
│ ...更多 text-delta chunk... │
└───────────────────────────────────────────────┘
│
▼
result.response.messages = [
{ role: "assistant", content: [tool-call, text] },
{ role: "tool", content: "文件内容" }
]
│
▼
追加到 messages → 下一轮对话带上所有历史5. 关键设计决策
5.1 stopWhen: stepCountIs(10) — 防死循环
模型可能陷入"调工具 → 不满意 → 再调 → 再不满意 → …"的循环。stepCountIs(10) 是最简单的刹车:最多 10 步(一次 tool-call + tool-result = 1 步),超过就强制终止。
5.2 result.response.messages — SDK 帮你记账
不需要手动追踪模型发了哪些 tool-call、收到了哪些 tool-result——SDK 的 result.response.messages 包含了本轮生成的所有新消息(assistant 文本 + tool-call + tool-result),直接 concat 就行。
5.3 流式消费 — 用户体验
fullStream 让文本边生成边显示,用户不用盯着空白终端等 30 秒。tool-call/tool-result 也实时可见,让用户知道"模型在读文件,不是在发呆"。
6. 这段代码没做什么
这是刻意做减法的结果,以下是明确 不做 的事情(以及为什么不做):
这些是小册后续章节要逐一补齐的能力。
7. 快速参考
# 启动
npm run dev # 等同于 npx tsx main.ts
# 前提条件
# 1. Node.js ≥ 20.19
# 2. .env 中填入 DEEPSEEK_API_KEY=sk-...代码骨架(最小复刻)
// 1. 定义工具
const myTool = tool({ description: "...", inputSchema: z.object({...}), execute: async (args) => {...} });
// 2. 维护历史
const messages = [];
// 3. 主循环
for (;;) {
messages.push({ role: "user", content: await getUserInput() });
const result = streamText({ model, messages, tools: { myTool }, stopWhen: stepCountIs(10) });
for await (const chunk of result.fullStream) { /* 打印 */ }
messages.push(...(await result.response).messages);
}这就是 AI Agent 最朴素的形态
Agent-CLI 最小实现
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。




评论交流
欢迎留下你的想法