用 TypeScript 写一个最小 CLI Agent:理解模型为什么会调用函数
最近在看岗位要求和面试方向时,我明显感觉到前端开发的边界正在变化。
以前前端更多关注:React / Vue、组件封装、工程化、性能优化、业务交互
但现在越来越多岗位开始出现:AI Agent、大模型应用开发、智能工作流、工具调用
这说明 AI Agent 已经不只是一个概念,而是逐渐变成一种真实的工程能力。
作为前端开发者,我更希望从熟悉的 TypeScript 开始理解它,而不是一上来就进入复杂的 Python Agent 生态。
所以这篇文章会用一个最小的 CLI Agent 来解释:
- 自然语言是怎么进入程序的?
- 模型为什么会调用我们定义的函数?
- 函数执行之后,结果又是怎么回到模型里的?
什么是 CLI
CLI 你可以理解成:用“打字”的方式操作软件。
平时我们用网页 / App 界面 像是你去餐厅看菜单,然后点菜。
CLI 像是你直接跟服务员说:“给我来一碗牛肉面,不要香菜。”
所以一句话总结:
CLI 就是一种通过终端输入命令来操作程序的方式。
比如:
npm install
npm run dev
git status
而 CLI Agent 就是通过终端来直接点菜
用户输入
-> 模型理解
-> 工具调用
-> 函数执行
-> 输出结果
第一步:用 CLI 接收自然语言
我们先不接模型,只看如何从终端接收一段自然语言。
这里用 commander 写一个最小 CLI:
// test.ts
import { Command } from "commander";
const program = new Command();
program
.name("task-agent")
.description("一个最小 CLI Agent 示例")
.version("0.1.0");
program
.argument("[prompt...]", "你想交给 Agent 的任务")
.option("-y, --yes", "自动确认")
.action((promptParts: string[], options: { yes?: boolean }) => {
const prompt = promptParts.join(" ").trim();
if (!prompt) {
program.help();
}
console.log("用户输入:", prompt);
console.log("是否自动确认:", options.yes === true);
});
program.parse(process.argv);
运行:
node test.ts "帮我规划一个 3 天学习计划" -y // 这里的 `-y` 就是我们前面通过 `.option("-y, --yes", "自动确认")` 定义的命令选项
输出:
用户输入:帮我规划一个 3 天学习计划
是否自动确认:true
这里最关键的是:
.argument("[prompt...]", "你想交给 Agent 的任务")
[prompt...] 表示命令后面的内容都可以收进来。
然后通过:
const prompt = promptParts.join(" ").trim();
把命令行参数整理成一段自然语言。
到这里,我们还没有 Agent,也没有模型,只是完成了第一步:把终端输入变成 prompt 字符串
第二步:创建一个 Agent
接下来创建 Agent。
可以先把 Agent 理解成一个“带工具的模型角色”。
它一般包含三类信息:
model 使用哪个模型
instructions 给模型的行为规则
tools 模型可以调用的工具
示例代码:
const agent = new Agent({
name: "个人任务执行 Agent",
model: "gpt-4.1-mini",
instructions: [
"你是一个个人任务执行 Agent。",
"当用户给出一个宽泛目标时,把它拆成具体小任务。",
"如果需要保存任务,请调用 create_task 工具。",
"最终回复要简洁,并说明你做了什么。",
].join("\n"),
tools: [createTaskTool],
});
这段代码的重点不是 name,而是:
instructions
tools
instructions 告诉模型应该怎么思考。
tools 告诉模型它可以调用哪些函数。
如果没有 tools,模型只能生成文本。
有了 tools,模型就可以在需要时请求调用某个函数。
第三步:普通 TypeScript 函数如何变成工具
先写一个普通函数。
比如我们想创建一个任务:
type Task = {
title: string;
description: string;
priority: "low" | "medium" | "high";
};
const tasks: Task[] = [];
async function createTask(input: Task) {
tasks.push(input);
return {
ok: true,
task: input,
};
}
这个函数本身和 AI 没有任何关系。
它只是普通 TypeScript 代码:
接收任务信息
写入 tasks 数组
返回创建结果
现在我们把它包装成一个 tool:
const createTaskTool = tool({
name: "create_task",
description: "当用户需要创建或保存一个任务时,调用这个工具。",
parameters: z.object({
title: z.string(),
description: z.string(),
priority: z.enum(["low", "medium", "high"]),
}),
execute: async (input) => {
return createTask(input);
},
});
一个 tool 通常包含四个关键部分:
name 工具名
description 工具说明
parameters 参数结构
execute 真正执行的函数
这里的 execute 才是真正会运行的 TypeScript 代码。
模型不会直接执行 createTask。
模型只会根据工具描述,决定:
我要不要调用 create_task?
如果调用,应该传什么参数?
模型为什么会调用这个函数
这是理解 Agent 最关键的一点。
很多人第一次看到 tool calling 时,会以为:
是不是我定义了函数,模型就自动知道什么时候调用?
更准确地说,不是“自动知道”,而是 SDK 会把工具信息一起发给模型。
当我们运行 Agent 时,模型收到的不只是用户输入:
帮我规划一个 3 天学习计划
它还会收到工具列表,类似下面这种结构化信息:
[
{
"name": "create_task",
"description": "当用户需要创建或保存一个任务时,调用这个工具。",
"parameters": {
"title": "string",
"description": "string",
"priority": "low | medium | high"
}
}
]
也就是说,模型在生成回答前,会同时看到:
用户想做什么
系统给它的 instructions
当前有哪些 tools 可以用
每个 tool 是干什么的
每个 tool 需要什么参数
然后模型会做一次判断:
用户只是问一个概念?
-> 直接回答文本
用户想创建任务?
-> 调用 create_task
所以,模型会调用函数的原因是:
我们把函数包装成 tool,并把 tool 的名字、说明和参数结构暴露给了模型。
模型根据用户意图和 tool 描述,选择是否发起 tool call。
它不是随便执行本地函数。
它只能调用我们暴露给它的工具。
tool call 不是函数调用本身
这里还有一个容易混淆的点。
模型并不会真的在它自己那里执行 TypeScript 函数。
它生成的是一个 tool call 请求,类似:
{
"name": "create_task",
"arguments": {
"title": "第 1 天:了解 CLI 和 Agent 基础",
"description": "学习 CLI 如何接收自然语言,并理解 Agent 的基本结构。",
"priority": "high"
}
}
然后 SDK 看到这个 tool call,才会在本地执行:
const tool = tools["create_task"];
const result = await tool.execute({
title: "整理项目文档",
description: "明天整理项目文档",
priority: "high"
});
也就是:
execute: async (input) => {
return createTask(input);
}
所以真正的执行链路是:
模型生成 tool call
-> SDK 校验参数
-> SDK 调用 execute
-> execute 执行 TypeScript 函数
-> 函数结果返回给模型
这点非常重要。
Agent 并不是让模型直接操作你的系统。
而是让模型在你提供的一组工具里做选择。
第四步:Runner 负责跑完整流程
定义好 Agent 和 tools 之后,需要 Runner 把它们跑起来。
可以把 Runner 理解成执行引擎。
它负责:
把用户 prompt 发给模型
把 tools 信息发给模型
接收模型返回
如果模型请求调用工具,就执行对应工具
再把工具结果交回模型
最后拿到最终回答
示例代码:
const runner = new Runner();
const result = await runner.run(agent, prompt);
console.log(result.finalOutput);
如果模型调用了工具,流程大概是:
runner.run(agent, prompt)
-> 模型返回 tool call
-> Runner 找到对应 tool
-> Runner 执行 tool.execute
-> 工具结果返回给模型
-> 模型生成最终回复
这样,一个最小 Agent 流程就跑起来了。
第五步:人工确认
如果一个工具只是查询信息,通常可以直接执行。
但如果工具会产生副作用,比如:
写文件
发消息
删除数据
提交代码
调用付费接口
就不应该让模型直接执行。
这时可以给工具加上人工确认:
const createTaskTool = tool({
name: "create_task",
description: "当用户需要创建或保存一个任务时,调用这个工具。",
parameters: createTaskSchema,
needsApproval: true,
execute: async (input) => {
return createTask(input);
},
});
needsApproval: true 的意思是:模型想调用这个工具时,先暂停。让用户确认之后,再真正执行。
这个过程常被叫做:human-in-the-loop
也就是人在关键操作前参与确认。
比如模型想创建任务时,CLI 可以先打印:
模型想调用 create_task:
{
"title": "第 1 天:了解 Agent 基础",
"priority": "high"
}
是否批准?[y/N]
用户输入 y 后,程序才真正执行 createTask。
整体流程回顾
现在我们再回头看整个流程:
1. 用户在 CLI 输入一句自然语言
2. CLI 把输入整理成 prompt
3. Runner 把 prompt、instructions、tools 发给模型
4. 模型根据用户意图和 tool 描述判断是否调用工具
5. 如果要调用工具,模型生成 tool call
6. SDK 校验 tool 参数
7. 如果需要审批,先让用户确认
8. 审批通过后执行 execute
9. TypeScript 函数产生真实结果
10. 工具结果返回给模型
11. 模型生成最终回复
可以浓缩成一句话:Agent = 模型 + 指令 + 工具 + 执行循环
其中最关键的是:
模型负责判断
工具负责行动
代码负责真正的副作用
人负责关键审批
总结
这个最小 CLI Agent 想说明的是:
- Agent 不是让模型随便操作系统。
- Agent 是让模型在一组受控工具里做决策。
我们定义工具时,其实是在告诉模型:
- 你可以使用这些能力。
- 每个能力是做什么的。
- 调用时需要传哪些参数。
模型根据用户输入和工具描述,决定是否发起 tool call。
真正执行动作的,仍然是我们写的 TypeScript 函数。
而这些能力,本质上都建立在今天这条最小链路上:自然语言 -> 模型判断 -> 工具调用 -> 代码执行