Обновлено
Перевод и локализация китайского ↔ английского через чат-API
Перевод строк интерфейса и документов с помощью чат-модели легко продемонстрировать, но сложно внедрить в продакшн. Ошибки банальны: переименованный плейсхолдер, термин глоссария переведен тремя разными способами, ответ JSON обернут в текст. Это руководство рассматривает перевод как конвейер с проверками на каждом этапе, используя совместимый с OpenAI эндпоинт чат-завершений.
Думайте этапами, а не одним промптом
Производственная задача перевода имеет четыре этапа: подготовка строк, вызов модели, проверка результата и слияние. Большинство проблем с качеством возникают из-за пропуска этапа проверки. Инженер по локализации никогда не выпустит файл перевода без запуска линтера плейсхолдеров, и тот же подход применим, когда переводчиком является модель.
Эндпоинт — https://api.chinesellmapi.com/v1/chat/completions, идентификатор модели — uncensored, аутентификация Bearer. Он поддерживает оба направления: с английского на китайский и с китайского на английский. В примерах ниже используются исходные строки на английском, переводимые на упрощённый китайский. Поменяйте направление в системном промпте для обратного перевода.
Имейте в виду общее контекстное окно на 100 000 токенов, значение по умолчанию max_tokens равное 2 048 (увеличьте его для длинных документов, до 32 000) и ограничение тела запроса в 8 МБ. Ни одно из этих ограничений не затрагивает строки интерфейса, но все три важны для целых документов.
Внедрение глоссария
Названия брендов, существительные продуктов и юридически чувствительные термины требуют единого варианта перевода. Самый дешевый механизм — блок глоссария в системном промпте, перечисляющий исходный термин и требуемый перевод. Укажите правило явно: используйте глоссарий дословно при любом склонении исходного термина. Держите глоссарий коротким, так как каждая строка оплачивается как входные данные при каждом вызове; несколько десятков записей — это нормально, тысячи — нет.
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; креативное отклонение — это ошибка в тексте интерфейса.
Также заранее определите стилистическое руководство для целевого языка: формальное или неформальное обращение, нужно ли ставить пробелы между китайскими иероглифами и встроенными латинскими словами или числами, и какой набор пунктуации использовать. Поместите эти решения в тот же системный промпт, чтобы каждый пакет наследовал их.
Сохранение плейсхолдеров и разметки
Плейсхолдеры ломаются предсказуемо: модель переводит имя переменной, пропускает завершающий %s или добавляет пробелы внутри фигурных скобок. Правила промпта снижают эти сбои, но не устраняют их полностью, поэтому проверяйте результат в коде. Извлекайте плейсхолдеры и теги из исходного и целевого текста с помощью регулярного выражения, затем сравнивайте их как отсортированные списки. Порядок может законно меняться между языками, поэтому сравнивайте их как мультимножества, а не последовательности.
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 принимает стандартные поля завершения чата; структурированный вывод здесь достигается четкими инструкциями, а не переключателем, принуждающим к схеме, поэтому пропишите контракт в промпте, включите идентификаторы, чтобы вы могли сопоставить результаты, и разбирайте ответ с запасом. Модели иногда оборачивают 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>"]))
Всегда проверяйте после разбора: идентификаторы должны совпадать, количество должно совпадать, и каждый элемент должен пройти проверку плейсхолдера из предыдущего раздела. Пятьдесят строк на запрос — разумный начальный размер пакета для коротких текстов интерфейса; увеличивайте его только пока процент успешной проверки остается высоким.
Пакетный перевод в пределах лимита запросов
Каждый ключ ограничен 300 запросами в минуту. Задача локализации с 50 000 строк при 20 строк на запрос — это 2 500 запросов, что укладывается менее чем в десять минут при равномерной нагрузке. Скрипт ниже объединяет семафор для активных вызовов с планировщиком на основе блокировок и делает паузу при 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-пакете: где появляется строка, ее максимальная длина и является ли она меткой или предложением.
Измеряйте ограничения длины явно. Тексты интерфейса на английском часто длиннее китайского оригинала, и наоборот для китайских целей, которые короче по символам, но шире на экране, так как каждый глиф занимает полное пространство. Укажите бюджет символов в промпте, если кнопка или заголовок таблицы имеют жесткое ограничение, и проверьте его в коде после получения ответа.
Для китайских исходных документов с именами людей и мест заранее укажите конвенцию транслитерации, например, Ханьюй Пиньинь без тоновых знаков, и добавьте повторяющиеся имена в глоссарий. Без этого один и тот же человек может появиться под двумя написаниями в одном документе, что именно та несогласованность, которую заметит рецензент в первую очередь.
Сэмплирование и проверка перед слиянием
Автоматические проверки ловят структурные ошибки, а не неверные переводы. Добавьте легкий человеческий этап: выбирайте фиксированный процент каждого пакета, например, каждую двадцатую строку, плюс все строки, потребовавшие повторного запроса, и отправляйте их двуязычному рецензенту. Отслеживайте частоту правок рецензента на пакет. Если она растет, ужесточите промпт, уменьшите размер пакета или добавьте записи в глоссарий для терминов, которые исправляют.
Храните промпт, версию глоссария и настройки пакета рядом с каждым файлом вывода. Когда термин меняется в глоссарии, вы можете затем перевести только строки, которые его содержат, вместо перезапуска всей корпусной базы. Простой хеш содержимого исходной строки плюс версия глоссария работает как ключ кэша, и это означает, что неизмененные строки ничего не стоят при следующем запуске.
Наконец, храните набор регрессионных тестов сложных строк: с вложенными плейсхолдерами, формами множественного числа, встроенным HTML и длинными составными существительными. Запускайте его при каждом изменении промпта и сравнивайте выводы бок о бок перед развертыванием изменений.
Оценка стоимости запуска локализации
Предположения, изложенные прямо: 30 000 исходных строк, по 12 английских слов в каждой, переводимые пакетами по 20 штук. Занимает примерно 20 входных токенов на строку плюс 300 токенов фиксированного системного промпта на запрос и около 35 выходных токенов на строку. Это даёт 1 500 запросов, около 1,05 миллиона входных токенов и около 1,05 миллиона выходных токенов. При цене $0,25 за миллион входных токенов и $1,00 за миллион выходных токенов запуск обойдётся примерно в $0,26 плюс $1,05, итого около $1,31. Воспринимайте это как оценку порядка величины и замените предположения данными из ваших собственных журналов usage.
Пробный баланс составляет $0,50 на семь дней, чего достаточно для проверки конвейера на выборке из нескольких тысяч строк. Продолжайте с быстрым стартом приложения для управления скриптом и обработки UTF-8 или посмотрите страницу тарифов для актуальных ставок.
Вопросы и ответы
Может ли API переводить в обоих направлениях?
Да. Перевод с китайского на английский и с английского на китайский работает через один и тот же чат-эндпоинт; вы выбираете направление в системном промпте.
Как предотвратить изменение модели, которая заменяет такие плейсхолдеры, как {name}?
Укажите правило в промпте, затем проверьте в коде, сравнив списки плейсхолдеров между исходным и целевым текстом, повторите запрос или отметьте несовпадения.
Есть ли переключатель JSON-режима?
Структурированный вывод получается через инструкции. Укажите точную структуру в промпте, удалите лишние блоки кода, затем разберите и проверьте результат.
Как быстро я могу запустить большой пакет?
Каждый ключ позволяет отправлять 300 запросов в минуту. Равномерно распределяйте запросы и делайте паузу при ответах 429 или 503.
Ваш ключ — в одной форме от вас
Создайте аккаунт, скопируйте ключ, измените базовый URL. Вот и вся настройка.