ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

在便携电脑上实现Perplexity风格搜索智能体

在便携电脑上实现Perplexity风格搜索智能体 如果把“Perplexity 便携电脑智能体研究发布”这个标题拆开来看真正值得研究的是三件事Perplexity 所代表的搜索型智能体形态便携电脑这种资源受限载体带来的工程约束以及如何把一个研究原型真正“发布”到可访问的地址而不只是停留在 Notebook 或者一段临时脚本里。Perplexity 这类产品解决的核心问题非常清晰用户输入一个开放性问题系统先搜索外部信息或检索本地知识库再让大模型基于搜集到的上下文生成回答并在回答里给出引用来源。相比纯聊天机器人它的优势是回答有依据、信息更新、能追溯来源。便携电脑上做同样的研究不是为了复刻一个大规模服务而是验证 Agent 的核心链路规划、检索、搜索、生成、引用在有限算力下能否跑通以及发布后能否稳定对外服务。本文围绕这一目标会带你完成一个最小可运行的 Perplexity 风格搜索智能体。它会包含本地知识库 RAG 检索、远程网页搜索、大模型生成带引用回答、FastAPI 服务化以及局域网发布和问题排查。整个过程适合在 Windows、macOS 或 Linux 笔记本上复现重点是理解每一步的原理而不是单纯抄代码。1. 先理解“便携电脑智能体”研究的是什么1.1 智能体与普通聊天机器人的区别普通聊天机器人通常是单轮或多轮问答输入问题大模型直接输出。智能体则多了一个“使用工具”的循环。一个典型的智能体执行流程是接收用户请求判断需要哪些工具调用工具得到结果把结果和原始问题一起交给大模型进行推理和回答。如果第一次结果不够还可能继续调用工具修正答案。Perplexity 风格的搜索智能体就是把“联网搜索”和“知识库检索”当作工具。问题进来后系统先做检索或搜索然后把命中的网页摘要、文档片段作为上下文发给大模型。大模型的任务不是凭空回答而是阅读这些材料综合出答案。这也是为什么它能给出引用来源因为上下文里有明确的段落和链接。用工程语言描述这个循环可以抽象成用户问题 - 查询改写 - Retriever召回 - 上下文拼装 - LLM 生成 - 答案 引用当本地知识库没有相关内容时智能体会触发 WebSearch把远程搜索返回的结果作为候选上下文。这样即使问题超出本地文档系统也能给出有依据的回答。1.2 为什么单独讨论便携电脑载体便携电脑的优点是环境可控、成本低、便于演示。缺点是算力、内存、电量和散热都有限。很多智能体示例默认在 GPU 服务器上运行直接搬到笔记本上会遇到一系列问题模型参数量太大、内存溢出、CPU 推理过慢、向量库文件占用磁盘过多。所以便携电脑上做智能体研究首先要面对的是“资源约束下的取舍”。模型选型要尽量小上下文窗口要控制向量库和文档索引不能无限制增长远程搜索和本地模型推理要避免同时抢占 CPU。这些约束反而逼着研究者把链路设计得更加清晰不至于靠堆算力掩盖问题。便携电脑的典型资源情况可以参考下表资源最低要求推荐配置说明内存8 GB16 GB 或以上本地模型、向量库、浏览器同时运行16 GB 更稳CPU4 核8 核影响本地模型推理速度GPU可选NVIDIA 显卡显存 4 GB 以上没有 GPU 也能运行只是生成速度更慢磁盘10 GB 可用空间20 GB模型文件通常 4 到 6 GB索引和依赖也要空间系统Windows 10 以上 / macOS / LinuxPython 3.10 以上与本方案依赖兼容性相关1.3 “研究发布”不是产品上线“研究发布”可以理解为把一个研究原型以服务方式暴露给测试者、协作者或自己的第二台设备访问。它和正式产品上线有明显差异。产品上线需要考虑多租户、高并发、权限体系、审计、监控告警、故障回滚、数据备份和合规审查。而研究发布只需要在固定网络环境下保证稳定可访问并有基本的安全措施。本文中的“发布”指通过 FastAPI 将智能体服务运行在便携电脑上并在局域网内开放访问。生产环境的额外保障会在后面的章节单独说明。2. 便携电脑上的运行环境与依赖准备在写代码之前先对齐运行环境。环境不同后面报错的现象和解法也不同。这里以 Ollama 作为本地大模型运行器以 Python 作为智能体开发语言。2.1 安装 Ollama 并准备本地模型Ollama 的优势是安装简单支持 CPU 和 GPU并提供了统一的本地模型接口。在便携电脑上推荐使用量化模型比如 qwen2.5:7b 或 qwen2.5:3b。参数量越小内存占用越低但回答质量也会同步下降。安装完成后启动服务并拉取模型# 启动 Ollama 服务 ollama serve # 另开一个终端拉取模型 ollama pull qwen2.5:7b ollama pull nomic-embed-text这里 qwen2.5:7b 用于最终回答生成nomic-embed-text 用于生成文本向量。如果你更关注英文场景也可以把生成模型换成 llama3.1:8b。要注意的是模型名称和版本会随 Ollama 仓库更新变化落地前先确认本机拉取的是哪个 tag。检查模型是否可用ollama list看到 qwen2.5:7b 和 nomic-embed-text 都出现在列表里就说明模型已经就绪。2.2 创建 Python 虚拟环境并安装依赖使用虚拟环境可以避免系统 Python 环境被污染也方便后续复现。这里以 venv 为例mkdir perplexity-agent-lab cd perplexity-agent-lab python3 -m venv venv source venv/bin/activateWindows 下激活命令是venv\Scripts\activate。然后将依赖写入 requirements.txtfastapi0.115.6 uvicorn[standard]0.32.1 langchain0.3.14 langchain-community0.3.14 langchain-chroma0.1.4 langchain-text-splitters0.3.4 chromadb0.5.23 python-dotenv1.0.1 duckduckgo-search7.0.4 pydantic2.10.4 httpx0.28.1安装依赖pip install -r requirements.txt如果某个包版本已经升级安装时以实际可用版本为准。同一套代码在不同版本下 API 可能有差异遇到 ImportError 或 AttributeError 时优先检查 LangChain 和 Chroma 的版本变更说明。2.3 选择生成模型、Embedding 模型和搜索源这里的选型直接决定运行质量。便携电脑上建议参考下表用途可选模型内存占用参考适用场景本地生成qwen2.5:7b5 GB 左右中文问答质量较好本地生成qwen2.5:3b3 GB 左右低内存便携电脑本地生成llama3.1:8b6 GB 左右英文场景更优Embeddingnomic-embed-text小于 1 GB通用语义检索Embeddingbge-m32 GB 左右中文和多语言能力更强远程搜索源需要单独考虑。示例中会使用 duckduckgo_search 库调用搜索接口。实际发布时要根据网络环境和接口合规要求选择合适的搜索服务比如 Bing Web Search API 或其他可用的搜索服务。如果远程搜索不可用可以把本地知识库检索作为兜底保证服务至少能回答本地文档相关的问题。3. 搭建可复现的搜索智能体实现这一章会完成一个可运行的最小系统。项目结构、代码和配置都会给出你可以直接在一个空目录里照做。3.1 项目目录设计目录设计追求职责清晰避免把所有逻辑堆在一个文件里。推荐结构如下perplexity-agent-lab/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── agent.py │ ├── retriever.py │ ├── searcher.py │ └── config.py ├── docs/ │ └── product.md ├── data/ │ └── chroma/ ├── scripts/ │ └── build_index.py ├── requirements.txt └── .envapp/main.py 是 FastAPI 入口app/agent.py 是智能体编排逻辑app/retriever.py 负责本地知识库召回app/searcher.py 负责远程搜索。doc 目录放本地知识文档data/chroma 存储向量索引。3.2 配置项创建一个 .env 文件集中管理模型名称、搜索开关和端口# 模型配置 LLM_MODELqwen2.5:7b EMBED_MODELnomic-embed-text OLLAMA_BASE_URLhttp://localhost:11434 # 服务配置 HOST0.0.0.0 PORT8000 API_TOKENdev-token-123 # 搜索配置 ENABLE_WEB_SEARCHtrue WEB_SEARCH_MAX_RESULTS5 SEARCH_TIMEOUT10config.py 读取这些配置import os from dotenv import load_dotenv load_dotenv() class Config: llm_model os.getenv(LLM_MODEL, qwen2.5:7b) embed_model os.getenv(EMBED_MODEL, nomic-embed-text) ollama_base_url os.getenv(OLLAMA_BASE_URL, http://localhost:11434) host os.getenv(HOST, 0.0.0.0) port int(os.getenv(PORT, 8000)) api_token os.getenv(API_TOKEN, dev-token-123) enable_web_search os.getenv(ENABLE_WEB_SEARCH, true).lower() true web_search_max_results int(os.getenv(WEB_SEARCH_MAX_RESULTS, 5)) search_timeout int(os.getenv(SEARCH_TIMEOUT, 10)) config Config()3.3 建立本地知识库索引在 scripts/build_index.py 中把 docs 目录下的文档拆分成 chunk写入 Chroma。这是 RAG 链路中的离线段只需要在文档更新时运行。from pathlib import Path from langchain_chroma import Chroma from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain_community.embeddings import OllamaEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter from app.config import config loader DirectoryLoader(docs, glob*.md, loader_clsTextLoader) documents loader.load() splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , ., !, ?, ], ) chunks splitter.split_documents(documents) embeddings OllamaEmbeddings( modelconfig.embed_model, base_urlconfig.ollama_base_url, ) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directorydata/chroma, ) print(findexed {len(chunks)} chunks)chunk_size 设置为 500 字符是便携电脑上的一个推荐值。过大的 chunk 会让大模型上下文迅速接近窗口上限过小的 chunk 又会丢失上下文。chunk_overlap 设置为 80可以减少段落边界语义被切断的问题。运行索引脚本python scripts/build_index.py再次运行时如果提示 collection 已存在可以先删除 data/chroma 目录后重建或者使用 Chroma 的 update 逻辑。研究阶段最简单的方法是清空重建。3.4 检索器从向量库召回相关片段retriever.py 负责根据用户问题从本地知识库召回相关段落from langchain_chroma import Chroma from langchain_community.embeddings import OllamaEmbeddings from app.config import config embeddings OllamaEmbeddings( modelconfig.embed_model, base_urlconfig.ollama_base_url, ) def retrieve(query: str, top_k: int 3): vectorstore Chroma( embedding_functionembeddings, persist_directorydata/chroma, ) docs vectorstore.similarity_search_with_score(query, ktop_k) return [ { content: doc.page_content, source: doc.metadata.get(source, local), score: round(score, 4), } for doc, score in docs ]相似度分数可以直接用于判断本地知识是否与问题相关。分数越低通常表示语义距离越近但具体范围取决于 embedding 模型不建议跨模型比较。如果检索分数差距不大说明本地知识库可能没有明确答案这时候应该触发远程搜索。3.5 搜索工具接入远程搜索源searcher.py 提供统一的搜索接口。使用 duckduckgo_search 库时的实现如下from ddgs import DDGS def search_web(query: str, max_results: int 5): results [] with DDGS() as ddgs: raw_results ddgs.text(query, max_resultsmax_results) for item in raw_results: results.append( { title: item.get(title, ), url: item.get(href, ), snippet: item.get(body, ), } ) return results这里要注意远程搜索服务通常有频率限制连续请求过快会被限流。研究阶段可以增加一个最小重试间隔或者捕获异常后返回空列表让智能体退化为本地 RAG。import time def search_web_with_retry(query: str, max_results: int 3, retries: int 2): for attempt in range(retries): try: return search_web(query, max_results) except Exception as exc: print(fsearch failed: {exc}, attempt{attempt 1}) time.sleep(2) return []3.6 智能体编排检索、搜索和生成agent.py 是整个系统的核心。它先把检索结果和搜索结果合并成上下文再让大模型生成最终回答。from langchain_community.llms import Ollama from app.config import config from app.retriever import retrieve from app.searcher import search_web_with_retry SYSTEM_PROMPT 你是搜索型智能体助手。你的任务是基于用户提供的参考资料回答问题。 如果参考资料不足请直接说明不要编造。 回答中请用 [1]、[2] 这样的编号标注引用来源。 引用必须来自提供的参考资料不能凭空生成。 def build_context(local_docs, web_results): lines [] for index, doc in enumerate(local_docs, start1): lines.append(f【本地资料 {index}】{doc[content]}) for index, item in enumerate(web_results, startlen(local_docs) 1): lines.append(f【网页资料 {index}】{item[title]}\n{item[snippet]}) return \n.join(lines) def build_sources(local_docs, web_results): sources [] for doc in local_docs: sources.append({type: local, source: doc[source], score: doc[score]}) for item in web_results: sources.append({type: web, source: item[url], title: item[title]}) return sources def run_agent(query: str): local_docs retrieve(query, top_k3) web_results [] if config.enable_web_search: web_results search_web_with_retry(query, max_resultsconfig.web_search_max_results) context build_context(local_docs, web_results) sources build_sources(local_docs, web_results) llm Ollama( modelconfig.llm_model, base_urlconfig.ollama_base_url, temperature0.3, top_k10, num_predict1024, ) user_prompt f用户问题{query}\n\n参考资料\n{context} answer llm.invoke( [ (system, SYSTEM_PROMPT), (human, user_prompt), ] ) return { query: query, answer: answer, sources: sources, local_hit_count: len(local_docs), web_hit_count: len(web_results), }关键点有两个。第一temperature 设置为 0.3让回答更保守减少幻觉。第二system prompt 明确要求回答中使用引用编号并且引用必须来自参考资料。这样大模型就不会随意编造来源。3.7 FastAPI 服务接口main.py 把智能体暴露成 HTTP 接口from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel from app.agent import run_agent from app.config import config app FastAPI(titlePerplexity Agent Lab) class QueryRequest(BaseModel): query: str class QueryResponse(BaseModel): query: str answer: str sources: list local_hit_count: int web_hit_count: int def verify_token(authorization: str): expected fBearer {config.api_token} if authorization ! expected: raise HTTPException(status_code401, detailinvalid token) app.get(/health) def health(): return {status: ok} app.post(/api/search, response_modelQueryResponse) def search(request: QueryRequest, authorization: str Header(default)): verify_token(authorization) if not request.query.strip(): raise HTTPException(status_code400, detailquery is empty) return run_agent(request.query)这个接口通过 Bearer Token 做基本鉴权。研究环境下不能完全依赖它但至少避免服务暴露后任意设备都可以调用。如果要在公网发布还需要 HTTPS、更严格的认证和限流。4. 在便携电脑上完成运行与“发布”4.1 首次运行和验证启动服务前确认 Ollama 已经在后台运行否则代码调用模型时会报连接错误。ollama serve在项目目录下启动 FastAPIsource venv/bin/activate uvicorn app.main:app --host 127.0.0.1 --port 8000然后打开另一个终端验证健康检查curl http://127.0.0.1:8000/health返回{status:ok}再测试一次完整问答curl -X POST http://127.0.0.1:8000/api/search \ -H Content-Type: application/json \ -H Authorization: Bearer dev-token-123 \ -d {query: 这个项目里的产品有什么核心功能}如果 docs/product.md 里包含对应内容并且索引已经建立返回的 answer 会引用本地资料。如果文档里没有这个信息系统会触发远程搜索answer 的 sources 里会出现 web 类型来源。4.2 以服务方式发布到局域网要让同一局域网内的其他设备访问启动时需要绑定 0.0.0.0uvicorn app.main:app --host 0.0.0.0 --port 8000此时查看本机局域网 IPWindowsipconfigmacOS / Linuxifconfig或ip addr假设本机 IP 是 192.168.1.100其他设备访问的地址就是http://192.168.1.100:8000/docs这里还有一个常见坑Windows 防火墙或 macOS 网络权限会拦截外部访问。如果其他设备无法访问优先检查防火墙是否放行 8000 端口而不是先怀疑代码。4.3 可选用启动脚本或 Docker 简化发布研究阶段可以只使用 uvicorn 命令。但如果你需要反复重启可以写一个简单的 start.sh 脚本#!/usr/bin/env bash set -e if ! curl -s http://localhost:11434/api/tags /dev/null; then echo Ollama is not running exit 1 fi source venv/bin/activate uvicorn app.main:app --host 0.0.0.0 --port 8000如果需要更稳定的隔离环境可以用 Docker。不过便携电脑 Docker 会引入额外内存开销并且需要把 Ollama 和 Chroma 数据目录挂载到宿主机。示例 Dockerfile 如下FROM python:3.11-slim WORKDIR /workspace COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app COPY docs ./docs COPY scripts ./scripts RUN python scripts/build_index.py EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]这里把索引构建也放进了镜像适合文档相对固定的研究场景。如果文档经常变动建议启动后再执行索引更新而不是每次构建镜像。5. 研究过程中的常见问题和排查链路5.1 模型加载失败或内存不足现象是启动后第一个请求非常慢或者终端报 Ollama 相关错误比如Ollama served at ...无法访问或者进程被系统杀掉。先确认 Ollama 服务是否运行curl http://localhost:11434/api/tags如果能返回模型列表说明服务正常。接着看内存如果是 8 GB 内存的电脑跑 7B 模型建议换成 qwen2.5:3b或者使用 Ollama 量化版本。启动时也可以限制并发只跑一个推理请求不要同时访问 /api/search 多个连接。5.2 搜索超时或返回空远程搜索会受网络和服务方限流影响。如果返回结果为空先单独测试搜索函数python -c from app.searcher import search_web; print(search_web(test, max_results2))如果能返回结果说明搜索接口正常问题在智能体编排。如果返回空或报错检查搜索服务是否需要 API Key、是否被限流、超时时间是否太短。研究阶段可以把 config.enable_web_search 设为 false先验证本地 RAG 链路。5.3 向量检索结果不相关表现是回答里引用的本地资料与问题完全无关。这类问题可以先看检索分数如果所有 chunk 的 score 都很高说明 embedding 模型或者文档拆分方式不适合当前语料。建议按以下顺序排查确认索引是否是最新旧索引不会包含新文档。确认文档编码是否为 UTF-8避免乱码导致语义混乱。调小 chunk_size减少每个 chunk 中的无关信息。检查 query 是否过短短查询可能无法表达检索意图。5.4 局域网访问不到服务服务绑定了 0.0.0.0但其他设备仍然访问不到。先在本机确认端口是否监听curl http://127.0.0.1:8000/health本机能访问说明服务运行正常。然后在另一台设备上 ping 本机 IP确认网络通。如果 ping 通但端口不通就是防火墙或安全组拦截。Windows 上命令行执行netsh advfirewall firewall add rule nameperplexity-agent-8000 dirin actionallow protocolTCP localport8000排查顺序一定是先服务进程再本机端口再网络连通性再防火墙。5.5 引用来源错误大模型可能给出 [1]但来源列表里没有第 1 条或者引用了不在参考资料里的内容。这通常是 prompt 约束不够。可以打开 agent.py 的 SYSTEM_PROMPT增加一条“如果没有可用引用回答只能使用编号 [0] 并在末尾注明没有检索到参考资料。”更好的做法是在后处理里校验回答中的引用编号是否落在 sources 范围内。如果发现越界引用可以重新调用大模型生成或者在回答末尾追加“来源引用存在缺失”。5.6 常见问题速查表问题现象常见原因检查方式处理建议Ollama 连接失败服务未启动curl localhost:11434/api/tags启动ollama serve内存溢出模型过大或并发过高查看系统进程和内存占用换小模型或限制并发远程搜索无结果网络不可达、限流、无 API Key单独测试搜索函数换搜索源或关闭远程搜索检索结果不相关索引过期、chunk 过大、embedding 不匹配查看 score 和索引时间重建索引、调小 chunk、换 embedding局域网不可访问绑定 127.0.0.1 或防火墙拦截本机 health ping 端口检测绑定 0.0.0.0 并放行端口回答引用错误prompt 约束不足检查 answer 中编号和 sources增加后处理和来源校验6. 研究发布的最佳实践和扩展方向6.1 学习环境与生产环境的关键差异便携电脑研究环境适合快速验证但绝不能用同一套配置直接当生产服务。差异主要在安全、稳定性和运维维度研究环境生产环境模型部署本机 OllamaGPU 集群或模型服务考虑高可用搜索源单源、低频多源切换、限流、缓存服务暴露局域网公网或内网网关需要 HTTPS鉴权Bearer TokenOAuth、密钥管理、操作审计日志print 输出结构化日志、集中监控数据本地测试文档数据安全、脱敏、备份更新手动重启CI/CD、灰度发布、回滚6.2 发布前检查清单每次准备发布研究原型时可以按下面的清单逐项确认[ ] 确认 Ollama 服务在后台正常运行模型已下载。[ ] 确认向量索引和 docs 目录中的文档一致。[ ] 先用 127.0.0.1 完成一次完整问答确认 answer 和 sources 都正确。[ ] 检查远程搜索源是否可用是否会被限流。[ ] 修改 .env 中的 API_TOKEN 为随机字符串不要使用默认值。[ ] 确认服务绑定 0.0.0.0 时防火墙已放行对应端口。[ ] 在局域网另一台设备上验证接口和 Swagger 页面。[ ] 确认 CPU 和内存占用在可接受范围内长期运行不会过热或卡死。[ ] 记录日志输出文件方便后续排查。[ ] 如果文档会更新确认索引重建脚本和触发方式。6.3 下一步扩展方向这个最小系统跑通后可以从多个方向继续深入第一加入查询改写。用户问题往往不够清晰可以先让一个小模型把问题拆成关键词再用关键词去检索和搜索这会明显提升召回质量。第二加入记忆和会话管理。当前每次请求都是独立状态没有上下文。如果要支持多轮对话需要把历史消息传入 prompt并设计会话 ID。第三加入评估集。准备一组带标准答案的问题用回答的召回率、引用准确率和用户满意度来评估系统好坏。只有可量化评估才能判断模型替换、参数调整是变好还是变坏。第四把搜索源抽象成插件。现在 searcher.py 只依赖一个远程搜索实现你可以扩展出本地文件搜索、数据库查询、内部 API 调用等工具逐步让智能体从“搜索 RAG”变成真正的多工具 Agent。第五做模型蒸馏和量化。便携电脑上的推理效率仍有很多优化空间可以尝试更小的模型、更长的上下文管理、流式输出以及 prompt 压缩。这项研究的价值在于它把一个看起来很“大厂”的智能体形态压缩到一台普通笔记本可以跑通的复杂度里。你理解了这条链路之后无论是换模型、换搜索源还是扩展到其他工具都会比直接调用一个在线 API 更清楚系统到底在做什么。下一步如果需要生产化就围绕安全、可观测性和高可用逐个补齐而不是继续堆功能。
返回列表