- BrainTools - https://www.braintools.ru -
Если дедлайн плавающий или его нет, обучение [1] и пет-проекты превращаются в вечный “черновик”: сегодня читаешь доки, завтра переписываешь пример, послезавтра думаешь про идеальную архитектуру. Это нормальный творческий процесс – пока не заметишь, что за месяц у тебя так и нет ничего, что можно запустить и показать.
Когда я проходил AI Advent Challenge [2] этот режим прокрастинации сломался: 28 дней подряд у тебя есть ровно сутки. В 10:00 приходит задание, а в 10:00 следующего дня – дедлайн. Поэтому каждый день заканчивается одной из двух вещей: либо у тебя есть работающий кусок, либо ты точно понимаешь, где решение не выдержало и почему.
Разработка шла в стиле “vibe coding”: главный артефакт – backlog “что сделать”, минимум upfront-дизайна, максимум итераций и постоянная проверка, что базовая работоспособность не сломалась.
Самое принципиальное решение было не про “какую модель взять”, а про стек. Вместо привычного “Python + что-то вокруг LLM” я остался в своем прод-стеке: Java + Spring Boot, а AI-часть собрал на Spring AI [3]. Цель была прикладная: понять, насколько LLM-интеграции нормально живут в Java-стеке, если делать не “демо-чатик”, а инструмент, который реально помогает в задачах и умеет:
sync / stream / structured режимы,
“память” и управление контекстом,
tool calling с ограничениями,
RAG по коду с цитатами,
sandbox для тестов и команд,
интеграции с GitHub/workspace,
немного orchestration поверх этого.
Началось все с базового чата. Но по мере того как задания усложнялись, получилась связка “retrieval + инструменты + окружение” – когда модель не просто отвечает, а помогает искать по коду, проверять гипотезы и воспроизводимо выполнять сценарии вокруг проекта.
Spring AI хорошо ускоряет старт, но важно понимать границу: он закрывает слой интеграции, а “прод-поведение” почти всегда рождается из вашей инженерной обвязки.
ChatClient [4] – единый API к моделям: prompt -> call()/stream() -> response.
Advisors [5] – прослойки вокруг вызова модели (как middleware).
Chat Memory [6] – хранение истории/фактов вне модели и подмешивание их в запрос по conversationId.
Tools [7] – tool calling / function calling внутри приложения.
RAG [8] – базовые паттерны retrieval + интеграции для vector store.
Observability [9] – точки интеграции в метрики/трейсы вокруг вызовов.
Token budget + preflight: оценить заранее “влезает ли запрос” и что резать, если не влезает.
Контекст-менеджмент: summarization/pruning/pinned facts + лимиты на tool outputs и RAG-контент.
Безопасность инструментов: allow-list, таймауты, лимиты ресурсов, dry-run.
RAG как пайплайн: multi-query, дедуп, пост-обработка + цитаты.
Sandbox для команд/тестов: изоляция, ограничения, cleanup.
Два коротких “продовых” примера, почему это нужно:
Стриминг может оборваться на середине – а вы все равно должны корректно закрыть соединение, не оставить подписки и сохранить “что успели сгенерировать”.
Инструмент может вернуть огромный вывод (логи/дифф/тесты) – и если не ограничить размер, он “съест” весь контекст и ухудшит качество следующего шага.
Ниже – минимум, чтобы воспроизвести проект локально и пройти сценарии из статьи.
Что потребуется:
ключ/доступ к LLM-провайдеру (или локальная модель),
Docker,
базовое понимание Spring Boot,
GitHub token (если хотите сценарии интеграции с GitHub),
дополнительные интеграции (граф, голос и т.п.) – их можно не включать.
В реальных системах почти всегда нужны три сценария:
Интерактивный чат – нужен streaming (UX), устойчивость к разрывам, понятная деградация.
Сервисные вызовы – удобнее sync: проще таймауты/ретраи/тестирование.
Structured output – нужен контракт, валидация и предсказуемая обработка ошибок.
Несколько моделей – под разные задачи (скорость/стоимость/контекст), и необходимость тюнить параметры запроса на лету – без релиза (например, overrides на temperature/top_p/max_tokens под конкретный запрос).
ChatClient [4] – единый интерфейс, где и sync, и stream выглядят одинаково по стилю.
Развести режимы по контракту API: streaming-чат != sync-операция != structured вызов.
Устойчивый lifecycle стрима:
корректно закрывать SSE,
не оставлять подписки,
обрабатывать таймауты/ошибки.
Делать preflight до открытия стрима (чтобы не начинать SSE, если запрос заведомо “не влезет”).
Пробрасывать conversationId везде, где есть сессия/память.
Полный файл: ChatStreamController.java [11]
preflightManager.run(context.sessionId(), selection, sanitizedMessage, "stream-chat");
SseEmitter emitter = new SseEmitter(0L);
var promptSpec = chatProviderService.chatClient(selection.providerId()).prompt();
promptSpec =
promptSpec
.user(sanitizedMessage)
.advisors(advisors ->
advisors.param(ChatMemory.CONVERSATION_ID, context.sessionId().toString()));
if (researchContext.hasCallbacks()) {
promptSpec = promptSpec.toolCallbacks(researchContext.callbacks());
}
Flux<ChatResponse> responseFlux =
promptSpec.options(chatOptions).stream().chatResponse();
Очень быстро выясняется, что обычный “вызов модели” – не самый сложный кусок. Интересное начинается вокруг вызовов:
подключить память [12],
добавить RAG,
подмешать системные инструкции/политики,
нормализовать и ограничить контекст,
включить наблюдаемость.
Если размазать это по контроллерам/сервисам – получится код, который сложно расширять и еще сложнее поддерживать.
Advisors [5] – middleware-слои вокруг вызова модели, подключаемые через .advisors(...).
Практически полезная мысль: advisors – это место, где удобно держать “стандартные” части поведения [13], например:
memory-подмешивание,
retrieval-подмешивание,
trimming/budget-политики,
safety/policy-правила,
observability-обвязку.
Политики: что добавлять в контекст всегда, что – только для отдельных режимов, сколько токенов выделять под память/tools/RAG.
Ограничение размеров tool outputs и retrieval документов (иначе они “съедают” окно).
Вокруг advisor-цепочки – сервисные “рамки”: preflight, лимиты, allow-list tools.
Ключевой “якорь”, с которого все начинает работать согласованно – привязка conversationId:
Именно там видно: .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, ...)), после чего память и любые другие advisors начинают работать по одному и тому же ключу.
У LLM нет памяти в привычном смысле: модель не хранит состояние между запросами и отвечает только на то, что вы дали ей в контекст конкретного вызова. В Spring AI это прямо отражено в концепции “внешней памяти”: Chat Memory [6].
Отсюда вытекает ключевая инженерная мысль:
Каждый запрос к модели – это сборка контекста “заново”: system prompt + сообщение пользователя + история (или ее сжатая версия) + RAG-фрагменты + результаты инструментов + правила/ограничения.
Когда кто-то говорит “у нас есть память в llm”, обычно это означает: мы где-то храним историю/факты и каждый раз решаем, какую часть вернуть в prompt.
Контекстное окно у модели ограничено, а в запрос “лезут”:
история диалога,
system prompt,
tool schemas и tool outputs,
RAG-документы,
сама задача пользователя.
Что может излишне “раздуть” контекст после чего модель может начать теряться в переданных токенах.
ChatMemory [6] и ChatMemoryRepository + типовые стратегии вроде “окна сообщений”.
Token budget + preflight: оценка токенов и решение “что резать/сжимать” до вызова модели.
Summarization истории, но контролируемо (очередь + backpressure + деградации).
Хранение памяти в БД, чтобы переживать рестарты и масштабирование.
Паттерн “summary + tail”: краткая сводка + хвост последних сообщений.
Полезная практическая стратегия деградации (в общих чертах):
сначала ограничивать tool outputs -> потом ограничивать RAG -> потом сжимать историю -> и только потом “не выполняем запрос”.
Полный файл: DefaultTokenUsageEstimator.java [16]
TokenComputation promptComputation =
computeTokenCount(encoding, tokenizerName, PROMPT_SEGMENT, request.prompt());
TokenComputation completionComputation =
computeTokenCount(encoding, tokenizerName, COMPLETION_SEGMENT, request.completion());
int promptTokens = promptComputation.tokens();
int completionTokens = completionComputation.tokens();
int totalTokens = promptTokens + completionTokens;
return new Estimate(
promptTokens,
completionTokens,
totalTokens,
promptComputation.cacheHit(),
completionComputation.cacheHit());
Полный файл: ChatMemorySummarizerService.java [17]
if (!enqueueSummarization(result)) {
log.warn("Summarisation queue is full (session={}), skipping request", result.sessionId());
recordFailure(result.sessionId(), "chat", "Summarisation queue saturated");
}
Полный файл: DatabaseChatMemoryRepository.java [18]
int summarizedOrder =
summaries.stream().mapToInt(SummaryRow::sourceEndOrder).max().orElse(0);
// summaries -> вперед
for (SummaryRow summary : summaries) {
result.add(summary.asMessage());
}
// tail -> после, без дублей
storedMessages.stream()
.filter(entry -> entry.messageOrder() > summarizedOrder)
.map(StoredMessage::message)
.forEach(result::add);
Structured output – это подход, где вы просите модель вернуть ответ в строго заданном формате (чаще всего JSON), чтобы затем:
распарсить в DTO,
провалидировать,
и безопасно встроить в бизнес-логику (пайплайны, оркестрации, автоматические решения).
классификация (например, “определи тип запроса”),
извлечение структурированных данных из текста (сущности, поля формы),
построение плана действий (список шагов, параметры инструмента),
генерация настроек/конфига,
машиночитаемые промежуточные результаты между шагами оркестрации.
Structured Output Converter [19] и связанные механики.
У крупных провайдеров есть механики, которые помогают добиваться более предсказуемого структурированного формата, но это не отменяет необходимости валидации на вашей стороне:
OpenAI: Structured Outputs [20]
Anthropic: Structured Outputs [21]
Даже при “строгих” подсказках/схемах в реальности могут появлятся типовые сбои:
“почти JSON” (лишний текст до/после),
сломанные кавычки/экранирование,
несоответствие типов,
недостающие обязательные поля или “лишние” поля,
частичный ответ из-за лимита токенов,
сложности со streaming-парсингом, если пытаться разбирать JSON “на лету”.
Поэтому structured output в проде – это контролируемая, но все равно best-effort интеграция, где валидация и fallback – обязательны.
Отдельный плюс structured-подхода: он хорошо тестируется. Можно делать контрактные/”golden”-тесты на JSON и на DTO-валидацию, а не пытаться тестировать “красоту текста”.
Полный файл: StructuredSyncService.java [22]
preflightManager.run(conversation.sessionId(), selection, userPrompt, "structured-sync");
ChatResponse response = prompt.options(options).call().chatResponse();
String content = extractContent(response);
if (!StringUtils.hasText(content)) {
throw new SchemaValidationException("Model returned empty response for structured sync");
}
StructuredSyncResponse payload = convert(content);
Если упростить до одной фразы: LLM хороша в рассуждении и планировании, но “действия” она совершает через инструменты.
Tool calling (оно же function calling / tool use) – де-факто стандартный способ “подключить внешний мир” к модели:
OpenAI: Function calling [23]
Anthropic: Tool use [24]
На практике это означает: поиск по данным, вызовы внутренних сервисов, работа с репозиторием, запуск тестов – все это оформляется как инструменты с четким контрактом.
Tools [7]: ToolCallback, @Tool-аннотации и регистрация tool’ов.
В системе tools – это не “магия модели”, а API-контракты + контроль исполнения:
allow-list инструментов под режим/запрос,
классификация по риску: read / write / execute / external,
таймауты и лимиты на все (включая внешние API),
“dry-run first” для любых операций, меняющих состояние,
ограничение размера результатов инструментов (чтобы не “съесть” контекст),
предсказуемые ошибки [25] “почему нельзя” вместо непойманных исключений.
Полный файл: GitHubTools.java [26]
@Tool(
name = "github.list_pull_requests",
description = "Список PR с фильтрами, лимитами и пагинацией. Возвращает truncated=true если обрезано.")
GitHubPullRequestsResponse listPullRequests(GitHubListPullRequestsRequest request) {
// ...
}
Полный файл: CodingTools.java [27]
@Tool(
name = "coding.apply_patch_preview",
description = "Preview патча (dryRun) + опциональный запуск тестов. Таймаут в ISO-8601.")
ApplyPatchPreviewResponse applyPatchPreview(ApplyPatchPreviewRequest request) {
return codingAssistantService.applyPatchPreview(request);
}
Как только у модели появляются инструменты уровня “выполни команду” / “запусти тесты”, появляется новый класс рисков:
таймауты,
ресурсы (CPU/RAM/disk),
изоляция workspace,
очистка после выполнения,
безопасность параметров и команд.
И тут важен практический момент:
Любой MCP-инструмент, который работает с живым окружением (git, docker, CI, внешние API), надо проектировать как инфраструктурный компонент со строгими ограничениями.
Spring AI не “сделает sandbox за вас” – исполнение остается зоной ответственности приложения.
Docker runner с профилями (gradle/maven/npm/…),
таймауты и лимиты ресурсов,
ограничения команд (лучше allow-list/шаблоны, чем произвольные строки),
изоляция и cleanup.
Полный файл: DockerRunnerService.java [28]
if (Files.exists(projectAbsolute.resolve("pom.xml"))) return RunnerProfile.MAVEN;
if (Files.exists(projectAbsolute.resolve("package.json"))) return RunnerProfile.NPM;
RAG (Retrieval-Augmented Generation) – подход, где вы не надеетесь на “память модели”, а подключаете к ответу вашу базу знаний:
заранее индексируете источники (код/доки/вики),
на запрос находите релевантные фрагменты (retrieval),
добавляете их в контекст,
модель отвечает с опорой на реальные куски данных.
Это дает:
меньше галлюцинаций по вашему проекту,
возможность отвечать “по факту кода”, а не “как обычно принято”,
проверяемость через цитаты (file_path/чанк).
Spring AI описывает RAG как паттерн: RAG [8].
Если данных много (репозитории, монорепы, доки), “просто закинуть в vector store” быстро не получится. Почти всегда нужна обертка в виде ETL:
Extract: собрать источники, обновления, диффы,
Transform: декодирование, фильтры, чанкинг, метаданные, дедуп,
Load: embeddings -> vector store, контроль версий, пересборка.
В Spring AI это выделено отдельным понятием: ETL Pipeline [29].
Индексация с защитой от мусора: skip binary, skip unchanged.
Retrieval как пайплайн:
multi-query,
дедуп,
topK после merge,
пост-обработка/реранкинг.
“Цитаты” (file_path/чанк) как обязательный формат.
Бюджеты: ограничивать число документов и размер контента в prompt.
Набор “отладочных сигналов”: что реально попало в контекст, сколько, почему.
Полный файл: RepoRagIndexService.java [30]
if (isBinaryFile(file)) {
appendWarning(warnings, "Skipped binary file " + relativePath);
return FileVisitResult.CONTINUE;
}
byte[] rawBytes = Files.readAllBytes(file);
String fileHash = hashBytes(rawBytes);
RepoRagFileStateEntity existingState = stateByPath.get(relativePath);
if (existingState != null && fileHash.equals(existingState.getFileHash())) {
filesSkipped.incrementAndGet();
return FileVisitResult.CONTINUE;
}
Полный файл: RepoRagRetrievalPipeline.java [31]
List<Query> queries = expandQueries(transformedQuery, input, appliedModules);
Map<String, AggregatedDocument> dedup = new LinkedHashMap<>();
List<QueryRetrievalResult> retrievalResults = retrieveAll(queries, input);
for (QueryRetrievalResult result : retrievalResults) {
for (Document document : result.documents()) {
accumulateDocument(dedup, document, result.query(), result.index());
}
}
List<Document> merged =
dedup.values().stream().map(AggregatedDocument::toDocument).toList();
List<Document> top =
merged.size() > input.topK() ? merged.subList(0, input.topK()) : merged;
Полный файл: RepoRagGenerationService.java [32]
int limit = Math.min(documents.size(), 5);
for (int i = 0; i < limit; i++) {
Document document = documents.get(i);
Map<String, Object> metadata = document.getMetadata();
String path = metadata != null ? (String) metadata.getOrDefault("file_path", "") : "";
builder.append(i + 1).append(". ").append(path).append("n");
builder.append(document.getText()).append("nn");
}
LLM-часть легко превратить в “черный ящик”, который сожрет весь ваш бюджет, если не мерить:
latency по режимам (stream/sync/structured),
ошибки (где именно: модель, tools, retrieval, sandbox),
токены/стоимость.
Spring AI поддерживает интеграцию с наблюдаемостью через Observability [9].
latency p50/p95 по режимам,
tokens in/out (и “fallback-оценка”, если провайдер не отдает usage),
tool success/error rate + latency,
retrieval empty rate,
cost estimate.
Идея – не просто “почитать про подходы”, а включить инструменты, дать ассистенту репозиторий и прогнать реальные сценарии: анализ кода, поиск, проверка сборки/тестов, объяснение архитектуры, ревью изменений.
Поднимите проект по README (backend + backend-mcp + frontend).
Убедитесь, что настроены ключи, которые нужны сценарию (LLM-провайдер, GitHub).
Во фронтенде включите доступные MCP-инструменты (в проекте есть каталог инструментов, который UI использует, чтобы показать “что доступно”).
Скачай репозиторий https://github.com/GrinRus/ai_advent_challenge в workspace.
Дальше:
1) Коротко объясни, что делает проект и из каких компонентов он состоит.
2) Опиши, как связаны backend, backend-mcp и frontend.
3) Если информации не хватает - используй поиск по коду и приводи цитаты (пути файлов + фрагменты).
Формат ответа: сначала summary на 5-7 строк, затем список "компонент -> роль".
Найди все gradle подпроекты (settings.gradle/settings.gradle.kts) и перечисли их.
Затем запусти гредл тесты
Если есть падения:
- перечисли упавшие тесты
- покажи ключевые строки стека
- предположи 2-3 причины
- предложи варианты исправления (без применения изменений)
Найди в проекте, где реализован preflight и оценка токенов.
Дай ссылки на файлы и коротко опиши, как устроен процесс и какие решения принимаются (что режется/сжимается).
Ответь на вопрос: "Где в проекте реализованы инструменты GitHub и sandbox?"
Требования:
- приведи цитаты с путями файлов
- покажи по 1-2 ключевых фрагмента кода (коротко)
- если результатов слишком много - объясни, как ты ограничивал поиск/дедуп/выбор topK
Если GitHub-интеграция доступна:
1) Выведи список открытых PR (или последних PR, если открытых нет).
2) Выбери один PR и сделай ревью:
- что меняется
- потенциальные риски
- что стоит протестировать
- что можно упростить
Формат: краткое резюме + список замечаний.
Самый полезный эффект оказался не в “количестве фич”, а в том, что ежедневная работа с LLM быстро калибрует ожидания и вырабатывает понимание что это такое и с чем его едят.
За этот месяц я намного четче понял:
где LLM реально ускоряет работу (обзор кода, сводки, поиск связей, генерация черновиков, классификация/роутинг);
где она стабильно ошибается без “обвязки” (контекст раздувается, structured “плывет”, инструменты без лимитов начинают вести себя непредсказуемо);
что качество ответа часто определяется не “умностью модели”, а качеством контекста (RAG, цитаты, лимиты, preflight);
что “инструменты” – не дополнение, а центр практического применения: модель рассуждает, а действия делает через tool-слой;
что без наблюдаемости и дисциплины логов вы слишком поздно узнаете, где именно “дорого”, “долго” и “ломается”.
LLM – это не “магический мозг [33], который все помнит и все знает”, а скорее это еще один инструмент в стеке, но со своими особенностями: модель статична, контекст ограничен, ошибки выглядят непривычно, а “интеллект” проявляется не всегда как ты это ожидаешь.
Поэтому рабочая интеграция выглядит не как “мы прикрутили чат”, а как полноценная инженерная система, где LLM играет роль оркестратора:
RAG дает модели факты из ваших данных, а не “догадки”,
tools/MCP дают модели действия (прочитать код, запустить тесты, сходить в GitHub),
контекст-менеджмент делает все это возможным в рамках ограниченного окна,
sandbox и ограничения делают действия безопасными и воспроизводимыми,
observability объясняет, что происходит и сколько это стоит.
А дальше уже вы выбираете режим работы процесса:
human-in-the-loop, когда важны контроль и подтверждения,
human-out-of-the-loop, когда готовы автоматизировать целиком отдельные шаги.
Если относиться к LLM именно так – как к компоненту, вокруг которого нужна дисциплина, ограничения и измеримость – тогда “AI-часть” начинает жить в Java/Spring так же естественно, как любые другие части Spring зоопарка.
Автор: GrinRus
Источник [35]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/25764
URLs in this post:
[1] обучение: http://www.braintools.ru/article/5125
[2] AI Advent Challenge: https://mobiledeveloper.tech/ai_advent_challenge
[3] Spring AI: https://docs.spring.io/spring-ai/reference/
[4] ChatClient: https://docs.spring.io/spring-ai/reference/api/chatclient.html
[5] Advisors: https://docs.spring.io/spring-ai/reference/api/advisors.html
[6] Chat Memory: https://docs.spring.io/spring-ai/reference/api/chat-memory.html
[7] Tools: https://docs.spring.io/spring-ai/reference/api/tools.html
[8] RAG: https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html
[9] Observability: https://docs.spring.io/spring-ai/reference/observability/index.html
[10] Image: https://sourcecraft.dev/
[11] ChatStreamController.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/controller/ChatStreamController.java#L95
[12] память: http://www.braintools.ru/article/4140
[13] поведения: http://www.braintools.ru/article/9372
[14] ChatStreamController.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/controller/ChatStreamController.java#L116
[15] StructuredSyncService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/service/StructuredSyncService.java#L165
[16] DefaultTokenUsageEstimator.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/token/DefaultTokenUsageEstimator.java#L36
[17] ChatMemorySummarizerService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/memory/ChatMemorySummarizerService.java#L338
[18] DatabaseChatMemoryRepository.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/memory/DatabaseChatMemoryRepository.java#L100
[19] Structured Output Converter: https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html
[20] Structured Outputs: https://platform.openai.com/docs/guides/structured-outputs
[21] Structured Outputs: https://platform.claude.com/docs/en/build-with-claude/structured-outputs
[22] StructuredSyncService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend/src/main/java/com/aiadvent/backend/chat/service/StructuredSyncService.java#L155
[23] Function calling: https://platform.openai.com/docs/guides/function-calling
[24] Tool use: https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
[25] ошибки: http://www.braintools.ru/article/4192
[26] GitHubTools.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/github/GitHubTools.java#L108
[27] CodingTools.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/coding/CodingTools.java#L87
[28] DockerRunnerService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/docker/DockerRunnerService.java#L178
[29] ETL Pipeline: https://docs.spring.io/spring-ai/reference/api/etl-pipeline.html
[30] RepoRagIndexService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/github/rag/RepoRagIndexService.java#L158
[31] RepoRagRetrievalPipeline.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/github/rag/RepoRagRetrievalPipeline.java#L78
[32] RepoRagGenerationService.java: https://github.com/GrinRus/ai_advent_challenge/blob/main/backend-mcp/src/main/java/com/aiadvent/mcp/backend/github/rag/RepoRagGenerationService.java#L101
[33] мозг: http://www.braintools.ru/parts-of-the-brain
[34] GrinRus/ai_advent_challenge: https://github.com/GrinRus/ai_advent_challenge
[35] Источник: https://habr.com/ru/articles/979950/?utm_source=habrahabr&utm_medium=rss&utm_campaign=979950
Нажмите здесь для печати.