Извикване на модел през 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 думи.
class ApiError extends Error { constructor(status, message) { super(message); this.status = status; }}// заместител на истинския клиент: записани отговори, без мрежаclass FakeClient { constructor(failures = []) { this.failures = [...failures]; }
create({ model, max_tokens, system, messages }) { if (this.failures.length > 0) { const status = this.failures.shift(); throw new ApiError(status, reason(status)); } let text = answer(messages); let stop_reason = "end_turn"; if (tokens(text) > max_tokens) { // max_tokens: отговорът спира по средата text = text.split(" ").slice(0, Math.floor((max_tokens * 3) / 4)).join(" "); stop_reason = "max_tokens"; } let input_tokens = tokens(system); for (const m of messages) { input_tokens += tokens(m.content); } const usage = { input_tokens, output_tokens: tokens(text) }; return { content: [{ type: "text", text }], stop_reason, usage }; }}
// записаният отговор: избира се по дума в последното съобщениеfunction answer(messages) { const last = messages[messages.length - 1].content; const hasOrder = messages.some((m) => m.content.includes("1042")); if (last.includes("върн")) return "Можете да върнете стоката до 14 дни от получаването."; if (last.includes("пристиг")) { return hasOrder ? "Поръчка 1042 е при куриера и пристига утре до 18 ч." : "Кой е номерът на поръчката ви?"; } return "С какво да помогна: статус на поръчка, доставка или връщане?";}
// текстът на грешката по кода ѝfunction reason(status) { const reasons = { 400: "невалидна заявка", 401: "невалиден ключ", 429: "твърде много заявки", }; return reasons[status] ?? "грешка на сървъра";}
// грубо: 4 токена на 3 думи (истинския брой дава токенизаторът)function tokens(text) { const words = text.trim().split(/\s+/).length; return Math.floor((words * 4 + 2) / 3);}Едно извикване, после същото с max_tokens 8:
// текстовите блокове, после защо спря и колко токенаfunction show(response) { for (const block of response.content) { if (block.type === "text") console.log(block.text); } const u = response.usage; console.log(`stop_reason: ${response.stop_reason}, input_tokens: ${u.input_tokens}, output_tokens: ${u.output_tokens}`);}const client = new FakeClient();const system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.";const messages = [{ role: "user", content: "Кога ще пристигне поръчка 1042?" }];
let response = client.create({ model: MODEL, max_tokens: 1024, system, messages });show(response);
response = client.create({ model: MODEL, max_tokens: 8, system, 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.
const response = await fetch("https://<доставчик>/messages", { method: "POST", headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "<името на модела>", max_tokens: 1024, system: "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.", messages: [{ role: "user", content: "Кога ще пристигне поръчка 1042?" }], }),});if (!response.ok) throw new Error(`HTTP ${response.status}`);const reply = await response.json();for (const block of reply.content) { if (block.type === "text") console.log(block.text);}Доставчиците имат и официални библиотеки (SDK) за Java, C#, Python и JavaScript със същата форма: съобщения с роли, токени, инструменти. Точните имена на класовете и методите са в документацията им. await: извикването ходи по мрежата и е асинхронно.
Идеята
Ключът е в средата, не в кода
Ключът за API-то е като парола за сметката: който го има, харчи от нея. В кода той стига до git, до всеки колега и понякога до публично хранилище. Затова се чете от променлива на средата, тук LLM_API_KEY.
const key = process.env.LLM_API_KEY ?? "";if (key.trim() === "") { console.log("Няма ключ: задай LLM_API_KEY в средата.");} else { console.log("Ключът е зареден от средата. Не го печатай и не го пиши в кода.");}Няма ключ: задай LLM_API_KEY в средата.Програмата казва дали ключът го има, но никога не го печата. Файлът с локалните тайни (например .env) е в .gitignore.
Виж го
API без памет
ask добавя въпроса към историята, праща цялата история и пази отговора с ролята assistant. Накрая същият въпрос, пратен сам:
// добавя въпроса, праща цялата история и пази отговораfunction ask(client, system, history, text) { history.push({ role: "user", content: text }); const r = client.create({ model: MODEL, max_tokens: 1024, system, messages: history }); const reply = r.content[0].text; history.push({ role: "assistant", content: reply }); return reply;}const client = new FakeClient();const system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.";const history = [];for (const text of ["Здравейте, имам поръчка 1042.", "Кога ще пристигне?"]) { console.log(`клиент: ${text}`); console.log(`асистент: ${ask(client, system, history, text)}`);}
const alone = client.create({ model: MODEL, max_tokens: 1024, system, messages: [{ role: "user", content: "Кога ще пристигне?" }],});console.log(`само въпросът, без историята: ${alone.content[0].text}`);клиент: Здравейте, имам поръчка 1042.асистент: С какво да помогна: статус на поръчка, доставка или връщане?клиент: Кога ще пристигне?асистент: Поръчка 1042 е при куриера и пристига утре до 18 ч.само въпросът, без историята: Кой е номерът на поръчката ви?С историята моделът вижда номера 1042 от първото съобщение. Без нея пита за него. Паметта на разговора е в програмата, не в модела, а историята расте и се плаща при всеки ход.
Пробвай
Намери грешката
Колега пише ask. На втория въпрос асистентът пак пита за номера на поръчката. Кой ред е грешен?
Избери реда с грешката.
Историята се пази, но не се праща: заявката носи само последното съобщение. Моделът не помни предишните заявки, затова вижда въпрос без номер. Трябва да се прати цялата история.
Пробвай
Попълни
С каква роля влиза в историята отговорът на модела?
function ask(client, system, history, text) {
history.push({ role: "user", content: text });
const r = client.create({ model: MODEL, max_tokens: 1024, system, messages: history });
const reply = r.content[0].text;
history.push({ role: , content: reply });
return reply;
}Отговорите на модела са с ролята "assistant", а съобщенията на клиента с "user". Така в следващата заявка моделът знае кое е казал той и кое клиентът.
Идеята
Грешките на API-то
Мрежата и сървърът на доставчика падат, както всяка външна услуга. Грешките идват с HTTP код и се делят на две:
- Временни: 429 (твърде много заявки, достигнат лимит), 500 и останалите 5xx (сървърът има проблем или е претоварен), 408 и 409. След малко същата заявка може да мине. Тях си струва да повториш.
- Постоянни: 400 (грешна заявка), 401 (невалиден ключ), 404. Повторението дава същото. Тях програмата казва веднага.
Повторението чака между опитите, а паузата се удвоява: 1, 2, 4 секунди. Така кратък проблем минава бързо, а претовареният сървър не получава поток от нови заявки. Това е експоненциалното чакане (exponential backoff). След определен брой опити програмата се отказва.
Виж го
Повторение с удвояваща се пауза
// повтаря временните грешки, паузата се удвояваfunction createWithRetry(client, system, messages) { const maxAttempts = 4; let delay = 1; for (let attempt = 1; ; attempt++) { try { return client.create({ model: MODEL, max_tokens: 1024, system, messages, }); } catch (e) { const last = attempt === maxAttempts; if (last || !retryable(e.status)) throw e; console.log(` опит ${attempt}: ${e.status},`, `чакам ${delay} с`); // истинската програма чака тук: // await sleep(delay * 1000) delay *= 2; } }}
// временни: 408, 409, 429 и 5xxfunction retryable(status) { return status === 408 || status === 409 || status === 429 || status >= 500;}Стъпка по стъпка, с думи
- Заявката: моделът, системният промпт и съобщенията с ролите им. Тук е едно, от клиента (user).
- Първият опит връща 429: „твърде много заявки“. Временна грешка.
- 429 е от временните, затова програмата чака 1 с и опитва пак.
- Вторият опит: 500, „грешка на сървъра“. Също временна.
- Паузата се удвоява: 2 с. Претовареният сървър получава въздух.
- Третият опит минава.
- Отговорът: текстов блок, stop_reason end_turn и usage: 21 входни и 15 изходни токена.
- 401 и 400 не се повтарят: грешният ключ не се оправя с чакане. След 4 опита програмата се отказва.
// повтаря временните грешки, паузата се удвояваfunction createWithRetry(client, system, messages) { const maxAttempts = 4; let delay = 1; for (let attempt = 1; ; attempt++) { try { return client.create({ model: MODEL, max_tokens: 1024, system, messages, }); } catch (e) { const last = attempt === maxAttempts; if (last || !retryable(e.status)) throw e; console.log(` опит ${attempt}: ${e.status},`, `чакам ${delay} с`); // истинската програма чака тук: // await sleep(delay * 1000) delay *= 2; } }}
// временни: 408, 409, 429 и 5xxfunction retryable(status) { return status === 408 || status === 409 || status === 429 || status >= 500;}Три сценария: 429 и 500, после отговор; 401, без повторение; четири пъти 429, отказ след четвъртия опит. Паузата само се печата, за да върви програмата веднага.
const system = "Ти си асистент на поддръжката на онлайн магазин. Отговаряй кратко.";const messages = [{ role: "user", content: "Кога ще пристигне поръчка 1042?" }];const scenarios = [[429, 500], [401], [429, 429, 429, 429]];for (const failures of scenarios) { console.log(`сценарий [${failures.join(", ")}]:`); try { const r = createWithRetry(new FakeClient(failures), system, messages); console.log(` отговор: ${r.content[0].text}`); } catch (e) { console.log(` отказ: ${e.status} ${e.message}`); }}сценарий [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 са както по-горе. Какво ще отпечата програмата?
const client = new FakeClient([429, 400]);try { const r = createWithRetry(client, system, messages); console.log(`отговор: ${r.content[0].text}`);} catch (e) { console.log(`отказ: ${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