别急着接大模型API:做AI搜索前必须避开的10个坑(附源码)
AI搜索 使用避坑指南|附源码
在大模型快速普及之后,“AI搜索”逐渐成为很多产品、团队和个人知识库的标配能力。它看起来很简单:把资料丢进去,用户提问,AI给答案。但真正做过之后你会发现,AI搜索并不是“接一个大模型API”那么简单。
很多AI搜索项目在Demo阶段效果惊艳,一上线却问题频出:答案张冠李戴、引用来源不准确、搜索不到明明存在的内容、成本暴涨、响应很慢、用户追问时上下文混乱,甚至出现一本正经胡说八道的情况。
本文将系统梳理AI搜索的核心原理、常见坑点、落地建议,并附上一份可运行的简化版源码示例,帮助你从“能跑”走向“可用”。
一、什么是AI搜索?
传统搜索通常基于关键词匹配,例如:
用户搜索“退款流程”,系统在文档中查找包含“退款”“流程”的内容。
而AI搜索通常会结合以下几类能力:
-
向量检索
- 将文档和问题转换为向量;
- 通过语义相似度找到相关内容;
- 即使用户没有使用完全相同的关键词,也能搜到相关答案。
-
全文检索
- 使用关键词、倒排索引、BM25等方式;
- 对专有名词、编号、代码、精确匹配更友好。
-
大模型生成
- 将检索到的资料交给大模型;
- 由大模型组织语言,生成自然、完整的回答。
-
RAG架构
- RAG全称为Retrieval-Augmented Generation,即“检索增强生成”;
- 先检索资料,再基于资料生成答案;
- 这是目前AI搜索最常见的技术路线。
一个典型的AI搜索流程如下:
用户问题
↓
问题改写 / 意图识别
↓
向量检索 + 关键词检索
↓
结果重排
↓
拼接上下文
↓
大模型生成答案
↓
返回答案 + 引用来源
二、AI搜索最容易踩的坑
1. 只做向量检索,不做关键词检索
很多人一开始做AI搜索,会直接使用Embedding模型,把文档切片后存入向量数据库,然后通过相似度搜索返回结果。
这种方式在普通语义问答中确实有效,但它有明显短板:
- 对产品型号、订单号、合同编号、错误码不敏感;
- 对人名、地名、品牌名等专有名词容易召回不准;
- 对短问题的理解不稳定;
- 对数值、版本号、日期等精确信息匹配较差。
例如用户问:
ERR_1024是什么原因?
如果只做向量检索,系统可能召回一堆和“错误”“原因”相关的内容,却漏掉真正包含ERR_1024的文档。
建议做法
采用混合检索:
- 向量检索负责语义相似;
- BM25或全文检索负责关键词精准匹配;
- 两者结果合并后再重排。
这样可以显著提高召回稳定性。
2. 文档切片过大或过小
RAG的核心环节之一是“文档切片”。把一篇长文档拆成多个片段,然后分别入库检索。
切片过大:
- 单个片段信息太多;
- 检索相似度被稀释;
- 传给大模型的上下文成本高;
- 容易超过上下文限制。
切片过小:
- 信息不完整;
- 上下文断裂;
- 大模型拿到片段后无法理解前因后果;
- 答案容易缺失关键条件。
例如一份退款规则中可能写道:
会员用户可在付款后7天内申请退款。
虚拟商品、定制商品不支持退款。
退款到账时间为3-5个工作日。
如果切片太小,大模型可能只看到“会员用户可在付款后7天内申请退款”,却没有看到“不支持退款”的例外条件,从而给出错误答案。
建议做法
一般中文文档可以参考:
- 每个chunk约300~800中文字符;
- 相邻chunk保留50~150字符重叠;
- 按标题、段落、列表、表格结构优先切分;
- 不要机械地按固定长度硬切。
3. 没有保留文档结构
很多团队处理文档时,只把正文抽出来,丢掉标题层级、表格、章节路径等信息。这样会导致一个严重问题:检索结果缺少上下文身份。
例如原文结构是:
第三章 售后政策
3.1 普通商品退款
3.2 虚拟商品退款
3.3 企业采购退款
如果你只保存正文,而不保存“章节标题”,当用户问“企业采购怎么退款”时,系统可能难以判断某段文字到底属于普通商品还是企业采购。
建议做法
每个切片最好保留元数据:
{
"doc_id": "refund_policy_2024",
"title": "售后政策",
"section": "3.3 企业采购退款",
"source": "https://example.com/refund",
"updated_at": "2024-05-01"
}
在交给大模型生成答案时,也要把这些信息一并放进上下文。
4. 检索结果不重排
向量数据库返回的Top K结果,不一定就是最适合回答问题的结果。尤其是当文档数量较大时,初始召回往往会包含噪声。
常见问题包括:
- 相似但不相关;
- 主题接近但条件不同;
- 历史版本文档排在新版本前面;
- FAQ中的泛化答案压过了正式政策。
建议做法
在召回之后增加Rerank重排:
初始召回 Top 30
↓
Reranker模型重新打分
↓
选出 Top 5
↓
交给大模型生成答案
如果预算有限,也可以先用规则做简单重排:
- 新版本优先;
- 标题命中优先;
- 关键词完全匹配加权;
- 权威文档优先;
- 已废弃文档降权。
5. 让大模型自由发挥
AI搜索最大的风险是“看起来很像正确答案的错误答案”。如果你没有给大模型明确约束,它会倾向于补全缺失信息。
例如检索资料中没有写退款到账时间,模型可能根据常识回答:
一般会在3-5个工作日到账。
这对用户体验非常危险,尤其在金融、医疗、法律、企业制度等场景中。
建议做法
系统提示词必须明确要求:
- 只能基于给定资料回答;
- 资料不足时要说“不确定”或“当前资料未说明”;
- 不允许编造政策、链接、数字、时间;
- 必须给出引用来源;
- 多条资料冲突时,要提示冲突并优先使用最新/最高优先级资料。
示例提示词:
你是企业知识库问答助手。
请严格基于【参考资料】回答用户问题。
如果参考资料中没有答案,请回答“当前资料未说明”,不要根据常识编造。
回答时请尽量简洁,并在关键结论后标注来源编号。
如果资料之间存在冲突,请说明冲突点。
6. 不展示引用来源
很多AI搜索产品只给答案,不给出处。这会降低可信度,也会让用户无法核验。
尤其在企业知识库、政策问答、技术文档搜索场景中,引用来源非常重要。
建议做法
返回结果应包含:
- 答案正文;
- 引用文档标题;
- 引用段落;
- 来源链接;
- 更新时间;
- 相关片段编号。
例如:
根据《售后政策》第3.3节,企业采购订单需要联系客户经理发起退款申请。[来源1]
来源:
[来源1] 售后政策 / 3.3 企业采购退款
https://example.com/refund
更新时间:2024-05-01
7. 忽略数据更新与版本管理
AI搜索不是一次性工程。知识库内容会持续变化:
- 产品功能更新;
- 政策调整;
- API文档变更;
- 旧文档废弃;
- FAQ新增。
如果没有更新机制,AI搜索很快会“过期”。
建议做法
建立文档生命周期管理:
| 状态 | 含义 |
|---|---|
| draft | 草稿,不参与检索 |
| active | 正式生效,参与检索 |
| deprecated | 已废弃,默认不参与检索 |
| archived | 归档,仅后台可查 |
同时要记录:
- 文档ID;
- 文档版本;
- 更新时间;
- 生效时间;
- 失效时间;
- 权限范围。
8. 没有权限控制
企业内部AI搜索尤其容易忽略权限问题。传统搜索中,用户不能看的文档不会出现在结果里;AI搜索也必须如此。
危险情况包括:
- 普通员工问到了财务预算;
- 外部客户问到了内部SOP;
- 销售人员看到其他区域的客户合同;
- 模型根据无权限资料生成了答案,但前端不展示来源。
最后一种尤其隐蔽:即使不显示原文,大模型也可能把敏感信息“说出来”。
建议做法
权限控制应放在检索阶段,而不是只在展示阶段。
也就是说:
用户身份
↓
计算可访问文档范围
↓
只在有权限的文档中检索
↓
生成答案
不要先全量检索,再过滤引用。
9. 没有评测集
很多AI搜索系统上线前只靠人工随便问几个问题判断效果,这很不可靠。
你需要构建一个评测集,包括:
- 高频问题;
- 边界问题;
- 容易混淆的问题;
- 无答案问题;
- 权限问题;
- 多轮追问问题;
- 版本冲突问题。
每个测试样本至少包含:
{
"question": "企业采购订单如何退款?",
"expected_answer": "需要联系客户经理发起退款申请",
"expected_sources": ["售后政策 3.3 企业采购退款"],
"must_not_include": ["7天无理由退款"]
}
评测指标可以包括:
- 召回率;
- 答案准确率;
- 引用正确率;
- 幻觉率;
- 响应时间;
- 单次查询成本。
10. 成本没有控制
AI搜索的成本主要来自:
- Embedding调用;
- 向量数据库存储;
- Rerank模型;
- 大模型生成;
- 长上下文输入;
- 多轮对话历史。
如果不控制,很容易出现“用户问一句话,系统花很多钱”的情况。
建议做法
- 对文档Embedding做增量更新,不要全量重算;
- 控制传给大模型的chunk数量;
- 对高频问题做缓存;
- 对无意义问题提前拦截;
- 使用小模型处理改写、分类等轻任务;
- 大模型输出设置合理长度;
- 监控每次查询token消耗。
三、AI搜索推荐架构
一个较稳妥的AI搜索架构可以这样设计:
┌──────────────────┐
│ 用户问题 │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ 问题清洗/改写 │
└────────┬─────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 向量检索 │ │ 关键词检索 │
└────────┬────────┘ └────────┬────────┘
└───────────────┬───────────────┘
▼
┌──────────────────┐
│ 结果合并去重 │
└────────┬─────────┘
▼
┌──────────────────┐
│ Rerank重排 │
└────────┬─────────┘
▼
┌──────────────────┐
│ 权限/版本过滤 │
└────────┬─────────┘
▼
┌──────────────────┐
│ 构造Prompt │
└────────┬─────────┘
▼
┌──────────────────┐
│ 大模型生成答案 │
└────────┬─────────┘
▼
┌──────────────────┐
│ 答案+引用来源 │
└──────────────────┘
四、附源码:一个简化版AI搜索Demo
下面给出一个Python版本的简化AI搜索示例。它实现了:
- 文档切片;
- TF-IDF关键词检索;
- 向量语义检索;
- 简单混合排序;
- 构造RAG提示词。
为了方便演示,代码使用本地库实现,不依赖向量数据库。实际生产中可以替换为 Milvus、Qdrant、Elasticsearch、OpenSearch、pgvector 等。
1. 安装依赖
pip install scikit-learn numpy jieba
2. 示例代码
# ai_search_demo.py
# -*- coding: utf-8 -*-
import jieba
import numpy as np
from dataclasses import dataclass
from typing import List, Dict, Tuple
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
@dataclass
class Document:
doc_id: str
title: str
section: str
source: str
updated_at: str
content: str
@dataclass
class Chunk:
chunk_id: str
doc_id: str
title: str
section: str
source: str
updated_at: str
content: str
def chinese_tokenizer(text: str) -> List[str]:
"""
中文分词函数。
TF-IDF对中文不友好,需要先分词。
"""
return list(jieba.cut(text))
def split_text(text: str, chunk_size: int = 300, overlap: int = 60) -> List[str]:
"""
简单滑动窗口切片。
生产环境建议按标题、段落、表格结构切分。
"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
if end >= len(text):
break
start = end - overlap
return chunks
class AISearchEngine:
def __init__(self):
self.documents: List[Document] = []
self.chunks: List[Chunk] = []
# 这里为了演示,关键词检索和“语义检索”都用TF-IDF近似。
# 真实项目中,semantic_vectorizer应替换为Embedding模型。
self.keyword_vectorizer = TfidfVectorizer(tokenizer=chinese_tokenizer)
self.keyword_matrix = None
self.semantic_vectorizer = TfidfVectorizer(tokenizer=chinese_tokenizer, ngram_range=(1, 2))
self.semantic_matrix = None
def add_documents(self, documents: List[Document]):
self.documents.extend(documents)
for doc in documents:
parts = split_text(doc.content, chunk_size=280, overlap=50)
for idx, part in enumerate(parts):
self.chunks.append(
Chunk(
chunk_id=f"{doc.doc_id}_{idx}",
doc_id=doc.doc_id,
title=doc.title,
section=doc.section,
source=doc.source,
updated_at=doc.updated_at,
content=part
)
)
def build_index(self):
texts = [
f"{chunk.title} {chunk.section} {chunk.content}"
for chunk in self.chunks
]
self.keyword_matrix = self.keyword_vectorizer.fit_transform(texts)
self.semantic_matrix = self.semantic_vectorizer.fit_transform(texts)
def search(self, query: str, top_k: int = 5) -> List[Tuple[Chunk, float]]:
if self.keyword_matrix is None or self.semantic_matrix is None:
raise RuntimeError("请先调用 build_index() 构建索引")
# 关键词检索
keyword_query_vec = self.keyword_vectorizer.transform([query])
keyword_scores = cosine_similarity(keyword_query_vec, self.keyword_matrix)[0]
# 语义检索:演示中用ngram TF-IDF模拟
semantic_query_vec = self.semantic_vectorizer.transform([query])
semantic_scores = cosine_similarity(semantic_query_vec, self.semantic_matrix)[0]
# 混合得分
# 实际项目中可以动态调整权重:
# - 短查询、包含编号/代码时,提高关键词权重
# - 口语化长问题,提高语义权重
final_scores = 0.45 * keyword_scores + 0.55 * semantic_scores
# 简单规则加权:如果标题或章节命中关键词,额外加分
for i, chunk in enumerate(self.chunks):
if query in chunk.title or query in chunk.section:
final_scores[i] += 0.1
ranked_indices = np.argsort(final_scores)[::-1][:top_k]
results = []
for idx in ranked_indices:
if final_scores[idx] > 0:
results.append((self.chunks[idx], float(final_scores[idx])))
return results
def build_prompt(self, query: str, results: List[Tuple[Chunk, float]]) -> str:
references = []
for i, (chunk, score) in enumerate(results, start=1):
references.append(
f"""[来源{i}]
标题:{chunk.title}
章节:{chunk.section}
更新时间:{chunk.updated_at}
链接:{chunk.source}
相关度:{score:.4f}
内容:{chunk.content}
"""
)
refs_text = "\n".join(references)
prompt = f"""
你是企业知识库问答助手。
请严格基于【参考资料】回答用户问题。
如果参考资料中没有答案,请回答“当前资料未说明”,不要根据常识编造。
回答时请尽量简洁。
涉及关键结论时,请标注来源编号,例如:[来源1]。
如果资料之间存在冲突,请说明冲突点。
【用户问题】
{query}
【参考资料】
{refs_text}
【回答】
"""
return prompt
def main():
docs = [
Document(
doc_id="refund_2024",
title="售后政策",
section="3.3 企业采购退款",
source="https://example.com/refund",
updated_at="2024-05-01",
content="""
企业采购订单如需退款,应先联系对应客户经理,由客户经理在系统中发起退款申请。
财务审核通过后,退款将在5到10个工作日内原路退回。
企业采购订单不适用普通消费者的7天无理由退款规则。
如果订单已经开具增值税专用发票,客户需先完成发票红冲流程。
"""
),
Document(
doc_id="refund_normal_2024",
title="售后政策",
section="3.1 普通商品退款",
source="https://example.com/refund-normal",
updated_at="2024-05-01",
content="""
普通商品支持付款后7天内申请无理由退款。
退款申请通过后,款项一般会在3到5个工作日内原路退回。
虚拟商品、定制商品、生鲜商品不适用7天无理由退款。
"""
),
Document(
doc_id="error_code",
title="系统错误码说明",
section="ERR_1024",
source="https://example.com/error-code",
updated_at="2024-04-20",
content="""
ERR_1024表示用户当前登录状态已失效。
常见原因包括登录凭证过期、账号在其他设备修改密码、管理员强制下线。
处理方式是重新登录。如果仍然失败,请清理浏览器缓存后再次尝试。
"""
)
]
engine = AISearchEngine()
engine.add_documents(docs)
engine.build_index()
query = "企业采购订单怎么退款?"
results = engine.search(query, top_k=3)
print("检索结果:")
for chunk, score in results:
print("-" * 60)
print("得分:", score)
print("标题:", chunk.title)
print("章节:", chunk.section)
print("内容:", chunk.content.strip())
print("\n生成给大模型的Prompt:")
print("=" * 60)
print(engine.build_prompt(query, results))
if __name__ == "__main__":
main()
3. 运行代码
python ai_search_demo.py
你会看到类似输出:
检索结果:
------------------------------------------------------------
得分:0.38
标题:售后政策
章节:3.3 企业采购退款
内容:企业采购订单如需退款,应先联系对应客户经理...
然后程序会输出一段可以直接发送给大模型的Prompt。
需要注意:这份代码是教学版,重点是展示流程。生产环境需要进一步替换和增强。
五、生产环境如何升级这份Demo?
1. 替换Embedding模型
教学版代码用TF-IDF模拟语义检索,实际项目应使用Embedding模型,例如:
- text-embedding-3-large;
- bge-large-zh;
- bge-m3;
- m3e;
- GTE;
- Jina Embeddings。
Embedding模型选择时需要关注:
- 中文效果;
- 长文本支持;
- 多语言支持;
- 成本;
- 推理速度;
- 是否支持本地部署。
2. 引入向量数据库
当文档数量增长后,不适合把所有向量放在内存中计算。可以使用:
| 工具 | 适用场景 |
|---|---|
| Milvus | 大规模向量检索 |
| Qdrant | 易用、过滤能力强 |
| Weaviate | 功能完整 |
| pgvector | 已使用PostgreSQL的团队 |
| Elasticsearch | 关键词检索与向量检索一体化 |
| OpenSearch | 开源搜索引擎生态 |
向量数据库中至少要保存:
{
"chunk_id": "refund_2024_0",
"embedding": [0.01, 0.02, 0.03],
"content": "企业采购订单如需退款...",
"metadata": {
"doc_id": "refund_2024",
"title": "售后政策",
"section": "3.3 企业采购退款",
"source": "https://example.com/refund",
"updated_at": "2024-05-01",
"permission": ["sales", "support"]
}
}
3. 增加真正的Rerank
Rerank模型用于判断“问题”和“候选片段”的匹配程度。它通常比向量召回更精细,但成本更高,因此适合用于Top N候选结果重排。
常见策略:
向量检索 Top 30
关键词检索 Top 30
合并去重
Rerank Top 60
取 Top 5 给大模型
Rerank可以使用:
- bge-reranker;
- Cohere Rerank;
- Jina Reranker;
- 自训练Cross Encoder模型。
4. 增加答案后处理
大模型生成答案后,不应直接原样返回。可以做一些后处理:
- 检查是否包含引用;
- 检查是否出现“资料中没有”的内容;
- 检查答案长度;
- 检查敏感词;
- 检查是否泄露系统提示词;
- 检查是否输出了无权限信息。
六、AI搜索上线前检查清单
上线前可以逐项确认:
- [ ] 是否支持混合检索?
- [ ] 是否有合理文档切片策略?
- [ ] 是否保留标题、章节、来源、更新时间?
- [ ] 是否支持权限过滤?
- [ ] 是否过滤废弃文档?
- [ ] 是否有Rerank或等价排序机制?
- [ ] Prompt是否限制模型不能编造?
- [ ] 答案是否展示引用来源?
- [ ] 是否有无答案兜底策略?
- [ ] 是否构建了评测集?
- [ ] 是否监控召回率、幻觉率、响应时间和成本?
- [ ] 是否支持增量更新索引?
- [ ] 是否支持日志追踪和问题回放?
- [ ] 是否有敏感信息保护机制?
七、总结
AI搜索的难点不在于“让大模型回答问题”,而在于让它稳定、准确、可追溯、可控地回答问题。
如果只是做一个Demo,你只需要:
文档切片 → 向量入库 → 检索Top K → 大模型回答
但如果要做一个真正可用的AI搜索系统,你至少需要关注:
- 混合检索;
- 合理切片;
- 文档结构;
- 结果重排;
- 权限控制;
- 引用来源;
- 版本管理;
- 成本控制;
- 评测体系;
- 幻觉抑制。
一句话概括:
AI搜索不是“搜索 + AI”的简单拼接,而是一套围绕知识、检索、生成、权限、评测和运营的完整工程体系。
只有把这些坑提前避开,AI搜索才能从“看起来很智能”,真正变成“用起来很可靠”。