Skip to content

Vercel AI SDK 最小用法 #71

@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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions