RAG i AWS Bedrock – krótkie wprowadzenie
Zanim przejdę do głównego tematu, warto przypomnieć czym jest RAG (Retrieval Augmented Generation) i dlaczego AWS Bedrock jest interesującym narzędziem w tym kontekście.
RAG to podejście, które łączy możliwości generatywne modeli językowych (LLM) z wyszukiwaniem w zewnętrznych źródłach wiedzy. W skrócie – pozwala modelowi „czytać” dokumenty i kod, a następnie generować odpowiedzi na ich podstawie.
AWS Bedrock dostarcza zarządzany pipeline RAG, który automatyzuje kluczowe etapy tego procesu:
- Ekstrakcję dokumentów
- Chunkowanie (dzielenie na mniejsze fragmenty)
- Tworzenie embeddingów (reprezentacji wektorowych)
- Przechowywanie w bazie wektorowej
Bedrock współpracuje z różnymi magazynami wektorów, takimi jak OpenSearch, Amazon Neptune, Pinecone czy Redis, co daje sporą elastyczność.
Głównym celem tego podejścia jest wzbogacenie modeli językowych o wiedzę „z zewnątrz” – czy to firmowe dokumenty, specyfikacje techniczne, czy właśnie kod źródłowy – oraz umożliwienie generowania dobrze udokumentowanych odpowiedzi z odwołaniami do oryginalnych źródeł.
Indeksowanie surowego kodu
Przy pracy z AWS Bedrock, moim pierwszym pomysłem było zaindeksowanie całego repozytorium kodu.
Proces wyglądał następująco:
- Zebrałem wszystkie pliki źródłowe
- Wykorzystałem domyślne ustawienia chunkowania (stały rozmiar tokenów)
- Zastosowałem standardowy model embeddingowy
- Załadowałem fragmenty jako dokumenty do bazy wektorowej
Teoretycznie wszystko powinno działać, prawda? Szybko okazało się, że niekoniecznie.
Główne problemy przy indeksowaniu surowego kodu
1. Utrata „całości” funkcji i klas
Model odpowiadając na pytania wyciąga chunki kodu. Problem w tym przypadku, że w dany chunk nie musi zawierać całej implementacji kodu, co bardzo rzutuje na finalną odpowiedź modelu.
// Chunk 1:
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findUserById(Long id) {
return userRepository.findById(id)
// Chunk 2:
.orElseThrow(() -> new UserNotFoundException(id));
}
public List<User> getAllActiveUsers() {
return userRepository.findByStatus(UserStatus.ACTIVE);
}
}
Widzisz problem? Funkcja findUserById
została przecięta w połowie, co sprawia że kontekst funkcji jest niezrozumiały.
2. Słabe dopasowanie semantyczne
Uniwersalne modele embeddingowe nie są zaprojektowane do rozumienia specyfiki kodu. Nie „wiedzą” nic o:
- Strukturze API
- Obiektach transferu danych (DTO)
- Wzorcach projektowych
- Zależnościach między modułami
W efekcie, zadając pytanie „Jak działa proces weryfikacji płatności?”, możemy otrzymać fragmenty kodu, które zawierają słowo „płatność”, ale niekoniecznie te, które faktycznie implementują proces weryfikacji.
3. Ograniczone okno kontekstowe LLM
Nawet najnowsze modele LLM mają ograniczone okno kontekstowe. Przy dużych repozytoriach nie ma możliwości wstrzyknięcia pełnej logiki do promptu. Model może „zobaczyć” tylko wycinek całego systemu, co prowadzi do niekompletnych lub błędnych odpowiedzi.
4. Brak wysokopoziomowej dokumentacji
RAG zwraca fragmenty kodu, ale nie generuje opisu: co robi metoda, jakie ma parametry, jaka jest logika biznesowa. Użytkownik musi sam analizować kod, co przy skomplikowanych systemach może być bardzo czasochłonne.