УРОК 29 / 30 0%

Spring AI + LLM

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 Что такое LLM и как с ним общаться

LLM (Large Language Model) — нейросеть, обученная на текстах. Примеры: GPT-4, Claude, Llama, Mistral.

Общение — через prompt: текстовый запрос → текстовый ответ.

2 Что такое Spring AI

Spring AI — Spring-абстракция над разными LLM-провайдерами (OpenAI, Anthropic, Ollama, Azure, AWS Bedrock).

Как JdbcTemplate для разных БД — один API, разные драйверы.

3 Ключевые концепции
  • ChatClient — высокоуровневый API для диалога
  • Prompt — структурированный запрос (system + user message)
  • Embedding — векторное представление текста (для семантического поиска)
  • RAG (Retrieval Augmented Generation) — LLM + поиск по БД → ответ на основе реальных данных
  • Tools/Function calling — LLM вызывает наш Java-код (например, findTask(id))

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости
    
    dependencies {
        implementation("org.springframework.ai:spring-ai-starter-model-openai-spring-boot-starter:1.0.0-M6")
    }
    

    ⚠️ Spring AI ещё в milestone — версия может отличаться.

  2. 2
    application.yml
    
    spring:
      ai:
        openai:
          api-key: ${OPENAI_API_KEY}
          chat:
            options:
              model: gpt-4o-mini     # дешевле, чем gpt-4
              temperature: 0.7
    
    taskflow:
      ai:
        enabled: true
    

    ⚠️ OPENAI_API_KEY — переменная окружения. НЕ коммитить в Git!

  3. 3
    service/AiService.java
    
    package com.taskflow.service;
    
    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.ai.chat.prompt.Prompt;
    import org.springframework.ai.chat.prompt.SystemPromptTemplate;
    import org.springframework.ai.openai.OpenAiChatModel;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    import java.util.Map;
    
    @Service
    public class AiService {
        private final ChatClient chatClient;
        private final boolean enabled;
    
        public AiService(OpenAiChatModel chatModel,
                         @Value("${taskflow.ai.enabled:true}") boolean enabled) {
            this.chatClient = ChatClient.create(chatModel);
            this.enabled = enabled;
        }
    
        public String chat(String userMessage) {
            if (!enabled) return "AI is disabled";
    
            // System prompt — задаёт роль
            String systemPrompt = """
                You are a helpful assistant for TaskFlow, a task management service.
                You help users create, organize, and prioritize their tasks.
                Be concise. Use bullet points for lists.
                """;
    
            return chatClient.prompt()
                .system(systemPrompt)
                .user(userMessage)
                .call()
                .content();
        }
    
        // Streaming response
        public Flux streamChat(String userMessage) {
            return chatClient.prompt()
                .user(userMessage)
                .stream()
                .content();
        }
    }
    
  4. 4
    controller/AiController.java
    
    package com.taskflow.controller;
    
    import com.taskflow.dto.ChatRequest;
    import com.taskflow.dto.ChatResponse;
    import com.taskflow.service.AiService;
    import jakarta.validation.Valid;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.*;
    import reactor.core.publisher.Flux;
    
    @RestController
    @RequestMapping("/api/chat")
    public class AiController {
        private final AiService aiService;
    
        public AiController(AiService aiService) { this.aiService = aiService; }
    
        @PostMapping
        public ChatResponse chat(@Valid @RequestBody ChatRequest req) {
            return new ChatResponse(aiService.chat(req.message()));
        }
    
        @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
        public Flux stream(@Valid @RequestBody ChatRequest req) {
            return aiService.streamChat(req.message());
        }
    }
    
  5. 5
    DTO
    
    public record ChatRequest(@NotBlank String message) {}
    public record ChatResponse(String reply) {}
    
  6. 6
    Function calling — AI вызывает наш код
    
    @Bean
    @Description("Get task by ID")
    public Function getTaskFunction(TaskService taskService) {
        return request -> {
            return taskService.findById(request.id())
                .orElseThrow(() -> new RuntimeException("Task not found: " + request.id()));
        };
    }
    
    public record GetTaskRequest(@JsonProperty(required = true) Long id) {}
    

    В AiService:

    
    public String chatWithFunctions(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .functions("getTaskFunction")  // Spring AI найдёт бин
            .call()
            .content();
    }
    

    Теперь LLM может вызвать наш getTaskFunction для ответа на вопрос «какая задача #42?»

  7. 7
    Embeddings (для RAG)
    
    @Service
    public class EmbeddingService {
        private final EmbeddingClient embeddingClient;
    
        public EmbeddingService(EmbeddingClient embeddingClient) {
            this.embeddingClient = embeddingClient;
        }
    
        public float[] embed(String text) {
            return embeddingClient.embed(text);
        }
    }
    

    Хранить embeddings в pgvector (расширение PostgreSQL) или специализированной БД (Pinecone, Weaviate, Milvus).

  8. 8
    RAG (Retrieval Augmented Generation) — простой пример
    
    public String ragChat(String userQuestion) {
        // 1. Embed user question
        float[] questionVec = embeddingClient.embed(userQuestion);
    
        // 2. Find similar tasks in DB (cosine similarity)
        List similarTasks = taskRepository.findSimilar(questionVec, 5);
    
        // 3. Build context from similar tasks
        String context = similarTasks.stream()
            .map(t -> "- " + t.getTitle() + ": " + t.getDescription())
            .collect(Collectors.joining("\n"));
    
        // 4. Ask LLM with context
        String prompt = """
            Based on these tasks:
            %s
    
            Answer the question: %s
            """.formatted(context, userQuestion);
    
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
    
  9. 9
    Тест
    
    # Нужен OPENAI_API_KEY
    export OPENAI_API_KEY=sk-...
    
    $ curl -X POST http://localhost:8080/api/chat \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"message":"What are good practices for task management?"}'
    
    {
      "reply": "Here are 5 best practices for task management:\n1. Set clear priorities..."
    }
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

AI-фичи в продукте — чат-бот, summarization, smart-search, рекомендации.
Spring AI — не вендор-лок, можно менять OpenAI → Anthropic → Ollama одной строчкой.
RAG — главный паттерн для «общения с вашими данными» (вместо fine-tuning).
Function calling — LLM как UI к вашему API, на естественном языке.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Что значит temperature у LLM?

Разбор внутри: детерминированные и более творческие ответы.

2
RAG vs fine-tuning — что выбрать?

Разбор внутри: поиск по документам против дообучения модели.

3
Что такое system prompt?

Разбор внутри: роль, тон и ограничения для LLM-запроса.

4
Что такое prompt injection?

Разбор внутри: пользовательский ввод, который пытается переписать инструкции.

5
Что такое embeddings и vector search?

Разбор внутри: векторное представление текста и поиск похожего смысла.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 30

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 28
🎉
Новый тир
Новый тир достигнут!