主页
项目
博客
闲聊
暗室

已断开连接

使用 Next.js&NextMe 构建

© 2021-2025 @Hamster1963e855bd7
BackIconBackIcon回到列表

AI SDK: From v4 to v6

发布日期

2025年12月11日

作者

Hamster1963

仓鼠

阅读数

Hamster1963

Ship AI

前不久,Vercel 举办了 Ship AI 的 Conf,推出了一些新鲜的想法和库,不仅有引起热烈讨论的

use workflow

,还有 Vercel Agent 等一系列的产品更新,那就借着 Ship AI,一起聊聊最核心的更新发布 - AI SDK 5。

Ship AI 2025

AI SDK

自发布以来,AI SDK 一直都是最热门的构建 AI 应用的框架之一,这个使用 TS 语言编写的框架,与其称之为一个第三方库,我更愿意称之为一套完整却灵活的AI应用构建解决方案。

AI SDK

CleanShot 2025-11-13 at 17。40。29@2x。png

但是与 Vercel 公司旗下的核心 React 框架 - Next.js 类似,在简单的示例程序背后,需要花费大量时间去学习与构建真正可用且好用的应用程序,AI SDK 同样需要你阅读大量的官方文档,更新日志甚至源码来紧跟最新的“最佳实践”。

AI SDK最大的特色便是UI层与逻辑层的无缝衔接,构建起来的逻辑很顺,构建一个简易的 Chatbox 的流程可以分为三步。

  • 在 api 层(或独立后端)使用 AI SDK 定义好模型提供商与模型参数
  • 在 UI 层使用 AI SDK 使用 useChat 获取 api 层流式返回的内容
  • 使用 react-markdown 或其他 markdown 渲染库将 AI 返回的内容渲染到页面上

这样子就可以在短时间内搓出一个还不错的 Chatbox。

alt:一个简易的 Chatbox

一个简易的 Chatbox

但是构建一个完整的包含用户管理、绘画历史、图片识别等等功能的完整 Chatbox,还是需要在许多方面进行细节的构建,接下来,就跟随我升级我的个人 Chatbox - Chatty 从 v4 升级到 v5 与 v6 的过程,来聊聊如何基于 AI SDK 一步步构建这些功能吧。

项目介绍

Chatty 是一个基于 AI SDK(Next.js) 与 Hono 的 AI 聊天工具,内含图片识别、联网搜索、MCP 等功能,同时以优化渲染性能为核心,为用户带来快速、流畅的使用体验。

首先看看最终构建的成果吧。

正如你看到的一样,Chatty 的基础对话功能和 Chatgpt 完全一致:

  • 用户输入文字按下回车
  • 模型通过流式的方式返回已生成的文本
  • 通过一个简约的渐变动画显示在用户的界面上

关于如何实现这个渐变动画,将来会有一篇独立的博客来介绍如何构建高效的 markdown 流式渲染组件与如何实现这个渐变的 css 动画。

因此使用 AI SDK 构建与自己直接对接 API 有什么不同呢,接下来我将从:

  1. useChat
  2. MCP
  3. Resumable Stream
  4. 消息元数据
  5. 联网搜索实现
  6. Reasoning
  7. FollowUp

这几个方面详细聊聊使用 AI SDK 构建 Chatty 与升级 AI SDK 的过程。

useChat

useChat 可以算得上是在 AI SDK UI 层最核心的功能,useChat 主要包含三个功能:

  • 流式传输聊天信息
  • 管理聊天状态
  • 收到新消息时自动更新UI

一个最基础的示例代码如下:

alt:简单的 useChat 使用方式

简单的 useChat 使用方式

通过 useChat 导出的这三个方法即可轻松地实现发消息,渲染消息与获取消息状态这三个流程。

AI SDK 5 的破坏性更改

在从 V4 升级到 V5 的过程中,useChat 存在不少的 Breaking Changes,而最核心的便是 useChat 的内部共享状态逻辑的改变。

在 V4 中,useChat 可以通过相同的 id 共享 useChat 中的数据,而在 V5 中,我们需要手动来维护这个全局共享,例如用 React Context 的方式来维护 useChat 实例。

alt:V4 版本的 useChat

V4 版本的 useChat

使用 React Context 的方式可以参考:

useSharedChat

alt: V5 版本的 useSharedChat

V5 版本的 useSharedChat

在使用的时候,通过 useSharedChat 的方式去获取 useChat 实例。

精简发送消息与发送自定义数据

在 useChat 中,在使用 sendMessage 时,默认情况下会将当前的全部消息都发送至 API 端口,这在大量上下文的对话中,会十分消耗网络流量与影响速度,除此之外,我们可能还需要在请求中附带许多额外的信息。

因此我们可以通过

prepareSendMessagesRequest

来精简发送信息与自定义发送数据。

alt:使用 prepareSendMessagesRequest 自定义发送内容skip-compression
使用 prepareSendMessagesRequest 自定义发送内容

我们通过设置最终发送的 message 为最新一条记录(也就是用户发送的消息)来大幅精简请求体的大小,并且在 body 中我们可以轻松设置其他的自定义参数。

在后端,则是通过获取数据库中存储的消息记录后拼接从接口收到的 message 形成了完整的信息。

alt:组装完整信息skip-compression
组装完整信息

那接下来,就从 MCP 的整体构建流程来如何在 AI SDK 中实现工具调用与 UI 的渲染。

MCP

MCP(模型上下文协议)是一个开源标准,用于连接人工智能应用程序到外部系统。

通过 MCP 协议,我们可以快速定义与构建一组工具供模型去使用,可以大幅扩展模型获取外部数据的能力,帮助模型获取解决问题所需的核心资料来更好地进行回复。

同时,模型也可以通过工具调用直接去实现一些基础的操作,这也是构建 Agent 的核心,关于构建 Agent 的话题我们以后再聊,目前让我们专注于如何构建 MCP 服务与 AI SDK 的流程衔接。

使用 FastMCP 构建 MCP 服务器

在 MCP 服务的构建上,本次选择了 FastMCP 框架作为后端进行项目的构建。

FastMCP 是一个基于 Python 的 MCP 服务端框架,可以将一组工具快速转化为一个完整的 MCP 服务,让 MCP 客户端可以通过标准协议(SSE、Streamable HTTP)连接到服务并调用定义好的工具。

alt:一个最简单的 FastMCP 服务端示例

一个最简单的 FastMCP 服务端示例

在 AI SDK 中接入 MCP

在 AI SDK 中, MCP 中的工具等同于传统的 tools ,因此整体的接入流程为:

  • 创建 MCP 客户端
  • 将 MCP 客户端连接至 MCP 服务端
  • 获取可用的工具列表
  • 将工具列表传递至 AI

alt:连接MCP服务端并获取可用工具列表

连接MCP服务端并获取可用工具列表

首先我们通过 StreamableHTTPClientTransport (MCP官方库)来创建一个使用 StreamableHTTP 连接方式的 MCP 客户端。

创建完成后将其传递至 AI SDK 的

experimental_createMCPClient

,这样子便可以通过

.tools

方式去获取 MCP 服务端中已经注册的工具列表。

最后,将获取到的工具作为

streamText

的

tools

参数,便完成了接入 MCP 的整个流程。

alt:传入工具与请求后关闭连接

传入工具与请求后关闭连接

在 UI 层显示工具调用

在用户对话交互界面展示AI选择并调用了哪些工具是很重要的, 用户不仅可以清晰地了解到模型调用了什么工具,还可以了解到调用工具所用的参数与工具返回的数据。

AI SDK对于工具调用相关的数据主要分为两类:

  1. 在 AI SDK 服务端预定义好的工具调用数据
  2. 通过 MCP 或其他方式动态获取的动态工具调用数据

解析类型为

dynamic-tool

的消息块作为MCP工具调用的数据来源, 可以通过解析

part.toolName

与

part.state

来获取调用的工具名称与调用状态。

alt:筛选出工具调用相关数据

筛选出工具调用相关数据

为了更加含义地显示调用工具的信息,可以通过在本地维护一份对应表来显示一些特定工具的调用过程的名称,例如读取页面与网络搜索。

alt:工具名称与中文名称对应表

工具名称与中文名称对应表

同时,用户可以通过点击工具信息来查看更加详细的入出参数信息与工具状态信息,这在AI模型调用工具的结果不尽人意时,可以帮助用户了解工具调用的流程以来优化提示词的编写。

alt:点击工具以显示详细调用信息

点击工具以显示详细调用信息

在聊天上下文中精简工具相关数据

在经过长时间的对话后,工具调用累计的数据可以会极大地占据了上下文空间,因此可以通过 AI SDK 提供的

pruneMessages

来精简上下文,使用的方式也很简单:

let prunedMessages = pruneMessages({
  messages: convertToModelMessages(originMessages),
  reasoning: "all",
  toolCalls: "before-last-2-messages",
  emptyMessages: "remove",
});

精简的核心在于

toolCalls

参数,我们可以通过

before-last-${number}-messages

的方式来决定需要在上下文中保留多少个工具调用的完整数据,通常保留最后 2 个工具调用数据就可以大幅精简上下文且不会对 AI 的回复质量有大的影响。

Resumable Stream

在与 AI 对话的过程中,常常会出现这两种情况

  1. 用户发送完消息后在等待的过程中关闭了页面后重新打开
  2. 在另一个设备中打开了相同的对话

这时就需要一种机制来还原流式传输的数据,在 AI SDK 中这被称为 Resumable Stream。

通过 Resumable Stream,可以在多设备或是断线重连的情况下重新同步流式传输的数据,而无需等待消息生成完成。

配置 Resumable Stream 的方式也很简单,只需要一个可用的 Redis 数据库即可。

要实现 Resumable Stream 主要分为三个步骤:

  • 新增一个

    /stream

    API端点来实现消息流的重新获取
  • 在

    /api/chat

    实现通过

    streamContext

    存储流信息
  • 在客户端使用

    resumeStream

    来恢复流

创建上下文

resumable-stream

是vercel 推出的一个用于可恢复流的工具库,通过 redis 存储流信息搭配 sub/pub 的方式来实现消息流的同步与恢复。

alt:构建可恢复流上下文管理skip-compression
构建可恢复流上下文管理

这便是一个完整的流上下文所需要的方法,特别需要注意的是,如果是部署在 serverless 的环境中,例如 vercel, cloudflare 时,需要设置一个

waitUntil

参数来保证在消息流为传输状态时,无状态的函数会一直等待到消息流完成后才会销毁,这可以避免由于函数的提前销毁而导致上下文丢失的问题。

在创建

/stream

API端口时,我们需要用到对话 ID 来查询是否存在正在进行中的对话流并进行恢复,而在没有可用流时则返回一个空的流式返回结果。

alt:/stream端口skip-compression
/stream端口

而在AI对话流程中使用到的

/api/chat

,则是在返回流式数据前通过 streamID 将消息流的标识信息写入到上下文中(存储在 Redis 中)。

这看似会对 Redis 造成不小的读写压力,但是实际上,真正的流式数据存储在内存中, Redis 只是存储消息流的元数据与状态,真正的数据传输是通过 sub/pub 的方式进行的。

alt:将流写入到下上文

将流写入到下上文

在客户端获取可恢复流

在客户端,通过

useSharedChat

中的

resumeStream

方式,便可以让客户端通过先前我们创建的

/stream

API端口获取到可恢复流。

alt:在客户端处理流恢复

在客户端处理流恢复

通过依靠 sessionId 的变化来进行可恢复流的获取,以实现多设备同步与断线重连的效果。

接下来,让我们讨论一下如何在消息中存储自定义元数据。

消息元数据

在构建AI对话时,我们也会想尽可能地保留每一条消息的相关数据,比较基础的例如:

  • 输入的 token 数 - prompt_tokens
  • 输出的 token 数 - completion_tokens
  • 思考过程使用的token 数 - reasoning_tokens
  • 模型ID - model

这些数据往往会记录在模型消息的输出中,除此之外,我们常常还想要存储一些自定义的数据,例如:

  • 首 token 的等待时间

  • 总输出时间

  • 模型输出 token 速度

    CleanShot 2025-12-01 at 12。51。21@2x。png

这些数据与每一条 AI 回复的信息进行关联,帮助用户了解模型或者对话的信息。

临时元数据与永久元数据

在 AI SDK 中,消息的附带数据分为临时与永久两种,主要的区别与用法为:

  • 临时数据 - 用来表示当前的处理状态,例如: 正在精简上下文 正在生成搜索关键词 已完成搜索 这些状态在完成后即被丢弃而无需进行存储,因此被称为临时数据。
  • 永久数据 - 上文中提到的需要一直显示或记录在消息中的数据,便于用户查看与了解对话流程的数据。

在 AI SDK 中, 可以通过使用 UIMessageStreamWriter 将这些元数据写入到消息中。

其中

transient

表示此为临时还是永久元数据。

alt:通过 transient 参数控制是否需要存储

通过 transient 参数控制是否需要存储

在客户端中,可以通过

onData

回调来特别处理临时元数据的展示。

alt:在客户端获取临时消息元数据

在客户端获取临时消息元数据

接下来,让我们一起讨论一下目前比较常见的联网搜索功能如何在 AI SDK 中实现。

P.S. 消息token的自定义计算

在某些模型提供商的返回中,可能并不会返回上文中所提及的消息 token 数信息,因此在这种情况下,可以通过

gpt-tokenizer

这个库去粗略计算一下。

import { encode } from "gpt-tokenizer";

export function estimateTokens(text: string): number {
  return encode(text)。length;
}

两种联网搜索的实现方式

由于大模型的特性, 大模型真正在训练中得到的是“能力”而非“记忆”, 因此在一些较为小众的问题或超出知识时限的问题上, 大模型可能会出现幻觉从而回答并不存在的内容或无法正确回复, 因此如何让大模型获取外部知识便是一个很好的话题。

在众多的方式中, 给予大模型联网搜索的能力是应用得最多的方式之一, 而如何给予大模型这种能力在目前看来有两种较为通用的方法:

  • 分析用户输入的内容 → 生成搜索关键词 → 进行搜索 → 将结果放置在系统提示词
  • 在 MCP 中定义联网搜索工具 → AI 调用工具 → 进行搜索 → AI 获取到工具返回的搜索结果

这两种实现方式适合于不同的使用场景,那就先从第一个实现方式开始说起吧。

被动的联网搜索

对于一些没有工具调用能力的模型,或者在工具调用方面较差的模型,被动的联网搜索可以在不使用工具调用的情况下给予模型足够的上下文进行回复。

接下来就一步一步介绍一下如何实现这种方式。

分析用户输入内容并生成提示词

在这种模式下,可以通过给用户提供一个联网搜索的选项按钮,来区分用户的联网搜索意图。

在接收到用户的输入内容后,可以通过一些小模型针对上下文进行分析,进而生成一个较为符合上下文的搜索词。

alt:使用 GPT 5-mini 生成搜索关键词

使用 GPT 5-mini 生成搜索关键词

直接通过

generateText

与提示词搭配的方式来生成搜索词,而如果获取到当前的用户意图并不需要搜索的,则返回

no search

来表示无需进行后续的搜索。

编写联网搜索流程

在获取到搜索词后,就有很多种方式去进行联网的数据检索,常见的联网搜索方式有:

  • 自建的开源搜索服务: SearXNG
  • 免费的搜索服务: duckduckgo-api
  • 付费的搜索服务提供商: linkup tavily

本次就以 linkup 为例进行搜索流程的构建。

linkup 在免费计划中包含了 5€/月的免费额度, 对于基础的查询,每次花费大约为 0.005€ - 0.05€,因此在个人轻度使用下,这个额度还是绰绰有余的。

alt:linkup的调用费用

linkup的调用费用

同时 linkup 支持多种输出格式与搜索深度设置,可以很轻松地使用输出的数据进行自定义的展示与进一步的处理。

在上面的处理中,我们已经成功地获取到了根据上下文而获取的搜索提示词,接下来就可以直接通过

linkup-sdk

中的

LinkupClient

进行联网检索了。

alt:linkup的调用流程

linkup的调用流程

如上图,我们设置搜索深度为标准,同时在搜索结果中包含相关的图片信息,以构建我们的自定义搜索结果UI。

写入元数据与构建联网搜索结果提示词

在获取到 linkup 返回的搜索结果后,下一步则是将数据处理成三个不同的数据块。

  • AI模型推理所需的包含搜索结果的系统提示词
  • 返回给用户并绑定在消息元数据上的搜索网页结果列表
  • 返回给用户并绑定在消息元数据上的图片列表

让我们先简要说说如何将搜索网页结果列表与图片列表绑定在消息元数据。

在获取到搜索结果后,提取搜索结果中我们所需的

  • 站点名称
  • 站点内容
  • 通过站点地址获取的 icon 地址

将这些数据记录为

searchAnnotation

,定义类型为

search_results

。

最后,通过writer 将

searchAnnotation

流式写入到本次的AI回复消息中。

💡 特别注意的是,在调用写入的时候,如果设置了

transient: true

则表示数据为临时信息,不会写入到消息元数据中,因此在这里可以直接忽略这个参数的设置。

alt:将搜索结果写入到消息元数据中

将搜索结果写入到消息元数据中

接下来便是构建一个提示词,让模型可以知道搜索的关键词与搜索结果,从而帮助AI推理出更准确的内容。

alt:构建完整联网搜索提示词

构建完整联网搜索提示词

上图便是一个简单却十分有效的提示词,对于目前 2025 年底 所有的大模型来说,这已经提供了足够的数据与指引,模型可以基于这些信息进行正确的推理与输出。

提示词中包含:

  • 当前的时间
  • 用户的搜索词
  • 返回的搜索结果

以帮助模型进行推理回答。

构建搜索状态展示

对于模型来说,在推理前只需要等待联网搜索流程执行即可,但是对于用户来说,这可能是一个持续一段时间的过程,因此我们可以通过上文中提及的临时元数据展示当前的处理流程,以优化用户的使用体验。

对于用户而言,可以清晰的看到当前的联网搜索执行状态

  • 正在提取搜索关键词
  • 正在搜索
  • 搜索完成

在服务端,只需要在执行节点前后使用

write

将状态写入到临时元数据中即可。

alt:发送流程节点提示

发送流程节点提示

在用户端,则是通过

onData

回调来进行当前执行状态的获取与展示。

alt:客户端获取当前节点流程

客户端获取当前节点流程

构建搜索结果UI

对于用户来说,了解模型是基于哪些内容进行回答也是十分重要的,因此在联网搜索场景,可以在模型回复的上方展示推理所用到的搜索结果以及相关的图片。

alt:搜索结果UI

搜索结果UI

而其中的数据便来自于上面步骤中写入到消息中需要保留的元数据,而我们现在重点关注一下客户端如何获取并提取出这些数据。

如果你使用 Next.js 完成整个 ChatBox 的构建,AI SDK的类型推断会帮助你轻松构建类型安全的消息元数据写入与获取,而目前这种前后端分离的架构则需要手动进行类型定义。

在 AI SDK 中,每个消息内部都是由一个个的块(parts)构成的,因此我们需要手动筛选出搜索结果与相关图片的

parts

,在筛选后即可进行后续的数据提取与展示。

alt:筛选出搜索结果相关数据

筛选出搜索结果相关数据

对于搜索结果与图片,我们需要提取去在服务端定义的两种类型,也就是服务端进行

write

时定义的

type

参数,提取后将数据传入组件进行平铺展示。

主动联网搜索

随着模型的能力越来越强,模型的工具调用能力也在一直提升,因此主动联网搜索便是将搜索这个操作的主动权交给模型。

这个主动权的交接则是通过自定义工具或者 MCP 的方式直接将工具调用这个能力提供给模型,模型通过选定方法与入参进行调用,以达到联网搜索或者更多功能的效果。

这种方式有两种好处:

  • 无需自己编写联网搜索流程,只需提供联网搜索相关工具即可
  • 模型可以自主进行多次搜索,直到获取到模型推理所需的数据

alt:自主进行多次搜索的调用过程

自主进行多次搜索的调用过程

构建搜索工具

在构建联网搜索工具时,在最精简的情况下只需要三个工具:

  • 使用关键词进行联网搜索
  • 获取网页信息
  • 获取代码相关文档

幸运的是这三种工具都可以很轻松地通过调用三方服务的方式去实现。

对于联网搜索,除了上文中提到的 linkup 以外,Tavily 也是一个不错的选择。

alt:基于Tavily的联网搜索方法

基于Tavily的联网搜索方法

在 FastMCP 中,只需定义好方法名,入参与类型,再加上必须的方法注释,即可在模型获取MCP工具列表时自动转换为 MCP 的工具信息,因此在调用三方 SDK 的场景下编写工具是十分方便的。

alt:基于Tavily的页面内容获取方法

基于Tavily的页面内容获取方法

相同的,针对网页内容的获取,使用 Tavily 提供的

extract

方法即可。

使用 FastMCP 框架时,一个非常好用的功能则是你可以将其他的 MCP 服务桥接进来,也就是可以直接将其他 MCP 服务提供的工具提供给客户端。

对于获取代码文档这个工具,便可以桥接 exa 公司提供的 MCP 服务以使用其中的

get_code_context_exa

工具来获取代码相关文档与上下文。

alt:通过代理使用 exa 的MCP服务

通过代理使用 exa 的MCP服务

在构建好这三个工具后,在 AI SDK 就可以获取到这些工具并交给模型进行使用。

P.S. 限制模型最大工具调用次数

在联网搜索时,有的时候模型可能会陷入无止尽的检索中,因此除了在提示词中限制模型的行为,AI SDK 中提供的

stopWhen

参数可以使我们手动停止模型的工具调用流程。

alt:通过 stopWhen 参数来避免模型进行无穷的工具调用

通过 stopWhen 参数来避免模型进行无穷的工具调用

最简单的方式则是使用

stepCountIs

方法,通过限制模型的最大工具调用次数,来进行最后的兜底,避免模型进行无止尽的搜索以造成巨大的 tokens 开销。

Reasoning

thinking…thinking…thinking…

深度思考模型 在2025年可谓是大模型的发展风潮,深度思考模型的本质是通过启用思维链(Chain of Thought)机制,让模型在回答问题前进行深层次的分析和推理,以提高在复杂问题中回答的准确性与可靠性。

而从年初 DeepSeek 发布的 R1 模型开始,后续越来越多的模型开始将模型深度思考过程中的内容暴露给用户。

在 AI SDK 中,思考过程中产生的文本会被归类到 Reasoning 这一类型下,接下来就来聊聊如何在 AI SDK 中对接思考模型与构建思考过程UI。

提取思考内容

在 AI SDK 中,有许多的不同AI模型提供商的 providers,一些是官方的,也有一些是社区进行维护的。

简而言之这些 providers 的作用便是降低开发者与不同AI模型提供商之间的对接难度,只需在调用模型时指定使用哪一个 provider,内部便会自动完成 API 与返回格式的转换,让我们只专注核心的模型输出。

providers

在 DeepSeek 的官方 API 返回中,思考内容记录在 reasoning 或者 reasoning_content 中,在这种情况下,可以直接使用 DeepSeek Provider,API 返回的思考内容会自动解析到 AI SDK 的 reasoning 出参中。

而对于一些第三方的模型提供商或者自建的 vllm 服务,可能并不像 DeepSeek 的官方API的返回格式,最常见的便是如 groq 返回的数据,思考内容被包裹在

<think/><think/>

XML标签中,需要我们自行进行内容的解析与提取。

在 AI SDK 中,我们可以通过处理中间件来轻松实现这一点。

alt:通过中间件进行思考内容的提取

通过中间件进行思考内容的提取

extractReasoningMiddleware

通过使用

extractReasoningMiddleware

这个中间件,我们可以自定义在模型输出内容中提取思考内容的规则,例如上图中,

tagName

等同于

<think>

的标签名,同时我们可以设置

startWithReasoning

来告诉中间件模型一定会先返回思考内容,以便一开始就可以进行解析。

设置思考参数

对于 R1 模型来说,思考是一个不可关闭的过程,每次的推理输出中必定都是先输出思考的内容,完成思考内容的输出后再输出回复的内容。

而对于像 Gemini 2.5 或者是 GPT 5 这样的模型来说,模型的思考不仅仅是可以关闭的,同时也可以对模型的思考能力进行控制,在这种情况下,我们就需要在 AI SDK 模型配置中进行相应的配置以发挥模型的推理能力。

alt:设置 GPT 5 模型的推理参数

设置 GPT 5 模型的推理参数

以 GPT 5 为例,我们可以通过 OpenAIResponsesProviderOptions 来获取模型的可配置项,对于 GPT 5,我们可以通过:

  • reasoningEffort 推理等级(low medium high)
  • reasoningSummary 推理总结(因为 openai 不直接返回推理内容)

来配置模型思考相关的参数。

发送思考内容

在 AI SDK 中,思考内容默认是不输出给客户端的,这时可以通过配置

sendReasoning

参数来控制是否发送思考内容至客户端。

alt:通过sendReasoning参数控制输出中是否包含思考内容

通过sendReasoning参数控制输出中是否包含思考内容

构建思考过程UI

在客户端中,我们通过筛选出类型为 reasoning 的 part 来获取思考内容。

alt:从parts筛选出思考内容

从parts筛选出思考内容

在获取到思考内容后,便可以通过一个单独的组件将其展示出来。

如视频中所展示的效果,思考内容不断流式地显示出来,在超过一定的高度后,会进行滚动,同时在上下两侧会有一个淡淡的遮罩层以提醒用户文本正在滚动中。

这样的设计主要基于两个想法:

  1. 用户并不真的关心模型的思考内容,只需要知道模型正在思考就够了。
  2. 思考内容的滚动会让用户在等待真正的回复时的愉悦感好一些。

到目前为止,从用户输入文本到模型输出的全流程的构建就完成了!

那下一步是什么呢?

那便是如何让用户可以轻松继续进行下一次的对话。

FollowUp

alt:follow-up

follow-up

在 AI 回复完成后,在下方展示 3-5 条对话建议的实用性是非常大的!

在我个人的使用体验中,往往是在这些 follow-up 中找到了灵感。

生成 follow-up

我们可以基于模型的最后一次回复来生成 follow-up,当然你可以可以通过总结完整的上下文来获取更加准确的 follow-up,但目前以实际的体验来说,依靠 AI 的最后一次回复已经可以生成比较准确的建议。

alt:通过最后一个模型回复生成 follow-up

通过最后一个模型回复生成 follow-up

如上图,首先在

modelMessages

中过滤出AI回复的内容,并将其作为上下文放入到消息列表中,在提示词中定义好所需的 follow-up 的格式或其他要求即可。

流式传输 follow-up

相同的,生成完整的包含多条的 follow-up 可能也需要一定的时间,因此我们可以借助消息元数据,通过一个相同的 ID ,不断更新元数据中 follow-up 的部分以达到流式传输 follow-up 的效果。

alt:流式更新 follow-up 列表skip-compression
流式更新 follow-up 列表

在获取到 follow-up 生成的文本流后,主要的流程为:

  • 生成一个固定的 ID 以便我们后续进行 follow-up 更新
  • 进行必要的字符过滤与列表拆分
  • 将列表格式的 follow-up 更新到消息元数据中

在客户端中,通过类型即可筛选出 follow-up 并进行显示。

alt:在 parts 中筛选出 follow-up 列表

在 parts 中筛选出 follow-up 列表

两个体验上的优化

现在我们便有了一个不错的 AI Chat,我们还可以如何优化呢?

默认监听键盘输入

💡 感谢 Grok 网页端提供的灵感

在打开各类的 AI Chat 时,许多情况下我们只是急匆匆地想要询问一个简短的问题。

在移动端上,点击输入框即可开始输入,但是在桌面端上想要输入则需要:

  • 将鼠标移动至输入框
  • 点击输入框
  • 将焦点锁定在输入框

在这一连串的操作后我们才真正地开始文本的输入,因此我们可以通过监听键盘输入来改善这一点。

也就是,用户打开页面时,只要按下了键盘的键位,便会自动将焦点锁定在输入框。

参考代码如下:

// Add global keyboard event listener for auto-focus
  useEffect(() => {
    const handleGlobalKeyDown = (event: KeyboardEvent) => {
      // Only focus if:
      // 1. The event is not from the textarea itself
      // 2. No modifier keys are pressed
      // 3. It's a printable character or backspace/delete
      // 4. No input/textarea is currently focused
      // 5. The textarea reference exists
      const target = event。target as HTMLElement
      const isFromTextarea = target === textareaRef。current
      const isModifierPressed = event.ctrlKey || event.metaKey || event.altKey
      const isPrintableKey =
        event.key.length === 1 ||
        event.key === 'Backspace' ||
        event.key === 'Delete'
      const isInputFocused =
        target.tagName === 'INPUT' ||
        target.tagName === 'TEXTAREA' ||
        target.contentEditable === 'true'

      if (
        !isFromTextarea &&
        !isModifierPressed &&
        isPrintableKey &&
        !isInputFocused &&
        textareaRef.current
      ) {
        textareaRef.current.focus()

        // If it's a printable character, add it to the input
        if (event.key.length === 1) {
          const currentValue = textareaRef.current.value
          const newValue = currentValue + event.key
          textareaRef.current.value = newValue

          // Trigger the onChange event to update the state
          const syntheticEvent = {
            target: textareaRef.current,
            currentTarget: textareaRef.current,
          } as React.ChangeEvent<HTMLTextAreaElement>
          handleInputChange(syntheticEvent)

          // Prevent the default behavior to avoid double input
          event.preventDefault()
        }
      }
    }

    document.addEventListener('keydown', handleGlobalKeyDown)

    return () => {
      document.removeEventListener('keydown', handleGlobalKeyDown)
    }
  }, [handleInputChange])

将配置项放到二级菜单

过多的功能与模型配置会让用户感受到焦虑。

因此一个简洁的配置按钮加输入框便是最好的搭配。

alt:在二级菜单放置模型列表与功能选项

在二级菜单放置模型列表与功能选项

总结

呼!这一路走来可真不容易!

构建一个 AI Chat 绝非只是一个套壳那么简单,想要构建用户体验良好的AI Chat,在这个过程中往往是各种平衡的决策与细节的构建,希望这篇博客对你构建属于自己的 AI Chat 有帮助,下篇博客见👋!

0%
    回到顶部
  • Ship AI
  • AI SDK
  • 项目介绍
  • useChat
  • AI SDK 5 的破坏性更改
  • 精简发送消息与发送自定义数据
  • MCP
  • 使用 FastMCP 构建 MCP 服务器
  • 在 AI SDK 中接入 MCP
  • 在 UI 层显示工具调用
  • 在聊天上下文中精简工具相关数据
  • Resumable Stream
  • 创建上下文
  • 在客户端获取可恢复流
  • 消息元数据
  • 临时元数据与永久元数据
  • P.S. 消息token的自定义计算
  • 两种联网搜索的实现方式
  • 被动的联网搜索
  • 分析用户输入内容并生成提示词
  • 编写联网搜索流程
  • 写入元数据与构建联网搜索结果提示词
  • 构建搜索状态展示
  • 构建搜索结果UI
  • 主动联网搜索
  • 构建搜索工具
  • P.S. 限制模型最大工具调用次数
  • Reasoning
  • 提取思考内容
  • 设置思考参数
  • 发送思考内容
  • 构建思考过程UI
  • FollowUp
  • 生成 follow-up
  • 流式传输 follow-up
  • 两个体验上的优化
  • 默认监听键盘输入
  • 将配置项放到二级菜单
  • 总结