news 2026/7/16 3:44:57

JavaScript调用SmallThinker-3B-Preview模型API实现浏览器端智能问答

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript调用SmallThinker-3B-Preview模型API实现浏览器端智能问答

JavaScript调用SmallThinker-3B-Preview模型API实现浏览器端智能问答

最近在捣鼓一些前端AI应用,发现一个挺有意思的需求:能不能直接在浏览器里,用JavaScript调用大模型API,做一个完全跑在前端的智能问答应用?这样既不用操心后端服务器的搭建和维护,又能让用户获得更即时的交互体验。

听起来不错,但实际操作起来,你会发现几个绕不开的坎儿:首先是跨域问题,浏览器的安全策略不允许你随便请求不同源的API;其次是API密钥这种敏感信息,直接写在前端代码里等于“裸奔”;最后是网络延迟,如果模型响应慢,用户对着一个卡死的界面可没什么耐心。

这篇文章,我就想和你聊聊怎么用JavaScript,特别是结合SmallThinker-3B-Preview这样的轻量模型API,来搞定一个浏览器端的智能问答Demo。我们会重点解决上面提到的几个核心问题,让你看到,即使没有后端,前端也能玩转AI。

1. 为什么要在浏览器里直接调用模型API?

你可能要问,现在不都是后端调用API,前端只负责展示吗?干嘛要折腾这个?其实,在某些场景下,前端直连API有它独特的优势。

想象一下,你要做一个简单的工具类网站,比如一个代码片段解释器,或者一个快速翻译小工具。用户打开网页,输入问题,立刻就能得到AI的回复。整个过程轻快、直接,用户不需要等待后端服务器的响应中转。对于SmallThinker-3B-Preview这类设计上就比较注重效率和响应速度的模型来说,这种模式尤其合适。

当然,最大的好处是简化架构。你不需要去维护一个专门用于转发API请求的后端服务,省去了服务器成本、部署和运维的麻烦。整个应用就是一些静态文件,可以轻松地托管在GitHub Pages、Vercel或任何静态网站托管服务上。

但硬币都有两面。这么做的挑战也很明显:

  1. API密钥暴露风险:这是最要命的一点。把密钥放在前端JavaScript里,任何人查看网页源代码都能看到。
  2. 跨域限制:大多数模型API服务商的后端地址,和你前端网页的域名是不同的,浏览器默认会阻止这种请求。
  3. 用户体验:模型生成文本需要时间,如果是长回答,用户会看着空白页面干等,体验很差。
  4. 功能限制:一些复杂的预处理、后处理或者需要访问数据库的操作,纯前端就无能为力了。

所以,我们今天的实践,更适合那些对安全性要求不是极端苛刻、追求快速原型验证或打造轻量级工具的场景。接下来,我们就看看怎么一步步解决这些问题。

2. 核心挑战与应对思路

在动手写代码之前,我们得先把路子想清楚,尤其是怎么跨过“跨域”和“安全”这两座大山。

2.1 跨域问题:绕不开的CORS

当你从https://your-app.com的网页上,用JavaScript去请求https://api.model-provider.com的接口时,浏览器会说不。这就是“同源策略”在起作用,目的是防止恶意网站窃取数据。

模型API的服务器通常会在响应头里设置Access-Control-Allow-Origin,用来指定允许哪些来源的网页来访问它。如果这个头部的值不是你的网页来源(或者是*),请求就会被浏览器拦截。

怎么办?对于我们自己无法控制的第三方API,一个常见的变通方案是使用一个CORS代理。这个代理服务器位于你的前端和模型API之间,它负责转发请求,并在响应中添加允许所有源(*)的CORS头部,从而“骗”过浏览器。网上有一些公开的CORS代理服务,但对于生产环境,尤其是涉及API密钥时,使用公开代理非常不安全。更稳妥的做法是,为了演示和学习,我们可以假设API服务端已经正确配置了CORS,或者我们有一个自己搭建的、安全的代理。在本文的示例中,我们将重点放在前端逻辑的实现上。

2.2 安全地管理API密钥

把密钥写在const apiKey = 'sk-xxxx'这样的代码里,然后提交到GitHub,是初级开发者常犯的错误。一旦泄露,别人就可以用你的密钥疯狂调用API,产生巨额费用。

前端如何相对安全地处理?

  1. 环境变量(针对构建时):如果使用Webpack、Vite等构建工具,可以在项目根目录创建.env文件,将密钥存入类似VITE_API_KEY=sk-xxx的变量中。在代码中通过import.meta.env.VITE_API_KEY来引用。这个文件必须被.gitignore忽略。但请注意,这只是在构建阶段将值注入到代码中,最终打包后的代码里,这个值仍然是明文可见的。
  2. 后端中转(最安全):真正安全的方式还是需要一个轻量级后端。前端将用户输入发送到自己的后端服务器,由后端服务器携带密钥去调用模型API,再将结果返回前端。这样密钥完全不会暴露给浏览器。这是我们最终推荐的、可用于实际项目的方案。
  3. 临时令牌或代理端点:一些服务可能提供前端专用的、权限受限的临时令牌,或者允许你设置一个允许特定前端域名调用的代理端点。

为了本文的演示完整性,我们将展示一个使用“假设已安全配置”的API端点的前端代码。你必须清楚,在真实项目中,如果没有可靠的后端保护,切勿将重要API密钥用于前端直连。

2.3 优化用户体验:应对延迟

大模型生成文本是逐词(Token)进行的,如果等全部生成完再一次性返回给前端,用户等待时间会很长,且没有任何反馈。

流式响应(Streaming)是解决这个问题的利器。服务器不是一次性返回完整答案,而是以数据流的形式,每生成一小段(一个Token或几个Token)就立刻发送给前端。前端可以实时地将这些片段拼接到页面上,让用户看到答案逐渐“生长”出来的过程,这极大地提升了感知速度和交互体验。

我们将使用fetchAPI 的响应体(response.body)来读取这个流。

3. 手把手搭建前端智能问答应用

理论说得差不多了,我们开始写代码。我们将创建一个非常简单的HTML页面,包含输入框、按钮和显示区域,并用JavaScript实现完整的调用逻辑。

3.1 项目结构与基础HTML

首先,创建一个新的项目文件夹,里面放一个index.html文件。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>前端AI问答助手 - SmallThinker-3B预览版</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; max-width: 800px; margin: 40px auto; } header { text-align: center; margin-bottom: 40px; } h1 { color: #2c3e50; margin-bottom: 10px; } .subtitle { color: #7f8c8d; font-size: 1.1em; } .container { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); } .input-area { margin-bottom: 25px; } textarea { width: 100%; padding: 15px; border: 2px solid #e0e6ed; border-radius: 8px; font-size: 16px; resize: vertical; min-height: 100px; transition: border 0.3s; } textarea:focus { outline: none; border-color: #3498db; } .controls { display: flex; gap: 15px; margin-top: 15px; } button { flex: 1; padding: 14px 20px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; } #submitBtn { background-color: #3498db; color: white; } #submitBtn:hover:not(:disabled) { background-color: #2980b9; } #clearBtn { background-color: #ecf0f1; color: #34495e; } #clearBtn:hover { background-color: #d5dbdb; } button:disabled { opacity: 0.6; cursor: not-allowed; } .response-area { border-top: 1px solid #eee; padding-top: 25px; } .response-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .token-info { font-size: 0.9em; color: #95a5a6; background: #f8f9fa; padding: 5px 10px; border-radius: 4px; } #responseContent { min-height: 200px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; white-space: pre-wrap; /* 保留换行和空格 */ line-height: 1.8; } #responseContent.streaming { border-left: 4px solid #3498db; } .loading { text-align: center; color: #7f8c8d; padding: 20px; } .error { color: #e74c3c; background-color: #fdf0ed; padding: 15px; border-radius: 8px; border-left: 4px solid #e74c3c; margin-top: 20px; } footer { text-align: center; margin-top: 40px; color: #95a5a6; font-size: 0.9em; } </style> </head> <body> <header> <h1>🤖 前端AI问答助手</h1> <p class="subtitle">基于SmallThinker-3B-Preview模型 | 纯浏览器端运行</p> </header> <main class="container"> <section class="input-area"> <h2>向AI提问</h2> <textarea id="userInput" placeholder="请输入你的问题,例如:用JavaScript写一个快速排序函数。或者:解释一下什么是闭包。"></textarea> <div class="controls"> <button id="submitBtn">发送问题</button> <button id="clearBtn">清空对话</button> </div> </section> <section class="response-area"> <div class="response-header"> <h2>AI回复</h2> <div class="token-info">消耗Token: <span id="tokenCount">0</span></div> </div> <div id="responseContent"> <p class="loading">等待你的提问...</p> </div> </section> </main> <footer> <p>本演示仅用于技术展示。实际部署请注意API密钥安全与跨域配置。</p> </footer> <script src="app.js"></script> </body> </html>

3.2 JavaScript核心逻辑实现

接下来,在同一个文件夹创建app.js文件。这里包含了所有的交互和API调用逻辑。

// app.js - 前端智能问答核心逻辑 // 注意:这是一个演示代码。在实际项目中,API_BASE_URL 应指向你自己配置了CORS的后端服务, // 或者使用其他安全方式获取。API_KEY 绝不应该以明文形式出现在前端代码或仓库中。 const API_BASE_URL = 'https://api.example.com/v1'; // 替换为你的实际API端点 const API_KEY = 'YOUR_API_KEY_HERE'; // !!! 警告:此处仅为演示,生产环境必须通过后端中转 !!! // DOM元素 const userInputEl = document.getElementById('userInput'); const submitBtn = document.getElementById('submitBtn'); const clearBtn = document.getElementById('clearBtn'); const responseContentEl = document.getElementById('responseContent'); const tokenCountEl = document.getElementById('tokenCount'); // 状态变量 let isGenerating = false; let currentController = null; // 用于中止fetch请求 /** * 主函数:处理用户提交问题 */ async function handleSubmit() { const question = userInputEl.value.trim(); if (!question) { alert('请输入问题内容。'); return; } if (isGenerating) { alert('正在生成中,请稍候...'); return; } // 更新UI状态 setGeneratingState(true); responseContentEl.innerHTML = `<p class="loading">AI正在思考...</p>`; responseContentEl.classList.add('streaming'); tokenCountEl.textContent = '0'; // 创建AbortController以便可以取消请求 currentController = new AbortController(); try { // 构建请求体,这里模拟了类似OpenAI的ChatCompletion格式 // 你需要根据SmallThinker-3B-Preview模型API的实际文档进行调整 const requestBody = { model: "smallthinker-3b-preview", // 模型名称,根据实际API调整 messages: [ { role: "system", content: "你是一个有帮助的AI助手。" }, { role: "user", content: question } ], stream: true, // 关键:开启流式响应 max_tokens: 500, temperature: 0.7, }; const response = await fetch(`${API_BASE_URL}/chat/completions`, { // 端点路径根据实际API调整 method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify(requestBody), signal: currentController.signal, // 绑定中止信号 }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API请求失败 (${response.status}): ${errorText}`); } if (!response.body) { throw new Error('响应体不可读,可能不支持流式传输。'); } // 处理流式响应 await processStreamResponse(response.body, question); } catch (error) { // 如果是用户主动中止,不显示错误 if (error.name === 'AbortError') { console.log('请求被用户中止。'); responseContentEl.innerHTML += `\n\n[生成已停止]`; } else { console.error('调用API时出错:', error); responseContentEl.innerHTML = `<div class="error"><strong>出错了:</strong>${error.message}</div>`; } } finally { // 恢复UI状态 setGeneratingState(false); responseContentEl.classList.remove('streaming'); currentController = null; } } /** * 处理服务器返回的流式数据 * @param {ReadableStream} stream - 响应流 * @param {string} originalQuestion - 原始问题,用于上下文 */ async function processStreamResponse(stream, originalQuestion) { const reader = stream.getReader(); const decoder = new TextDecoder('utf-8'); let accumulatedText = ''; let accumulatedTokens = 0; // 清空并准备显示区域 responseContentEl.innerHTML = ''; try { while (true) { const { done, value } = await reader.read(); if (done) { console.log('流式响应结束。'); break; } // 解码数据块 const chunk = decoder.decode(value, { stream: true }); // 处理SSE (Server-Sent Events) 格式的数据行 const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.slice(6); // 去掉 'data: ' 前缀 if (dataStr === '[DONE]') { // 流结束标志 console.log('收到结束标志[DONE]。'); return; } try { const parsedData = JSON.parse(dataStr); // 假设API返回格式为 { choices: [{ delta: { content: "..." }, ... }], usage: {...} } const choice = parsedData.choices?.[0]; if (choice?.delta?.content) { const textDelta = choice.delta.content; accumulatedText += textDelta; // 将新内容追加到显示区域 responseContentEl.textContent = accumulatedText; // 滚动到底部,确保用户看到最新内容 responseContentEl.scrollTop = responseContentEl.scrollHeight; } // 更新Token计数(如果API在流中返回usage信息,通常是在最后一条消息) if (parsedData.usage) { accumulatedTokens = parsedData.usage.total_tokens || 0; tokenCountEl.textContent = accumulatedTokens; } } catch (e) { console.warn('解析流数据时出错:', e, '原始数据:', dataStr); } } } } } finally { reader.releaseLock(); // 最终更新一次Token计数(如果流结束时没有包含usage,可能需要额外调用一次非流接口获取) // 这里可以添加一个非流请求来获取最终用量,但为了简化演示,我们假设流内包含了。 if (accumulatedTokens === 0) { // 简单估算:一个中文字符或英文单词大约对应1-2个token const estimatedTokens = Math.ceil(accumulatedText.length * 0.8); tokenCountEl.textContent = estimatedTokens + ' (估算值)'; } } } /** * 设置生成状态,更新按钮和输入框 * @param {boolean} generating - 是否正在生成 */ function setGeneratingState(generating) { isGenerating = generating; submitBtn.disabled = generating; submitBtn.textContent = generating ? '生成中...' : '发送问题'; userInputEl.disabled = generating; } /** * 清空当前对话 */ function handleClear() { if (isGenerating && currentController) { // 如果正在生成,先中止请求 currentController.abort(); } userInputEl.value = ''; responseContentEl.innerHTML = '<p class="loading">等待你的提问...</p>'; responseContentEl.classList.remove('streaming'); tokenCountEl.textContent = '0'; setGeneratingState(false); } /** * 监听回车键提交(Ctrl+Enter 或 Cmd+Enter) */ userInputEl.addEventListener('keydown', (event) => { if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); handleSubmit(); } }); // 绑定按钮事件 submitBtn.addEventListener('click', handleSubmit); clearBtn.addEventListener('click', handleClear); // 初始化提示 console.log('前端AI问答应用已加载。请确保已正确配置API_BASE_URL和API_KEY(仅用于演示)。');

3.3 关键代码解析与安全提醒

上面的代码已经是一个可工作的Demo了,但有几点需要特别强调:

  1. API端点与格式:代码中的API_BASE_URL、请求路径/chat/completions以及请求/响应体的结构(如messages数组、stream: truedata:前缀)都是基于类似OpenAI的API格式假设的。你必须根据 SmallThinker-3B-Preview 模型提供方的官方API文档进行修改。这是让代码跑通的第一步。
  2. 密钥管理(再次警告)API_KEY变量在演示代码中是硬编码的,这是极其危险的做法。一旦这个页面被部署到线上,任何人都可以通过浏览器开发者工具查看网络请求或直接查看源代码来窃取你的密钥。请务必使用前面提到的后端中转方案。
  3. 流式处理processStreamResponse函数是体验优化的核心。它逐块读取服务器推送的数据,并实时更新页面。注意处理[DONE]标志和可能的解析错误。
  4. 用户交互:我们添加了“生成中”的状态提示、按钮禁用、以及使用AbortController来支持用户中途停止生成,这些都是提升体验的重要细节。

4. 更进一步:优化与扩展思路

一个基础的Demo跑起来后,我们可以考虑如何让它更实用、更健壮。

1. 对话历史管理现在的代码是“单轮”问答。你可以修改messages数组,在每次用户提问时,不仅包含当前问题,还把之前所有的对话记录(用户问题和AI回复)都附加上去。这样模型就能理解上下文,实现真正的多轮对话。需要注意管理Token总数,避免超出模型限制。

2. 更优雅的后端集成(解决安全与跨域)创建一个非常简单的后端服务(例如用Node.js + Express,Python + Flask等),只需一个端点:

  • 前端:POST/api/chat, body里只包含{ "message": "用户问题" }
  • 后端:接收请求,用自己的安全方式(环境变量)加载API密钥,然后按照模型API的格式构造请求,调用模型API,并将流式响应原样转发给前端。 这样,前端代码里就彻底没有了密钥,跨域问题也可以通过配置后端服务(使用cors中间件)只允许自己的前端域名来访问而解决。

3. 前端性能与体验

  • 本地存储:使用localStorage保存最近的对话记录,即使刷新页面也不会丢失。
  • 生成动画:在流式输出时,可以添加一个闪烁的光标动画,让“正在打字”的感觉更真实。
  • 错误重试:网络不稳定时,可以给用户提供“重试”按钮。
  • Markdown渲染:如果模型返回的是Markdown格式的文本,前端可以使用像marked.js这样的库来渲染,让回复更美观。

4. 适配不同的模型APISmallThinker-3B-Preview的API可能和示例不同。核心是仔细阅读其文档,调整:

  • 请求头(Authorization格式可能不同)。
  • 请求体结构(参数名、消息格式)。
  • 响应流解析逻辑(数据格式、结束标志)。

5. 写在最后

通过这个项目,我们看到了在前端直接与AI模型API交互的可行性。它最大的魅力在于极致的轻量化快速的交互反馈,特别适合做技术原型、内部工具或者对实时性要求高的小应用。

当然,我们也反复提到了它的“阿喀琉斯之踵”——安全性。对于任何涉及敏感密钥或处理重要数据的场景,引入一个轻量级后端做中转是必须的。这个后端可以非常简单,只做代理和密钥管理,但它是保障应用安全的关键一环。

流式响应技术的应用,彻底改变了AI交互的体验,从“等待-响应”模式变成了“实时对话”模式。这对于保持用户注意力、提升满意度非常有帮助。

如果你正在构思一个AI功能,不妨先试试用这种纯前端的方式快速搭个Demo出来,验证想法和效果。等核心逻辑跑通后,再根据实际需要,决定是否引入后端、如何部署以及怎样优化。希望这篇文章能给你带来一些动手的灵感。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

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

vLLM部署千问72B大模型实战:从Docker镜像到API调用的完整避坑指南

vLLM实战&#xff1a;千问72B大模型高效部署与API服务优化指南 在人工智能技术快速迭代的今天&#xff0c;百亿参数级别的大模型已成为企业智能化转型的核心竞争力。如何高效部署这些"庞然大物"&#xff0c;使其在实际业务中发挥价值&#xff0c;是每个技术团队面临的…

作者头像 李华
网站建设 2026/7/14 13:53:53

all-MiniLM-L6-v2效率提升:用这个小模型让搜索响应快3倍

all-MiniLM-L6-v2效率提升&#xff1a;用这个小模型让搜索响应快3倍 1. 为什么需要轻量级嵌入模型 在构建现代搜索和推荐系统时&#xff0c;文本嵌入模型扮演着核心角色。传统的大型语言模型虽然性能出色&#xff0c;但在实际生产环境中常常面临三大挑战&#xff1a; 响应延…

作者头像 李华
网站建设 2026/7/14 13:54:12

零基础也能玩转!丹青识画AI艺术系统快速上手指南

零基础也能玩转&#xff01;丹青识画AI艺术系统快速上手指南 1. 开篇&#xff1a;当科技遇见东方美学 想象一下&#xff0c;你随手拍的一张照片&#xff0c;转眼间就变成了一幅配有优雅书法题跋的艺术作品。这不是魔法&#xff0c;而是"丹青识画"AI艺术系统带来的神…

作者头像 李华
网站建设 2026/7/14 13:54:10

多臂机器人协同控制技术:从原理到实践的全栈开发指南

多臂机器人协同控制技术&#xff1a;从原理到实践的全栈开发指南 【免费下载链接】lerobot &#x1f917; LeRobot: State-of-the-art Machine Learning for Real-World Robotics in Pytorch 项目地址: https://gitcode.com/GitHub_Trending/le/lerobot 引言&#xff1a…

作者头像 李华