为网站添加 AI 搜索功能:语义搜索与 RAG 实现指南
一个典型的痛点:某文档站有 400 多篇教程,用户搜索「怎么改页面标题」什么都搜不到,因为页面里写的是「修改 title 标签」。关键词搜索按字面匹配,用户必须用词精确才能命中,而真实用户的表达千奇百怪。AI 语义搜索把「文本」编码成「向量」,在语义空间里找最近邻——即使措辞完全不同,也能返回意思相近的内容。下面从原理、实现到选型,给出可落地的方案。
语义搜索 vs 关键词搜索
| 特性 | 关键词搜索 | AI 语义搜索 |
|---|---|---|
| 匹配方式 | 字面匹配 | 语义理解 |
| 拼写错误 | 不支持 | 自动纠错 |
| 同义词 | 需手动配置 | 自动理解 |
| 自然语言 | 不支持 | 完全支持 |
| 冷启动 | 无需训练 | 需嵌入模型 |
| 搜索质量 | ⭐⭐ | ⭐⭐⭐⭐ |
需要说明:语义搜索不是要替代全文检索,实践中两者经常搭配成「混合检索」——先用 BM25 拿到关键词命中的结果,再和向量检索结果做加权融合,召回率会明显好于单独用任何一种。
技术架构
三个核心组件,缺一不可:
- 嵌入模型:把文本变成 1536 维(text-embedding-3-small)左右的向量,文本越相似向量越接近;
- 向量数据库:存向量、建索引、做最近邻查询;
- LLM 生成(可选):把检索结果组织成自然语言回答,即 RAG。
这里容易被忽略的是**分块(chunking)**策略。页面太长直接整篇嵌入,向量会被「稀释」;建议按段落或 500-1000 字符切块,块之间留 50-100 字符重叠,并写入标题、URL 等元数据,检索时才不丢上下文。
实现方案
方案一:基于向量数据库的语义搜索(推荐)
用 OpenAI 嵌入 + Chroma,代码很短,适合先跑通概念:
from openai import OpenAI
import chromadb
client = OpenAI()
chroma_client = chromadb.PersistentClient(path="./chroma_db")
collection = chroma_client.get_or_create_collection("website_content")
# 1. 索引文档
def index_content(docs):
for i, doc in enumerate(docs):
response = client.embeddings.create(
model="text-embedding-3-small",
input=doc["text"]
)
collection.add(
embeddings=[response.data[0].embedding],
documents=[doc["text"]],
metadatas=[{"title": doc["title"], "url": doc["url"]}],
ids=[f"doc_{i}"]
)
# 2. 搜索
def search(query, n_results=5):
response = client.embeddings.create(
model="text-embedding-3-small",
input=query
)
results = collection.query(
query_embeddings=[response.data[0].embedding],
n_results=n_results
)
return results
几个工程要点:嵌入接口有 QPS 限制,批量索引时要做并发控制;数据更新时用 upsert(相同 id 覆盖)而不是重复 add,避免向量库里堆满旧版本。
方案二:RAG 搜索(检索 + 生成)
在语义搜索之上,让 LLM 基于检索到的内容作答,并「引用来源」避免幻觉:
def rag_search(query):
# 1. 检索相关内容
context_docs = search(query)
context = "\n\n".join(context_docs["documents"][0])
# 2. 生成回答
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"基于以下内容回答用户问题。如果内容不足以回答,请如实告知。\n\n相关内容:\n{context}"},
{"role": "user", "content": query}
]
)
return response.choices[0].message.content
RAG 的关键是把「来源」一起返回,让回答旁列出对应的原文链接,用户能点进去核实——这也是企业知识库问答最常见的做法,接入思路可参考网站 AI 聊天机器人。
前端集成
// 搜索前端组件
class AISearch {
constructor(inputEl, resultEl) {
this.input = inputEl;
this.results = resultEl;
this.debounceTimer = null;
this.input.addEventListener('input', (e) => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.search(e.target.value);
}, 300);
});
}
async search(query) {
if (query.length < 2) return;
const response = await fetch('/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const data = await response.json();
this.renderResults(data);
}
renderResults(data) {
this.results.innerHTML = data.map(item => `
<a href="${item.url}" class="search-result">
<h4>${item.title}</h4>
<p>${item.excerpt}</p>
</a>
`).join('');
}
}
前端做了 300ms 防抖,避免每次击键都请求一次;实际部署时建议后端再加一层缓存(相同 query 结果缓存 5-10 分钟),把成本压下来。
向量数据库选择
| 数据库 | 类型 | 托管选项 | 免费额度 |
|---|---|---|---|
| Chroma | 嵌入式 | 自托管 | 完全免费 |
| Pinecone | 托管服务 | SaaS | 有免费层 |
| Weaviate | 自托管/云 | SaaS/自托管 | 有免费层 |
| Qdrant | 自托管/云 | SaaS/自托管 | 有免费层 |
| pgvector | PostgreSQL 扩展 | 自托管 | 完全免费 |
选型建议:小站先用 Chroma 或 pgvector,零成本起步;数据量到百万级向量、需要 SLA 和弹性,再考虑 Pinecone/Qdrant 的托管版。如果网站已经在用 PostgreSQL,直接加 pgvector 扩展最省事,不用引入新组件。
成本估算
以 text-embedding-3-small 为例,100 万 token 约 $0.02,一个 500 页、每页 2000 字的站点,全量索引成本不到 1 美元;检索阶段的嵌入费用几乎可以忽略,大头其实是 RAG 场景下的 LLM 生成 token。整体下来,一个中小站点的语义搜索月成本通常控制在几美元以内。
上线后怎么衡量效果
搜得准不准,不能靠感觉。建议上线前先准备 50-100 条真实用户问题作为测试集,统计「前 3 条结果里有没有正确答案」的命中率,作为迭代的基线;上线后用三个指标持续跟踪:零结果率(用户搜索后一条结果都没有的比例)、搜索结果点击率、以及「搜索后是否立刻发起新搜索」的比例——后者越低说明结果越对路。每周跑一次测试集,模型或分块策略每次改动后都对比基线,语义搜索才会越用越准。
16IDC 观察
AI 语义搜索让网站的内容价值被更充分地发现:对文档、博客、知识库这类内容密集型站点,语义搜索的用户满意度通常比传统搜索高 40%-60%,因为用户不用再「猜关键词」。建议从 Chroma 或 pgvector 开始,先跑通「索引 + 检索」,再加 RAG 和混合检索逐步优化。更多AI 相关应用可参考本站相关文章。
参考:OpenAI Embeddings 文档 https://platform.openai.com/docs/guides/embeddings;Chroma 官方文档 https://docs.trychroma.com/