ASHD Dev_Blog

aiohttp, b64encoding (1)base64.b64encode를 하는 이유

이재룡
이재룡 Oct 28, 2025

결론부터 말씀드리면, 네, 맞습니다. 현재 llm/scoring.py에서 asyncpg를 직접 사용하여 conn.fetch(...)로 데이터를 가져오는 방식 대신, apps/deps.py에 만들어두신 get_vector_store() 함수를 사용하는 것이 훨씬 더 효율적이고 LangChain의 설계 철학에 부합하는 방법입니다.

현재 방식과 개선된 방식을 비교하며 왜 바꿔야 하는지, 그리고 어떻게 바꿀 수 있는지 설명해 드리겠습니다.

현재 llm/scoring.py의 방식과 문제점

  1. 동작 방식:
      • get_score 함수가 호출될 때마다 announcement_embedding 테이블의 모든 청크와 모든 임베딩 벡터를 메모리로 불러옵니다 (SELECT ...).
      • 불러온 전체 데이터를 기반으로 BM25Okapi 모델을 매번 새로 학습시킵니다.
      • HybridScorer는 메모리에 있는 전체 벡터와 쿼리 벡터 간의 코사인 유사도를 처음부터 다시 계산합니다.
  1. 문제점:
      • 비효율적인 메모리 사용: 청크가 수만, 수십만 개로 늘어나면 모든 데이터를 메모리에 올리는 것 자체가 불가능해지거나 시스템에 큰 부담을 줍니다.
      • 느린 속도: 매번 BM25 모델을 새로 학습하고, 전체 벡터에 대해 유사도를 계산하는 것은 매우 느립니다.
      • DB 인덱스 미활용pgvector를 사용하는 가장 큰 이유 중 하나는 HNSW나 IVFFlat 같은 고속 벡터 인덱스를 사용하기 위함입니다. 현재 방식은 이 인덱스를 전혀 활용하지 않고, 단순히 데이터를 저장하는 용도로만 사용하고 있습니다.

개선된 방식: get_vector_store와 Retriever 활용

apps/deps.py의 get_vector_store()는 LangChain의 PGVector 클래스 인스턴스를 반환합니다. 이 객체는 단순한 데이터 저장소가 아니라, 데이터베이스의 벡터 인덱스를 활용하여 효율적인 검색을 수행할 수 있는 강력한 검색기(Retriever) 역할을 합니다.

이 방식을 사용하면 llm/scoring.py의 로직을 다음과 같이 근본적으로 바꿀 수 있습니다.

  1. 벡터 검색은 DB에 위임: 전체 벡터를 메모리에 올리는 대신, retriever.ainvoke(query)를 호출합니다. 그러면 pgvector가 DB 인덱스를 사용해 쿼리와 가장 유사한 상위 K개의 후보 문서만 빠르게 찾아 반환합니다.
  1. BM25는 후보군에만 적용: 전체 코퍼스가 아닌, 벡터 검색으로 추려진 소수의 후보 문서들(예: 상위 100개)에 대해서만 BM25 점수를 계산합니다.
  1. 하이브리드 점수 계산: 이 후보 문서들에 대해서만 하이브리드 점수를 계산하고, 재조정(re-ranking)합니다.

이 접근법을 **"Two-Stage Retrieval" (2단계 검색)**이라고 부르며, 효율성과 정확성을 모두 잡는 표준적인 방법입니다.

코드 수정 제안 (llm/scoring.py)

기존 get_score 함수를 2단계 검색 방식으로 리팩토링하는 것을 제안합니다.

 
 
 
 

안녕하세요! sqlalchemy.exc.MissingGreenlet 오류가 발생하셨군요. 이 오류는 SQLAlchemy가 asyncpg와 같은 비동기 데이터베이스 드라이버를 사용할 때, 동기적인 코드 경로에서 비동기 I/O 작업을 수행하려고 시도할 때 발생하는 문제입니다.

오류의 흐름을 따라가 보면 원인을 명확히 알 수 있습니다.

오류의 원인

  1. llm/scoring.py의 get_score 함수에서 await get_vector_store()를 호출합니다.
  1. apps/deps.py의 get_vector_store 함수는 PGVector(...)를 호출하여 PGVector 객체를 생성합니다.
  1. langchain-postgres의 PGVector 클래스 생성자(__init__)는 동기(synchronous) 함수입니다.
  1. 이 동기 생성자 내부에서, 데이터베이스에 vector 확장 기능이 설치되어 있는지 확인하고 없으면 생성하기 위해 create_vector_extension()이라는 메서드를 호출합니다. 이 과정에서 **데이터베이스 연결(I/O)**이 발생합니다.
  1. 문제는 asyncpg 드라이버가 비동기 전용이므로, 동기 함수 내에서 DB 연결을 시도하면 SQLAlchemy는 greenlet이라는 라이브러리를 사용하여 이 비동기 작업을 "동기처럼" 처리하려고 합니다.
  1. 오류 메시지 greenlet_spawn has not been called는 greenlet 라이브러리가 설치되어 있지 않거나, 설치되었더라도 현재 컨텍스트에서 사용할 수 없음을 의미합니다.

결론적으로, PGVector의 동기적인 생성자 안에서 비동기 I/O가 발생하면서 greenlet이 필요하게 되었고, 이 의존성이 충족되지 않아 오류가 발생한 것입니다.

해결 방법

이 문제를 해결하는 가장 확실한 방법은 SQLAlchemy의 비동기 지원에 필수적인 greenlet 라이브러리를 설치하는 것입니다.

1단계: requirements.txt 파일에 greenlet 추가

requirements.txt 파일에 greenlet을 추가하여, pip가 이 패키지를 설치하도록 명시해야 합니다.

2단계: 패키지 재설치

터미널에서 아래 명령어를 실행하여 greenlet을 포함한 패키지들을 다시 설치합니다.

이 간단한 수정으로 SQLAlchemy가 asyncpg 드라이버와 함께 비동기 작업을 올바르게 처리할 수 있게 되어, PGVector 객체가 정상적으로 생성되고 오류가 해결될 것입니다.

 
 
 
 

오류의 근본 원인

  1. apps/deps.py의 get_vector_store 함수에서 PGVector 객체를 생성할 때, connection 인자로 postgresql+psycopg://...와 같은 **연결 문자열(string)**을 전달했습니다.
  1. langchain-postgres는 연결 문자열을 받으면, 기본적으로 동기(synchronous)용 SQLAlchemy 엔진을 생성합니다.
  1. 이후 llm/scoring.py에서 retriever.ainvoke(user_query)와 같은 비동기(asynchronous) 함수를 호출합니다.
  1. 이 비동기 함수는 내부적으로 비동기 작업을 위한 **비동기 엔진(_async_engine)**을 찾으려고 시도합니다.
  1. 하지만 PGVector 객체는 동기 엔진만 가지고 있고 비동기 엔진은 생성되지 않았기 때문에, AssertionError: _async_engine not found 오류가 발생하며 프로그램이 중단됩니다.

해결 방법: 비동기 엔진(AsyncEngine)을 직접 생성하여 전달

이 문제를 해결하는 가장 확실한 방법은, PGVector에 연결 문자열을 직접 전달하는 대신, SQLAlchemy의 create_async_engine을 사용하여 비동기 엔진을 명시적으로 만들어 전달하는 것입니다.

아래와 같이 c:\Users\ashd\OneDrive - UOS\바탕 화면\notice_rag\apps\deps.py 파일을 수정하여 이 문제를 해결할 수 있습니다.

수정 내용 설명

  1. create_async_engine 임포트: SQLAlchemy에서 비동기 엔진을 생성하는 함수를 가져옵니다.
  1. asyncio.to_thread 제거: 더 이상 동기 함수를 별도 스레드에서 실행할 필요가 없으므로 asyncio.to_thread 호출을 제거했습니다.
  1. 비동기 엔진 생성create_async_engine(connection_string)을 호출하여 psycopg 드라이버를 사용하는 비동기 데이터베이스 엔진을 명시적으로 생성합니다.
  1. PGVector.from_engine 사용PGVector를 직접 생성하는 대신, PGVector.from_engine() 클래스 메서드를 사용합니다. 이 메서드는 비동기 엔진을 인자로 받아, 비동기 작업에 완벽하게 호환되는 PGVector 인스턴스를 생성합니다.

이 수정으로 PGVector 객체는 처음부터 비동기 엔진을 갖게 되어, asimilarity_search와 같은 비동기 함수를 호출할 때 _async_engine을 정상적으로 찾을 수 있습니다. 따라서 AssertionError가 해결되고 코드가 원활하게 실행될 것입니다.

 
 
 
 
 
 
 

추천 글

BlogPro logo