Skip to content

第 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 只是一个变量,装不下多次调用的历史。

→ 下一步:用消息数组存历史,用循环替代写死的步骤。

MIT 许可证开源