---
title: "AI 代理聊天机器人"
description: "使用 Server AI Toolkit API 构建一个可以读取和编辑 Tiptap 文档的 AI 代理。"
canonical_url: "https://tiptap.zhcndoc.com/ai/ai-toolkit/agents/ai-agent-chatbot"
---

# AI 代理聊天机器人

使用 Server AI Toolkit API 构建一个可以读取和编辑 Tiptap 文档的 AI 代理。

构建一个简单的 AI 代理聊天机器人，它可以读取和编辑 Tiptap 文档。

> **Interactive demo:** [server ai agent chatbot](https://ai-toolkit-demos.vercel.app/server-ai-agent-chatbot)

查看 [GitHub 上的源码](https://github.com/ueberdosis/ai-toolkit-demos)。

## 技术栈

- 一个客户端 [React](https://react.dev/) 应用程序
- 一个服务器应用程序
- 一个 AI 代理框架，例如 [Vercel AI SDK](https://ai-sdk.dev/)
- [OpenAI](https://openai.com/) 模型
- Server AI Toolkit

Server AI Toolkit 可与任何 AI 代理框架和 AI 模型配合使用。可将其用于任何前端框架和后端编程语言。本指南使用一个简单的 Node.js 服务器端点，它接收一个 [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 对象并返回一个 [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)。

在继续之前，请按照 [安装指南](https://tiptap.zhcndoc.com/ai/ai-toolkit/install.md) 配置 Server AI Toolkit。

## 设置演示应用

```bash
npx create-next-app@latest server-ai-agent-chatbot
```

安装核心 Tiptap 包、协作扩展，以及用于 OpenAI 的 [Vercel AI SDK](https://ai-sdk.dev/)：

```bash
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-collaboration @tiptap-pro/provider ai @ai-sdk/react @ai-sdk/openai zod uuid yjs jose
```

安装 Server AI Toolkit 包：

```bash
npm install @tiptap/ai-toolkit
```

## Server API 端点

创建一个 API 端点，使用 [Vercel AI SDK](https://ai-sdk.dev/) 调用 OpenAI 模型。从 Server AI Toolkit API 获取工具定义，并通过 API 执行工具。

安装依赖

在你的服务器应用中安装这些依赖：

```bash
npm install ai @ai-sdk/openai jose zod
```

### 环境变量

> **先设置授权:**
>
> 在创建辅助函数之前，请先按照 [授权指南](https://tiptap.zhcndoc.com/ai/ai-toolkit/install.md#set-up-authorization) 配置你的环境变量和身份验证辅助函数
> (`createJwtToken` 和 `getAuthHeaders`)。

你还需要一个 OpenAI API 密钥；如果使用 Tiptap Cloud 协作，还需要提供包含你的 Document Server ID 的 `TIPTAP_CLOUD_DOCUMENT_SERVER_ID`。将它们添加到你的 `.env` 文件中，并与 [授权指南](https://tiptap.zhcndoc.com/ai/ai-toolkit/install.md#set-up-authorization) 中的变量一起配置：

```sh
# .env（除了 Server AI Toolkit 变量之外）
TIPTAP_CLOUD_DOCUMENT_SERVER_ID=document-server-id
OPENAI_API_KEY=your-openai-key # AI SDK 会自动获取它
```

### 辅助函数

创建几个辅助函数以支持你的端点并与 Server AI Toolkit 交互：

#### 获取工具定义

此函数会从 Server AI Toolkit API 获取提示词和可用的工具定义。

```ts
// lib/server-ai-toolkit/get-tool-definitions.ts
import type z from 'zod'
import { getAuthHeaders } from './get-auth-headers'

/**
 * 从 Server AI Toolkit API 获取工具定义
 */
export async function getToolDefinitions(editorContext: unknown): Promise<{
  prompt: string
  tools: {
    name: string
    description: string
    inputSchema: z.core.JSONSchema.JSONSchema
  }[]
}> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/fetch-tools`, {
    method: 'POST',
    headers: await getAuthHeaders(),
    body: JSON.stringify({
      editorContext,
      tools: {
        tiptapRead: true,
        tiptapEdit: true,
      },
    }),
  })

  if (!response.ok) {
    throw new Error(`获取工具失败：${response.statusText}`)
  }
  const responseData = await response.json()

  return {
    prompt: responseData.systemPrompt,
    tools: responseData.tools,
  }
}
```

> **使用自定义节点？:**
>
> 如果你的编辑器包含自定义节点或标记，请使用 `addJsonSchemaAwareness` 进行配置。请参见
> [自定义节点指南](https://tiptap.zhcndoc.com/ai/ai-toolkit/advanced-guides/custom-nodes.md)。

#### 执行工具

此函数通过 Server AI Toolkit API 执行工具。它会将工具名称、输入参数、文档 ID 和编辑器上下文数据发送到 API。当 JWT 包含该文档的 `Documents:Write` 权限时，服务器会自动获取并保存 Tiptap Cloud 文档。

```ts
// lib/server-ai-toolkit/execute-tool.ts
import { getAuthHeaders } from './get-auth-headers'

/**
 * 通过 Server AI Toolkit API 执行工具
 */
export async function executeTool(
  toolName: string,
  input: unknown,
  documentId: string,
  editorContext: unknown,
): Promise<{
  tool: { name: string; output: unknown }
  docChanged: boolean
  document: object | null
}> {
  const apiBaseUrl = process.env.TIPTAP_CLOUD_AI_API_URL || 'https://api.tiptap.dev'

  const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
    method: 'POST',
    headers: await getAuthHeaders(documentId),
    body: JSON.stringify({
      editorContext,
      document: {
        type: 'cloud',
        id: documentId,
      },
      user: 'ai-assistant',
      tool: {
        name: toolName,
        input,
      },
    }),
  })

  if (!response.ok) {
    throw new Error(`工具执行失败：${response.statusText}`)
  }

  return response.json()
}
```

创建 `POST /api/server-ai-agent-chatbot` 端点：

```ts
// server/server-ai-agent-chatbot.ts
import { openai } from '@ai-sdk/openai'
import { createAgentUIStreamResponse, ToolLoopAgent, tool } from 'ai'
import z from 'zod'
import { executeTool } from '@/lib/server-ai-toolkit/execute-tool'
import { getToolDefinitions } from '@/lib/server-ai-toolkit/get-tool-definitions'

export async function handleChatRequest(request: Request): Promise<Response> {
  const {
    messages,
    editorContext,
    documentId,
  }: {
    messages: unknown[]
    editorContext: unknown
    documentId: string
  } = await request.json()

  // 从 Server AI Toolkit API 获取提示词和工具定义
  const { prompt, tools: toolDefinitions } = await getToolDefinitions(editorContext)

  // 将 API 工具定义转换为 AI SDK 工具格式
  const tools = Object.fromEntries(
    toolDefinitions.map((toolDef) => [
      toolDef.name,
      tool({
        description: toolDef.description,
        inputSchema: z.fromJSONSchema(toolDef.inputSchema),
        execute: async (input) => {
          try {
            const result = await executeTool(toolDef.name, input, documentId, editorContext)
            return result.tool.output
          } catch (error) {
            console.error(`执行工具 ${toolDef.name} 失败:`, error)
            return {
              error: error instanceof Error ? error.message : '未知错误',
            }
          }
        },
      }),
    ]),
  )

  const agent = new ToolLoopAgent({
    model: openai('gpt-5.4-mini'),
    instructions: `你是一个可以编辑富文本文档的助手。
在你的回复中，请简洁并直奔主题。不过，你在文档中生成的内容不需要简洁直白：相反，它应尽可能贴合用户的请求。
在调用任何工具之前，请先用一句话或更少的话，简要说明你打算做什么，作为对任务的高层概述，就像人类作者会描述的那样。
规则：在你的回复中，不要提供任何工具调用的细节。
规则：在你的回复中，不要提供文档 HTML 内容的任何细节。

${prompt}`,
    tools,
  })

  return createAgentUIStreamResponse({
    agent,
    uiMessages: messages,
  })
}
```

## 客户端设置

安装依赖

在你的客户端应用中安装这些依赖：

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

创建一个客户端 React 组件，用于渲染支持协作的 Tiptap 编辑器以及一个简单的聊天界面。该组件利用 Vercel AI SDK 中的 [useChat hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) 来调用 API 端点并管理聊天对话。

首先，创建一个用于生成 Tiptap Cloud 协作 JWT 的 API 端点。此端点会授权客户端访问 Tiptap Cloud 协作服务中的特定文档。

创建 `POST /api/collaboration-token` 端点。

```ts
// server/collaboration-token.ts
import { SignJWT, importPKCS8 } from 'jose'

const TIPTAP_PRIVATE_KEY = process.env.TIPTAP_PRIVATE_KEY
const TIPTAP_ENVIRONMENT_ID = process.env.TIPTAP_ENVIRONMENT_ID
const TIPTAP_CLOUD_DOCUMENT_SERVER_ID = process.env.TIPTAP_CLOUD_DOCUMENT_SERVER_ID

export async function handleCollaborationTokenRequest(request: Request): Promise<Response> {
  const { documentId, userId } = await request.json()
  const privateKey = await importPKCS8(TIPTAP_PRIVATE_KEY, 'ES256')

  const token = await new SignJWT({
    permissions: [{ action: 'Documents:Write', resource: documentId }],
  })
    .setProtectedHeader({ alg: 'ES256' })
    .setIssuer(TIPTAP_ENVIRONMENT_ID)
    .setSubject(userId)
    .setAudience(['Documents'])
    .setExpirationTime('1h')
    .sign(privateKey)

  return Response.json({ token, appId: TIPTAP_CLOUD_DOCUMENT_SERVER_ID })
}
```

现在创建主页面组件：

```tsx
// components/AiChatbot.tsx
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 { TiptapCollabProvider } from '@tiptap-pro/provider'
import { ServerAiToolkit, getEditorContext } from '@tiptap/ai-toolkit'
import { DefaultChatTransport } from 'ai'
import { useEffect, useState } from 'react'
import { v4 as uuid } from 'uuid'
import * as Y from 'yjs'

export function Page() {
  const [doc] = useState(() => new Y.Doc())
  const [documentId] = useState(() => `server-ai-agent-chatbot/${uuid()}`)

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

  useEffect(() => {
    let provider: TiptapCollabProvider | undefined

    const connect = async () => {
      const response = await fetch('/api/collaboration-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ documentId, userId }),
      })

      if (!response.ok) {
        throw new Error('获取协作凭证失败')
      }

      const { appId, token } = await response.json()
      provider = new TiptapCollabProvider({
        appId,
        name: documentId,
        token,
        document: doc,
        user: 'user-1',
      })
    }

    void connect().catch(console.error)

    return () => provider?.destroy()
  }, [doc, documentId, userId])

  const editorContext = editor ? getEditorContext(editor) : null

  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/server-ai-agent-chatbot',
      body: { documentId },
    }),
  })

  const [input, setInput] = useState('替换最后一段为关于 Tiptap 的短篇故事')

  if (!editor) return null

  return (
    <div>
      <EditorContent editor={editor} />
      {messages?.map((message) => (
        <div key={message.id} style={{ whiteSpace: 'pre-wrap' }}>
          <strong>{message.role}</strong>
          <br />
          <div className="mt-2 whitespace-pre-wrap">
            {messages?.map((message) =>
              message.parts
                .filter((p) => p.type === 'text')
                .map((p) => p.text)
                .join('\n') || '加载中...'
            )}
          </div>
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (input.trim()) {
            sendMessage({ text: input }, { body: { editorContext } })
            setInput('')
          }
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit">发送</button>
      </form>
    </div>
  )
}
```

> **文档状态管理:**
>
> 此实现传入了 `document: { type: 'cloud', id: documentId }`，因此 Server AI Toolkit 会自动从 Tiptap Cloud 获取并保存文档。客户端使用 Tiptap Collaboration 实时同步更改。

## 最终结果

配合额外的 CSS 样式，效果是一个简单但精致的 AI 聊天机器人应用，使用 Server AI Toolkit 进行文档编辑：

> **Interactive demo:** [server ai agent chatbot](https://ai-toolkit-demos.vercel.app/server-ai-agent-chatbot)

查看 [GitHub 上的源码](https://github.com/ueberdosis/ai-toolkit-demos)。

## 替代方案：直接提供文档

不使用 Tiptap Cloud 文档时，你可以通过在 execute-tool 请求体中传入内联的 `document` 字段，自己提供并管理文档。

采用这种方式时，你需要在每次工具执行前获取文档，并在 `docChanged` 为 `true` 时将更新后的文档保存回去。

```ts
import { loadDocument, saveDocument } from './db'

const document = await loadDocument()
const response = await fetch(`${apiBaseUrl}/v4/ai/toolkit/execute-tool`, {
  method: 'POST',
  headers: await getAuthHeaders(),
  body: JSON.stringify({
    editorContext,
    document: {
      type: 'inline',
      content: document,
    },
    tool: {
      name: toolName,
      input,
    },
  }),
})

if (!response.ok) {
  throw new Error(`工具执行失败：${response.statusText}`)
}

const body = await response.json()

// 如果工具执行修改了文档，则存储该文档
if (body.docChanged && body.document) {
  await saveDocument(body.document)
}
```

> **tiptapRead 工具可以修改文档:**
>
> `tiptapRead` 工具可以对文档进行编辑，以便为后续的读取操作做准备。
> 因此，如果 `docChanged` 属性为 `true`，你应该在执行 `tiptapRead` 工具后更新文档。

在 `document` 字段中以内联方式提供文档有几个限制。首先，不支持 [Tiptap 评论](https://tiptap.zhcndoc.com/comments/getting-started/overview.md)，因为它们存储在 Tiptap Cloud 文档中。此外，变更无法关联到某个特定用户，也没有与 [版本历史](https://tiptap.zhcndoc.com/collaboration/documents/snapshot.md) 的内置集成。

有关 `document` 字段的更多信息，请参阅 [REST API
参考](https://tiptap.zhcndoc.com/ai/ai-toolkit/api-reference/rest-api.md)。

## 下一步

- 允许你的 AI 使用 [自定义节点指南](https://tiptap.zhcndoc.com/ai/ai-toolkit/advanced-guides/custom-nodes.md) 生成自定义元素
- 在 [Agents 部分](https://tiptap.zhcndoc.com/ai/ai-toolkit/agents.md) 了解更多可用工具
- 浏览 [REST API 参考](https://tiptap.zhcndoc.com/ai/ai-toolkit/api-reference/rest-api.md) 以查看完整的端点文档
