文本生成的进阶:generateText / streamText 里迟早会撞上的东西

《读懂 Vercel AI SDK》系列第 2 篇。上一篇里已经把 generateText 和 streamText 都跑通了。这一篇不重复"这俩函数是干嘛的",而是讲从 hello-world 到能上生产,中间那些你迟早会踩到的坑和用到的能力。


一、你拿到的不是一段字符串,是一个"结果对象"

新手最容易忽略的一点:generateText 返回的远不止 text。真正上生产,有两个字段几乎每次都要用。

javascript 复制代码
const { text, usage, finishReason } = await generateText({
  model: 'openai/gpt-5.2',
  prompt: '总结这份周报......',
});

usage:你的账单

usage 里有 inputTokens / outputTokens。成本核算、限流、给用户显示额度,全靠它。别等月底看账单才发现某个功能在烧钱------在代码里就能把每次调用的 token 记下来。

finishReason:它为什么停下来

这个字段回答"模型是正常说完了,还是被打断了":

值 含义 你该做什么
stop 正常说完 正常处理
length 撞到 maxOutputTokens 上限被截断 调大上限,或提示用户"继续"
tool-calls 停下来要调工具 交给工具流程(见第 4 篇)
content-filter 被内容安全拦了 给用户友好提示

遇到"回答莫名其妙被截断"的 bug,第一反应就是打印 finishReason ------十有八九是 length。这是新手最常见的坑,而它在总览和 quickstart 里都不会告诉你。


二、流不止 textStream:被低估的 fullStream

quickstart 里我们用 textStream 拿纯文本。但 streamText 其实同时在吐一条信息量大得多 的流------fullStream。

textStream 只给你文本片段;fullStream 给你带类型的事件:文本增量、推理过程、工具调用、错误、结束......每一个都能单独处理。

javascript 复制代码
const result = streamText({ model: 'openai/gpt-5.2', prompt: '......' });

for await (const part of result.fullStream) {
  switch (part.type) {
    case 'text-delta':
      process.stdout.write(part.text);       // 正文
      break;
    case 'reasoning-delta':
      logThinking(part.text);                // 模型的思考过程
      break;
    case 'tool-call':
      console.log('要调工具:', part.toolName);
      break;
    case 'finish':
      console.log('用量:', part.totalUsage); // 流结束时的汇总
      break;
    case 'error':
      console.error(part.error);
      break;
  }
}

什么时候用哪个?

  • 只想把字打到屏幕上 → textStream,够简单。
  • 想区分正文 / 思考过程 / 工具调用,做更精细的 UI(比如把"推理"折叠起来)→ fullStream。

这条 fullStream 的事件,和前端 useChat 收到的 parts、以及后面是一脉相承的------它们其实是同一套事件模型在不同层的样子。


三、勒住生成:别让它跑飞或卡死

面向用户的流式接口,有几个"缰绳"是生产必备的。

中断:用户点了"停止"

streamText 支持 abortSignal。用户关掉页面、点停止按钮时,你得能真的把这次生成掐掉------否则它在后台继续烧 token。

javascript 复制代码
const controller = new AbortController();

const result = streamText({
  model: 'openai/gpt-5.2',
  prompt: '......',
  abortSignal: controller.signal,
});

// 用户点停止:
controller.abort();

兜底:超时与重试

  • maxRetries(默认 2):模型服务抖动时自动重试,不用自己写。
  • 配合 abortSignal 设一个整体超时,避免请求卡死拖垮你的服务。

别让错误无声消失:onError

一个很隐蔽的坑:streamText 默认会"吞掉"流式过程中的错误 ------为了不让整条流崩掉,它不会把错误 throw 到你的 for await 里,而是塞进流里。结果就是出了错你却什么都没看到。所以流式生产一定要挂 onError (顺便用 onFinish 在正常结束时记 token / 落库):

javascript 复制代码
streamText({
  model: 'openai/gpt-5.2',
  prompt: '......',
  onError: ({ error }) => log(error),       // 不挂它,错误就无声消失了
  onFinish: ({ usage }) => record(usage),   // 正常结束:记账 / 持久化
});

(generateText 不同------它是一次性的,出错会正常 throw,直接 try/catch 即可。这个"吞错误"只发生在流式。)

控制风格:temperature 与 maxOutputTokens

  • temperature:低(如 0.2)=稳定、可复现,适合抽取/分类;高=发散,适合创意。
  • maxOutputTokens:输出上限。既兜住成本,也是上面那个 finishReason: 'length' 的来源------两者要一起想。

四、多轮对话:把上一轮"滚"进下一轮

quickstart 的例子是单轮。真做对话,关键问题是:模型不记得上一句,上下文得你自己维护。

手动拼 messages 数组很容易出错。AI SDK 给了个省心的做法------结果里的 response.messages 就是"这一轮该追加进历史的消息",直接接上去:

javascript 复制代码
let messages = [{ role: 'user', content: '你好' }];

const r1 = await generateText({ model: 'openai/gpt-5.2', messages });
messages.push(...r1.response.messages);        // 把 AI 的回复滚进历史

messages.push({ role: 'user', content: '接着上面继续' });
const r2 = await generateText({ model: 'openai/gpt-5.2', messages }); // 带着完整上下文

要点:用 response.messages 累积历史,而不是自己手搓 assistant 消息------它会把工具调用等结构也一并带上,少踩很多坑。


五、把结果交出去的三种姿势

streamText 的结果不是只能 for await 自己消费,它内置了几种"交出去"的方式,对应不同场景:

方法 交给谁 场景
toUIMessageStreamResponse() 前端 useChat 做聊天应用(最常用)
toTextStreamResponse() 任意 SSE/fetch 客户端 只要纯文本流,不用 SDK 前端
for await (result.textStream) 你自己的代码 后端内部消费,比如写进数据库

选哪个,取决于"接收方是不是 AI SDK 的前端"。做标准聊天用第一个;想让非 JS 前端也能接,就是后面的话题了。


小结

这一篇的信息,都是 quickstart 那个能跑的例子不会告诉你、但上生产会撞上的:

  • 返回的是结果对象 :usage 管账单,finishReason 排"回答被截断"的坑(多半是 length)。
  • 流有两条:textStream(纯文本)和 fullStream(带类型事件,做精细 UI 用)。
  • 生产必备的缰绳:abortSignal 中断、maxRetries 兜底、temperature/maxOutputTokens 控风格。
  • 多轮对话用 response.messages 累积历史,别手搓。
  • 三种交付姿势,按"接收方是谁"来选。

下一篇,我们解决另一个更实际的问题:怎么让模型别输出散文,而是稳定地吐出一段 JSON------结构化输出。


参考:AI SDK Core · Generating Text

相关推荐
张小姐的猫3 分钟前
【AI大模型接入SDK】 —— 前端页面 & 项目总结与拓展
前端·数据结构·数据库·c++·人工智能·chatgpt
web打印社区5 分钟前
远程打印:WebSocket 与 HTTP 轮询怎么选
前端·vue.js·websocket·网络协议·http·electron·pdf
Json____6 分钟前
基于 Spring Boot + Vue3 的在线考试管理系统实战
java·spring boot·后端·it学习·wwwoop.com
吴声子夜歌9 分钟前
HTML——庞杂的表单控件元素(一)
前端·html
全栈弄潮儿10 分钟前
《周复盘:过去三周,我的开发效率真正提升在哪》
aigc·openai·ai编程
吴佳浩11 分钟前
走向 Memory OS:企业私有化 Agent 设计与实现
人工智能·agent·ai编程
吴佳浩18 分钟前
构建企业级 DevOps 排错 Agent:从日志告警到自动化修复 PR
人工智能·agent·ai编程
Sam_Deep_Thinking20 分钟前
如何理解java的信号量
java·后端·面试·程序员
东风破_29 分钟前
NestJS 快速入门:先别背装饰器,把一条请求跑明白
后端
晴空蓝天32 分钟前
MDC traceId 全链路日志追踪:Spring Boot 3.5 里把日志串成一条线
java·spring boot·后端·python