流式传输

Alpha

流式传输目前处于 alpha 阶段:暂时还不建议用于生产部署,而且 API 可能会 发生变化。我们预计它会在未来几周内进入 beta 阶段。如果你在这之前就需要可用于生产环境的能力, 可以查看 AI Toolkit 客户端包中的流式传输

AI 编辑会从服务器流式传输,因此当模型生成时,它们会实时显示在文档中。编辑在你的服务器上运行,并通过实时连接应用到 Tiptap Cloud 文档,因此每个协作者都会看到它发生。

Requires Collaboration

流式传输会将编辑应用到一个实时的 Tiptap Cloud 文档,并通过 协作 将其同步到编辑器,因此这两者都必不可少。与 非流式工具不同,这里没有内联文档模式。如果你还没有使用 Tiptap Cloud 协作,那么你将需要它才能从服务器流式传输编辑。

查看 GitHub 上的源代码

技术栈

  • 一个客户端 React 应用程序
  • 一个服务器应用程序
  • 一个 AI 代理框架,例如 Vercel AI SDK
  • OpenAI 模型
  • Server AI Toolkit

Server AI Toolkit 可与任何 AI 代理框架和 AI 模型配合使用。它可与任何前端框架和后端编程语言一起使用。本指南使用一个简单的 Node.js 服务器端点,该端点接收一个 Request 对象并返回一个 Response

在继续之前,请按照安装指南设置 Server AI Toolkit。

工作原理

  1. 你的服务器获取 tiptapEdit 工具定义并读取文档,就像它处理一个非流式 agent时一样。
  2. 当模型生成工具调用时,你的服务器会将原始输入增量作为 NDJSON startdeltaend 消息转发到 POST /v4/ai/toolkit/stream-tool
  3. 之后由 Server AI Toolkit 为你应用编辑:随着转发的增量到达,它会通过实时连接逐步更新 Tiptap Cloud 文档。你无需自行应用任何内容;文档更新会在服务器端随着增量流入而自动发生。
  4. 客户端通过 协作 渲染这些变更。编辑器本身不会调用任何流式方法,因此这些编辑会作为文档更新到达。

流式编辑

适用于大多数 AI 后端

此示例使用 Vercel 的 AI SDK,但你可以将 stream-tool 接入大多数 AI 后端架构。你只需把模型的工具调用输入转发为 NDJSON 增量即可。

在服务端,向 stream-tool 打开一个 双工 NDJSON 请求,并用模型的工具调用流来驱动它。双工请求允许你逐步向上游流式传输请求体,这样你就可以在模型生成每个工具调用增量时立即转发,而不是一次性发送整个输入。

stream-tool 请求和响应都使用 NDJSON:每行一个 JSON 对象。你会发送三种消息类型:start 用于打开流,delta 用于 AI 生成的每个工具输入片段,end 用于输入完成时。

安装依赖

在你的服务端应用中安装这些依赖:

npm install ai zod
import { gateway, stepCountIs, streamText, tool, type UIMessage } from 'ai'
import { z } from 'zod'
import { getAuthHeaders } from './server-ai-toolkit/get-auth-headers'
import { streamToolFetch } from './server-ai-toolkit/stream-tool-fetch'

// Server AI Toolkit API 基础 URL
const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

export async function handleStreamEditRequest(request: Request): Promise<Response> {
  const { messages, editorContext, documentId } = await request.json()
  // 单次编辑:任务就是最新的用户消息。
  const lastUser = (messages as UIMessage[]).filter((m) => m.role === 'user').at(-1)
  const task =
    lastUser?.parts
      .filter((p) => p.type === 'text')
      .map((p) => (p as { text: string }).text)
      .join(' ') ?? ''

  // 1. 获取 tiptapEdit 工具定义并读取文档。
  //    这些步骤与非流式流程一致。请参阅 REST API 参考。
  const { tools, systemPrompt } = await fetch(`${apiBaseUrl}/v4/ai/toolkit/fetch-tools`, {
    method: 'POST',
    headers: await getAuthHeaders(),
    body: JSON.stringify({ editorContext, tools: { tiptapEdit: true } }),
  }).then((res) => res.json())
  const tiptapEdit = tools.find((t) => t.name === 'tiptapEdit')

  const read = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
    method: 'POST',
    headers: await getAuthHeaders(documentId),
    body: JSON.stringify({
      tool: { name: 'readDocument', input: {} },
      document: { type: 'cloud', id: documentId },
      editorContext,
    }),
  }).then((res) => res.json())

  // 2. 打开到 stream-tool 的 duplex NDJSON 请求。每条消息占一行。
  const encoder = new TextEncoder()
  let controller: ReadableStreamDefaultController<Uint8Array>
  const body = new ReadableStream<Uint8Array>({ start: (c) => (controller = c) })
  const send = (message: object) =>
    controller.enqueue(encoder.encode(`${JSON.stringify(message)}\n`))

  // `streamToolFetch` 通过 HTTP/2 发送请求,因此 duplex 上传
  // 可以在反向代理下正常工作(HTTP/1.1 代理会在响应开始后清空
  // 请求体,从而截断流)。请参阅演示源码了解该辅助函数。
  // 在流式传输请求体时,始终需要 `duplex: 'half'`。
  const upstream = streamToolFetch(`${apiBaseUrl}/v4/ai/toolkit/stream-tool`, {
    method: 'POST',
    headers: { ...(await getAuthHeaders(documentId)), 'Content-Type': 'application/x-ndjson' },
    body,
    duplex: 'half',
  })

  // 3. 驱动模型。将其工具输入作为 start / delta / end 消息转发。
  const result = streamText({
    model: gateway('openai/gpt-5.4-mini'),
    system: `${systemPrompt}\n\n使用 tiptapEdit 完成编辑,然后只用一句简短的话回复,确认你修改了什么。`,
    prompt: JSON.stringify({ content: read.tool.output, task }),
    tools: {
      tiptapEdit: tool({
        description: tiptapEdit.description,
        inputSchema: z.fromJSONSchema(tiptapEdit.inputSchema),
        // 将采样限制在该工具的输入模式内。
        strict: true,
        onInputStart: () =>
          send({
            version: 1,
            type: 'start',
            editorContext,
            document: { type: 'cloud', id: documentId },
            tool: { name: 'tiptapEdit' },
            user: 'ai-assistant',
          }),
        onInputDelta: ({ inputTextDelta }) =>
          send({ version: 1, type: 'delta', argsTextDelta: inputTextDelta }),
        onInputAvailable: () => {
          send({ version: 1, type: 'end' })
          controller.close()
        },
        // 等待 AI 服务端完成应用编辑,然后返回一个
        // 结果,这样聊天轮次就能完成。
        execute: async () => {
          const res = await upstream
          await res.text()
          return { ok: res.ok }
        },
      }),
    },
    // 第 0 步:强制执行编辑。第 1 步:禁止使用工具,
    // 让模型为聊天写出确认句。
    stopWhen: stepCountIs(2),
    prepareStep: ({ stepNumber }) => ({
      toolChoice: stepNumber === 0 ? 'required' : 'none',
    }),
  })

  // 编辑通过 Y.Doc 同步到达编辑器;这个流用于驱动聊天。
  return result.toUIMessageStreamResponse()
}

请参阅 REST API 参考 了解完整的请求和响应消息结构,并参阅 演示源码 获取带错误处理、可用于生产环境的路由示例。

客户端设置

安装依赖

在你的客户端应用中安装以下依赖:

npm install @ai-sdk/react @tiptap/extension-collaboration @tiptap/react @tiptap/starter-kit @tiptap-pro/provider @tiptap/ai-toolkit ai yjs

在客户端中,添加 CollaborationServerAiToolkit 扩展,使用 TiptapCollabProvider 连接文档,并通过 AI SDK 的 useChat 钩子驱动聊天(与其他服务端演示相同)。流式编辑内容会通过 Collaboration 到达编辑器;useChat 会显示模型的回复。

import { useChat } from '@ai-sdk/react'
import { Collaboration } from '@tiptap/extension-collaboration'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { ServerAiToolkit, getEditorContext } from '@tiptap/ai-toolkit'
import { DefaultChatTransport } from 'ai'
import { useRef } from 'react'

const editor = useEditor({
  immediatelyRender: false,
  extensions: [
    StarterKit.configure({ undoRedo: false }),
    Collaboration.configure({ document: doc }),
    ServerAiToolkit,
  ],
})

// 将 editorContext 放在 ref 中,以避免 AI SDK 的陈旧闭包问题(vercel/ai#7819)。
const editorContextRef = useRef(editor ? getEditorContext(editor) : null)
editorContextRef.current = editor ? getEditorContext(editor) : null

const { messages, sendMessage, status } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/stream-edit',
    body: () => ({
      editorContext: editorContextRef.current,
      documentId: 'your-document-id',
    }),
  }),
})

以跟踪更改形式流式传输

为了让用户审阅流式编辑而不是直接应用它们,请在 start 消息中添加 reviewOptions。这样,编辑内容会以跟踪更改建议的形式出现,实时输入到文档中,用户可以接受或拒绝。

send({
  version: 1,
  type: 'start',
  editorContext,
  document: { type: 'cloud', id: documentId },
  tool: { name: 'tiptapEdit' },
  user: 'ai-assistant',
  reviewOptions: { mode: 'trackedChanges' },
})

在客户端添加 Tracked Changes 扩展,以渲染这些建议并接受或拒绝它们。请参阅 Use with Tracked Changes 指南Tracked Changes 流式演示

最终结果

结果是一个简单但精致的 AI 聊天机器人应用:

请查看 GitHub 上的源代码

下一步