结合搜索API、RAG与LLM构建智能搜索系统

前言

AI 搜索引擎(如 Perplexity、SearchGPT)代表了搜索的未来。它们不仅提供链接,还通过实时检索网页内容并结合 LLM 生成结构化的回答。本文将探讨如何构建一个具备实时搜索、内容提取和引用溯源能力的 AI 搜索引擎。


AI 搜索架构

AI 搜索引擎的核心是 “实时 RAG”。与传统 RAG 检索本地向量库不同,AI 搜索检索的是整个互联网。

┌─────────────────────────────────────────────────────────────────┐
│                      AI 搜索工作流                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 用户提问 → [Query 改写] → 优化后的搜索词                    │
│                                                                 │
│  2. [搜索 API] → 获取网页链接与摘要 (Tavily/Serper/Bing)        │
│                                                                 │
│  3. [网页抓取] → 提取核心正文内容 (Jina Reader/Firecrawl)       │
│                                                                 │
│  4. [内容过滤] → 筛选相关性最高的片段                           │
│                                                                 │
│  5. [LLM 生成] → 结合检索内容生成回答 + 引用标注                │
│                                                                 │
│  6. [结果输出] → 结构化回答 + 来源列表                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

搜索 API 集成

构建 AI 搜索的第一步是选择合适的搜索 API。

常用搜索 API 对比

API 特点 适用场景
Tavily 为 LLM 优化,自带内容提取和过滤 AI 搜索首选
Serper 价格低廉,Google 搜索结果镜像 高频基础搜索
Bing Search 覆盖面广,支持新闻/图片/视频 综合性搜索
SerpApi 支持多种搜索引擎 (Google, Baidu等) 跨平台需求

使用 Tavily 搜索

import os
from tavily import TavilyClient

class SearchEngine:
    """搜索引擎封装"""
    
    def __init__(self, api_key: str = None):
        self.client = TavilyClient(api_key=api_key or os.getenv("TAVILY_API_KEY"))
    
    def search(self, query: str, search_depth: str = "advanced", max_results: int = 5):
        """执行搜索"""
        # search_depth: "basic" 或 "advanced"
        response = self.client.search(
            query=query,
            search_depth=search_depth,
            max_results=max_results,
            include_answer=True, # Tavily 自带的简短回答
            include_raw_content=True # 包含抓取的网页原始内容
        )
        return response["results"]

# 使用示例
engine = SearchEngine()
results = engine.search("2024年诺贝尔物理学奖得主是谁?")

for res in results:
    print(f"Title: {res['title']}")
    print(f"URL: {res['url']}")
    print(f"Snippet: {res['content'][:100]}...")

网页内容提取

搜索 API 通常只返回摘要。为了获得深度回答,我们需要抓取网页全文并将其转换为 LLM 易于理解的 Markdown 格式。

推荐工具:Jina Reader & Firecrawl

  • Jina Reader:只需在 URL 前加上 https://r.jina.ai/,即可获得干净的 Markdown。
  • Firecrawl:专门为 LLM 设计的爬虫,能处理 JavaScript 渲染,并自动将整个网站转换为 Markdown。
import requests

def get_markdown_content(url: str):
    """使用 Jina Reader 提取网页内容"""
    reader_url = f"https://r.jina.ai/{url}"
    response = requests.get(reader_url)
    return response.text

核心技术:引用与溯源 (Citations)

AI 搜索最怕“一本正经地胡说八道”。引用标注是建立信任的关键。

实现逻辑

  1. 编号:为每个检索到的网页片段分配一个唯一的 ID(如 [1], [2])。
  2. Prompt 约束:要求 LLM 在回答时,必须在每个事实后面标注来源 ID。
  3. 后处理:将 ID 映射回原始 URL。
prompt = """
你是一个专业的搜索助手。请根据提供的参考资料回答用户问题。

参考资料:
{context}

要求:
1. 必须在回答中引用参考资料,格式为 [n]。
2. 如果资料中没有相关信息,请直说不知道。
3. 回答要客观、准确。

用户问题:{query}
"""

# context 示例:
# [1] 来源: https://example.com 内容: 2024年诺贝尔物理学奖授予了...
# [2] 来源: https://news.com 内容: 瑞典皇家科学院宣布...

进阶:多步搜索与查询分解

对于复杂问题(如“对比特斯拉和比亚迪在 2023 年的财报表现”),单次搜索往往不够。

流程

  1. 分解:LLM 将问题分解为两个子问题:“特斯拉 2023 财报”和“比亚迪 2023 财报”。
  2. 并行搜索:同时执行两次搜索。
  3. 综合分析:LLM 汇总两份搜索结果,进行对比分析。

幻觉校验 (Hallucination Check)

在输出前,可以增加一个校验步骤:

  • 逻辑:让 LLM 检查生成的回答中的每一个断言,是否都能在检索到的原文中找到支撑。如果找不到,则删除或修改该断言。

总结

构建 AI 搜索引擎不仅仅是调用一个搜索 API。

  • 广度:取决于搜索 API 的覆盖面。
  • 深度:取决于网页内容提取的质量。
  • 可信度:取决于引用溯源的严谨性。

通过结合 TavilyJina Reader结构化 Prompt,你可以构建出一个媲美 Perplexity 的专业级 AI 搜索应用。

使用 Jina Reader API

Jina Reader 可以将任何网页转换为适合 LLM 阅读的 Markdown 格式。

import requests

def extract_content(url: str) -> str:
    """使用 Jina Reader 提取网页内容"""
    reader_url = f"https://r.jina.ai/{url}"
    response = requests.get(reader_url)
    if response.status_code == 200:
        return response.text
    return ""

# 示例
content = extract_content("https://en.wikipedia.org/wiki/Large_language_model")
print(content[:500])

Query 改写与分解

用户的问题往往不适合直接搜索。我们需要 LLM 将其转化为多个高效的搜索关键词。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser

class QueryOptimizer:
    """查询优化器"""
    
    def __init__(self):
        self.llm = ChatOpenAI(model="gpt-4o-mini")
        self.parser = JsonOutputParser()
    
    def optimize(self, query: str) -> list[str]:
        """将用户问题分解为多个搜索词"""
        prompt = ChatPromptTemplate.from_template("""
        你是一个搜索专家。请将用户的复杂问题分解为 2-3 个独立的、适合搜索引擎的关键词或短语。
        
        用户问题:{query}
        
        请以 JSON 数组格式输出,例如:["关键词1", "关键词2"]
        """)
        
        chain = prompt | self.llm | self.parser
        return chain.invoke({"query": query})

# 示例
optimizer = QueryOptimizer()
search_queries = optimizer.optimize("对比 Llama 3.1 和 GPT-4o 在代码生成方面的差异")
# 输出: ["Llama 3.1 vs GPT-4o code generation benchmarks", "Llama 3.1 coding capabilities", "GPT-4o programming performance"]

引用与溯源实现

AI 搜索最关键的特性是引用标注。我们需要在生成的文本中插入 [1], [2] 等标记,并对应到具体的来源。

提示词工程

def generate_answer_with_citations(query: str, contexts: list[dict]):
    """生成带引用的回答"""
    
    # 构建上下文字符串
    context_str = ""
    for i, res in enumerate(contexts):
        context_str += f"Source [{i+1}]:\nTitle: {res['title']}\nURL: {res['url']}\nContent: {res['content']}\n\n"
    
    prompt = f"""
    你是一个专业的 AI 搜索助手。请根据提供的参考资料回答用户的问题。
    
    要求:
    1. 必须在回答中通过 [n] 的形式标注引用来源,其中 n 是参考资料的编号。
    2. 如果资料中没有相关信息,请诚实说明。
    3. 回答要客观、准确、结构清晰。
    4. 在回答末尾列出所有参考资料的链接。
    
    参考资料:
    {context_str}
    
    用户问题:{query}
    
    回答:
    """
    
    llm = ChatOpenAI(model="gpt-4o")
    return llm.invoke(prompt).content

在复杂的搜索任务中,单次搜索往往不够。我们需要一个能够“思考”并决定是否需要补充搜索的 Agent。

from typing import Annotated, List, TypedDict
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from tavily import TavilyClient

# 1. 定义状态
class AgentState(TypedDict):
    query: str
    search_queries: List[str]
    search_results: List[dict]
    answer: str
    steps: int

# 2. 定义节点逻辑
class SearchAgent:
    def __init__(self):
        self.llm = ChatOpenAI(model="gpt-4o")
        self.tavily = TavilyClient()

    async def plan_search(self, state: AgentState):
        """根据问题生成搜索计划"""
        prompt = f"针对问题 '{state['query']}',生成 3 个互补的搜索关键词。"
        res = await self.llm.ainvoke(prompt)
        return {"search_queries": res.content.split("\n"), "steps": state.get("steps", 0) + 1}

    async def execute_search(self, state: AgentState):
        """执行并行搜索"""
        results = []
        for q in state["search_queries"]:
            res = self.tavily.search(q, max_results=3)
            results.extend(res["results"])
        return {"search_results": results}

    async def synthesize(self, state: AgentState):
        """综合信息生成回答"""
        context = "\n".join([r["content"] for r in state["search_results"]])
        prompt = f"基于以下内容回答 '{state['query']}'\n\n{context}"
        res = await self.llm.ainvoke(prompt)
        return {"answer": res.content}

# 3. 构建图
workflow = StateGraph(AgentState)
agent = SearchAgent()

workflow.add_node("plan", agent.plan_search)
workflow.add_node("search", agent.execute_search)
workflow.add_node("synthesize", agent.synthesize)

workflow.set_entry_point("plan")
workflow.add_edge("plan", "search")
workflow.add_edge("search", "synthesize")
workflow.add_edge("synthesize", END)

app = workflow.compile()

核心优化:HyDE (假设性文档嵌入)

HyDE 的核心思想是:让 LLM 先针对问题生成一个“假设性”的答案,然后用这个答案去搜索。因为答案与答案之间的语义相似度通常高于问题与答案之间的相似度。

def hyde_search(query: str):
    # 1. 生成假设性回答
    hypothetical_answer = llm.invoke(f"请针对问题 '{query}' 写一个简短的假设性回答。")
    
    # 2. 使用假设性回答进行向量检索或网页搜索
    # 这样可以显著提升检索的召回率 (Recall)
    results = tavily.search(hypothetical_answer.content)
    return results

引用溯源的严谨性:Fact-Checking

为了防止 LLM 幻觉,可以在生成后增加一个校验步骤:

def verify_citations(answer: str, sources: List[dict]):
    """
    校验生成的回答中的每一条引用是否在来源中真实存在。
    """
    prompt = f"请检查以下回答中的引用标注是否准确。如果来源中没有提到相关事实,请指出。\n\n回答:{answer}\n\n来源:{sources}"
    verification = llm.invoke(prompt)
    return verification.content

进阶优化技术

1. 重新排序 (Re-ranking)

搜索返回的结果可能包含噪声。使用 Cohere RerankBGE-Reranker 对抓取的内容进行二次筛选,只保留与问题最相关的片段。

2. 异步抓取

使用 playwrighthttpx 异步抓取多个网页,提高响应速度。

3. 意图识别

在搜索前判断用户意图。如果是“天气”、“股价”等实时信息,调用专门的 API;如果是“如何做某事”,则进行深度网页搜索。

4. 结构化输出

使用 Pydantic 强制 LLM 输出结构化的引用数据,方便前端渲染点击跳转。

from pydantic import BaseModel, Field

class Citation(BaseModel):
    source_id: int
    quote: str = Field(description="引用的原文片段")

class SearchResponse(BaseModel):
    answer: str = Field(description="生成的回答内容")
    citations: List[Citation]
    references: List[str] = Field(description="参考链接列表")

总结

构建 AI 搜索引擎不仅仅是调用一个搜索接口,它涉及到查询理解、多源检索、内容提取、重排序以及严谨的溯源机制。通过合理组合 Tavily、Jina Reader 和高性能 LLM,我们可以构建出体验媲美顶级产品的智能搜索应用。


参考资源

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 LLM应用开发——AI搜索引擎实战 》

本文链接:http://localhost:3015/ai/AI%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E.html

本文最后一次更新为 天前,文章中的某些内容可能已过时!