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

Цена и бавност: бюджет, кеш, по-малък модел

Накратко: Цената на езиков модел е токени × цена, отделно за входа и за изхода; бюджет на токени, кеш на отговорите и по-малък модел свалят сметката, а кешът и малкият модел и времето за отговор.

Проблем

Сметката в края на месеца

Асистентът на поддръжката работи от месец. Отговаря вярно, а наборът от урока за оценяването минава. После идват две оплаквания: сметката за модела е по-висока от очакваното, а клиентите чакат за отговор около секунда.

Ето едно извикване, както го прави асистентът. Цените на токените са примерни, в евро за милион токена:

String question = "Колко струва доставката?";
Response r = CLIENT.create(LARGE, 1024, PROMPT + " " + ALL_DOCS, List.of(new Message("user", question)));
ModelInfo m = MODELS.get(LARGE);
double euro = cost(m, r.usage());
double input = r.usage().inputTokens() * m.inPrice() / 1_000_000;
System.out.printf("вход: %d токена, изход: %d токена%n", r.usage().inputTokens(), r.usage().outputTokens());
System.out.printf("цена: %.5f € (входът е %.0f%%)%n", euro, 100 * input / euro);
System.out.printf("примерно време: %d ms%n", latency(m, r.usage()));
Изход
вход: 314 токена, изход: 32 токена
цена: 0.00177 € (входът е 71%)
примерно време: 1071 ms

Въпросът „Колко струва доставката?“ е 6 токена, а входът е 314. Част от сметката е отговорът, а по-голямата част е входът: системният промпт и всички документи на магазина пътуват с всеки въпрос. А в дневника половината въпроси са едни и същи: колко струва доставката, кога работи поддръжката.

Идеята

Откъде идват парите и времето

Моделът се плаща на токени (урокът за токените), отделно за входа и за изхода:

  • входът е всичко, което пращаш: системният промпт, документите, историята на разговора, въпросът. API-то няма памет, затова това се праща всеки път (урокът за контекста);
  • изходът е отговорът. Токен изход обикновено струва няколко пъти повече от токен вход, тук 4 пъти.

Времето е подобно, но с друг баланс. Моделът чете входа бързо, а отговора го пише токен по токен. Затова дългият отговор е бавен, а дългият вход е най-вече скъп.

Оттук и трите поправки, които урокът мери с програма:

  1. бюджет на токени: по-малко вход (само нужното) и таван за изхода;
  2. кеш: еднаквият въпрос не стига до модела;
  3. по-малък модел за лесните въпроси: по-евтин и по-бърз токен.

Бюджетът на токени е горна граница на токените на едно извикване, планирана предварително, както планираш паметта или времето за отговор на една заявка.

Виж го

Асистентът в код

Фалшивият клиент е същият като в урока за оценяването: формата на истинското API, записани отговори и груби токени (всяка дума е 2 токена). Отговорите са записани по дума във въпроса:

// фалшив клиент: формата на истинското API, отговорите са записани
record Message(String role, String content) {}
record Usage(int inputTokens, int outputTokens) {}
record Response(String text, String stopReason, Usage usage) {}
// записан отговор: модел ("" = всеки), дума във въпроса, нужен текст в промпта
record Recorded(String model, String keyword, String needs, String text) {}
class FakeLlm {
private final List<Recorded> recorded;
FakeLlm(List<Recorded> recorded) {
this.recorded = recorded;
}
// грубо: всяка дума е 2 токена
static int tokens(String text) {
return 2 * text.trim().split("\\s+").length;
}
Response create(String model, int maxTokens, String system, List<Message> messages) {
String question = messages.get(messages.size() - 1).content();
int input = tokens(system);
for (Message m : messages) input += tokens(m.content());
for (Recorded r : recorded) {
boolean forModel = r.model().isEmpty() || r.model().equals(model);
if (forModel && question.contains(r.keyword()) && system.contains(r.needs()))
return reply(r.text(), maxTokens, input);
}
return reply("(няма записан отговор)", maxTokens, input);
}
// max_tokens реже отговора
private static Response reply(String text, int maxTokens, int input) {
String[] words = text.split(" ");
if (2 * words.length <= maxTokens)
return new Response(text, "end_turn", new Usage(input, 2 * words.length));
int kept = maxTokens / 2;
String cut = String.join(" ", Arrays.copyOf(words, kept));
return new Response(cut, "max_tokens", new Usage(input, 2 * kept));
}
}
static final List<Recorded> RECORDED = List.of(
new Recorded("", "струва", "", "Доставката до адрес е 4,90 €, до офис 3,50 €, а над 50 € е безплатна."),
new Recorded("", "наложен", "", "Да, може с наложен платеж. Таксата е 1,50 €."),
new Recorded("", "върна", "", "Можеш да върнеш стоката до 14 дни след доставката, ако е запазена и с етикета."),
new Recorded("", "работи", "", "Поддръжката работи от понеделник до петък, от 9 до 18 ч."),
new Recorded("", "1042", "", "Поръчка 1042 е изпратена и пристига в четвъртък."),
new Recorded("", "1077", "", "Поръчка 1077 се приготвя и тръгва утре."),
new Recorded("", "повредена", "", "Съжалявам! Снимай пратката и етикета и ни пиши с номера на поръчката. Ще ти изпратим нова или ще върнем парите до 5 работни дни."),
new Recorded("", "дни", "", "Пратките пристигат за 2 до 4 работни дни."),
new Recorded("", "едро", "", "Не знам, ще те свържа с колега."),
new Recorded("", "Mastercard", "", "Да, приемаме Visa и Mastercard."));

Документите на магазина са пет раздела. Всеки има думи, по които се познава:

// раздел от документите и думите, по които се познава
record Doc(List<String> triggers, String text) {}
static final String PROMPT = "Ти си асистентът на поддръжката на онлайн магазин. Отговаряй кратко и само от документите. Ако отговорът го няма, кажи: „Не знам, ще те свържа с колега.“";
static final List<Doc> DOCS = List.of(
new Doc(List.of("доставка", "пратка"), "Доставка: пратките пристигат за 2 до 4 работни дни. До адрес доставката струва 4,90 €, до офис на куриер 3,50 €, а над 50 € е безплатна. Пратката се следи по номера ѝ."),
new Doc(List.of("върна", "повредена"), "Връщане: стоката се връща до 14 дни след доставката, ако е запазена и с етикета. Повредена пратка се снима и се пише на поддръжката. Парите се връщат по картата до 5 работни дни."),
new Doc(List.of("плат", "карт", "mastercard"), "Плащане: приемаме Visa и Mastercard. Наложеният платеж е с такса 1,50 €. Плащането с карта е защитено, а сумата се тегли при потвърждаване на поръчката."),
new Doc(List.of("поръчка"), "Поръчки: статусът на поръчката се вижда в профила. Поддръжката проверява поръчка само по номера ѝ и никога не иска данните на картата."),
new Doc(List.of("работи", "работно"), "Поддръжка: работим от понеделник до петък, от 9 до 18 ч. Пиши ни на help@shop.example."));
static final String ALL_DOCS = String.join(" ", DOCS.stream().map(Doc::text).toList());

Моделите, цените и времената. Всички числа тук са примерни, а времето е по формула, не е мерено:

// примерни цени в € за 1 000 000 токена и примерно време (не са мерени)
record ModelInfo(double inPrice, double outPrice, int baseMs, int inputPerMs, int msPerToken) {}
static final String LARGE = "model-large", SMALL = "model-small";
static final Map<String, ModelInfo> MODELS = Map.of(
LARGE, new ModelInfo(4.00, 16.00, 400, 10, 20),
SMALL, new ModelInfo(0.40, 1.60, 150, 40, 5));
static final int CACHE_MS = 5;
// цената на едно извикване в €
static double cost(ModelInfo m, Usage u) {
return (u.inputTokens() * m.inPrice() + u.outputTokens() * m.outPrice()) / 1_000_000;
}
// примерно време в ms: начало + прочитане на входа + генериране
static int latency(ModelInfo m, Usage u) {
return m.baseMs() + u.inputTokens() / m.inputPerMs() + u.outputTokens() * m.msPerToken();
}

Отговорът за поръчка 1042 е записан. В истинския асистент статусът идва от инструмент (урокът за инструментите).

1_000_000 е същото като 1000000: подчертаването само разделя цифрите. u.inputTokens() / m.inputPerMs() е деление на цели числа и реже остатъка.

Пробвай

Попълни

Цената е токените вход по цената на входа плюс токените изход по…?

return (u.inputTokens() * m.inPrice() + u.outputTokens() * ) / 1_000_000;

Виж го

Един ден

Ден в поддръжката е мостра от 20 въпроса. Някои се повтарят, понякога с малка буква или с излишен интервал. Три са за конкретна поръчка:

List<String> questions = List.of(
"Колко струва доставката?",
"Кога работи поддръжката?",
"Къде е поръчка 1042?",
"колко струва доставката?",
"Мога ли да платя с наложен платеж?",
"До кога мога да върна стоката?",
"Колко струва доставката?",
"Пратката ми дойде повредена, какво да правя?",
"Кога работи поддръжката?",
"Къде е поръчка 1077?",
"Приемате ли Mastercard?",
"Колко струва доставката? ",
"За колко дни идва пратка до Бургас?",
"Мога ли да платя с наложен платеж?",
"Продавате ли на едро?",
"До кога мога да върна стоката?",
"Къде е поръчка 1042?",
"Кога работи поддръжката?",
"Колко струва доставката до офис?",
"Как да върна стоката?");

day пуска въпросите и събира цената и времето. Трите флага включват трите поправки, засега са изключени:

static Totals day(List<String> questions,
boolean budget, boolean cache,
boolean route) {
Map<String, String> answers = new HashMap<>();
double euro = 0;
int ms = 0, hits = 0, onSmall = 0;
for (String question : questions) {
String k = key(question);
// попадение: без модел
if (cache && answers.containsKey(k)) {
hits++;
ms += CACHE_MS;
continue;
}
String model = route ? pickModel(question) : LARGE;
String docs = budget ? relevant(question)
: ALL_DOCS;
int maxTokens = budget ? 200 : 1024;
var msg = List.of(new Message("user", question));
Response r = CLIENT.create(model, maxTokens,
PROMPT + " " + docs, msg);
ModelInfo m = MODELS.get(model);
euro += cost(m, r.usage());
ms += latency(m, r.usage());
if (model.equals(SMALL)) onSmall++;
// поръчката е на един клиент: не се кешира
if (cache && !k.contains("поръчка"))
answers.put(k, r.text());
}
return new Totals(euro, ms, hits, onSmall);
}
static void row(String name, Totals t, int n) {
System.out.printf("%s: %.2f € за 1000 въпроса, средно %d ms%n", name, t.euro() / n * 1000, t.ms() / n);
}
static void month(Totals t, int n) {
System.out.printf("месец при 3000 въпроса на ден: %.2f €%n", t.euro() / n * 3000 * 30);
}
Totals all = day(questions, false, false, false);
row("голям модел, всички документи", all, questions.size());
month(all, questions.size());
Изход
голям модел, всички документи: 1.66 € за 1000 въпроса, средно 917 ms
месец при 3000 въпроса на ден: 149.08 €

Цената за 1000 въпроса е под 2 €. При 3000 въпроса на ден месецът е около 150 €, а един въпрос чака средно почти секунда.

Идеята

Бюджет на токени

Първата поправка е входът. На въпроса за доставката трябва само разделът за доставката. relevant взима разделите, чиито думи има във въпроса, а ако няма нито един, праща всички (въпросът „на едро“ трябва да види всичко, за да каже „Не знам“):

// бюджет: само разделите, чиито думи има във въпроса
static String relevant(String question) {
String low = question.toLowerCase();
List<String> parts = new ArrayList<>();
for (Doc doc : DOCS)
if (doc.triggers().stream().anyMatch(low::contains))
parts.add(doc.text());
return parts.isEmpty() ? ALL_DOCS : String.join(" ", parts);
}

Вторият таван е max_tokens: най-много 200 токена за отговор. Той не прави отговорите по-кратки. Той казва колко най-много може да струва едно извикване.

Totals all = day(questions, false, false, false);
row("голям модел, всички документи", all, questions.size());
Totals budget = day(questions, true, false, false);
row("+ бюджет на токени", budget, questions.size());
Изход
голям модел, всички документи: 1.66 € за 1000 въпроса, средно 917 ms
+ бюджет на токени: 0.90 € за 1000 въпроса, средно 898 ms

Цената пада почти наполовина. Времето почти не мърда: входът се чете бързо, а отговорът е същият.

Тук разделите се избират по думи. Как се избират по смисъл, с вектори, е в модула за търсене по смисъл (RAG).

doc.triggers().stream().anyMatch(low::contains) е вярно, ако поне една дума от раздела се съдържа във въпроса. low::contains е съкратена ламбда: word -> low.contains(word).

Пробвай

Предвиди изхода

max_tokens е 10, а въпросът е за повредена пратка. Какво ще отпечата?

Response r = CLIENT.create(LARGE, 10, PROMPT, List.of(new Message("user", "Пратката ми дойде повредена, какво да правя?")));
System.out.println(r.text());
System.out.println(r.stopReason() + ", " + r.usage().outputTokens());
Отговор

Идеята

Кеш на отговорите

Втората поправка е повторението. Ако същият въпрос е задаван преди малко, отговорът е същият: не е нужно да питаш модела пак. Това е cache-aside от урока „Кеш“: гледаш в кеша, а при пропуск питаш модела и пазиш копие.

Ключът е въпросът с малки букви и по един интервал, така „колко струва доставката? “ е същото като „Колко струва доставката?“:

// ключът на кеша: малки букви, по един интервал
static String key(String question) {
return String.join(" ", question.trim().toLowerCase().split("\\s+"));
}

В day това са редовете с answers: попадението връща записания отговор за 5 ms и без пари. Въпрос за поръчка не се кешира. Отговорът е на един клиент, а статусът се мени.

Totals all = day(questions, false, false, false);
row("голям модел, всички документи", all, questions.size());
Totals budget = day(questions, true, false, false);
row("+ бюджет на токени", budget, questions.size());
Totals cached = day(questions, true, true, false);
row("+ кеш", cached, questions.size());
System.out.println("от кеша: " + cached.hits() + " от " + questions.size());
Изход
голям модел, всички документи: 1.66 € за 1000 въпроса, средно 917 ms
+ бюджет на токени: 0.90 € за 1000 въпроса, средно 898 ms
+ кеш: 0.59 € за 1000 въпроса, средно 568 ms
от кеша: 7 от 20

7 от 20 въпроса идват от кеша. Сега пада и времето, защото попадението не чака модела.

Пробвай

Намери грешката

Колега опростява кеша. Тестовете за цена минават, а след ден клиент се оплаква, че асистентът му е казал статуса на чужда поръчка. На кой ред е грешката?

Избери реда с грешката.

Идеята

По-малък модел за лесното

Третата поправка е моделът. Малкият модел е 10 пъти по-евтин на токен и пише по-бързо. В урока за оценяването наборът показа къде се справя: цената на доставката, плащането, връщането. И къде не: срока на доставката и въпроса извън документите. Затова при него отиват само одобрените теми:

// лесните теми: тестовият набор показа, че малкият модел ги поема
static final List<String> EASY = List.of("струва", "наложен", "върна", "Mastercard");
static String pickModel(String question) {
for (String word : EASY)
if (question.contains(word)) return SMALL;
return LARGE;
}
Totals all = day(questions, false, false, false);
row("голям модел, всички документи", all, questions.size());
Totals budget = day(questions, true, false, false);
row("+ бюджет на токени", budget, questions.size());
Totals cached = day(questions, true, true, false);
row("+ кеш", cached, questions.size());
Totals routed = day(questions, true, true, true);
row("+ малък модел за лесното", routed, questions.size());
System.out.println("при малкия модел: " + routed.onSmall() + " от " + questions.size());
month(all, questions.size());
month(routed, questions.size());
Изход
голям модел, всички документи: 1.66 € за 1000 въпроса, средно 917 ms
+ бюджет на токени: 0.90 € за 1000 въпроса, средно 898 ms
+ кеш: 0.59 € за 1000 въпроса, средно 568 ms
+ малък модел за лесното: 0.35 € за 1000 въпроса, средно 376 ms
при малкия модел: 6 от 20
месец при 3000 въпроса на ден: 149.08 €
месец при 3000 въпроса на ден: 31.75 €

Месецът пада от около 149 € до около 32 €, а средното време от 917 до 376 ms. Числата са от примерни цени и примерна формула, но редът на подобренията е истински.

Виж го

Сметката, поправка по поправка

static Totals day(List<String> questions,
boolean budget, boolean cache,
boolean route) {
Map<String, String> answers = new HashMap<>();
double euro = 0;
int ms = 0, hits = 0, onSmall = 0;
for (String question : questions) {
String k = key(question);
// попадение: без модел
if (cache && answers.containsKey(k)) {
hits++;
ms += CACHE_MS;
continue;
}
String model = route ? pickModel(question) : LARGE;
String docs = budget ? relevant(question)
: ALL_DOCS;
int maxTokens = budget ? 200 : 1024;
var msg = List.of(new Message("user", question));
Response r = CLIENT.create(model, maxTokens,
PROMPT + " " + docs, msg);
ModelInfo m = MODELS.get(model);
euro += cost(m, r.usage());
ms += latency(m, r.usage());
if (model.equals(SMALL)) onSmall++;
// поръчката е на един клиент: не се кешира
if (cache && !k.contains("поръчка"))
answers.put(k, r.text());
}
return new Totals(euro, ms, hits, onSmall);
}
Стъпка по стъпка, с думи
  1. Едно извикване: промптът, всички документи и въпросът влизат (314 токена), отговорът излиза (32). Документите са по-голямата част от сметката: 0,00177 €.
  2. Бюджетът на токени: само разделът, за който пита въпросът, и таван за отговора. 124 токена вход, 0,00101 € за същия отговор.
  3. Ден от 20 въпроса, на 1000 въпроса: 1,66 € с всички документи, 0,90 € с бюджета. Времето почти не мърда: то е най-вече отговорът.
  4. Кешът: 7 от 20 въпроса са повторение и не стигат до модела. Въпрос за поръчка не се кешира.
  5. Малкият модел поема лесните теми, които тестовият набор одобри: 6 от 20 въпроса. Той е по-евтин и по-бърз.
  6. От 1,66 € до 0,35 € за 1000 въпроса (79% по-малко) и от 917 до 376 ms. Цените и времената са примерни.

Идеята

Пусни при теб

Истинският отговор казва колко токена е използвал. Ето заявката с бюджета за въпроса за доставката и отговора. Текстът и бройките са от фалшивия клиент:

Заявка
{
"model": "<името на модела>",
"max_tokens": 200,
"system": "<системният промпт>",
"messages": [
{
"role": "user",
"content": "Колко струва доставката?"
}
]
}
Отговор
{
"content": [
{
"type": "text",
"text": "Доставката до адрес е 4,90 €, до офис 3,50 €, а над 50 € е безплатна."
}
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 124,
"output_tokens": 32
}
}

Полетата са usage.input_tokens и usage.output_tokens, а stop_reason казва дали отговорът е цял (end_turn) или таванът го е отрязал (max_tokens). С тях програмата смята същата сметка като тук, само че с истински бройки.

Доставчиците имат официални SDK-та за Java, C#, Python и JavaScript със същата форма: съобщения, роли, токени, инструменти. Точните имена на класовете и методите са в документацията на доставчика. Тук заявката е пратена със стандартния HTTP клиент на езика, без SDK. Адресът и моделът са заместители:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
// само за показ: адресът и заглавката за ключа са в документацията на доставчика
static final String ENDPOINT = "https://<доставчик>/...";
static String quote(String s) {
return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
static String ask(String system, String question) throws Exception {
String body = "{\"model\": " + quote("<името на модела>")
+ ", \"max_tokens\": 200, \"system\": " + quote(system)
+ ", \"messages\": [{\"role\": \"user\", \"content\": "
+ quote(question) + "}]}";
String key = System.getenv("LLM_API_KEY");
HttpRequest request = HttpRequest.newBuilder(URI.create(ENDPOINT))
.header("content-type", "application/json")
.header("authorization", "Bearer " + key)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
return response.body(); // JSON: content, stop_reason, usage
}
// в Java няма JSON парсер в стандартната библиотека: usage и stop_reason са в отпечатания JSON
System.out.println(ask(PROMPT + " " + relevant(question), question));

Този код не се пуска в проверката на сайта. Ключът е в променливата на средата LLM_API_KEY, а не в кода (урокът „Тайни в кода и в git“), ти трябва акаунт при доставчик и всяко пускане струва пари.

quote слага низа в кавички и екранира \ и ". В стандартната библиотека на Java няма JSON парсер: полетата на отговора чете JSON библиотека.

Идеята

Кога вреди

  • Бюджет, който реже нужното. Ако relevant пропусне раздела с отговора, моделът казва „Не знам“ или, по-лошо, измисля. Твърде нисък max_tokens дава половин отговор. Всяка смяна на бюджета минава през тестовия набор.
  • Кеш, който лъже. Цената на доставката се сменя, а кешът дава старата. Кешът има срок (TTL) и се трие, когато документите се сменят. Отговори, които зависят от клиента или от времето, не влизат в него.
  • Малък модел без набор. „Изглежда по-просто“ не е довод. Малкият модел поема тема само след като наборът я е минал, и наборът се пуска пак при всяка смяна.
  • Пестене, което не си струва. Ако асистентът получава 50 въпроса на ден, сметката е няколко евро на месец. Сложността на кеша и на рутера може да струва повече от тях. Първо мери.
По-точно, за любопитните

Кеш на началото на промпта. Много доставчици пазят повтарящото се начало на промпта (системния промпт и документите) и таксуват повторното му четене по-евтино. Това е друг кеш: моделът пак отговаря, но входът е по-евтин. Той работи, ако началото е еднакво до последния символ, затова променливите части (въпросът, името на клиента) са накрая.

Кеш по смисъл. „Как да върна стоката?“ и „До кога мога да върна стоката?“ са различни ключове. Кеш с вектори (урокът за векторите) ги смята за един въпрос. Той намира повече попадения, но и грешни: „Мога ли да върна обувки?“ и „Мога ли да върна подарък?“ са близки, а отговорите може да са различни.

Поточен отговор. Отговорът може да идва дума по дума (streaming). Общото време е същото, но клиентът вижда първите думи след стотина ms вместо след секунда. Това е бързина за човека, не за сметката.

Партиди. Работа, която не чака клиент (обобщение на 10 000 отзива през нощта), може да мине през партиден режим: по-бавно и по-евтино.

Времето в продукция. Средното време крие опашката: 95% от отговорите може да са под секунда, а 5% над 5 секунди. Мери процентили, както в урока „Нефункционални изисквания, които се мерят“ от „Софтуерно инженерство“. Самото следене на цената и времето в продукция е част от MLOps и се учи в ZeroToOps.

Map.of(...) прави неизменим речник, а String.join(" ", ...) слепва текстовете с интервал.

Упражнение

Коя поправка пести най-много

Реши задачата в редактора си.

Програмата пуска поправките натрупано, затова не се вижда колко дава всяка сама. Пусни day три пъти, всеки път само с един флаг, и отпечатай редовете до този без поправки:

Изход
голям модел, всички документи: 1.66 € за 1000 въпроса, средно 917 ms
само бюджет: 0.90 € за 1000 въпроса, средно 898 ms
само кеш: 1.06 € за 1000 въпроса, средно 580 ms
само малък модел: 0.82 € за 1000 въпроса, средно 544 ms

Сам по себе си най-евтин е малкият модел, а бюджетът е малко след него. Бюджетът почти не пести време, а кешът и малкият модел го смъкват най-много, защото не чакат големия модел да пише. Трите пестят от различни места, затова заедно дават най-много.

Още задачи

  • Още една лесна тема. Добави „работи“ в лесните теми. С колко пада цената? Какво трябва да направиш, преди да го пуснеш? (Подсказка: урокът за оценяването.)
  • Таван за деня. Спри да питаш големия модел, когато цената за деня мине 0,01 €, и отговаряй „Ще те свържа с колега“. Колко въпроса остават без отговор от модела?

Тест

Провери се

1. Въпросът е 6 токена, а сметката на извикването е за 314 токена вход. Откъде са останалите?
2. Бюджетът на токени свали цената почти наполовина, а средното време почти не мръдна. Защо?
3. Кой въпрос не бива да отива в кеша на отговорите?
4. Как избираш кои въпроси да поеме малкият модел?
5. max_tokens е 10, а отговорът трябва да е 30 токена. Какво получаваш?

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