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Зависимости
dependencies { implementation("org.springframework.ai:spring-ai-starter-model-openai-spring-boot-starter:1.0.0-M6") }⚠️ Spring AI ещё в milestone — версия может отличаться.
-
2application.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
service/AiService.javapackage 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 FluxstreamChat(String userMessage) { return chatClient.prompt() .user(userMessage) .stream() .content(); } } -
4
controller/AiController.javapackage 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 Fluxstream(@Valid @RequestBody ChatRequest req) { return aiService.streamChat(req.message()); } } -
5DTO
public record ChatRequest(@NotBlank String message) {} public record ChatResponse(String reply) {} -
6Function calling — AI вызывает наш код
@Bean @Description("Get task by ID") public FunctiongetTaskFunction(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?» -
7Embeddings (для 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).
-
8RAG (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) ListsimilarTasks = 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Тест
# Нужен 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..." }---
Зачем это на собесе
После урока ты должен уметь ответить на:
5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: детерминированные и более творческие ответы.
Разбор внутри: поиск по документам против дообучения модели.
Разбор внутри: роль, тон и ограничения для LLM-запроса.
Разбор внутри: пользовательский ввод, который пытается переписать инструкции.
Разбор внутри: векторное представление текста и поиск похожего смысла.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 30Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 28