更新
透過聊天 API 進行中英翻譯與在地化
使用聊天模型翻譯 UI 字串和文件很容易示範,卻難以上線。常見失敗包括:佔位符被更名、詞彙表中的詞彙被翻譯成三種不同的樣子、JSON 回覆被包裹在散文之中。本指南將翻譯視為一個管線,在每個階段都設有檢查機制,使用 OpenAI 相容的聊天完成端點。
以階段思考,而非單一提示詞
生產環境的翻譯作業有四個階段:準備字串、呼叫模型、驗證結果、合併回傳。大多數品質問題都來自跳過驗證階段。在地化工程師絕不會在未執行佔位符偵錯工具的情況下發布翻譯檔案,當翻譯者是模型時,同樣的紀律也適用。
端點為 https://api.chinesellmapi.com/v1/chat/completions,模型 ID 為 uncensored,使用 Bearer 驗證。它支援雙向翻譯(英文翻中文與中文翻英文),以下範例使用英文來源字串轉換為簡體中文。若要反向操作,請在系統提示詞中交換方向。
請記住共享的 100,000 token 上下文視窗、預設的 max_tokens 為 2,048(長文件可提高至 32,000),以及 8 MB 的請求主體上限。這些限制不會影響 UI 字串,但對於完整文件來說都很重要。
注入詞彙表
品牌名稱、產品名詞和具法律敏感性的詞彙需要固定的譯法。最便宜的方法是將詞彙表區塊放入系統提示詞,列出來源詞彙與所需的目標譯法。明確說明規則:在任何來源詞彙的變化形式中都應逐字使用詞彙表。保持詞彙表簡短,因為每一行都會在每次呼叫時作為輸入計費;幾十個條目是常態,數千個則不然。
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])
GLOSSARY = {
"workspace": "工作区",
"pull request": "合并请求",
"seat": "席位",
"billing cycle": "计费周期",
}
def build_system(glossary, target="Simplified Chinese"):
rows = "\n".join(f"- {src} => {dst}" for src, dst in glossary.items())
return (
f"You are a software localization translator. Translate English UI strings into {target}.\n"
"Rules:\n"
"1. Use the glossary below verbatim whenever the source term appears, in any inflection.\n"
"2. Copy placeholders such as {name}, %s, %d and {{count}} exactly, character for character.\n"
"3. Copy HTML tags and attributes exactly; translate only the text between tags.\n"
"4. Output the translation only, with no notes.\n\n"
"Glossary:\n" + rows
)
def translate(text):
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": build_system(GLOSSARY)},
{"role": "user", "content": text},
],
temperature=0.2,
max_tokens=400,
)
return r.choices[0].message.content.strip()
print(translate("Invite {name} to the workspace before the next billing cycle."))
如果詞彙表很大,請依請求進行篩選:僅包含批次中出現來源詞條的項目。對小寫文字進行簡單的子字串測試即可滿足第一版需求,且能保持提示詞精簡。將 temperature 設定為 0.2 左右;在 UI 文案中,創意變化屬於錯誤。
同時,請事先決定目標語言的風格指南:正式或口語化的稱呼、中文字元與嵌入的拉丁字母或數字之間是否加空格,以及使用哪種標點符號集。將這些決定放入同一個系統提示詞中,讓每個批次都能繼承這些設定。
保持佔位符與標記完整
佔位符會以可預測的方式出錯:模型會翻譯變數名稱、遺漏尾端的 %s,或在花括號內加入空格。提示詞規則可降低這些錯誤,但無法完全消除,因此請在程式碼中進行驗證。使用正規表示式從來源和目標中提取佔位符與標籤,然後將它們作為排序後的列表進行比較。順序在不同語言之間可能會合法地改變,因此請將它們視為多重集(multisets)而非序列來比較。
import re
PLACEHOLDER = re.compile(r"\{\{?\w+\}?\}|%[sd]|</?[a-zA-Z][^>]*>")
def placeholders_ok(source: str, target: str) -> bool:
# Same multiset of placeholders and tags, regardless of order.
return sorted(PLACEHOLDER.findall(source)) == sorted(PLACEHOLDER.findall(target))
src = 'You have <b>{count}</b> unread messages in %s.'
bad = '你在 %s 中有 <b>{数量}</b> 条未读消息。'
good = '你在 %s 中有 <b>{count}</b> 条未读消息。'
print(placeholders_ok(src, bad), placeholders_ok(src, good)) # False True
當檢查失敗時,請將失敗的配對以引號形式放回提示詞中並重試一次,例如使用者訊息指出前次輸出變更了佔位符且必須修正。如果仍然失敗,請將該字串標記為人工審查,而非繼續迴圈。對於富文字應格外小心:優先翻譯文字節點並自行重建標籤結構,因為這樣可以從結構上避免標籤損壞。
透過指示取得結構化輸出
在單一請求中批次處理字串需要機器可讀取的回覆。API 接受標準的聊天完成欄位;此處的結構化輸出來自清晰的指示,而非強制執行架構的開關,因此請將合約寫入提示詞,包含 ID 以便重新對齊結果,並謹慎解析。模型有時會將 JSON 包裹在程式碼框中或加上友好的句子,因此在解析前請移除程式碼框,並將解析錯誤視為可重試的事件。
import json
import os
import re
from openai import OpenAI
client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])
SYSTEM = (
"Translate each item from English to Simplified Chinese. "
"Reply with a JSON object only, no code fences, no commentary. "
'Shape: {"items": [{"id": <number>, "zh": "<translation>"}]}. '
"Keep the same ids, keep every placeholder and HTML tag unchanged."
)
def translate_batch(strings):
payload = [{"id": i, "en": s} for i, s in enumerate(strings)]
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
],
temperature=0.2,
max_tokens=2000,
)
raw = r.choices[0].message.content.strip()
raw = re.sub(r"^```(?:json)?|```$", "", raw, flags=re.M).strip() # tolerate stray fences
data = json.loads(raw)
by_id = {item["id"]: item["zh"] for item in data["items"]}
return [by_id[i] for i in range(len(strings))]
print(translate_batch(["Save changes", "Delete {count} files?", "Welcome back, <b>{name}</b>"]))
解析後務必驗證:ID 必須匹配,計數必須匹配,且每個項目都必須通過上一節的佔位符檢查。每個請求 50 個字串是短 UI 文字的合理起始批次;僅在驗證通過率保持高檔時才增加批次大小。
在速率限制下批次翻譯
每個金鑰每分鐘限制 300 次請求。一個包含 50,000 個字串的本地化任務,若每次請求處理 20 個字串,則需要 2,500 次請求。如果均勻配速,可在十分鐘內完成。下方的腳本將用於控制並行呼叫的訊號量與基於鎖的配速器(pacer)結合,並在收到 429 和 503 時退避。與驗證失敗不同,這兩種錯誤是暫態的:429 表示減速,503 且帶有 upstream_busy 表示幾秒後重試。402 表示餘額已耗盡,這無法透過重試解決。
import asyncio
import os
import time
from openai import AsyncOpenAI, APIStatusError
client = AsyncOpenAI(
base_url="https://api.chinesellmapi.com/v1",
api_key=os.environ["API_KEY"],
max_retries=0,
timeout=90,
)
RATE = 240 / 60 # requests per second, below the 300/min cap
gate = asyncio.Lock()
next_slot = 0.0
slots = asyncio.Semaphore(6)
async def pace():
global next_slot
async with gate:
now = time.monotonic()
if next_slot > now:
await asyncio.sleep(next_slot - now)
next_slot = max(now, next_slot) + 1 / RATE
async def call(batch, attempt=0):
async with slots:
await pace()
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "Translate to Simplified Chinese. Keep placeholders unchanged. One line per input line."},
{"role": "user", "content": "\n".join(batch)},
],
max_tokens=1500,
temperature=0.2,
)
return r.choices[0].message.content.splitlines()
except APIStatusError as e:
if e.status_code in (429, 503) and attempt < 4:
await asyncio.sleep(2 ** attempt + 1) # 1s, 3s, 5s, 9s
return await call(batch, attempt + 1)
raise
async def run(all_strings, size=20):
chunks = [all_strings[i:i + size] for i in range(0, len(all_strings), size)]
results = await asyncio.gather(*(call(c) for c in chunks))
return [line for part in results for line in part]
if __name__ == "__main__":
strings = [f"Item {n} was updated by {{user}}" for n in range(60)]
out = asyncio.run(run(strings))
print(len(out), out[0])
此版本為了簡潔而按行分割,這對於單行字串是可以的;對於任何多行內容,請使用上一節的 JSON 變體,以免字串內的換行符被誤認為字串邊界。
反向方向:中文來源文字
將中文翻譯成英文有其特有的失敗模式。中文經常省略主詞和複數形式,因此模型必須猜測;請提供上下文。像「已發送」這樣的字串可能是「Sent」、「Has been sent」或「You sent it」,取決於是標籤按鈕、狀態徽章還是提示訊息。解決方案是在每個字串旁邊提供上下文欄位,由你的開發人員提供並在 JSON 批次中傳遞:字串出現的位置、其最大長度,以及它是標籤還是句子。
明確測量長度限制。英文 UI 文字通常比中文原文長,而中文目標文字則相反,它們字元較少但螢幕佔用較寬,因為每個字元都是全寬。當按鈕或表格標題有硬性限制時,請在提示詞中聲明字元預算,並在收到回應後在程式碼中進行驗證。
對於包含人名和地名來源的中文文件,請事先指定羅馬化慣例,例如無聲調的漢語拼音,並將常見人名加入詞彙表。否則,同一個人可能會在同一份文件中出現兩種拼寫,這正是審查員最先注意到的不一致之處。
合併前的抽樣與審查
自動化檢查可捕捉結構性錯誤,而非翻譯錯誤。加入輕量級的人工步驟:對每個批次抽取固定百分比進行抽樣,例如每二十個字串抽一個,以及所有需要重試的字串,並將這些送交雙語審查員。追蹤審查員每個批次的修訂率。如果修訂率上升,請收紧提示詞、縮小批次大小,或為正在修正的詞條新增詞彙表項目。
將提示詞、詞彙表版本和批次設定與每個輸出檔案一起保存。當詞彙表中的詞彙發生變化時,你可以只重新翻譯包含該詞彙的字串,而不必重新運行整個語料庫。來源字串加詞彙表版本的簡單內容雜湊可用作快取金鑰,這意味著未更改的字串在下次運行時不會產生任何費用。
最後,保留一組難題字串的回歸測試集:包含嵌套佔位符、複數形式、嵌入 HTML 和長複合名詞的字串。每當更改提示詞時都運行它,並在推出變更之前並排比較輸出結果。
估算在地化作業的成本
明確的假設:30,000 個來源字串,每個包含 12 個英文單詞,以每批 20 個進行翻譯。每個字串約需 20 個輸入 token,每個請求需 300 個輸入 token 的固定系統提示詞,每個字串約需 35 個輸出 token。這將產生 1,500 次請求,約 1.05 百萬個輸入 token 和約 1.05 百萬個輸出 token。以每百萬輸入 token $0.25 和每百萬輸出 token $1.00 計算,執行成本約為 $0.26 加上 $1.05,總計約 $1.31。請將其視為數量級估計,並使用您自己的 usage 日誌中的數據替換假設。
試用額度為 $0.50,有效期七天,足以在數千個字串的樣本上驗證管線。請繼續前往 App Quickstart 以進行腳本控制和 UTF-8 處理,或查看 Pricing Page 以獲取當前費率。
問答
API 是否支援雙向翻譯?
是的。中文翻英文與英文翻中文均可透過相同的聊天端點進行;你在系統提示詞中選擇方向即可。
如何防止模型修改 {name} 等佔位符?
在提示詞中說明規則,然後在程式碼中比對來源與目標的佔位符清單,並在出現不一致時重試或標記。
有 JSON 模式開關嗎?
透過指示取得結構化輸出。在提示詞中指定確切格式,移除多餘的程式碼框,然後解析並驗證結果。
執行大批次時速度有多快?
每個金鑰每分鐘允許 300 個請求。平均分配請求,並在收到 429 或 503 回應時退避。
只差一張表單,即可取得金鑰
建立帳戶、複製金鑰、更改 base URL。這就是完整的設定。