
LlamaIndex와 VectorAI DB를 활용한 로컬 문서 지능형 에이전트 구축
요약
LlamaIndex와 Actian VectorAI DB를 통합하여 외부 네트워크 연결 없이 로컬에서 작동하는 문서 지능형 에이전트 구축 방법을 소개합니다. Ollama를 활용해 임베딩과 LLM을 모두 로컬에서 실행함으로써 데이터 보안과 에어갭 환경 대응이 가능합니다.
핵심 포인트
- Actian VectorAI DB를 활용한 로컬 벡터 스토어 통합 방법 제시
- Ollama를 이용한 로컬 임베딩 및 LLM 실행으로 데이터 보안 강화
- LlamaIndex의 VectorStore 인터페이스(add, delete, query, get_nodes) 활용
- 에어갭 환경 및 클라우드 의존성 제거가 필요한 프로젝트에 최적화
대부분의 LlamaIndex 벡터 스토어 (vector store) 통합 방식은 외부 연결을 전제로 합니다. Pinecone, Weaviate, Qdrant Cloud, 그리고 공식적으로 지원되는 나머지 목록들은 모두 외부 서비스로의 네트워크 호출을 필요로 합니다. 이는 문제가 없을 때도 있지만, 배포 대상이 에어갭 (air-gapped) 환경이거나, 데이터 분류 규정상 외부 API 호출이 금지되어 있거나, 혹은 프로젝트가 단순히 클라우드 의존성을 가질 수 없는 상황이 되면 문제가 됩니다.
이 튜토리얼은 커스텀 벡터 스토어 (VectorStore) 인터페이스를 통해 Actian VectorAI DB를 LlamaIndex와 통합하고, 이를 기반으로 문서 지능형 에이전트 (document intelligence agent)를 구축합니다. 모든 구성 요소는 로컬 하드웨어에서 실행됩니다: 오케스트레이션 (orchestration)을 위한 LlamaIndex, 임베딩 (embeddings)을 위한 Ollama 기반의 nomic-embed-text, 검색 (retrieval)을 위한 VectorAI DB, 그리고 생성을 위한 Ollama 기반의 로컬 대규모 언어 모델 (LLM)까지 모두 포함됩니다. 그 어떤 데이터도 기기를 벗어나지 않습니다.
이 통합 방식은 다른 모든 LlamaIndex 통합 방식이 따르는 것과 동일한 인터페이스 패턴인 공식 llama-index-vector-stores-actian-vectorai 패키지의 ActianVectorAIVectorStore를 사용하며, 기존의 어떤 LlamaIndex 프로젝트에도 임포트(import) 가능한 독립형 모듈로서 GitHub 리포지토리에 제공됩니다.
LlamaIndex의 VectorStore 인터페이스 작동 방식
구현 코드를 작성하기 전에 인터페이스를 파악해 두십시오. 이미 LlamaIndex의 내장 통합 기능을 사용해 보았다면, 이 패턴을 즉시 알아차릴 수 있을 것입니다. 생태계 내의 모든 통합 기능은 동일한 네 가지 메서드를 구현하기 때문입니다.
LlamaIndex의 VectorStore 베이스 클래스 (base class)는 다음을 요구합니다:
add:BaseNode객체 리스트를 받습니다. 각 노드에서 임베딩 (embedding)과 메타데이터 (metadata)를 추출합니다. 이를 대상 데이터베이스에 삽입합니다. 삽입된 노드 ID 리스트를 반환합니다.delete: 노드 ID를 받습니다. 대상 데이터베이스에서 해당 레코드를 삭제합니다.query: 쿼리 임베딩 (query embedding), top-k 값, 그리고 선택적 메타데이터 필터 (metadata filters)를 포함하는VectorStoreQuery객체를 받습니다. 벡터 검색 (vector search)을 실행합니다.VectorStoreQueryResult를 반환합니다.get_nodes: 노드 ID 리스트를 받습니다. 대상 데이터베이스에서 해당 레코드를 검색합니다. 이를BaseNode객체로 반환합니다.
모든 공식 LlamaIndex 벡터 스토어 (vector store) 통합 라이브러리는 대상 데이터베이스의 SDK를 사용하여 이 네 가지 메서드를 구현합니다.
다음은 LlamaIndex VectorStore 베이스 클래스 (base class)의 메서드 시그니처 (method signatures)입니다:
# LlamaIndex VectorStore 베이스 클래스 메서드 시그니처
from llama_index.core.vector_stores.types import (
...
여러분의 VectorAI DB 통합 구현은 Python SDK의 VectorAIClient를 사용하여 이 네 가지 메서드를 구현합니다. 이것이 구현의 전체 범위입니다.
구축하게 될 내용
구현 코드를 작성하기 전의 전체 에이전트 아키텍처 (agent architecture)는 다음과 같습니다.
여러분의 에이전트는 LlamaIndex의 SimpleDirectoryReader를 사용하여 로컬 PDF 문서 라이브러리를 가져옵니다 (ingest). 문서는 LlamaIndex의 SentenceSplitter를 사용하여 512-토큰 청크 크기 (chunk size)와 50-토큰 오버랩 (overlap) 설정으로 노드 (nodes)로 분할됩니다.
각 노드는 Ollama를 통해 실행되는 nomic-embed-text를 사용하여 임베딩되며, 768차원 벡터를 생성합니다. 해당 임베딩들은 이 튜토리얼에서 여러분이 직접 구축할 커스텀 VectorStore 통합을 통해 VectorAI DB에 저장됩니다.
쿼리 시점에 에이전트는 동일한 nomic-embed-text 모델로 질문을 임베딩하고, VectorAI DB에서 가장 유사한 상위 5개(top-5) 노드를 검색한 뒤, 이를 생성 (generation)을 위한 컨텍스트 (context)로서 Ollama를 통해 로컬 LLM에 전달합니다. LLM은 응답 시 소스 청크 (source chunks)를 인용합니다.
그림 1: 전체 에이전트 아키텍처 (The full agent architecture)
하드웨어 기준 (Hardware baseline)
16 GB RAM, 4코어 CPU. 전체 스택은 표준 개발자 하드웨어에서 GPU 없이도 실행됩니다. GPU가 없으면 생성 (Generation) 속도는 더 느리지만, 기능은 완전히 작동합니다.
사전 요구 사항 (Prerequisites)
실습을 따라 하려면 다음 도구들을 설치하세요:
환경 설정 (Set Up the Environment)
통합 코드를 작성하기 전에 모든 종속성 (dependencies)을 실행합니다. 아래의 모든 명령은 표준 Linux 또는 macOS 머신에서 수정 없이 실행됩니다.
UV를 사용하여 프로젝트 설정.
uv init llamaindex-vectoraidb-agent
cd llamaindex-vectoraidb-agent
mkdir -p docs
Docker Compose로 VectorAI DB 시작하기
docker-compose.yml 파일을 생성합니다:
services:
vectoraidb:
image: actian/vectorai:latest
...
서비스를 시작합니다:
docker compose up -d
예상되는 시작 출력:
그림 2: 실행 중인 VectorAI DB Docker 서비스 (VectorAI DB Docker service running)
헬스 체크 (health check)가 통과될 때까지 기다린 다음, VectorAI DB가 실행 중인지 확인합니다:
curl localhost:6574/health
예상 응답:
{"status":"ok"}
Ollama를 설치하고 필요한 모델을 가져옵니다 (pull).
# Ollama 설치 (macOS/Linux)
curl -fsSL https://ollama.com/install.sh | sh
...
모든 종속성을 추가합니다:
uv add \
llama-index-vector-stores-actian-vectorai \
llama-index-core \
...
uv add는 모든 것을 한 단계로 해결(resolve), 고정(pin), 설치(install)합니다. llama-index-vector-stores-actian-vectorai 패키지는 VectorAIClient와 AsyncVectorAIClient를 제공하는 SDK인 actian-vectorai-client를 자동으로 가져옵니다.
pyproject.toml 파일에는 다음 내용이 포함됩니다:
[project]
name = "llamaindex-vectoraidb-agent"
version = "0.1.0"
...
이제 환경 준비가 완료되었습니다. VectorAI DB는 6574 포트에서 실행 중이며, Ollama는 두 모델을 서빙하고 있고, 모든 Python 패키지가 설치되었습니다.
VectorAI DB VectorStore 통합 구축
이 통합 기능은 공식 llama-index-vector-stores-actian-vectorai 패키지에 ActianVectorAIVectorStore로 포함되어 제공됩니다. 별도로 작성해야 할 커스텀 클래스는 없습니다. 아래 모듈은 단일 로컬 경로에서 이를 다시 내보내기(re-export)하므로, 프로젝트 내의 모든 스크립트가 일관되게 임포트(import)할 수 있습니다:
# vectoraidb_vectorstore.py -- 통합 진입점
"""
vectoraidb_vectorstore.py
...
이 파일을 기존의 어떤 LlamaIndex 프로젝트에도 넣기만 하면 됩니다. 향후 버전에서 공식 패키지 내 ActianVectorAIVectorStore의 위치가 바뀌더라도 임포트 경로는 동일하게 유지됩니다.
운영 환경(production)에 배포하기 전, VectorAI DB를 통해 검증된 주요 생성자(constructor) 파라미터는 다음과 같습니다:
| 파라미터 (Parameter) | 기본값 (Default) | 참고 (Notes) |
|---|---|---|
url | "localhost:6574" | host:port 형식, http:// 스킴(scheme) 제외 |
| ... |
이 클래스는 세 가지 연결 패턴을 지원합니다: 컨텍스트 매니저 (context manager, 권장), 수동 connect()/close(), 그리고 외부 클라이언트입니다. 아래 스크립트에서 이 세 가지 방식을 모두 보여줍니다.
문서 인제스트 (Ingest)
컨텍스트 매니저 패턴을 사용하여 로컬 PDF 문서 라이브러리를 VectorAI DB로 로드합니다. 컨텍스트 매니저는 진입 시 connect()를 호출하고 종료 시 shutdown()을 호출하므로, 연결 생명주기(lifecycle)가 자동으로 관리됩니다.
# ingest.py -- 전체 인제스트 파이프라인
import argparse
...
실행 방법:
uv run ingest.py
# 또는 다른 디렉토리를 지정하려면:
uv run ingest.py --docs /path/to/your/pdfs --collection my_collection
예상 출력:
'./docs'에서 문서를 로드하는 중 ...
87개의 문서 페이지를 로드했습니다.
localhost:6574의 VectorAI DB에 연결하는 중 ...
...
컬렉션은 docker-compose.yml에 정의된 Docker 볼륨(volume)에 기록됩니다. 이는 재시작 후에도 유지됩니다.
쿼리 에이전트(Query Agent) 구축
인덱스(index)를 로컬 LLM에 연결하고 쿼리 인터페이스를 구축합니다. agent.py 스크립트는 외부 클라이언트 패턴(external client pattern)을 사용합니다. 즉, VectorAIClient 컨텍스트 매니저(context manager)가 전체 쿼리 세션을 감싸고, ActianVectorAIVectorStore가 이를 client 인자로 전달받습니다. 이를 통해 에이전트가 여러 개의 쿼리를 처리할 때 연결 생명주기(connection lifecycle)를 명시적으로 제어할 수 있습니다.
# agent.py -- QueryEngine 설정 및 인용 출력이 포함된 첫 번째 쿼리
import argparse
import sys
from typing import Any
from actian_vectorai import VectorAIClient
from llama_index.core import VectorStoreIndex, StorageContext, Settings
from llama_index.core.agent import ReActAgent
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from vectoraidb_vectorstore import ActianVectorAIVectorStore
VECTORAIDB_URL = "localhost:6574"
COLLECTION_NAME = "document_intelligence"
EMBED_MODEL = "nomic-embed-text"
LLM_MODEL = "llama3.2"
TOP_K = 5
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Query the document intelligence agent")
p.add_argument("--query", default=None, help="Single query")
p.add_argument("--react", action="store_true", help="Use ReActAgent")
p.add_argument("--collection", default=COLLECTION_NAME, help="Collection name")
p.add_argument("--url", default=VECTORAIDB_URL, help="VectorAI DB host:port")
p.add_argument("--top-k", type=int, default=TOP_K, help="Chunks to retrieve")
return p.parse_args()
def configure_settings() -> None:
Settings.embed_model = OllamaEmbedding(
model_name=EMBED_MODEL,
base_url="http://localhost:11434",
)
Settings.llm = Ollama(
model=LLM_MODEL,
base_url="http://localhost:11434",
temperature=0, # temperature 0 for factual retrieval tasks
request_timeout=120.0,
)
def print_response(response: Any) -> None:
print("\n" + "=" * 60)
print("ANSWER:")
print(response.response if hasattr(response, "response") else str(response))
if hasattr(response, "source_nodes") and response.source_nodes:
print("\nSOURCES:")
for i, node in enumerate(response.source_nodes, 1):
meta = node.node.metadata
fname = meta.get("file_name", meta.get("source", "unknown"))
chunk = meta.get("chunk_index", "?")
score = f"{node.score:.4f}" if node.score is not None else "n/a"
print(f" [{i}] {fname} | chunk {chunk} | score {score}")
print("=" * 60 + "\n")
def run_query_engine(index: VectorStoreIndex, query: str, top_k: int) -> None:
engine = index.as_query_engine(
similarity_top_k=top_k,
response_mode="compact",
)
print(f"\nQuery: {query}")
response = engine.query(query)
print_response(response)
def interactive_loop(index: VectorStoreIndex, react: bool, top_k: int) -> None:
print("대화형 모드로 진입합니다. 종료하려면 'exit' 또는 'quit'을 입력하세요.")
while True:
try:
query = input("Query> ").strip()
except (EOFError, KeyboardInterrupt):
print("\n대화형 모드를 종료합니다.")
break
if not query or query.lower() in {"exit", "quit"}:
print("대화형 모드를 종료합니다.")
break
if react:
print("ReActAgent 모드가 선택되었습니다. 쿼리 엔진 폴백 (query engine fallback)을 사용합니다.")
run_query_engine(index, query, top_k)
def main() -> None:
args = parse_args()
configure_settings()
print(f"{args.url}의 VectorAI DB에 연결 중 ...")
# 외부 클라이언트 패턴 (External client pattern): 미리 구성된 VectorAIClient를 전달합니다.
# client= 가 제공되면 url 및 client_kwargs는 무시됩니다.
# 호출자 (이 스크립트)가 클라이언트의 생명주기 (lifecycle)를 관리할 책임이 있습니다.
with VectorAIClient(args.url) as client:
try:
info = client.get_collection(args.collection)
print(f"컬렉션 '{args.collection}' 로드 완료: {info.points_count}개의 포인트 (points).")
except Exception:
print(f"\nERROR: 컬렉션 '{args.collection}'을 찾을 수 없습니다.")
print("해결 방법: 먼저 'uv run ingest.py'를 실행하세요.")
sys.exit(1)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기