news 2026/9/6 16:05:29

Vane 架构全解:一个 Next.js 应用如何把 AI 聊天与网络搜索融合成回答引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vane 架构全解:一个 Next.js 应用如何把 AI 聊天与网络搜索融合成回答引擎

Vane 架构全解:一个 Next.js 应用如何把 AI 聊天与网络搜索融合成回答引擎

【免费下载链接】VaneVane is an AI-powered answering engine.项目地址: https://gitcode.com/GitHub_Trending/pe/Vane

Vane(README)是一个用 Next.js 编写的 AI 回答引擎,它的核心思路是:用户在聊天界面提问后,系统先分类问题,再并行执行"研究(搜索)"与"小部件",最后由大语言模型生成带引用的答案。本文以 架构总览 文档为骨架,结合 工作流程文档 与源码实现,逐组件讲清 Vane 的七大核心部件、端到端的请求管线以及存储、模型、搜索后端的具体落地方式,读完你可以掌握"分类—研究—生成"这类 AI 搜索回答系统的完整架构设计。

一、整体定位:聊天体验与搜索能力的合体

架构文档开宗明义:Vane 是一个把 AI 聊天体验与搜索结合起来的 Next.js 应用(docs/architecture/README.md)。这一定位决定了它的分层方式——前端负责聊天、搜索与引用展示,后端负责编排智能体、调用模型与搜索后端、持久化会话。

从源码结构看(可对照 CONTRIBUTING.md 中的项目结构说明),整个系统可以分成四层:

位置职责
界面层src/components、src/app聊天、搜索、引用展示等 UI 组件与页面路由
API 层src/app/apiNext.js Route Handler,即/api/chat/api/search等服务端端点
智能体层src/lib/agents分类、研究、小部件与写作管线(src/lib/agents/search
基础设施层src/lib/db、src/lib/models、src/lib/searxng.tsSQLite 持久化、模型注册表、元搜索后端集成

下面按架构文档列出的七个关键组件逐一展开。

二、关键组件一:用户界面(User Interface)

架构文档的第一条组件描述是:一个基于 Web 的 UI,允许用户聊天、搜索并查看引用

从源码结构看,界面侧由 Next.js App Router 的页面路由驱动,主应用路由包括首页(/)、聊天页(/c)、发现页(/discover)和资料库(/library),见 CONTRIBUTING.md。核心 UI 组件包括:

  • ChatWindow.tsx 与 Chat.tsx:聊天窗口与消息渲染主体;
  • MessageSources.tsx 与 Citation.tsx:在答案旁渲染引用与来源链接,对应文档中 "view citations" 的能力;
  • WeatherWidget.tsx 与 Widgets/:天气、股票、计算器等结构化小部件的前端渲染,与研究阶段推送的widget类型块对应(详见第五节);
  • SearchImages.tsx 与 SearchVideos.tsx:图片与视频搜索结果的展示。

界面与后端之间不是普通的 HTTP JSON 交互,而是一个块(block)级的增量流协议:服务端把每一次 UI 更新表达为block(新增块)、updateBlock(按路径打补丁更新块)事件,前端据此增量渲染研究过程、小部件与答案文本,细节见第五节。

三、关键组件二:API 路由

架构文档明确列出三个核心端点:

  1. POST /api/chat——驱动聊天 UI;
  2. POST /api/search——面向程序化集成的搜索端点;
  3. GET /api/providers——列出可用的 provider 与模型 key。

3.1POST /api/chat:聊天 UI 的动力

route.ts 用 Zod 定义了严格的请求体结构(第 34 行至第 48 行):

字段类型说明
message{ messageId, chatId, content }消息三要素,三者均必填
optimizationMode'speed' \| 'balanced' \| 'quality'速度/质量权衡档位,直接决定研究迭代的深度
sourcesstring[],默认[]启用的搜索来源(web、academic、discussions)
history[role, content][],默认[]会话历史,role 取human/assistant
filesstring[],默认[]关联的上传文件 ID,用于个人文件语义检索
chatModel/embeddingModel{ providerId, key }各自独立的模型选择,均必填
systemInstructionsstring,可空用户自定义指令,优先级低于系统核心指令

路由的处理流程(第 103 行至第 246 行):

  1. 校验请求体,非法则返回 400 及逐字段错误明细;
  2. 通过 ModelRegistry 并行加载聊天模型与嵌入模型(Promise.all);
  3. history中的[role, content]二元组规范化为{role, content}消息对象;
  4. 创建SearchAgentSessionManager会话,随后以TransformStream将 session 事件桥接为 SSE 响应流;
  5. 异步调用agent.searchAsync(...)执行核心管线;
  6. 通过ensureChatExists保证对应chatId的会话记录已落库。

3.2POST /api/search:程序化搜索端点

如果你要把 Vane 集成进其他产品,架构文档指出的入口就是这个端点,其详细参数说明见 Search API 文档。route.ts 的行为与 API 文档一一对应:

  • 请求体要求sourcesquery必填,缺失返回 400(第 23 行至第 28 行);
  • optimizationMode缺省回落到speedstream缺省为false
  • stream: false时返回{ message, sources }JSON——message为生成的答案,sources为答案引用的支撑来源;
  • stream: true时返回Content-Type: text/event-stream的逐行 JSON 流,消息类型依次为initsources→ 多段responsedone

值得注意的是,API 文档 中的请求示例要求providerId必须来自/api/providers的真实 UUID,这保证了模型路由始终经过服务端注册表校验。

3.3GET /api/providers:模型能力发现

route.ts 从模型注册表取出全部激活 provider,过滤掉模型加载报错的条目(chatModels中出现 key 为error的会被剔除)后返回。返回结构中每个 provider 携带chatModelsembeddingModels两组模型列表,key字段(如gpt-4o-minitext-embedding-3-large)就是后续请求中应填写的模型标识。该端点同时支持POST新增 provider(第 35 行至第 74 行),为"运行时注册模型"提供了管理面。

四、关键组件三:智能体与编排(Agents and Orchestration)

架构文档对编排层的描述是三步走:先分类问题,可并行执行研究与小部件,最后生成带引用的答案。这正是 SearchAgent 类searchAsync方法的实现主线。

4.1 第一步:分类(Classification)

分类实现在 classifier.ts,它用generateObject让 LLM 按一个显式 Zod schema 输出结构化决策(第 6 行至第 35 行)。schema 决定了分类器"能决定什么":

const schema = z.object({ classification: z.object({ skipSearch: z.boolean(), // 是否跳过搜索 personalSearch: z.boolean(), // 是否检索用户上传文件 academicSearch: z.boolean(), // 是否做学术搜索 discussionSearch: z.boolean(), // 是否做社区讨论搜索 showWeatherWidget: z.boolean(), // 是否显示天气小部件 showStockWidget: z.boolean(), // 是否显示股票小部件 showCalculationWidget: z.boolean(), // 是否显示计算器小部件 }), standaloneFollowUp: z.string(), // 把追问改写成独立、无上下文依赖的完整问句 });

这与 WORKING.md 中"分类决定是否研究、是否显示小部件、如何改写问题"的三点描述完全吻合。分类的输入是<conversation_history><user_query>拼接后的用户消息(第 44 行至第 47 行),提示词模板位于 prompts/search/classifier.ts。

standaloneFollowUp这一字段体现了多轮对话处理的典型工程手段:追问"那它的价格呢"必须改写成不依赖上下文的可独立检索的问句,后续研究与写作都基于改写后的句子展开。

4.2 第二步:研究与小部件并行

searchAsync 中,分类完成后立即分叉出两个 Promise:

const widgetPromise = WidgetExecutor.executeAll({ classification, ... }); let searchPromise: Promise<ResearcherOutput> | null = null; if (!classification.classification.skipSearch) { const researcher = new Researcher(); searchPromise = researcher.research(session, { ... }); } const [widgetOutputs, searchResults] = await Promise.all([widgetPromise, searchPromise]);

这里有两个架构上的要点:

  • skipSearch生效时searchPromise保持为nullPromise.all仍安全等待,写作阶段会用占位上下文<Query to be answered without searching; Search not made>代替搜索结果(第 102 行至第 112 行)。也就是说 Vane 允许"只靠模型知识 + 小部件"直接作答;
  • 小部件与研究的先后次序解耦:小部件结果通过session.emitBlockwidget类型块实时推给 UI,用户在等待答案生成期间就能看到天气、股票等结构化结果——这正是 WORKING.md 所说"widget 相关时,在答案生成期间就显示在 UI 上"。

4.3 研究(Research):一个工具驱动的多轮智能体

Researcher 是编排层里最重的部分,它把"搜索"实现为一个带工具调用循环的 LLM 智能体

  • 每一轮,LLM 按 researcher 提示词(prompts/search/researcher.ts)输出工具调用;
  • ActionRegistry 按分类结果动态提供可用工具集。从源码结构看,研究动作包括网页搜索(webSearch.ts)、学术搜索(academicSearch.ts)、社区讨论搜索(socialSearch.ts)、上传文件搜索(uploadsSearch.ts)与网页抓取(scrapeURL.ts),并配合一个done动作(done.ts)表示研究完成;
  • 循环以"最后一个工具调用是done"或"没有任何工具调用"或达到最大迭代数为终止条件(第 150 行至第 156 行)。

optimizationMode在这里落地为具体数值:最大迭代数按档位取speed为 2、balanced为 6、quality为 25(第 15 行至第 20 行)。这就是 API 文档中"speed 求快、quality 求质"在实现层的量化定义。

研究过程本身也是可观测的:Researcher 会先 emit 一个research类型块,随后把模型的推理前言(__reasoning_preamble工具调用中的plan参数)作为reasoning子步骤实时推给前端(第 37 行至第 134 行),对应 UI 上的研究步骤展示组件 AssistantSteps.tsx。

研究结束前还有一步按 URL 去重合并:同 URL 的多条结果内容拼接后只保留一条(第 185 行至第 208 行),随后以source类型块推给 UI,并作为searchFindings返回。

4.4 第三步:写作与引用

汇合阶段把两类上下文组装进写作提示词(第 102 行至第 126 行):

<search_results note="These are the search results and assistant can cite these"> <result index=1 title=...>...</result> ... </search_results> <widgets_result noteForAssistant="Its output is already showed to the user, assistant can use this information to answer the query but do not CITE this as a source"> <result>...</result> </widgets_result>

这段提示词工程值得注意:搜索结果与 widget 结果被显式区分——前者"可以引用",后者"已展示给用户、可参考但不得作为引用来源"。这与 WORKING.md 中"widgets are helpful context for the answer, but they are not part of what the model should cite"逐句对应。写作提示词由 getWriterPrompt 依据上下文、系统指令与mode生成。

答案以streamText流式产出:第一个 chunk 创建text块,后续 chunk 累加进同一块并走updateBlock补丁事件(第 142 行至第 172 行)。流结束后session.emit('end'),并把消息状态更新为completed、持久化全部块(第 174 行至第 188 行)。

五、关键组件四:搜索后端(Meta Search)

架构文档表述为"启用研究时,使用一个元搜索(meta search)后端来获取相关网页结果"。Vane 的具体选型是SearXNG——一个开源的元搜索引擎,通过 src/lib/searxng.ts 接入。

searchSearxng 的实现要点:

  • 从服务端配置注册表 serverRegistry 读取 SearXNG 实例 URL,请求${url}/search?format=json&q=...
  • 支持categoriesengineslanguagepageno四个可选参数,数组参数以逗号拼接(第 3 行至第 8 行);
  • 内置 10 秒AbortController超时,超时抛出明确的SearXNG search timed out错误;
  • 返回results(含 title、url、content 等)与suggestions

仓库内附带了完整的 SearXNG 部署配置:settings.yml、uwsgi.ini 与 limiter.toml,说明官方推荐把 SearXNG 作为配套服务部署,而把实例地址填入 Vane 的设置(安装/更新流程见 UPDATING.md 与仓库 README)。图片与视频搜索走同样的后端思路:由聊天模型先生成聚焦查询,再从搜索后端取回匹配结果(media/image.ts、media/video.ts 对应POST /api/imagesPOST /api/videos端点)。

六、关键组件五与六:LLM 与嵌入模型

架构文档把 LLM 与嵌入模型拆成两个独立组件,职责分别是:

  • LLM(Large Language Models):用于分类、写作答案与产生引用;
  • Embedding Models:用于对用户上传文件做语义检索。

Vane 的关键设计是把这两类模型都做成可插拔的 provider 体系,位于 src/lib/models:

  • 基类抽象:base/llm.ts、base/embedding.ts、base/provider.ts;
  • provider 实现(src/lib/models/providers):anthropic、gemini、groq、lemonade、lmstudio、ollama、openai 均提供聊天 LLM;嵌入侧提供 openai、gemini、lemonade、lmstudio、ollama 以及 transformers(transformerEmbedding.ts,本地嵌入);
  • registry.ts 作为统一入口,/api/chat/api/search都经由它按providerId+key加载模型实例。

请求体中chatModelembeddingModel分离,意味着"用 A 家的聊天模型分类写作、用 B 家的嵌入模型检索文件"这样的组合是受支持的。

七、关键组件七:存储(Storage)

架构文档对存储的描述是"聊天与消息被持久化,以便会话可以重新加载"。实现层在 src/lib/db,采用 Drizzle ORM + SQLite,表结构定义在 schema.ts:

export const chats = sqliteTable('chats', { id: text('id').primaryKey(), title: text('title').notNull(), createdAt: text('createdAt').notNull(), sources: text('sources', { mode: 'json' }).$type<SearchSources[]>().default(sql`'[]'`), files: text('files', { mode: 'json' }).$type<DBFile[]>().default(sql`'[]'`), }); export const messages = sqliteTable('messages', { id: integer('id').primaryKey(), messageId: text('messageId').notNull(), chatId: text('chatId').notNull(), backendId: text('backendId').notNull(), query: text('query').notNull(), createdAt: text('createdAt').notNull(), responseBlocks: text('responseBlocks', { mode: 'json' }).$type<Block[]>().default(sql`'[]'`), status: text({ enum: ['answering', 'completed', 'error'] }).default('answering'), });

两个设计点呼应了前文的架构描述:

  • responseBlocks直接存 Block 数组(第 13 行至第 15 行):因为 UI 的渲染单位就是块,持久化"块"而非纯文本,会话重载时可以完整还原研究步骤、小部件、来源列表与答案文本,而不只是一段字符串;
  • status三态机answering / completed / errorsearchAsync在管线启动时把消息置为answering(index.ts 第 22 行至第 53 行),完成时写回completed与全部块;若用户在同一消息上重新提问,会先截断该消息之后的旧块再重跑。

迁移文件 drizzle/ 下的 SQL 与 meta 快照记录了三代表结构演进,CONTRIBUTING.md 说明数据库迁移在应用启动时自动应用,开发时无需手动执行迁移命令。

八、贯穿全流程的块流协议

把前面各节串起来,Vane 的"可观测性"来自一条统一的块流协议。chat/route.ts 中 session 事件到 SSE 行的映射是:

会话事件SSE 行类型触发场景
新增块{ type: 'block', block }widget(小部件)、research(研究开始)、source(来源列表)、text(首段答案)
更新块{ type: 'updateBlock', blockId, patch }研究推理子步骤、答案文本累加(patch 为 JSON Patch 形式,如{ op: 'replace', path: '/data' }
研究完成{ type: 'researchComplete' }研究与小部件汇合之后
结束{ type: 'messageEnd' }session.emit('end')后关闭流
错误{ type: 'error', data }任意阶段异常

客户端侧由 useChat.tsx 消费该流。对集成方来说,理解这套"块 + 补丁"模型,就能解释为什么 Vane 的答案不是一次性返回,而是研究过程、小部件与文本可以交错增量呈现的。

九、一次请求的完整旅程(端到端小结)

把架构文档的组件视角换成时间轴,一条聊天消息在 Vane 内的旅程如下:

  1. UI 发出POST /api/chat,请求体携带optimizationModesources、模型选择与历史;
  2. chat/route.ts 校验、加载聊天/嵌入模型,启动SearchAgent并建立 SSE 流;
  3. classifier 输出结构化分类(7 个布尔决策 + 独立化问句);
  4. WidgetExecutorResearcher并行:研究侧按 mode 最多迭代 2/6/25 轮工具调用,通过 searxng.ts 等动作抓取网页/学术/讨论/上传文件内容并按 URL 去重;
  5. 汇合后组装"可引用 + 不可引用"两类上下文,streamText流式生成答案并逐块推送;
  6. 消息以completed状态连同全部responseBlocks写入 SQLite,会话可重新加载;
  7. 客户端收到messageEnd,界面呈现最终答案、引用与来源。

若走POST /api/search,则是同一管线的"无界面版":不落库、聚合出{ message, sources },可选 SSE 流式输出(详见 docs/API/SEARCH.md)。

十、延伸阅读

  • WORKING.md:提问到回答的高层流程说明,含optimizationMode三档语义与引用机制的官方描述;
  • CONTRIBUTING.md:组件级实现细节与"改动应该落在哪个目录"的地图;
  • docs/API/SEARCH.md:/api/search/api/providers的完整参数与响应示例;
  • docs/installation/UPDATING.md:安装与更新流程,包括搜索后端地址配置。

【免费下载链接】VaneVane is an AI-powered answering engine.项目地址: https://gitcode.com/GitHub_Trending/pe/Vane

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 15:56:37

数据服务平台建设全攻略:从架构设计到落地实践与避坑指南

简介&#xff1a;这是一份面向政企数字化建设、智慧城市与公共安全领域从业者的数据中台数据服务平台建设方案PPT&#xff08;PDF版&#xff09;&#xff0c;聚焦以“国内领先、国际先进”为目标的城市应急平台与一张图可视化方案。内容系统梳理平台核心能力&#xff0c;包括数…

作者头像 李华
网站建设 2026/9/6 15:55:23

网页媒体资源怎么抓:从装好猫抓到存下第一个文件的完整流程

网页媒体资源怎么抓&#xff1a;从装好猫抓到存下第一个文件的完整流程 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 你在某个在线课程平台看到一…

作者头像 李华