Chapter 86
🤖 벡터 검색 에이전트: 허깅페이스 허브를 백엔드로 하는 지능형 검색 엔진
🤖 벡터 검색 에이전트: 허깅페이스 허브를 백엔드로 하는 지능형 검색 엔진
_참조: Martin Elstner _작성자: 안정
검색 엔진은 크게 키워드 검색과 벡터 검색으로 나눌 수 있습니다. 키워드 검색과 달리, 벡터 검색을 사용할 때는 두 가지를 고려해야 합니다.
- 적합한 임베딩 모델로 데이터셋과 쿼리를 임베딩하는 작업
- 임베딩 데이터를 처리할 수 있는 DB
하지만 임베딩 값을 기반으로 한 벡터 검색만으로는 ‘사용자가 원하는 답변’을 보장하기 어렵습니다.
그래서 검색 과정에서 에이전트가 자율적으로 판단하고 최적화한다면, 사용자 의도에 더 가까운 검색 결과를 얻을 수 있습니다.
Agentic 접근 방식의 차별점
기존 벡터 검색 워크플로우
데이터 ➡ 데이터 임베딩(고정 모델) ➡ 인덱스 생성 ➡ 사용자 질의 ➡ 유사도 검색 ➡ 답변똑똑한 Agentic 방식
사용자 질의 분석(검색 전략 세움) ➡ 최적 임베딩 모델 선택 ➡ 데이터 임베딩 ➡ 인덱스 생성 ➡ 유사도 검색 ➡ 검색결과 바탕으로 답변 정제DuckDB?
허깅페이스의 데이터셋은 파켓(parquet) 파일을 기반으로 동작하는데, 이때 빠른 인메모리 데이터베이스 시스템인 DuckDB를 사용하면 이 파일들과 상호작용할 수 있습니다. 또한 DuckDB의 기능 중 하나는 벡터 유사도 검색으로, 인덱스 유무에 관계없이 사용할 수 있습니다.
이번 노트북에서는 단일 Agent에 여러가지 도구를 주어 수행하는 간단한 Agentic 벡터 검색 엔진을 만들어 보겠습니다.
필요한 의존성을 설치합니다 :
# 본 예제 파일은 Python 3.10 이상 버전에서만 실행할 수 있습니다.
!pip install -U smolagents datasets sentence-transformers duckdb openaiHuggingFace의 추론 API를 사용하기 위해 로그인합니다 :
from huggingface_hub import notebook_login
notebook_login()from smolagents import tool
from datasets import Dataset
import os
# 도구 사용을 위해 OPENAI 키를 발급 받아야합니다.
os.environ["OPENAI_API_KEY"] = "YOUR KEY"도구 정의
정의할 도구는 아래와 같습니다.
- 임베딩 생성 도구
- 인덱스 생성 도구
- 유사도 검색 도구
- 답변 생성 도구
도구1 : 임베딩 생성
일반적으로, 임베딩 작업에서는 작은 배치 사이즈로 데이터를 청킹하지만 여기서는 단순히 데이터셋을 임베딩으로 변환하는 과정만 수행하겠습니다.
@tool
def create_embeddings(
dataset: Dataset,
model_id: str,
column_name: str,
) -> Dataset:
"""
주어진 데이터셋에 대해 임베딩을 생성합니다.
Args:
dataset: 임베딩을 생성할 대상 데이터셋
model_id: 임베딩에 사용할 모델
column_name: 임베딩할 열 이름
Returns:
임베딩이 추가된 데이터셋
"""
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(model_id)
def embed_batch(batch):
embeddings = model.encode(batch[column_name], convert_to_numpy=True)
batch["embeddings"] = embeddings.tolist()
return batch
dataset = dataset.map(embed_batch, batched=True)
return datasetDuckDB로 벡터 검색 수행하기
duckdb를 사용하여 데이터셋에서 벡터 검색을 수행할 수 있습니다.
인덱스는 사용하거나 사용하지 않을 수 있습니다. 인덱스를 활용하지 않고 검색하는 것은 더 느리지만 더 정확하고, 인덱스를 활용하여 검색하는 것은 더 빠르지만 덜 정확합니다.
인덱스를 활용하지 않고 검색하기
인덱스를 활용하지 않고 검색하는 것은 느린 작업지만 일반적으로 약 10만 행까지의 작은 데이터셋에서는 충분히 빠르게 동작합니다. 하지만 이번 노트북에서는 DuckDB의 인덱스를 활용해서 검색해보겠습니다.
인덱스를 활용하여 검색하기
이 접근법은 데이터셋의 로컬 복사본을 생성하고 이를 사용하여 인덱스를 생성합니다. 약간의 오버헤드가 있지만 한번 인덱스를 생성한 후에는 검색 속도가 개선될 것입니다.
도구2 : DuckDB 인덱스 만들기
@tool
def create_db_index(
dataset_with_embeddings: Dataset, # 임베딩이 포함된 데이터셋
table_name: str,
embedding_column: str = "embeddings"
) -> None:
"""
임베딩이 포함된 데이터셋에 대해 DuckDB 인덱스를 생성합니다.
Args:
dataset_with_embeddings: 이미 임베딩이 포함된 데이터셋
table_name: 생성할 테이블 이름
embedding_column: 임베딩 열 이름
Returns:
None
"""
import duckdb
# VSS 확장 설치 및 로드
duckdb.sql("INSTALL vss; LOAD vss;")
duckdb.sql(f"DROP TABLE IF EXISTS {table_name};")
# 데이터셋을 pandas DataFrame으로 변환
df = dataset_with_embeddings.to_pandas()
# DuckDB에 DataFrame 등록
duckdb.register(f"{table_name}_temp", df)
# 모델에서 임베딩 차원 가져오기
embedding_dim = len(df[embedding_column].iloc[0])
# embedding_dim = model.get_sentence_embedding_dimension()
# 테이블 생성 (임베딩을 FLOAT 배열로 변환)
duckdb.sql(f"""
CREATE TABLE {table_name} AS
SELECT *, {embedding_column}::FLOAT[{embedding_dim}] AS {embedding_column}_float
FROM {table_name}_temp;
""")
# HNSW 인덱스 생성
duckdb.sql(f"""
CREATE INDEX idx_{embedding_column} ON {table_name}
USING HNSW ({embedding_column}_float) WITH (metric = 'cosine');
""")
# 임시 테이블 정리
duckdb.sql(f"DROP VIEW IF EXISTS {table_name}_temp;")이 도구를 통해 인덱스를 사용하여 벡터 검색을 수행할 수 있으며, 결과는 즉시 반환됩니다.
도구3 : 벡터 검색 수행하기
@tool
def similarity_search_with_duckdb_index(
query: str,
table_name: str,
model_id: str,
k: int = 5,
embedding_column: str = "embeddings"
)-> dict:
"""
DuckDB 인덱스를 이용해 벡터 검색을 수행합니다.
Args:
query: 검색할 쿼리 문자열
model_id: 임베딩에 사용할 모델
k: 반환할 결과 수
table_name: 검색할 테이블 이름
embedding_column: 임베딩 컬럼 이름
Returns:
dict: 검색 결과
"""
from sentence_transformers import SentenceTransformer
import duckdb
model = SentenceTransformer(model_id)
embedding = model.encode(query).tolist()
return duckdb.sql(
query=f"""
SELECT *, array_cosine_distance({embedding_column}_float, {embedding}::FLOAT[{model.get_sentence_embedding_dimension()}]) as distance
FROM {table_name}
ORDER BY distance
LIMIT {k};
"""
).to_df()무거운 벡터 검색 엔진을 따로 배포할 필요 없고, 저장소는 허브에서 처리됩니다.
도구4 : 답변 생성 도구
유사도 검색 결과 청크를 기반으로, LLM이 사용자가 원할 만한 답변을 생성합니다.
@tool
def generate_answer(chunks: list, query: str) -> str:
"""
쿼리를 기반으로 주어진 텍스트 청크 목록에서 답변을 생성합니다.
Args:
chunks: 답변 생성에 사용할 텍스트 청크 목록
query: 답변할 쿼리 문자열
Returns:
str: 생성된 답변
"""
import openai # OPENAI 키 발급이 필요합니다.
context = "\n\n".join(chunks)
prompt = f"Context:\n{context}\n\nQuestion: {query}\nAnswer:"
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
return response["choices"][0]["message"]["content"]도구를 정의하였으니, 데이터셋을 로드하고 에이전트를 동작해보겠습니다.
사용한 데이터셋은 huggingface-KREW/KoCultre-Descriptions으로, 한국어 밈(meme)에 대한 데이터입니다.
from datasets import load_dataset
dataset = load_dataset("huggingface-KREW/KoCultre-Descriptions")에이전트를 정의합니다. 사용할 모델은 Qwen/Qwen2.5-Coder-32B-Instruct 입니다.
from smolagents import CodeAgent, InferenceClientModel
model = InferenceClientModel(
"Qwen/Qwen2.5-Coder-32B-Instruct",
provider="together",
max_tokens=2048
)
tools = [
create_embeddings, # 임베딩 생성 도구
create_db_index, # 인덱스 생성 도구
similarity_search_with_duckdb_index, # 유사도 검색 도구
generate_answer # 답변 생성 도구
]
agent = CodeAgent(
model=model,
tools=tools,
verbosity_level=2,
max_steps=15
)에이전트가 자율적으로 임베딩 모델을 택할 수 있도록 프롬프트를 구성하겠습니다.
def agentic_prompt(query: str):
return f"""
당신은 지능형 검색 전문가입니다. {query}:
검색을 위해 먼저 쿼리와 데이터셋을 분석하고, 분석 결과에 따라 가장 적합한 임베딩 모델을 선택하세요
- sentence-transformers/all-MiniLM-L6-v2 (fast, lightweight, good for simple queries)
- sentence-transformers/all-mpnet-base-v2 (balanced performance)
- intfloat/e5-large-v2 (high quality for complex tasks)
- minishlab/potion-base-8M (very efficient)
"""데이터셋에 대한 설명과, 검색어를 입력하고 검색엔진 에이전트를 실행합니다.
result = agent.run(
agentic_prompt(query="아이돌 관련 밈 알려주세요"),
additional_args={"dataset": dataset,
"dataset_description": "한국어 밈(meme)에 대한 데이터셋으로, meme과(title), meme의 뜻(content)을 알려준다.",
"column_name": "content"}
)print(f"Final result: {result}")Output
Final result: 엄마가되는 밈은 최근 SNS와 온라인 커뮤니티에서 유행하는 표현으로, 주로 누군가가 매우 귀엽거나 사랑스러워서 모성애적 감정을 느낄 때 사용됩니다. 이 표현은 아이돌 팬덤 문화에서 자주 사용되며, '엄마가 돼'가 올바른 표기이지만 의도적인 맞춤법 파괴를 통해 밈으로서의 독특한 성격을 갖게 되었습니다.
Final result: 엄마가되는 밈은 최근 SNS와 온라인 커뮤니티에서 유행하는 표현으로, 주로 누군가가 매우 귀엽거나 사랑스러워서 모성애적 감정을 느낄 때 사용됩니다. 이 표현은 아이돌 팬덤 문화에서 자주 사용되며, '엄마가 돼'가 올바른 표기이지만 의도적인 맞춤법 파괴를 통해 밈으로서의 독특한 성격을 갖게 되었습니다.Conclusion
단순히 검색 결과만을 가져오는 것이 아니라, 검색결과를 바탕으로 쿼리에 따른 답변을 정제해서 보여주었습니다.✌🏻
지금까지 간단한 에이전트 시스템을 만들어보았습니다. 여기에 품질 평가, 분석 등을 추가한다면 진정한 Agentic 검색 엔진을 구현할 수 있습니다.
