Извикване на модел през API
Накратко: Извикването на езиков модел през API е заявка с модел, системен промпт и съобщения с роли; отговорът носи текста, причината за спиране и броя токени.
Проблем
Асистентът излиза пред клиентите
Досега моделите бяха малки и живееха в програмата. Асистентът на поддръжката трябва да отговаря на истински клиенти, а това прави голям модел при доставчик. Програмата го пита през API: праща заявка по мрежата и чака отговор.
Първият ден на разпродажбата клиентите виждат „Грешка 429“ вместо отговор. На друг клиент асистентът пита за номера на поръчката, който той току-що каза.
Как изглежда една заявка, какво връща моделът и какво правиш, когато отговорът е грешка?
Идеята
Заявка и отговор
Извикването на модел е HTTP заявка с JSON. Заявката носи:
model: кой модел;max_tokens: най-много колко токена да е отговорът;system: системният промпт, инструкцията за ролята и тона;messages: съобщенията с ролите им,user(клиентът) иassistant(предишните отговори на модела).
Отговорът носи:
content: списък от блокове сtype; текстът е в блокtext;stop_reason: защо моделът спря,end_turn(свърши) илиmax_tokens(стигна тавана);usage: колко токена влязоха (input_tokens) и колко излязоха (output_tokens). По тях се плаща.
API-то няма памет. Всяка заявка е сама за себе си и носи всичко, което моделът трябва да види, както в урока за прозореца на контекста. API-тата на различните доставчици имат същата форма: съобщения с роли, токени, инструменти.
Виж го
Фалшив клиент със записани отговори
Програмите в урока не викат истински модел: проверката им няма мрежа, а истинският отговор е различен всеки път (урокът за границите). Затова вместо клиента на доставчика има заместител със същата форма: взима model, max_tokens, system и messages и връща content, stop_reason и usage. Отговорите са записани предварително и се избират по дума в последното съобщение. Токените са преброени грубо, 4 на 3 думи.
@dataclassclass Block: type: str text: str
@dataclassclass Usage: input_tokens: int output_tokens: int
@dataclassclass Response: content: list stop_reason: str usage: Usage
class ApiError(Exception): def __init__(self, status, message): super().__init__(message) self.status = status# заместител на истинския клиент: записани отговори, без мрежаclass FakeClient: def __init__(self, failures=()): self.failures = list(failures)
def create(self, model, max_tokens, system, messages): if self.failures: status = self.failures.pop(0) raise ApiError(status, reason(status)) text = answer(messages) stop_reason = "end_turn" if tokens(text) > max_tokens: # max_tokens: отговорът спира по средата text = " ".join(text.split()[: max_tokens * 3 // 4]) stop_reason = "max_tokens" input_tokens = tokens(system) for message in messages: input_tokens += tokens(message["content"]) usage = Usage(input_tokens, tokens(text)) return Response([Block("text", text)], stop_reason, usage)
# записаният отговор: избира се по дума в последното съобщениеdef answer(messages): last = messages[-1]["content"] has_order = any("1042" in m["content"] for m in messages) if "върн" in last: return "Можете да върнете стоката до 14 дни от получаването." if "пристиг" in last: return "Поръчка 1042 е при куриера и пристига утре до 18 ч." if has_order else "Кой е номерът на поръчката ви?" return "С какво да помогна: статус на поръчка, доставка или връщане?"
# текстът на грешката по кода ѝdef reason(status): reasons = { 400: "невалидна заявка", 401: "невалиден ключ", 429: "твърде много заявки", } return reasons.get(status, "грешка на сървъра")
# грубо: 4 токена на 3 думи (истинския брой дава токенизаторът)def tokens(text): words = len(text.split()) return (words * 4 + 2) // 3Едно извикване, после същото с max_tokens 8:
# текстовите блокове, после защо спря и колко токенаdef show(response): for block in response.content: if block.type == "text": print(block.text) u = response.usage print(f"stop_reason: {response.stop_reason}, input_tokens: {u.input_tokens}, output_tokens: {u.output_tokens}")client = FakeClient()system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко."messages = [{"role": "user", "content": "Кога ще пристигне поръчка 1042?"}]
response = client.create(model=MODEL, max_tokens=1024, system=system, messages=messages)show(response)
response = client.create(model=MODEL, max_tokens=8, system=system, messages=messages)show(response)Поръчка 1042 е при куриера и пристига утре до 18 ч.stop_reason: end_turn, input_tokens: 21, output_tokens: 15Поръчка 1042 е при куриера иstop_reason: max_tokens, input_tokens: 21, output_tokens: 8С 8 токена отговорът спира по средата на изречението, а stop_reason е max_tokens. Програмата трябва да го проверява: отрязан отговор, показан на клиент, изглежда като грешка на магазина.
Виж го
Пусни при теб: истинското извикване
Същото извикване към истински модел. То не е част от проверените програми и тук не се пуска: трябва ти профил при доставчик, ключ в средата, а всяко извикване струва пари.
По мрежата отива JSON. Заявката:
{ "model": "<името на модела>", "max_tokens": 1024, "system": "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.", "messages": [ { "role": "user", "content": "Кога ще пристигне поръчка 1042?" } ]}И отговорът (числата тук са от фалшивия клиент):
{ "content": [ { "type": "text", "text": "Поръчка 1042 е при куриера и пристига утре до 18 ч." } ], "stop_reason": "end_turn", "usage": { "input_tokens": 21, "output_tokens": 15 }}Със стандартния HTTP клиент на езика това е една POST заявка. Адресът, името на модела и заглавката с ключа са заместители: истинските са в документацията на доставчика. Ключът се чете от променливата LLM_API_KEY.
import jsonimport osimport urllib.request
body = { "model": "<името на модела>", "max_tokens": 1024, "system": "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.", "messages": [{"role": "user", "content": "Кога ще пристигне поръчка 1042?"}],}request = urllib.request.Request( "https://<доставчик>/messages", data=json.dumps(body).encode("utf-8"), headers={ "Authorization": "Bearer " + os.environ["LLM_API_KEY"], "Content-Type": "application/json", },)with urllib.request.urlopen(request) as response: reply = json.load(response)for block in reply["content"]: if block["type"] == "text": print(block["text"])Доставчиците имат и официални библиотеки (SDK) за Java, C#, Python и JavaScript със същата форма: съобщения с роли, токени, инструменти. Точните имена на класовете и методите са в документацията им.
Идеята
Ключът е в средата, не в кода
Ключът за API-то е като парола за сметката: който го има, харчи от нея. В кода той стига до git, до всеки колега и понякога до публично хранилище. Затова се чете от променлива на средата, тук LLM_API_KEY.
key = os.environ.get("LLM_API_KEY", "")if not key.strip(): print("Няма ключ: задай LLM_API_KEY в средата.")else: print("Ключът е зареден от средата. Не го печатай и не го пиши в кода.")Няма ключ: задай LLM_API_KEY в средата.Програмата казва дали ключът го има, но никога не го печата. Файлът с локалните тайни (например .env) е в .gitignore.
Виж го
API без памет
ask добавя въпроса към историята, праща цялата история и пази отговора с ролята assistant. Накрая същият въпрос, пратен сам:
# добавя въпроса, праща цялата история и пази отговораdef ask(client, system, history, text): history.append({"role": "user", "content": text}) r = client.create(model=MODEL, max_tokens=1024, system=system, messages=history) reply = r.content[0].text history.append({"role": "assistant", "content": reply}) return replyclient = FakeClient()system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко."history = []for text in ["Здравейте, имам поръчка 1042.", "Кога ще пристигне?"]: print(f"клиент: {text}") print(f"асистент: {ask(client, system, history, text)}")
alone = client.create(model=MODEL, max_tokens=1024, system=system, messages=[{"role": "user", "content": "Кога ще пристигне?"}])print(f"само въпросът, без историята: {alone.content[0].text}")клиент: Здравейте, имам поръчка 1042.асистент: С какво да помогна: статус на поръчка, доставка или връщане?клиент: Кога ще пристигне?асистент: Поръчка 1042 е при куриера и пристига утре до 18 ч.само въпросът, без историята: Кой е номерът на поръчката ви?С историята моделът вижда номера 1042 от първото съобщение. Без нея пита за него. Паметта на разговора е в програмата, не в модела, а историята расте и се плаща при всеки ход.
Пробвай
Намери грешката
Колега пише ask. На втория въпрос асистентът пак пита за номера на поръчката. Кой ред е грешен?
Избери реда с грешката.
Историята се пази, но не се праща: заявката носи само последното съобщение. Моделът не помни предишните заявки, затова вижда въпрос без номер. Трябва да се прати цялата история.
Пробвай
Попълни
С каква роля влиза в историята отговорът на модела?
def ask(client, system, history, text):
history.append({"role": "user", "content": text})
r = client.create(model=MODEL, max_tokens=1024, system=system, messages=history)
reply = r.content[0].text
history.append({"role": , "content": reply})
return replyОтговорите на модела са с ролята "assistant", а съобщенията на клиента с "user". Така в следващата заявка моделът знае кое е казал той и кое клиентът.
Идеята
Грешките на API-то
Мрежата и сървърът на доставчика падат, както всяка външна услуга. Грешките идват с HTTP код и се делят на две:
- Временни: 429 (твърде много заявки, достигнат лимит), 500 и останалите 5xx (сървърът има проблем или е претоварен), 408 и 409. След малко същата заявка може да мине. Тях си струва да повториш.
- Постоянни: 400 (грешна заявка), 401 (невалиден ключ), 404. Повторението дава същото. Тях програмата казва веднага.
Повторението чака между опитите, а паузата се удвоява: 1, 2, 4 секунди. Така кратък проблем минава бързо, а претовареният сървър не получава поток от нови заявки. Това е експоненциалното чакане (exponential backoff). След определен брой опити програмата се отказва.
Виж го
Повторение с удвояваща се пауза
# повтаря временните грешки, паузата се удвояваdef create_with_retry(client, system, messages): max_attempts = 4 delay = 1 for attempt in range(1, max_attempts + 1): try: return client.create( model=MODEL, max_tokens=1024, system=system, messages=messages) except ApiError as e: last = attempt == max_attempts if last or not retryable(e.status): raise print(f" опит {attempt}: {e.status},", f"чакам {delay} с") # истинската програма чака тук: # time.sleep(delay) delay *= 2
# временни: 408, 409, 429 и 5xxdef retryable(status): return (status == 408 or status == 409 or status == 429 or status >= 500)Стъпка по стъпка, с думи
- Заявката: моделът, системният промпт и съобщенията с ролите им. Тук е едно, от клиента (user).
- Първият опит връща 429: „твърде много заявки“. Временна грешка.
- 429 е от временните, затова програмата чака 1 с и опитва пак.
- Вторият опит: 500, „грешка на сървъра“. Също временна.
- Паузата се удвоява: 2 с. Претовареният сървър получава въздух.
- Третият опит минава.
- Отговорът: текстов блок, stop_reason end_turn и usage: 21 входни и 15 изходни токена.
- 401 и 400 не се повтарят: грешният ключ не се оправя с чакане. След 4 опита програмата се отказва.
# повтаря временните грешки, паузата се удвояваdef create_with_retry(client, system, messages): max_attempts = 4 delay = 1 for attempt in range(1, max_attempts + 1): try: return client.create( model=MODEL, max_tokens=1024, system=system, messages=messages) except ApiError as e: last = attempt == max_attempts if last or not retryable(e.status): raise print(f" опит {attempt}: {e.status},", f"чакам {delay} с") # истинската програма чака тук: # time.sleep(delay) delay *= 2
# временни: 408, 409, 429 и 5xxdef retryable(status): return (status == 408 or status == 409 or status == 429 or status >= 500)Три сценария: 429 и 500, после отговор; 401, без повторение; четири пъти 429, отказ след четвъртия опит. Паузата само се печата, за да върви програмата веднага.
system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко."messages = [{"role": "user", "content": "Кога ще пристигне поръчка 1042?"}]scenarios = [[429, 500], [401], [429, 429, 429, 429]]for failures in scenarios: print(f"сценарий {failures}:") try: r = create_with_retry(FakeClient(failures), system, messages) print(f" отговор: {r.content[0].text}") except ApiError as e: print(f" отказ: {e.status} {e}")сценарий [429, 500]: опит 1: 429, чакам 1 с опит 2: 500, чакам 2 с отговор: Поръчка 1042 е при куриера и пристига утре до 18 ч.сценарий [401]: отказ: 401 невалиден ключсценарий [429, 429, 429, 429]: опит 1: 429, чакам 1 с опит 2: 429, чакам 2 с опит 3: 429, чакам 4 с отказ: 429 твърде много заявкиОфициалните SDK на доставчиците обикновено правят това вътре: сами повтарят временните грешки с нарастваща пауза. Провери в документацията и не слагай свой цикъл отгоре: опитите се умножават. Свой цикъл пишеш, когато викаш API-то направо по HTTP, както в блока „пусни при теб“.
Timeout, повторение с jitter и circuit breaker са в урока за устойчивостта.Пробвай
Предвиди изхода
Фалшивият клиент връща първо 429, после 400. system и messages са както по-горе. Какво ще отпечата програмата?
client = FakeClient([429, 400])try: r = create_with_retry(client, system, messages) print(f"отговор: {r.content[0].text}")except ApiError as e: print(f"отказ: {e.status}")429 е временна: програмата печата първия опит, чака 1 с и опитва пак. 400 е постоянна: retryable(400) е false, грешката излиза от createWithRetry веднага, без втори ред за чакане, и програмата печата отказа с кода 400. До отговор не се стига.
Идеята
Какво вижда клиентът
Повторенията не помагат винаги. Когато и последният опит падне, клиентът не трябва да вижда „ApiException: 503“. Програмата хваща грешката и казва нещо вежливо и вярно: „В момента не мога да отговоря. Ще ви свържа с колега.“ Грешката се записва в лога за екипа.
По-точно, за любопитните
Jitter. Когато хиляди програми паднат заедно и чакат точно 1, 2, 4 секунди, те опитват пак заедно. Към паузата се добавя случайна част (jitter), за да се разпръснат.
Timeout. Заявката може и да виси, затова всяко извикване по мрежата има таван на времето за отговор. Дълъг отговор е бавен, затова max_tokens пази и времето, и парите. Повече за бавността и цената е в урока за цената и бавността.
Лимитите. 429 значи, че си стигнал лимита на заявки или токени за минута. Повторението помага при пик. Ако го стигаш постоянно, решението е по-малко заявки, по-къси заявки или по-висок лимит, не повече повторения.
Какво следва. Системният промпт е първата стъпка към промпта като код. Отговор, който програмата чете, а не само показва, е структурираният изход, а модел, който сам иска данни от програмата, е използването на инструменти. Текст от потребител или документ, който се опитва да смени инструкциите (prompt injection), е в модула за агенти по-нататък в курса.
Упражнение
Отговор или вежлив отказ
Реши я в редактора си.
Напиши askSupport(client, system, question): вика createWithRetry, връща текста на отговора, а ако и последният опит падне (или грешката е постоянна), връща „В момента не мога да отговоря. Ще ви свържа с колега.“. Пусни я с въпроса „Мога ли да върна якето?“ и четири сценария на фалшивия клиент: без грешки; четири пъти 503; 401; един 429.
сценарий []: Можете да върнете стоката до 14 дни от получаването.сценарий [503, 503, 503, 503]: опит 1: 503, чакам 1 с опит 2: 503, чакам 2 с опит 3: 503, чакам 4 с В момента не мога да отговоря. Ще ви свържа с колега.сценарий [401]: В момента не мога да отговоря. Ще ви свържа с колега.сценарий [429]: опит 1: 429, чакам 1 с Можете да върнете стоката до 14 дни от получаването.Още задачи
- Лог за екипа. При отказ отпечатай отделен ред с кода и текста на грешката, преди вежливия отговор. Клиентът вижда само вежливия.
- Отрязаният отговор. Добави параметър за
max_tokensкъмcreateWithRetry. Акоstop_reasonеmax_tokens, върни отговора с „…“ накрая и запиши предупреждение. Пробвай с 8.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Текстът на парчета: токени · Предсказване на следващия токен · Прозорецът на контекста · Границите на модела
- Продължава в: Промптът е код · Структуриран изход: JSON по схема · Инструменти: моделът пита, програмата прави · Оценяване: тестов набор за асистента
- Задачи по темата: Приложения с LLM