想让AI翻译既准确又地道?本文将教你如何利用 FastGPT 打造一个革命性的翻译工作流。
它不仅支持文本翻译,还能直接处理文档,更能通过自定义术语表确保专业术语的翻译准确性,堪称翻译神器!
直接看效果:
再来看术语表:
这也太适合翻译产品官网和官方文档了吧??
背景
吴恩达教授最近提出了一个创新的大语言模型(LLM)翻译方案 —— translation-agent,这个方案的独特之处在于引入了"自我反思"机制,具体工作流程如下:
- LLM 将原文从源语言翻译为目标语言;
- LLM 对翻译结果进行自我反思,提出改进建议;
- 根据这些建议优化翻译结果。
这个 AI 翻译流程是目前比较新的一种翻译方式,利用 LLM 对自己的翻译结果进行改进来获得较好的 AI 翻译效果。
项目中展示了可以利用对长文本进行分片,然后分别进行反思翻译处理,以突破 LLM 对 tokens 数量的限制,真正实现长文本一键高效率高质量翻译。
该项目还通过给大模型限定国家地区,已实现更精确的 AI 翻译,如美式英语、英式英语之分;同时提出一些可能能带来更好效果的优化,如对于一些 LLM 未曾训练到的术语 (或有多种翻译方式的术语) 建立术语表,进一步提升翻译的精确度等等。
而这一切都能通过 FastGPT 工作流轻松实现,本文将手把手教你如何使用 FastGPT 复刻吴恩达老师的 translation-agent。
而且 FastGPT 的工作流还更进一步,既可以直接输入文本进行翻译,也可以上传文档进行翻译。
话不多说,拿起键盘,开始教学 ~
整个工作流大概分为三个板块:
- 判断是否上传了文档
- 对文本进行切分,使切分出来的文本不超出 LLM tokens 数量限制
- 依次按顺序对每个切分出来的文本进行翻译
FastGPT 国内版地址:https://fastgpt.cn
FastGPT 海外版地址:https://tryfastgpt.ai
判断是否上传文档
首先第一步,在【系统配置】中添加两个全局变量用来表示源语言和目标语言,同时需要开启文件上传功能。
接下来进入正式的工作流流程,先判断一下源语言与目标语言是否相同,如果是一样的,那就没必要翻译了!你让我把中文翻译成中文?开什么玩笑,Token 不要钱的啦?
如果源语言和目标语言不同,就继续往下走,判断是否上传了文档,如果上传了文档,就将文档里的内容解析出来。
OK,进入下一个流程。
切分长文本
接下来我们可以通过分片和循环,实现对长文本也即多文本块的反思翻译。
整体的逻辑是,首先对传入文本的 tokens 数量做判断,如果不超过设置的 tokens 限制,那么直接调用单文本块反思翻译,如果超过设置的 tokens 限制,那么切割为合理的大小,再分别进行对应的反思翻译处理。
至于为什么要切割分块,有两个原因:
- 大模型输出上下文只有 4k,无法输出超过 4k token 内容的文字。
- 输入分块可以减少太长的输入导致的幻觉。
下面我们接着看工作流。
不管是否上传了文档,最终都要把文本提取出来。如果直接输入了文本,那么直接将文本传递给下一个节点;如果上传了文档,那么解析完文档之后再将解析出来的文本传递给下一个节点。
下一个节点要干的事情就是对文本进行切分。
这里我们用到了 Jina AI 开源的一个强大的正则表达式,它能利用所有可能的边界线索和启发式方法来精确切分文本,来看看代码:
const MAX_HEADING_LENGTH = 7; // 最大标题长度 const MAX_HEADING_CONTENT_LENGTH = 200; // 最大标题内容长度 const MAX_HEADING_UNDERLINE_LENGTH = 200; // 最大标题下划线长度 const MAX_HTML_HEADING_ATTRIBUTES_LENGTH = 100; // 最大HTML标题属性长度 const MAX_LIST_ITEM_LENGTH = 200; // 最大列表项长度 const MAX_NESTED_LIST_ITEMS = 6; // 最大嵌套列表项数 const MAX_LIST_INDENT_SPACES = 7; // 最大列表缩进空格数 const MAX_BLOCKQUOTE_LINE_LENGTH = 200; // 最大块引用行长度 const MAX_BLOCKQUOTE_LINES = 15; // 最大块引用行数 const MAX_CODE_BLOCK_LENGTH = 1500; // 最大代码块长度 const MAX_CODE_LANGUAGE_LENGTH = 20; // 最大代码语言长度 const MAX_INDENTED_CODE_LINES = 20; // 最大缩进代码行数 const MAX_TABLE_CELL_LENGTH = 200; // 最大表格单元格长度 const MAX_TABLE_ROWS = 20; // 最大表格行数 const MAX_HTML_TABLE_LENGTH = 2000; // 最大HTML表格长度 const MIN_HORIZONTAL_RULE_LENGTH = 3; // 最小水平分隔线长度 const MAX_SENTENCE_LENGTH = 400; // 最大句子长度 const MAX_QUOTED_TEXT_LENGTH = 300; // 最大引用文本长度 const MAX_PARENTHETICAL_CONTENT_LENGTH = 200; // 最大括号内容长度 const MAX_NESTED_PARENTHESES = 5; // 最大嵌套括号数 const MAX_MATH_INLINE_LENGTH = 100; // 最大行内数学公式长度 const MAX_MATH_BLOCK_LENGTH = 500; // 最大数学公式块长度 const MAX_PARAGRAPH_LENGTH = 1000; // 最大段落长度 const MAX_STANDALONE_LINE_LENGTH = 800; // 最大独立行长度 const MAX_HTML_TAG_ATTRIBUTES_LENGTH = 100; // 最大HTML标签属性长度 const MAX_HTML_TAG_CONTENT_LENGTH = 1000; // 最大HTML标签内容长度 const LOOKAHEAD_RANGE = 100; // 向前查找句子边界的字符数 const AVOID_AT_START = `[\s\]})>,']`; // 避免在开头匹配的字符 const PUNCTUATION = `[.!?…]|\.{3}|[\u2026\u2047-\u2049]|[\p{Emoji_Presentation}\p{Extended_Pictographic}]`; // 标点符号 const QUOTE_END = `(?:'(?=`)|''(?=``))`; // 引号结束 const SENTENCE_END = `(?:${PUNCTUATION}(?<!${AVOID_AT_START}(?=${PUNCTUATION}))|${QUOTE_END})(?=\S|$)`; // 句子结束 const SENTENCE_BOUNDARY = `(?:${SENTENCE_END}|(?=[\r\n]|$))`; // 句子边界 const LOOKAHEAD_PATTERN = `(?:(?!${SENTENCE_END}).){1,${LOOKAHEAD_RANGE}}${SENTENCE_END}`; // 向前查找句子结束的模式 const NOT_PUNCTUATION_SPACE = `(?!${PUNCTUATION}\s)`; // 非标点符号空格 const SENTENCE_PATTERN = `${NOT_PUNCTUATION_SPACE}(?:[^\r\n]{1,{MAX_LENGTH}}${SENTENCE_BOUNDARY}|[^\r\n]{1,{MAX_LENGTH}}(?=${PUNCTUATION}|${QUOTE_END})(?:${LOOKAHEAD_PATTERN})?)${AVOID_AT_START}*`; // 句子模式 const regex = new RegExp( "(" + // 1. Headings (Setext-style, Markdown, and HTML-style, with length constraints) `(?:^(?:[#*=-]{1,${MAX_HEADING_LENGTH}}|\w[^\r\n]{0,${MAX_HEADING_CONTENT_LENGTH}}\r?\n[-=]{2,${MAX_HEADING_UNDERLINE_LENGTH}}|<h[1-6][^>]{0,${MAX_HTML_HEADING_ATTRIBUTES_LENGTH}}>)[^\r\n]{1,${MAX_HEADING_CONTENT_LENGTH}}(?:</h[1-6]>)?(?:\r?\n|$))` + "|" + // New pattern for citations `(?:\[[0-9]+\][^\r\n]{1,${MAX_STANDALONE_LINE_LENGTH}})` + "|" + // 2. List items (bulleted, numbered, lettered, or task lists, including nested, up to three levels, with length constraints) `(?:(?:^|\r?\n)[ \t]{0,3}(?:[-*+•]|\d{1,3}\.\w\.|\[[ xX]\])[ \t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}` + `(?:(?:\r?\n[ \t]{2,5}(?:[-*+•]|\d{1,3}\.\w\.|\[[ xX]\])[ \t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}}` + `(?:\r?\n[ \t]{4,${MAX_LIST_INDENT_SPACES}}(?:[-*+•]|\d{1,3}\.\w\.|\[[ xX]\])[ \t]+${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_LIST_ITEM_LENGTH))}){0,${MAX_NESTED_LIST_ITEMS}})?)` + "|" + // 3. Block quotes (including nested quotes and citations, up to three levels, with length constraints) `(?:(?:^>(?:>|\s{2,}){0,2}${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_BLOCKQUOTE_LINE_LENGTH))}\r?\n?){1,${MAX_BLOCKQUOTE_LINES}})` + "|" + // 4. Code blocks (fenced, indented, or HTML pre/code tags, with length constraints) `(?:(?:^|\r?\n)(?:```|~~~)(?:\w{0,${MAX_CODE_LANGUAGE_LENGTH}})?\r?\n[\s\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:```|~~~)\r?\n?` + `|(?:(?:^|\r?\n)(?: {4}|\t)[^\r\n]{0,${MAX_LIST_ITEM_LENGTH}}(?:\r?\n(?: {4}|\t)[^\r\n]{0,${MAX_LIST_ITEM_LENGTH}}){0,${MAX_INDENTED_CODE_LINES}}\r?\n?)` + `|(?:<pre>(?:<code>)?[\s\S]{0,${MAX_CODE_BLOCK_LENGTH}}?(?:</code>)?</pre>))` + "|" + // 5. Tables (Markdown, grid tables, and HTML tables, with length constraints) `(?:(?:^|\r?\n)(?:\|[^\r\n]{0,${MAX_TABLE_CELL_LENGTH}}\|(?:\r?\n\|[-:]{1,${MAX_TABLE_CELL_LENGTH}}\|){0,1}(?:\r?\n\|[^\r\n]{0,${MAX_TABLE_CELL_LENGTH}}\|){0,${MAX_TABLE_ROWS}}` + `|<table>[\s\S]{0,${MAX_HTML_TABLE_LENGTH}}?</table>))` + "|" + // 6. Horizontal rules (Markdown and HTML hr tag) `(?:^(?:[-*_]){${MIN_HORIZONTAL_RULE_LENGTH},}\s*$|<hr\s*/?>)` + "|" + // 10. Standalone lines or phrases (including single-line blocks and HTML elements, with length constraints) `(?!${AVOID_AT_START})(?:^(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}(?:</[a-zA-Z]+>)?(?:\r?\n|$))` + "|" + // 7. Sentences or phrases ending with punctuation (including ellipsis and Unicode punctuation) `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_SENTENCE_LENGTH))}` + "|" + // 8. Quoted text, parenthetical phrases, or bracketed content (with length constraints) "(?:" + `(?<!\w)"""[^"]{0,${MAX_QUOTED_TEXT_LENGTH}}"""(?!\w)` + `|(?<!\w)(?:['"`'"])[^\r\n]{0,${MAX_QUOTED_TEXT_LENGTH}}\1(?!\w)` + `|(?<!\w)`[^\r\n]{0,${MAX_QUOTED_TEXT_LENGTH}}'(?!\w)` + `|(?<!\w)``[^\r\n]{0,${MAX_QUOTED_TEXT_LENGTH}}''(?!\w)` + `|\([^\r\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\([^\r\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\)[^\r\n()]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\)` + `|\[[^\r\n\[\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}(?:\[[^\r\n\[\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}\][^\r\n\[\]]{0,${MAX_PARENTHETICAL_CONTENT_LENGTH}}){0,${MAX_NESTED_PARENTHESES}}\]` + `|\$[^\r\n$]{0,${MAX_MATH_INLINE_LENGTH}}\$` + `|`[^`\r\n]{0,${MAX_MATH_INLINE_LENGTH}}`` + ")" + "|" + // 9. Paragraphs (with length constraints) `(?!${AVOID_AT_START})(?:(?:^|\r?\n\r?\n)(?:<p>)?${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_PARAGRAPH_LENGTH))}(?:</p>)?(?=\r?\n\r?\n|$))` + "|" + // 11. HTML-like tags and their content (including self-closing tags and attributes, with length constraints) `(?:<[a-zA-Z][^>]{0,${MAX_HTML_TAG_ATTRIBUTES_LENGTH}}(?:>[\s\S]{0,${MAX_HTML_TAG_CONTENT_LENGTH}}?</[a-zA-Z]+>|\s*/>))` + "|" + // 12. LaTeX-style math expressions (inline and block, with length constraints) `(?:(?:\$\$[\s\S]{0,${MAX_MATH_BLOCK_LENGTH}}?\$\$)|(?:\$[^\$\r\n]{0,${MAX_MATH_INLINE_LENGTH}}\$))` + "|" + // 14. Fallback for any remaining content (with length constraints) `(?!${AVOID_AT_START})${SENTENCE_PATTERN.replace(/{MAX_LENGTH}/g, String(MAX_STANDALONE_LINE_LENGTH))}` + ")", "gmu" ); function main({text}){ const chunks = []; let currentChunk = ''; const tokens = countToken(text) const matches = text.match(regex); if (matches) { matches.forEach((match) => { if (currentChunk.length + match.length <= 1000) { currentChunk += match; } else { if (currentChunk) { chunks.push(currentChunk); } currentChunk = match; } }); if (currentChunk) { chunks.push(currentChunk); } } return {chunks, tokens}; }
算了别看了,你直接用就好啦。。
格式化文本
切分完文本之后,开始对源文本进行格式化:
格式化代码如下:
function main({source_text_chunks, source_doc_text_chunks, i=0}){ let chunks = source_doc_text_chunks || source_text_chunks; let before = chunks.slice(0, i).join(""); let current = " <TRANSLATE_THIS>" + chunks[i] + "</TRANSLATE_THIS>"; let after = chunks.slice(i + 1).join(""); let tagged_text = before + current + after; return { tagged_text, chunk_to_translate: chunks[i], } }
最终会输出两个变量,其中 tagged_text
包含了整个文本,而 chunk_to_translate
只包含了本轮需要翻译的文本块。
接入术语表
在正式翻译之前,我们还可以将专有名词的词库作为知识库,在翻译前进行搜索。
记得要开启问题优化哦。
开始翻译
现在我们终于进入了翻译环节,这里我们使用 CoT 思维链,让 LLM 显式地、系统地生成推理链条,展示翻译的完整思考过程。
系统提示词如下:
# Role: 资深翻译专家 ## Background: 你是一位经验丰富的翻译专家,精通{{source_lang}}和{{target_lang}}互译,尤其擅长将{{source_lang}}文章译成流畅易懂的{{target_lang}}。你曾多次带领团队完成大型翻译项目,译文广受好评。 ## Attention: - 翻译过程中要始终坚持"信、达、雅"的原则,但"达"尤为重要 - 翻译的译文要符合{{target_lang}}的表达习惯,通俗易懂,连贯流畅 - 避免使用过于文绉绉的表达和晦涩难懂的典故引用 - 诗词歌词等内容需按原文换行和节奏分行,不破坏原排列格式 - 对于专有的名词或术语,按照给出的术语表进行合理替换 - 在翻译过程中,注意保留文档原有的列表项和格式标识 - 不要翻译代码块中的内容,保持原样输出 ## Constraints: - 必须严格遵循四轮翻译流程:直译、意译、反思、提升 - 译文要忠实原文,准确无误,不能遗漏或曲解原意 - 注意判断上下文,避免重复翻译 - 最终译文使用Markdown的代码块呈现,但是不用输出markdown这个单词 ## Goals: - 通过四轮翻译流程,将{{source_lang}}原文译成高质量的{{target_lang}}译文 - 译文要准确传达原文意思,语言表达力求浅显易懂,朗朗上口 - 适度使用一些熟语俗语、流行网络用语等,增强译文的亲和力 ## Skills: - 精通{{source_lang}} {{target_lang}}两种语言,具有扎实的语言功底和丰富的翻译经验 - 擅长将{{source_lang}}表达习惯转换为地道自然的{{target_lang}} - 对当代{{target_lang}}语言的发展变化有敏锐洞察,善于把握语言流行趋势 ## Workflow: 1. 第一轮直译:逐字逐句忠实原文,不遗漏任何信息(代码块内容除外) 2. 第二轮意译:在直译的基础上用通俗流畅的{{target_lang}}意译原文(代码块内容除外) 3. 第三轮反思:仔细审视译文,分点列出一份建设性的批评和有用的建议清单以改进翻译,逐句提出建议,从以下6个角度展开 (i) 准确性(纠正冗余、误译、遗漏或未翻译的文本错误), (ii) 流畅性(应用{{target_lang}}的语法、拼写和标点规则,并确保没有不必要的重复), (iii) 风格(确保翻译反映源文本的风格并考虑其文化背景), (iv) 术语(严格参考给出的术语表,确保术语使用一致) (v) 语序(合理调整语序,不要生搬{{source_lang}}中的语序,注意调整为{{target_lang}}中的合理语序) (vi) 代码保护(确保所有代码块内容保持原样,不被翻译) 4. 第四轮提升:严格遵循第三轮提出的建议对翻译修改,定稿出一个简洁畅达、符合大众阅读习惯的译文 ## OutputFormat: - 每一轮前用【思考】说明该轮要点 - 第一轮和第二轮翻译后用【翻译】呈现译文 - 第三轮用【建议】输出建议清单,分点列出,在每一点前用*xxx*标识这条建议对应的要点,如*风格*;建议前用【思考】说明该轮要点,建议后用【建议】呈现建议 - 第四轮在```代码块中展示最终译文内容,如```xxx```,不用输出markdown这个单词 ## Suggestions: - 直译时力求忠实原文,但不要过于拘泥逐字逐句 - 意译时在准确表达原意的基础上,用最朴实无华的{{target_lang}}来表达 - 反思环节重点关注译文是否符合{{target_lang}}表达习惯,是否通俗易懂,是否准确流畅,是否术语一致 - 提升环节采用反思环节的建议对意译环节的翻译进行修改,适度采用一些口语化的表达、网络流行语等,增强译文的亲和力 - 所有包含在代码块(```)中的内容都应保持原样,不进行翻译
用户问题如下:
同时不要忘了接入词库。
还需要在 AI 模型配置中关闭【返回 AI 内容】。
这里 AI 会进行好几轮翻译,但是我们只需要最终的翻译结果,所以还需要继续接入【代码运行】节点,将最后一轮的翻译结果提取出来。
代码如下:
function main({data1}){ const result = data1.split("```").filter(item => !!item.trim()) if(result[result.length-1]) { return { result: result[result.length-1] } } return { result: '未截取到翻译内容' } }
到这里翻译基本上就结束了,但是有些模型在输出中文内容时,会夹杂英文标点符号,所以为了以防万一,我们可以再加一个【代码运行】节点来对输出内容进行格式化。
代码如下:
function main({target_lang, source_text}) { let text = source_text; if (target_lang === '简体中文' || target_lang === '繁體中文') { // 存储代码块内容 const codeBlocks = []; let text = source_text.replace(/```[sS]*?```/g, (match) => { codeBlocks.push(match); return `__CODE_BLOCK_${codeBlocks.length - 1}__`; }); // 替换成对的英文引号 text = text.replace(/"(.*?)"/g, '“$1”'); // 保护 Markdown 链接格式中的括号 text = text.replace(/[(.*?)]((.*?))/g, function(match) { return match.replace(/(/g, 'LEFTPAREN') .replace(/)/g, 'RIGHTPAREN'); }); // 替换成对的英文括号 text = text.replace(/((.*?))/g, '($1)'); // 恢复被保护的 Markdown 链接括号 text = text.replace(/LEFTPAREN/g, '(') .replace(/RIGHTPAREN/g, ')'); // 更新句号替换逻辑,增加对版本号和URL的保护 text = text.replace(/(d+).(d+).(d+)/g, '$1DOT$2DOT$3') // 保护版本号 (如 16.2.1) .replace(/(d).(d)/g, '$1DOT$2') // 临时替换小数点 .replace(/([a-zA-Z]).([a-zA-Z])/g, '$1DOT$2') // 临时替换缩写中的句号 .replace(/([a-zA-Z]).(d)/g, '$1DOT$2') // 临时替换字母与数字之间的句号 .replace(/(d).([a-zA-Z])/g, '$1DOT$2') // 临时替换数字与字母之间的句号 .replace(/([a-zA-Z])./g, '$1DOT') // 临时替换字母后面的句号(如 a.) .replace(/https?:/g, 'HTTPCOLON') // 保护 URL 中的冒号 .replace(/./g, '。') // 替换其他句号 .replace(/DOT/g, '.') // 恢复被保护的句号 .replace(/HTTPCOLON/g, 'http:'); // 恢复 URL 中的冒号 // 替换英文逗号,但不替换数字中的逗号 text = text.replace(/(d),(d)/g, '$1COMMA$2') // 临时替换数字中的逗号 .replace(/,/g, ',') // 替换其他逗号 .replace(/COMMA/g, ','); // 恢复数字中的逗号 // 替换其他常见符号 const replacements = { '!': '!', '?': '?', ';': ';', }; for (const [key, value] of Object.entries(replacements)) { text = text.replace(new RegExp(`\${key}`, 'g'), value); } // 在中文和英文字符之间添加空格 // 中文字符范围: u4e00-u9fa5 // 英文字符范围: a-zA-Z0-9 text = text.replace(/([u4e00-u9fa5])([a-zA-Z0-9])/g, '$1 $2') .replace(/([a-zA-Z0-9])([u4e00-u9fa5])/g, '$1 $2'); // 恢复代码块 text = text.replace(/__CODE_BLOCK_(d+)__/g, (_, index) => codeBlocks[index]); } return { text }; }
循环
单文本块翻译完成后,需要判断一下所有的文本是否都翻译完成了,如果还没翻译完,就回到循环的起点,按照之前的流程继续翻译。
代码如下:
function main({chunks, doc_chunks, currentChunk}){ let new_chunks = doc_chunks || chunks const findIndex = new_chunks.findIndex((item) => item ===currentChunk) return { isEnd: new_chunks.length-1 === findIndex, i: findIndex + 1, } }
如果翻译完成了,就直接回复翻译完成,整个工作流结束。
效果演示
咱们先来导入一个词库。
下载链接:https://images.tryfastgpt.ai/vocabulary.csv
导入方式很简单,在 FastGPT 中新建一个通用知识库:
然后导入表格数据集:
然后上传你的 csv 数据集,一路下一步,最后就得到了处理完成的词库。
点进去可以看到详情:
导入完成后,就可以在工作流中选择该词库了。
最终我们来测试一下翻译效果:
点击聊天框左侧的回形针图标上传附件,然后选择需要上传的文档。
测试文档地址:https://images.tryfastgpt.ai/Sealos-Devbox-quick-start.pdf
上传文档后,点击右边的发送按钮开始翻译。
术语翻译的一致性保持的非常完美:
总结
好啦!到这里我们就完整复刻了吴恩达老师的 translation-agent,而且通过 FastGPT 工作流的能力,我们不仅实现了文本翻译,还支持了文档翻译功能。整个翻译过程不仅准确,而且通过术语表的加持,专业术语的翻译也能保持高度一致性。
相信有了这个工具,你的翻译工作效率一定能上一个台阶!
如果你连一点点代码都不想写,那也没问题,只需要导入我分享的工作流就可以了。
工作流导入方式:将鼠标指针悬停在新建的工作流左上方标题处,然后点击【导入配置】
完整工作流:https://pan.quark.cn/s/019132869eca
最后,如果你觉得这篇教程对你有帮助,欢迎分享给更多需要的朋友。当然,如果你在使用过程中遇到任何问题,也欢迎随时反馈交流。让我们一起把 AI 翻译变得更好!
Happy translating! 🚀