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/api | Next.js Route Handler,即/api/chat、/api/search等服务端端点 |
| 智能体层 | src/lib/agents | 分类、研究、小部件与写作管线(src/lib/agents/search) |
| 基础设施层 | src/lib/db、src/lib/models、src/lib/searxng.ts | SQLite 持久化、模型注册表、元搜索后端集成 |
下面按架构文档列出的七个关键组件逐一展开。
二、关键组件一:用户界面(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 路由
架构文档明确列出三个核心端点:
POST /api/chat——驱动聊天 UI;POST /api/search——面向程序化集成的搜索端点;GET /api/providers——列出可用的 provider 与模型 key。
3.1POST /api/chat:聊天 UI 的动力
route.ts 用 Zod 定义了严格的请求体结构(第 34 行至第 48 行):
| 字段 | 类型 | 说明 |
|---|---|---|
message | { messageId, chatId, content } | 消息三要素,三者均必填 |
optimizationMode | 'speed' \| 'balanced' \| 'quality' | 速度/质量权衡档位,直接决定研究迭代的深度 |
sources | string[],默认[] | 启用的搜索来源(web、academic、discussions) |
history | [role, content][],默认[] | 会话历史,role 取human/assistant |
files | string[],默认[] | 关联的上传文件 ID,用于个人文件语义检索 |
chatModel/embeddingModel | { providerId, key } | 各自独立的模型选择,均必填 |
systemInstructions | string,可空 | 用户自定义指令,优先级低于系统核心指令 |
路由的处理流程(第 103 行至第 246 行):
- 校验请求体,非法则返回 400 及逐字段错误明细;
- 通过 ModelRegistry 并行加载聊天模型与嵌入模型(
Promise.all); - 把
history中的[role, content]二元组规范化为{role, content}消息对象; - 创建
SearchAgent与SessionManager会话,随后以TransformStream将 session 事件桥接为 SSE 响应流; - 异步调用
agent.searchAsync(...)执行核心管线; - 通过
ensureChatExists保证对应chatId的会话记录已落库。
3.2POST /api/search:程序化搜索端点
如果你要把 Vane 集成进其他产品,架构文档指出的入口就是这个端点,其详细参数说明见 Search API 文档。route.ts 的行为与 API 文档一一对应:
- 请求体要求
sources与query必填,缺失返回 400(第 23 行至第 28 行); optimizationMode缺省回落到speed,stream缺省为false;stream: false时返回{ message, sources }JSON——message为生成的答案,sources为答案引用的支撑来源;stream: true时返回Content-Type: text/event-stream的逐行 JSON 流,消息类型依次为init→sources→ 多段response→done。
值得注意的是,API 文档 中的请求示例要求providerId必须来自/api/providers的真实 UUID,这保证了模型路由始终经过服务端注册表校验。
3.3GET /api/providers:模型能力发现
route.ts 从模型注册表取出全部激活 provider,过滤掉模型加载报错的条目(chatModels中出现 key 为error的会被剔除)后返回。返回结构中每个 provider 携带chatModels与embeddingModels两组模型列表,key字段(如gpt-4o-mini、text-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保持为null,Promise.all仍安全等待,写作阶段会用占位上下文<Query to be answered without searching; Search not made>代替搜索结果(第 102 行至第 112 行)。也就是说 Vane 允许"只靠模型知识 + 小部件"直接作答;- 小部件与研究的先后次序解耦:小部件结果通过
session.emitBlock以widget类型块实时推给 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=...; - 支持
categories、engines、language、pageno四个可选参数,数组参数以逗号拼接(第 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/images与POST /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加载模型实例。
请求体中chatModel与embeddingModel分离,意味着"用 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 / error:searchAsync在管线启动时把消息置为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 内的旅程如下:
- UI 发出
POST /api/chat,请求体携带optimizationMode、sources、模型选择与历史; - chat/route.ts 校验、加载聊天/嵌入模型,启动
SearchAgent并建立 SSE 流; - classifier 输出结构化分类(7 个布尔决策 + 独立化问句);
WidgetExecutor与Researcher并行:研究侧按 mode 最多迭代 2/6/25 轮工具调用,通过 searxng.ts 等动作抓取网页/学术/讨论/上传文件内容并按 URL 去重;- 汇合后组装"可引用 + 不可引用"两类上下文,
streamText流式生成答案并逐块推送; - 消息以
completed状态连同全部responseBlocks写入 SQLite,会话可重新加载; - 客户端收到
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),仅供参考