-
Notifications
You must be signed in to change notification settings - Fork 6
Description
为什么需要 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 zodProvider 配置
这段代码用 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 接收 apiKey 和 baseURL,返回一个 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-code 的 src/agent/loop.ts 里,onStepFinish 用来实时打印每一步的执行过程,就是这个用法。
streamText
streamText 是 generateText 的流式版本,适合需要实时展示输出的场景(如命令行打字机效果、Web 流式响应)。API 与 generateText 几乎一致,把函数名换掉即可。mini-claude-code 当前使用 generateText,streamText 不在本文范围内。
与 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 回调 |