资讯动态

用FastAPI与原生JS为网站打造AI对话助手:流式响应实战

发布时间:2026/10/16 13:32:55 来源:尧图企业网站定制
1. 项目概述与核心思路1.1 这个项目到底要解决什么问题我最近在给个人网站加AI对话功能先说一个背景过去两年里各种大模型API的价格一降再降调用延迟也从原来的“转圈等半天”变成了现在的“首字几百毫秒”这直接催生了一个新需求——能不能让网站本身就带一个能聊天的助手而不是让用户每次都要跳转到独立平台去提问这个项目“给web增加简单的ai对话功能”核心目标就是在不引入复杂架构的前提下给普通网页塞进一个可交互的AI助手。它适合三类人个人博客站长想给访客提供内容问答、企业官网想要一个产品咨询机器人、以及正在做毕设或Demo的学生。我选择的实现路径是前端用原生JavaScript写浮动聊天窗口后端用Python的FastAPI写一个轻量代理服务最终接入大模型API完成对话。1.2 整体技术方案选型为什么是“前端浮动窗口 后端代理”先说结论我见过很多人在这一步就开始纠结框架其实完全没必要。对于“简单”二字定位的项目最稳妥的结构就是三件套——前端聊天UI、后端API代理、大模型接口。前端为什么不用那些现成的组件库因为我踩过一次坑。某次图省事引入了一个重型聊天组件光是CSS就拖慢了整个首屏渲染200多毫秒而且移动端适配一堆问题。原生JavaScript写浮动聊天窗口本质上就是一个固定定位的div、一个消息列表、一个输入框、三个按钮的事代码量不超过200行却能保证零依赖、零构建、零兼容性焦虑。后端选择FastAPI而不是Flask或Node.js理由有三个第一FastAPI自带异步支持调用大模型接口时不会阻塞事件循环第二它的数据校验用Pydantic定义请求体非常省事第三如果你后续想让这个对话功能接入更多业务逻辑比如查数据库、调内部接口Python写起来比Node.js更顺手。当然如果你本来就熟Node用Express重写一个代理也完全可行核心逻辑是通用的。大模型API的选择我使用的是字节跳动旗下豆包的大模型API。选它的原因很现实价格便宜、国内访问稳定、API文档清晰而且它的ChatCompletion接口与OpenAI的调用方式高度类似以后想换别的模型服务改动成本很小。这里多说一句如果你所在团队已经有其他可用的大模型API参数照着调就行核心思路完全一致。这个“前端浮动窗口 后端代理”的结构还有一层隐藏好处你的API密钥不会暴露给浏览器端。如果有人直接在前端请求大模型接口密钥就裸奔在Network面板里了那基本等于给羊毛党送温暖。加一层代理以后密钥安全、接口限流、日志记录这些都能在后端统一处理。2. 核心功能详解与前置准备2.1 功能范围界定哪些能力必须要有哪些可以砍掉做任何一个功能第一步不是写代码而是想清楚“最小可用版本”长什么样。我给自己定义的需求清单是这样的必须有的能力网页右下角浮动的聊天气泡点击展开/收起用户输入文本AI返回流式回复对话历史保存在当前页面内存中关闭页面自动清空接入API密钥支持基础对话可以砍掉的能力多轮记忆持久化存数据库——首版不做页面刷新就重新开始用户身份区分——首版不分所有访客共享一个会话上下文富文本渲染Markdown转HTML——首版支持基础的换行处理即可语音输入、图片识别、情绪分析——一律砍掉这个砍法的逻辑很简单首版跑通闭环、验证价值后续按反馈迭代。一上来就搞Redis存历史、搞用户体系的大概率会在还没看到成果之前就被复杂度压垮。2.2 后端接口设计一个代理服务该有的样子后端其实只做三件事接收前端请求、转发给大模型API、把流式响应回传给前端。听起来简单但接口设计有几个细节值得注意。我的请求接口设计为POST /api/chat请求体结构如下{ messages: [ {role: system, content: 你是网站助手回答简洁友好。}, {role: user, content: 你好这个网站是做什么的} ], stream: true }这里有个前后端配合的关键点messages列表是前端自己维护的每次请求都带上完整对话历史。后端不存状态做一个无状态转发。这样设计的好处是后端逻辑极简、天然支持横向扩展坏处是请求体积会随对话轮数线性增长但对普通网站的场景来说完全够用。响应格式我选择流式stream: true也就是SSEServer-Sent Events风格。大模型平台普遍是逐字返回内容的如果等全部生成完再一次性返回用户看到的是一个光标转圈体验非常糟糕流式返回则能让用户看到文字一个字一个字蹦出来整个交互的“有生命感”就出来了。接口的响应头需要设置X-Accel-Buffering: no这是给Nginx看的防止它在反代时缓冲整个响应导致流式失效。这一行是我当初排查了半天才发现的强烈建议直接写进代码注释里。2.3 前端交互设计不要小看这个浮动窗口浮动聊天窗口看起来简单但有几个交互细节不做就很容易显得业余第一个是聊天气泡的入场动画。很多人忽略了这个细节导致AI说话时文字是瞬间全部出现的毫无“正在思考后陆续表达”的感觉。我的实现是收到流式数据时先把内容追加到一个缓冲变量里然后通过CSS的transition或者每次插入一两个字符的定时器来实现打字机效果。实测下来每帧插入10-20个字符的速率比较自然太快没有质感太慢让用户着急。第二个是输入区和消息区的联动。用户发送消息后输入框要立即清空并保持聚焦收到回复期间输入框要禁用并显示“正在输入…”的状态提示消息列表要自动滚动到底部确保用户始终看到最新内容。这几个状态用三个布尔变量就能控制但漏掉任何一个交互就显得生硬。第三个是移动端适配。这个很关键因为现在一半以上的访客都在手机上浏览。我的做法是在屏幕宽度小于768px时让聊天窗口全屏显示而不是右下角小窗。全屏模式下发送按钮要足够大输入框要避开键盘弹起消息气泡的圆角也要调整。2.4 开发环境与依赖清单关于环境准备如果你的机器上已经有Python 3.9那就只需要装几个依赖pip install fastapi uvicorn httpx pydanticfastapiWeb框架uvicornASGI服务器用于启动应用httpx异步HTTP客户端用来转发请求到模型APIpydantic数据校验FastAPI依赖它其实不用单独装前端不需要安装任何第三方库原生JavaScript就够了。整个项目的文件结构非常简单web_ai_chat/ ├── main.py ├── static/ │ ├── chat.js │ └── chat.css └── templates/ └── index.html3. 实操演示从零搭建完整流程3.1 后端代理服务完整代码拆解我直接贴一个能用的main.py然后逐段解释为什么这么写。import os from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import httpx app FastAPI() app.mount(/static, StaticFiles(directorystatic), namestatic) # 这里填入你自己的API密钥也可以从环境变量读取 API_KEY os.getenv(AI_API_KEY, 你的密钥) API_URL https://ark.cn-beijing.volces.com/api/v3/chat/completions MODEL_NAME doubao-lite-4k-250615 class ChatMessage(BaseModel): role: str content: str class ChatRequest(BaseModel): messages: list[ChatMessage] stream: bool True app.post(/api/chat) async def chat(req: ChatRequest): payload { model: MODEL_NAME, messages: [m.dict() for m in req.messages], stream: req.stream, temperature: 0.7 } headers { Authorization: fBearer {API_KEY}, Content-Type: application/json } if not req.stream: async with httpx.AsyncClient(timeout30) as client: resp await client.post(API_URL, jsonpayload, headersheaders) return JSONResponse(resp.json()) async def generate(): async with httpx.AsyncClient(timeout30) as client: async with client.stream(POST, API_URL, jsonpayload, headersheaders) as resp: if resp.status_code ! 200: error_body await resp.aread() yield fdata: {error_body.decode()}\n\n return async for line in resp.aiter_lines(): if line.startswith(data:): yield line \n\n return StreamingResponse(generate(), media_typetext/event-stream, headers{X-Accel-Buffering: no})代码核心逻辑就三层第一层ChatRequest用Pydantic定义了请求体结构FastAPI会自动校验前端传错字段会直接返回422错误省去了手写if判断的麻烦。第二层流式转发的核心在generate协程里。我用了httpx的client.stream模式这会建立一个长连接逐行读取大模型返回的数据然后原样转发给浏览器。注意这里的逐行读取大模型服务端的SSE格式就是一行一行的data: {...}所以我们的代理不需要解析内容直接透传就行。第三层错误处理。如果模型服务返回非200状态码说明可能是密钥失效、欠费、超量我们需要把这个错误信息输出给前端。这里有一点和直觉相悖错误信息也要走SSE格式前端才能在stream模式下收到并展示。3.2 前端聊天窗口完整代码拆解前端部分我拆成HTML结构、JavaScript逻辑、CSS样式三块来讲。HTML结构用最简单的元素层级div idchat-widget classchat-widget div idchat-bubble classchat-bubble?/div div idchat-panel classchat-panel hidden div classchat-header网站助手/div div idchat-messages classchat-messages/div div classchat-input-area input idchat-input typetext placeholder向我提问... button idchat-send发送/button /div /div /divJavaScript部分的核心是fetch配合ReadableStream来读取SSE数据。这里有个常见的坑很多人不知道fetch默认只处理JSON对SSE流式响应需要手动读取流。我用的是response.body.getReader()外加TextDecoder来解码遍历每个chunk并提取data:后面的内容。下面是核心的发送逻辑async function sendMessage() { const input document.getElementById(chat-input); const text input.value.trim(); if (!text) return; // 追加用户消息到界面上 appendMessage(user, text); input.value ; setLoading(true); // 构造带历史的消息序列 const messages [...history, {role: user, content: text}]; try { const resp await fetch(/api/chat, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({messages, stream: true}) }); const reader resp.body.getReader(); const decoder new TextDecoder(); let aiText ; // 创建一个空的助手消息气泡然后持续往里填充 appendMessage(assistant, ); const aiBubble chatMessages.lastElementChild; while (true) { const {done, value} await reader.read(); if (done) break; aiText decoder.decode(value, {stream: true}); aiBubble.textContent aiText; chatMessages.scrollTop chatMessages.scrollHeight; } history [...messages, {role: assistant, content: aiText}]; } catch (e) { appendMessage(assistant, 请求失败请稍后重试。); } setLoading(false); }这段代码要解释两个细节一是历史记录history保存在内存数组中每次发消息时把整个人机对话序列传给后端。前端就是本轮的“记忆仓库”。二是aiBubble.textContent aiText直接在整个AI回复过程中持续更新这同一个节点。这在流式场景下性能足够因为文本渲染的DOM操作远小于图片渲染的成本。但如果你追求打字机特效可以把赋值改成定时器分段追加原理都是一样的。CSS样式方面浮动窗口的核心就两条定位和层级。position: fixed加bottom: 20px; right: 20px再加一个很高的z-index确保它在所有页面内容之上。展开状态下窗口尺寸设成360px x 500px左右移动端全屏模式另写一组媒体查询。另外建议给聊天气泡和输入框加上box-shadow和border-radius这是让整个组件看起来“不廉价”的最低成本手段。3.3 接入网站的三种方式代码写完之后怎么把它塞进你的网站里有三种方式我按推荐程度排个序第一种是直接静态引入适用于简单页面。在你的HTML的/body之前加上link relstylesheet href/static/chat.css div idchat-root/div script src/static/chat.js/scriptchat.js里用createElement动态生成聊天组件所有DOM这样接入任何页面都只需要这三行。第二种是作为异步组件加载适用于已有项目、不想影响主页面渲染的情况。把chat.js改造成一个可以动态插入的模块页面加载完成后再调用initChatWidget()。第三种是嵌入到SPA框架中适合React/Vue项目。这种场景下我不建议直接塞原生DOM因为框架的虚拟DOM管理和原始DOM操作容易冲突。建议把上面的JS逻辑封装成一个React组件或Vue组件本质思路不变只改外层封装。3.4 参数配置与调优经验参数层面有三个地方值得你手动调一调temperature温度这个控制回答的随机性。我默认设成0.7适合闲聊和泛知识问答。如果你的网站是技术文档站想让回复更严谨调到0.3以下如果是创意文案站可以到0.9甚至1.0。有一个简单规律需要事实准确性时往低调需要发散性时往高调。max_tokens最大生成长度这个决定单次回答的最长token数。普通对话建议设成800避免回答过长让用户失去耐心如果你的网站是写攻略、写长文那种场景可以设到1500以上。我通常在代理层设一个默认值把真实可配能力留给前端传参。模型版本的选择豆包API提供了多个规格的模型。我当前用的这个版本主打轻量快速适合对话场景如果你需要更高质量的深度推理可以换更强的模型系列价格相应也更高。作为网站助手轻量版其实性价比极高响应速度和成本都跑得很漂亮。4. 常见问题排查与体验优化4.1 我在实际部署中踩过的四个坑第一个坑是流式响应失效。现象是AI生成完所有内容后前端才一次性全部显示。原因基本出在Nginx或Cloudflare这类代理层在缓冲响应。解决方法是后端代码里加X-Accel-Buffering: no响应头Nginx还支持在location块里配置proxy_buffering off;。我在FastAPI里两个都写了双保险。第二个坑是跨域问题。如果你的前端域名和后端API域名不同浏览器会拦截请求。我遇到过一次前端页面是www.example.com后端接口在api.example.com直接请求就报CORS错误。解决方法是FastAPI里加CORSMiddleware中间件允许指定域名跨域访问。from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[https://www.example.com], allow_methods[POST], allow_headers[*], )第三个坑是密钥安全。我之前见过有人把密钥直接写在前端代码里被搜索引擎爬虫抓取后一天内就被盗刷上千次。密钥一定要留在后端前端永远只接触经过代理的接口。这是红线。第四个坑是长对话性能劣化。当对话轮数增加到十几轮之后请求体里的完整历史会让每次请求体积变得很大响应速度明显变慢。我的处理是在前端设置一个上限超过十轮对话后自动只保留最近六轮消息发送给后端。你可以根据自己的场景调整这个策略。4.2 用户体验优化的小技巧功能能跑通只是第一步离“好用”还差一些锦上添花的细节。第一加一个开场白。默认找到浮窗的用户可能不知道怎么提问AI助手在前端初始化后自动发出一条“你好我是网站助手有什么可以帮你的吗”能显著降低使用门槛这个对话会出现在消息列表顶部但记录在history之外。第二实现Markdown渲染。目前的实现是纯文本AI回答里的代码块、列表会显示得很痛苦。想优化的话可以引入一个轻量Markdown解析库比如marked或markdown-it。我首版没做是因为不想引入额外依赖但如果你的网站是技术类博客这个建议优先级很高。第三增加停止生成按钮。流式输出时用户可能觉得信息已经够了想停止。实现方式是调用AbortController中断fetch请求同时后端断开流。很少有人会频繁用到这个功能但一旦用户想用却没有体验就很差。第四输入框支持Enter发送、ShiftEnter换行。这是聊天输入的基础交互别忘了实现。4.3 基于实际使用的进一步扩展方向简单的对话功能跑通之后如果你想继续深化有几个方向性价比很高第一个方向是接入网站内容数据。给系统提示词加上网站的关键信息描述甚至把业务数据塞进提示词让AI能回答“这个产品多少钱”“你们的售后政策是什么”。这是很多企业网站的刚需。第二个方向是增加聊天气泡的个性化状态。比如AI回复时显示“正在输入”的动效、消息已读状态、系统通知提示等。这些能提升整个页面品质感但别做得太闹腾。第三个方向是完善日志与统计。后端的接口已经天然拿到了用户提问的日志你可以记录用户问了什么、AI回答了多久、是否有错误。这对业务方了解访客痛点非常有价值。我自己的网站跑了一周之后从日志里发现用户问得最多的是“你的联系方式是什么”于是我把联系信息加到系统提示词里问题当场就解决了。5. 部署与工程化建议5.1 服务器部署的最小步骤这个项目部署到服务器上不需要复杂工具链。我这里以最常见的方式为例假设你手上有一台Linux服务器域名和HTTPS证书已经配好。第一步把代码上传到服务器创建虚拟环境并安装依赖python3 -m venv venv source venv/bin/activate pip install fastapi uvicorn httpx第二步用uvicorn启动服务。为了方便后台运行我用systemd管理一个常驻进程配置文件指向项目目录启动命令是uvicorn main:app --host 0.0.0.0 --port 8000第三步配置Nginx反向代理把/api/chat和/static路径都代理到8000端口。注意加流式相关的配置proxy_http_version 1.1和proxy_buffering off在SSE场景下至关重要。这一步完成之后你的AI对话功能就正式上线了。因为整个项目没有数据库、没有消息队列、没有缓存服务所以运维负担极低理论上可以跑很久不用管。5.2 性能与成本控制这几条建议不仅省钱还能让系统稳定很多给后端接口加一个简单的限流。如果接口裸奔出去很可能被刷。最简单的实现是内存滑动窗口限流每IP每60秒最多20次请求。代码量很小但能挡住绝大多数滥用。在流式响应的代码里给整个请求设置一个超时时间。我用30秒因为大模型的SDK通常也在30秒左右返回超过这个时间大概率是模型卡住了断开总比死等强。大模型API按token计费但豆包这类国内产品的收费其实很低。即使一个用户每天聊50轮单个用户成本也基本可以忽略不计。不过要注意的是不要把超长文、多轮历史一股脑全发给模型那才是成本失控的根源。5.3 代码版本管理与后续迭代建议我很推荐把这个项目单独建一个仓库因为它的边界非常清晰一个前端静态文件加一个后端路由。后续迭代时每次改动都限制在很小的范围里回归成本也很低。如果要在此基础上加认证能力比如必须登录才能用、加管理后台、加更多AI功能如摘要生成、文章改写我的建议是不要在这个仓库里无限堆功能而是把它当作一个可复用的“AI接入层”其他模块按需调用。保持它的简单本身就是一种架构上的优势。6. 个人实操心得与踩坑总结这一节写点代码之外的话。我在做这个项目的过程中最大的体会就是“简单”二字比想象中难得多。你以为不引入框架、不搞数据库、不做构建工具就很朴素了但真正做起来你会发现每个小的体验问题都会诱惑你加复杂度。比如我做到了第三版很自然地想把对话记录存进数据库、想做用户系统、想在后台配置多套提示词。我全部忍住了因为我知道这套功能的定位就是“简单AI对话”如果升级成了“AI客服平台”那这个项目就不该由我一个人业余完成了。如果你也想做类似的东西我建议严格按我的思路来第一步用半天跑通前后端联调第二步部署上线让真实用户用第三步才根据日志来看要加什么功能。多数情况下你会发现真实用户的需求和你的脑补完全不一样。比如我就完全没预料到用户对“流式打字效果”的在意程度远高于“回答准确性”。站在用户角度想一个会逐字蹦字的机器人看起来就是比回答问题一板一眼的机器人更“聪明”。关于接入大模型API我也想多说一句流程上的事。去模型平台创建API密钥时建议把密钥单独存到一个环境变量文件里避免误传到公开仓库。密钥权限能不开就别开太宽有些平台支持生成只读或限定模型的子密钥对网站场景来说够用了。最后分享一个小细节调试流式接口时别依赖浏览器控制台的Network面板去看响应格式不一定完整。更可靠的方法是命令行直接curl查看流curl -N -X POST http://localhost:8000/api/chat \ -H Content-Type: application/json \ -d {messages:[{role:user,content:你好}],stream:true}-N参数禁用curl的缓冲能在终端里看到逐字输出的效果排查“流式是否生效”问题最直观。这套小项目做下来它带给我的不只是一个挂在网站角落的聊天框更是一整套“如何低成本接入AI能力到存量系统”的通用套路——这个经验放到任何一个Web项目里都能用得上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑