RAG (Retrieval-Augmented Generation), Function / Tools Calling, CAG (Cache-Augmented Generation) для LLM моделей
Привіт!
Спробую викласти тут свій досвід із реалізацією RAG та функціональних викликів для LLM моделі, запущеної локально на телефоні.
Для cloud моделей загальна ідея не відрізнятиметься, тож почитавши, ви зможете зрозуміти, як це працює і для чого використовується.
Цікаво тут те, що успішність роботи сильно залежить від правильності написання промпту того, що ми хочемо від моделі, тобто просто хороший код тут не працює, потрібна ще гуманітарна частина- пояснити все моделі й сподіватися на те, що вона правильно все зрозуміла.
Що таке RAG (Retrieval-Augmented Generation)
У LLM модель під час навчання поміщаються терабайти інформації, але інформацію про вас, ваш проєкт, ваші документи та вашого кота вона може не знати.
Ви можете описати все це в запиті до моделі, у промпті, але якщо інформації багато- розмір контексту моделі може закінчитися до того, як ви дійдете до питання, або вам набридне набирати текст.
Щоб модель знала інформацію, на якій вона не була навчена, використовується RAG (Retrieval Augmented Generation).
У RAG може бути багато різних реалізацій, але загальний сенс такий:
До вашого повідомлення додається додаткова інформація, зазвичай у текстовому вигляді, наприклад:
- Результат інтернет запиту
- Дані з бази даних
- Текст із прикріпленого документа, і так далі
У цьому прикладі я використовуватиму векторну базу, яка чудово підходить для роботи з LLM моделями.
Що таке векторна база даних
Перед запитами до моделі потрібно згодувати базі даних ваші дані, щоб потім модель могла знайти найбільш підхожі з них для вашого запиту.
Під час додавання до бази тексту:
- Текст розбивається на шматки
- За допомогою спеціальної embedding моделі Gecko для кожного шматка генерується вектор із 768 вимірами, сенс якого максимально відповідає вмісту шматка тексту
- Вектор разом із текстом записується до бази
Під час пошуку тексту в базі:
- Пошуковий запит за допомогою моделі Gecko перетворюється на вектор
- У базі шукається вектор, максимально близький до вектора пошукового запиту
- Для цього вектора витягується текст
- Текст додається до вашого запиту до LLM моделі
Що таке Function Calling
Різниця з RAG у тому, що в цьому випадку модель сама вирішує, звертатися їй до векторної бази чи іншого джерела для отримання інформації.
Для цього:
- у промпті вказується, що модель має доступ до функціональних викликів, описується їхній формат, описуються умови, за яких моделі потрібно робити цей виклик
- якщо модель вирішує зробити такий виклик- вона повертає службову інформацію із запитом на виклик
- парсер розуміє, що потрібно зробити виклик, читає дані з векторної бази, додає їх до черги повідомлень і надсилає запит знову.
Що таке CAG (Cache-Augmented Generation)
Техніка для прискорення та здешевлення роботи LLM, заснована на повторному використанні результатів минулих запитів.
LLM під час роботи зберігає проміжні представлення тексту (hidden states, attention-кеші).
За повторних або схожих запитів не потрібно повністю проганяти всю послідовність через модель. Замість цього береться збережений кеш і модель доопрацьовує лише «нову» частину.
Це скорочує кількість обчислень і знижує затримку.
Варіанти застосування:
Token-level caching:
зберігаються KV-кеші уваги для вже оброблених токенів (уже використовується в llama.cpp, Transformers та інших).
Prompt-level caching:
результати для промптів, що часто трапляються, зберігаються і використовуються повторно.
Semantic caching:
зберігання відповідей за смисловою схожістю (наприклад, через векторне сховище). Якщо новий запит схожий на старий, повертається збережений результат або він використовується як контекст.
Різниця зі звичайним RAG:
RAG (Retrieval-Augmented Generation) підтягує зовнішні дані з бази знань.
CAG заощаджує обчислення за рахунок повторного використання минулих прогонів моделі.
Приклад 1 - Низькорівнева реалізація Function Calling на Kotlin
Тут я наведу приклад, як працюють Function Calling з погляду реалізації.
Спочатку робимо темплейт, який максимально докладно пояснить LLM моделі, що вона має доступ до функціональних викликів, і як з ними працювати.
Дуже важливо написати ключове слово, за яким ми потім зможемо визначити, що модель хоче зробити функціональний виклик, у цьому прикладі це tool_code, воно має бути унікальним і не повинно траплятися в тексті.
Далі ми описуємо структури, які ми хочемо отримати, у цьому прикладі це Json, але структура має бути будь-якою, я використовувати Json тому, що його легко парсити в клас, але з погляду використання токенів він не оптимальний.
Є структура складна і є ймовірність того, що модель помилиться, можна повертати помилку парсингу, додавати її до черги повідомлень разом з описом, що модель помилилася і має виправити запит, і надіслати моделі для нової спроби.
У цьому прикладі я використав 2 функціональні виклики
get_weather
{ "name": "get_weather", "arguments": { "location": "New York" }}Це дуже простий виклик з єдиним параметром, з яким важко помилитися.
search_docs
{ "name": "search_docs", "arguments": { "query": "quantum computing basics", "top_k": 5, "min_similarity_score": 0.6 }}Це пошуковий запит до бази, у якій шукатимуться підхожі документи.
Цей виклик можна помістити в цикл зі стартовими параметрами і додати до промпту опис- якщо модель не отримає підхожі результати, вона може зменшити min_similarity_score або змінити формулювання query і зробити виклик знову.
Дуже важливо в промпті описати докладні інструкції для моделі для кожного кроку, модель не має мотивації та інтуїції для того, щоб зрозуміти, що вона має робити далі, і якщо подальші кроки не будуть описані- вона поверне порожнє повідомлення.
Також важливо розуміти, що промпт- це не програмний код, і його правильне виконання має ймовірність, яка залежить від того, наскільки добре ми описали всі дії та структури.
Наприкінці промпту важливо написати, що модель отримає результат у форматі Json, структура якого не описана в промпті, і модель сама має зрозуміти, як сформулювати відповідь для користувача відповідно до його запиту.
Сценарій у промпті має практично нескінченні можливості кастомізації, наприклад якщо нам необхідно отримати дані з бази даних, ми можемо описати структуру бази і попросити модель згенерувати SQL запит відповідно до запиту користувача, який потім виконати й отримати дані, у цьому випадку важливо не забути обмежити права підключення тільки на читання.
Деякі моделі треновані на виконання функціональних викликів, певного формату та ключових слів, у них також може бути окрема роль для результату виклику, наприклад function. Я використовував модель, яка не була тренована на це, і вона чудово справлялася, я думаю, будь-яка сучасна більш-менш "розумна" модель впорається.
Повний текст промпту:
const val DEFAULT_FUNCTION_CALLING_PROMPT = """Call tools only when the user explicitly asks to check, verify, look up, find, or search for something. In all other cases, answer directly without calling any tool.
If you decide to invoke a function, output only a JSON object inside a fenced block marked as tool_code without any text before or after it.
Available tools:
search_docsDescription: Searches knowledge and returns the most relevant results as plain text.Arguments:query (string) — User string query to search in the vector store.top_k (integer, default 5) — Number of results to return.min_similarity_score (float, default 0.6) — Minimum similarity score from 0.0 (no filtering) to 1.0 (exact match).
get_weatherDescription: Gets current weather for a location.Arguments:location (string) — City or place name.
JSON format inside the fenced block:tool_code{"name": "<tool_name>","arguments": {"<arg_name>": <arg_value>}}
Examples:tool_code{ "name": "search_docs", "arguments": { "query": "quantum computing basics", "top_k": 5, "min_similarity_score": 0.6 } }
tool_code{ "name": "get_weather", "arguments": { "location": "New York" } }
Important rules:Use tools only when explicitly requested by the user to check, verify, look up, find, or search. Otherwise, provide a direct answer.When calling a tool, the output must be valid JSON with exactly two keys: name and arguments.Do not include any explanations or extra content outside the tool_code block.
After receiving tool results (the result will be in JSON format):1. Parse and process the result.2. Convert it into a simple, clear, and human-readable natural-language response.3. Always reply to the user with this processed answer as the next message.4. If no relevant results are found, briefly explain that nothing relevant was found and suggest next steps."""У цьому прикладі я використовував локальні моделі Gemma та DeepSeek, запущені в LM Studio, який має OpenAI сумісне API, тобто мій застосунок підключається до нього через Localhost і робить запити, cloud моделі впораються ще краще, оскільки мають набагато більше параметрів і спеціальне API для функціональних викликів, але в прикладі я спеціально захотів використати найбільш низькорівневу реалізацію.
fun main() = runBlocking {
val messages = mutableListOf(
// Додаємо промпт у чергу повідомлень з роллю system ChatMessage( role = "system", content = DEFAULT_FUNCTION_CALLING_PROMPT ),
// Додаємо в чергу запит користувача, за цим запитом зрозуміло, // що користувач просить перевірити погоду, модель знає, що їй доступна // функція перевірки погоди, описана в промпті, і викликає її, // надіславши у відповідь повідомлення, що починається з tool_code ChatMessage( role = "user", content = "Check weather in London", ) ) var iterator = 0 while (iterator < 3) { try { chatAnswerWithToolsStream( ChatCompletionRequest( model = MODEL_ID, messages = messages, temperature = 0.2 ) ).collect { piece -> when (piece) { is ChatResult.Debug -> { println("DEBUG MESSAGE:\n${piece.message}") }
is ChatResult.Message -> { println("FINAL MESSAGE:\n${piece.message}") iterator = 3 return@collect }
is ChatResult.SearchDocs -> { println("SEARCH DOCS data:\n$piece") iterator = 3
// У цьому прикладі я буду використовувати лише перевірку погоди, // але додав ще одну функцію, щоб показати, що їх може бути багато return@collect }
is ChatResult.GetWeather -> { println("GET WEATHER result:\n$piece")
// Якщо модель вирішила викликати функцію, додаємо її виклик // у чергу повідомлень з роллю assistant // Важливо додавати всі повідомлення в чергу, щоб // модель знала всю історію діалогу messages.add( ChatMessage( role = "assistant", content = piece.message ?: "" ) )
// запитуємо API отримання даних про погоду val result = getCurrentWeatherByCity( city = piece.location!!, ) val jsonResult: String = kotlinxJsonConfig.encodeToString(result) println("GET WEATHER request result:\n$jsonResult")
// додаємо в чергу повідомлень з роллю user, оскільки ця модель // не підтримує спеціальні ролі для функціональних викликів messages.add( ChatMessage( role = "user", content = jsonResult ) ) iterator++ } } } } catch (e: Throwable) { println(e.message ?: "error") break } }
}
// код для роботи з OpenAI API, тут важливий виклик функції// parseToolJson, яка поверне null, якщо не вдалося перетворити відповідь на функціональний викликfun chatAnswerWithToolsStream(req: ChatCompletionRequest): Flow<ChatResult> = flow { val response = client.post("$BASE_URL/api/v0/chat/completions") { contentType(ContentType.Application.Json) setBody(req.copy(stream = true)) } val channel: ByteReadChannel = response.bodyAsChannel()
val fullBuilder = StringBuilder()
while (!channel.isClosedForRead) { val line: String = channel.readUTF8Line() ?: break val payload: String = extractDataLine(line) ?: continue
val chunk: StreamChatChunk = runCatching { kotlinxJsonConfig.decodeFromString(StreamChatChunk.serializer(), payload) }.getOrNull() ?: continue
val piece: String = extractChatDeltaContent(chunk) if (piece.isNotEmpty()) { fullBuilder.append(piece) emit(ChatResult.Debug(message = fullBuilder.toString())) }
if (isFinished(chunk)) break }
val fullMessage: String = fullBuilder.toString().trim() val toolResult: ChatResult? = parseToolJson(fullMessage)
if (toolResult != null) { emit(toolResult) } else { emit(ChatResult.Message(message = fullMessage)) }}
fun parseToolJson(input: String): ChatResult? { try { val json = when {
// якщо відповідь починається з tool_code, ми розуміємо, що це не відповідь користувачу // а функціональний виклик і далі йтиме json з його типом і параметрами input.startsWith("tool_code") -> input.replace("tool_code", "") .trim()
input.startsWith("```tool_code") -> input.replace("```tool_code", "") .removeSuffix("```") .trim()
else -> return null } val rawTool = runCatching { gson.fromJson(json, RawTool::class.java) }.getOrNull() ?: return null
val jsonObject = rawTool.arguments?.asJsonObject ?: return null
// визначаємо, яку саме функцію модель хоче викликати // і перетворюємо json на модель даних. // Цей код можна покращити, повернувши моделі помилку, якщо перетворення // не вдасться, з описом, що модель має відредагувати свою відповідь і виправити її return when (rawTool.name?.lowercase()) { "search_docs" -> { gson.fromJson(jsonObject, ChatResult.SearchDocs::class.java).copy( message = json ) } "get_weather" -> gson.fromJson(jsonObject, ChatResult.GetWeather::class.java).copy( message = json ) else -> null } } catch (e: Exception) { println(e.message ?: "error") return null }}Додатковий код, якщо захочете повторити:
ext { ktor_version = "3.2.3" kotlinx_serialization_json_version = "1.8.1" logback_version = "1.4.14"}
dependencies { implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-cio:$ktor_version" implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" implementation "io.ktor:ktor-client-logging:$ktor_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_json_version" implementation "ch.qos.logback:logback-classic:$logback_version" implementation "com.google.code.gson:gson:2.13.1"}import io.ktor.client.call.*import io.ktor.client.request.*
suspend fun getCurrentWeatherByCity( apiKey: String = WEATHER_API_KEY, city: String, units: String = "metric", lang: String = "en"): WeatherResponse { return client.get(OWM_BASE) { url { parameters.append("q", city) parameters.append("appid", apiKey) parameters.append("units", units) parameters.append("lang", lang) } }.body()}import com.google.gson.JsonElementimport kotlinx.serialization.SerialNameimport kotlinx.serialization.Serializableimport kotlinx.serialization.json.JsonObject
@Serializabledata class ChatMessage( @SerialName("role") val role: String, @SerialName("content") val content: String)
@Serializabledata class ChatCompletionRequest( @SerialName("model") val model: String, @SerialName("messages") val messages: List<ChatMessage>, @SerialName("temperature") val temperature: Double? = null, @SerialName("max_tokens") val maxTokens: Int? = null, @SerialName("stream") val stream: Boolean? = null)
@Serializabledata class StreamChatChunk( @SerialName("choices") val choices: List<StreamChatChoice>? = null)
@Serializabledata class ChatChoiceMessage( @SerialName("content") val content: String? = null)
@Serializabledata class ChatChoice( @SerialName("message") val message: ChatChoiceMessage? = null)
@Serializabledata class ChatResponse( @SerialName("choices") val choices: List<ChatChoice> = emptyList())
@Serializabledata class StreamChatChoice( @SerialName("delta") val delta: JsonObject? = null, @SerialName("finish_reason") val finishReason: String? = null)
data class RawTool( val name: String?, val arguments: JsonElement?)
sealed interface ChatResult {
data class Debug( val message: String?, ) : ChatResult
data class Message( val message: String?, ) : ChatResult
data class SearchDocs( val message: String? = null, val query: String?, val topK: Int?, val minSimilarityScore: Float? ) : ChatResult
data class GetWeather( val message: String? = null, val location: String?, ) : ChatResult
}import kotlinx.serialization.json.JsonElementimport kotlinx.serialization.json.contentOrNullimport kotlinx.serialization.json.jsonPrimitive
fun extractDataLine(line: String): String? { val t: String = line.trim() if (t.isEmpty()) return null if (t == "data: [DONE]" || t == "[DONE]") return null if (t.startsWith("event:")) return null return if (t.startsWith("data:")) t.removePrefix("data:").trim() else t}
fun extractChatDeltaContent(chunk: StreamChatChunk): String { val choice: StreamChatChoice = chunk.choices?.firstOrNull() ?: return "" val delta = choice.delta ?: return "" val el: JsonElement? = delta["content"] return el?.jsonPrimitive?.contentOrNull ?: ""}
fun extractTextDelta(chunk: StreamTextChunk): String { val choice: StreamTextChoice = chunk.choices?.firstOrNull() ?: return "" val direct: String? = choice.text if (direct != null) return direct val delta = choice.delta val el: JsonElement? = delta?.get("content") ?: delta?.get("text") return el?.jsonPrimitive?.contentOrNull ?: ""}
fun isFinished(chunk: StreamChatChunk): Boolean { val reason: String? = chunk.choices?.firstOrNull()?.finishReason return !reason.isNullOrEmpty()}
fun isFinished(chunk: StreamTextChunk): Boolean { val reason: String? = chunk.choices?.firstOrNull()?.finishReason return !reason.isNullOrEmpty()}import com.google.gson.FieldNamingPolicyimport com.google.gson.Gsonimport com.google.gson.GsonBuilderimport io.ktor.client.*import io.ktor.client.engine.cio.*import io.ktor.client.plugins.*import io.ktor.client.plugins.contentnegotiation.*import io.ktor.serialization.kotlinx.json.*import kotlinx.serialization.json.Json
const val BASE_URL: String = "http://localhost:1234"
const val OWM_BASE = "https://api.openweathermap.org/data/2.5"
const val WEATHER_API_KEY = "..."
const val MODEL_ID = "google/gemma-3n-e4b"
val gson: Gson = GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create()
val kotlinxJsonConfig: Json = Json { ignoreUnknownKeys = true prettyPrint = false}
val client: HttpClient = HttpClient(CIO) { install(ContentNegotiation) { json(kotlinxJsonConfig) } install(HttpTimeout) { requestTimeoutMillis = 60_000 connectTimeoutMillis = 10_000 socketTimeoutMillis = 60_000 }}Приклад 2 - Python фреймворк Dspy
DSPy — це open-source Python-фреймворк для «програмування, а не промптингу» LLM: ви описуєте поведінку модулів у коді, а оптимізатори автоматично добирають підказки і, за бажання, донавчають ваги під обрану метрику. Підходить для класифікаторів, RAG та агентів.
dspy.ai
https://github.com/stanfordnlp/dspy
Signatures:
декларативні специфікації входів/виходів модулів.
Modules:
готові стратегії виклику LLM (Predict, ChainOfThought, ReAct та ін.) з яких збирають пайплайни.
- Predict: Базовий будівельний блок. Робить запит до LLM за заданою сигнатурою (input -> output). Використовується для простих завдань типу QA, сумаризації, класифікації.
- Signature: Визначає структуру вхідних та вихідних даних. Можна використовувати як "q -> a" або як Python-клас з анотаціями. Робить код читабельним і строгим.
- Chain: Дозволяє зв'язати кілька Predict у pipeline. Зручно, коли є проміжні кроки (витягання фактів → резюме → відповідь).
- ReAct: Агентний режим: модель міркує і викликає інструменти. Дозволяє підключати API, бази даних, зовнішні функції. Типовий варіант для чат-ботів з доступом до тулзів.
- ChainOfThought (CoT): Змушує модель пояснювати кроки міркування. Зручно для складних завдань, математики, логіки.
- Retrieve / RAG: Інтеграція з векторними базами даних. Дозволяє додавати пошук по документах у workflow.
Optimizers (раніше teleprompters):
алгоритми, які «компілюють» програму в ефективні підказки/ваги за метрикою; працюють навіть із 5–10 прикладами.
Приклад аналогічний за функціями попередньому:
requirements.txt
dspy==3.0.1litellm==1.75.8openai==1.99.9optuna>=4.5.0gepa[dspy]==0.0.4regex>=2025.7.34diskcache>=5.6.3json-repair>=0.49.0magicattr>=0.1.6backoff>=2.2.1asyncer==0.0.8cachetools>=6.1.0aiohttp>=3.12.15import dspyimport requestsfrom typing import Dict
dspy.enable_logging()
OPENWEATHER_API_KEY = "..."
# Задаємо провайдер для LLMlm = dspy.LM( "openai/google/gemma-3n-e4b", api_base="http://localhost:1234/v1", api_key="lm-studio", temperature=0)dspy.configure(lm=lm)
# Перевіряємо, чи знайшовся провайдерprint("Provider:\n", type(lm.provider))
# Надсилаємо моделі тестові дані, використовуючи модуль Predictprobe = dspy.Predict("question -> answer")try: result = probe(question="Who are you", max_tokens=1) print("Model supported by provider, test answer:\n", result.answer)except Exception as e: print("Model not supported by provider:\n", e)
def get_weather(city: str, units: str = "metric") -> str: url = "https://api.openweathermap.org/data/2.5/weather" params = { "q": city, "appid": OPENWEATHER_API_KEY, "units": units, "lang": "en" } response = requests.get(url, params=params) if response.status_code == 200: data: Dict = response.json() temp = data["main"]["temp"] description = data["weather"][0]["description"] return f"Weather in {city}: {temp}°C, {description}" else: return f"Error fetching weather: {response.text}"
# Додаємо формат спілкування питання -> відповідь і# інструмент get_weather і модуль ReAct# Фреймворк сам згенерує промпт відповідно# до того, який модуль і з якими параметрами ми використовуємоagent = dspy.ReAct( "question -> answer", tools=[get_weather])
result = agent(question="Check weather in London")
print("Answer:\n", result.answer)
# Можна подивитися історію спілкування з LLM,# які tools викликалися і який prompt згенерувавсяhistory = dspy.inspect_history(n=10)Виведення в консоль:
Provider: <class 'dspy.clients.openai.OpenAIProvider'>Model supported by provider, test answer: I am Gemma, an open-weights AI assistant. I am a large language model trained by Google DeepMind.Answer: Weather in London: 15.89°C, overcast cloudsЛог:
-
модель відповідає на просте питання без інструментів (
Who are you). Тут вона одразу дає[[ ## answer ## ]]. -
система ставить завдання: "У тебе є інструменти get_weather/finish". Модель думає і видає перший крок:
next_tool_name = get_weather. -
модель отримує trajectory з результатом API (
Weather in London: ...) і має вирішити, що робити далі. Вона пишеnext_tool_name = finish. -
система підставляє trajectory з обома кроками (
get_weather+finish). Модель має видати підсумковий[[ ## answer ## ]].
Тестове повідомлення:
System message:
Your input fields are:1. `question` (str):Your output fields are:1. `answer` (str):All interactions will be structured in the following way, with the appropriate values filled in.[[ ## question ## ]]{question}[[ ## answer ## ]]{answer}[[ ## completed ## ]]In adhering to this structure, your objective is: Given the fields `question`, produce the fields `answer`.
User message:
[[ ## question ## ]]Who are youRespond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.
Response:[[ ## answer ## ]]I am Gemma, an open-weights AI assistant. I am a large language model trained by Google DeepMind.[[ ## completed ## ]]Основне повідомлення:
тут моделі пояснюються Tools які вона може використовувати - get_weather і finish.
Від моделі очікуються next_thought, next_tool_name, next_tool_args
Модель відповідає викликом [[ ## next_tool_name ## ]] get_weather з параметрами [[ ## next_tool_args ## ]] {"city": "London"}
System message:
Your input fields are:1. `question` (str):2. `trajectory` (str):Your output fields are:1. `next_thought` (str):2. `next_tool_name` (Literal['get_weather', 'finish']):3. `next_tool_args` (dict[str, Any]):All interactions will be structured in the following way, with the appropriate values filled in.[[ ## question ## ]]{question}[[ ## trajectory ## ]]{trajectory}[[ ## next_thought ## ]]{next_thought}[[ ## next_tool_name ## ]]{next_tool_name} # note: the value you produce must exactly match (no extra characters) one of: get_weather; finish[[ ## next_tool_args ## ]]{next_tool_args} # note: the value you produce must adhere to the JSON schema: {"type": "object", "additionalProperties": true}[[ ## completed ## ]]In adhering to this structure, your objective is: Given the fields `question`, produce the fields `answer`.
You are an Agent. In each episode, you will be given the fields `question` as input. And you can see your past trajectory so far. Your goal is to use one or more of the supplied tools to collect any necessary information for producing `answer`.
To do this, you will interleave next_thought, next_tool_name, and next_tool_args in each turn, and also when finishing the task. After each tool call, you receive a resulting observation, which gets appended to your trajectory.
When writing next_thought, you may reason about the current situation and plan for future steps. When selecting the next_tool_name and its next_tool_args, the tool must be one of:
(1) get_weather. It takes arguments {'city': {'type': 'string'}, 'units': {'type': 'string', 'default': 'metric'}}. (2) finish, whose description is <desc>Marks the task as complete. That is, signals that all information for producing the outputs, i.e. `answer`, are now available to be extracted.</desc>. It takes arguments {}. When providing `next_tool_args`, the value inside the field must be in JSON format
User message:
[[ ## question ## ]]Check weather in London[[ ## trajectory ## ]]Respond with the corresponding output fields, starting with the field `[[ ## next_thought ## ]]`, then `[[ ## next_tool_name ## ]]` (must be formatted as a valid Python Literal['get_weather', 'finish']), then `[[ ## next_tool_args ## ]]` (must be formatted as a valid Python dict[str, Any]), and then ending with the marker for `[[ ## completed ## ]]`.
Response:
[[ ## next_thought ## ]]I need to check the weather in London. I should use the get_weather tool for this.[[ ## next_tool_name ## ]]get_weather[[ ## next_tool_args ## ]]{"city": "London"}[[ ## completed ## ]]Далі програма робить виклик до API погоди, отримуємо результат і додає його до наступного повідомлення.
Модель вирішує, що далі потрібно викликати [[ ## next_tool_name ## ]] finish
На цьому етапі модель ще агент і вирішила, який тул викликати далі.
User message:
[[ ## question ## ]]Check weather in London[[ ## trajectory ## ]][[ ## thought_0 ## ]]I need to check the weather in London. I should use the get_weather tool for this.[[ ## tool_name_0 ## ]]get_weather[[ ## tool_args_0 ## ]]{"city": "London"}
[[ ## observation_0 ## ]]Weather in London: 23.75°C, overcast cloudsRespond with the corresponding output fields, starting with the field `[[ ## next_thought ## ]]`, then `[[ ## next_tool_name ## ]]` (must be formatted as a valid Python Literal['get_weather', 'finish']), then `[[ ## next_tool_args ## ]]` (must be formatted as a valid Python dict[str, Any]), and then ending with the marker for `[[ ## completed ## ]]`.
Response:
[[ ## next_thought ## ]]The weather for London has been retrieved. I can now finish the task.[[ ## next_tool_name ## ]]finish[[ ## next_tool_args ## ]]{}[[ ## completed ## ]]Далі останній виклик, модель уже асистент, видає reasoning + фінальну відповідь.
Промпт також змінився.
System message:
Your input fields are:1. `question` (str):2. `trajectory` (str):Your output fields are:1. `reasoning` (str):2. `answer` (str):All interactions will be structured in the following way, with the appropriate values filled in.[[ ## question ## ]]{question}[[ ## trajectory ## ]]{trajectory}[[ ## reasoning ## ]]{reasoning}[[ ## answer ## ]]{answer}[[ ## completed ## ]]In adhering to this structure, your objective is:Given the fields `question`, produce the fields `answer`.
User message:
[[ ## question ## ]]Check weather in London[[ ## trajectory ## ]][[ ## thought_0 ## ]]I need to check the weather in London. I should use the get_weather tool for this.[[ ## tool_name_0 ## ]]get_weather[[ ## tool_args_0 ## ]]{"city": "London"}[[ ## observation_0 ## ]]Weather in London: 23.75°C, overcast clouds[[ ## thought_1 ## ]]The weather for London has been retrieved. I can now finish the task.[[ ## tool_name_1 ## ]]finish[[ ## tool_args_1 ## ]]{}[[ ## observation_1 ## ]]Completed.Respond with the corresponding output fields, starting with the field `[[ ## reasoning ## ]]`, then `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.
Response:
[[ ## reasoning ## ]]The question asks to check the weather in London. I need to use a tool that can provide weather information. The `get_weather` tool is suitable for this task, and I should provide the city as "London". After retrieving the weather information, I will finish the task.[[ ## answer ## ]]Weather in London: 23.75°C, overcast clouds[[ ## completed ## ]]Чому викликів із finish два:
Причина в архітектурі агентного циклу в DSPy.
Виклик із next_tool_name:
Цей крок моделюється як "дія агента". Агент завжди працює у форматі:
думаю → обираю тул → викликаю тул → отримую observation.
Навіть якщо тул = finish, формально це все одно вважається дією.
Виклик із reasoning + answer:
Після того як агент "завершив завдання", система робить окремий запит, де контекст включає всю trajectory.
Ця двокрокова схема — спосіб розділити ролі:
один формат спілкування, коли модель — агент, що керує тулзами;
інший формат, коли модель — асистент, який відповідає користувачеві.
Далі в LangChain буде приклад з 1м фінальним повідомленням.
Приклад 3 - Python фреймворк LangChain
LangChain — це фреймворк для роботи з LLM (Large Language Models), який допомагає будувати складні застосунки поверх моделей, а не просто «питання → відповідь».
Його завдання — дати зручний шар поверх моделі, щоб вона могла:
працювати з інструментами (наприклад, API, бази даних, калькулятор);
зберігати контекст і пам'ять (чати, історія діалогу);
використовувати ланцюжки (chains) — послідовність кроків, де модель виконує одне завдання і передає результат у наступне;
запускати агентів (agents), які самі вирішують, які інструменти їм використовувати і в якому порядку;
інтегруватися з популярними LLM-провайдерами (OpenAI, Anthropic, LM Studio, HuggingFace та ін.);
працювати з retrieval та RAG (діставати знання з документів або векторних баз).
https://github.com/langchain-ai/langchain
https://en.wikipedia.org/wiki/LangChain
Приклад аналогічний за функціями попередньому:
requirements.txt
langchain-core==0.3.74langchain-openai==0.3.30langchain==0.3.27openai==1.100.1requests==2.32.5import requestsfrom typing import Dictfrom langchain_core.tools import toolfrom langchain_openai import ChatOpenAIfrom langchain import hubfrom langchain.agents import AgentExecutor, create_react_agentfrom langchain_core.callbacks.base import BaseCallbackHandler
OPENWEATHER_API_KEY = "..."
# Callback для логуванняclass CustomLogger(BaseCallbackHandler): def on_tool_start(self, serialized, input_str, **kwargs): print(f"\nTOOL START:\n{serialized.get('name')}\nINPUT:\n{input_str}\n")
def on_tool_end(self, output, **kwargs): print(f"\nTOOL END:\n{output}\n")
def on_llm_start(self, serialized, prompts, **kwargs): print(f"\nLLM START:\n{serialized.get('name')}\nPROMPTS\n{prompts}\n")
def on_llm_end(self, response, **kwargs): print(f"\nLLM END:\n{response}\n")
# Визначаємо інструмент (tool), який агент може викликати@tool(description="Get current weather for a city.")def get_weather(city: str) -> str: # Формуємо запит до OpenWeather API url = "https://api.openweathermap.org/data/2.5/weather" params = {"q": city, "appid": OPENWEATHER_API_KEY, "units": "metric", "lang": "en"} r = requests.get(url, params=params, timeout=15) if not r.ok: # Якщо помилка — повертаємо текст помилки return f"error: {r.status_code} {r.text}" # Парсимо відповідь JSON data: Dict = r.json() # Повертаємо короткий рядок з погодою return f"Weather in {data.get('name', city)}: {data['main']['temp']}°C, {data['weather'][0]['description']}"
# Підключаємо LLM через LM Studio (локальний сервер, сумісний з OpenAI API)llm = ChatOpenAI( model="google/gemma-3n-e4b", openai_api_base="http://localhost:1234/v1", openai_api_key="lm-studio", temperature=0,)
# Завантажуємо готовий шаблон промпта ReAct з LangChain Hubprompt = hub.pull("hwchase17/react")
# Створюємо агента в стилі ReAct (модель думає + викликає інструменти)agent = create_react_agent( llm, tools=[get_weather], prompt=prompt)
# Загортаємо агента в Executor, щоб запускати та керувати його роботоюexecutor = AgentExecutor( agent=agent, tools=[get_weather], verbose=True)
# Запускаємо агента з питанням про погоду в Лондоніresult = executor.invoke( {"input": "Check weather in London"}, config={"callbacks": [CustomLogger()]})Виведення в консоль:
> Entering new AgentExecutor chain...I need to find the weather in London. I will use the get_weather tool to do this.Action: get_weatherAction Input: LondonWeather in London: 16.16°C, overcast cloudsI have retrieved the weather for London. Now I can provide the answer.Final Answer: Weather in London: 16.16°C, overcast clouds
> Finished chain.Weather in London: 16.16°C, overcast cloudsПовний лог:
ON_LLM_START:ChatOpenAI
PROMPTS:['Human: Answer the following questions as best you can. You have access to the following tools:get_weather(city: str) -> str - Get current weather for a city.Use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: the action to take, should be one of [get_weather] Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input questionBegin!Question: Check weather in LondonThought:']
ON_LLM_END:generations=[[ChatGenerationChunk(text='I need to find the weather in London. I will use the get_weather tool to do this. Action: get_weather Action Input: London', generation_info={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'}, message=AIMessageChunk( content='I need to find the weather in London. I will use the get_weather tool to do this. Action: get_weather Action Input: London', additional_kwargs={}, response_metadata={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'}, id='run--ce1137e0-edde-4be7-95df-8b26c7ddddce'))]] llm_output=None run=None type='LLMResult'
I need to find the weather in London. I will use the get_weather tool to do this.Action: get_weatherAction Input: LondonON_TOOL_START:get_weather
INPUT:London
ON_TOOL_END:Weather in London: 23.01°C, overcast cloudsON_LLM_START:ChatOpenAI
PROMPTS:['Human: Answer the following questions as best you can. You have access to the following tools:get_weather(city: str) -> str - Get current weather for a city.Use the following format: Question: the input question you must answer Thought: you should always think about what to do Action: the action to take, should be one of [get_weather] Action Input: the input to the action Observation: the result of the action ... (this Thought/Action/Action Input/Observation can repeat N times) Thought: I now know the final answer Final Answer: the final answer to the original input questionBegin!
Question: Check weather in LondonThought: I need to find the weather in London. I will use the get_weather tool to do this.Action: get_weatherAction Input: LondonObservation: Weather in London: 23.01°C, overcast cloudsThought: ']
ON_LLM_END:generations=[[ChatGenerationChunk(text='I have retrieved the weather for London. Now I can provide the answer.Final Answer: Weather in London: 23.01°C, overcast clouds', generation_info={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'},message=AIMessageChunk( content='I have retrieved the weather for London. Now I can provide the answer. Final Answer: Weather in London: 23.01°C, overcast clouds', additional_kwargs={}, response_metadata={'finish_reason': 'stop', 'model_name': 'google/gemma-3n-e4b', 'system_fingerprint': 'google/gemma-3n-e4b'}, id='run--342aee15-0171-4549-94c4-a02a620aad7f'))]]llm_output=Nonerun=None type='LLMResult'
I have retrieved the weather for London. Now I can provide the answer.Final Answer: Weather in London: 23.01°C, overcast clouds
> Finished chain.
Process finished with exit code 0Приклад 4 - Java фреймворк LangChain4j
Оскільки LLM зі стартапів пробирається в ентерпрайз, поява рішень на Java лише питання часу.
Із популярних рішень є:
Spring AI
https://spring.io/projects/spring-ai
https://github.com/spring-projects/spring-ai
Microsoft Semantic Kernel
https://learn.microsoft.com/en-us/semantic-kernel/overview
https://github.com/microsoft/semantic-kernel
LangChain for Java
https://docs.langchain4j.dev
https://github.com/langchain4j/langchain4j
Ось приклад LangChain4j аналогічний за функціями попереднім прикладам:
ext { ktor_version = '3.2.3' lc_4_j_version = '1.3.0' logback_version = '1.5.13' kotlinx_serialization_json_version = "1.8.1"}
dependencies { implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-client-cio:$ktor_version" implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" implementation "io.ktor:ktor-client-logging:$ktor_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_json_version" implementation "ch.qos.logback:logback-classic:$logback_version" implementation "dev.langchain4j:langchain4j:$lc_4_j_version" implementation "dev.langchain4j:langchain4j-open-ai:$lc_4_j_version" implementation("dev.langchain4j:langchain4j-http-client-jdk:$lc_4_j_version")}import dev.langchain4j.http.client.jdk.JdkHttpClientBuilderimport dev.langchain4j.model.openai.OpenAiChatModelimport dev.langchain4j.service.AiServicesimport kotlinx.coroutines.runBlockingimport java.net.http.HttpClientimport java.time.Duration
fun main() = runBlocking {
val httpClientBuilder = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .version(HttpClient.Version.HTTP_1_1)
val jdkClientBuilder = JdkHttpClientBuilder() .httpClientBuilder(httpClientBuilder) .connectTimeout(Duration.ofSeconds(10)) .readTimeout(Duration.ofSeconds(90))
val model = OpenAiChatModel.builder() .httpClientBuilder(jdkClientBuilder) .baseUrl(BASE_URL) .apiKey("lm-studio") .modelName(MODEL_ID) .temperature(0.2) .logRequests(true) .logResponses(true) .timeout(Duration.ofSeconds(90)) .listeners(listOf(ChatLogger())) .build()
val assistant = AiServices.builder(Assistant::class.java) .chatModel(model) .tools(WeatherTools) .build()
val result = assistant.chat("Check weather in London") println(result)
}import dev.langchain4j.service.UserMessageimport dev.langchain4j.service.V
interface Assistant {
@UserMessage("{{input}}") fun chat(@V("input") message: String): String
}import dev.langchain4j.agent.tool.Toolimport io.ktor.client.call.*import io.ktor.client.request.*import io.ktor.http.*import kotlinx.coroutines.runBlocking
object WeatherTools {
@Tool( name = "current_weather_city", value = ["Get current weather for a city"] ) @JvmStatic fun currentWeatherByCity(city: String): String = runBlocking { client.use { httpClient -> val url = URLBuilder(OWM_BASE).apply { appendPathSegments("weather") parameters.append("q", city) parameters.append("appid", WEATHER_API_KEY) parameters.append("units", "metric") parameters.append("lang", "en") }.buildString()
val response: WeatherResponse = httpClient.get(url).body() val name = response.name ?: city val temp = response.main?.temp val desc = response.weather.firstOrNull()?.description ?: "n/a" if (temp == null) { "Can't read temperature for $name" } else { "Weather in $name: $temp °C, $desc" } } }}import io.ktor.client.*import io.ktor.client.engine.cio.*import io.ktor.client.plugins.*import io.ktor.client.plugins.contentnegotiation.*import io.ktor.serialization.kotlinx.json.*import kotlinx.serialization.json.Json
const val BASE_URL: String = "http://localhost:1234/v1"
const val OWM_BASE = "https://api.openweathermap.org/data/2.5"
const val WEATHER_API_KEY = "..."
const val MODEL_ID = "google/gemma-3n-e4b"
val kotlinxJsonConfig: Json = Json { ignoreUnknownKeys = true prettyPrint = false}
val client: HttpClient = HttpClient(CIO) { install(ContentNegotiation) { json(kotlinxJsonConfig) } install(HttpTimeout) { requestTimeoutMillis = 60_000 connectTimeoutMillis = 10_000 socketTimeoutMillis = 60_000 }}import kotlinx.serialization.SerialNameimport kotlinx.serialization.Serializable
@Serializabledata class WeatherResponse( @SerialName("coord") val coord: Coord? = null, @SerialName("weather") val weather: List<WeatherItem> = emptyList(), @SerialName("base") val base: String? = null, @SerialName("main") val main: MainBlock? = null, @SerialName("visibility") val visibility: Int? = null, @SerialName("wind") val wind: Wind? = null, @SerialName("clouds") val clouds: Clouds? = null, @SerialName("dt") val dt: Long? = null, @SerialName("sys") val sys: Sys? = null, @SerialName("timezone") val timezone: Int? = null, @SerialName("id") val id: Long? = null, @SerialName("name") val name: String? = null, @SerialName("cod") val cod: Int? = null)
@Serializabledata class Coord( @SerialName("lon") val lon: Double? = null, @SerialName("lat") val lat: Double? = null)
@Serializabledata class WeatherItem( @SerialName("id") val id: Int? = null, @SerialName("main") val main: String? = null, @SerialName("description") val description: String? = null, @SerialName("icon") val icon: String? = null)
@Serializabledata class MainBlock( @SerialName("temp") val temp: Double? = null, @SerialName("feels_like") val feelsLike: Double? = null, @SerialName("temp_min") val tempMin: Double? = null, @SerialName("temp_max") val tempMax: Double? = null, @SerialName("pressure") val pressure: Int? = null, @SerialName("humidity") val humidity: Int? = null)
@Serializabledata class Wind( @SerialName("speed") val speed: Double? = null, @SerialName("deg") val deg: Int? = null, @SerialName("gust") val gust: Double? = null)
@Serializabledata class Clouds( @SerialName("all") val all: Int? = null)
@Serializabledata class Sys( @SerialName("type") val type: Int? = null, @SerialName("id") val id: Int? = null, @SerialName("country") val country: String? = null, @SerialName("sunrise") val sunrise: Long? = null, @SerialName("sunset") val sunset: Long? = null)import dev.langchain4j.model.chat.listener.ChatModelErrorContextimport dev.langchain4j.model.chat.listener.ChatModelListenerimport dev.langchain4j.model.chat.listener.ChatModelRequestContextimport dev.langchain4j.model.chat.listener.ChatModelResponseContext
class ChatLogger : ChatModelListener {
override fun onRequest(request: ChatModelRequestContext) { println("ON_LLM_START:") println(request) }
override fun onResponse(response: ChatModelResponseContext) { println("ON_LLM_END:") println(response) }
override fun onError(context: ChatModelErrorContext) { println("ON_LLM_ERROR:") println(context.error().message) }
}Виведення в консоль:
C:\Users\Roman\.jdks\corretto-21.0.6\bin\java.exe "-javaagent:C:\Program Files\JetBrains\IntelliJIdea2025.1\lib\idea_rt.jar=52675" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\Roman\IdeaProjects\LangChain4j_example\build\classes\kotlin\main;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlin\kotlin-stdlib\2.2.0\fdfc65fbc42fda253a26f61dac3c0aca335fae96\kotlin-stdlib-2.2.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-classic\1.5.13\e6f82a9ce14a912c7dab831164ec6f10cc3eeb1b\logback-classic-1.5.13.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j\1.3.0\207a49cc2a25b14e61c219057023c97ea6344836\langchain4j-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-open-ai\1.3.0\abd013b6600fefb6db2f2d08cad4970a601bf71a\langchain4j-open-ai-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-http-client-jdk\1.3.0\ade5c4f73164b4b16f191312b80af2d847a26fd6\langchain4j-http-client-jdk-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-cio-jvm\3.2.3\7fb001474733cba10b0e234be7e25c984bac1360\ktor-client-cio-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-content-negotiation-jvm\3.2.3\9ca43332547aaf2e7af0ef9e75b0e55a7fd5d7c9\ktor-client-content-negotiation-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-logging-jvm\3.2.3\7731df6cd3b0da0be75d0d91e66c5c5eab0efb08\ktor-client-logging-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-client-core-jvm\3.2.3\58361d3e3cb2f4e77c4eedc294b2889fdf82b2ac\ktor-client-core-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-kotlinx-json-jvm\3.2.3\211555a0b1cad21ccaf31ef4b482374c146e6318\ktor-serialization-kotlinx-json-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-json-jvm\1.8.1\4de3bace4b175753df5484d2acd74c14bfeb5be9\kotlinx-serialization-json-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains\annotations\23.0.0\8cc20c07506ec18e0834947b84a864bfc094484e\annotations-23.0.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\ch.qos.logback\logback-core\1.5.13\ccd418c6f1a5a93e57e7b5bb9a3f708dbd563cd8\logback-core-1.5.13.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.slf4j\slf4j-api\2.0.17\d9e58ac9c7779ba3bf8142aff6c830617a7fe60f\slf4j-api-2.0.17.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-core\1.3.0\18fba60f0f2b76b18e2ba362b8ef8076d675e9af\langchain4j-core-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.apache.opennlp\opennlp-tools\2.5.4\11a7671f2169280c0e24b979cb326833b96ca246\opennlp-tools-2.5.4.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-core\2.19.2\50f3b4bd59b9ff51a0ed493e7b5abaf5c39709bf\jackson-core-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-databind\2.19.2\46509399d28f57ca32c6bb4b0d4e10e8f062051e\jackson-databind-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.fasterxml.jackson.core\jackson-annotations\2.19.2\c5381f11988ae3d424b197a26087d86067b6d7d\jackson-annotations-2.19.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\dev.langchain4j\langchain4j-http-client\1.3.0\c0d03a935c32c5252561ce0004af633477bf0e8a\langchain4j-http-client-1.3.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\com.knuddels\jtokkit\1.1.0\b7370f801db3eb8c7c6a2c2c06231909ac6de0b0\jtokkit-1.1.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-coroutines-slf4j\1.10.2\1271f9d4a929150bb87ab8d1dc1e86d4bcf039f3\kotlinx-coroutines-slf4j-1.10.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jspecify\jspecify\1.0.0\7425a601c1c7ec76645a78d22b8c6a627edee507\jspecify-1.0.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-http-cio-jvm\3.2.3\9ba86bd196344eb2f33f4de7c01b2a76079563d8\ktor-http-cio-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-websockets-jvm\3.2.3\180024bd52774540635a9fdaca021f9392400e60\ktor-websockets-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-network-tls-jvm\3.2.3\d1f24a1e38fd0f940ee8b55c8e11be151491a3de\ktor-network-tls-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-coroutines-core-jvm\1.10.2\4a9f78ef49483748e2c129f3d124b8fa249dafbf\kotlinx-coroutines-core-jvm-1.10.2.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-jvm\3.2.3\f3ee77994a1ba0ddc0d65bbb9bd8db2900f40009\ktor-serialization-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-websocket-serialization-jvm\3.2.3\e62e9fdebc262c2b4c556f208628a03740f14046\ktor-websocket-serialization-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-http-jvm\3.2.3\c83599639fc1fc9312e63ed86423ee535d80bc5\ktor-http-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-events-jvm\3.2.3\38a0c405c218e64b0c967ad300ca587cde8154bc\ktor-events-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-sse-jvm\3.2.3\67421e4181c68c6b6d82cb6f8af9c8e44e2838c8\ktor-sse-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-json-io-jvm\1.8.1\de4e31bfc7ddf8585418f8d4fca649feca574cdf\kotlinx-serialization-json-io-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-serialization-kotlinx-jvm\3.2.3\316c3d4122d2dcfecd2ae2146060d557a2a4b4\ktor-serialization-kotlinx-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-serialization-core-jvm\1.8.1\510cb839cce9a3e708052d480a6fbf4a7274dfcd\kotlinx-serialization-core-jvm-1.8.1.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-network-jvm\3.2.3\f3eb3f831652f4b5407703f58e4885cd1ae93e92\ktor-network-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-io-jvm\3.2.3\81a70f5d028cb2878fba2107df2101edc7c0240f\ktor-io-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\io.ktor\ktor-utils-jvm\3.2.3\5cc6a940a5ca98f7f0c36e0b699c6fbd2f594598\ktor-utils-jvm-3.2.3.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-io-core-jvm\0.7.0\1d9595ed390f6304ce90a049e6b2f1fab3b408c4\kotlinx-io-core-jvm-0.7.0.jar;C:\Users\Roman\.gradle\caches\modules-2\files-2.1\org.jetbrains.kotlinx\kotlinx-io-bytestring-jvm\0.7.0\ffe5bd231da40d21870250703326113277dbf9c3\kotlinx-io-bytestring-jvm-0.7.0.jar MainKtON_LLM_START:dev.langchain4j.model.chat.listener.ChatModelRequestContext@33d512c100:48:03.351 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:- method: POST- url: http://localhost:1234/v1/chat/completions- headers: [Authorization: Beare...io], [User-Agent: langchain4j-openai], [Content-Type: application/json]- body: { "model" : "google/gemma-3n-e4b", "messages" : [ { "role" : "user", "content" : "Check weather in London" } ], "temperature" : 0.2, "stream" : false, "tools" : [ { "type" : "function", "function" : { "name" : "current_weather_city", "description" : "Get current weather for a city", "parameters" : { "type" : "object", "properties" : { "arg0" : { "type" : "string" } }, "required" : [ "arg0" ] } } } ]}
00:48:04.961 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:- status code: 200- headers: [connection: keep-alive], [content-length: 741], [content-type: application/json; charset=utf-8], [date: Tue, 19 Aug 2025 21:48:04 GMT], [etag: W/"2e5-JWXcPUg7aNE2xJTlIOO4yntOymw"], [keep-alive: timeout=5], [x-powered-by: Express]- body: { "id": "chatcmpl-ox8omdn9nxl0o4pbihqqi", "object": "chat.completion", "created": 1755640083, "model": "google/gemma-3n-e4b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "", "tool_calls": [ { "type": "function", "id": "458045748", "function": { "name": "current_weather_city", "arguments": "{\"arg0\":\"London\"}" } } ] }, "logprobs": null, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 399, "completion_tokens": 34, "total_tokens": 433 }, "stats": {}, "system_fingerprint": "google/gemma-3n-e4b"}
ON_LLM_END:dev.langchain4j.model.chat.listener.ChatModelResponseContext@1c481ff2ON_LLM_START:dev.langchain4j.model.chat.listener.ChatModelRequestContext@70d2e40b00:48:05.501 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP request:- method: POST- url: http://localhost:1234/v1/chat/completions- headers: [Authorization: Beare...io], [User-Agent: langchain4j-openai], [Content-Type: application/json]- body: { "model" : "google/gemma-3n-e4b", "messages" : [ { "role" : "user", "content" : "Check weather in London" }, { "role" : "assistant", "tool_calls" : [ { "id" : "458045748", "type" : "function", "function" : { "name" : "current_weather_city", "arguments" : "{\"arg0\":\"London\"}" } } ] }, { "role" : "tool", "tool_call_id" : "458045748", "content" : "Weather in London: 17.76 °C, few clouds" } ], "temperature" : 0.2, "stream" : false, "tools" : [ { "type" : "function", "function" : { "name" : "current_weather_city", "description" : "Get current weather for a city", "parameters" : { "type" : "object", "properties" : { "arg0" : { "type" : "string" } }, "required" : [ "arg0" ] } } } ]}
00:48:06.257 [main] INFO dev.langchain4j.http.client.log.LoggingHttpClient -- HTTP response:- status code: 200- headers: [connection: keep-alive], [content-length: 554], [content-type: application/json; charset=utf-8], [date: Tue, 19 Aug 2025 21:48:06 GMT], [etag: W/"22a-YKbJMzdQdr5Qv/9Z3J+Mf2kPeAw"], [keep-alive: timeout=5], [x-powered-by: Express]- body: { "id": "chatcmpl-6aslomnvzvh3tu6jo8tfl6", "object": "chat.completion", "created": 1755640085, "model": "google/gemma-3n-e4b", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "The weather in London is 17.76 °C with few clouds.", "tool_calls": [] }, "logprobs": null, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 469, "completion_tokens": 18, "total_tokens": 487 }, "stats": {}, "system_fingerprint": "google/gemma-3n-e4b"}
ON_LLM_END:dev.langchain4j.model.chat.listener.ChatModelResponseContext@2449cff7The weather in London is 17.76 °C with few clouds.
Process finished with exit code 0Приклад 5 - Java бібліотеки від Google для мобільних пристроїв
У цьому прикладі я використовуватиму
Рушій: Google AI Edge MediaPipe
LLM модель: gemma-3n-E4B-it-int4
RAG / Functional Calling: On-Device RAG SDK & On-Device Function Calling SDK
Embedding модель: Gecko-110m-en
Як це має працювати- можна почитати тут
RAG:
https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation
https://en.wikipedia.org/wiki/Retrieval-augmented_generation
https://www.ibm.com/think/topics/retrieval-augmented-generation
Function Calling:
https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling
https://huggingface.co/docs/hugs/guides/function-calling
https://platform.openai.com/docs/guides/function-calling
https://medium.com/@danushidk507/function-calling-in-llm-e537b286a4fd
У цьому прикладі буде 3 класи
MediaPipeEngineCommon: тут зберігатимуться спільні компоненти для роботи з векторною базою
MediaPipeEngineWithRag: тут можна запустити генерацію, додавши до запиту дані з векторної бази
MediaPipeEngineWithTools: тут можна запустити генерацію і модель сама вирішує, чи робити запит до векторної бази
Якщо вирішує, що потрібно, робить функціональний виклик, який обробляє не вручну, як у попередньому прикладі, а бібліотекою com.google.ai.edge.localagents:localagents-fc
Залежності в проєкті, у новому Gradle форматі:
localagentsRag = "0.2.0"localagentsFc = "0.1.0"tasksGenai = "0.10.25"tasksText = "0.10.26.1"tasksVision = "0.10.26.1"tensorflowLite = "2.17.0"kotlinxCoroutinesGuava = "1.10.2"
tasks-genai = { module = "com.google.mediapipe:tasks-genai", version.ref = "tasksGenai" }tasks-text = { module = "com.google.mediapipe:tasks-text", version.ref = "tasksText" }tasks-vision = { module = "com.google.mediapipe:tasks-vision", version.ref = "tasksVision" }tensorflow-lite = { module = "org.tensorflow:tensorflow-lite", version.ref = "tensorflowLite" }localagents-rag = { module = "com.google.ai.edge.localagents:localagents-rag", version.ref = "localagentsRag" }localagents-fc = { module = "com.google.ai.edge.localagents:localagents-fc", version.ref = "localagentsFc" }kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" }MediaPipeEngineCommon: Клас для роботи з векторною базою даних, потрібен у цьому прикладі і для RAG, і для Function Calling
import com.google.ai.edge.localagents.rag.chunking.TextChunkerimport com.google.ai.edge.localagents.rag.memory.DefaultSemanticTextMemoryimport com.google.ai.edge.localagents.rag.memory.SqliteVectorStoreimport com.google.ai.edge.localagents.rag.models.Embedderimport com.google.ai.edge.localagents.rag.prompt.PromptBuilder
interface MediaPipeEngineCommon {
var chunker: TextChunker var embedder: Embedder<String> var vectorStore: SqliteVectorStore var promptBuilder: PromptBuilder var semanticMemory: DefaultSemanticTextMemory
fun init( geckoModelPath: String, // Gecko_256_quant.tflite tokenizerModelPath: String, // sentencepiece.model useGpuForEmbeddings: Boolean = true, )
fun saveTextToVectorStore( text: String, chunkOverlap: Int = 20, chunkTokenSize: Int = 128, chunkMaxSymbolsSize: Int = 1000, chunkBySentences: Boolean = false, ): String?
fun readEmbeddingVectors(): List<VectorStoreEntity>
suspend fun readEmbeddingVectors( query: String, topK: Int, minSimilarityScore: Float, ): List<VectorStoreEntity>
fun makeSQLRequest(query: String): Boolean
}import android.app.Applicationimport android.database.Cursorimport android.database.sqlite.SQLiteDatabaseimport com.google.ai.edge.localagents.rag.chunking.TextChunkerimport com.google.ai.edge.localagents.rag.memory.DefaultSemanticTextMemoryimport com.google.ai.edge.localagents.rag.memory.SqliteVectorStoreimport com.google.ai.edge.localagents.rag.memory.VectorStoreRecordimport com.google.ai.edge.localagents.rag.models.EmbedDataimport com.google.ai.edge.localagents.rag.models.Embedderimport com.google.ai.edge.localagents.rag.models.EmbeddingRequestimport com.google.ai.edge.localagents.rag.models.GeckoEmbeddingModelimport com.google.ai.edge.localagents.rag.prompt.PromptBuilderimport com.google.common.collect.ImmutableListimport com.romankryvolapov.offlineailauncher.common.extensions.toDurationStringimport com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logDebugimport com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logErrorimport kotlinx.coroutines.guava.awaitimport java.io.Fileimport java.nio.ByteBufferimport java.nio.ByteOrderimport java.util.Optional
class MediaPipeEngineCommonImpl( private val application: Application) : MediaPipeEngineCommon {
companion object { private const val TAG = "CommonComponentsTag" private const val GECKO_EMBEDDING_MODEL_DIMENSION = 768 private const val PROMPT_TEMPLATE: String = "You are an assistant for question-answering tasks. Here are the things I want to remember: {0} Use the things I want to remember, answer the following question the user has: {1}"
}
override lateinit var chunker: TextChunker override lateinit var embedder: Embedder<String> override lateinit var vectorStore: SqliteVectorStore override lateinit var promptBuilder: PromptBuilder override lateinit var semanticMemory: DefaultSemanticTextMemory
override fun init( geckoModelPath: String, tokenizerModelPath: String, useGpuForEmbeddings: Boolean, ) { logDebug("init", TAG) chunker = TextChunker()
// в embedder додаємо шлях до моделі Gecko-110m-en // я використовую версію Gecko_256_quant.tflite тут 256 це максимальний розмір текстового входу // ця версія оптимальна з точки зору розміру шматків тексту та швидкодії // важливо- далі в коді ми передаємо, на які шматки розбивати текст, це залежить від параметрів моделі embedder = GeckoEmbeddingModel( geckoModelPath, Optional.of(tokenizerModelPath), useGpuForEmbeddings, )
// тут я буду використовувати вже готову SQLite базу в застосунку // до неї просто додасться ще одна таблиця rag_vector_store // зі стовпцями text для тексту та embeddings для вектора val database = File(application.getDatabasePath("database").absolutePath) if (!database.exists()) { logError("startEngine database not exists", TAG) }
// Можна зробити й кастомну реалізацію бази даних, що успадковує // інтерфейс VectorStore<String>, але метод getNearestRecords має // бути реалізований правильно і працювати швидко, він шукає найближчі вектори vectorStore = SqliteVectorStore( GECKO_EMBEDDING_MODEL_DIMENSION, database.absolutePath ) semanticMemory = DefaultSemanticTextMemory( vectorStore, embedder ) promptBuilder = PromptBuilder( PROMPT_TEMPLATE ) logDebug("init ready", TAG) }
override fun saveTextToVectorStore( text: String,
// наскільки заходити в текст до фрагмента, тут 20 chunkOverlap: Int,
// Зверніть увагу, що розмір начебто в токенах, // але він використовується для chunker і може не відповідати // розміру токенів для embedder, тут chunkTokenSize 128 chunkTokenSize: Int,
// при розбитті за допомогою chunkBySentences // розмір речень може бути великим // якщо він перевищить можливості embedder моделі, вона видасть помилку // для цього використовується обрізання до максимального розміру // тут він 1000 символів chunkMaxSymbolsSize: Int,
// використовувати метод розбиття за реченнями chunkBySentences: Boolean, ): String? { logDebug("saveTextToVectorStore text length: ${text.length}", TAG) // таймер, щоб зрозуміти, наскільки швидко працює val start = System.currentTimeMillis() val chunks: List<String> = if (chunkBySentences) chunker.chunkBySentences( text, chunkTokenSize, ).filter { it.isNotBlank() }.map { chunk -> if (chunk.length > chunkMaxSymbolsSize) { logError("saveTextToVectorStore crop chunk", TAG) chunk.substring(0, chunkMaxSymbolsSize) } else { chunk } } else chunker.chunk( text, chunkTokenSize, chunkOverlap ).filter { it.isNotBlank() }.map { chunk -> if (chunk.length > chunkMaxSymbolsSize) { logError("saveTextToVectorStore crop chunk", TAG) chunk.substring(0, chunkMaxSymbolsSize) } else { chunk } } val end = System.currentTimeMillis() val delta = end - start logDebug("saveTextToVectorStore chunks delta: ${delta.toDurationString()} size: ${chunks.size}", TAG) chunks.forEach { logDebug("length: ${it.length}", TAG) } if (chunks.isEmpty()) { logError("saveTextToVectorStore chunks.isEmpty()", TAG) return "Chunks is empty" } return try { // генерація вектора відбувається всередині semanticMemory val result: Boolean? = semanticMemory.recordBatchedMemoryItems( ImmutableList.copyOf(chunks) )?.get() val end = System.currentTimeMillis() val delta = end - start logDebug("saveTextToVectorStore ready delta: ${delta.toDurationString()} result: $result", TAG) null } catch (t: Throwable) { logError("saveTextToVectorStore failed: ${t.message}", t, TAG) t.message } }
// пошук за запитом query, знайде всі схожі на запит шматки тексту override suspend fun readEmbeddingVectors( query: String,
// кількість результатів запиту до бази topK: Int,
// наскільки вектор запиту query має бути схожим на запис у базі // 0.0 = шукати всі записи, відсортувати за найсхожішими // 1.0 = тільки ідеальний збіг // я використовую значення 0.6 - 0.8 minSimilarityScore: Float, ): List<VectorStoreEntity> { logDebug("readEmbeddingVectors query: $query", TAG) val queryEmbedData: EmbedData<String> = EmbedData.create( query, EmbedData.TaskType.RETRIEVAL_QUERY ) val embeddingRequest: EmbeddingRequest<String> = EmbeddingRequest .create( listOf(queryEmbedData) ) val vector: ImmutableList<Float> = try { embedder.getEmbeddings(embeddingRequest).await() } catch (t: Throwable) { logError("readEmbeddingVectors: embedding failed: ${t.message}", t, TAG) return emptyList() } logDebug("searchDocsInternal vector size: ${vector.size}", TAG) if (vector.isEmpty()) { logError("readEmbeddingVectors vector.isEmpty()", TAG) return emptyList() } val hits: ImmutableList<VectorStoreRecord<String>> = try { vectorStore.getNearestRecords( vector, topK, minSimilarityScore ) } catch (t: Throwable) { logError("readEmbeddingVectors: vector search failed: ${t.message}", t, TAG) return emptyList() } if (hits.isEmpty()) { logError("readEmbeddingVectors hits.isEmpty()", TAG) return emptyList() } val result = hits.map { VectorStoreEntity( id = null, text = it.data, embedding = it.embeddings ) } logDebug("readEmbeddingVectors\nsize: ${result.size}\nresult: $result", TAG) return result }
// просто виводить усі записи в базі override fun readEmbeddingVectors(): List<VectorStoreEntity> { logDebug("readEmbeddingPreview", TAG) var cursor: Cursor? = null var database: SQLiteDatabase? = null return try { val databaseFile = File(application.getDatabasePath("database").absolutePath) database = SQLiteDatabase.openDatabase( databaseFile.absolutePath, null, SQLiteDatabase.OPEN_READONLY ) cursor = database.rawQuery("SELECT ROWID, text, embeddings FROM rag_vector_store", null) val result = mutableListOf<VectorStoreEntity>() while (cursor.moveToNext()) { val rowId = cursor.getLong(0) val text = cursor.getString(1) val blob = cursor.getBlob(2) val buffer = ByteBuffer.wrap(blob).order(ByteOrder.LITTLE_ENDIAN) val floats = mutableListOf<Float>() while (buffer.hasRemaining()) { floats.add(buffer.float) } result.add( VectorStoreEntity( id = rowId, text = text, embedding = floats ) ) } logDebug("readEmbeddingPreview\nsize: ${result.size}\nresult: $result", TAG) result } catch (t: Throwable) { logError("readEmbeddingPreview failed: ${t.message}", t, TAG) emptyList() } finally { cursor?.close() database?.close() } }
// можна написати свій запит і він виконається, // наприклад "DELETE FROM rag_vector_store" override fun makeSQLRequest(query: String): Boolean { logDebug("makeSQLRequest query: $query", TAG) var cursor: Cursor? = null var database: SQLiteDatabase? = null return try { val databaseFile = File(application.getDatabasePath("database").absolutePath) database = SQLiteDatabase.openDatabase( databaseFile.absolutePath, null, SQLiteDatabase.OPEN_READWRITE ) cursor = database.rawQuery(query, null) val result = cursor.moveToFirst() logDebug("makeSQLRequest result: $result", TAG) result } catch (t: Throwable) { logError("makeSQLRequest failed: ${t.message}", t, TAG) false } finally { cursor?.close() database?.close() } }
}MediaPipeEngineWithRag: Тут підтримується лише простий механізм RAG
import kotlinx.coroutines.flow.Flowimport java.io.File
interface MediaPipeEngineWithRag {
fun startEngine( modelFile: File, isSupportImages: Boolean = false, engineParams: MediaPipeEngineParams, )
fun resetSession()
fun generateResponse( prompt: String, topK: Int = 5, minSimilarityScore: Float = 0.6F, ): Flow<ResultEmittedData<String>>
}import android.app.Applicationimport com.google.ai.edge.localagents.rag.chains.ChainConfigimport com.google.ai.edge.localagents.rag.chains.RetrievalAndInferenceChainimport com.google.ai.edge.localagents.rag.models.AsyncProgressListenerimport com.google.ai.edge.localagents.rag.models.LanguageModelResponseimport com.google.ai.edge.localagents.rag.models.MediaPipeLlmBackendimport com.google.ai.edge.localagents.rag.retrieval.RetrievalConfigimport com.google.ai.edge.localagents.rag.retrieval.RetrievalConfig.TaskTypeimport com.google.ai.edge.localagents.rag.retrieval.RetrievalRequestimport com.google.common.util.concurrent.FutureCallbackimport com.google.common.util.concurrent.Futuresimport com.google.common.util.concurrent.ListenableFutureimport com.google.common.util.concurrent.MoreExecutorsimport com.google.mediapipe.tasks.genai.llminference.GraphOptionsimport com.google.mediapipe.tasks.genai.llminference.LlmInferenceimport com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptionsimport com.google.mediapipe.tasks.genai.llminference.LlmInferenceSession.LlmInferenceSessionOptionsimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.callbackFlowimport java.io.Fileimport java.util.concurrent.Executor
class MediaPipeEngineWithRagImpl( private val application: Application, private val common: MediaPipeEngineCommon,) : MediaPipeEngineWithRag {
companion object { private const val TAG = "MediaPipeEngineWithRagTag" }
private var chainConfig: ChainConfig<String>? = null private var retrievalAndInferenceChain: RetrievalAndInferenceChain? = null private var engineMediaPipe: LlmInference? = null private var sessionOptions: LlmInferenceSessionOptions? = null private var mediaPipeLanguageModel: MediaPipeLlmBackend? = null private var interfaceOptions: LlmInferenceOptions? = null private val executor: Executor = MoreExecutors.directExecutor()
private var future: ListenableFuture<LanguageModelResponse>? = null
override fun startEngine( modelFile: File, isSupportImages: Boolean, engineParams: MediaPipeEngineParams, ) { logDebug("startEngine", TAG) interfaceOptions = createInterfaceOptions( modelFile = modelFile, engineParams = engineParams, isSupportImages = isSupportImages, ) engineMediaPipe = LlmInference.createFromOptions( application, interfaceOptions ) if (engineMediaPipe == null) { logError("startEngine llmInference == null", TAG) return } sessionOptions = createSessionOptions( engineParams = engineParams, isSupportImages = isSupportImages, ) mediaPipeLanguageModel = MediaPipeLlmBackend( application.applicationContext, interfaceOptions, sessionOptions, executor ) chainConfig = ChainConfig.create( mediaPipeLanguageModel, common.promptBuilder, // додаємо базу, в якій потрібно перевіряти запити common.semanticMemory )
// робимо ланцюжок з перевіркою в базі retrievalAndInferenceChain = RetrievalAndInferenceChain( chainConfig ) Futures.addCallback( mediaPipeLanguageModel!!.initialize(), object : FutureCallback<Boolean> { override fun onSuccess(result: Boolean) { logDebug("mediaPipeLanguageModel initialize onSuccess", TAG) }
override fun onFailure(t: Throwable) { logError( "mediaPipeLanguageModel initialize onFailure: ${t.message}", t, TAG, ) } }, executor ) logDebug("startEngine ready", TAG) }
override fun resetSession() { logDebug("resetSession", TAG) try { retrievalAndInferenceChain = RetrievalAndInferenceChain( chainConfig ) logDebug("Session reset completed", TAG) } catch (e: Exception) { logError("Failed to reset session: ${e.message}", e, TAG) } logDebug("resetSession ready", TAG) }
override fun generateResponse( prompt: String,
// Кількість результатів запиту до бази topK: Int,
// наскільки вектор запиту query має бути схожим на запис у базі // 0.0 = шукати всі записи, відсортувати за найсхожішими // 1.0 = тільки ідеальний збіг // я використовую значення 0.6 - 0.8 minSimilarityScore: Float, ): Flow<ResultEmittedData<String>> = callbackFlow { logDebug("generateResponse prompt: $prompt", TAG) try { if (retrievalAndInferenceChain == null) { logError("generateResponse retrievalAndInferenceChain == null", TAG) trySend( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "retrievalAndInferenceChain == null", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@callbackFlow } val retrievalConfig = RetrievalConfig.create( topK, minSimilarityScore, TaskType.QUESTION_ANSWERING )
// запит уже включає ланцюжок з перевіркою val retrievalRequest = RetrievalRequest.create( prompt, retrievalConfig ) logDebug("generateResponse retrievalRequest", TAG) val messageBuilder = StringBuilder() val listener = AsyncProgressListener<LanguageModelResponse> { partial, done -> val delta = partial.text.orEmpty() logDebug("generateResponse delta: $delta", TAG) if (!done && delta.isNotBlank()) { messageBuilder.append(delta) trySend( ResultEmittedData.loading( model = messageBuilder.toString(), ) ) } } future = retrievalAndInferenceChain!!.invoke( retrievalRequest, listener ) future?.addListener({ val fullText = future?.get()?.text if (fullText.isNullOrEmpty()) { logError("generateResponse fullText isNullOrEmpty", TAG) trySend( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Empty response", errorType = ErrorType.EXCEPTION ) ) close() return@addListener } logDebug("generateResponse fullText: $fullText", TAG) trySend( ResultEmittedData.success( model = fullText, message = null, responseCode = null ) ) close() }, executor) logDebug("generateResponse ready", TAG) } catch (t: Throwable) { logError("generateResponse failed: ${t.message}", t, TAG) trySend( ResultEmittedData.error( model = null, error = t, title = "MediaPipe engine error", responseCode = null, message = t.message, errorType = ErrorType.EXCEPTION, ) ) } }
private fun createInterfaceOptions( modelFile: File, engineParams: MediaPipeEngineParams, isSupportImages: Boolean, ): LlmInferenceOptions { val backend = when (engineParams.backend) { MediaPipeBackendParams.CPU -> LlmInference.Backend.CPU MediaPipeBackendParams.GPU -> LlmInference.Backend.GPU } return LlmInferenceOptions.builder().apply { setModelPath(modelFile.absolutePath) setMaxTokens(engineParams.contextSize) setPreferredBackend(backend) val maxNumImages = if (isSupportImages) 1 else 0 setMaxNumImages(maxNumImages) if (engineParams.useMaxTopK) setMaxTopK(engineParams.maxTopK) }.build() }
private fun createSessionOptions( engineParams: MediaPipeEngineParams, isSupportImages: Boolean, ): LlmInferenceSessionOptions { return LlmInferenceSessionOptions.builder().apply { if (engineParams.useTopK) setTopK(engineParams.topK) if (engineParams.useTopP) setTopP(engineParams.topP) if (engineParams.useTemperature) setTemperature(engineParams.temperature) if (engineParams.useRandomSeed) setRandomSeed(engineParams.randomSeed) setGraphOptions( GraphOptions.builder() .setEnableVisionModality(isSupportImages) .build() ) }.build() }
private fun isInGeneration(): Boolean { return future != null && future?.isDone != true && future?.isCancelled != true }
}MediaPipeEngineWithTools: Тут підтримуються функціональні виклики
import kotlinx.coroutines.flow.Flowimport java.io.File
interface MediaPipeEngineWithTools {
fun startEngine( modelFile: File, isSupportImages: Boolean = false, engineParams: MediaPipeEngineParams, )
fun generateResponse( userQuery: String, maxSteps: Int = 3, ): Flow<ResultEmittedData<String>>
}package com.romankryvolapov.offlineailauncher.mediapipe
import android.app.Applicationimport com.google.ai.edge.localagents.core.proto.Contentimport com.google.ai.edge.localagents.core.proto.FunctionCallimport com.google.ai.edge.localagents.core.proto.FunctionDeclarationimport com.google.ai.edge.localagents.core.proto.FunctionResponseimport com.google.ai.edge.localagents.core.proto.GenerateContentResponseimport com.google.ai.edge.localagents.core.proto.Partimport com.google.ai.edge.localagents.core.proto.Schemaimport com.google.ai.edge.localagents.core.proto.Toolimport com.google.ai.edge.localagents.fc.GemmaFormatterimport com.google.ai.edge.localagents.fc.GenerativeModelimport com.google.ai.edge.localagents.fc.LlmInferenceBackendimport com.google.ai.edge.localagents.rag.memory.VectorStoreRecordimport com.google.ai.edge.localagents.rag.models.EmbedDataimport com.google.ai.edge.localagents.rag.models.EmbeddingRequestimport com.google.common.collect.ImmutableListimport com.google.mediapipe.tasks.genai.llminference.LlmInferenceimport com.google.mediapipe.tasks.genai.llminference.LlmInference.LlmInferenceOptionsimport com.google.protobuf.Structimport com.google.protobuf.Valueimport com.romankryvolapov.offlineailauncher.common.models.common.ErrorTypeimport com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logDebugimport com.romankryvolapov.offlineailauncher.common.models.common.LogUtil.logErrorimport com.romankryvolapov.offlineailauncher.common.models.common.ResultEmittedDataimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.flowimport kotlinx.coroutines.guava.awaitimport java.io.File
class MediaPipeEngineWithFunctionCallingImpl( private val application: Application, private val common: MediaPipeEngineCommon,) : MediaPipeEngineWithFunctionCalling {
companion object { private const val TAG = "MediaPipeEngineWithFunctionCallingsTag"
private const val DEFAULT_MIN_SIMILARITY_SCORE = 0.8
private const val TOOLS_CODE = "tool_code" private const val RESULTS = "results"
private const val TOOLS_ACTION_SEARCH_DOCS = "search_docs" private const val TOOLS_ACTION_SEARCH_DOCS_DESCRIPTION = "Searches knowledge and returns the most relevant results as plain text."
private const val TOOLS_PARAM_QUERY = "query" private const val TOOLS_PARAM_QUERY_DESCRIPTION = "User query to search in the vector store."
private const val TOOLS_PARAM_TOP_K = "top_k" private const val TOOLS_PARAM_TOP_K_DESCRIPTION = "Number of results to return (default 5)."
private const val TOOLS_PARAM_MIN_SIMILARITY_SCORE = "min_similarity_score"
private const val MIN_SIMILARITY_SCORE_DESCRIPTION = """ Minimum similarity score threshold (float) for filtering search results, from 0.0 (no filtering) to 1.0 (exact match). Start with $DEFAULT_MIN_SIMILARITY_SCORE, and if no results are found, lower the value and retry the search. """
// від цього темплейта залежить дуже багато // неправильно підібрані параметри зроблять виклик інструмента неможливим // або після виклику інструмента генерація зупиниться // для інших LLM моделей темплейт може відрізнятися // ```tool_code добре працює для Gemma 3n, схоже вона була на цьому ключовому слові навчена // якщо через інструменти нічого не знайдено, у промпті вказано, що схожість можна зменшити // інструменти можуть бути абсолютно різними, наприклад SQL запит, запит в інтернет, важливо їх правильно описати private val PROMPT_TEMPLATE_WITH_TOOLS = """ You are an on-device assistant. You have access to special tools (also called: "function call", "invoke tool", "use API", "search", "lookup", "query tool")
If you decide to invoke any of the function, it should be wrapped with ```$TOOLS_CODE```
You have access to the following tools. * `$TOOLS_ACTION_SEARCH_DOCS`: Searches knowledge and returns the most relevant results as plain text.
WHEN TO USE A TOOL - If you do not have enough information to answer with high confidence. - If the user explicitly or implicitly asks to check/verify/find out/look up ("check via tools", "verify", "lookup", etc.).
Tool args: $TOOLS_PARAM_QUERY: User string query to search in the vector store. $TOOLS_PARAM_TOP_K: Integer number of results to return (default 5). $TOOLS_PARAM_MIN_SIMILARITY_SCORE: Minimum similarity score threshold (float) for filtering search results, from 0.0 (no filtering) to 1.0 (exact match). Start with $DEFAULT_MIN_SIMILARITY_SCORE, and if no results are found, lower the value and retry the search.
Rules for tool call: ```$TOOLS_CODE $TOOLS_ACTION_SEARCH_DOCS($TOOLS_PARAM_QUERY="<string>", $TOOLS_PARAM_TOP_K=<integer>, $TOOLS_PARAM_MIN_SIMILARITY_SCORE=<float>) ```
Tool response: $RESULTS: Plain text results.
IMPORTANT: After receiving tool results, ALWAYS write a natural-language answer for the user in the very next message. If tool results are empty, briefly explain that nothing relevant was found and propose next steps. """.trimIndent()
}
private var generativeModel: GenerativeModel? = null
override fun startEngine( modelFile: File, isSupportImages: Boolean, engineParams: MediaPipeEngineParams, ) { logDebug("startEngine", TAG) val interfaceOptions = createInterfaceOptions( modelFile = modelFile, engineParams = engineParams, isSupportImages = isSupportImages, ) val engineMediaPipe = LlmInference.createFromOptions( application, interfaceOptions ) if (engineMediaPipe == null) { logError("startEngine llmInference == null", TAG) return } val searchDocs = FunctionDeclaration.newBuilder() .setName(TOOLS_ACTION_SEARCH_DOCS) .setDescription(TOOLS_ACTION_SEARCH_DOCS_DESCRIPTION) .setParameters( Schema.newBuilder() .setType(com.google.ai.edge.localagents.core.proto.Type.OBJECT) .putProperties( TOOLS_PARAM_QUERY, Schema.newBuilder() .setType(com.google.ai.edge.localagents.core.proto.Type.STRING) .setDescription(TOOLS_PARAM_QUERY_DESCRIPTION) .build() ) .putProperties( TOOLS_PARAM_TOP_K, Schema.newBuilder() .setType(com.google.ai.edge.localagents.core.proto.Type.INTEGER) .setDescription(TOOLS_PARAM_TOP_K_DESCRIPTION) .build() ) .putProperties( TOOLS_PARAM_MIN_SIMILARITY_SCORE, Schema.newBuilder() .setType(com.google.ai.edge.localagents.core.proto.Type.NUMBER) .setDescription(MIN_SIMILARITY_SCORE_DESCRIPTION) .build() ) .build() ) .build() val systemInstruction = Content.newBuilder() .setRole(Gemma3nRoles.SYSTEM.type) .addParts( Part.newBuilder().setText( PROMPT_TEMPLATE_WITH_TOOLS ) ) .build() val tool = Tool.newBuilder() .addFunctionDeclarations(searchDocs) .build() val inferenceBackend = LlmInferenceBackend( engineMediaPipe, GemmaFormatter() ) generativeModel = GenerativeModel( inferenceBackend, systemInstruction, listOf(tool), ) logDebug("startEngine ready", TAG) }
override fun generateResponse( userQuery: String, maxSteps: Int, ): Flow<ResultEmittedData<String>> = flow { logDebug("generateResponseWithTools userQuery: $userQuery", TAG) try { val generativeModel = generativeModel ?: run { logError("generateResponseWithTools generativeModel is null", TAG) emit( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Model is not initialized;", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } val contentPart = Part.newBuilder() .setText(userQuery) .build() val userContent = Content.newBuilder() .setRole(Gemma3nRoles.USER.type) .addParts(contentPart) .build() val conversation = mutableListOf(userContent) var step = 0
// про всяк випадок тут є цикл // моделі надсилається запит, якщо вона вважає, що потрібно викликати інструмент, // вона пише службову інформацію, інструмент викликається і запит з результатом повторюється // якщо вам потрібно, щоб модель намагалася знайти кращий результат запиту, // змінюючи текст запиту або мінімальну схожість, напишіть про це в промпті, щоб // модель знала, що викликів інструментів може бути багато while (step < maxSteps) { logDebug("generateResponseWithTools step: $step conversation: ${conversation.size}", TAG) step++ val response: GenerateContentResponse = generativeModel.generateContent( conversation ) val responseContent: Content = response.candidatesList.firstOrNull()?.content ?: run { logError("generateResponseWithTools content is null", TAG) emit( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Candidates list is null", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } val functionCall: FunctionCall? = responseContent.partsList.firstOrNull { it.hasFunctionCall() }?.functionCall
// якщо модель вирішила, що інструменти викликати не потрібно- просто надсилаємо відповідь користувачу if (functionCall == null) { val text = extractText(response) if (text.isBlank()) { logError( "generateResponseWithTools text is blank, response: $response", TAG ) emit( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Empty text", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } logDebug("generateResponseWithTools functionCall is null text: $text", TAG) emit( ResultEmittedData.success( model = text, message = null, responseCode = null ) ) return@flow } if (functionCall.name != TOOLS_ACTION_SEARCH_DOCS) { logError("generateResponseWithTools wrong name: ${functionCall.name}", TAG) val text = extractText(response) if (text.isBlank()) { logError( "generateResponseWithTools text is blank, response: $response", TAG ) emit( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Wrong function call", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } emit( ResultEmittedData.success( model = text, message = null, responseCode = null ) ) return@flow } val args = functionCall.args.fieldsMap
// модель повертає в параметрах виклику інструмента текст запиту до бази, // кількість результатів і схожість // якщо нічого не знайдено, у промпті вказано, що схожість можна зменшити val query = args[TOOLS_PARAM_QUERY]?.stringValue val topK = args[TOOLS_PARAM_TOP_K]?.numberValue?.toInt() ?: 5 val minSimilarityScore = args[TOOLS_PARAM_MIN_SIMILARITY_SCORE]?.numberValue?.toFloat() ?: 0.0F if (query.isNullOrEmpty()) { logError("generateResponseWithTools query is null or empty", TAG) val text = extractText(response) if (text.isBlank()) { logError( "generateResponseWithTools text is blank, response: $response", TAG ) emit( ResultEmittedData.error( model = null, error = null, title = "MediaPipe engine error", responseCode = null, message = "Wrong function call", errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } logDebug("generateResponseWithTools query is null or empty text: $text", TAG) emit( ResultEmittedData.success( model = text, message = null, responseCode = null ) ) return@flow } val results: String = searchDocsInternal( query, topK, minSimilarityScore ) val respStruct = Struct.newBuilder() .putFields( TOOLS_PARAM_QUERY, Value.newBuilder().setStringValue(query).build() ) .putFields( TOOLS_PARAM_TOP_K, Value.newBuilder().setNumberValue(topK.toDouble()).build() ) .putFields( TOOLS_PARAM_MIN_SIMILARITY_SCORE, Value.newBuilder().setNumberValue(minSimilarityScore.toDouble()).build() ) .putFields( RESULTS, Value.newBuilder().setStringValue(results).build() ) .build() val functionResponse = FunctionResponse.newBuilder() .setName(TOOLS_ACTION_SEARCH_DOCS) .setResponse(respStruct) .build() val functionResponsePart = Part.newBuilder() .setFunctionResponse(functionResponse) .build() val toolContent = Content.newBuilder() .setRole(Gemma3nRoles.MODEL.type) .addParts(functionResponsePart) .build()
// додаємо відповідь моделі з викликом інструмента і сам виклик інструмента // у ланцюжок повідомлень і запускаємо наступну ітерацію циклу // модель таким чином бачитиме всі свої запити і всі результати виклику інструмента conversation.add(responseContent) conversation.add(toolContent) logDebug("conversation: $conversation", TAG)
if (step == maxSteps) { val finalResponse = generativeModel.generateContent(conversation) val text = extractText(finalResponse) if (text.isBlank()) { logError("generateResponseWithTools finalResponse text is blank", TAG) emit( ResultEmittedData.error( title = "MediaPipe engine error", message = "Empty final response", error = null, model = null, responseCode = null, errorType = ErrorType.ERROR_IN_LOGIC, ) ) return@flow } emit( ResultEmittedData.success( model = text, message = null, responseCode = null ) ) return@flow } } } catch (t: Throwable) { logError("generateResponseWithTools failed: ${t.message}", t, TAG) emit( ResultEmittedData.error( model = null, error = t, title = "MediaPipe engine error", responseCode = null, message = t.message, errorType = ErrorType.EXCEPTION, ) ) } }
// пошук у векторній базі, при цьому всі параметри задає сама модель private suspend fun searchDocsInternal( query: String, topK: Int, minSimilarityScore: Float, ): String { logDebug("searchDocsInternal query: $query topK: $topK minSimilarityScore: $minSimilarityScore", TAG) val queryEmbedData: EmbedData<String> = EmbedData.create( query, EmbedData.TaskType.RETRIEVAL_QUERY ) val embeddingRequest: EmbeddingRequest<String> = EmbeddingRequest.create(listOf(queryEmbedData)) val vector: ImmutableList<Float> = try { common.embedder.getEmbeddings(embeddingRequest).await() } catch (t: Throwable) { logError( "searchDocsInternal: embedding failed: ${t.message}", t, TAG ) return "No results." } if (vector.isEmpty()) { logError("searchDocsInternal vector.isEmpty()", TAG) return "No results." } val hits: ImmutableList<VectorStoreRecord<String>> = try { common.vectorStore.getNearestRecords( vector, topK, minSimilarityScore ) } catch (t: Throwable) { logError("searchDocsInternal: failed: ${t.message}", t, TAG) return "No results." } if (hits.isEmpty()) { logError("searchDocsInternal hits.isEmpty()", TAG) return "No results." } val result = buildString { for (h in hits) { appendLine(h.data.trim()) } }.trim() logDebug("searchDocsInternal ready size: ${result.length}", TAG) return result }
private fun extractText(response: GenerateContentResponse): String { response.candidatesList.forEach { candidate -> candidate.content.partsList.forEach { part -> if (part.text.isNotEmpty()) return part.text } } return "" }
private fun createInterfaceOptions( modelFile: File, engineParams: MediaPipeEngineParams, isSupportImages: Boolean, ): LlmInferenceOptions { val backend = when (engineParams.backend) { MediaPipeBackendParams.CPU -> LlmInference.Backend.CPU MediaPipeBackendParams.GPU -> LlmInference.Backend.GPU } return LlmInferenceOptions.builder().apply { setModelPath(modelFile.absolutePath) setMaxTokens(engineParams.contextSize) setPreferredBackend(backend) val maxNumImages = if (isSupportImages) 1 else 0 setMaxNumImages(maxNumImages) if (engineParams.useMaxTopK) setMaxTopK(engineParams.maxTopK) }.build() }
}Сподіваюся було цікаво.
Хто захоче повторити, використані допоміжні класи:
enum class ErrorType { EXCEPTION, SERVER_ERROR, ERROR_IN_LOGIC, SERVER_DATA_ERROR, NO_INTERNET_CONNECTION, AUTHORIZATION}
data class ResultEmittedData<out T>( val model: T?, val error: Any?, val status: Status, val title: String?, val message: String?, val responseCode: Int?, val errorType: ErrorType?,) {
enum class Status { SUCCESS, ERROR, LOADING, }
companion object { fun <T> success( model: T, message: String?, responseCode: Int?, ): ResultEmittedData<T> = ResultEmittedData( error = null, title = null, model = model, errorType = null, message = message, status = Status.SUCCESS, responseCode = responseCode, )
fun <T> loading( model: T? = null, message: String? = null, ): ResultEmittedData<T> = ResultEmittedData( model = model, error = null, title = null, errorType = null, message = message, responseCode = null, status = Status.LOADING, )
fun <T> error( model: T?, error: Any?, title: String?, message: String?, responseCode: Int?, errorType: ErrorType?, ): ResultEmittedData<T> = ResultEmittedData( model = model, error = error, title = title, message = message, errorType = errorType, status = Status.ERROR, responseCode = responseCode, ) }}
inline fun <T : Any> ResultEmittedData<T>.onLoading( action: ( model: T?, message: String?, ) -> Unit): ResultEmittedData<T> { if (status == ResultEmittedData.Status.LOADING) action( model, message ) return this}
inline fun <T : Any> ResultEmittedData<T>.onSuccess( action: ( model: T, message: String?, responseCode: Int?, ) -> Unit): ResultEmittedData<T> { if (status == ResultEmittedData.Status.SUCCESS && model != null) action( model, message, responseCode, ) return this}
inline fun <T : Any> ResultEmittedData<T>.onFailure( action: ( model: Any?, title: String?, message: String?, responseCode: Int?, errorType: ErrorType?, ) -> Unit): ResultEmittedData<T> { if (status == ResultEmittedData.Status.ERROR) action( model, title, message, responseCode, errorType ) return this}data class MediaPipeEngineParams( val name: String, val topK: Int, val topP: Float, val temperature: Float, val randomSeed: Int, val contextSize: Int, val maxTopK: Int, val useTopK: Boolean, val useTopP: Boolean, val useTemperature: Boolean, val useRandomSeed: Boolean, val useMaxTopK: Boolean, val backend: MediaPipeBackendParams,)
enum class MediaPipeBackendParams { CPU, GPU}
fun Long.toDurationString(): String { var msRemaining = this
val years = msRemaining / (365L * 24 * 60 * 60 * 1000) msRemaining %= (365L * 24 * 60 * 60 * 1000)
val months = msRemaining / (30L * 24 * 60 * 60 * 1000) msRemaining %= (30L * 24 * 60 * 60 * 1000)
val days = msRemaining / (24L * 60 * 60 * 1000) msRemaining %= (24L * 60 * 60 * 1000)
val hours = msRemaining / (60L * 60 * 1000) msRemaining %= (60L * 60 * 1000)
val minutes = msRemaining / (60L * 1000) msRemaining %= (60L * 1000)
val seconds = msRemaining / 1000 val milliseconds = msRemaining % 1000
return buildString { if (years > 0) append("$years years, ") if (months > 0) append("$months months, ") if (days > 0) append("$days days, ") if (hours > 0) append("$hours hours, ") if (minutes > 0) append("$minutes minutes, ") if (seconds > 0) append("$seconds seconds, ") append("$milliseconds milliseconds") }}import android.annotation.SuppressLintimport android.os.Bundleimport android.os.Environmentimport android.util.Logimport com.google.firebase.analytics.FirebaseAnalyticsimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.Jobimport kotlinx.coroutines.launchimport org.koin.core.component.KoinComponentimport org.koin.core.component.injectimport java.io.Fileimport java.io.FileOutputStreamimport java.text.SimpleDateFormatimport java.util.Date
object LogUtil : KoinComponent {
private val timeDirectoryName: String
private const val QUEUE_CAPACITY = 10000 private const val CURRENT_TAG = "LogUtilExecutionStatusTag" private const val LOG_APP_FOLDER_NAME = "app" private const val TIME_FORMAT_FOR_LOG = "HH:mm:ss dd-MM-yyyy" private const val TIME_FORMAT_FOR_DIRECTORY = "HH-mm-ss_dd-MM-yyyy" private const val TAG = "TAG: " private const val TIME = "TIME: " private const val ERROR_STACKTRACE = "ERROR STACKTRACE: " private const val ERROR_MESSAGE = "ERROR: " private const val DEBUG_MESSAGE = "MESSAGE: " private const val NEW_LINE = "\n"
private val queue = ArrayDeque<LogData>(QUEUE_CAPACITY)
private var saveLogsToTxtFileJob: Job? = null
private val analytics: FirebaseAnalytics by inject()
@Volatile private var isSaveLogsToTxtFile = false
init { Log.d(CURRENT_TAG, "init") timeDirectoryName = getCurrentTimeForDirectory() }
fun logDebug(message: String, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.d(tag, message) enqueue( LogData.DebugMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, message) enqueue( LogData.ErrorMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
fun logError(exception: Throwable, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, exception.message, exception) enqueue( LogData.ExceptionMessage( tag = tag, time = System.currentTimeMillis(), exception = exception, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, exception: Throwable, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, "$message, exception: ${exception.message}", exception) enqueue( LogData.ErrorMessageWithException( tag = tag, time = System.currentTimeMillis(), message = message, exception = exception, ) ) saveLogsToTxtFile() } } }
fun logError(message: String, error: String?, tag: String) { CoroutineScope(Dispatchers.IO).launch { if (BuildConfig.DEBUG) { Log.e(tag, "$message, error: $error") enqueue( LogData.ErrorMessage( tag = tag, time = System.currentTimeMillis(), message = message, ) ) saveLogsToTxtFile() } } }
@SuppressLint("SimpleDateFormat") private fun getTime(time: Long): String { return try { val date = Date(time) val timeString = SimpleDateFormat(TIME_FORMAT_FOR_LOG).format(date) timeString.ifEmpty { Log.e(CURRENT_TAG, "getTime time.ifEmpty") time.toString() } } catch (e: Exception) { Log.e(CURRENT_TAG, "getCurrentTime exception: ${e.message}", e) time.toString() } }
@SuppressLint("SimpleDateFormat") private fun getCurrentTimeForDirectory(): String { val time = System.currentTimeMillis() return try { val date = Date(time) val timeString = SimpleDateFormat(TIME_FORMAT_FOR_DIRECTORY).format(date) Log.d(CURRENT_TAG, "getCurrentTimeForDirectory time: $time") timeString.ifEmpty { Log.e(CURRENT_TAG, "getCurrentTimeForDirectory time.ifEmpty") time.toString() } } catch (e: Exception) { Log.e(CURRENT_TAG, "getCurrentTimeForDirectory exception: ${e.message}", e) time.toString() } }
private fun enqueue(message: LogData) { try { while (queue.size >= QUEUE_CAPACITY) { Log.d(CURRENT_TAG, "enqueue removeFirst") queue.removeFirst() } queue.addLast(message) } catch (e: Exception) { Log.e(CURRENT_TAG, "enqueue exception: ${e.message}", e) } }
}Приклад 6 - CAG (Cache-Augmented Generation) у LLama.cpp
Тут я напишу концепцію, як це може працювати.
Конкретно цей код працює повільно і для повноцінного використання не годиться.
Приклад отримання контексту моделі як ByteArray.
Далі отриманий контекст можна зберегти в базу даних або файл.
Контекст може включати промпт, питання та відповіді, додані документи і так далі.
#include <vector>#include <cstdint>#include <stdexcept>#include <cstddef>#include "llama.h"
std::vector<uint8_t> get_full_state_raw(llama_context* ctx) {
// Перевіряємо, що вказівник на контекст не дорівнює nullptr if (ctx == nullptr) { throw std::invalid_argument("llama_context pointer is null"); }
// Отримуємо розмір стану моделі (у байтах) const size_t state_size = llama_state_get_size(ctx); if (state_size == 0) { throw std::runtime_error("llama_state_get_size returned 0"); }
// Виділяємо буфер потрібного розміру для зберігання стану std::vector<uint8_t> out(state_size);
// Зберігаємо бінарні дані стану в наш буфер const size_t written = llama_state_get_data(ctx, out.data(), out.size());
// Перевіряємо, що розмір збігається з очікуваним if (written != state_size) { throw std::runtime_error("state size changed during serialization"); }
// Повертаємо стан як масив байтів return out;}Приклад відновлення контексту з ByteArray.
Потрібно пам'ятати, що контекст вдасться відновити тільки для тієї самої моделі, для якої він був збережений, оскільки він включає проміжні стани, також не вдасться комбінувати кілька контекстів- ще немає повноцінної модульної архітектури для моделей.
#include <vector>#include <cstdint>#include <stdexcept>#include <cstddef>#include "llama.h"
int set_full_state_raw(llama_context* ctx, const std::vector<uint8_t>& data) { // Перевіряємо, що контекст і дані передані if (ctx == nullptr) { throw std::invalid_argument("llama_context pointer is null"); } if (data.empty()) { throw std::invalid_argument("data is empty"); }
// Відновлюємо стан моделі з масиву байтів const size_t written = llama_state_set_data(ctx, data.data(), data.size());
// Повертаємо кількість реально завантажених байтів return static_cast<int>(written);}