Към съдържанието
wannacode

Извикване на модел през 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;
}

Идеята

Грешките на 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 и 5xx
function retryable(status) {
return status === 408 || status === 409
|| status === 429 || status >= 500;
}
Стъпка по стъпка, с думи
  1. Заявката: моделът, системният промпт и съобщенията с ролите им. Тук е едно, от клиента (user).
  2. Първият опит връща 429: „твърде много заявки“. Временна грешка.
  3. 429 е от временните, затова програмата чака 1 с и опитва пак.
  4. Вторият опит: 500, „грешка на сървъра“. Също временна.
  5. Паузата се удвоява: 2 с. Претовареният сървър получава въздух.
  6. Третият опит минава.
  7. Отговорът: текстов блок, stop_reason end_turn и usage: 21 входни и 15 изходни токена.
  8. 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 и 5xx
function 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}`);
}
Отговор

Идеята

Какво вижда клиентът

Повторенията не помагат винаги. Когато и последният опит падне, клиентът не трябва да вижда „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.

Тест

Провери се

1. Клиентът пише „Здравейте, имам поръчка 1042.“, а после „Кога ще пристигне?“. Програмата праща на втората заявка само новия въпрос. Какво става?
2. Отговорът идва със stop_reason: max_tokens. Какво значи?
3. Кои грешки си струва да повториш?
4. Защо паузата между опитите се удвоява (1, 2, 4 с)?
5. Къде е мястото на ключа за API-то?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·