Perplexica:Perplexity开源平替

Perplexica通过 SearXNG 搜索网络,可选地使用嵌入/相似性重新排序结果,然后使用 LLM 生成带引用的响应。

SearXNG 是一个免费的互联网元搜索引擎,聚合了多达 245 个搜索服务的结果。用户不会被跟踪或建立画像。此外,SearXNG 可以通过 Tor 使用以实现在线匿名。

Perplexica 提供几个很酷的功能:

  • 提供商无关: 随附 Ollama 或可插入 OpenAI/Claude/Gemini/Groq。
  • 模式: 速度、平衡、质量,以权衡延迟、深度和成本。
  • 源控制: 根据任务使用网络、讨论或学术搜索。
  • 小部件: 用于快速查找的即时卡片(天气、计算、股票)。
  • 私有网络搜索: 目前是 SearxNG,稍后会有更多检索集成。
  • 图像 + 视频: 答案不仅仅是文章。
  • 文件问答: 上传文档并查询它们。
  • 域范围搜索: 针对特定站点/文档。
  • 智能建议: 更好的查询,更快的响应。
  • 本地历史: 随时回顾研究。

在开始之前,请务必获取2026 年获胜的智能体 SaaS 模式 ***https://stan.store/agentnative***我们每周都会发送这里显示的每一层的详细分析。

让我们看看如何快速设置 Perplexica。

1、Perplexica 入门

你有两个选项来运行 Perplexica。

(1) Docker Compose

复制代码
git clone https://github.com/ItzCrazyKns/Perplexica.git
cd Perplexica

# 创建配置
cp sample.config.toml config.toml
# 使用所需的密钥/端点编辑 config.toml

docker compose up -d
# 打开 http://localhost:3000

对于 Docker + Ollama,通常使用 http://host.docker.internal:11434 作为 Ollama API URL。

你可以在 UI 的"设置对话框"中稍后更改模型密钥/设置

(2) 非 Docker

  • 安装 SearXNG 并在 SearXNG 设置中允许 JSON 格式。

  • 克隆存储库并将 sample.config.toml 文件重命名为根目录中的 config.toml。

  • (确保完成此文件中的所有必填字段)

  • 然后运行:

    npm i
    npm run build
    npm run start

如果有任何问题,你可以在这里找到更多信息

2、架构和请求流程

Perplexica 将系统描述为:

  1. UI

  2. 智能体/链

  3. SearXNG 用于网络源

  4. LLM 用于推理/回答/引用

  5. 嵌入模型用于重新排序
    概念流程是:

    (1) 请求
    (2) 链决定是否需要网络搜索 + 生成查询
    (3) SearXNG 搜索
    (4) 嵌入 + 相似性重新排序
    (5) 响应生成器流式传输到 UI

这通过特定焦点模式处理器和可重用的智能体实现。

src/lib/search/index.ts 是最重要的"产品地图"文件之一。

它将焦点模式注册为 MetaSearchAgent 的实例,设置包括:

  • activeEngines(例如,学术使用 arxiv/scholar/pubmed;youtube 使用 youtube;reddit 使用 reddit)

  • rerankrerankThreshold

  • searchWeb 对比"不搜索网络"(writingAssistant 设置 searchWeb: false

  • summarizer(为 webSearch 启用)
    src/lib/search/metaSearchAgent.ts 是编排器,它:

  • 构建"搜索检索器链"(温度为 0 的 LLM 用于查询生成)

  • 可以摄取"直接链接"输出(当启用 summarizer 时,获取并总结页面),按 URL 对文档进行分组,并使用结构化的系统提示将它们总结为 2-4 段

  • 使用 SearXNG 搜索(searchSearxng),然后使用相似性评分(computeSimilarity)重新排序/选择源(如文档中所述)

3、服务器 API

由于 Perplexica 是一个 Next.js App Router 项目,端点位于 src/app/api/* 下。

/api/search(文档化的"搜索 API"),其 POST 主体包括:

  • focusModeoptimizationModequeryhistory

  • chatModelembeddingModel(提供商 + 模型名称)

  • 可选的 systemInstructions

  • 可选的 stream
    Perplexica/src/app/api/search/route.ts

    import ModelRegistry from '@/lib/models/registry';
    import { ModelWithProvider } from '@/lib/models/types';
    import SessionManager from '@/lib/session';
    import { ChatTurnMessage } from '@/lib/types';
    import { SearchSources } from '@/lib/agents/search/types';
    import APISearchAgent from '@/lib/agents/search/api';

    interface ChatRequestBody {
    optimizationMode: 'speed' | 'balanced' | 'quality';
    sources: SearchSources[];
    chatModel: ModelWithProvider;
    embeddingModel: ModelWithProvider;
    query: string;
    history: Array<[string, string]>;
    stream?: boolean;
    systemInstructions?: string;
    }

    export const POST = async (req: Request) => {
    try {
    const body: ChatRequestBody = await req.json();

    复制代码
      if (!body.sources || !body.query) {
        return Response.json(
          { message: 'Missing sources or query' },
          { status: 400 },
        );
      }
    
      body.history = body.history || [];
      body.optimizationMode = body.optimizationMode || 'speed';
      body.stream = body.stream || false;
    
      const registry = new ModelRegistry();
    
      const [llm, embeddings] = await Promise.all([
        registry.loadChatModel(body.chatModel.providerId, body.chatModel.key),
        registry.loadEmbeddingModel(
          body.embeddingModel.providerId,
          body.embeddingModel.key,
        ),
      ]);
    
      const history: ChatTurnMessage[] = body.history.map((msg) => {
        return msg[0] === 'human'
          ? { role: 'user', content: msg[1] }
          : { role: 'assistant', content: msg[1] };
      });
    
      const session = SessionManager.createSession();
    
      const agent = new APISearchAgent();
    
      agent.searchAsync(session, {
        chatHistory: history,
        config: {
          embedding: embeddings,
          llm: llm,
          sources: body.sources,
          mode: body.optimizationMode,
          fileIds: [],
          systemInstructions: body.systemInstructions || '',
        },
        followUp: body.query,
        chatId: crypto.randomUUID(),
        messageId: crypto.randomUUID(),
      });
    
      if (!body.stream) {
        return new Promise(
          (
            resolve: (value: Response) => void,
            reject: (value: Response) => void,
          ) => {
            let message = '';
            let sources: any[] = [];
    
            session.subscribe((event: string, data: Record<string, any>) => {
              if (event === 'data') {
                try {
                  if (data.type === 'response') {
                    message += data.data;
                  } else if (data.type === 'searchResults') {
                    sources = data.data;
                  }
                } catch (error) {
                  reject(
                    Response.json(
                      { message: 'Error parsing data' },
                      { status: 500 },
                    ),
                  );
                }
              }
    
              if (event === 'end') {
                resolve(Response.json({ message, sources }, { status: 200 }));
              }
    
              if (event === 'error') {
                reject(
                  Response.json(
                    { message: 'Search error', error: data },
                    { status: 500 },
                  ),
                );
              }
            });
          },
        );
      }
    
      const encoder = new TextEncoder();
    
      const abortController = new AbortController();
      const { signal } = abortController;
    
      const stream = new ReadableStream({
        start(controller) {
          let sources: any[] = [];
    
          controller.enqueue(
            encoder.encode(
              JSON.stringify({
                type: 'init',
                data: 'Stream connected',
              }) + '\n',
            ),
          );
    
          signal.addEventListener('abort', () => {
            session.removeAllListeners();
    
            try {
              controller.close();
            } catch (error) {}
          });
    
          session.subscribe((event: string, data: Record<string, any>) => {
            if (event === 'data') {
              if (signal.aborted) return;
    
              try {
                if (data.type === 'response') {
                  controller.enqueue(
                    encoder.encode(
                      JSON.stringify({
                        type: 'response',
                        data: data.data,
                      }) + '\n',
                    ),
                  );
                } else if (data.type === 'searchResults') {
                  sources = data.data;
                  controller.enqueue(
                    encoder.encode(
                      JSON.stringify({
                        type: 'sources',
                        data: sources,
                      }) + '\n',
                    ),
                  );
                }
              } catch (error) {
                controller.error(error);
              }
            }
    
            if (event === 'end') {
              if (signal.aborted) return;
    
              controller.enqueue(
                encoder.encode(
                  JSON.stringify({
                    type: 'done',
                  }) + '\n',
                ),
              );
              controller.close();
            }
    
            if (event === 'error') {
              if (signal.aborted) return;
    
              controller.error(data);
            }
          });
        },
        cancel() {
          abortController.abort();
        },
      });
    
      return new Response(stream, {
        headers: {
          'Content-Type': 'text/event-stream',
          'Cache-Control': 'no-cache, no-transform',
          Connection: 'keep-alive',
        },
      });
    } catch (err: any) {
      console.error(`Error in getting search results: ${err.message}`);
      return Response.json(
        { message: 'An error has occurred.' },
        { status: 500 },
      );
    }

    };

src/app/api/chat/route.ts 是"聊天运行时":

  • 接受 messagehistoryfiles、模型选择、焦点模式、优化模式、systemInstructions

  • 使用相同的 searchHandlers[focusMode].searchAndAnswer(...) 机制

    流式传输的事件如:

  • { type: "message", data: "...", messageId }

  • { type: "sources", data: [...], messageId }

  • { type: "messageEnd", messageId }

    它还通过 Drizzle 持久化聊天/消息(chatsmessages 架构),并在存在时将源存储在消息元数据中。

你可能接触的其他 API 路由来自 src/app/api 文件夹列表:modelsconfigsuggestionsimagesvideosuploadsweather 等。

4、模型提供商和选择

Perplexica 将模型抽象在"提供商"之后,路由处理器请求:

  • "可用的聊天模型提供商"
  • "可用的嵌入模型提供商"
    提供商的配置密钥位于 config.tomlMODELS.*)中,并通过辅助函数如 getOpenaiApiKey()getGroqApiKey() 等读取。

支持的提供商通过以下方式反映:

  • 配置模板(sample.config.toml

  • 提供商目录列表(OpenAI、Ollama、Groq、Anthropic、Gemini、DeepSeek、LM Studio、自定义端点、AI/ML API、transformers)
    这是非流式搜索的最小"API 使用"示例:

    curl -X POST http://localhost:3000/api/search
    -H "Content-Type: application/json"
    -d '{
    "focusMode": "webSearch",
    "optimizationMode": "balanced",
    "query": "What is Perplexica?",
    "history": [],
    "stream": false
    }'

以及流式搜索:

复制代码
curl -N -X POST http://localhost:3000/api/search \
  -H "Content-Type: application/json" \
  -d '{
    "focusMode": "webSearch",
    "optimizationMode": "balanced",
    "query": "Explain how Perplexica reranks sources",
    "history": [],
    "stream": true
  }'

如果你正在构建搜索或深度研究智能体,一定要试试看,我很乐意在评论中听到你的经验。


原文链接:Perplexica:Perplexity开源平替 - 汇智网

相关推荐
熊猫钓鱼>_>6 小时前
【开源鸿蒙跨平台开发先锋训练营】Day 19: 开源鸿蒙React Native动效体系构建与混合开发复盘
react native·华为·开源·harmonyos·鸿蒙·openharmony
向哆哆6 小时前
构建健康档案管理快速入口:Flutter × OpenHarmony 跨端开发实战
flutter·开源·鸿蒙·openharmony·开源鸿蒙
FIT2CLOUD飞致云6 小时前
赛道第一!1Panel成功入选Gitee 2025年度开源项目
服务器·ai·开源·1panel
向哆哆7 小时前
构建智能健康档案管理与预约挂号系统:Flutter × OpenHarmony 跨端开发实践
flutter·开源·鸿蒙·openharmony·开源鸿蒙
向哆哆7 小时前
Flutter × OpenHarmony:打造校园勤工俭学个人中心界面实战
flutter·开源·鸿蒙·openharmony
开源能源管理系统7 小时前
开源筑基,智领零碳:MyEMS 赋能零碳工厂全周期转型新实践
大数据·开源·能源·能源管理系统·零碳工厂
CoderJia程序员甲7 小时前
GitHub 热榜项目 - 日榜(2026-01-30)
开源·大模型·llm·github·ai教程
熊猫钓鱼>_>7 小时前
【开源鸿蒙跨平台开发先锋训练营】鸿蒙应用开发 Day 10 - React Native for OpenHarmony 实战:多端响应式布局与高可用交互设计
华为·开源·交互·harmonyos·鸿蒙·rn·gridrow
昇腾CANN8 小时前
DeepSeek-V3.2-Exp高吞吐优化实践
开源·昇腾·cann
OpenLoong 开源社区8 小时前
数据开源 | 白虎-VTouch 正式开源:首个跨本体视触觉多模态数据集
开源