# Fumadocs (Framework Mode, React Router): AI 與 LLM
URL: /zh-TW/docs/react-router/integrations/llms
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/llms.zh-TW.mdx

在 Fumadocs 中整合 AI 功能。

## 供 LLM 使用的文件 [#docs-for-llm]

以 Markdown 格式將您的文件提供給 LLM 與 AI 代理：

```package-install
npx @fumadocs/cli feature llms
```

它會為您的 docs 集合啟用處理後的 Markdown、在 `lib/source.ts` 中加入 `docsLlms`，並提供下方列出的[檔案](#files)。若您偏好手動設定，可參考此處列出的產生檔案。

### `docsLlms` [#docsllms]

`llms()` 會將您的文件算繪為供 LLM 使用的格式。`renderPage` 會將單一頁面轉換為 Markdown，且依據的是處理後的文件，而非原始檔案內容：

```ts title="lib/source.ts"
import { llms } from 'fumadocs-core/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => `# ${page.data.title} (${page.url})

${await page.data.getText('processed')}`,
});

```

| 方法             | 輸出                          |
| -------------- | --------------------------- |
| `index(lang?)` | `llms.txt` 索引，依據頁面樹建立       |
| `page(page)`   | 單一頁面，以 `renderPage` 算繪      |
| `full(lang?)`  | 所有頁面以 `renderPage` 算繪後合併的內容 |
| `files()`      | [供 AI 代理使用的檔案](#files)的路徑   |
| `serve(path)`  | 指定路徑檔案的回應                   |

<Callout title="renderPage 為必要選項">
只有在傳入 `renderPage` 時，才能使用 `page()`、`full()`、`files()` 與 `serve()`，因為
Fumadocs 無從得知您的內容來源如何提供 Markdown。
</Callout>

`renderPage` 會讀取頁面處理後的 Markdown，因此請啟用 `includeProcessedMarkdown`：

```ts title="source.config.ts"
import { defineDocs } from 'fumadocs-mdx/config';

export const docs = defineDocs({
  dir: 'content/docs',
  docs: { // [!code ++:5]
    postprocess: {
      includeProcessedMarkdown: true,
    },
  },
});

```

MDX 元件會以 JSX 語法呈現。若要將其算繪為 [Markdown 形式](/docs/headless/utils/markdown#asmarkdown)，請將[您的 MDX 元件](/docs/headless/mdx/remark-llms#jsx-elements)作為頁面 props 傳入。

[`@fumadocs/local-md`](/docs/react-router/integrations/content/local-md) 這類執行階段內容來源會隨需解析。只要傳入其 `getSource`，CLI 就會產生相同的檔案：

```ts
export const docsLlms = llms(getSource, {
  renderPage: (page) => `# ${page.data.title} (${page.url})

${page.data.content}`,
});
```

使用 `getSource` 時，`index()` 與 `indexNode()` 會回傳 Promise，而非字串。

### 檔案 [#files]

CLI 會為 AI 代理提供以下檔案：

* `/llms.txt`：文件的索引，附有每個頁面 Markdown 版本的連結。
* `/llms-full.txt`：所有頁面的內容。
* `<page>.md`：單一頁面的 Markdown，例如 `/docs/ui.md`。位於 `/` 的頁面則對應 `/index.md`。
* `<section>/llms-full.txt`：某個[區段](#sections)的內容，例如 `/docs/ui/llms-full.txt`。

若要將特定頁面 (例如草稿) 排除在這些檔案之外，請傳入 `filter`。`llms.txt` 仍會列出這些頁面，但會連結至其 HTML 頁面：

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  filter: (page) => page.slugs[0] !== 'drafts',
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

在這些檔案中，`renderUrl` 會接收連結所指向檔案的路徑，例如頁面的 `.md` 檔案。

讀取頁面 HTML 的 AI 代理，會透過 `<head>` 中的連結找到該頁面的 Markdown：

```html
<link rel="alternate" type="text/markdown" href="/docs/ui.md" />
```

### 區段 [#sections]

區段是頁面樹中擁有獨立 `llms-full.txt` 的資料夾，讓 AI 代理能分段閱讀大型文件。預設情況下，每個根資料夾都是一個區段。在 `llms.txt` 中，資料夾對應的那一行會連結到其所屬區段的 `llms-full.txt`。

若要自訂區段，請傳入 `section`。它會接收一個資料夾，並回傳該資料夾是否為區段：

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  // 所有含 index 頁面的資料夾
  section: (folder) => folder.index !== undefined,
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

在大型網站上，您可以收合區段，讓 `llms.txt` 保持精簡。收合的區段在 `llms.txt` 中只會列為一個連結，指向該區段專屬的 `<section>/llms.txt`，再由該檔案列出區段內的頁面：

```ts title="lib/source.ts"
export const docsLlms = llms(source, {
  section: (folder) => (folder.root ? { collapse: true } : false),
  renderPage: async (page) => `# ${page.data.title}\n\n${await page.data.getText('processed')}`,
});
```

區段的檔案會以其資料夾 index 頁面的 URL 提供。若資料夾沒有 index 頁面，則改用其中頁面所在的 URL 目錄。當兩個資料夾共用同一個 URL 時 (例如資料夾群組) ，請傳入 `asPath`，將其中一個改放在其他路徑下提供，例如 `{ asPath: '/docs/guides' }`。

### 路由 [#routes]

`files()` 會列出檔案的路徑，而 `serve(path)` 會回應指定路徑上的檔案，因此每個路由只需將請求路徑傳給 `serve()` 即可。

React Router 無法比對與文件頁面同層的路徑 (例如 `/docs/ui.md`) ，因此改由一個資源路由 (resource route) 在 `/llms.mdx` 下提供這些檔案，並透過 `renderUrl` 將 `llms.txt` 中的連結指向該處。根路由的中介軟體也會在已比對到的路由下提供 `/docs/ui.md` 及其他檔案：

```ts tab="app/routes/llms.txt.ts"
import { docsLlms } from '@/lib/source';

export async function loader() {
  return (await docsLlms.serve('/llms.txt'))!;
}

```

```ts tab="app/routes/llms-full.txt.ts"
import { docsLlms } from '@/lib/source';

export async function loader() {
  return (await docsLlms.serve('/llms-full.txt'))!;
}

```

```ts tab="app/routes/llms.mdx.ts"
import type { Route } from './+types/llms.mdx';
import { docsLlms } from '@/lib/source';

export async function loader({ params }: Route.LoaderArgs) {
  return (await docsLlms.serve(`/${params['*']}`)) ?? new Response(null, { status: 404 });
}

```

```ts tab="app/lib/source.ts"
import { llms } from 'fumadocs-core/source';

export const docsLlms = llms(source, {
  renderPage: async (page) => `# ${page.data.title} (${page.url})

${await page.data.getText('processed')}`,
  // link to the files under `/llms.mdx`
  renderUrl: (item) => (/^\/.*\.(md|txt)$/.test(item.url) ? `/llms.mdx${item.url}` : item.url),
});

```

```ts tab="app/routes.ts"
import { index, route, type RouteConfig } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('llms.txt', 'routes/llms.txt.ts'), // [!code ++:3]
  route('llms-full.txt', 'routes/llms-full.txt.ts'),
  route('llms.mdx/*', 'routes/llms.mdx.ts'),
] satisfies RouteConfig;

```

```tsx tab="app/root.tsx"
import type { Route } from './+types/root';
import { isMarkdownPreferred } from 'fumadocs-core/negotiation'; // [!code ++:18]
import { docsLlms } from '@/lib/source';

/** serve the files of `docsLlms` at their paths, and docs pages as Markdown to clients that prefer it */
const llmsMiddleware: Route.MiddlewareFunction = async ({ request }) => {
  const { pathname } = new URL(request.url);
  const file = await docsLlms.serve(pathname);
  if (file) return file;

  const markdown = isMarkdownPreferred(request) && (await docsLlms.serve(`${pathname}.md`));
  if (markdown) {
    // this URL has two representations, selected by `Accept`
    markdown.headers.set('Vary', 'Accept');
    return markdown;
  }
};

export const middleware = [llmsMiddleware];

```

若路徑沒有對應的檔案，`serve()` 會回傳 `undefined`，讓中介軟體能將請求繼續往下傳遞。使用 `hideLocale: 'always'` 時，URL 不會包含語系：請將語言作為 `serve()` 的第二個引數及 `files()` 的第一個引數傳入。

#### 靜態輸出 [#static-output]

在 SPA 模式下，請在 `react-router.config.ts` 中預先算繪這些檔案。React Router 載入設定檔時不會套用您的 Vite 外掛，因此請使用 Vite 的 `runnerImport` 載入來源：

```ts title="react-router.config.ts"
import type { Config } from '@react-router/dev/config';
import { fumadocsMdx } from 'fumadocs-mdx/vite';
import { runnerImport } from 'vite';

export default {
  ssr: false,
  async prerender({ getStaticPaths }) {
    const paths = getStaticPaths();
    const { docsLlms } = (
      await runnerImport<typeof import('./app/lib/source')>('./app/lib/source.ts', {
        plugins: [fumadocsMdx()],
        resolve: { tsconfigPaths: true },
      })
    ).module;

    for (const path of await docsLlms.files())
      paths.push(path === '/llms.txt' || path === '/llms-full.txt' ? path : `/llms.mdx${path}`);
    return paths;
  },
} satisfies Config;
```

#### `Accept` [#accept]

AI 代理可以透過 `Accept` 標頭要求取得 Markdown。

當請求偏好 Markdown 時，上述中介軟體會以文件頁面的 Markdown 回應。

<Callout>
預先算繪的頁面會在中介軟體執行前就以靜態檔案形式提供，因此一律以 HTML 回應。
</Callout>

同一個 URL 現在有兩種表示形式，因此回應需要加上 `Vary: Accept`，以避免快取衝突。

### 頁面動作 [#page-actions]

頁面動作可讓讀者複製頁面的 Markdown，或在 AI 應用程式中開啟該頁面。這些動作會讀取頁面的 [`.md` 檔案](#files)。

![AI 頁面動作](/docs/ai-page-actions.png)

從文件頁面的載入器回傳 Markdown URL，再於頁面中算繪這些動作：

```tsx
import { MarkdownCopyButton, ViewOptionsPopover } from 'fumadocs-ui/layouts/docs/page';

// 在您的載入器中
return {
  path: page.path,
  markdownUrl: `/llms.mdx${page.url}.md`,
};

// 在您的頁面中
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
  <MarkdownCopyButton markdownUrl={markdownUrl} />
  <ViewOptionsPopover
    markdownUrl={markdownUrl}
    githubUrl={`https://github.com/${owner}/${repo}/blob/main/content/docs/${encodeURI(path)}`}
  />
</div>;
```

## MCP 伺服器 [#mcp-server]

透過 [MCP](https://modelcontextprotocol.io) 伺服器將您的文件開放給 AI 代理使用，並提供列出、搜尋及讀取頁面的工具。

```package-install
npx @fumadocs/cli feature mcp
```

這會在上述 [LLM 路由](#docs-for-llm)之外，再建立一個使用 Streamable HTTP 傳輸層的 `/api/mcp` 路由。

這些工具來自 [`fumadocs-core/mcp`](/docs/headless/utils/mcp)。

```ts title="app/routes/api.mcp.ts"
import type { Route } from './+types/api.mcp';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';
import { createFromSource } from 'fumadocs-core/search/server';
import { docsLlms, source } from '@/lib/source';

const handler = createMcpHandler(() => {
  const mcp = new McpServer(
    {
      name: 'docs',
      version: '1.0.0',
    },
    // the tools never change, a stream to watch them would hold a serverless function open
    { capabilities: { tools: { listChanged: false } } },
  );

  registerSourceTools(mcp, source, docsLlms);
  registerSearchTool(mcp, createFromSource(source));

  return mcp;
});

export async function loader({ request }: Route.LoaderArgs) {
  return handler.fetch(request);
}

export async function action({ request }: Route.ActionArgs) {
  return handler.fetch(request);
}

```

您可以透過以下方式將 AI 代理連接到此路由：

```json
{
  "mcpServers": {
    "docs": { "url": "https://your-site.com/api/mcp" }
  }
}
```

## WebMCP [#webmcp]

<Callout type="warn" title="實驗性功能">
[WebMCP](https://webmachinelearning.github.io/webmcp/) 是一項仍處於早期階段的 Web 標準，目前可在
Chrome 149+ 中透過 `#enable-webmcp-testing` 旗標及來源試用 (origin trial) 使用。此 API 日後仍可能
變動。
</Callout>

上述 MCP 伺服器服務的是連線至您網站的 AI 代理；WebMCP 則是在讀者正在瀏覽的頁面上，將工具提供給瀏覽器內建的 AI 代理使用。

```package-install
npx @fumadocs/cli feature webmcp
```

它會以 [LLM 路由](#docs-for-llm)為基礎，建立一個 `WebMCP` 元件。請在 `<RootProvider />` 內算繪此元件。

* `search_docs` 會查詢搜尋 API 路由。您可以將 `fetchClient` 替換為[其他用戶端](/docs/react-router/search)。
* `read_page` 會從 `/llms.mdx/docs/page.md` 取得頁面的 Markdown 內容。

## 詢問 AI [#ask-ai]

![AI 搜尋](/docs/ai-search.png)

您可以使用 Fumadocs CLI 安裝 AI 聊天對話方塊。

<Tabs items={["AI SDK","LLMGateway","Inkeep AI"]}>
<Tab>
```npm
npx @fumadocs/cli add ai/openrouter
```

它會透過 Vercel AI SDK 自動完成 [OpenRouter](https://openrouter.ai) 的設定，並為 AI 提供 `/search` 工具。

您可以修改 `/api/chat` 路由來改用其他模型。

> Fumadocs 不提供 AI 模型，需由您自行決定。
>
> 您的 AI 模型可以使用上方產生的 `llms-full.txt` 檔案；若搭配第三方解決方案，也能使用更多元的資訊來源。
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/llmgateway
```

它會透過 Vercel AI SDK 自動完成 [LLMGateway](https://llmgateway.io) 的設定，並為 AI 提供 `/search` 工具。

LLMGateway 是開放原始碼的 API 閘道，可將請求代理至 300 多個語言模型，因此同一個路由處理常式只需變更模型 ID 即可切換供應商。

將您的 LLMGateway API 金鑰加入環境變數：

```dotenv
LLM_GATEWAY_API_KEY="..."
```

此路由預設使用 `anthropic/claude-3.5-sonnet`。若要改用 LLMGateway 支援的其他模型，請設定 `LLM_GATEWAY_MODEL` 環境變數，無須修改程式碼：

```dotenv
# 可使用 https://llmgateway.io/models 中的任何模型 ID
LLM_GATEWAY_MODEL="openai/gpt-4o"
```
</Tab>

<Tab>
```npm
npx @fumadocs/cli add ai/inkeep
```

它會透過 Vercel AI SDK 自動完成 [Inkeep AI](https://inkeep.com) 的設定。

將您的 Inkeep API 金鑰加入環境變數：

```dotenv
INKEEP_API_KEY="..."
```
</Tab>
</Tabs>

文件版面配置會依據 `aiChat` 選項算繪聊天介面。此選項需要用戶端狀態，因此請為您的版面配置建立用戶端元件，並用它取代原本的版面配置：

```tsx tab="components/ai/layout.tsx"
'use client';
// 也可改用 Notebook、Glass 或 Spacious 版面配置
import { DocsLayout as Layout, type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
// 已安裝的元件
import { AIChat, AIChatPanel, useAIChat } from './search';

export function DocsLayout(props: DocsLayoutProps) {
  return (
    <AIChat>
      <ChatLayout {...props} />
    </AIChat>
  );
}

function ChatLayout(props: DocsLayoutProps) {
  const { open, setOpen } = useAIChat();

  return (
    <Layout
      {...props}
      aiChat={{ open, onOpenChange: setOpen, panel: <AIChatPanel />, trigger: true }}
    />
  );
}
```

```tsx tab="app/routes/docs.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/docs'; // [!code --]
import { DocsLayout } from '@/components/ai/layout'; // [!code ++]
```