golang

Как сделать (очень) маленькие LLM действительно полезными

  • суббота, 24 января 2026 г. в 00:00:14
https://habr.com/ru/articles/986770/

Команда AI for Devs подготовила перевод статьи о том, как выжать максимум из маленьких языковых моделей. Автор показывает, что даже очень компактные LLM могут быть полезны в реальных задачах — если правильно работать с контекстом, embeddings и RAG.


Здравствуйте, меня зовут Филипп, я Principal Solutions Architect и помогаю клиентам эффективно использовать Docker. Серьёзно интересоваться генеративным ИИ я начал около двух лет назад. Больше всего меня привлекает возможность запускать языковые модели (LLM) прямо на собственном ноутбуке. Для работы у меня MacBook Pro M2 Max, а в личных проектах я запускаю LLM на своём MacBook Air M4 и на Raspberry Pi — да, это возможно, но об этом я расскажу в другой раз.

Сразу проясню: воспроизвести Claude AI Desktop или ChatGPT на ноутбуке с помощью небольших языковых моделей невозможно. Тем более что я сознательно ограничиваюсь моделями от 0,5 до 7 миллиардов параметров. Но мне кажется интересным вызовом понять, насколько далеко можно зайти с такими маленькими моделями. Так можно ли делать действительно полезные вещи с небольшими LLM? Ответ — да, но для этого нужно проявить изобретательность и приложить определённые усилия.

Я возьму конкретный пример, связанный с разработкой (хотя в будущем предложу и менее технические сценарии).

(Конкретный) сценарий использования: помощь в написании кода

Мне нужна помощь с написанием кода.

Сейчас в свободное время я работаю над open source-проектом — это библиотека на Golang для быстрого создания небольших генеративных ИИ-агентов. Этот проект нужен и для того, чтобы глубже разобраться в Golang, и для подготовки инструментов для других задач. Проект называется Nova; ничего секретного в нём нет, вы можете найти его здесь.

Если воспользоваться Claude AI и попросить его помочь с кодом для Nova, например:
«Мне нужен пример кода Golang для чат-агента Nova с потоковой генерацией ответа».

Результат будет довольно разочаровывающим, потому что Claude не знает о Nova (что вполне нормально — проект новый). Но Claude не хочет меня разочаровывать и всё равно предложит что-то, что на самом деле никак не связано с моим проектом.

То же самое будет и с Gemini.

Вы скажете: тогда просто передай Claude AI или Gemini исходный код репозитория. Хорошо, но представим ситуацию, когда у меня нет доступа к этим сервисам по разным причинам. Это может быть связано с конфиденциальностью или с тем, что я работаю над проектом, где, например, запрещено использование интернета. В таких условиях Claude AI и Gemini сразу отпадают. Как тогда получить помощь в написании кода с помощью небольшой локальной LLM? Как вы уже догадались — с помощью локальной LLM. Более того, с помощью «очень маленькой» LLM.

Выбор языковой модели

При разработке решения на базе генеративного ИИ выбор языковой модели имеет ключевое значение. Придётся много следить за технологиями, проводить исследования и тестирование, чтобы найти модель, которая лучше всего подходит под ваш сценарий использования. И важно понимать, что это нетривиальная работа.

Для этой статьи (и потому что я сам её использую) я возьму модель hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m, которую можно найти здесь. Это языковая модель с 3 миллиардами параметров, оптимизированная для генерации кода. Установить её можно с помощью Docker Model Runner следующей командой:

docker model pull hf.co/Qwen/Qwen2.5-Coder-3B-Instruct-GGUF:Q4_K_M

А чтобы начать общение с моделью, можно использовать такую команду:

docker model run hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m

Или воспользоваться Docker Desktop.

Разумеется, как видно на иллюстрации выше, этот маленький «Qwen Coder» тоже не знает о моей библиотеке Nova. Но это мы сейчас исправим.

Подкормка модели специфической информацией

Для своего проекта я веду markdown-файл, в котором сохраняю примеры кода, используемые при разработке примеров с Nova. Найти его можно здесь. Пока в нём немного содержимого, но этого достаточно, чтобы доказать и проиллюстрировать мою мысль.

Можно было бы просто добавить весь текст этого файла в пользовательский запрос, который я передам модели. Но это будет неэффективно. Дело в том, что у маленьких моделей относительно небольшое контекстное окно. И даже если бы мой «Qwen Coder» смог проглотить весь markdown-файл целиком, ему было бы сложно сфокусироваться на моём запросе и понять, что именно делать с этой информацией. Поэтому:

  • Первое ключевое правило: при работе с очень маленькой LLM чем больше контента вы передаёте модели, тем менее эффективно она будет работать.

  • Второе ключевое правило: чем больше истории диалога вы сохраняете, тем сильнее разрастается контент, передаваемый модели, а значит, тем ниже становится её эффективность.

Чтобы обойти эту проблему, я буду использовать технику под названием RAG (Retrieval Augmented Generation). Принцип здесь простой: вместо того чтобы скармливать модели весь контент целиком, мы сохраняем его в базе данных векторного типа, а когда пользователь делает запрос, ищем в этой базе наиболее релевантную информацию на основе этого запроса. Затем передаём языковой модели только эту релевантную часть. В рамках этой статьи данные будут храниться в памяти (что не оптимально, но вполне достаточно для демонстрации).

RAG?

На эту тему уже написано множество статей, поэтому я не буду углубляться в детали. Вместо этого расскажу, что именно я собираюсь сделать в рамках этого блога:

  1. Мой файл со сниппетами состоит из разделов: заголовок markdown (## имя сниппета), при необходимости — описание в свободной форме и блок кода (golang …).

  2. Разобью этот файл по разделам на фрагменты текста (их также называют chunks),

  3. Затем для каждого раздела я создам embedding — векторное представление текста (то есть математическое представление его семантического смысла) — с помощью модели ai/embeddinggemma:latest (относительно небольшой и эффективной модели для embeddings). После этого я сохраню эти embeddings (вместе с соответствующим текстом) во временной in-memory векторной базе данных (простом массиве JSON-объектов).

  4. Если вы хотите узнать больше про embeddings, прочитайте эту статью: Run Embedding Models and Unlock Semantic Search with Docker Model Runner

Диаграмма процесса создания векторной базы данных:

Поиск по сходству и формирование пользовательского запроса

Когда всё это будет готово, при обращении к языковой модели (то есть hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m) я буду делать следующее:

  • Создават�� embedding пользовательского запроса с помощью модели для embeddings,

  • Сравнивать этот embedding с embeddings, сохранёнными в векторной базе данных, чтобы найти наиболее релевантные разделы (путём вычисления расстояния между векторным представлением моего вопроса и векторными представлениями сниппетов). Это называется поиском по сходству (similarity search),

  • На основе самых релевантных (наиболее похожих) разделов формировать пользовательский запрос, который будет включать только действительно полезную информацию и мой исходный вопрос.

Диаграмма процесса поиска и формирования пользовательского запроса:

Итак, итоговый пользовательский запрос будет содержать:

  • Системные инструкции. Например: «Ты — полезный ассистент по написанию кода, специализирующийся на Golang и библиотеке Nova. Используй предоставленные сниппеты кода, чтобы помогать пользователю с его запросами»;

  • Релевантные разделы, извлечённые из векторной базы данных;

  • Сам запрос пользователя.

Замечания:

  • Я объясняю принципы и результаты, но весь исходный код (NodeJS с LangchainJS), использованный для получения этих выводов, доступен в этом проекте;

  • Для вычисления расстояний между векторами я использовал косинусное сходство (значение косинусного сходства, равное 1, означает, что векторы направлены в одну сторону; значение 0 означает, что векторы ортогональны, то есть не имеют направленного сходства);

  • JavaScript-функцию, которую я использовал, можно найти здесь;

  • А вот фрагмент кода, который я использую для разбиения markdown-файла со сниппетами.

  • Предупреждение: модели для embeddings ограничены размером текстовых фрагментов, которые они могут обрабатывать. Поэтому при разбиении исходного файла нужно внимательно следить, чтобы не превышать этот лимит. В некоторых случаях придётся менять стратегию разбиения (например, использовать фрагменты фиксированного размера — с перекрытием или без него).

Реализация и результаты, или как создать моего агентa-эксперта по Golang

Теперь, когда у нас есть принцип работы, давайте посмотрим, как превратить это в рабочее решение с LangchainJS, Docker Model Runner и Docker Agentic Compose.

Конфигурация Docker Agentic Compose

Начнём со структуры проекта Docker Agentic Compose:

services:
  golang-expert:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      TERM: xterm-256color
 
      HISTORY_MESSAGES: 2
      MAX_SIMILARITIES: 3
      COSINE_LIMIT: 0.45
 
      OPTION_TEMPERATURE: 0.0
      OPTION_TOP_P: 0.75
      OPTION_PRESENCE_PENALTY: 2.2
 
      CONTENT_PATH: /app/data
 
    volumes:
      - ./data:/app/data
 
    stdin_open: true   # docker run -i
    tty: true          # docker run -t
 
    configs:
      - source: system.instructions.md
        target: /app/system.instructions.md
 
    models:
      chat-model:
        endpoint_var: MODEL_RUNNER_BASE_URL
        model_var: MODEL_RUNNER_LLM_CHAT
 
      embedding-model:
        endpoint_var: MODEL_RUNNER_BASE_URL
        model_var: MODEL_RUNNER_LLM_EMBEDDING
 
 
models:
  chat-model:
    model: hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m
 
  embedding-model:
    model: ai/embeddinggemma:latest
 
configs:
  system.instructions.md:
    content: |
      Your name is Bob (the original replicant).
      You are an expert programming assistant in Golang.
      You write clean, efficient, and well-documented code.
      Always:
      - Provide complete, working code
      - Include error handling
      - Add helpful comments
      - Follow best practices for the language
      - Explain your approach briefly
 
      Use only the information available in the provided data and your KNOWLEDGE BASE.

Здесь важно следующее:

Я храню в истории диалога только последние 2 сообщения и выбираю максимум 2–3 лучших совпадения (чтобы ограничить размер пользовательского запроса):

HISTORY_MESSAGES: 2
MAX_SIMILARITIES: 3
COSINE_LIMIT: 0.45

Вы можете подбирать эти значения под свой сценарий и возможности вашей языковой модели.

Раздел models, где я задаю языковые модели, которые буду использовать:

models:
  chat-model:
    model: hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m
 
  embedding-model:
    model: ai/embeddinggemma:latest

Одно из преимуществ этого раздела в том, что он позволяет Docker Compose скачивать модели, если их ещё нет на вашей машине.

А также раздел models у сервиса golang-expert, где я сопоставляю переменные окружения с моделями, объявленными выше:

models:
    chat-model:
    endpoint_var: MODEL_RUNNER_BASE_URL
    model_var: MODEL_RUNNER_LLM_CHAT
 
    embedding-model:
    endpoint_var: MODEL_RUNNER_BASE_URL
    model_var: MODEL_RUNNER_LLM_EMBEDDING

И, наконец, конфигурация файла системных инструкций:

configs:
    - source: system.instructions.md
    target: /app/system.instructions.md

Её содержимое я задаю чуть ниже в секции configs:

configs:
  system.instructions.md:
    content: |
      Your name is Bob (the original replicant).
      You are an expert programming assistant in Golang.
      You write clean, efficient, and well-documented code.
      Always:
      - Provide complete, working code
      - Include error handling
      - Add helpful comments
      - Follow best practices for the language
      - Explain your approach briefly
 
      Use only the information available in the provided data and your KNOWLEDGE BASE.

Разумеется, вы можете адаптировать эти системные инструкции под свой сценарий. При желании их можно хранить в отдельном файле.

Dockerfile

Он довольно простой:

FROM node:22.19.0-trixie
 
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY *.js .
 
# Create non-root user
RUN groupadd --gid 1001 nodejs && \
    useradd --uid 1001 --gid nodejs --shell /bin/bash --create-home bob-loves-js
 
# Change ownership of the app directory
RUN chown -R bob-loves-js:nodejs /app
 
# Switch to non-root user
USER bob-loves-js

Теперь, когда конфигурация готова, перейдём к исходному коду агента.

Исходный код агента-эксперта по Golang: немного LangchainJS с RAG

JavaScript-код довольно простой (его, вероятно, можно улучшить, но он рабочий) и выполняет такие основные шаги:

1.Начальная конфигурация

  • Подключение к обеим моделям (чат и embeddings) через LangchainJS;

  • Загрузка параметров из переменных окружения.

2.Создание векторной базы данных (при старте)

  • Чтение файла snippets.md;

  • Разбиение на разделы (фрагменты, chunks);

  • Генерация embedding для каждого раздела;

  • Сохранение во временной in-memory векторной базе данных.

3. Интерактивный цикл диалога

  • Пользователь задаёт вопрос;

  • Создаётся embedding вопроса;

  • Выполняется поиск по сходству во векторной базе, чтобы найти наиболее релевантные сниппеты;

  • Формируется итоговый запрос из: истории + системных инструкций + релевантных сниппетов + вопроса;

  • Запрос отправляется в LLM, а ответ выводится в режиме стриминга;

  • История обновляется (и ограничивается последними N сообщениями).

import { ChatOpenAI } from "@langchain/openai";
import { OpenAIEmbeddings} from '@langchain/openai';
 
import { splitMarkdownBySections } from './chunks.js'
import { VectorRecord, MemoryVectorStore } from './rag.js';
 
 
import prompts from "prompts";
import fs from 'fs';
 
// Define [CHAT MODEL] Connection
const chatModel = new ChatOpenAI({
  model: process.env.MODEL_RUNNER_LLM_CHAT || `ai/qwen2.5:latest`,
  apiKey: "",
  configuration: {
    baseURL: process.env.MODEL_RUNNER_BASE_URL || "http://localhost:12434/engines/llama.cpp/v1/",
  },
  temperature: parseFloat(process.env.OPTION_TEMPERATURE) || 0.0,
  top_p: parseFloat(process.env.OPTION_TOP_P) || 0.5,
  presencePenalty: parseFloat(process.env.OPTION_PRESENCE_PENALTY) || 2.2,
});
 
 
// Define [EMBEDDINGS MODEL] Connection
const embeddingsModel = new OpenAIEmbeddings({
    model: process.env.MODEL_RUNNER_LLM_EMBEDDING || "ai/embeddinggemma:latest",
    configuration: {
    baseURL: process.env.MODEL_RUNNER_BASE_URL || "http://localhost:12434/engines/llama.cpp/v1/",
        apiKey: ""
    }
})
 
const maxSimilarities = parseInt(process.env.MAX_SIMILARITIES) || 3
const cosineLimit = parseFloat(process.env.COSINE_LIMIT) || 0.45
 
// ----------------------------------------------------------------
//  Create the embeddings and the vector store from the content file
// ----------------------------------------------------------------
 
console.log("========================================================")
console.log(" Embeddings model:", embeddingsModel.model)
console.log(" Creating embeddings...")
let contentPath = process.env.CONTENT_PATH || "./data"
 
const store = new MemoryVectorStore();
 
let contentFromFile = fs.readFileSync(contentPath+"/snippets.md", 'utf8');
let chunks = splitMarkdownBySections(contentFromFile);
console.log(" Number of documents read from file:", chunks.length);
 
 
// -------------------------------------------------
// Create and save the embeddings in the memory vector store
// -------------------------------------------------
console.log(" Creating the embeddings...");
 
for (const chunk of chunks) {
  try {
    // EMBEDDING COMPLETION:
    const chunkEmbedding = await embeddingsModel.embedQuery(chunk);
    const vectorRecord = new VectorRecord('', chunk, chunkEmbedding);
    store.save(vectorRecord);
 
  } catch (error) {
    console.error(`Error processing chunk:`, error);
  }
}
 
console.log(" Embeddings created, total of records", store.records.size);
console.log();
 
 
console.log("========================================================")
 
 
// Load the system instructions from a file
let systemInstructions = fs.readFileSync('/app/system.instructions.md', 'utf8');
 
// ----------------------------------------------------------------
// HISTORY: Initialize a Map to store conversations by session
// ----------------------------------------------------------------
const conversationMemory = new Map()
 
let exit = false;
 
// CHAT LOOP:
while (!exit) {
  const { userMessage } = await prompts({
    type: "text",
    name: "userMessage",
    message: `Your question (${chatModel.model}): `,
    validate: (value) => (value ? true : "Question cannot be empty"),
  });
 
  if (userMessage == "/bye") {
    console.log(" See you later!");
    exit = true;
    continue
  }
 
  // HISTORY: Get the conversation history for this session
  const history = getConversationHistory("default-session-id")
 
  // ----------------------------------------------------------------
  // SIMILARITY SEARCH:
  // ----------------------------------------------------------------
  // -------------------------------------------------
  // Create embedding from the user question
  // -------------------------------------------------
  const userQuestionEmbedding = await embeddingsModel.embedQuery(userMessage);
 
  // -------------------------------------------------
  // Use the vector store to find similar chunks
  // -------------------------------------------------
  // Create a vector record from the user embedding
  const embeddingFromUserQuestion = new VectorRecord('', '', userQuestionEmbedding);
 
  const similarities = store.searchTopNSimilarities(embeddingFromUserQuestion, cosineLimit, maxSimilarities);
 
  let knowledgeBase = "KNOWLEDGE BASE:\n";
 
  for (const similarity of similarities) {
    console.log(" CosineSimilarity:", similarity.cosineSimilarity, "Chunk:", similarity.prompt);
    knowledgeBase += `${similarity.prompt}\n`;
  }
 
  console.log("\n Similarities found, total of records", similarities.length);
  console.log();
  console.log("========================================================")
  console.log()
 
  // -------------------------------------------------
  // Generate CHAT COMPLETION:
  // -------------------------------------------------
 
  // MESSAGES== PROMPT CONSTRUCTION:
  let messages = [
      ...history,
      ["system", systemInstructions],
      ["system", knowledgeBase],
      ["user", userMessage]
  ]
 
  let assistantResponse = ''
  // STREAMING COMPLETION:
  const stream = await chatModel.stream(messages);
  for await (const chunk of stream) {
    assistantResponse += chunk.content
    process.stdout.write(chunk.content);
  }
  console.log("\n");
 
  // HISTORY: Add both user message and assistant response to history
  addToHistory("default-session-id", "user", userMessage)
  addToHistory("default-session-id", "assistant", assistantResponse)
 
}
 
// Helper function to get or create a conversation history
function getConversationHistory(sessionId, maxTurns = parseInt(process.env.HISTORY_MESSAGES)) {
  if (!conversationMemory.has(sessionId)) {
    conversationMemory.set(sessionId, [])
  }
  return conversationMemory.get(sessionId)
}
 
// Helper function to add a message to the conversation history
function addToHistory(sessionId, role, content) {
  const history = getConversationHistory(sessionId)
  history.push([role, content])
 
  // Keep only the last maxTurns conversations
  const maxTurns = parseInt(process.env.HISTORY_MESSAGES) // Adjust this value based on your needs
  if (history.length > maxTurns * 2) { // *2 because each turn has user & assistant message
    history.splice(0, 2) // Remove oldest turn (user + assistant messages)
  }
}

Вы можете найти его здесь:

Остаётся только запустить всё это и проверить мои гипотезы.

В папке проекта выполните команду:

docker compose up --build --no-log-prefix -d

Затем подключитесь к контейнеру и запустите приложение:

docker compose exec golang-expert /bin/bash
node index.js

Чат-агент Nova с потоковой генерацией ответа

После запуска приложения самое время задать нашему агенту-эксперту по Golang вопрос, связанный с разработкой на библиотеке Nova:

========================================================
 Embeddings model: ai/embeddinggemma:latest
 Creating embeddings...
 Number of documents read from file: 4
 Creating the embeddings...
 Embeddings created, total of records 4
 
========================================================
?  Your question (hf.co/qwen/qwen2.5-coder-3b-instruct-gguf:q4_k_m):  › I need a code snippet of a Golang Nova Chat agent using a stream completion

После быстрого поиска по сходству агент нашёл два релевантных сниппета в векторной базе данных. И смог выдать мне полный, рабочий и снабжённый комментариями код, который отвечает на мой запрос:

Certainly! Below is a complete, working Go code snippet for a Nova Chat agent that uses streaming completion. This example demonstrates how to set up an agent with the necessary configurations and generate a stream completion response.
 
```go
package main
 
import (
        "context"
        "fmt"
 
        "github.com/snipwise/nova/nova-sdk/agents"
        "github.com/snipwise/nova/nova-sdk/agents/chat"
        "github.com/snipwise/nova/nova-sdk/messages"
        "github.com/snipwise/nova/nova-sdk/messages/roles"
        "github.com/snipwise/nova/nova-sdk/models"
)
 
func main() {
 
        ctx := context.Background()
 
        // Create a new chat agent with streaming capabilities
        agent, err := chat.NewAgent(
                ctx,
                agents.Config{
                        EngineURL:          "http://localhost:12434/engines/llama.cpp/v1",
                        SystemInstructions: "You are Bob, a helpful AI assistant.",
                        KeepConversationHistory: true,
                },
                models.Config{
                        Name:        "ai/qwen2.5:1.5B-F16",
                        Temperature: models.Float64(0.8),
                },
        )
        if err != nil {
                panic(err)
        }
 
        // Generate a stream completion response
        result, err := agent.GenerateStreamCompletion(
                []messages.Message{
                        {Role: roles.User, Content: "Who is James T Kirk?"},
                },
                func(chunk string, finishReason string) error {
                        // Simple callback that receives strings only
                        if chunk != "" {
                                fmt.Print(chunk)
                        }
                        if finishReason == "stop" {
                                fmt.Println()
                        }
                        return nil
                },
        )
        if err != nil {
                panic(err)
        }
 
        // Print the final response and finish reason
        fmt.Println("Final Response:", result.Response)
        fmt.Println("Finish Reason:", result.FinishReason)
}
### Explanation:
 
1. **Imports**: The code imports necessary packages from the Nova SDK to interact with chat agents.
 
2. **Context**: A context is created using `context.Background()` to manage the lifecycle of requests.
 
3. **Agent Creation**:
   - An agent is created using `chat.NewAgent` with specified configurations:
     - `EngineURL`: The URL of the engine to use for generating text.
     - `SystemInstructions`: Instructions for the AI assistant, such as who Bob is.
     - `KeepConversationHistory`: Whether to keep a history of previous messages.
 
4. **Model Configuration**:
   - A model configuration is provided with:
     - `Name`: The name of the model to use.
     - `Temperature`: Controls the randomness of the output (higher values make responses more random).
     - `MaxTokens`: Limits the maximum number of tokens in the response.
 
5. **Stream Completion Generation**:
   - The `GenerateStreamCompletion` method is called with a list of messages and a callback function that processes each chunk of text as it arrives.
   - The callback prints chunks to the console until the finish reason is "stop", indicating the completion has been reached.
 
6. **Output**:
   - The final response and finish reason are printed after the stream completes.
 
This code snippet demonstrates how to set up a streaming chat agent using Nova, allowing you to receive text responses incrementally as they become available.

Затем я задал другие вопросы.

Structured Agent в Nova

На этот раз я попросил пример для структурированного агента: «Мне нужен пример кода Nova Structured Agent».

И результат получился убедительным.

Фрагмент ответа:

Certainly! Below is a complete, working example of a Nova Structured Agent in Go. This agent will generate structured data about a specified country based on the user's input.
 
```go
package main
 
import (
        "context"
        "fmt"
 
        "github.com/snipwise/nova/nova-sdk/agents"
        "github.com/snipwise/nova/nova-sdk/agents/structured"
        "github.com/snipwise/nova/nova-sdk/messages"
        "github.com/snipwise/nova/nova-sdk/messages/roles"
        "github.com/snipwise/nova/nova-sdk/models"
)
 
type Country struct {
        Name       string   `json:"name"`
        Capital    string   `json:"capital"`
        Population int      `json:"population"`
        Languages  []string `json:"languages"`
}
 
func main() {
        ctx := context.Background()
 
        // Create a new Structured Agent
        agent, err := structured.NewAgent[Country](
                ctx,
                agents.Config{
                        EngineURL: "http://localhost:12434/engines/llama.cpp/v1",
                        SystemInstructions: `
                                Your name is Bob.
                                You are an assistant that answers questions about countries around the world.
                        `,
                },
                models.Config{
                        Name:        "hf.co/menlo/jan-nano-gguf:q4_k_m",
                        Temperature: models.Float64(0.0),
                },
        )
        if err != nil {
                panic(err)
        }
 
        // Generate structured data about Canada
        response, finishReason, err := agent.GenerateStructuredData([]messages.Message{
                {Role: roles.User, Content: "Tell me about Canada."},
        })
 
        if err != nil {
                panic(err)
        }
 
        // Print the generated country information
        fmt.Println("Country Name:", response.Name)
        fmt.Println("Capital:", response.Capital)
        fmt.Println("Population:", response.Population)
        fmt.Println("Languages:", strings.Join(response.Languages, ", "))
}
### Explanation:
 
1. **Imports**: The code imports necessary packages from the Nova SDK to interact with agents and models.
 
2. **Country Struct**: A simple struct `Country` is defined to represent the data structure expected by the agent.
 
3. **Main Function**:
   - A context is created using `context.Background()`.
   - A new Structured Agent is created for generating structured data about countries.
   - The system instructions are set to provide a friendly greeting and instruction on answering questions about countries.
   - The model configuration specifies the engine URL, temperature, and other parameters.
 
4. **GenerateStructuredData Method**: This method generates structured data based on the user's input. It takes an array of messages as input, where each message has a role (e.g., `User`) and content (e.g., "Tell me about Canada").
 
5. **Error Handling**: The code includes error handling to ensure that any issues during agent creation or data generation are properly caught and reported.
 
6. **Output**: The generated country information is printed, including the name, capital, population, and languages.
 
This example demonstrates how to use a Nova Structured Agent to generate structured data about countries based on user input. You can modify the system instructions and model configuration as needed for your specific use case.

Затем я перешёл к следующему вопросу.

RAG-агент Nova с векторным хранилищем. Осторожнее с настройками сходства!

На этот раз я попросил «RAG»-агента: «Мне нужен сниппет Nova RAG agent с векторным хранилищем».

И снова получил релевантный ответ.

Однако когда я попробовал такой вопрос (после перезапуска агента, чтобы начать с чистого листа без истории диалога): «Мне нужен сниппет Nova RAG agent».

Поиск по сходству не вернул ничего релевантного (потому что в сниппетах не было слов «vector store»). И агент ответил обобщённым кодом, который либо вообще не имел отношения к Nova, либо использовал код из Nova Chat Agents.

Причин может быть несколько:

  • Модель для embeddings не подходит под мой сценарий;

  • Модель для embeddings недостаточно точная;

  • Разбиение файла со сниппетами кода устроено не оптимально (например, можно добавлять метаданные к фрагментам, чтобы улучшить поиск по сходству, но не забывайте: фрагменты не должны превышать максимальный размер, который модель embeddings способна обработать).

В таком случае есть простое решение, которое довольно хорошо работает: снизить пороги сходства и/или увеличить число возвращаемых совпадений. Это даёт больше результатов для формирования пользовательского запроса, но важно следить, чтобы не превысить максимальный размер контекста чат-модели. Также можно протестировать другие, «побольше» LLM (с большим числом параметров и/или с более широким контекстным окном).

В последней версии файла со сниппетами я добавил строку KEYWORDS: … сразу под markdown-заголовками, чтобы помочь поиску по сходству. Это заметно улучшило результаты.

Вывод

Работа с «Small Language Models» (SLM) или «Tiny Language Models» (TLM) требует дополнительных усилий и продуманности, чтобы обходить их ограничения. Но при этом вполне реально собирать эффективные решения под очень узкие, конкретные задачи. И, как всегда, нужно держать в голове размер контекста чат-модели и то, как вы структурируете информацию для модели embeddings. А комбинируя несколько специализированных «маленьких агентов», можно получать очень интересные результаты. Этому и будут посвящены следующие статьи.

Русскоязычное сообщество про AI в разработке

Друзья! Эту статью подготовила команда ТГК «AI for Devs» — канала, где мы рассказываем про AI-ассистентов, плагины для IDE, делимся практическими кейсами и свежими новостями из мира ИИ. Подписывайтесь, чтобы быть в курсе и ничего не упустить!