Skip to content
概述
Vercel AI SDK 的 Core API 将语言模型的调用抽象为两个基础函数:generateText 和 streamText。两者共享同一个语言模型接口,但返回方式不同:generateText 等待完整输出后返回一个对象;streamText 立即返回一个流式结果对象,文本片段按顺序到达。以下内容以 Node.js + TypeScript 为运行环境,说明这两个函数的设计、执行流程和选型边界。
基本概念:模型、消息与工具调用
Core API 是 AI SDK 中与模型交互的最底层公共接口。它不依赖 React、Next.js 等框架,可以直接在 Node.js 进程、Edge Runtime 或现代浏览器中调用。语言模型(LanguageModel)是 Core API 最核心的抽象。
语言模型对象
几乎所有 Core API 函数都需要一个 model 参数。这个参数是一个对象,而不是模型名字符串。provider 包负责把模型服务封装成统一的对象。以 OpenAI 为例:
ts
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-4o-mini');model 对象内部实现了 AI SDK 定义的 provider 接口。在源码中,这一接口被称为 LanguageModelV3,包含 doGenerate 和 doStream 两个方法 [1]:
doGenerate:一次性获取完整生成结果。doStream:获取流式生成结果。
generateText 与 streamText 分别对应这两个方法。
消息与系统提示
文本输入有两种形式。最简单的是 prompt:
ts
const result = await generateText({
model,
prompt: '解释什么是异步迭代。'
});需要更精细的控制时,可以用 messages 数组。消息数组的每个元素包含 role 和 content,role 常见取值为 system、user 和 assistant。系统消息用来设定模型的行为边界:
ts
const result = await generateText({
model,
messages: [
{ role: 'system', content: '用中文回答,且不超过三句话。' },
{ role: 'user', content: '解释 Node.js 中的背压。' }
]
});工具调用
工具调用(tool calling)是模型输出的一部分。它表示模型不是直接返回文本,而是请求调用一个外部函数。AI SDK 的 tools 参数由一组工具定义组成。每个工具需要提供 description、parameters 和 execute:
ts
const result = await generateText({
model,
tools: {
getWeather: {
description: '获取指定城市的天气',
parameters: {
type: 'object',
properties: {
city: { type: 'string' }
},
required: ['city']
},
execute: async ({ city }) => {
return { city, weather: 'sunny' };
}
}
},
prompt: '北京天气如何?'
});模型不会直接执行 execute,它只负责输出一个工具调用请求。真正执行函数的代码位于 execute 中。执行结果会被送回模型,模型据此生成最终文本。对于非流式调用,这一系列交换都在 generateText 的等待过程中完成。
generateText:输入参数、返回结果与执行流程
输入参数
generateText 接受一个普通对象。常用字段包括:
model:语言模型对象,必填。prompt:字符串提示,与messages二选一。messages:结构化消息数组。tools:工具定义对象。temperature、maxOutputTokens等生成参数。abortSignal:AbortSignal实例,用于中断请求。
不同 provider 对生成参数的支持程度可能不同。不支持的参数会在请求阶段被忽略或导致报错,具体行为由 provider 决定。
返回结果
generateText 返回一个 Promise,resolve 后的对象包含以下常见字段:
text:完整文本字符串。如果模型输出不是文本,该字段可能是空字符串。finishReason:生成结束原因。'stop'表示模型自然停止。usage:token 用量统计。reasoning:部分模型在回答前会生成推理内容,SDK 将其保存在该字段中。sources:模型引用的来源列表,由支持该能力的 provider 提供。
执行流程
调用 generateText 后,SDK 会执行以下步骤:
- 合并
prompt或messages、tools与生成参数,形成一个标准化的模型请求。 - 调用
model.doGenerate(request)。 doGenerate向模型服务发送 HTTP 请求,并等待完整响应。- SDK 将 provider 的原始响应解析成统一的
GenerateResult对象。
对调用方来说,这些步骤表现为一个 await:
ts
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await generateText({
model: openai('gpt-4o-mini'),
prompt: '用一句话说明 Stream API。',
maxOutputTokens: 100
});
console.log(result.text);
console.log('结束原因:', result.finishReason);这段代码会阻塞在 await 上,直到模型输出全部结束。对于长文本,用户需要等待完整内容生成后才能看到第一个字符。这是非流式生成最明显的特征。
streamText:输入参数与流式生成
streamText 的输入参数与 generateText 基本一致。它同样支持 model、prompt、messages、tools 和 abortSignal。不同之处在于返回值。
调用 streamText 不需要 await:
ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = streamText({
model: openai('gpt-4o-mini'),
prompt: '请从 1 数到 10。'
});streamText 返回一个流式结果对象,该对象的核心成员是 textStream 和 fullStream。它们都是异步可迭代的流。
模型输出产生后,SDK 会持续解析 HTTP 响应。每收到一个文本片段,就向 textStream 中写入一个字符串。调用方可以通过 for await...of 消费这些片段:
ts
for await (const delta of result.textStream) {
process.stdout.write(delta);
}这段代码会逐字打印模型输出。由于 streamText 不等待完整输出,第一个字符到达的时间通常远早于完整生成结束。
如果需要把片段收集成完整文本,可以在循环中拼接:
ts
let fullText = '';
for await (const delta of result.textStream) {
fullText += delta;
}
console.log(fullText);这种聚合方式等价于 generateText 返回的 text,但它保留了对中间过程的控制。
textStream 与 fullStream:两种流的使用方式
textStream
textStream 是一个 ReadableStream<string> 类对象。它只产生字符串片段,适合需要逐字渲染的场景。例如,在终端里模拟打字效果:
ts
const result = streamText({
model,
prompt: '介绍 Web Streams 的基本概念。'
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
await new Promise((resolve) => setTimeout(resolve, 30));
}这里每收到一个 chunk,就等待 30 毫秒,形成逐字输出的效果。这种延迟不影响数据源本身,只是消费者主动放慢了读取速度。
fullStream
fullStream 产生结构化的事件对象。每个对象都有一个 type 字段,用于区分事件类型。常见的事件类型包括:
text-delta:一段文本增量。tool-call:模型发出的工具调用。finish:生成结束。
下面的示例演示了如何同时处理文本增量、工具调用和结束事件:
ts
const result = streamText({
model,
tools: {
currentTime: {
description: '获取当前时间',
parameters: {},
execute: async () => new Date().toString()
}
},
prompt: '现在几点?'
});
for await (const part of result.fullStream) {
if (part.type === 'text-delta') {
process.stdout.write(part.textDelta);
} else if (part.type === 'tool-call') {
console.log('\n调用工具:', part.toolName);
} else if (part.type === 'finish') {
console.log('\n生成完成:', part.finishReason);
}
}需要注意,textStream 和 fullStream 来自同一个底层流,因此通常只选择其中一个进行消费。如果同时消费两个流,事件可能被随机分配到其中一侧,导致数据不完整。
异步迭代、背压与取消机制
异步迭代
ES2018 引入了异步迭代器协议。for await...of 可以顺序读取异步数据源。AI SDK 的文本流实现了这一协议,因此可以用 for await...of 直接读取:
ts
for await (const delta of result.textStream) {
// 处理每一个文本片段
}这个语法要求运行环境支持异步迭代器。Node.js 10 之后的版本基本都支持。Edge Runtime 也支持基于 Web Stream 的异步迭代 [2]。
背压
背压是指当消费者处理速度慢于生产者时,数据流会自动调整速度的机制。textStream 基于 Web Stream,天然具备背压能力。消费者每次从流中读取下一个片段时,底层会准备下一个数据。如果缓冲的数据超过一定阈值,流的实现会暂停向网络层请求更多数据,从而避免内存无限增长。
这种机制与 generateText 不同。generateText 必须把完整输出缓存在内存中,直到 HTTP 响应结束。streamText 则允许调用方以可控的速度处理输出。
取消机制
streamText 和 generateText 都支持通过 AbortSignal 取消请求。常用的做法是创建一个 AbortController,在需要停止生成时触发 abort():
ts
const controller = new AbortController();
const result = streamText({
model,
prompt: '生成一份很长的报告。',
abortSignal: controller.signal
});
// 5 秒后停止生成
setTimeout(() => controller.abort(), 5000);
for await (const delta of result.textStream) {
process.stdout.write(delta);
}取消后,textStream 可能会抛出一个 AbortError,也可能会直接结束。具体行为取决于运行环境。在浏览器环境中,调用 controller.abort() 通常会中断底层请求。
对于 generateText,取消请求会使得 await 抛出错误,而不是返回部分结果。因此,如果业务需要处理“生成到一半被中断”的场景,更适合使用 streamText。
provider 适配:同步与流式接口的统一封装
AI SDK 与模型 provider 之间的边界非常清楚。在源码中,语言模型接口 LanguageModelV3 要求实现两个方法 [1]:
doGenerate(options):返回完整生成结果。doStream(options):返回流式生成结果。
generateText 在内部调用 doGenerate,streamText 调用 doStream。SDK 负责把用户的参数转换为统一格式,再把不同 provider 的响应转换为统一的 API。
下面是一个简化示意,展示 provider 如何实现这两个方法:
ts
class MyProviderModel implements LanguageModelV3 {
async doGenerate(options) {
const response = await this.client.generate(options);
return {
text: response.choices[0].text,
finishReason: mapFinishReason(response.choices[0].finishReason),
usage: response.usage
};
}
async doStream(options) {
const stream = await this.client.stream(options);
return {
stream: convertToWebStream(stream)
};
}
}这不是某个真实 provider 的代码,只是用来表达接口形态。实际 provider 会有更复杂的参数映射、错误处理和版本适配。
这种双方法设计也出现在其他语言生态中。例如,xAI 的 Go provider 提供了 LanguageModel 接口,并实现了 DoGenerate 与 DoStream [4]。这说明同步/流式双方法是模型 provider 的常见封装模式。
对比示例与迁移方式:从 generateText 到 streamText
同一个文本生成需求,可以用 generateText 或 streamText 实现。
使用 generateText:
ts
const result = await generateText({
model,
prompt: '写一段关于异步 IO 的文字。'
});
console.log(result.text);使用 streamText:
ts
const result = streamText({
model,
prompt: '写一段关于异步 IO 的文字。'
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}迁移时,需要改变两处:
- 去掉
await,将generateText({...})改为const result = streamText({...})。 - 将原来读取
result.text的地方改为消费result.textStream,并在循环中拼装完整文本。
如果有多个函数调用了同一个非流式结果,可以先定义一个聚合函数:
ts
async function streamToText(result) {
let text = '';
for await (const delta of result.textStream) {
text += delta;
}
return text;
}然后:
ts
const result = streamText({ model, prompt: '...' });
const text = await streamToText(result);这样能减少调用方的改动。不过,聚合流意味着调用方放弃了流式处理的中间控制能力。如果只是为了兼容旧代码,可以这样做;如果是为了改善用户感知延迟,则应在 UI 侧直接消费 textStream。
generateText 与 streamText 在参数层面的差别不大,主要差异集中在返回值和处理模型上:
| 维度 | generateText | streamText |
|---|---|---|
| 返回结果 | Promise 对象 | 流式结果对象 |
| 文本获取 | 一次性读取 text 字段 | 遍历 textStream 或 fullStream |
| 生成中监听 | 不支持 | 遍历流 |
| 取消方式 | abortSignal,取消后 Promise 抛错 | abortSignal,流结束或抛错 |
另外,完成后通知的方式也不同。generateText 的 await 返回即完整结果;streamText 需要通过 fullStream 的 finish 事件,或者等待 for await...of 自然结束,来判断生成是否完成。如果只关心文本,自然结束就是完成。如果需要读取 finishReason,则需要处理 finish 事件。
工程选型:延迟、成本、超时与测试策略
延迟
如果应用需要在用户界面上逐字显示模型输出,streamText 是更直接的选择。首字延迟通常只包含网络往返时间和模型的一小段生成时间。generateText 则必须等待完整文本结束。
如果输出是一个最终连写在一起的字符串,并且不需要中间状态,generateText 的代码更简洁。
成本
流式和非流式在 token 消耗上是相同的。模型按 token 计费,与传输方式无关。streamText 不会因为使用了流而更便宜或更贵。它只是改变了响应的传输方式。
超时
两类 API 都需要超时处理。generateText 的 await 始终对应一个完整的 HTTP 请求,可以在外层使用 Promise.race 或传入 AbortSignal:
ts
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const result = await generateText({
model,
prompt: '...',
abortSignal: controller.signal
});
} finally {
clearTimeout(timer);
}streamText 有一个容易被忽视的地方:streamText({...}) 本身立即返回,并不等待网络响应。超时必须在流消费期间处理。可以在调用时直接传入 AbortSignal,并在流结束时清理定时器:
ts
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const result = streamText({
model,
prompt: '...',
abortSignal: controller.signal
});
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
} finally {
clearTimeout(timer);
}如果网络连接建立后在超时时间内没有任何数据到来,for await...of 会因 abort 而中断。
测试策略
测试 generateText 时,可以直接 mock model.doGenerate,让它在已知输入下返回固定响应。例如:
ts
const model = {
doGenerate: async () => ({
text: 'mocked text',
finishReason: 'stop'
})
};
const result = await generateText({ model, prompt: 'test' });
expect(result.text).toBe('mocked text');测试 streamText 时,由于流只能读取一次,通常需要把整个流消费完,再对聚合结果做断言:
ts
const model = {
async doStream() {
const stream = new ReadableStream({
start(controller) {
controller.enqueue({ type: 'text-delta', textDelta: 'Hello, ' });
controller.enqueue({ type: 'text-delta', textDelta: 'world!' });
controller.close();
}
});
return { stream };
}
};
const result = streamText({ model, prompt: '...' });
let text = '';
for await (const delta of result.textStream) {
text += delta;
}
expect(text).toBe('Hello, world!');上面的 doStream 返回对象只是一个示意。真实 provider 的返回值必须符合 SDK 对 StreamResponse 的约定。
注意点
streamText的返回类型和字段名可能随 SDK 版本变化。textStream、fullStream以及finishReason等成员的名称在不同版本中不一定完全相同。引用这些 API 时,以当前安装版本的类型定义为准。textStream和fullStream应只选择其中一个进行消费。它们共享同一底层数据源,同时读取会导致数据被拆散,无法获得完整输出。- 流必须被完整消费,或者显式取消。如果开始读取后没有继续消费,底层请求会暂停,可能造成资源占用。没有充足理由时,不要在循环中途
break。 streamText基于 Web Stream。Node.js 的stream模块可以通过Duplex.fromWeb()与 Web Stream 互相转换 [3]。因此,在 Express 等 Node 框架中,可以把textStream接入 Node.js 的响应流。- Edge Runtime 支持
ReadableStream、TransformStream、WritableStream等 Web Stream API [2]。因此streamText可以运行在边缘运行时环境。 - 浏览器兼容性取决于运行环境对 Web Stream 的支持。现代浏览器基本支持
ReadableStream,但如果目标环境较旧,需要先确认ReadableStream和异步迭代器是否可用。 - 不同 provider 对模型参数的处理不一致。例如,
maxOutputTokens、temperature等参数在 OpenAI、Anthropic、Google 的模型上语义基本一致,但单位或边界可能不同。传入前应查看 provider 文档。 tools参数的execute函数可以抛出异常。若execute抛出错误,SDK 会将该错误传给调用方。对于流式调用,错误会出现在流中,而不是在streamText调用时抛出。
