RU ▾

Chinese LLM APIПеревод

Получить API-ключ

Обновлено

Перевод и локализация китайского ↔ английского через чат-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. Вот и вся настройка.

Получить API-ключ