Perplexity Search 这类 AI 搜索产品登上各类 AI 产品关注榜,背后不只是产品包装做得好,更多是整个搜索交互范式发生了变化。用户输入问题后,系统先通过搜索引擎拿到一批候选网页,再让大模型基于这些网页摘要生成一段带引用编号的答案。和传统搜索返回蓝色链接不同,AI 搜索直接给出结论、证据来源和继续追问的入口,使用者不再需要一个个点开链接自己拼信息。
这篇文章适合产品经理、后端开发者和正在做大模型应用的人。产品侧关心它为什么好用、引用怎么做、如何评估;开发侧更关心 RAG 管线、搜索 API、LLM 调用、参数取舍和排错。下面先从产品逻辑入手,再落到可运行的工程实现,最后给出衡量标准、常见坑和生产化建议。示例用简化的 Python 服务演示,实际落地时可根据团队技术栈替换。
1. Perplexity Search 走红背后的技术逻辑:AI 搜索到底解决了什么
1.1 传统搜索的痛点:索引到了信息,但把筛选工作留给了用户
传统搜索引擎本质上是一个索引系统。它先抓取海量网页,建立倒排索引,再根据用户 query 返回相关链接。这套机制对“我知道要找一个网站”“我想了解某个词的标准定义”这类任务效率很高;但对“基于多个来源做综合判断”的任务,用户要连续搜索多次、打开多个页面、自己对比信息,整个过程非常耗时。
更麻烦的是长尾问题。比如要比较两家 AI 翻译产品的价格、隐私策略、支持语言,传统搜索会返回几十个页面,用户要把价格区间和功能差异手动整理出来。另一个问题是多轮上下文,你在传统搜索里搜了第一轮,第二轮换关键词,前面语境基本丢失,只能靠用户自己继续拼。
AI 搜索要解决的问题,就是把“信息检索”和“信息综合”合并成一步,同时用引用让结果可回查。Perplexity Search 真正切中的不是“聊天框里多了搜索按钮”,而是把传统搜索引擎留给用户的整理工作接管过来,让用户从“逐页浏览判断”变成“先看答案,再通过来源验证”。
1.2 AI 搜索的新交互:答案、引用与追问
Perplexity 类产品的页面形态通常包含四个部分:顶部是答案区块,答案中穿插引用编号;底部或右侧是来源列表,包含标题、域名和链接;再往下是推荐追问,引导用户继续深入;同时用户可以在同一个对话里不断追问,系统会记住上下文。
这种形态的关键词不是“生成”,而是“可验证”。大模型回答问题非常流畅,但流畅不等于正确。引用列表给了用户一条验证路径:如果回答中引用了某个网站,用户可以直接点击源去核对。这也倒逼系统在构造 prompt 时,只把检索到的真实内容交给模型,而不是让模型凭记忆发挥。
这种交互从“用户自己浏览链接做判断”变成“系统先综合、用户再验证”,正是 AI 搜索让人感觉更直接的原因。它也没有丢掉传统搜索的长处,网页标题、域名、摘要仍然保留,只是从主菜变成了验证依据。
1.3 为什么关注度会上升:检索和生成终于可以被工程化组合
热度上升不是单点原因。从技术侧看,最近几代大模型在指令跟随和长上下文上进步明显,模型能够按照“只使用检索资料回答并标注引用”这类约束生成内容;检索成本也降下来了,普通开发者可以拿到搜索 API 或开源检索组件。
从产品侧看,AI 搜索把“聊天”和“搜索”两个过去分开的入口合并。用户既可以得到对话式回答,又保留了信息来源,比纯聊天窗口更适合做知识获取入口。再加上 API 可以嵌入编程助手、客服系统、办公软件,形成新的集成场景,开发者关注度自然上升。
需要说明的是,各种 AI 产品指数榜的统计口径并不统一,有的偏网站访问、有的偏开发者讨论、有的偏应用下载。某一时段排名冲到前面,只能说明市场关注度在快速提高,不能完全等同于市场份额。研究这类产品时,真正值得关注的是背后的交互模型和技术链路有没有解决实际问题。
2. 理解 AI 搜索的底层链路:检索、增强、生成、溯源
2.1 查询理解与多轮改写
AI 搜索和传统搜索一样,第一步不是直接检索,而是先理解 query。用户真正想问的,往往和输入的文字不完全一样。比如用户输入“它和谷歌搜索比有什么优势”,搜索系统需要知道“它”指代什么;用户输入“最近 RAG 有哪些新进展”,“最近”要结合当前时间解释成日期范围。
在工程上,常见做法是先用一个小模型或一段精简 prompt 对 query 做改写和意图识别。改写结果会传给搜索引擎,而不是直接拿来生成答案。这个步骤虽然很小,但直接影响检索质量。
QUERY_REWRITE_PROMPT = """你是搜索查询改写助手。请把用户问题改写成适合搜索引擎的关键词查询。 要求:保留核心实体,补充省略的指代,不添加编造条件。 只输出改写后的查询文本。 用户问题:{question} 改写结果:"""这里要注意,改写不是无中生有。比如用户问“它收费吗”,如果上下文里没有明确过对象,系统应该直接返回原始问题,而不是猜一个实体进去。多轮改写只有在对话历史明确时才有意义。
2.2 实时检索:从模型记忆到网络证据
查询改写完成后,进入检索层。检索层一般调用搜索 API,拿到标题、URL、摘要和发布时间。选择搜索服务时,至少关心四件事:结果相关性、内容更新速度、请求限制和返回字段是否包含摘要。只给链接不给摘要的接口,会增加后续构建 prompt 的成本。
AI 搜索里的“实时”有两层含义:一是索引要包含足够新的网页;二是查询时要显式传递时间上下文,让搜索 API 优先返回近期内容。如果模型本身的训练数据有截止日期,实时检索就是弥补时效性的关键。
检索结果通常要限制数量。常见做法是取前 5 到 10 条,再按相关度和域名质量重排。返回条数太多,生成阶段容易超长,也容易引入噪音。
2.3 RAG:把外部证据注入生成流程
RAG 是这套链路的标准名词,全称 Retrieval-Augmented Generation。它把外部知识注入 prompt,让模型在生成时“有据可依”。在 AI 搜索场景中,外部知识来自搜索 API;在私有知识库场景中,外部知识来自向量检索或关键词检索。两类场景的流程非常相似:召回候选内容、整理成上下文、交给模型生成、再后处理校验。
很多刚接触大模型的人以为只需要把问题丢给模型,让模型用训练时学过的知识回答。这在简单常识问题上可以,但一旦涉及新闻事件、产品文档、内部资料,模型就会幻觉。RAG 的价值不是让模型“更聪明”,而是让模型“有当下可查的依据”。
完整的 AI 搜索链路可以概括成:query 改写、内容召回、结果重排、上下文构造、LLM 生成、引用校验、结果返回。其中任何一环出问题,最终答案都会偏离事实。
2.4 引用溯源:把信任做进界面和数据结构
引用是 AI 搜索和纯聊天机器人最明显的区别。没有引用,模型给出再流畅的回答,用户也无法判断内容真假;有引用,用户至少可以把答案拆回来源,逐一验证。从数据结构上看,引用要贯穿到 result 对象:每条引用必须对应一个真实 URL,而不是让模型嘴上说“根据某网站”。
实现层面有两种做法。第一种是让模型在回答文本里输出[1][2]编号,服务端再把编号映射成 link;第二种是让模型输出结构化 JSON,包含 answer 和 citations 数组。第二种更稳。无论哪种,生成后都要做校验,防止模型编造编号或漏标来源。
注意:引用编号的价值是让链接指向真实来源,不是让回答看起来专业。一个不能点击、不能验证的来源列表没有信任价值。
3. 动手实现一个 Perplexity 风格的 AI 搜索最小应用
3.1 学习环境的技术选型与依赖
这里做一个最小应用,学习阶段关键在于把链路跑通,不追求高并发。选择 Python 3.10 + FastAPI + requests + openai 就够。如果你所在团队用 Java,也可以把后面的实现换成 Spring AI 或 WebClient,逻辑完全一致。
pip install fastapi "uvicorn[standard]" requests openai pydantic需要准备两个外部服务:一个 LLM 接口,一个搜索接口。学习环境用任何 OpenAI 兼容的服务都可以,搜索服务也可以用测试额度。不要在生产还依赖学习期的临时 key。
3.2 项目结构:按职责拆层,方便后续排查
ai-search-demo/ ├── app.py # FastAPI 入口 ├── search.py # 检索层 ├── prompt.py # 提示词构造 ├── llm.py # 模型调用 ├── config.py # 环境变量读取 └── requirements.txt把层拆开是为了排查方便。回答问题后如果发现答案质量差,先看是检索层没召回好内容,还是模型把内容用错了。如果所有逻辑写在一个文件里,定位问题只能从头读到尾。
3.3 检索层代码:先拿回候选网页和摘要
search.py 的核心是统一搜索 API 的返回结构。不同搜索服务返回字段不一样,统一成 title、url、snippet 三个字段后,prompt 层只依赖这套结构。
# search.py import os import requests from typing import List, Dict SEARCH_ENDPOINT = os.getenv("SEARCH_ENDPOINT", "") SEARCH_API_KEY = os.getenv("SEARCH_API_KEY", "") def search_web(query: str, top_k: int = 5) -> List[Dict[str, str]]: if not SEARCH_ENDPOINT: raise RuntimeError("SEARCH_ENDPOINT 未配置,请先设置搜索服务地址") params = { "q": query, "count": top_k, } headers = { "Authorization": f"Bearer {SEARCH_API_KEY}", } resp = requests.get(SEARCH_ENDPOINT, params=params, headers=headers, timeout=5) resp.raise_for_status() items = resp.json().get("items", []) results = [] for item in items[:top_k]: results.append({ "title": item.get("title", ""), "url": item.get("link") or item.get("url", ""), "snippet": item.get("snippet") or item.get("description", ""), }) return results这段代码把搜索 API 的返回结果统一成三个字段。搜索服务不同,字段名会有差异,落地前要读官方文档确认。注意加了 timeout,避免检索服务慢拖垮整个接口。
3.4 生成层代码:把检索结果带入模型并保留引用
prompt.py 负责构造上下文。关键约束有三条:只使用资料、回答中标注编号、资料不足时明确说明。
# prompt.py from typing import List, Dict def build_prompt(question: str, results: List[Dict[str, str]]) -> str: context_lines = [] for i, item in enumerate(results, start=1): context_lines.append( f"[{i}] 标题:{item['title']}\n" f" 来源:{item['url']}\n" f" 摘要:{item['snippet']}" ) context = "\n\n".join(context_lines) return f"""请基于以下检索资料回答用户问题。 规则: 1. 只使用资料中出现的信息,不要凭模型记忆补充。 2. 回答中需要引用资料时,在句尾使用[N]标注。 3. 如果资料不足以回答,明确说明“根据现有资料无法回答”。 4. 回答控制在 300 字以内。 检索资料: {context} 用户问题: {question} 回答: """llm.py 负责调用模型。任何兼容 OpenAI Chat Completions 接口的服务都可以接进来。
# llm.py import os from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY", ""), base_url=os.getenv("LLM_BASE_URL", None), ) def generate_answer(prompt: str, temperature: float = 0.2) -> str: resp = client.chat.completions.create( model=os.getenv("LLM_MODEL", "gpt-4o-mini"), messages=[{"role": "user", "content": prompt}], temperature=temperature, ) return resp.choices[0].message.content注意 temperature 不建议调太高,AI 搜索需要稳定性和可验证性,太高的随机性会让同样的问题每次返回不同结论。
最后用 FastAPI 把链路串起来。
# app.py from fastapi import FastAPI from pydantic import BaseModel from search import search_web from prompt import build_prompt from llm import generate_answer app = FastAPI() class Query(BaseModel): q: str top_k: int = 5 @app.post("/api/search") def ai_search(query: Query): results = search_web(query.q, query.top_k) prompt = build_prompt(query.q, results) answer = generate_answer(prompt) return { "answer": answer, "sources": results, }回答文本里的[1]可以在前端通过 sources 数组映射成可点击链接。这个最小闭环已经具备 AI 搜索的核心形态。
3.5 参数说明与默认值选择
| 参数 | 含义 | 常见默认值 | 调大影响 | 调小影响 | 推荐场景 |
|---|---|---|---|---|---|
| top_k | 传给模型的结果条数 | 5 | 信息更全但噪音和成本上升 | 上下文更短但可能漏关键信息 | 通用问题取 5,垂直小语料取 3 |
| snippet_max_chars | 每条摘要最大长度 | 300 | 上下文更长、更贵 | 信息不足 | 长文档场景适当加大 |
| temperature | 采样温度 | 0.2 | 更发散但可能偏离资料 | 更稳定但偏保守 | AI 搜索用 0.1 到 0.3 |
| max_tokens | 生成最大 token 数 | 500 | 回答更长 | 可能被截断 | 看答案预期长度调整 |
| search_count | 搜索返回原始条数 | 10 | 覆盖更多但重排耗时 | 召回不完整 | 先取 10 再过滤到 5 |
调参不是一劳永逸。同一个参数在不同 query 下表现不同,建议准备 20 到 50 条测试 query,每次只改一个参数对比效果。
4. 运行验证:如何判断搜索结果不是编出来的
4.1 准备测试用例与预期行为
只测“能返回结果”不够。建议准备四类用例:时效性问题、知识截止问题、多跳比较问题、明显不可答问题。
- 时效性问题:例如“某大会最近发布了什么新功能”,预期是答案中包含近期信息。
- 知识截止问题:例如模型训练后发生的事件,预期是模型能借助检索回答,而不是复述旧知识。
- 多跳比较问题:例如“A 产品定价和 B 产品定价谁更适合小团队”,预期是模型综合多个来源比较。
- 明显不可答问题:例如“某内部系统接口文档在哪”,预期是模型明确说资料不足,而不是编一个 URL。
启动服务并请求:
uvicorn app:app --reload --port 8000curl -X POST http://127.0.0.1:8000/api/search \ -H "Content-Type: application/json" \ -d '{"q": "RAG 检索增强生成和传统搜索的区别", "top_k": 5}'这里演示的是通过命令验证。真实环境还要准备自动化测试集,把常见 query 的预期行为固化下来。
4.2 验证引用真实性和答案一致性
回答返回后检查三件事。第一,来源是否真实存在,逐个打开引用 URL。第二,答案是否真的来自摘要,把模型输出里的关键句与检索摘要比对。第三,编号是否一致,答案里写了[2],但第二条来源不是它说的内容,说明引用映射有问题。
正常输出示例:
{ "answer": "RAG 的核心是把检索到的外部资料注入 prompt,让模型基于资料生成回答[1][2]。", "sources": [ { "title": "Retrieval-Augmented Generation 介绍", "url": "https://example.com/rag-intro", "snippet": "RAG 通过检索外部知识来增强大模型生成的准确性。" }, { "title": "RAG 与传统搜索的差异", "url": "https://example.com/rag-vs-search", "snippet": "传统搜索返回链接,RAG 直接生成基于证据的回答。" } ] }注意:不要只检查接口返回 HTTP 200。AI 搜索应用即使返回成功,也可能包含伪造引用和幻觉内容,验证必须覆盖输出本身。
如果检索服务没有返回摘要,模型仍然生成了很具体的数字或结论,就要怀疑模型是不是用了自己的记忆。
4.3 学习环境与生产环境的差异
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 检索服务 | 测试 key、单文档 | 稳定 API、限流、重试、降级 |
| 模型服务 | OpenAI 兼容接口即可 | 并发、超时、鉴权、敏感信息过滤 |
| 缓存 | 不缓存 |