---
title: 向 AI 提问
description: 一个由 AI 提供支持的聊天助手，通过上下文感知的回答帮助用户理解您的文档
type: conceptual
summary: 具有上下文感知搜索、持久历史记录和建议提示的 AI 聊天助手。
prerequisites:
  - /docs/env
related:
  - /docs/open-in-chat
  - /docs/llms-txt
  - /docs/configuration
---

The Ask AI feature provides an intelligent chat assistant built directly into your documentation. Users can ask questions about your docs and get instant, context-aware answers powered by AI. The assistant can search through your documentation, understand context, and provide helpful responses.

## 工作原理

The AI chat assistant is available in two ways:

### 1. 每页快速访问

每个文档页面的标题旁的页面操作菜单中都有 "Ask AI about this page" 操作，在移动端则位于描述下方的操作行中。读者选择该操作后，Ask AI 会：

1. 清除当前对话（包括浏览器中存储的历史记录）并打开聊天
2. 在输入框上方显示一张标明该页面的卡片。读者可以移除卡片，在不附带页面的情况下提问
3. 将该页面随读者的下一条消息一起发送，然后用该消息上标明页面的标签替换卡片

Ask AI 只发送页面路径。聊天路由会从站点自身的内容中读取该页面的完整 Markdown，并以此为依据回答，因此读者无需粘贴任何内容就能询问"这个页面讲了什么？"。只有下一条消息会附带该页面，重新加载后该消息上的标签仍会保留。

聊天路由只读取 Ask AI 可见的页面。如果路径指向通过 `excludeFrom: [chat]` 或 `internal: true` 隐藏的页面、被路由的 `filterPage` 选项移除的页面，或者不指向任何页面，路由会忽略该页面并正常回答问题。

更新日志条目不属于聊天路由的内容来源，因此在更新日志页面上，"Ask AI about this page" 保持原有行为：打开聊天并预填一条请 Ask AI 阅读该条目 URL 的提示，读者可以在发送前编辑。

同一菜单中的 "Open in ChatGPT"、"Open in Claude" 等外部提供商仍会使用链接到该页面的提示打开对应的提供商。

### 2. 全局聊天界面

Users can open the chat interface at any time by:

- Clicking the "Ask AI" button in the navbar
- Using the keyboard shortcut: `⌘I` (Mac) or `Ctrl+I` (Windows/Linux)

Once open, users can:

- Ask questions about any part of your documentation
- View the documentation pages used as sources for each answer
- Copy an answer as Markdown
- Get step-by-step guidance on complex topics

## 功能

### 上下文感知搜索

The AI assistant includes a built-in `search_docs` tool that:

1. Searches through your documentation content
2. Finds relevant pages based on the user's question
3. Shows source citations with links to the referenced pages
4. Uses the content to provide accurate, contextual answers

### 持久化聊天历史

Ask AI 会将对话保存到浏览器的 IndexedDB 中，因此用户可以：

- 在页面重新加载和导航后保留对话
- 在之后访问时继续之前的对话
- 从聊天标题栏复制或删除对话

Geistdocs 会在对话的最近一条消息之后保留该对话 30 天。每条新消息都会重新开始计时，重新加载页面则不会。当 Ask AI 加载超过保留期的对话时，会清除该对话并以空白状态开始。

设置 `ai.history.retentionDays` 可以更改保留期：

```tsx title="geistdocs.tsx"
export const ai = {
  history: {
    retentionDays: 7,
  },
};
```

| 值                              | 行为                                                       |
| ------------------------------- | ---------------------------------------------------------- |
| 未设置                          | 在最近一条消息之后保留对话 30 天。                         |
| 正数                            | 保留对话指定的天数。`0.5` 表示 12 小时。                   |
| `0`                             | 不保存历史。Ask AI 加载时会清除已存储的对话。              |
| `Infinity`                      | 已存储的对话永不过期。                                     |
| 负数、`NaN` 或 `-Infinity`      | 使用 30 天的默认值，并在浏览器控制台中输出警告。           |

聊天历史按站点存储，并以站点 `title` 作为键，因此更改 `ai.title` 不会影响已有的历史。

### 交互功能

- **建议**：聊天为空时，用户最多会看到四个建议的问题
- **支持 Markdown**：响应以正确的 Markdown 格式呈现
- **代码语法高亮**：响应中的代码块包含语法高亮
- **活动指示**：响应流式传输时，模型推理期间显示 "Thinking..."，工具运行时显示活动标签

Ask AI 仅接受文本问题，不支持文件或图片附件。

### 移动响应式

The chat interface adapts to different screen sizes:

- **桌面**：作为侧边栏面板从右侧滑入
- **移动端**：以底部抽屉的形式打开，便于访问

## 从您自己的组件控制 Ask AI

`@vercel/geistdocs/controls` 中的 `useGeistdocsChat` 可以从您自己的客户端组件中打开、关闭 Ask AI 聊天并向其发送消息。可以在 `GeistdocsProvider` 内渲染的任何组件中调用它，例如自定义页眉、提示框或 MDX 组件。

| 属性           | 类型                                               | 说明                                                                       |
| -------------- | -------------------------------------------------- | -------------------------------------------------------------------------- |
| `enabled`      | `boolean`                                          | 当 `ai.enabled` 为 `false` 时为 `false`，此时所有操作都不会生效。          |
| `isOpen`       | `boolean`                                          | 聊天面板是否已打开。                                                       |
| `open`         | `() => void`                                       | 打开聊天。                                                                 |
| `close`        | `() => void`                                       | 关闭聊天。                                                                 |
| `toggle`       | `() => void`                                       | 聊天关闭时打开，打开时关闭。                                               |
| `openWithPage` | `(page?: { url: string; title?: string }) => void` | 开始关于某个页面的新对话，与 "Ask AI about this page" 相同。默认为当前页面。 |
| `ask`          | `(text: string) => void`                           | 打开聊天并将 `text` 作为用户消息发送。                                     |

### 在自定义页眉中添加"询问此页面"按钮

`openWithPage` 的行为与 "Ask AI about this page" 操作相同：清除对话、打开聊天，并将页面附加到读者的下一条消息。

```tsx title="components/ask-about-page-button.tsx"
"use client";

import { useGeistdocsChat } from "@vercel/geistdocs/controls";

export const AskAboutPageButton = () => {
  const { enabled, openWithPage } = useGeistdocsChat();

  if (!enabled) {
    return null;
  }

  return (
    <button onClick={() => openWithPage()} type="button">
      询问此页面
    </button>
  );
};
```

不传参数时，`openWithPage` 会按路径附加当前页面，卡片显示该路径。传入 `title` 可以改为显示页面标题，例如 `openWithPage({ url: "/docs/getting-started", title: "Getting Started" })`。`url` 是站点内路径，可以包含也可以不包含 `basePath`。对于绝对 URL，Ask AI 只发送其路径，因此生产环境 URL 在预览部署中同样可以解析。

### 使用 `ask` 发送问题

`ask` 会打开聊天并像读者输入一样发送文本。可以用它创建开启特定对话的按钮：

```tsx title="components/ask-button.tsx"
"use client";

import { useGeistdocsChat } from "@vercel/geistdocs/controls";

export const AskButton = ({ question }: { question: string }) => {
  const { ask, enabled } = useGeistdocsChat();

  if (!enabled) {
    return null;
  }

  return (
    <button onClick={() => ask(question)} type="button">
      {question}
    </button>
  );
};
```

`ask` 会去除文本首尾空白并忽略空字符串。聊天尚未加载时，Ask AI 会先加载聊天、恢复已存储的对话，然后发送消息。正在流式传输响应时，消息会等到响应结束后再发送，多次调用 `ask` 会按顺序发送。

先调用 `openWithPage` 再调用 `ask`，即可在新对话中发送关于某个页面的问题：

```tsx
const { ask, openWithPage } = useGeistdocsChat();

const summarize = () => {
  openWithPage();
  ask("用三个要点总结此页面。");
};
```

## 重命名聊天面板

设置 `ai.title` 可以更改聊天面板的标题、无障碍标签，以及面板加载失败时显示的错误信息。默认值为 "Ask AI"，空白值会回退到默认值。

```tsx title="geistdocs.tsx"
export const ai = {
  title: "Ask Acme AI",
};
```

`ai.title` 只影响聊天面板。导航栏中的 "Ask AI" 按钮和 "Ask AI about this page" 页面操作保持原有标签。

## 从其他路由提供聊天服务

Ask AI 默认将聊天请求发送到 `/api/chat`。当站点必须从其他路径提供聊天服务时（例如拥有 `/api/ai-chat` 并对其应用机器人防护或防火墙规则的微前端），请设置 `ai.endpoint`：

```tsx title="geistdocs.tsx"
export const ai = {
  endpoint: "/api/ai-chat",
};
```

然后将聊天路由适配器移动到对应的 App Router 路径，例如 `app/api/ai-chat/route.ts`。适配器代码保持不变。

`ai.endpoint` 是应用内路径。聊天客户端会自动添加 `basePath`，因此设置了 `basePath: "/help"` 和 `endpoint: "/api/ai-chat"` 的站点会调用 `/help/api/ai-chat`。eve agent 预热请求使用相同的 URL，`createOpenApiRoute` 生成的 OpenAPI 文档也会在配置的路径下列出聊天操作。

该值必须以单个 `/` 开头，且不能包含查询字符串、片段、空白字符、反斜杠、`.` 或 `..` 路径段（包括百分号编码形式）或末尾斜杠，也不能是其他 Geistdocs 路由已使用的路径，包括 `/api/search`、`/agents.md`、`/llms.txt`、`/sitemap.md`、`/.well-known/mcp.json` 以及站点 OpenAPI 文档路径。无效的 `ai.endpoint` 会在 `createOpenApiRoute` 运行时抛出错误，并让聊天面板显示错误状态，而不会向意外的 URL 发送请求。

## 复制回答

每个已完成的 Ask AI 回答下方都有一个复制按钮，用于将回答的文本复制为 Markdown。复制的文本不包含 "Thinking..." 和工具活动指示，也不包含来源列表。该按钮在回答流式传输完成后出现，没有文本的回答不显示该按钮，且无需任何配置。

要复制整个对话，请使用聊天标题栏中的复制按钮。

## 收集对回答的反馈

将 `ai.messageFeedback` 设置为 `true`，即可在每个已完成回答的复制按钮旁显示"赞"和"踩"按钮。`ai.messageFeedback` 默认为 `false`。

```tsx title="geistdocs.tsx"
export const ai = {
  messageFeedback: true,
};
```

投票会发送到接收[页面反馈](/docs/feedback)的同一个 Geistdocs 反馈服务，该服务会创建一个带有您的 `siteId` 标签的 GitHub issue。请求包含以下字段：

| 字段       | 内容                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------ |
| `note`     | 以 `[Ask AI]`、投票表情和问题的单行摘要开头，随后列出投票结果、完整问题、回答所显示的来源 URL，以及 Markdown 格式的回答。 |
| `thumbs`   | `"up"` 或 `"down"`。反馈服务会为 issue 添加 `thumbs-up` 或 `thumbs-down` 标签。                         |
| `url`      | 读者投票时所在的页面。                                                                                 |
| `label`    | 您的 `siteId`。                                                                                        |
| `ua`、`ip` | 读者的用户代理和 IP 地址，与页面反馈相同。                                                             |

问题是该回答之前读者发送的最近一条消息。超过 4,000 个字符的问题和回答会被截短，最多发送 10 个来源 URL。由于 `note` 包含读者输入的原始问题，请在启用 `ai.messageFeedback` 之前确认您站点的隐私要求。

每个回答只能投票一次。反馈服务接受投票后，所选按钮保持选中状态，两个按钮都会被禁用，并显示 "Thanks for your feedback" 提示。如果请求失败，会显示错误提示，读者可以再次投票。

Ask AI 会将投票与对话一起保存在浏览器中，因此重新加载后所选按钮仍保持选中，直到聊天历史被清除。投票存储在回答的消息元数据中，`createChatRoute` 会忽略该元数据，因此投票不会改变之后的聊天请求。

## 显示已登录读者的头像

从 `@vercel/geistdocs/controls` 渲染 `AskAIUser`，即可在 Ask AI 中读者的消息旁显示其头像。Geistdocs 不知道谁登录了您的站点，因此需要您在运行时从自己的身份验证中传入读者信息。

`AskAIUser` 本身不会在页面上渲染任何内容。当它在 `GeistdocsProvider` 内挂载并带有 `avatarUrl` 时，每条用户消息都会显示一个 24px 的圆形头像。当 `AskAIUser` 卸载或未收到 `avatarUrl` 时（例如读者退出登录后），用户消息会恢复为不显示头像。

| 属性        | 类型     | 说明                                                                                                                       |
| ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `avatarUrl` | `string \| null` | 读者头像的 URL。只会渲染 `https://` 和 `http://` URL 以及根相对路径（例如 `/api/avatar`）。其他值（包括 `data:` URL）会被忽略。 |
| `name`      | `string \| null` | 读者的姓名，用作头像的替代文本。未提供姓名时，头像为装饰性图片，替代文本为空。                                             |

创建一个客户端组件，从您的身份验证库读取读者信息并渲染 `AskAIUser`。以下示例使用 `useSession` hook，请替换为您所用库的客户端会话 hook：

```tsx title="components/ask-ai-user.tsx"
"use client";

import { AskAIUser } from "@vercel/geistdocs/controls";
import { useSession } from "@/lib/auth-client";

export const SignedInAskAIUser = () => {
  const { data: session } = useSession();

  if (!session?.user.image) {
    return null;
  }

  return (
    <AskAIUser avatarUrl={session.user.image} name={session.user.name} />
  );
};
```

然后在根布局的 `GeistdocsProvider` 内渲染一次：

```tsx title="app/[lang]/layout.tsx"
<GeistdocsProvider basePath={config.basePath} lang={lang}>
  <SignedInAskAIUser />
  <Navbar config={config} />
  {children}
  <Footer />
</GeistdocsProvider>
```

头像会延迟加载，并使用 `referrerPolicy="no-referrer"` 请求，因此图片主机不会收到文档页面的 URL。加载失败的头像会被隐藏。

## 配置

The environment variables `AI_GATEWAY_API_KEY` and `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` must be set, which you can read more about in the [环境变量](/docs/env) section.

The model used is `openai/gpt-5.6-sol` via the Vercel AI Gateway.

### 使用 `GEISTDOCS_CHAT_SECRET` 保持 eve 会话

在 `geistdocs.tsx` 中配置 `ai.eveAgent` 后，向 AI 提问由托管的 [eve](https://eve.dev) 智能体回答。eve 智能体模式需要仅在服务器端使用的 `GEISTDOCS_CHAT_SECRET` 环境变量，才能在多轮对话中沿用同一个 eve 会话，并支持 `prewarm: true` 会话预热。聊天路由会把每位读者的 eve 会话绑定到一个用该密钥签名的 httpOnly Cookie 上，因此页面脚本无法读取会话 ID，也无法在其他浏览器中重放它。浏览器只会在每条回答的元数据中保存流位置和一个简短且不可逆的会话标签。

`geistdocs_eve_session` Cookie 具有以下属性：

| 属性       | 值                                                                       |
| ---------- | ------------------------------------------------------------------------ |
| `HttpOnly` | 始终设置                                                                 |
| `SameSite` | `Strict`                                                                 |
| `Secure`   | 请求使用 HTTPS 时设置，包括位于发送 `x-forwarded-proto: https` 的代理之后 |
| `Path`     | 聊天路由自身的路径，例如 `/api/chat`，或基础路径下的 `/docs/api/chat`    |
| `Max-Age`  | 7 天，每轮对话后续期                                                     |

只有当对话中最新的回答带有该会话的标签，或者会话经过预热且对话中还没有 eve 回答时，聊天路由才会沿用 Cookie 中的会话。其他情况下，聊天路由会创建新的 eve 会话，在第一条消息中内联最多 8,000 个字符的之前对话内容，并设置新的 Cookie。以下情况会触发这一行为：

- 读者删除了聊天记录，或 Cookie 在 7 天内没有新的对话轮次而过期。
- 另一个标签页创建了自己的会话。每个浏览器的 Cookie 只保存一个会话，因此在两个对话之间来回切换时，每次切换都会创建新的会话。
- Cookie 被篡改、使用其他密钥签名，或为其他智能体 URL 签发。聊天路由绝不会访问此类 Cookie 所指向的会话。
- 该 eve 会话在智能体上已不再处于活动状态。

早期 Geistdocs 版本保存在读者浏览器中的聊天记录使用聊天路由会忽略的会话格式，因此这些对话会在新的会话中继续，并内联之前的对话内容。

生成至少 32 字节的密钥：

```bash title="Terminal"
openssl rand -base64 32
```

将该值作为 `GEISTDOCS_CHAT_SECRET` 添加到站点 Vercel 项目的 Production 和 Preview 环境变量中，然后重新部署。本地开发时，将其添加到 `.env.local`。每个文档站点使用单独的密钥。

Geistdocs 会按原样使用该值，因此任何 32 字节或更长的值都可以使用，包括已有的 base64 编码 32 字节密钥（44 个字符）。Geistdocs 使用自己带版本号的前缀进行签名，因此即使另一个服务也使用同一个密钥签名，两个服务也不会接受对方生成的值。

也可以通过 `createChatRoute` 的 `eveAgent.secret` 选项提供密钥。不要将密钥写入 `geistdocs.tsx`，也不要为其添加 `NEXT_PUBLIC_` 前缀。

轮换密钥会使所有现有的会话 Cookie 失效。每位读者的下一条消息会创建新的 eve 会话并内联其对话内容，因此轮换无需停机。

浏览器只会在请求来自与 Cookie 相同的站点时发送 `SameSite=Strict` Cookie。默认的 `/api/chat` 路由与文档页面位于同一域名下，因此无需额外配置。如果文档页面调用其他站点上的聊天路由，浏览器不会发送该 Cookie，每一轮对话都会创建新的 eve 会话。

未设置 `GEISTDOCS_CHAT_SECRET` 时，eve 智能体模式仍会回答每个问题，但不会设置或读取会话 Cookie：

- 每一轮对话都会创建新的 eve 会话，并在消息中内联最多 8,000 个字符的之前对话内容。
- 聊天路由会以 `400` 响应拒绝 `{ prewarm: true }` 请求。
- 服务器会输出一条提及 `GEISTDOCS_CHAT_SECRET` 的警告。

长度不足 32 字节的密钥会在路由模块加载时抛出错误。

### 选择代理或智能体接收的上下文

`createChatRoute` 的 `contextInjection` 选项控制 Geistdocs 在代理模式和 eve 智能体模式下向转发的消息中添加哪些文档上下文。默认的 AI Gateway 模式会忽略该选项，因为该模式使用自己的工具检索文档。

| `contextInjection` | 未附加页面的消息 | 附加了页面（`pageContext`）的消息 | Geistdocs 添加的来源引用 |
| ------------------ | ---------------- | --------------------------------- | ------------------------ |
| `"auto"`（默认）   | 在第一条用户消息中内联当前页面的 1,500 个字符摘录，以及检索得到的最多 5 个相关页面。后续消息按原样转发。 | 在第一条用户消息中，用附加的页面替代当前页面。 | 每个内联页面一条 |
| `"page-only"`      | 每一轮都按原样转发 | 在携带该页面的消息中完整内联页面，适用于任意一轮 | 无 |

当代理或智能体自行检索文档、需要原样接收读者的问题时，使用 `"page-only"`。

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  contextInjection: "page-only",
  proxy: { url: process.env.GEISTDOCS_CHAT_PROXY_URL! },
  sources: [geistdocsSource],
});
```

在 `"page-only"` 模式下，代理模式会把最后一条用户消息的文本替换为附加的页面和问题：

```md
Here's the content from the current page:

**Page:** Routing
**URL:** https://docs.example.com/docs/routing

---

<完整的页面 Markdown>

---

User question: How do I add a redirect?
```

eve 智能体模式会把附加的页面作为开头的文本部分发送（页面为一行 JSON），后面跟着问题。当该消息创建新的 eve 会话（包括替换不再活动的会话）时，页面部分后面跟着内联的之前对话内容：

```json
{
  "message": [
    {
      "type": "text",
      "text": "Page context:\n{\"title\":\"Routing\",\"url\":\"https://docs.example.com/docs/routing\",\"content\":\"<完整的页面 Markdown>\"}"
    },
    { "type": "text", "text": "How do I add a redirect?" }
  ]
}
```

对于 `pageContext: { url }`，Geistdocs 从路由的来源读取页面的完整 Markdown，并截断到 `limits.maxPageContextChars`（默认 200,000 个字符）。URL 是页面在 `siteUrl`（未设置时为请求来源）上的绝对公开 URL。旧版 `pageContext: { title, url, content }` 按原样使用。对 Ask AI 不可见或不存在的页面的引用会被忽略，消息按原样转发。

### 向 eve 智能体发送当前页面

设置 `eveAgent.clientContext: true`，即可在每条 eve 消息中发送读者正在查看的页面。eve 会把 `clientContext` 作为本轮上下文传给该轮的每次模型调用，且不会将其保存到会话历史中。

```json
{
  "message": "How do I add a redirect?",
  "clientContext": {
    "currentPage": {
      "url": "https://docs.example.com/docs/routing",
      "title": "Routing"
    }
  }
}
```

路由根据 Ask AI 客户端已经发送的 `currentRoute` 生成 `clientContext`，客户端无需改动。`url` 是该路径（含 `basePath`）在 `siteUrl` 或请求来源上的绝对 URL，会忽略查询字符串、片段以及 `currentRoute` 中的任何来源。当路由是 Ask AI 可见的页面时，`title` 为页面标题（最多 256 个字符）；否则 `currentPage` 只包含 `url`。没有 `currentRoute` 或其长度超过 2,048 个字符时，不发送 `clientContext`。发往会话 Cookie 中会话的后续消息，以及替换不再活动的会话时的第一条消息，也会携带 `clientContext`；预热请求从不携带 `clientContext`。

### 选择聊天中显示哪些智能体工具调用

默认情况下，聊天会把每个 eve 工具调用显示为活动标签，并把工具输出中的 URL 显示为来源引用。仅在服务器端使用的 `eveAgent` 选项可以改变这一行为：

- `visibleTools` 列出其调用会发送到聊天的工具名称。其他工具的调用及其结果和引用不会被转发。空数组会隐藏所有工具调用。
- `toolCitations: false` 保留活动标签，但把每个工具输出以 `null` 发送到聊天，并且不添加来源引用。

`geistdocs.tsx` 中的客户端配置 `ai.toolLabels` 会替换所列工具的 `Running <工具名>...` 活动标签，适用于所有 AI 模式。

### 在上游请求头中转发每个请求的值

`proxy.headers` 和 `eveAgent.headers` 接受静态对象或函数。函数会收到传入的聊天 `Request`，并且可以是异步函数，因此可以转发读者请求中的值。不接受参数的现有函数仍然有效。`x-evo-surface: docs` 等固定值使用静态对象即可。

### 匹配生产环境的 Vertex 或 eve 设置

Vercel 自己的文档默认通过基于 Vertex 的服务回答 Ask AI，并在功能开关后通过 eve 智能体回答。两者都会自行检索，因此路由按原样转发问题，只在读者附加页面时内联页面，并以统一的通用标签显示 eve 智能体的网页工具，不显示引用。

为每个响应方在模块作用域中创建一个路由，并按请求选择。不要在 `geistdocs.tsx` 中设置 `ai.eveAgent`：eve 路由使用仅限服务器端的 `eveAgent.url` 选项设置 URL，因为设置了 `ai.eveAgent.url` 时，带有 `proxy` 的路由会抛出错误。

```ts title="app/api/ai-chat/route.ts"
import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
import { enableDocsAgent } from "@/flags";

const vertex = createChatRoute({
  config,
  contextInjection: "page-only",
  proxy: {
    url: `${process.env.VERTEX_URL}/api/chat/docs`,
    headers: {
      "x-vercel-protection-bypass": process.env.VERTEX_BYPASS_SECRET ?? "",
    },
  },
  sources: [geistdocsSource],
});

const agent = createChatRoute({
  config,
  contextInjection: "page-only",
  eveAgent: {
    url: process.env.EVE_AGENT_URL,
    clientContext: true,
    headers: { "x-evo-surface": "docs" },
    visibleTools: ["web_search", "web_fetch", "knowledge-search"],
    toolCitations: false,
  },
  sources: [geistdocsSource],
});

export const maxDuration = 800;

export const POST = async (request: Request) =>
  (await enableDocsAgent()) ? agent.POST(request) : vertex.POST(request);
```

```tsx title="geistdocs.tsx"
export const ai = {
  endpoint: "/api/ai-chat",
  toolLabels: {
    web_search: "Searching...",
    web_fetch: "Searching...",
    "knowledge-search": "Searching...",
  },
};
```

### 请求大小限制

`createChatRoute` 会在请求到达模型、聊天代理或 eve 智能体之前拒绝过大的请求。以下默认值适用于所有 AI 模式：

| 限制选项              | 默认值                   | 超出时的响应 |
| --------------------- | ------------------------ | ------------ |
| `maxRequestBytes`     | `1_000_000` 字节（1 MB） | `413`        |
| `maxMessages`         | `200` 条消息             | `400`        |
| `maxPageContextChars` | `200_000` 个字符         | `400`        |

被拒绝的请求会收到 `{ "error": "..." }` 格式的 JSON 响应。过长的 `pageContext.content` 会被拒绝，而不是被截断。

`maxPageContextChars` 只会拒绝 API 调用方随页面文本一起发送的 `pageContext.content`。Ask AI 客户端发送不含内容的 `pageContext: { url }`，路由会从站点自身的来源读取该页面；使用 `contextInjection: "page-only"` 时，该页面的 Markdown 会在转发前截断到 `maxPageContextChars`。两种形式中的 `pageContext.url` 都必须是最多 2,048 个字符的非空字符串，否则返回 `400`。

使用 `limits` 选项修改这些限制：

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  sources: [geistdocsSource],
  limits: {
    maxRequestBytes: 500_000,
    maxMessages: 100,
    maxPageContextChars: 100_000,
  },
});
```

请求大小限制不会限制客户端调用路由的频率。`/api/chat` 是公开路由，每个被接受的请求都会调用您账户上的模型或智能体，因此请使用您自己的速率限制（例如 [Vercel Firewall 速率限制规则](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting-sdk)）包装该路由。

## 建议的提示

设置 `ai.suggestions` 来配置读者发送第一条消息之前看到的提示。选择某个建议会将其文本作为第一条消息发送。Ask AI 最多显示前四个不重复且非空的建议。

每个建议可以是字符串，也可以是包含 `text` 和可选 `icon` 的对象。字符串建议不显示图标。`icon` 字段接受任何 React 元素，例如内联 SVG 组件，或您的站点已安装的图标库中的图标：

```tsx title="geistdocs.tsx"
const RocketIcon = () => (
  <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
    <path d="M8 1.5c2.5 1.5 3.5 4 3.5 6.5L8 12 4.5 8c0-2.5 1-5 3.5-6.5Z" stroke="currentColor" />
  </svg>
);

const BookIcon = () => (
  <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
    <path d="M3 2.5h7.5v11H3zM10.5 2.5H13v11h-2.5" stroke="currentColor" />
  </svg>
);

export const suggestions = [
  "What is Acme?",
  { text: "How do I deploy?", icon: <RocketIcon /> },
  { text: "Where is the API reference?", icon: <BookIcon /> },
];
```

生成的 `lib/geistdocs/config.tsx` 会将 `suggestions` 传递给 `ai.suggestions`。您可以在 [配置](/docs/configuration) 部分了解配置文件的其余内容。
