使用 FastGPT 實現最佳 AI 翻譯工作流:全世界最信達雅的翻譯

米开朗基杨發表於2024-10-31

想讓AI翻譯既準確又地道?本文將教你如何利用 FastGPT 打造一個革命性的翻譯工作流。

不僅支援文字翻譯,還能直接處理文件,更能透過自定義術語表確保專業術語的翻譯準確性,堪稱翻譯神器!

直接看效果:

再來看術語表:

這也太適合翻譯產品官網和官方文件了吧??

背景

吳恩達教授最近提出了一個創新的大語言模型(LLM)翻譯方案 —— translation-agent,這個方案的獨特之處在於引入了"自我反思"機制,具體工作流程如下:

  1. LLM 將原文從源語言翻譯為目標語言;
  2. LLM 對翻譯結果進行自我反思,提出改進建議;
  3. 根據這些建議最佳化翻譯結果。

這個 AI 翻譯流程是目前比較新的一種翻譯方式,利用 LLM 對自己的翻譯結果進行改進來獲得較好的 AI 翻譯效果。

專案中展示了可以利用對長文字進行分片,然後分別進行反思翻譯處理,以突破 LLM 對 tokens 數量的限制,真正實現長文字一鍵高效率高質量翻譯。

該專案還透過給大模型限定國家地區,已實現更精確的 AI 翻譯,如美式英語、英式英語之分;同時提出一些可能能帶來更好效果的最佳化,如對於一些 LLM 未曾訓練到的術語 (或有多種翻譯方式的術語) 建立術語表,進一步提升翻譯的精確度等等。

而這一切都能透過 FastGPT 工作流輕鬆實現,本文將手把手教你如何使用 FastGPT 復刻吳恩達老師的 translation-agent。

而且 FastGPT 的工作流還更進一步,既可以直接輸入文字進行翻譯,也可以上傳文件進行翻譯

話不多說,拿起鍵盤,開始教學 ~

整個工作流大概分為三個板塊:

  1. 判斷是否上傳了文件
  2. 對文字進行切分,使切分出來的文字不超出 LLM tokens 數量限制
  3. 依次按順序對每個切分出來的文字進行翻譯
FastGPT 國內版地址:https://fastgpt.cn
FastGPT 海外版地址:https://tryfastgpt.ai

判斷是否上傳文件

首先第一步,在【系統配置】中新增兩個全域性變數用來表示源語言和目標語言,同時需要開啟檔案上傳功能。

接下來進入正式的工作流流程,先判斷一下源語言與目標語言是否相同,如果是一樣的,那就沒必要翻譯了!你讓我把中文翻譯成中文?開什麼玩笑,Token 不要錢的啦?

如果源語言和目標語言不同,就繼續往下走,判斷是否上傳了文件,如果上傳了文件,就將文件裡的內容解析出來。

OK,進入下一個流程。

切分長文字

接下來我們可以透過分片和迴圈,實現對長文字也即多文字塊的反思翻譯。

整體的邏輯是,首先對傳入文字的 tokens 數量做判斷,如果不超過設定的 tokens 限制,那麼直接呼叫單文字塊反思翻譯,如果超過設定的 tokens 限制,那麼切割為合理的大小,再分別進行對應的反思翻譯處理。

至於為什麼要切割分塊,有兩個原因:

  1. 大模型輸出上下文只有 4k,無法輸出超過 4k token 內容的文字。
  2. 輸入分塊可以減少太長的輸入導致的幻覺。

下面我們接著看工作流。

不管是否上傳了文件,最終都要把文字提取出來。如果直接輸入了文字,那麼直接將文字傳遞給下一個節點;如果上傳了文件,那麼解析完文件之後再將解析出來的文字傳遞給下一個節點。

下一個節點要乾的事情就是對文字進行切分。

這裡我們用到了 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(/```[\s\S]*?```/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! 🚀

相關文章