做RAG的同学应该都懂,LLM一本正经胡说八道的样子有多让人血压飙升。上周我把实时搜索接进RAG链路里,幻觉率直接掉了四成。这篇文章不是讲原理的,纯纯的踩坑实录。

一、先还原一下现场

我做的项目是一个技术问答助手,接的是某大厂的通用模型,知识库用的是我们自己攒的技术文档,大概几千篇博客和官方文档的切片。

一开始效果还行,毕竟都是内部文档,答案挺准。但最近用户开始问一些新技术的问题,比如"Next.js 15 的 Server Actions 有什么变化"、“Django 5.0 的生成字段怎么用”。

模型怎么回的?它把 Next.js 14 的内容套到 15 上,Server Actions 的 API 签名都变了,它还按老版本教。用户照着做,代码直接报错。

原因很直接:知识库是静态的,模型的知识截止到训练数据,新东西它没见过

传统的 RAG 思路是定期更新知识库,但我们这技术文档更新速度根本追不上社区。我算了下,要做到 T+1 更新,至少得配一个专门做数据同步的人,成本太高。

二、实时搜索:给模型装个"外接大脑"

跟同事讨论的时候,他说了个思路:与其让模型背所有知识,不如让它学会"查资料"

具体做法:用户提问后,先不急着调模型,而是把问题丢给搜索引擎,拿前几条结果作为上下文,再让模型基于这些实时信息生成答案。

这就是给 RAG 加一层实时检索(Real-time Retrieval)。

2.1 为什么不用 Bing/Google 官方 API?

我一开始看的是 Google Custom Search JSON API,结果一看定价:$5 per 1000 queries。我日活虽然不高,但一个用户会话可能触发 3-5 次搜索,算下来一个月几百刀起步。

Bing Web Search API 稍微便宜点,但也要按月订阅,最低档几十刀。

对我这种独立项目来说,按月订阅就是原罪。流量波动大的时候,要么浪费额度,要么不够用。

后来试了下 SerpBase,按量付费,$3 起步,用多少扣多少。我先拿 100 次免费额度做了个 POC。

2.2 链路设计

整个流程大概这样:

用户提问 
  -> 意图识别(判断是否需要搜索)
  -> 生成搜索 query(有时需要改写)
  -> SerpBase API 搜索
  -> 清洗/截断搜索结果
  -> 拼接 prompt(系统指令 + 搜索结果 + 用户问题)
  -> LLM 生成答案
  -> 返回答案 + 引用来源

最关键的两步:意图识别query 改写

不是所有问题都需要搜索。比如"你好"、"你是谁"这种,直接模型回就行。我加了个简单的规则:问题中包含版本号、时间、“最新”、"现在"等关键词时,触发搜索。

Query 改写也很重要。用户问的是"Next.js 15 Server Actions 有啥变化",直接搜这个没问题。但如果用户问的是"我刚才说的那个功能在 Next.js 新版本里还能用吗",这种有上下文的,需要把历史对话考虑进去生成搜索词。

三、核心代码:搜索+上下文拼接

用的 Python + LangChain,但为了减少依赖,核心逻辑我自己写的,没全用 LangChain 的封装。

3.1 搜索模块

import requests
from typing import List, Dict

class RealtimeSearch:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.serpbase.com/v1/search"
    
    def search(self, query: str, num_results: int = 5) -> List[Dict]:
        params = {
            "q": query,
            "api_key": self.api_key,
            "num": num_results,
            "hl": "zh-CN",
            "gl": "cn"
        }
        resp = requests.get(self.base_url, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        
        organic = data.get("organic_results", [])
        return [
            {
                "title": item.get("title", ""),
                "link": item.get("link", ""),
                "snippet": item.get("snippet", "")
            }
            for item in organic[:num_results]
        ]

3.2 上下文拼接

拿到搜索结果后,不能直接全塞进 prompt。Google 结果的 snippet 通常几十到一百多字,5 条加起来可能就 500 token 左右,控制住别超模型窗口。

def build_prompt(question: str, search_results: List[Dict]) -> str:
    context_parts = []
    for i, res in enumerate(search_results, 1):
        context_parts.append(
            f"[{i}] {res['title']}\n{res['snippet']}\n来源: {res['link']}"
        )
    
    context = "\n\n".join(context_parts)
    
    prompt = f"""你是一个技术助手。请基于以下参考信息回答用户问题。
如果参考信息不足以回答问题,请明确说明。
回答时请标注信息来源编号(如[1]、[2])。

参考信息:
{context}

用户问题:{question}

请用中文回答:"""
    return prompt

3.3 完整调用链路

class RAGWithSearch:
    def __init__(self, serp_key: str, llm_client):
        self.search = RealtimeSearch(serp_key)
        self.llm = llm_client  # 你的大模型客户端
    
    def needs_search(self, question: str) -> bool:
        triggers = ["最新", "现在", "版本", "2024", "2025", "2026", 
                    "怎么解决", "报错", "error", "bug"]
        return any(t in question.lower() for t in triggers)
    
    def answer(self, question: str) -> Dict:
        if not self.needs_search(question):
            # 直接走向量检索
            return self._vector_rag(question)
        
        # 实时搜索增强
        results = self.search.search(question, num_results=5)
        prompt = build_prompt(question, results)
        
        response = self.llm.chat(prompt)
        return {
            "answer": response,
            "sources": results,
            "used_search": True
        }
    
    def _vector_rag(self, question: str) -> Dict:
        # 你的原有向量检索逻辑
        ...

四、踩过的几个大坑

坑1:搜索结果质量不稳定

不是每次搜索都能拿到好结果。有些 query 搜出来的前几条是广告落地页、聚合站、或者 SEO 垃圾站,内容质量差。

我的解决方案是加个简单的内容过滤层

BLOCKED_DOMAINS = ["some-spam-site.com", "low-quality-aggregator.cn"]

def filter_results(results: List[Dict]) -> List[Dict]:
    filtered = []
    for r in results:
        domain = r["link"].split("/")[2].replace("www.", "")
        if domain not in BLOCKED_DOMAINS and len(r["snippet"]) > 30:
            filtered.append(r)
    return filtered[:5]

还可以加一层:snippet 里如果包含大量重复关键词(SEO 填充特征),权重降低。

坑2:搜索 query 太短,结果太泛

用户问"Django 怎么样",直接搜这个,返回的结果太宽泛,对回答没帮助。

解决方案是在 query 生成阶段做意图扩展。简单做法是把问题翻译成英文再搜(技术内容英文结果通常更好),或者加一些限定词。

比如"Django 怎么样"扩展成"Django framework pros cons 2025",结果质量明显上升。

坑3:延迟问题

实时搜索意味着同步请求搜索引擎,这会增加整体响应时间。我测了一下:

  • 纯向量检索:平均 800ms
  • 向量检索 + SerpBase 搜索:平均 2.2s
  • 如果搜索触发重试:可能到 3s+

对用户体验有影响。我的优化策略:

  1. 异步预搜索:用户输入时,根据已输入内容提前发搜索请求,缓存结果
  2. 流式返回:先返回向量检索结果,同时后台去搜,搜到了再增量更新答案(需要前端配合)
  3. 控制超时:搜索设置 3 秒超时,超时就只用向量库的结果兜底

坑4:成本比想象中低,但要注意配额

我算了下,一个典型会话触发 2 次搜索,每次搜 5 条结果。

SerpBase 普通搜索是 1 credit/次。我日活大概 200 个会话,一天 400 次搜索,一个月 1.2 万次。

用 $10 的 Starter 包(2 万次,$0.50/千次),一个月刚好。如果日活翻十倍,上 $50 的 Growth 包(12.5 万次,$0.40/千次),也才 50 刀。

相比 Bing API 的月订阅,这个成本结构对项目早期友好太多了。

五、效果对比

我做了个 A/B 测试,两周数据:

指标 纯向量 RAG RAG + 实时搜索
幻觉率(人工抽检100条) 28% 16%
用户满意度(1-5分) 3.6 4.2
平均响应时间 0.8s 2.1s
月搜索成本 $0 ~$6

幻觉率降了 43%,这个提升我个人非常满意。响应时间虽然变长了,但我们在前端加了 loading 文案和来源引用展示,用户感知反而觉得"更靠谱了"。

六、延伸思考:实时搜索 + RAG 的边界

这种模式也不是万能的,说几个不适合的场景:

  1. 高并发聊天:如果每秒几百条消息,实时搜索的 QPS 和延迟会成为瓶颈
  2. 企业内部知识:如果问题完全依赖内部文档(如公司规章制度),外部搜索只会引入噪音
  3. 需要深度推理的问题:搜索给的是碎片化信息,复杂推理还是得靠模型本身

我的建议是:把实时搜索当作 RAG 的一个"插件",按需启用,而不是完全替代向量检索

七、总结

如果你也在做 LLM 应用,被幻觉问题困扰,我真心建议试试加一层实时搜索。技术实现不复杂,核心就三步:判断要不要搜 -> 去搜 -> 把结果塞进 prompt。

SerpBase 这种按量付费的 API,对独立开发者和中小团队很友好,100 次免费额度足够你做个完整 POC。反正试试成本几乎为零,但幻觉率下降的收益是实实在在的。

对了,如果你搜出来的结果质量不稳定,记得加上我上面说的内容过滤和 query 改写,这两个小改动对最终效果影响巨大。


下一步我打算试试把搜索结果也做向量化存进向量库,搞个"动态知识库",有新搜索结果就增量更新。如果跑通了再写篇续集。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐