尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

Vane 架构全解:一个 Next.js 应用如何把 AI 聊天与网络搜索融合成回答引擎 Vane 架构全解一个 Next.js 应用如何把 AI 聊天与网络搜索融合成回答引擎【免费下载链接】VaneVane is an AI-powered answering engine.项目地址: https://gitcode.com/GitHub_Trending/pe/VaneVaneREADME是一个用 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 路由架构文档明确列出三个核心端点POST /api/chat——驱动聊天 UIPOST /api/search——面向程序化集成的搜索端点GET /api/providers——列出可用的 provider 与模型 key。3.1POST /api/chat聊天 UI 的动力route.ts 用 Zod 定义了严格的请求体结构第 34 行至第 48 行字段类型说明message{ messageId, chatId, content }消息三要素三者均必填optimizationModespeed \| balanced \| quality速度/质量权衡档位直接决定研究迭代的深度sourcesstring[]默认[]启用的搜索来源web、academic、discussionshistory[role, content][]默认[]会话历史role 取human/assistantfilesstring[]默认[]关联的上传文件 ID用于个人文件语义检索chatModel/embeddingModel{ providerId, key }各自独立的模型选择均必填systemInstructionsstring可空用户自定义指令优先级低于系统核心指令路由的处理流程第 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缺省回落到speedstream缺省为falsestream: 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 中分类完成后立即分叉出两个 Promiseconst widgetPromise WidgetExecutor.executeAll({ classification, ... }); let searchPromise: PromiseResearcherOutput | 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.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 noteThese are the search results and assistant can cite these result index1 title....../result ... /search_results widgets_result noteForAssistantIts 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?formatjsonq...支持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 与嵌入模型拆成两个独立组件职责分别是LLMLarge Language Models用于分类、写作答案与产生引用Embedding Models用于对用户上传文件做语义检索。Vane 的关键设计是把这两类模型都做成可插拔的 provider 体系位于 src/lib/models基类抽象base/llm.ts、base/embedding.ts、base/provider.tsprovider 实现src/lib/models/providersanthropic、gemini、groq、lemonade、lmstudio、ollama、openai 均提供聊天 LLM嵌入侧提供 openai、gemini、lemonade、lmstudio、ollama 以及 transformerstransformerEmbedding.ts本地嵌入registry.ts 作为统一入口/api/chat与/api/search都经由它按providerIdkey加载模型实例。请求体中chatModel与embeddingModel分离意味着用 A 家的聊天模型分类写作、用 B 家的嵌入模型检索文件这样的组合是受支持的。七、关键组件七存储Storage架构文档对存储的描述是聊天与消息被持久化以便会话可以重新加载。实现层在 src/lib/db采用 Drizzle ORM SQLite表结构定义在 schema.tsexport const chats sqliteTable(chats, { id: text(id).primaryKey(), title: text(title).notNull(), createdAt: text(createdAt).notNull(), sources: text(sources, { mode: json }).$typeSearchSources[]().default(sql[]), files: text(files, { mode: json }).$typeDBFile[]().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 }).$typeBlock[]().default(sql[]), status: text({ enum: [answering, completed, error] }).default(answering), });两个设计点呼应了前文的架构描述responseBlocks直接存 Block 数组第 13 行至第 15 行因为 UI 的渲染单位就是块持久化块而非纯文本会话重载时可以完整还原研究步骤、小部件、来源列表与答案文本而不只是一段字符串status三态机answering / completed / errorsearchAsync在管线启动时把消息置为answeringindex.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),仅供参考
返回列表