news 2026/10/7 6:02:52

零成本搭建大模型翻译插件:Node.js + 免费API实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零成本搭建大模型翻译插件:Node.js + 免费API实战

网页翻译这件事,说大不大,说小也不小。浏览器自带的翻译要么机翻味重得离谱,要么干脆把代码块和专有名词一起翻掉,读技术文档的时候简直是灾难。商业翻译插件倒是有做得不错的,但免费额度用完之后要么限速要么收费,长期用下来也是一笔开销。我自己的需求其实很朴素:读英文技术文档、看外文博客、偶尔翻几篇论文,翻译质量要过得去,术语别乱翻,最重要的是——别让我每个月为这个掏钱。

折腾了一圈之后我发现,现在国内好几家大模型厂商都提供了免费额度的 API,像智谱、DeepSeek 这些,注册就送一笔 token,日常翻译这点量根本用不完。那为什么不干脆自己搭一个翻译插件,把免费大模型 API 接进去?说干就干,这篇文章就把我整个搭建过程完整记录下来——从选模型、拿 API Key,到用 Node.js 写一个本地翻译服务,再到做成浏览器插件,最后处理各种实际使用中遇到的坑。整个过程零成本,代码量也不大,有一点 JavaScript 基础就能跟着做下来。

1. 为什么我不直接用现成的翻译插件

1.1 商业翻译插件的三个硬伤

先说清楚我为什么要自己造轮子,不然你可能会觉得这是多此一举。市面上主流的网页翻译插件我基本都用过一轮,问题集中在三个地方。

第一个是术语翻译不可控。技术文档里 "token" 这个词,在认证场景下应该翻成"令牌",在计费场景下是"词元",在大模型语境下又可能是"标记"。传统翻译引擎基本是固定译法,翻出来经常驴唇不对马嘴。而大模型的好处是你可以通过 prompt 告诉它"这是技术文档,token 统一译为词元",它就能按你的要求来。

第二个是格式破坏。很多翻译插件是把整个页面的文本节点抽出来翻译再塞回去,遇到代码块、行内代码、链接这些元素,要么翻掉要么搞乱结构。我读 GitHub README 的时候经常遇到代码块里的注释被翻译了,复制出来直接报错。

第三个是隐私和成本。商业插件要么把你的页面内容传到它自己的服务器,要么免费额度用完就限速。自己搭的话,请求直接发给你选的大模型厂商,中间不经过第三方,而且用的是免费额度,心里有底。

1.2 大模型翻译和传统机翻的本质区别

这里得稍微展开说一下,为什么大模型翻译值得折腾。传统统计机器翻译(SMT)和早期的神经机器翻译(NMT)本质上是"句子级"的映射,它看到一个句子,从训练语料里找最可能的对应译法。这种方式在通用场景下还行,但缺乏上下文理解能力。

大模型翻译是"理解后重述"。你给它一段话,它先理解这段话在说什么,然后用目标语言重新表达一遍。这意味着几个实际的好处:一是上下文连贯,代词指代、术语一致性都能保持;二是可以下指令,你可以在 prompt 里规定风格、术语、格式要求;三是能处理长文本,现在主流大模型的上下文窗口动辄 128K 甚至更大,整篇文档丢进去都没问题。

当然代价是速度比传统机翻慢,毕竟要跑推理。但对于读文档这种场景,慢个一两秒完全可以接受,质量提升是实打实的。

1.3 免费 API 额度到底够不够用

这是最关键的问题。我算过一笔账:一篇中等长度的英文技术文档大概 3000 到 5000 个英文单词,换算成 token 大约 4000 到 7000 个。翻译成中文,输出 token 数差不多也是这个量级。也就是说翻译一篇文档消耗大约 1 万到 1.5 万 token。

目前国内几家厂商的免费额度,注册送的基本都是百万 token 级别。按这个算,免费额度够你翻译七八十篇长文档。而且很多厂商的免费模型是长期免费的,不是一次性赠送。日常使用的话,一个月翻个几十篇文档,完全在免费范围内。就算偶尔超了,按量付费的价格也是每百万 token 几块钱的水平,比订阅制插件便宜太多。

提示:不同厂商的免费策略会调整,注册前先看清楚当前的政策。有的免费额度有有效期,有的免费模型有并发限制,这些都会影响实际体验。

2. 选哪家的大模型 API 最划算

2.1 主流免费 API 横向对比

我把当时能拿到的几家免费 API 都试了一遍,下面这张表是我自己的实测感受,不是官方参数,仅供参考。

厂商免费额度模型能力接口兼容性我的评价
智谱注册赠送,有免费模型中文理解强兼容 OpenAI 格式翻译质量稳,推荐首选
DeepSeek注册赠送推理能力强兼容 OpenAI 格式翻译准确,偶尔过度意译
其他国内厂商各有政策参差不齐多数兼容 OpenAI可作为备用

选型的核心逻辑是:优先选接口兼容 OpenAI 格式的。因为这样你的代码只需要改一个 baseURL 和 model 名字就能切换厂商,不用为每家写一套适配。这个兼容性太重要了,后面我会详细讲怎么利用这一点。

2.2 接口兼容 OpenAI 格式意味着什么

OpenAI 的 Chat Completions 接口格式现在基本成了行业事实标准。请求体长这样:

{ "model": "模型名称", "messages": [ {"role": "system", "content": "系统提示词"}, {"role": "user", "content": "用户输入"} ], "temperature": 0.3 }

响应体里取choices[0].message.content就是模型输出。只要厂商兼容这个格式,你的代码里换个baseURL就能无缝切换。我自己的做法是在配置文件里把厂商信息做成一个列表,想换哪家改一行配置就行,非常省事。

2.3 注册拿 Key 的完整流程

以智谱为例,流程大概是:进官网注册账号,完成实名认证(国内厂商基本都要求),进控制台找到 API Keys 页面,创建一个新的 Key,复制保存。这个 Key 就是一串字符,后面调用接口时要带上它做身份验证。

注意:API Key 等同于你的账号密码,绝对不能提交到 Git 仓库或者发到公开地方。我习惯把它放在环境变量里,代码里通过process.env.XXX读取,这样即使代码开源了也不会泄露。

DeepSeek 的流程类似,注册后在控制台创建 API Key。两家都建议先把 Key 存到本地一个安全的地方,因为创建后有些平台只显示一次,关掉页面就看不到了。

3. 用 Node.js 搭一个本地翻译服务

3.1 为什么需要一个中间层

你可能会问:浏览器插件直接调大模型 API 不就行了,为什么要多一层 Node.js 服务?

原因有三个。第一是跨域问题,浏览器插件直接请求第三方 API 经常会遇到 CORS 限制,中间加一层服务就绕过了。第二是Key 安全,如果把 API Key 写在插件代码里,别人解包就能看到,放在本地服务里就安全得多。第三是可以做缓存和批处理,翻译过的内容存下来,下次遇到相同内容直接返回,省额度也省时间。

所以整体架构是:浏览器插件 → 本地 Node.js 服务 → 大模型 API。插件负责抓取页面文本和展示译文,Node.js 服务负责调模型和缓存。

3.2 环境准备:Node.js 安装与版本选择

Node.js 是运行环境,去官网下载 LTS 版本就行。写这篇文章的时候 LTS 是 20.x,建议用 20 或更高版本,因为要用到原生的 fetch API,18 以下还得装额外的库。

安装过程一路下一步就行。装完之后打开终端验证:

node -v npm -v

能打印出版本号就说明装好了。如果你在 Ubuntu 上,用包管理器装可能会遇到版本太旧的问题,建议去 Node.js 官网按官方指引装,或者用 nvm 这种版本管理工具,切换版本方便。

提示:网上有些教程会让你装最新版而不是 LTS 版,但最新版可能还没稳定,生产环境或者日常使用都建议用 LTS。我踩过一次坑,用最新版跑某个依赖直接报错,换回 LTS 就好了。

3.3 初始化项目与安装依赖

新建一个文件夹,进去初始化:

mkdir translator-service cd translator-service npm init -y

然后装两个核心依赖:express用来起 HTTP 服务,dotenv用来读环境变量。

npm install express dotenv

在项目根目录建一个.env文件,写入你的配置:

API_KEY=你的APIKey API_BASE=https://open.bigmodel.cn/api/paas/v4 MODEL=glm-4-flash PORT=3000

这里的API_BASE和MODEL根据你选的厂商填。智谱的 base 地址和模型名去它文档里查,DeepSeek 的也类似。用.env的好处是配置和代码分离,换厂商只改这个文件。

3.4 核心翻译接口的实现

新建server.js,核心逻辑就是接收前端传来的文本,拼好 prompt,调大模型 API,返回结果。关键代码大概是这样:

require('dotenv').config(); const express = require('express'); const app = express(); app.use(express.json()); const cache = new Map(); app.post('/translate', async (req, res) => { const { text, targetLang = '中文' } = req.body; if (!text) return res.status(400).json({ error: 'text is required' }); const cacheKey = `${targetLang}:${text}`; if (cache.has(cacheKey)) { return res.json({ result: cache.get(cacheKey), cached: true }); } const systemPrompt = `你是一个专业翻译引擎。请将用户提供的文本翻译成${targetLang}。 要求: 1. 保持原文的格式和换行 2. 技术术语保留英文原文,首次出现时可在括号内注明中文 3. 代码、变量名、命令不要翻译 4. 只输出译文,不要添加任何解释`; try { const response = await fetch(`${process.env.API_BASE}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.API_KEY}` }, body: JSON.stringify({ model: process.env.MODEL, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: text } ], temperature: 0.3 }) }); const data = await response.json(); const result = data.choices[0].message.content; cache.set(cacheKey, result); res.json({ result, cached: false }); } catch (err) { res.status(500).json({ error: err.message }); } }); app.listen(process.env.PORT || 3000, () => { console.log(`翻译服务已启动,端口 ${process.env.PORT || 3000}`); });

这段代码有几个设计点值得说。temperature设成 0.3 是为了让翻译结果稳定,太高了模型会自由发挥,太低又可能死板。system prompt 里明确要求保留格式和不翻译代码,这是针对技术文档场景的优化。缓存用 Map 实现,简单够用,重启服务会清空,如果要持久化可以换成文件或数据库。

3.5 启动服务并做第一次测试

启动服务:

node server.js

看到"翻译服务已启动"就说明跑起来了。用 curl 测一下:

curl -X POST http://localhost:3000/translate \ -H "Content-Type: application/json" \ -d '{"text":"The quick brown fox jumps over the lazy dog."}'

如果返回了中文译文,说明整条链路通了。这一步很重要,先把服务端调通,再去搞插件,不然出了问题不知道是哪一层的事。

注意:如果报错说找不到 API Key 或者认证失败,先检查.env文件里的 Key 有没有多余空格,再确认 base 地址和模型名对不对。我遇到过 base 地址末尾多了个斜杠导致 404 的情况,这种小细节最容易忽略。

4. 把翻译服务做成浏览器插件

4.1 浏览器插件的最小结构

浏览器插件(以 Chrome 扩展为例)本质上就是几个文件加一个配置文件。最小可用的结构包括:

  • manifest.json:插件的配置文件,声明权限、入口、版本等
  • popup.html:点击插件图标弹出的界面
  • popup.js:弹窗的逻辑
  • content.js:注入到网页里的脚本,负责抓取和替换文本
  • background.js:后台脚本,处理跨域请求等

Manifest V3 是目前的标准,配置里要声明host_permissions允许访问本地服务,还要声明activeTab和scripting权限来操作页面。

4.2 抓取页面文本的正确姿势

抓文本这一步是坑最多的地方。最朴素的做法是document.body.innerText,但这会把整个页面的文本混在一起,丢失结构,翻译完塞回去就乱了。

正确的做法是遍历 DOM 树,只处理文本节点,跳过script、style、code、pre这些标签。核心逻辑:

function collectTextNodes(root) { const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode(node) { const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const tag = parent.tagName.toLowerCase(); if (['script', 'style', 'code', 'pre', 'noscript'].includes(tag)) { return NodeFilter.FILTER_REJECT; } if (!node.textContent.trim()) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); const nodes = []; let n; while (n = walker.nextNode()) nodes.push(n); return nodes; }

用TreeWalker遍历的好处是它原生支持过滤,性能也好。把符合条件的文本节点收集起来,批量发给翻译服务,拿到结果后按顺序写回node.textContent。这样页面结构完全不动,只换文字。

4.3 批量翻译与并发控制

一个页面可能有几百个文本节点,如果每个都单独发一次请求,一是慢,二是容易触发 API 的速率限制。我的做法是把文本节点按长度分组,短的合并成一批一起翻译,用特殊分隔符隔开,翻译完再拆开。

但合并也有风险,模型可能会把分隔符也翻译了,或者打乱顺序。所以更稳妥的做法是控制并发数,比如同时发 5 个请求,用 Promise 池来管理。这样既不会太慢,也不会把 API 打爆。

async function translateBatch(texts, concurrency = 5) { const results = new Array(texts.length); let index = 0; async function worker() { while (index < texts.length) { const i = index++; const res = await fetch('http://localhost:3000/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: texts[i] }) }); const data = await res.json(); results[i] = data.result; } } await Promise.all(Array.from({ length: concurrency }, worker)); return results; }

这个并发池模式很通用,值得记住。它保证同时最多有concurrency个请求在跑,跑完一个补一个,直到所有任务完成。

4.4 处理动态加载的内容

现代网页很多内容是滚动加载或者异步渲染的,你翻译完当前页面,往下滚又出来新内容。这时候可以用MutationObserver监听 DOM 变化,发现新节点就自动翻译。

不过这个功能要谨慎用,因为有些页面会频繁变动,导致翻译请求爆炸。我的做法是加一个开关,默认关闭自动翻译,需要的时候手动点一下"翻译新内容"。另外监听的时候要加防抖,DOM 变动后等个几百毫秒再处理,避免频繁触发。

5. 实际使用中踩过的坑和解决方案

5.1 翻译结果格式错乱

最常见的问题是模型返回的译文里多了 markdown 标记或者解释性文字。比如你让它翻译一句话,它回你"这句话的意思是:……"。解决办法是在 system prompt 里反复强调"只输出译文,不要任何解释",并且把temperature调低。如果还是不行,可以在代码里做后处理,把常见的多余前缀去掉。

另一个格式问题是换行丢失。原文有换行的地方,译文可能变成一整段。这个在 prompt 里要求"保持原文换行"能解决大部分情况,但模型偶尔还是会偷懒。对于段落级的文本,我建议一个段落一个请求,不要合并,这样换行天然就保留了。

5.2 速率限制和超时处理

免费 API 通常有速率限制,比如每分钟多少次请求。并发太高就会收到 429 错误。处理方式是加退避重试:遇到 429 就等一会儿再试,等待时间指数增长。

async function fetchWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const res = await fetch(url, options); if (res.status === 429) { await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); continue; } return res; } throw new Error('重试次数用尽'); }

超时也要处理,fetch 默认没有超时,请求卡住会一直等。可以用AbortController加超时:

const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeout);

5.3 长文本超出上下文限制

大模型有上下文窗口限制,虽然现在动辄 128K,但如果你把一整本书丢进去还是会超。而且就算不超,长文本的翻译质量也会下降,模型容易"忘记"前面的内容。

我的策略是按段落切分,每个段落单独翻译。段落之间本来就有语义边界,分开翻译不会影响连贯性。如果某个段落特别长(比如超过 2000 字),再按句子切。切分的时候注意不要在句子中间断开,按句号、问号、感叹号这些标点切比较安全。

5.4 缓存策略与额度节省

缓存是省额度的关键。我的缓存键是"目标语言 + 原文",这样同一段文本翻译过一次就不会再消耗额度。实际使用中,技术文档里重复的句子、导航栏、页脚这些内容特别多,缓存命中率相当高。

缓存可以做成两级:内存缓存(Map)用于当前会话,文件缓存用于跨会话。文件缓存简单点就用 JSON 文件存,复杂点用 SQLite。我自己的用量不大,内存缓存加定期导出就够了。

提示:缓存要注意失效策略。如果你改了 prompt 或者换了模型,之前的缓存可能就不适用了,这时候要清空缓存重新翻译。我一般会在配置里加个版本号,版本变了就自动清缓存。

6. 进阶优化与扩展思路

6.1 针对不同场景切换 prompt

翻译技术文档和翻译新闻、小说,要求完全不一样。技术文档要术语准确、格式保留,新闻要通顺流畅,小说要保留文风。我的做法是在插件里加一个场景选择器,不同场景用不同的 system prompt。

比如技术文档的 prompt 强调"保留代码和术语",小说的 prompt 强调"保留原文语气和风格,意译优先"。这个切换成本很低,但效果提升明显。

6.2 术语表的接入

如果你经常翻译某个特定领域的文档,可以维护一个术语表,翻译前把术语表塞进 prompt 里,告诉模型这些词必须按指定译法翻译。比如:

术语对照: - token → 词元 - embedding → 嵌入 - fine-tuning → 微调

这个功能对专业文档翻译帮助极大,能保证全文术语一致。术语表可以做成一个 JSON 文件,插件读取后拼进 prompt。

6.3 划词翻译与整页翻译的取舍

整页翻译适合快速浏览,但有时候你只想翻译某一段。划词翻译就是选中文本后弹出译文,适合精读。这两个功能可以共存,整页翻译用批量接口,划词翻译用单条接口,共用同一个后端服务。

实现划词翻译用mouseup事件监听选区,拿到window.getSelection().toString(),发给翻译服务,在选区附近弹个浮层显示结果。注意浮层要能点击关闭,不然会挡住内容。

6.4 多厂商自动切换

前面提到接口兼容 OpenAI 格式的好处,这里就体现出来了。你可以在配置里放多个厂商的信息,代码里做一个简单的故障转移:主厂商请求失败或者超时,自动切到备用厂商。这样即使某家服务临时抽风,你的翻译也不会中断。

const providers = [ { base: '主厂商地址', key: '主Key', model: '主模型' }, { base: '备用地址', key: '备用Key', model: '备用模型' } ]; async function translateWithFallback(text) { for (const p of providers) { try { return await callAPI(p, text); } catch (e) { console.warn(`${p.base} 失败,尝试下一个`); } } throw new Error('所有厂商都失败了'); }

这个模式在实际使用中非常实用,尤其是免费 API 偶尔会有波动的时候。

7. 一些实际使用中的体会

整套东西搭下来,代码量其实不大,核心逻辑加起来也就几百行。真正花时间的是调 prompt 和处理各种边界情况。我自己的使用感受是,大模型翻译在技术文档场景下确实比传统机翻好一大截,尤其是术语处理和格式保留这两块,提升非常明显。

有几个小技巧是我用了一段时间才总结出来的。第一,prompt 里明确说"不要翻译代码"比事后过滤有效得多,模型很听话,你说了它基本就不翻。第二,缓存一定要做,不然重复翻译同样的内容既慢又费额度。第三,并发不要开太高,免费 API 的速率限制比你想的严格,开 5 个并发基本是安全线,开 20 个大概率触发限流。

还有一点,这套方案不只可以用来翻译网页。同样的后端服务,你接个命令行工具就能翻译本地文件,接个输入框就能当通用翻译器用。我自己就顺手写了个脚本,把整个 markdown 文档目录批量翻译成中文,用来读开源项目的文档特别方便。核心思路就是:翻译能力做成一个独立的服务,前端怎么用是另一回事,这样复用性最好。

如果你也想搭一套,建议先从最小可用版本开始——一个能调通 API 的 Node.js 脚本,然后逐步加缓存、加插件、加并发控制。不要一上来就想着做得多完美,先把链路跑通,后面优化都是水到渠成的事。

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

Qlearning实战:FrozenLake冰湖环境从零跑通与调参避坑指南

简介&#xff1a;这份资源面向强化学习入门者与希望动手实践Q-learning的开发者&#xff0c;围绕经典FrozenLake冰湖游戏&#xff0c;提供一份可直接运行的Python实现&#xff0c;帮助理解模型无关强化学习的核心流程。压缩包内共1个文件&#xff0c;为单个py脚本&#xff0c;整…

作者头像 李华
网站建设 2026/10/7 6:01:37

VOC车辆检测数据集制作与转换:从标注到YOLOv8训练的全流程避坑指南

简介&#xff1a;这是面向视觉目标检测学习者与算法工程师的车辆检测标注数据集&#xff0c;包含 bus、car、suv、taxi、truck 五类常见车辆。图片按类别前缀统一命名&#xff0c;并同时提供 txt 与 xml 两套标注文件&#xff0c;适合用于 YOLO 系列、SSD 或 Faster R-CNN 等模…

作者头像 李华
网站建设 2026/10/7 6:01:04

OpenClaw本地部署与AI自动化任务编排实战指南

1. 为什么要在本地折腾 OpenClaw1.1 从一次“翻车”说起去年年底&#xff0c;我接手了一个需要批量处理本地文档的小项目。需求本身不复杂&#xff1a;把几百份 PDF 里的表格提取出来&#xff0c;清洗后写入数据库。一开始我图省事&#xff0c;直接调用了云端 API&#xff0c;结…

作者头像 李华
网站建设 2026/10/7 6:00:53

变压器基础全解析:原理、选型、安装与运维实战指南

变压器这行当&#xff0c;说难不难&#xff0c;说简单也真不简单。干了这么多年电气设备运维&#xff0c;接触过从几十千伏安的配电变压器到几万千伏安的大型主变&#xff0c;我最大的感受是&#xff1a;很多人对变压器的理解停留在“就是一个变电压的东西”这个层面&#xff0…

作者头像 李华
网站建设 2026/10/7 6:00:44

构建稳定AI Agent:Harness工程实战指南

做 AI Agent 项目做到第五个版本的时候&#xff0c;我终于承认一件事&#xff1a;模型本身不是最大的瓶颈&#xff0c;围绕模型的工程约束才是。标题里那句“构建稳定的 AI Agent”听起来像是网络上随手一搜就有的泛泛之谈&#xff0c;但真把 Agent 丢到生产环境里跑上一周&…

作者头像 李华