前言
偶然间看到Cloudflare的ai search,不需要自己配置向量化模型和问答模型就可以完成ai知识库配置,只需导入内容即可,遂尝试构建自己的ai知识库,并接入博客作为ai增强搜索以及ai知识库问答助手
知识库AI助手接入
搭建
在cloudflare中AI -->AI搜索中创建项目,数据来源可选R2存储桶和网站,先搭建知识库,所以这里选R2存储桶作为数据来源

我习惯使用Obsidian作为笔记工具,所以使用Obsidian和Remotely-Save插件将本地笔记同步到R2存储桶,然后下一步给AI网关和API等授权,cloudflare会自动处理创建网关等操作,并在ai搜索项目首次创建时进行一次索引构建,后续会定期同步数据源更新索引,也可以手动同步。
接入
在项目设置中启用Public URL,完成自定义样式设置,在src/layouts/Layout.astro文件约159行位置插入如下代码,替换<hash>和自定义的样式设置
<!-- 在布局文件末尾,</body> 之前添加 --><script type="module" src="https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js"></script>
<chat-bubble-snippetapi-url="https://<hash>.search.ai.cloudflare.com/"placeholder="Search..."theme="light"hide-branding="true"></chat-bubble-snippet>
<style is:global>/* 可选:自定义样式,作用全局 */chat-bubble-snippet {--search-snippet-primary-color: #74a7d8;--search-snippet-primary-hover: #92c6f7;--search-snippet-focus-ring: #74a7d8;--search-snippet-surface: #ffffff;--search-snippet-hover-background: #ffffff;--search-snippet-border-color: #f0f9ff;}</style>效果如下:

AI搜索接入
搭建
流程同上,这里作为网页内容搜索,所以数据来源选择网站,设置完sitemap后启用Public URL
接入
编辑src/components/Search.svelte,这里给出我用ai生成的代码,仅供参考,替换<hash>和外观配置,可能存在一些小bug,请自行修改优化:
<script lang="ts">import I18nKey from "@i18n/i18nKey";import { i18n } from "@i18n/translation";import Icon from "@iconify/svelte";import { url } from "@utils/url-utils.ts";import { onMount, tick } from "svelte";import type { SearchResult } from "@/global";
let keywordDesktop = "";let keywordMobile = "";let result: SearchResult[] = [];let isSearching = false;let pagefindLoaded = false;368 collapsed lines
let initialized = false;let showAISearch = false; // 新增:控制AI搜索显示状态let aiSearchContainer: HTMLElement; // 新增:AI搜索容器引用
const fakeResult: SearchResult[] = [ { url: url("/"), meta: { title: "This Is a Fake Search Result", }, excerpt: "Because the search cannot work in the <mark>dev</mark> environment.", }, { url: url("/"), meta: { title: "If You Want to Test the Search", }, excerpt: "Try running <mark>npm build && npm preview</mark> instead.", },];
// 新增:点击外部关闭AI搜索框const handleClickOutside = (event: MouseEvent) => { if (!showAISearch || !aiSearchContainer) return;
const target = event.target as HTMLElement; const searchBar = document.getElementById("search-bar"); const aiButton = searchBar?.querySelector('button[aria-label="AI Search"]');
// 检查点击是否在AI搜索容器、搜索框或AI按钮内部 const isClickInside = aiSearchContainer.contains(target) || searchBar?.contains(target) || aiButton?.contains(target);
if (!isClickInside) { showAISearch = false; }};
// 新增:切换AI搜索显示const toggleAISearch = async (event?: MouseEvent) => { // 阻止事件冒泡,避免立即触发外部点击检测 event?.stopPropagation();
showAISearch = !showAISearch;
if (showAISearch) { // 等待DOM更新,确保容器已渲染 await tick();
// 添加全局点击监听器 setTimeout(() => { document.addEventListener('click', handleClickOutside); }, 0);
// 如果开启了AI搜索,加载Cloudflare AI搜索库 if (!document.querySelector('script[src*="search.ai.cloudflare.com"]')) { const script = document.createElement('script'); script.type = 'module'; script.src = 'https://<hash>.search.ai.cloudflare.com/assets/v0.0.25/search-snippet.es.js'; document.head.appendChild(script);
// 添加样式 const style = document.createElement('style'); style.textContent = ` search-bar-snippet { --search-snippet-primary-color: #74a7d8; --search-snippet-primary-hover: #92c6f7; --search-snippet-focus-ring: #74a8d8; } `; document.head.appendChild(style); } } else { // 移除全局点击监听器 document.removeEventListener('click', handleClickOutside); }};
// 新增:组件卸载时清理事件监听器const cleanup = () => { document.removeEventListener('click', handleClickOutside);};
const togglePanel = () => { const panel = document.getElementById("search-panel"); panel?.classList.toggle("float-panel-closed");};
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => { const panel = document.getElementById("search-panel"); if (!panel || !isDesktop) return; if (show) { panel.classList.remove("float-panel-closed"); } else { panel.classList.add("float-panel-closed"); }};
const search = async (keyword: string, isDesktop: boolean): Promise<void> => { if (!keyword) { setPanelVisibility(false, isDesktop); result = []; return; }
if (!initialized) { return; }
isSearching = true; try { let searchResults: SearchResult[] = []; if (import.meta.env.PROD && pagefindLoaded && window.pagefind) { const response = await window.pagefind.search(keyword); searchResults = await Promise.all( response.results.map((item) => item.data()), ); } else if (import.meta.env.DEV) { searchResults = fakeResult; } else { searchResults = []; console.error("Pagefind is not available in production environment."); }
result = searchResults; setPanelVisibility(result.length > 0, isDesktop); } catch (error) { console.error("Search error:", error); result = []; setPanelVisibility(false, isDesktop); } finally { isSearching = false; }};
onMount(() => { const initializeSearch = () => { initialized = true; pagefindLoaded = typeof window !== "undefined" && !!window.pagefind && typeof window.pagefind.search === "function"; console.log("Pagefind status on init:", pagefindLoaded);
if (keywordDesktop) search(keywordDesktop, true); if (keywordMobile) search(keywordMobile, false); };
if (import.meta.env.DEV) { console.log( "Pagefind is not available in development mode. Using mock data.", ); initializeSearch(); } else { document.addEventListener("pagefindready", () => { console.log("Pagefind ready event received."); initializeSearch(); }); document.addEventListener("pagefindloaderror", () => { console.warn( "Pagefind load error event received. Search functionality will be limited.", ); initializeSearch(); // Initialize with pagefindLoaded as false });
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs setTimeout(() => { if (!initialized) { console.log("Fallback: Initializing search after timeout."); initializeSearch(); } }, 2000); // Adjust timeout as needed }
// 返回清理函数 return cleanup;});
$: if (initialized && keywordDesktop) { (async () => { await search(keywordDesktop, true); })();}
$: if (initialized && keywordMobile) { (async () => { await search(keywordMobile, false); })();}
// 新增:当AI搜索关闭时,移除事件监听器$: if (!showAISearch) { document.removeEventListener('click', handleClickOutside);}</script>
<!-- 桌面版搜索容器 --><div class="hidden lg:flex flex-col relative"> <!-- search bar for desktop view --> <div id="search-bar" class="flex transition-all items-center h-11 mr-2 rounded-lg bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06] dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10 "> <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon> <input placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop} on:focus={() => search(keywordDesktop, true)} class="transition-all pl-10 text-sm bg-transparent outline-0 h-full w-40 active:w-60 focus:w-60 text-black/50 dark:text-white/50" >
<!-- 新增:AI搜索按钮 --> <button on:click={toggleAISearch} aria-label="AI Search" class="ml-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10" style:background-color={showAISearch ? '#92c6f7' : 'transparent'} > <Icon icon="material-symbols:smart-toy-outline" class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}" ></Icon> </button> </div>
<!-- AI搜索栏(桌面版,在搜索框下方弹出) --> {#if showAISearch} <div bind:this={aiSearchContainer} class="absolute top-full left-0 right-0 mt-2 z-50" role="dialog" tabindex="0" aria-modal="true" on:click|stopPropagation on:keydown={(e) => { if (e.key === "Escape") { showAISearch = false; } }} > <search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet> </div> {/if}</div>
<!-- toggle btn for phone/tablet view --><button on:click={togglePanel} aria-label="Search Panel" id="search-switch" class="btn-plain scale-animation lg:!hidden rounded-lg w-11 h-11 active:scale-90"> <Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon></button>
<!-- search panel --><div id="search-panel" class="float-panel float-panel-closed search-panel absolute md:w-[30rem]top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2"> <!-- search bar inside panel for phone/tablet --> <div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl bg-black/[0.04] hover:bg-black/[0.06] focus-within:bg-black/[0.06] dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10 "> <Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon> <input placeholder="Search" bind:value={keywordMobile} class="pl-10 absolute inset-0 text-sm bg-transparent outline-0 focus:w-60 text-black/50 dark:text-white/50" >
<!-- 新增:移动端AI搜索按钮 --> <button on:click={toggleAISearch} aria-label="AI Search" class="absolute right-2 p-2 rounded-lg transition-colors duration-200 hover:bg-black/[0.08] dark:hover:bg-white/10" style:background-color={showAISearch ? '#92c6f7' : 'transparent'} > <Icon icon="material-symbols:smart-toy-outline" class="text-[1.25rem] transition-colors {showAISearch ? 'text-white' : 'text-black/50 dark:text-white/50'}" ></Icon> </button> </div>
<!-- AI搜索栏(移动端,在搜索框下方) --> {#if showAISearch} <div class="lg:hidden mt-2 mb-4 relative z-10" role="dialog" aria-label="AI Search" tabindex="-1" on:click|stopPropagation on:keydown={(e) => { if (e.key === "Escape") { showAISearch = false; } }} > <!-- 包裹容器,确保AI搜索组件能够正常展开 --> <div class="ai-search-container-mobile"> <search-bar-snippet placeholder="使用AI搜索..." api-url="https://<hash>.search.ai.cloudflare.com/" hide-branding="true"></search-bar-snippet> </div> </div> {/if}
<!-- search results --> {#each result as item} <a href={item.url} class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block rounded-xl text-lg px-3 py-2 hover:bg-[var(--btn-plain-bg-hover)] active:bg-[var(--btn-plain-bg-active)]"> <div class="transition text-90 inline-flex font-bold group-hover:text-[var(--primary)]"> {item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-[var(--primary)]"></Icon> </div> <div class="transition text-sm text-50"> {@html item.excerpt} </div> </a> {/each}</div>
<style> input:focus { outline: 0; } .search-panel { max-height: calc(100vh - 100px); overflow-y: auto; }
/* Cloudflare AI搜索组件样式 */ search-bar-snippet { --search-snippet-primary-color: #74a7d8; --search-snippet-primary-hover: #92c6f7; --search-snippet-focus-ring: #74a8d8; width: 100%; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
/* 移动端AI搜索容器样式 */ .ai-search-container-mobile { position: relative; width: 100%; min-height: 60px; /* 确保有最小高度 */ }
/* 确保AI搜索组件在移动端能够正常展开 */ .ai-search-container-mobile search-bar-snippet { position: static !important; /* 覆盖可能的绝对定位 */ max-height: none !important; /* 移除高度限制 */ overflow: visible !important; /* 确保内容可见 */ }
/* 针对Cloudflare AI搜索组件的特定样式 */ .ai-search-container-mobile ::ng-deep search-bar-snippet, .ai-search-container-mobile ::slotted(search-bar-snippet) { position: static !important; max-height: none !important; overflow: visible !important; }
/* 确保搜索结果列表能够正常显示 */ .search-panel { overflow: visible; /* 允许内容溢出 */ }
/* 当AI搜索激活时,调整搜索面板样式 */ .search-panel:has(.ai-search-container-mobile search-bar-snippet[expanded]) { overflow-y: auto; /* 恢复滚动 */ max-height: 80vh; /* 增加最大高度 */ }</style>效果如下:

完结撒花
cf的AI搜索省去了自己配置嵌入模型和前端查询响应模型的繁琐步骤,并且每天提供10k神经元额度,这个评价必须给到夯。目前模型响应速度和国内访问速度都还不错,写完笔记点点Remotely-Save就能推送到知识库😋
题外话
AI发展真的太快了,从最初chatgpt发布,只觉得这玩意儿能聊天还挺好玩,受限于没有境外信用卡和网络环境,对gpt的使用仅限于网页聊天。到deepseek发布,第一次知道ai可以通过api接入到自己的程序里面,那时候deepseek每个月还有10元的免费额度,低廉的调用成本带来了巨量的使用量,可能还有竞争对手的攻击,让其api接口一度瘫痪,甚至连官网的reasoner模型都基本无法响应。后来国内各厂商也是紧追其后,阿里、字节等都发布了自己的模型。
从高中到大学短短几年,见证了ai从娱乐聊天机器人,变成了实打实的生产力工具,自己对ai的依赖也是越来越重。初学逆向时,我经常把整段的代码贴给ai,可往往一个花指令就能骗过ai,到现在比赛,配好agent,配好ida-mcp和wsl,接个powerful的模型api,ai接管了从代码分析到动态调试、接收报错继续修正的所有流程,除了烧token外几乎不需要额外介入。很难想象,赛场上隐藏在屏幕之下和你对线的,可能不是碳基生物。
openclaw发布后,总能刷到“付费上门安装龙虾”或是“付费卸载龙虾”一类的推文,且不论是博人眼球亦或是确有此事,ai对日常生活渗透之快是不可置否的,使用技术的门槛降低也许是好事,但对于我而言,对ai的依赖总是无声无息的消磨着我学习的欲望,盯着电脑花几个小时调试程序,不如问ai来的快,甚至容易产生”这就是我的水平”的错觉,希望自己能时刻保持清醒,继续学习🫡