上一篇 下一篇 分享链接 返回 返回顶部

别急着接大模型API:做AI搜索前必须避开的10个坑(附源码)

发布人:慈云数据-客服中心 发布时间:21小时前 阅读量:4

AI搜索 使用避坑指南|附源码

在大模型快速普及之后,“AI搜索”逐渐成为很多产品、团队和个人知识库的标配能力。它看起来很简单:把资料丢进去,用户提问,AI给答案。但真正做过之后你会发现,AI搜索并不是“接一个大模型API”那么简单。

很多AI搜索项目在Demo阶段效果惊艳,一上线却问题频出:答案张冠李戴、引用来源不准确、搜索不到明明存在的内容、成本暴涨、响应很慢、用户追问时上下文混乱,甚至出现一本正经胡说八道的情况。

本文将系统梳理AI搜索的核心原理、常见坑点、落地建议,并附上一份可运行的简化版源码示例,帮助你从“能跑”走向“可用”。


一、什么是AI搜索?

传统搜索通常基于关键词匹配,例如:

用户搜索“退款流程”,系统在文档中查找包含“退款”“流程”的内容。

而AI搜索通常会结合以下几类能力:

  1. 向量检索

    • 将文档和问题转换为向量;
    • 通过语义相似度找到相关内容;
    • 即使用户没有使用完全相同的关键词,也能搜到相关答案。
  2. 全文检索

    • 使用关键词、倒排索引、BM25等方式;
    • 对专有名词、编号、代码、精确匹配更友好。
  3. 大模型生成

    • 将检索到的资料交给大模型;
    • 由大模型组织语言,生成自然、完整的回答。
  4. 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搜索才能从“看起来很智能”,真正变成“用起来很可靠”。

目录结构
全文