PL ▾

Chinese LLM APITłumaczenie

Pobierz klucz API

Zaktualizowano

Tłumaczenie i lokalizacja chińsko-angielska przez API czatu

Tłumaczenie ciągów interfejsu i dokumentów za pomocą modelu czatu jest łatwe do zaprezentowania, ale trudne we wdrożeniu. Niepowodzenia są trywialne: zmieniona nazwa placeholdera, termin z glosariusza przetłumaczony na trzy różne sposoby, odpowiedź JSON osadzona w tekście. Ten przewodnik traktuje tłumaczenie jako potok z kontrolami na każdym etapie, wykorzystując endpoint uzupełnień czatu kompatybilny z OpenAI.

Myśl etapami, a nie jednym promptem

Produkcyjne zadanie tłumaczenia składa się z czterech etapów: przygotowanie ciągów, wywołanie modelu, walidacja wyniku i scalenie go z powrotem. Większość problemów z jakością wynika z pominięcia etapu walidacji. Inżynier ds. lokalizacji nigdy nie wydałby pliku tłumaczeń bez uruchomienia lintera placeholderów, a ta sama dyscyplina ma zastosowanie, gdy tłumaczem jest model.

Endpoint to https://api.chinesellmapi.com/v1/chat/completions, identyfikator modelu uncensored, uwierzytelnianie Bearer. Obsługuje oba kierunki, angielski na chiński i chiński na angielski, a poniższe przykłady używają ciągów źródłowych w języku angielskim przekazywanych do chińskiego uproszczonego. Zmień kierunek w promptcie systemowym dla kierunku odwrotnego.

Pamiętaj o wspólnym oknie kontekstu 100 000 tokenów, domyślnym max_tokens wynoszącym 2048 (podnieś go dla długich dokumentów, do 32 000) i limicie ciała zapytania 8 MB. Żaden z tych limitów nie ma znaczenia dla ciągów interfejsu, ale wszystkie trzy są istotne dla całych dokumentów.

Wstrzykiwanie glosariusza

Nazwy własne, rzeczowniki produktowe i terminy wrażliwe prawnie wymagają jednego ustalonego zapisu. Najtańszym mechanizmem jest blok glosariusza w promptcie systemowym, zawierający termin źródłowy i wymagany termin docelowy. Wyraźnie sformułuj regułę: używaj glosariusza dosłownie w każdej odmianie terminu źródłowego. Utrzymuj glosariusz krótki, ponieważ każda linia jest rozliczana jako dane wejściowe przy każdym wywołaniu; kilkadziesiąt wpisów to norma, tysiące to za dużo.

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."))

Jeśli Twój glosariusz jest duży, filtruj go dla każdego zapytania: uwzględnij tylko te wpisy, których termin źródłowy występuje w partii. Wystarczy zwykłe testowanie podciągów na tekście z małych liter dla pierwszej wersji, co utrzymuje prompt małym. Ustaw temperaturę około 0,2; kreatywna odmiana to błąd w tekstach interfejsu.

Zdecyduj również z góry o przewodniku stylistycznym dla języka docelowego: formalny czy nieformalny zwrot, czy stawiać spacje między znakami chińskimi a osadzonymi słowami łacińskimi lub liczbami oraz który zestaw znaków interpunkcyjnych użyć. Umieść te decyzje w tym samym promptcie systemowym, aby każda partia je dziedziczyła.

Zachowanie placeholderów i znaczników nienaruszonych

Placeholdery psują się w przewidywalny sposób: model tłumaczy nazwę zmiennej, pomija końcowy %s lub dodaje spacje wewnątrz nawiasów klamrowych. Reguły w promptcie zmniejszają te niepowodzenia, ale ich nie eliminują, więc zweryfikuj to w kodzie. Wyciągnij placeholdery i tagi z tekstu źródłowego i docelowego za pomocą wyrażenia regularnego, a następnie porównaj je jako posortowane listy. Kolejność może się słusznie zmieniać między językami, więc porównuj jako wielozbiory, a nie sekwencje.

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

Gdy sprawdzenie się nie powiedzie, powtórz zapytanie raz, cytując w promptcie parę, która się nie powiodła, na przykład w wiadomości użytkownika informującej, że poprzednie wyjście zmieniło placeholder i musi zostać poprawione. Jeśli nadal się nie powiedzie, oznacz ciąg do recenzji przez człowieka, zamiast zapętlać. Tekst sformatowany wymaga dodatkowej ostrożności: preferuj tłumaczenie węzłów tekstowych i samodzielne odbudowanie znaczników, ponieważ to uniemożliwia uszkodzenie tagów konstrukcyjnie.

Dane strukturalne poprzez instrukcje

Grupowanie ciągów w jednym zapytaniu wymaga odpowiedzi czytelnej dla maszyny. API przyjmuje standardowe pola uzupełnień czatu; dane strukturalne tutaj wynikają z jasnych instrukcji, a nie przełącznika wymuszającego schemat, więc zapisz kontrakt w promptcie, uwzględnij identyfikatory, aby móc ponownie dopasować wyniki, i analizuj ostrożnie. Modele czasem owijają JSON w znaczniki kodu lub dodają uprzejme zdanie, więc usuwaj znaczniki przed analizą i traktuj błąd parsowania jako zdarzenie do ponownego wykonania.

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>"]))

Zawsze waliduj po analizie: identyfikatory muszą się zgadzać, liczba musi się zgadzać, a każdy element musi przejść sprawdzanie placeholdera z poprzedniego rozdziału. Pięćdziesiąt ciągów na zapytanie to rozsądny rozmiar początkowej partii dla krótkiego tekstu interfejsu; zwiększaj go tylko, gdy wskaźnik pomyślnej walidacji pozostaje wysoki.

Tłumaczenie partii w ramach limitu

Każdy klucz jest ograniczony do 300 zapytań na minutę. Zadanie lokalizacyjne z 50 000 ciągów przy 20 ciągach na zapytanie to 2500 zapytań, co mieści się w mniej niż dziesięć minut, jeśli rozłożysz je równomiernie. Poniższy skrypt łączy semafor dla wywołań w toku z planerem opartym na blokadach i stosuje backoff przy 429 i 503. W przeciwieństwie do niepowodzenia walidacji, te dwa błędy są przemijające: 429 oznacza zwolnienie tempa, a 503 z upstream_busy oznacza ponowną próbę za kilka sekund. 402 oznacza wyczerpanie salda, czego żadna ponowna próba nie naprawi.

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])

Ta wersja dzieli się na linie dla zwięzłości, co jest w porządku dla ciągów jednoznakowych; dla czegokolwiek wielowierszowego użyj wariantu JSON z poprzedniego rozdziału, aby przerwy wierszowe wewnątrz ciągu nie były mylone z granicami ciągów.

Kierunek odwrotny: tekst źródłowy chiński

Tłumaczenie z chińskiego na angielski ma swoje własne tryby niepowodzeń. Chiński swobodnie pomija podmioty i liczby mnogą, więc model musi je zgadywać; podaj mu kontekst. Ciąg typu „已发送” może być „Sent”, „Has been sent” lub „You sent it”, w zależności od tego, czy oznacza przycisk, odznakę statusu, czy powiadomienie toast. Rozwiązaniem jest pole kontekstu przy każdym ciągu, dostarczane przez programistów i przekazywane w partii JSON: gdzie pojawia się ciąg, jego maksymalna długość oraz czy jest to etykieta, czy zdanie.

Mierz limity długości w sposób jawny. Tekst interfejsu w języku angielskim jest często dłuższy niż oryginał chiński, a odwrotna sytuacja ma miejsce dla celów chińskich, które mają mniej znaków, ale zajmują więcej miejsca na ekranie, ponieważ każdy znak jest pełnej szerokości. Określ budżet znaków w promptcie, gdy przycisk lub nagłówek tabeli ma twardy limit, i zweryfikuj go w kodzie po otrzymaniu odpowiedzi.

Dla chińskich dokumentów źródłowych z imionami i nazwiskami miejscowości określ z góry konwencję romanizacji, taką jak Hanyu Pinyin bez znaków tonalnych, i dodaj powtarzające się nazwy do glosariusza. Bez tego ta sama osoba może pojawić się pod dwiema pisowniami w ramach jednego dokumentu, co jest dokładnie tą niespójnością, którą recenzent zauważy jako pierwszy.

Próbki i recenzja przed scaleniem

Automatyczne sprawdzenia łapią błędy strukturalne, a nie błędne tłumaczenia. Dodaj lekki krok ludzki: pobieraj próbkę stałego procentu każdej partii, na przykład co dwudziesty ciąg, plus każdy ciąg, który wymagał ponownego wykonania, i wyślij je do recenzenta dwujęzycznego. Śledź wskaźnik edycji recenzenta na partię. Jeśli rośnie, zaostrz prompt, zmniejsz rozmiar partii lub dodaj wpisy do glosariusza dla terminów, które są poprawiane.

Przechowuj prompt, wersję glosariusza i ustawienia partii obok każdego pliku wyjściowego. Gdy termin zmieni się w glosariuszu, możesz wtedy przetłumaczyć tylko ciągi, które go zawierają, zamiast ponownie uruchamiać cały korpus. Prostym kluczem pamięci podręcznej jest skrót treści ciągu źródłowego plus wersja glosariusza, co oznacza, że niezmienione ciągi nie kosztują nic przy kolejnym uruchomieniu.

Na koniec przechowuj zestaw regresji trudnych ciągów: tych z zagnieżdżonymi placeholderami, formami liczby mnogiej, osadzonym HTML i długimi rzeczownikami złożonymi. Uruchamiaj go za każdym razem, gdy zmieniasz prompt, i porównuj wyjścia obok siebie przed wprowadzeniem zmiany.

Szacowanie kosztu dla zadania lokalizacyjnego

Założenia, przedstawione wprost: 30 000 ciągów źródłowych, po 12 słów angielskich każde, tłumaczone w partiach po 20. Zajmuje to około 20 tokenów wejściowych na ciąg plus 300 tokenów stałego promptu systemowego na zapytanie i około 35 tokenów wyjściowych na ciąg. Daje to 1500 zapytań, około 1,05 miliona tokenów wejściowych i około 1,05 miliona tokenów wyjściowych. Przy cenie $0,25 za milion tokenów wejściowych i $1,00 za milion tokenów wyjściowych, uruchomienie kosztuje około $0,26 plus $1,05, czyli około $1,31. Traktuj to jako szacunek rzędu wielkości i zamień założenia na dane z własnych dzienników usage.

Kredyt próbny wynosi $0,50 na siedem dni, co wystarczy do zweryfikowania potoku na próbie kilku tysięcy ciągów. Kontynuuj z szybkim startem aplikacji dla kontroli skryptu i obsługi UTF-8 lub zobacz stronę z cenami dla aktualnych stawek.

Pytania i odpowiedzi

Czy API tłumaczy w obu kierunkach?

Tak. Tłumaczenie z chińskiego na angielski i odwrotnie działa przez ten sam endpoint czatu; kierunek wybierasz w systemowym promptcie.

Jak sprawić, by model nie zmieniał placeholderów typu {name}?

Zdefiniuj regułę w promptcie, a następnie zweryfikuj ją w kodzie, porównując listy placeholderów między tekstem źródłowym a docelowym, powtarzając zapytanie lub oznaczając niezgodności.

Czy istnieje przełącznik trybu JSON?

Dane strukturalne uzyskujesz dzięki instrukcjom. Określ dokładny kształt w promptcie, usuń niepotrzebne znaczniki kodu, a następnie przeanalizuj i zweryfikuj wynik.

Jak szybko mogę przetworzyć dużą partię?

Każdy klucz pozwala na 300 zapytań na minutę. Rozłóż zapytania równomiernie i zastosuj mechanizm backoff przy odpowiedziach 429 lub 503.

Twój klucz jest o jeden formularz stąd

Utwórz konto, skopiuj klucz, zmień bazowy URL. To cała konfiguracja.

Pobierz klucz API