AI 代理聊天机器人
构建一个简单的 AI 代理聊天机器人,它可以读取和编辑 Tiptap 文档。
查看 GitHub 上的源码。
技术栈
- 一个客户端 React 应用程序
- 一个服务器应用程序
- 一个 AI 代理框架,例如 Vercel AI SDK
- OpenAI 模型
- Server AI Toolkit
Server AI Toolkit 可与任何 AI 代理框架和 AI 模型配合使用。可将其用于任何前端框架和后端编程语言。本指南使用一个简单的 Node.js 服务器端点,它接收一个 Request 对象并返回一个 Response。
在继续之前,请按照 安装指南 配置 Server AI Toolkit。
设置演示应用
npx create-next-app@latest server-ai-agent-chatbot安装核心 Tiptap 包、协作扩展,以及用于 OpenAI 的 Vercel AI SDK:
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 包:
npm install @tiptap/ai-toolkitServer API 端点
创建一个 API 端点,使用 Vercel AI SDK 调用 OpenAI 模型。从 Server AI Toolkit API 获取工具定义,并通过 API 执行工具。
安装依赖
在你的服务器应用中安装这些依赖:
npm install ai @ai-sdk/openai jose zod环境变量
先设置授权
在创建辅助函数之前,请先按照 授权指南 配置你的环境变量和身份验证辅助函数
(createJwtToken 和 getAuthHeaders)。
你还需要一个 OpenAI API 密钥;如果使用 Tiptap Cloud 协作,还需要提供包含你的 Document Server ID 的 TIPTAP_CLOUD_DOCUMENT_SERVER_ID。将它们添加到你的 .env 文件中,并与 授权指南 中的变量一起配置:
# .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 获取提示词和可用的工具定义。
// 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 进行配置。请参见
自定义节点指南。
执行工具
此函数通过 Server AI Toolkit API 执行工具。它会将工具名称、输入参数、文档 ID 和编辑器上下文数据发送到 API。当 JWT 包含该文档的 Documents:Write 权限时,服务器会自动获取并保存 Tiptap Cloud 文档。
// 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 端点:
// 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,
})
}客户端设置
安装依赖
在你的客户端应用中安装这些依赖:
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 来调用 API 端点并管理聊天对话。
首先,创建一个用于生成 Tiptap Cloud 协作 JWT 的 API 端点。此端点会授权客户端访问 Tiptap Cloud 协作服务中的特定文档。
创建 POST /api/collaboration-token 端点。
// 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 })
}现在创建主页面组件:
// 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 进行文档编辑:
查看 GitHub 上的源码。
替代方案:直接提供文档
不使用 Tiptap Cloud 文档时,你可以通过在 execute-tool 请求体中传入内联的 document 字段,自己提供并管理文档。
采用这种方式时,你需要在每次工具执行前获取文档,并在 docChanged 为 true 时将更新后的文档保存回去。
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 评论,因为它们存储在 Tiptap Cloud 文档中。此外,变更无法关联到某个特定用户,也没有与 版本历史 的内置集成。
有关 document 字段的更多信息,请参阅 REST API
参考。
下一步
- 允许你的 AI 使用 自定义节点指南 生成自定义元素
- 在 Agents 部分 了解更多可用工具
- 浏览 REST API 参考 以查看完整的端点文档