AI搜索任意算力下最佳:系统工程与优化实践

📅 发布时间:2026/8/31 18:38:27
AI搜索任意算力下最佳:系统工程与优化实践
Perplexity 搜索在任意算力水平下均为最佳这是一句在技术社区里引发过讨论的判断。不管它最初来自 CEO 的公开表态还是产品团队的方法论这句话落到工程层面其实在说另一件事搜索系统不应该因为部署环境的算力弱就大幅退化。现实中的 AI 搜索并不是“模型越大越强”这么简单它由查询理解、检索召回、重排序、摘要生成、引用溯源等多个环节组成每个环节都在消耗算力。这篇文章要讨论的是这句结论背后的工程链路AI 搜索的核心模块有哪些算力瓶颈通常出现在哪里以及当团队只有普通 CPU 或一块入门级 GPU 时怎样通过混合检索、小模型推理、缓存和上下文裁剪让搜索结果依然可用。适合正在做智能问答、垂直搜索、RAG 应用的后端和算法工程师也适合刚接触 Agent 的开发者。接下来会从概念拆解一直走到最小原型、调优和排查。1. 为什么“任意算力下最佳”实际上考验的是系统工程1.1 AI 搜索和传统搜索的关键差异传统搜索引擎处理的是“关键词到文档列表”的匹配问题。用户输入“Python 列表去重”搜索引擎里的倒排索引会快速找出包含这几个词的大量网页然后按链接权重、点击率等信号排列。这个过程天然对算力不太敏感因为核心数据结构是已经建好的索引查询阶段只是几十毫秒内的查表和打分。AI 搜索多了一步“生成答案”。系统不仅要找到候选文档还要在大模型里把这些文档压缩成一段通顺的摘要并附上引用。这一步让用户体验发生了质变用户不需要再打开多个网页自己拼信息AI 搜索直接给出答案。代价是每个问题都会产生一次或多次模型推理而模型推理的耗时会随问题长度、上下文长度和模型参数量同步上升。所以把“Perplexity 搜索在任意算力水平下均为最佳”当作技术命题来看它真正的意思是在算力弱的设备上搜索系统仍然要保证答案有较高的相关性而不是简单降级成“只检索不总结”或“答案乱七八糟”。要做到这一点不能只靠增大模型必须把检索质量、上下文组织、模型推理效率一起设计。1.2 Perplexity 搜索链路由哪些环节组成一个完整 AI 搜索链路通常包含五个环节。第一个环节是查询理解。用户输入的问题可能包含拼写错误、口语表达或指代不清系统需要做纠错、改写、补充实体信息甚至把“它的原理是什么”这种指代问题还原成完整问题。第二个环节是检索召回。系统在文档库、网页库或 API 数据源中找出可能相关的候选内容常见方式有 BM25、向量相似度、知识图谱查询以及这些方式的组合。第三个环节是重排序。召回的候选数量通常有几十条但大模型上下文有限系统需要把最可能满足问题的内容排到前面只保留前几条。第四个环节是摘要生成。大模型根据候选内容组织答案这一步消耗的算力最大也是最容易引起延迟的部分。第五个环节是引用溯源。系统把答案中的观点映射回原文段落方便用户查证。这五个环节不是必须串联执行。缓存命中时查询理解和检索可以直接跳过用户不要求生成摘要时第五个环节也可以省略。但整体上第一次查询一定需要打通完整链路。任何一个环节做得差最终搜索结果都会显得“不聪明”。1.3 算力约束如何影响每个环节在纯粹 CPU 环境下检索和重排序的算力开销还可以接受。BM25 是统计打分向量检索只要嵌入模型不是特别大也能运行瓶颈主要在生成阶段。一个小型 1.5B 参数模型在 CPU 上生成一个 200 字答案可能需要几十秒用户很难接受。在入门级 GPU 环境下生成阶段可以跑起来但并发能力有限。当搜索请求同时进来模型推理队列会加长首 token 时间会变大。很多团队会在这里发现问题单个请求的速度尚可一旦并发上来整个服务像被拖垮。在数据中心环境下GPU 资源充足瓶颈会转移到检索链路的复杂度和上下文管理。如果每个请求都把所有候选都塞进 prompttoken 数量会成倍增加既浪费算力又会让模型抓不住重点。所以“最佳”不是放在某一种硬件上单独看而是在给定算力预算下让各环节的分工更合理。注意算力约束不是“模型能不能跑”而是“在用户可接受的延迟和成本内能否跑得稳定”。判断一个 AI 搜索方案是否成立先要看完整的等待时间而不是只看模型指标。2. 算力消耗在 AI 搜索中的真实分布2.1 训练算力、推理算力与 token 成本很多技术文章提到“算力”时不会区分阶段这会给选型造成混乱。对 AI 搜索系统来说训练算力和推理算力是两回事。训练算力用于预训练或微调模型属于离线成本通常可以集中投入耗时以天或周计。推理算力用于用户请求到达后的实时计算属于在线成本直接决定延迟和单位请求费用。token 是模型处理文本的最小单位也是算力消耗的计量单位。一个查询加上下文的长度越长生成阶段需要计算的 token 越多延迟和成本随之上升。搜索系统里经常说的“上下文膨胀”指的就是为了提供足够信息把过多候选文档填入 prompt导致每次请求的 token 数暴涨。下面用一个简单表格说明两者差异类型发生阶段时间要求主要成本优化方向训练算力模型预训练/微调离线天/周GPU 集群、数据准备数据集质量、LoRA 微调、分布式训练推理算力在线请求毫秒到秒级GPU/CPU、token 数小模型、量化、缓存、上下文裁剪推理算力才是搜索系统日常最需要关注的预算。即使模型不大如果每次都生成上千 token整体成本也会很高。因此“任意算力下最佳”在工程上等于一个问题如何在有限的推理算力预算内用合适的 token 数量产出最高质量的答案。2.2 从大模型到小模型什么场景该用多大模型在搜索链路里并不是所有步骤都要使用同一个模型。查询改写可以用快速的小模型向量化可以使用专用 embedding 模型答案生成可以使用参数量较大的模型。为了在算力受限环境跑通又可以先把生成模型换成 1B 到 3B 的小模型后续再升级。可以按算力级别做一个粗略划分算力级别适合模型规模可接受场景典型限制纯 CPU1B 以下int4/int8 量化本地实验、内部知识库、低并发生成慢长上下文吃力入门 GPU8~12GB 显存1.5B 到 7B 量化模型团队内网搜索、原型验证、中等并发并发提升后队列变长中高端 GPU24GB 以上7B 到 32B 量化模型对外产品、多租户、复杂推理成本较高需要监控多卡集群32B 以上极端复杂推理、海量搜索运维复杂度高这个表只是一个参考实际效果受模型架构、量化方式、上下文长度、并发数影响很大。落地前需要用真实查询集测试。不要只根据模型参数量判断方案好坏同规模的模型在指令跟随、长文本理解上差异很大。2.3 把算力当作预算而不是性能指标很多建设项目一开始就把“用 70B 模型”当作目标这是把算力当成性能指标来理解。实际项目更应该把算力当作预算每个搜索请求允许消耗多少 token、允许在多长时间内返回、每千次请求的推理成本上限是多少先确定这些约束再去选择模型和链路。例如一个内部知识库搜索用户能接受 5 秒内出答案查询量每天几千次团队的 GPU 只有一块 8GB 显存。这里“最优解”可能不是一个 70B 模型而是一个 7B 量化模型配合高质量索引让模型只处理已经精选过的上下文。这样做单次请求 token 少延迟低显存也能容纳。如果以后用户增加优先加缓存和并行部署而不是盲目换更大的模型。把算力当作预算之后“任意算力水平下均为最佳”这个命题就可以落地为在给定预算内通过检索质量、上下文组织和模型选择把输出质量推到最高。接下来就用一个最小原型来说明这条链路。3. 搭建一个轻量级 AI 搜索原型3.1 环境准备与依赖安装这一节会实现一个可以在本地运行的 AI 搜索原型。它不依赖云端 API使用 Ollama 运行一个小模型使用 BM25 和向量检索混合获取上下文。整体思路是先用 BM25 做关键词召回用向量相似度做语义召回两者合并排序后把得分最高的片段拼成 prompt交给本地模型生成答案。建议在 Python 3.10 以上环境安装依赖Windows、macOS、Linux 都可以。先创建虚拟环境python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install rank-bm25 sentence-transformers ollama jieba依赖说明如下rank-bm25提供 BM25 关键词检索用于召回包含精确关键词的片段。sentence-transformers把文本映射成向量用于语义召回。测试环境可以使用paraphrase-multilingual-MiniLM-L12-v2它的体积小对中文有一定支持。ollamaPython 客户端用于调用本地模型生成答案。jieba中文分词BM25 检索前先用它切词否则中文检索效果会明显变差。同时安装并启动 Ollama。启动服务后拉取一个小模型例如ollama pull qwen2.5:1.5b这里把模型命名为 1.5b实际拉取时以你本地看到的 tag 为准。不同的 Ollama 版本和模型 tag 会变化落地时先运行ollama list确认模型已经存在。注意如果不想使用 Ollama可以把生成函数换成 OpenAI 兼容接口但那样就不是“本地任意算力”的实验场景了。本地运行更合适观察算力对延迟的影响。3.2 项目结构和测试数据为了便于维护按下面结构组织代码ai_search/ ├── docs/ │ ├── python_basics.md │ └── database_index.md ├── search_engine.py └── test_query.pydocs目录放测试文档。实际项目中可以换成公司的知识库文件但格式最好统一建议先全部转成 Markdown 或纯文本。测试数据不要放太多两三个文件足够验证链路。每个文件里写一段有明确主题的内容方便检查召回结果。例如python_basics.md可以包含Python 的列表去重有多种方式。如果要求保持顺序可以使用 dict.fromkeys。 如果需要去除重复的同时统计频率可以用 collections.Counter。 在数据量较大的场景需要关注哈希表的空间占用。database_index.md可以包含数据库索引可以加快查询速度但会增加写入开销和存储占用。 常见的索引结构是 B 树适合范围查询和等值查询。 创建过多索引会导致写入变慢需要根据业务查询模式取舍。这些内容看起来简单但足够验证混合检索是否能找到相关片段。3.3 混合检索与生成核心代码search_engine.py的完整逻辑分四步加载文档、切块、建立索引、查询检索。先把加载和切块写出来import os import re from pathlib import Path import jieba def load_documents(docs_dir: str) - list[dict]: documents [] for path in Path(docs_dir).glob(*.md): text path.read_text(encodingutf-8) sections re.split(r\n\s*\n, text.strip()) for idx, section in enumerate(sections): if not section.strip(): continue documents.append({ doc_id: f{path.stem}-{idx}, text: section.strip(), source: str(path) }) return documents这里按空行切块。实际项目中可以根据 Markdown 标题、段落长度和 token 上限做更精细的切块。切块大小直接影响后续检索质量后面调优章节会继续讨论。然后是建立 BM25 索引和向量索引from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer def tokenize(text: str) - list[str]: return list(jieba.cut(text)) class HybridIndex: def __init__(self, documents: list[dict], model_name: str paraphrase-multilingual-MiniLM-L12-v2): self.documents documents self.corpus [doc[text] for doc in documents] tokenized_corpus [tokenize(doc) for doc in self.corpus] self.bm25 BM25Okapi(tokenized_corpus) self.encoder SentenceTransformer(model_name) self.embeddings self.encoder.encode(self.corpus, normalize_embeddingsTrue) def search(self, query: str, top_k: int 5, bm25_weight: float 0.4) - list[dict]: query_tokens tokenize(query) bm25_scores self.bm25.get_scores(query_tokens) query_embedding self.encoder.encode([query], normalize_embeddingsTrue)[0] vector_scores self.embeddings query_embedding combined [] for i, doc in enumerate(self.documents): score bm25_weight * bm25_scores[i] (1 - bm25_weight) * float(vector_scores[i]) combined.append({ doc_id: doc[doc_id], text: doc[text], source: doc[source], bm25_score: float(bm25_scores[i]), vector_score: float(vector_scores[i]), score: score }) combined.sort(keylambda x: x[score], reverseTrue) return combined[:top_k]这里的混合分是 BM25 得分和向量余弦相似度的加权和。向量相似度做了归一化BM25 得分没有归一化直接相加会让量纲不一致。更完整的做法是把 BM25 得分先做 min-max 归一化这里为了示例清晰没有做实际项目中要补上。最后是生成函数import ollama def generate_answer(query: str, contexts: list[dict], model: str qwen2.5:1.5b) - str: context_block \n\n.join([f[来源 {i 1}]\n{ctx[text]} for i, ctx in enumerate(contexts)]) prompt f请根据以下参考资料回答问题。 参考资料 {context_block} 问题{query} 要求只根据参考资料给出答案如果资料不足明确说明无法回答。 response ollama.chat( modelmodel, messages[{role: user, content: prompt}], options{temperature: 0.3, num_ctx: 2048} ) return response[message][content]test_query.py用来串联整个流程from search_engine import load_documents, HybridIndex, generate_answer docs load_documents(docs) index HybridIndex(docs) query Python 列表去重有哪些方法 contexts index.search(query, top_k3) print(检索结果) for i, ctx in enumerate(contexts): print(f{i 1}. {ctx[text]}) answer generate_answer(query, contexts) print(生成答案) print(answer)执行搜索时会先打印召回的文档片段再打印模型生成的答案。如果模型没有安装运行generate_answer会报连接错误此时需要先确认 Ollama 服务和模型是否正常。3.4 用命令行跑通一次查询在项目根目录运行python test_query.py正常执行的输出大致分为两部分。第一部分是检索结果比如“Python 的列表去重有多种方式……”第二部分是模型根据这些上下文生成的答案比如“Python 中如果要求保持顺序可以使用 dict.fromkeys如果要去重并统计频率可以用 collections.Counter”。如果模型抽到的上下文不准确答案也会出错这不一定是模型问题更可能是切块或混合权重需要调整。第一次跑通后建议再换几个查询覆盖三种情况包含文档关键词的查询、语义相近但关键词不匹配的查询、完全超出文档范围的查询。这样可以快速判断检索链路和生成链路是否都正常。4. 算力受限时的调优方向与验证方法4.1 先测量延迟、吞吐和首 token 时间调优前必须建立测量指标否则很难判断改动是否有效。对 AI 搜索系统最常用的指标有三个首 token 时间TTFT用户发出请求后到收到第一个 token 的时间衡量模型的冷启动和预处理速度。总生成时间从请求发出到完整答案生成完毕的时间用户最终感知的等待时间。吞吐量单位时间能处理的请求数衡量系统在并发场景下的承载能力。可以在generate_answer外部记录时间import time start time.time() answer generate_answer(query, contexts) elapsed time.time() - start print(f生成耗时{elapsed:.2f} 秒)运行几次后记录平均值。如果总耗时超过用户