第 1 步:工具与两种输出
本节讲两件事:模型怎么"说出"它想调工具,以及工具结果怎么回到它手里。
模型就是一个函数
先把"Agent""框架"这些词放一边。从框架的视角看,模型就是一个函数:
输入 string ──→ [模型] ──→ 输出 string但这样的函数答不了"现在几点"——它没有外部信息。
工具:模型做不到的事,交给外部函数
模型不知道时间?写一个函数给它:
ts
function getTime(): string {
return new Date().toLocaleString("zh-CN");
}这就是工具(Tool)。定义很朴素:模型自己完成不了、需要外部代码代劳的能力。
模型怎么表达"我要调工具"
麻烦来了。如果模型只能返回 string:
- 返回
"现在是 12 点"—— 这是答案? - 返回
"getTime"—— 还是它想调一个叫 getTime 的工具?
框架分不清。所以让模型返回两种带标签的结构:
ts
type ModelOutput =
| { type: "tool"; name: string } // 我要调工具
| { type: "answer"; content: string }; // 我给最终答案type 字段就是标签。框架看一眼就知道该执行工具,还是该收工。
为什么这样设计
这在 TypeScript 里叫 discriminated union(可辨识联合)。好处是编译器能帮你收窄类型—— 写 if (output.type === "answer") 之后,TS 就知道这个分支里一定有 content 字段。
关键:工具结果必须回到模型手里
这是最容易被忽略的一环。看模型的签名:
ts
function model(toolResult?: string): ModelOutput {
if (toolResult === undefined) {
return { type: "tool", name: "getTime" }; // 还没结果,先查
}
return { type: "answer", content: `现在是 ${toolResult}` }; // 有结果了,回答
}参数是 toolResult。模型第一次被调用时手上什么都没有,只能请求工具; 拿到结果后再被调用,它才能给出答案。
执行完工具就把结果丢掉,模型是瞎的。 这条回路是 Agent 之所以是 Agent 的原因。
手动走两步
先不用循环,手动走一遍看清流程:
ts
console.log("用户: 现在几点?");
const first = model(); // 第一步:问模型
console.log("模型:", first);
if (first.type === "tool") {
const result = getTime(); // 执行工具
console.log("工具:", result);
const second = model(result); // 第二步:结果交回模型
console.log("模型:", second);
}运行
bash
npx tsx learn/1-tool.ts输出:
text
用户: 现在几点?
模型: { type: 'tool', name: 'getTime' }
工具: 2026/8/15 17:31:42
模型: { type: 'answer', content: '现在是 2026/8/15 17:31:42' }遗留问题
这里写死了两步。但模型可能要查 3 个工具、5 个工具。 而且 toolResult 只是一个变量,装不下多次调用的历史。
→ 下一步:用消息数组存历史,用循环替代写死的步骤。