Skip to content

Vercel AI SDK 最小用法 #3

@minorcell

Description

@minorcell
Image

为什么需要 AI SDK

projects/agent-loop 里,我们用原始 fetch 手写了一个 ReAct Agent。它能跑通,但存在三个问题:

问题 1:多 Provider 适配成本高。 换一个模型提供商(DeepSeek → 七牛 → OpenAI),就要重写请求 URL、Header 格式、响应解析逻辑。

问题 2:工具调用状态机要自己维护。 agent-loop 里的 for 循环、parseAssistant()、往 history 里推 observation,这些都是在手写一个工具调用的状态机。稍有差错,模型就会丢失上下文。

问题 3:没有类型安全。 工具的输入参数是一个裸字符串,解析 JSON 要靠 try/catch,参数字段靠字符串 key 访问,TypeScript 无法帮你检查。

Vercel AI SDK 解决了这三个问题:

  • createOpenAI 创建 Provider,换模型只改一行
  • generateText + maxSteps 内置了工具调用状态机,自动处理多轮循环
  • zod 定义参数 schema,工具的 execute 函数拿到的是已解析、有类型的对象

安装

bun add ai @ai-sdk/openai zod

Provider 配置

这段代码用 createOpenAI 接入七牛的 OpenAI 兼容接口,导出一个 model 对象供后续使用。

// provider.ts
import { createOpenAI } from "@ai-sdk/openai"

const qiniu = createOpenAI({
  apiKey: process.env.QINIU_API_KEY!,
  baseURL: "https://api.qnaigc.com/v1",
})

// 换模型只改这一行
export const model = qiniu("qwen-max-latest")

createOpenAI 接收 apiKeybaseURL,返回一个 Provider 工厂函数。把模型名传给这个工厂函数,得到 model 对象。

如果要换成标准 OpenAI:

import { openai } from "@ai-sdk/openai"

export const model = openai("gpt-4o-mini")

最简调用:generateText

这段代码发送一个 prompt,等待模型完整返回,打印结果。

import { generateText } from "ai"
import { model } from "./provider"

const { text } = await generateText({
  model,
  prompt: "用一句话解释什么是递归。",
})

console.log(text)
// => "递归是一个函数在其定义中调用自身的编程技术。"

generateText 是非流式的,等模型生成完毕后一次性返回。返回值 text 是模型输出的字符串。


多轮对话

多轮对话的关键是维护一个 messages 数组。每轮结束后,把 response.messages 追加到历史,下一轮把完整历史传进去。

import { generateText, type CoreMessage } from "ai"
import { model } from "./provider"

let history: CoreMessage[] = []

async function chat(userInput: string) {
  // 本轮消息 = 历史 + 当前用户输入
  const messages: CoreMessage[] = [
    ...history,
    { role: "user", content: userInput },
  ]

  const result = await generateText({ model, messages })

  // 把本轮的消息追加到历史(包含 user 和 assistant 两条)
  history.push({ role: "user", content: userInput })
  history.push(...result.response.messages)

  return result.text
}

// 第一轮
const reply1 = await chat("我叫张三。")
console.log(reply1) // => "你好,张三!有什么可以帮你?"

// 第二轮:模型能记住"张三"这个名字
const reply2 = await chat("你还记得我叫什么吗?")
console.log(reply2) // => "当然,你叫张三。"

注意:result.response.messages 包含本轮所有 assistant 消息(在有工具调用时,还包含中间的 tool 消息)。直接追加整个数组,不要只追加 result.text


工具调用(Tool Calling)

工具调用是 AI SDK 的核心功能,也是理解 mini-claude-code 的关键。

1. 用 tool() + zod 定义一个工具

这段代码定义了一个 getWeather 工具,参数用 zod schema 描述,execute 是实际执行逻辑。

import { tool } from "ai"
import { z } from "zod"

const getWeather = tool({
  description: "查询指定城市的当前天气",
  parameters: z.object({
    city: z.string().describe("城市名称,例如:上海"),
  }),
  execute: async ({ city }) => {
    // city 是有类型的字符串,由 zod schema 保证
    return `${city} 今天晴,气温 22°C`
  },
})

description 告诉模型这个工具的用途。parameters 定义模型调用时必须传哪些字段。execute 是工具的实现,接收已解析的参数对象。

2. 注册到 generateText,加 maxSteps 启动循环

import { generateText } from "ai"
import { model } from "./provider"

const { text } = await generateText({
  model,
  prompt: "上海今天天气怎么样?",
  tools: { getWeather },
  maxSteps: 5, // 最多执行 5 步,防止无限循环
})

console.log(text)
// => "上海今天晴,气温 22°C,非常适合外出。"

SDK 自动处理的四步循环

不加 maxSteps 时,模型调用工具后 SDK 就停下来,把控制权交给你。加了 maxSteps 之后,SDK 自动完成以下循环:

第 1 步:发送 prompt 给 LLM
         ↓
         LLM 输出 tool_call: getWeather({ city: "上海" })
         ↓
第 2 步:SDK 调用 execute({ city: "上海" })
         ↓
         execute 返回 "上海今天晴,气温 22°C"
         ↓
第 3 步:SDK 把结果作为 tool result 追加到 messages,再次调用 LLM
         ↓
         LLM 生成最终回答(finishReason: "stop")
         ↓
第 4 步:generateText 返回 result.text

这就是 agent-loop 里手写 for 循环 + history.push(observation) 做的事情,SDK 帮你自动处理了。

完整示例

import { generateText, tool } from "ai"
import { z } from "zod"
import { model } from "./provider"

const getWeather = tool({
  description: "查询指定城市的当前天气",
  parameters: z.object({
    city: z.string().describe("城市名称"),
  }),
  execute: async ({ city }) => {
    return `${city} 今天晴,气温 22°C`
  },
})

const { text } = await generateText({
  model,
  prompt: "上海今天天气怎么样?",
  tools: { getWeather },
  maxSteps: 5,
})

console.log(text)

onStepFinish 回调

generateText 在每一步完成后触发 onStepFinish,可以用来观察模型的决策过程。教学场景下这非常有用。

const { text } = await generateText({
  model,
  prompt: "上海今天天气怎么样?",
  tools: { getWeather },
  maxSteps: 5,

  onStepFinish: ({ text, toolCalls, toolResults, finishReason }) => {
    console.log("── 步骤完成 ──────────────")

    if (text) {
      console.log("模型输出:", text)
    }

    for (const call of toolCalls) {
      console.log(`调用工具: ${call.toolName}`, call.args)
    }

    for (const result of toolResults) {
      console.log(`工具结果: ${result.toolName}`, result.result)
    }

    console.log("结束原因:", finishReason)
    // finishReason: "tool-calls" 表示本步触发了工具,"stop" 表示模型生成完毕
  },
})

finishReason 有两个常见值:"tool-calls" 表示本步模型决定调用工具,"stop" 表示模型直接给出了最终回答。

mini-claude-codesrc/agent/loop.ts 里,onStepFinish 用来实时打印每一步的执行过程,就是这个用法。


streamText

streamTextgenerateText 的流式版本,适合需要实时展示输出的场景(如命令行打字机效果、Web 流式响应)。API 与 generateText 几乎一致,把函数名换掉即可。mini-claude-code 当前使用 generateTextstreamText 不在本文范围内。


与 agent-loop 的对比

agent-loop(手写) ai-sdk-demo(SDK)
调用模型 手写 fetch + 解析 JSON generateText()
工具参数 JSON.parse + 手动校验 zod schema 自动解析
工具调用循环 手写 for 循环 + history.push maxSteps 自动处理
换 Provider 改 URL、Header、解析逻辑 换一行 createOpenAI
观察执行过程 console.log 散落各处 onStepFinish 回调

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions