前端转战AI应用开发:手把手打造内部知识库问答助手

📅 发布时间:2026/9/20 10:54:18
前端转战AI应用开发:手把手打造内部知识库问答助手
前端这个圈子这些年有个很有趣的现象一到技术转型节点跳得最欢的往往不是后端而是天天跟页面打交道的前端。前两篇我们聊了本地大模型部署和对话网页怎么搭今天这篇我打算换个节奏从一个真实需求出发把“前端怎么正儿八经做一个能交给同事用的 AI 应用”这条路完整走一遍。标题里说的“跑路”不是说真的离开前端而是把前端技能挪到 AI 应用开发这条新赛道上把原来会的东西变成新的优势。这篇文章会从需求拆解、技术选型、核心链路实现、知识库落地到问题排查一步步讲清楚适合那些前端基础扎实、想往 AI 应用开发方向转但又被各种算法名词劝退的朋友。1. 先对齐一下前端做 AI 应用到底在做什么很多前端同学一听“AI 应用开发”脑子里马上浮现的是神经网络、反向传播、Transformer 结构这些东西然后直接自我劝退。实际上我们日常所说的 AI 应用开发跟训练模型完全是两码事。你可以把成熟的大模型想象成一个能力很强但完全不了解你们公司业务的实习生AI 应用开发做的就是给这个实习生配上工作台、资料库、工具链让他能真正上手干活。这个定位想清楚了前端在其中的价值就非常明显。1.1 你不需要训练模型你只需要用好模型训练一个属于自己的模型那是算法工程师和研究团队的工作范畴投入大、周期长对绝大多数业务团队来说既不现实也没必要。真正有落地价值的 AI 应用开发是把已经存在的大模型能力通过 API 调用、提示词工程、检索增强、工具调用等手段嵌入到具体的业务场景里去。拿我自己举例去年团队里积压了大量项目文档和技术方案新人入职后翻半天资料也找不到关键信息我就用本地部署的大模型接了一个简单的问答服务把资料文档切块存进向量库做了一个内部知识库问答助手。整个开发过程没有写一行模型训练代码核心工作量全在数据处理、接口对接和交互体验上这些东西正是前端最擅长的。后端同学做 AI 应用关注点往往是服务稳定性、并发性能、模型推理优化而前端同学做 AI 应用最大的优势在于对用户行为的理解和对交互细节的把控。同一个问答助手后端交付的是能跑的接口前端交付的是用户真正愿意用的产品。一个 AI 应用到后面拼的往往是产品体验而不是模型的参数大小。1.2 前端这套本事在 AI 应用里一点没浪费最近圈子里有“前端岗位从此消失”的说法我持保留态度。真正发生变化的是岗位内容不是岗位本身。以前我们写一个管理后台核心是把表单、表格、弹窗这些组件拼装起来数据是后端给什么我们就展示什么。到了 AI 应用时代数据变成了一股流式返回的文本交互从“点击-跳转”变成了“多轮对话实时生成”这要求前端具备更强的编排能力和状态管理能力。组件化思维在 AI 应用里特别好用。一个对话窗口、一个参数配置面板、一个知识库管理页面都可以拆成独立组件复用性很高。我在做问答助手的时候把“消息列表”“流式文本渲染”“引用来源展示”三个组件拆开后面接不同业务场景时直接复用省了大概三分之一的工作量。另外前端同学普遍对异步编程比较熟悉Promise、事件循环、回调这些概念在 AI 应用里频繁用到因为几乎每个交互背后都是一次异步的大模型调用。还有一点容易被忽视AI 应用的前端要做大量的状态分支处理。模型在响应过程中会有“连接中”“正在生成”“生成完成”“发生错误”等状态每个状态对应不同的 UI 展现这本质上就是一个复杂的前端状态机。用 Vuex 或 Pinia 管理这些状态简直不要太顺手。2. 从需求到方案一个内部知识库问答助手的设计纸上谈兵没意思我们直接用一个最常见的场景内部知识库问答助手。这个需求在几乎每个有一定规模的公司里都存在而且非常典型——它既包含了大模型调用又包含了对私有数据的处理还涉及完整的交互闭环用来做 AI 应用开发的练手项目再合适不过。2.1 先花时间把需求拆清楚别急着写代码接下这个需求后我没有直接打开编辑器而是先拉上提需求的同事聊了半小时。需求方说得很简单“想做一个能回答公司制度问题的机器人”。但如果只按字面意思做大概率会做一个没人用的玩具。我把需求拆成了三层第一层是用户问了问题系统能调用大模型给出一个基本合理的回答第二层是回答必须基于公司给定的资料不能瞎编这就引入了检索增强生成RAG第三层是回答要给出参考来源让用户能顺着答案点回去看原文这对建立信任感非常关键。三层需求从易到难每一层都对应明确的用户价值这样后续的工作量和验收标准就清晰了。拆完需求后你还会发现很多细节问题会浮现出来资料有 PDF、Word、Markdown 各种格式需要做格式解析制度文件经常更新知识库需要支持增量导入和删除用户提问时一定是口语化的而资料里的措辞往往是书面化的中间需要处理语义匹配的问题。这些问题你不提前想写到一半大概率要返工。2.2 技术选型别追求花哨选自己驾驭得了的方案技术选型这件事我一直秉承一个原则选自己团队最能驾驭的方案而不是网上讨论度最高的方案。这个项目我用的技术栈非常简单前端这块Vue3 Element Plus Vite 是我的老搭档。Vue3 的组合式 API 在管理复杂对话状态时比 Options API 舒服得多Element Plus 能快速搭出后台管理风格的界面Vite 的开发体验不用多说秒级热更新在频繁调试 AI 接口时非常省时间。后端我选的是 Python FastAPI。可能有人会问前端为什么不自始至终用 Node.js原因很简单Python 在 AI 生态里的统治地位太强了不管是接各种大模型的 SDK还是做文本切块、向量化Python 都是第一公民。遇到问题搜索引擎里随手一查就是答案这对半路出家的人来说最关键。FastAPI 本身的异步特性和自动生成接口文档的能力也让它特别适合做 AI 应用的后端。大模型这部分我的建议是本地部署和云端 API 并行。开发阶段用 Ollama 跑一个中等规模的模型比如 qwen 系列好处是不要钱、没有调用频率限制调试起来随心所欲上线阶段根据数据隐私要求和性能要求再切换到云端 API。本地部署对 GPU 的要求其实没那么恐怖7B 到 14B 参数规模的量化模型一张消费级显卡就能跑大多数公司开发机都能满足。向量数据库起步阶段用 Chroma 或者 FAISS 这种嵌入式方案就够了。很多人都想一步到位上专业的向量数据库但实际在数据量小的时候这些都是过度设计。我这个项目里文档总量不超过几千份用 Chroma 一个 Python 依赖就搞定了后面数据量真的大了再平滑迁移到专门的向量数据库也不迟。3. 核心链路实现前端如何把 AI 能力接进来技术选型定好后接下来就是最核心的部分前端到底怎么把 AI 能力接进来而且要接得流畅、接得自然。这一步里流式输出是绝对绕不开的坎。如果不用流式用户提问后要傻等好几秒甚至几十秒体验极其糟糕。用上流式输出后字是一个个蹦出来的用户第一眼反馈很快体感时长直接缩短一大截。3.1 先想清楚后端需要给前端提供什么前端开发的第一步往往是跟后端对齐接口格式。在这个项目里我的后端只需要提供三个核心接口第一个是POST /api/chat负责接收用户的提问和上下文返回大模型的流式响应第二个是POST /api/upload负责接收上传的文档做解析、切块、向量化并存入知识库第三个是GET /api/query负责给定一个问题召回相关的文档片段。这三个接口一确定前后端就可以完全并行开发前端甚至可以先用 Mock 数据把界面写好。关于响应格式我强烈建议用 SSEServer-Sent Events而不是轮询或者 WebSocket。SSE 和 HTTP 同源实现简单断线自动重连对大模型这种单方向持续输出的场景是天然匹配的。前端只需要用EventSource或者fetch的流式读取能力就能消费不需要额外引入复杂的客户端库。下面是我在后端用 FastAPI 实现的一个极简 SSE 流式接口示例核心逻辑就是通过yield不断输出文本片段from fastapi import FastAPI from fastapi.responses import StreamingResponse import json app FastAPI() def fake_llm_stream(prompt: str): # 实际场景这里会调用大模型逐句返回结果 response_text 你好我是企业内部知识助手。 for char in response_text: yield char time.sleep(0.05) app.post(/api/chat) async def chat(request: dict): prompt request.get(prompt, ) def event_stream(): for chunk in fake_llm_stream(prompt): # SSE 格式要求每条数据以 data: 开头 yield fdata: {json.dumps({content: chunk}, ensure_asciiFalse)}\n\n return StreamingResponse(event_stream(), media_typetext/event-stream)后端逻辑非常直白把大模型返回的内容逐片拆出来包上 SSE 格式返回。前端的核心任务就是逐段解析并渲染。3.2 流式输出的前端写法前端这边我推荐用fetch而不是EventSource。原因有两个一是EventSource只能发送 GET 请求而我们要传递用户提问内容POST 更合适二是fetch配合ReadableStream可以拿到更底层的控制权不管是处理错误还是中途停止都更方便。核心代码长这样async function streamChat(prompt, onMessage, onDone, onError) { const resp await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt }), }); if (!resp.ok) { onError(resp.statusText); return; } const reader resp.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // SSE 数据以空行分隔这里按空行拆分 const events buffer.split(\n\n); buffer events.pop() || ; for (const event of events) { const line event.trim(); if (!line.startsWith(data:)) continue; const payload line.slice(5).trim(); try { const data JSON.parse(payload); onMessage(data.content); } catch (e) { console.warn(解析失败, e); } } } onDone(); }这段代码的核心技巧在于用buffer缓存半截数据。因为网络传输是按数据块chunk到达的一个完整的 SSE 事件可能被拆成两次传过来如果直接按次解析就会漏数据。我在这里用空行作为分隔符先把每次收到的数据追加到缓冲区再按完整事件拆分剩下半截暂时留在缓冲区等下一次数据到达时再拼接这样就不会有事件被切开了。这个坑是实际开发中遇到的第一个大坑当时线上偶发性地出现答案末尾缺几个字排查了半天才发现是 SSD 传输中断导致的事件被截断。3.3 长文本渲染与响应展示拿到流式文本后前端要面临一个几乎必然遇到的问题模型输出的不是纯文本而是包含 Markdown 格式标记的内容。大模型的回答里经常会包含代码块、列表、表格如果直接用div{{ content }}/div插值用户看到的就是一坨带着#和*符号的原始文本体验很差。我的处理方案是引入一个支持流式的 Markdown 渲染库比如marked配合highlight.js做代码高亮。这里有一个很重要的细节流式场景下Markdown 文本是不完整的一段以##开头的标题在第二个#还没到达之前如果已经做了一次渲染页面就会出现一瞬间的闪烁。我的解决办法是做一个简单的节流机制每 50 到 100 毫秒才触发一次重新渲染而不是每个字符都渲染一遍同时在渲染前检查文本是否以 Markdown 的未闭合标记结尾如果是就暂时不渲染这个标记这样能显著减少闪烁感。另外生成过程中的 UI 状态管理也要处理好。我在 Pinia 里定义了一个会话状态机包含idle、streaming、completed、error四个状态。用户点击发送按钮后进入streaming状态这时输入框要禁用停止按钮要显示消息气泡下方可以放一个打字指示器生成完成后回到idle状态同时渲染“复制答案”“重新生成”这两个操作按钮。“重新生成”这个功能特别推荐加上因为大模型本身有随机性同一个问题多抽几次总有一次能给到满意答案这个操作的实现也简单把最后一条用户消息重新发送一遍就行。流式输出期间如果用户想终止生成不能简单粗暴地断开连接因为后端可能已经把整段内容都处理完了。正确做法是调用AbortController的abort方法中止前端的读取循环同时向后端发送一个取消请求。这样既保住了已经收到的内容也不会让后端继续空转消耗资源。4. 知识库落地的关键文本切块与检索增强前面说的问答助手如果只是把用户问题直接丢给大模型那它就是个没有灵魂的接口搬运工回答的质量完全取决于大模型本身训练时见过多少资料。想让模型准确回答公司内部问题必须把知识库检索RAG接进来。RAG 的原理非常简单用户提问后先从我们的资料库里找出跟问题最相关的几段文本把这几段文本和问题一起塞给大模型让它“参考这些资料回答”。4.1 为什么前端也得关注 RAG很多前端同学觉得自己又不写算法RAG 跟自己没关系。但实际在 AI 应用开发里RAG 的效果直接决定了产品能不能用而这个效果跟工程实现的关系比跟算法模型的关系大得多。你用什么策略去切文本、用哪个向量模型做编码、检索时怎么排序打分这些全都是工程决策不是说用了某个“标准方案“就万事大吉。更关键的一点是RAG 链路中的大部分工作本质上跟写业务接口没什么区别——读取文档、调用编码接口、写入数据库、查询数据这些都不涉及微积分和矩阵论。就好比你在业务系统里写一个订单查询接口公司订单表需要做分库分表你不需要自己研究分布式系统理论只需要掌握方案和实践经验。前端的结构化思维在 RAG 链路里也很重要。文档解析后应该存成怎样的数据结构切块后的文本和元信息如何关联检索结果按什么字段返回给前端展示这些问题前端处理起来天然有优势。4.2 最小可行的向量化与检索方案我用的方案分四步文档解析、文本切块、向量化存入向量库、检索时做相似度匹配。文档解析这步不同的文档格式有不同的处理库。PDF 用pypdfWord 用python-docxMarkdown 和纯文本直接读取再把解析出来的内容统一转成纯文本。这一步的坑主要在 PDF 上很多扫描版 PDF 其实是一张图片直接解析出来是空文本这种情况我用 OCR 接口去识别但那是另外一套流程了初期可以先把这一部分排除掉。文本切块是整个 RAG 链路里最影响效果的环节。切得太粗一块文本里包含多个主题检索时召回的内容不精准切得太细一个完整的逻辑被拆成好几段模型理解不了上下文。我实测下来按固定字符数切分块大小取 500 左右、重叠取 100 左右综合效果最好。具体实现很简单def split_text(text, chunk_size500, overlap100): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) if end len(text): break start end - overlap return chunks切块完成后把每一块文本送给 embedding 模型做向量化得到一组几百维的浮点数数组连同原文一起存入向量库。检索的时候把用户问题也做同样的向量化然后计算问题向量和库里所有文本向量的余弦相似度取 top_k 个最相似的片段。这个匹配过程在数据量小的时候用暴力计算就行几千条数据的计算量完全在可接受范围内。最后一步是把检索到的片段拼进 Prompt示意结构是这样的请根据以下参考资料回答用户问题。 如果参考资料中没有相关信息请如实说明“资料库中未找到相关内容”。 参考资料 1. [xxx 文档]公司年假政策…… 2. [xxx 文档]员工离职流程…… 用户问题入职满一年后年假有几天在 Prompt 里明确告诉模型“不知道就直说”这一步太重要了能避免大模型一本正经地胡说八道。引用来源的展示本质上是把你给模型的那几个知识片段原封不动地渲染出来用户点了能跳转到原文位置。4.3 中文内容的切块细节与调优经验切块的参数不能死记硬背必须拿自己的数据做测试。但中文内容有几个细节是通用的值得单独拿出来说。第一尽量优先在段落边界处切块也就是遇到换行符优先切而不是单纯数到 500 个字符就硬切。同一段文字硬被拆到两块里检索时会丢失语境信息。我的做法是先按段落把文本分组再尝试把多个段落拼成一个 500 字左右的块这样每块内容在语义上更完整。第二大标题和小标题尽量单独保留。很多文档一个章节的小标题后面跟着好几段正文如果把标题混在正文里检索时向量化会把标题语义稀释掉。更好的做法是切块时把最近的标题拼接在当前块的开头比如“【员工考勤管理制度】迟到早退的认定标准……”这样模型在回答时能更好地理解上下文出处。第三embedding 模型的选型直接影响检索效果。中英文混合场景下bge-m3 这类中文优化过的模型效果明显好于通用的多语言模型。另外同一个项目里文档入库和线上检索必须使用同一个 embedding 模型否则向量空间不一致检索结果完全不可用。这个坑我踩过一次当时切换模型后忘了更新之前的向量导致检索结果驴唇不对马嘴排查了半天才定位到问题。5. 常见问题与排查实录AI 应用开发跟传统前端开发有一个明显的区别传统的接口调用返回结果是确定的出了问题查一下网络请求基本就能定位而 AI 应用的返回结果带有很大的不确定性有时候同一段代码这次调用成功下次调用就报错甚至这次和下次生成的内容都会差很多。这种不确定性让很多人排查问题查到怀疑人生。这节我把自己反复踩过的几个坑分享出来希望能给你省点时间。5.1 CORS 跨域问题前端开发的第一道坎本地开发的时候前端跑在 Vite 的 5173 端口后端跑在 FastAPI 的 8000 端口两个服务一分开浏览器的同源策略马上就会拦你。这时候你有两个选择后端加 CORS 头或者前端配代理。后端的解决方案是给 FastAPI 加上 CORSMiddleware把前端的地址加进允许列表。但这个方案有个隐患一旦配置了allow_origins[*]等于让所有网站都能调你的接口本地调试没问题上了生产环境就是安全隐患。如果部署到公网建议把允许的来源限定成你自己的域名。前端的解决方案是在 Vite 配置文件里加代理把/api开头的请求全部转发到后端地址。这个方案我的体感更好因为开发环境和生产环境可以用同一套相对路径/api/xxx不需要在前端代码里区分环境变量部署时再用 Nginx 转发一次就行。配置如下// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, } } } }5.2 中文乱码与编码问题AI 应用里中文是绝对的主角但中文编码问题会在意想不到的地方冒出来。最常见的是 SSE 流式响应中的中文内容前端用TextDecoder解码时一定要带上utf-8参数而且要用{ stream: true }模式否则多字节的中文字符可能恰好在数据块边界处被截断解码出来就是乱码。后端也有一个隐蔽的坑FastAPI 在返回 JSON 时默认会做 Unicode 转义ensure_ascii默认是True导致返回的 JSON 里中文全变成\uXXXX格式。虽然前端JSON.parse能正常解析回来但在浏览器 Network 面板里看不到任何可读的中文排查问题时会特别痛苦。我习惯在后端统一设置JSONResponse的ensure_asciiFalse或者像第一节代码里那样在json.dumps时带上ensure_asciiFalse。5.3 上下文太长与请求超时大模型的输入长度是有上限的比如有些模型支持 8K 上下文有些支持 32K。当对话轮数多了或者文本块太长把历史消息全部塞进上下文很容易超出限制表现就是接口直接报 400 错误。这个问题有几种处理思路。最简单的是超了就直接报错然后把对话清空重新开始但这个方案体验太差。好一点的方案是做消息压缩当历史消息超过一定长度后把最早的几条消息做摘要用摘要替换原文或者直接丢弃最早的消息只保留最近的几轮。这个方案我现在还在用实现成本不高只是要注意摘要会丢失一些细节。还有一种情况需要专门处理前端请求超时。本地部署的模型生成速度本来就慢如果模型比较大一个长答案可能要生成一两分钟而很多 HTTP 客户端和服务端的默认超时时间是 60 秒。我的建议是前端把请求超时时间调到一个比较宽裕的值比如 120 秒同时实现一个心跳机制也就是每收到一个数据块就重置超时计时器确保长时间生成过程中连接不会被误杀。5.4 本地模型与云端 API 的差异开发阶段我强烈建议用本地模型但在最后上线之前一定要用云端 API 做一轮完整的回归测试。本地模型和云端 API 的差异比赛犬和家犬的差距还大不管是从响应速度、回答质量还是可用性上说。本地模型首字延迟普遍偏高因为模型推理需要先把整个模型加载到显存里。如果用 CPU 跑慢得简直难以忍受。云端 API 在这方面做了很多优化首字延迟通常能控制在 1 秒左右。但云端 API 也不是没有坑它的限流策略、并发控制、内容安全审查都会在某个意想不到的时候给你添堵。线上环境一定要做失败重试和降级策略比如用一个轻量本地模型作为兜底云端 API 不可用的时候自动切换过去。质量差异方面同一个问题云端的大模型回答得通常更有条理本地的小参数模型则会时不时冒出一些常识性错误。最好的策略是开发时本地调逻辑上线前线上跑真模型两边的差异心里有数上线后才不会慌。6. 再往前一步从对话助手到 Agent如果你的问答助手已经稳定跑起来了恭喜你完成了 AI 应用开发的第一阶段。下一步自然要考虑的是如何让助手不只是“回答问题”而是能“做事情”这就进入了 Agent 的范畴。Agent 这个词最近被炒得很热但剥开外壳看核心无非就是三件事调用工具、管理记忆、自主规划。6.1 工具调用让模型不只是会说还能“做”工具调用Function Calling是 Agent 能力的关键它让大模型不再局限于生成文本而是可以在生成过程中触发一些预设的外部操作。最经典的例子是用户问“帮我查一下昨天订单的退款进度”大模型识别出这是一个查询动作后会调用一个query_refund_status(order_id)的函数拿到结果后再把结果组织成自然语言回复用户。工程上的实现方式其实很简单。后端定义好一串函数描述包括函数名、参数含义、返回结构在调用大模型时把函数描述一起传过去让模型决定要不要调用、参数填什么。模型返回的结果分为两种情况如果是普通文本直接推给前端展示如果是一个函数调用指令后端就执行这个函数把执行结果回传给模型让模型基于结果再生成最终回复。这个“模型决策—函数执行—结果回收”的循环就是 Agent 的基本运行逻辑。从前端视角来看工具调用的交互设计特别有发挥空间。比如模型调用了一个查询函数前端可以在等待结果时展示一个 Loading 状态或者一个小的操作日志面板把“正在查询部门考勤数据…”这样的过程展示给用户让用户感觉Agent真的在干活而不是卡住了。这种过程透明的设计对用户信任感的建立帮助非常大。6.2 多轮记忆与上下文管理的进阶玩法Agent 的记忆管理是一个很让前端头疼的问题。每一轮对话都包含用户提问、模型回复、工具调用结果这几类信息如果全部留存在上下文中Token 消耗和上下文长度会迅速膨胀。我的经验是给消息打上标签分清哪些是用户消息、模型消息、工具消息、系统消息在组织上下文时按标签分类添加。工具调用结果一般留最新一轮的就够了历史会话的详细内容不必每次都传给模型。更进阶一点的做法是做一个独立的总结线程。对话进行一定轮数后启动一个小模型把目前为止的对话要点总结成 100 字左右的摘要替代之前全部的对话历史。这是一个典型的用空间换时间、用计算换带宽的策略实测对长会话的稳定性提升非常明显。到了这个阶段你会发现所谓 Agent 开发底层依然是前端工程思维——消息路由、状态管理、异常捕获、生命周期管理只是数据来源从“数据库”换成了“大模型 工具返回结果”。之前做前端积累的架构能力在这里不是没用反而是最稀缺的竞争力。我做 AI 应用开发这段时间最大的感受倒不是技术上的而是心态上的。很多前端同事聊起 AI 都会说“这东西太深了我等会儿再看看”然后就没有然后了。但事实上把 AI 能力接进产品跟当年把地图 SDK 接进 H5 页面没有本质区别——都是调用一个黑盒能力把返回结果做成好用的用户界面。你不需要读懂 Transformer 论文才能写 AI 应用你只需要把一个最小的闭环跑通然后把体验打磨到别人愿意天天用。先跑通再优化这是我自己一直信奉的开发法则同时也是前端这个职业在 AI 浪潮里不会掉队的底气所在。