2026년 2월 17일 화요일

고속학습과 모델추론을 지원하는 Unsloth 기반 파인튜닝모델 개발

이 글은 고속학습과 모델추론을 지원하는 Unsloth 기반 모델 파인튜닝 개발 방법을 나눔한다. 


unsloth  사용 순서
1. 환경 구성 및 의존성 설치
Unsloth는 최신 GPU 아키텍처에서 최적의 성능을 발휘하며, 라이브러리 설치 후에는 반드시 PyTorch 및 관련 의존성 패키지의 버전을 확인해야 한다. 특히 가속화된 연산을 위해 xformers  bitsandbytes 등을 함께 설치하는 것이 필수적이다. 이는 메모리 사용량을 70% 가까이 절감하면서도 훈련 속도를 2배 이상 높이는 핵심적인 기반이 된다. 

unsloth 설치는 다음과 같이 명령 입력하면 된다(리눅스에서 동작). 
pip install "unsloth @ git+https://github.com/unslothai/unsloth.git"

2. 모델 및 토크나이저 로드
Unsloth는 FastLanguageModel 클래스를 통해 Llama, Mistral, Gemma 등 주요 오픈 소스 모델을 빠르게 불러오는 기능을 제공한다. 4비트 양자화(4-bit Quantization)를 기본적으로 지원하여 VRAM이 제한적인 환경에서도 대규모 언어 모델을 로드할 수 있는 구조이다. 로딩 시 max_seq_length 와 dtype 등을 설정하여 프로젝트의 하드웨어 사양에 최적화된 상태를 유지하는 것이 중요하다.

3. LoRA 어댑터 설정 및 적용
모델 전체를 훈련시키는 대신, 특정 레이어에 하위 행렬을 추가하여 학습하는 LoRA(Low-Rank Adaptation) 기술을 적용한다. get_peft_model 함수를 호출하여 r(rank), alpha, target_modules 등의 하이퍼파라미터를 설정하는 과정이 핵심이다. 이 방식은 파인튜닝이 필요한 가중치의 양을 획기적으로 줄여 학습 속도를 비약적으로 향상시키고 과적합(Overfitting) 위험을 방지하는 전략이다.

4. 데이터셋 준비 및 포맷팅
신뢰성 있는 학습을 위해서는 데이터의 질과 형식이 정밀하게 관리되어야 한다. 주로 Alpaca나 ChatML 형식을 따르며, Unsloth에서 제공하는 표준화된 템플릿을 사용하여 데이터를 구성하는 것이 일반적이다. 데이터셋 내의 질문과 답변 쌍을 모델이 이해할 수 있는 토큰 형태로 변환하고, 패딩(Padding) 처리를 통해 배치 학습 효율을 높이는 과정이 필요하다.

5. SFTTrainer를 이용한 모델 학습
Hugging Face의 SFTTrainer 와 결합하여 실제 학습을 수행한다. 학습률(Learning Rate), 에폭(Epoch), 배치 사이즈(Batch Size) 등의 파라미터를 설정하고 unsloth 고유의 최적화 커널을 활성화한다. 학습 과정 중 손실(Loss) 변화를 모니터링하며 가중치가 안정적으로 수렴하는지를 확인하는 작업은 모델의 신뢰성을 확보하는 필수 단계이다.

6. 모델 저장 및 배포 준비
학습이 완료된 모델은 LoRA 어댑터 형태로 저장하거나 전체 모델과 병합(Merge)하여 배포할 수 있다. 특히 Unsloth는 GGUF 포맷으로의 내보내기 기능을 강력하게 지원하여, 파인튜닝된 모델을 llama.cpp 등 다양한 추론 엔진에서 즉시 활용할 수 있도록 돕는다. 이는 개발된 모델을 실무 환경에 빠르게 통합하고 배포하는 데 매우 유리한 조건이다.

개발 방법
Unsloth는 Hugging Face의 `SFTTrainer`와 완벽하게 호환되며, 모델 로드부터 학습까지의 과정을 비약적으로 단순화한 구조이다.

from unsloth import FastLanguageModel
import torch
from trl import SFTTrainer
from transformers import TrainingArguments

# 1. 모델 및 토크나이저 로드 (4비트 양자화 적용)
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/llama-3-8b-bnb-4bit", # 최적화된 프리셋 모델
max_seq_length = 2048,
load_in_4bit = True,
)

# 2. LoRA(Low-Rank Adaptation) 설정
model = FastLanguageModel.get_peft_model(
model,
r = 16, # Rank 설정
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha = 16,
lora_dropout = 0, # Unsloth는 0을 권장 (속도 최적화)
bias = "none",
)

# 3. 학습 인자 및 Trainer 설정
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset, # 준비된 데이터셋
dataset_text_field = "text",
max_seq_length = 2048,
args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
warmup_steps = 5,
max_steps = 60, # 테스트용 스텝 수
learning_rate = 2e-4,
fp16 = not torch.cuda.is_bf16_supported(),
bf16 = torch.cuda.is_bf16_supported(),
logging_steps = 1,
output_dir = "outputs",
),
)

# 4. 학습 실행
trainer.train()


보다시피 기존 파인튜닝 코드를 그대로 사용할 수 있다.

Unsloth의 주요 한계점
Unsloth는 매우 강력한 도구이지만, 특정 하드웨어와 소프트웨어 환경에 종속적인 몇 가지 제약 사항이 존재한다.

하드웨어 및 OS 제약
  • NVIDIA GPU 전용:  Unsloth는 OpenAI의 Triton 언어를 기반으로 최적화된 커널을 사용하므로, NVIDIA GPU(Turing, Ampere, Hopper 아키텍처 등)에서만 동작하는 구조이다.
  • Linux 환경 최우선:  기본적으로 리눅스 환경을 위해 설계되었으며, 윈도우 환경에서는 반드시 WSL2(Windows Subsystem for Linux)를 통해서만 안정적인 실행이 가능하다.
모델 및 라이브러리 지원 범위
  • 제한된 모델 아키텍처:  Llama, Mistral, Gemma, Phi, Qwen 등 널리 쓰이는 주요 오픈 소스 모델 위주로 최적화가 진행되어 있으며, 모든 최신 모델을 즉각적으로 지원하는 것은 아니다.
  • 싱글 GPU 최적화 편중:  기본 버전은 단일 GPU에서의 메모리 효율과 속도 극대화에 초점이 맞춰져 있어, 대규모 멀티 GPU 분산 학습(FSDP 등) 설정 시 추가적인 복잡성이 발생할 수 있는 형태이다.
기능적 제약
  • 고정된 최적화 커널:  성능을 위해 특정 연산을 수동으로 튜닝한 커널을 사용하므로, 사용자가 모델 아키텍처를 임의로 크게 수정하거나 특이한 레이어를 추가할 경우 Unsloth의 가속 혜택을 받기 어려운 구조이다.
  • DPO/PPO 학습의 복잡성:  단순한 지도 학습(SFT)은 매우 직관적이지만, 직접적인 인간 피드백 학습(RLHF) 단계인 DPO나 PPO를 적용할 때는 설정이 다소 까다로울 수 있다는 점이 한계이다.

마무리
Unsloth를 이용한 파인튜닝은 자원 소모를 최소화하면서도 모델의 성능을 극대화할 수 있는 최신 개발 방법론이다. 이러한 효율적인 훈련 체계는 개인 개발자나 중소규모 팀에서도 고성능의 특화 모델을 구축할 수 있게 하여, AI 민주화와 기술적 한계 극복에 기여하는 중요한 도구로 평가받는 기술이다.

레퍼런스

2026년 2월 4일 수요일

스케일AI와 라벨링 작업 뒷이야기

이 글은 스케일AI와 라벨링 작업 뒷이야기를 나눔합니다. 

AI의 이면: 보이지 않는 데이터 라벨러와 착취적 노동 생태계
고품질의 학습 데이터는 성능이 뛰어난 대규모 언어 모델(LLM)을 생성하는 핵심 요소이며, 이는 곧 사람의 손을 거친 라벨링된 데이터셋을 의미한다. LLM의 훈련 과정 중 지도 학습과 인간 피드백 기반 강화 학습(RLHF) 단계에서는 인간의 노동이 필수적이다. 라벨러들은 이미지나 텍스트 같은 원시 데이터에 정답을 붙임으로써 AI가 올바른 판단을 내리고 답변의 품질을 높이도록 돕는다. 하지만 이 거대한 기술의 뒤에는 전 세계에 흩어진 보이지 않는 노동자들의 희생이 존재한다.

글로벌 공급망과 미세 노동의 실체
AI 개발사들은 비용 절감을 위해 케냐, 인도, 필리핀 등 저임금 국가의 노동력을 활용하는 미세 노동(Microwork) 플랫폼에 라벨링 작업을 외주화한다. 이러한 플랫폼들은 노동자와 정식 고용 계약을 맺지 않는 인간 서비스(humans-as-a-service) 모델을 채택하고 있다. 노동자들은 시간당 2달러 미만의 임금을 받으면서도 살인, 학대, 아동 착취 등 트라우마를 유발할 수 있는 유해한 콘텐츠를 수 시간 동안 검토해야 하는 열악한 환경에 처해 있다. 이들은 수십억 달러 가치의 AI 시스템을 구축하는 핵심 동력이지만, 기업의 이익 공유에서는 철저히 배제된다.


투명성 결여와 알고리즘에 의한 관리
데이터 라벨링 생태계는 심각한 불투명성 문제를 안고 있다. Scale AI와 그 자회사인 Remotasks처럼 기업들은 복잡한 지배 구조를 통해 실제 고용주와 의뢰인(OpenAI, MS 등)의 정체를 숨긴다. 노동자들은 자신이 누구를 위해 일하는지도 모른 채 암호화된 프로젝트명 아래에서 단순 작업을 반복한다. 또한, 알고리즘 관리 시스템은 노동자의 일상과 생산성을 철저히 감시한다. 화장실 휴식조차 허용하지 않는 엄격한 타이머가 작동하며, 알고리즘이 산정하는 임금은 수요와 공급에 따라 실시간으로 변동되어 노동자의 소득 예측 가능성을 박탈한다.


불안정한 일자리와 기그 경제의 병폐
노동자들은 일감을 얻기 위해 밤낮없이 화면을 모니터링해야 하며, 알고리즘의 결정에 따라 예고 없이 계정이 정지되거나 일자리를 잃기도 한다. 무급으로 진행되는 장시간의 교육 과정과 테스트 역시 노동자에게 전가되는 부담이다. 최근 기업들이 비용 절감을 위해 더 저렴한 노동 시장으로 거점을 옮기면서 기존 지역 노동자들에게 임금을 지불하지 않고 철수하는 사례도 보고되고 있다. 이는 법적 보호가 취약한 기그 경제(Gig Economy)의 전형적인 착취 구조가 AI 산업에서 재현되고 있음을 보여준다.

자동화의 한계와 규제적 대응의 필요성
데이터 라벨링을 자동화하려는 시도가 이어지고 있으나, 여전히 최종 검수 단계에서는 인간의 판단이 필요하다. 자동화는 기업의 효율성을 높일 뿐 노동자의 권리나 처우 개선으로 이어지지 않는다. 다행히 유럽의 플랫폼 노동 지침(PWD)이나 국제노동기구(ILO)의 논의 등 규제적 노력이 시작되고 있다. AI 공급망의 가장 밑바닥에서 모델을 지탱하고 있는 미세 노동자들의 권리를 보호하고, 거대 기술 기업의 책임을 강화하는 정책적 대응이 시급한 시점이다. 이들이 겪는 착취와 학대를 멈추는 것이야말로 진정한 의미의 신뢰할 수 있는 AI 개발의 시작이다.

2026년 1월 7일 수요일

랭그래프, 웹 기반 AI Agents 개발 방법 및 디자인패턴

인공지능은 이제 단일 모델 기반의 응답 시스템에서 벗어나, 자율적인 구성 요소들이 추론하고 행동하며 협력하는 에이전트 중심 시스템으로 급격히 이동 중이다. 기존 LLM의 단발성 상호작용과 달리, 에이전트 아키텍처는 컨텍스트가 유지되는 상태 기반 세션, 작업별 특화 로직의 모듈화, 외부 도구 및 워크플로우와의 상호운용성을 지향한다. FastAPI, LangGraph, MCP(Model Context Protocol)를 통해 확장 가능한 플랫폼 구조와 디자인 패턴을 정리한다. 좀 더 상세한 내용은 레퍼런스를 참고한다.

프로젝트 아키텍처 및 주요 구성 요소
시스템은 클라이언트-API-오케스트레이션-도구 실행으로 이어지는 명확한 계층형 구조를 가진다. FastAPI는 고성능 비동기 웹 서비스를 통해 에이전트의 진입점 역할을 수행하며, 실제 추론 로직과 분리되어 있어 유지보수가 용이하다. 서비스 레이어는 API와 오케스트레이션 계층을 연결하며 세션 초기화와 최종 응답 포맷팅을 담당한다. LLM 제공자 추상화를 통해 OpenAI나 Anthropic과 같은 다양한 모델을 유연하게 선택할 수 있는 구조이다.
MCP(Model Context Protocol)는 모델과 도구 간의 상호작용을 표준화하여 시스템의 신뢰성을 높인다. 모든 도구는 @tool 데코레이터를 사용하여 선언적으로 정의되며, 중앙 집중식 레지스트리 패턴을 통해 관리된다. 이러한 방식은 로컬 도구를 외부 MCP 서버로 교체하거나 런타임에 새로운 기능을 동적으로 로드하는 것을 매우 간편하게 만든다. 결과적으로 에이전트는 단순한 텍스트 생성을 넘어 외부 API 호출, 계산, 데이터 분석 등 실질적인 액션을 수행하는 능력을 갖추게 된다.

이제 다이어그램에서 전체 에이전트 패턴 구조와 몇몇 핵심적인 부분을 구현해 본다. 

LangGraph를 이용한 에이전트 오케스트레이션
에이전트 아키텍처의 핵심은 LangGraph를 이용한 상태 관리와 워크플로우 정의이다. StateGraph를 활용하여 에이전트가 언제 도구를 호출하고 언제 작업을 종료할지 명시적으로 제어한다. 이는 에이전트의 행동을 투명하게 만들고 디버깅을 용이하게 하는 핵심적인 설계이다.

다음과 같은 에이전트 구조가 있다고 치자. agent는 사용자 입력을 받아 LLM 추론을 한다. 그 결과 tools 호출(예. 날씨, 온도, IoT 센서, 데이터베이스, 로보틱스 액추에이터 등등)이 필요하면 tools를 호출하고, 그 결과를 다시 LLM에게 전달해 답변을 출력한다. 아니면, END 종료한다. 

이를 랭그래프로 구현하면 다음과 같다.
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode

def create_agent_graph():
    llm = get_llm()
    tools = get_tools()
    llm_with_tools = llm.bind_tools(tools)

    workflow = StateGraph(AgentState)

    # LLM 추론 노드 정의
    async def call_model(state: AgentState) -> dict:
        response = await llm_with_tools.ainvoke(state["messages"])
        return {"messages": [response]}

    # 노드 등록 및 경로 설정
    workflow.add_node("agent", call_model)
    workflow.add_node("tools", ToolNode(tools))
    workflow.add_edge(START, "agent")

    # 조건부 엣지: 도구 호출 여부에 따라 경로 결정
    def should_continue(state: AgentState) -> str:
        last_message = state["messages"][-1]
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            return "tools"
        return END

    workflow.add_conditional_edges("agent", should_continue, ["tools", END])
    workflow.add_edge("tools", "agent")

    return workflow.compile()

LangGraph 기반 ReAct 패턴 구현 
사용자 질문에 대해 계속 도구를 호출해 반복된 구조로 답을 찾는 에이전트는 ReAct 구조를 사용할 수 있다. 앞의 구조에서 좀 더 실용적인 도구를 사용해 보겠다. 위키피디아 검색 및 계산기 도구를 호출하도록 랭그래프를 다음과 같이 수정한다. 

이 에이전트는 사용자 질문에 대한 특정 도구를 발견하지 못할때까지 반복해 호출할 것이다. 실제 예에서는 토큰 비용 및 성능도 고려해야 하므로, 그래프 LLM 호출 최적화가 필요할 것이다. 

from typing import Annotated, Literal
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from dotenv import load_dotenv

load_dotenv()

# 1. 도구(Tool) 정의
from langchain_core.tools import tool

@tool
def search_wikipedia(query: str) -> str:
    """Search Wikipedia for information. Use this when you need factual information."""
    # 실제로는 Wikipedia API 호출
    return f"Wikipedia search results for '{query}': [모의 검색 결과 - 실제로는 API 호출]"

@tool
def calculator(expression: str) -> str:
    """Calculate mathematical expressions. Input should be a valid Python expression."""
    try:
        result = eval(expression, {"__builtins__": {}})
        return str(result)
    except Exception as e:
        return f"Error: {e}"

tools = [search_wikipedia, calculator]
tool_node = ToolNode(tools)

# 2. State 정의
class AgentState(TypedDict):
    messages: Annotated[list, "The messages in the conversation"]

# 3. LLM 설정 (tool binding)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)

# 4. Agent 노드 - LLM이 판단
def agent_node(state: AgentState) -> dict:
    """LLM이 도구를 사용할지, 최종 답변을 할지 결정 (Thought)"""
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

# 5. 라우팅 로직 - 종료 조건 결정
def should_continue(state: AgentState) -> Literal["tools", "end"]:
    """
    ReAct의 핵심 종료 조건:
    - LLM이 tool_calls를 반환하면 → "tools" (Action 필요)
    - tool_calls가 없으면 → "end" (최종 답변 완성)
    """
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"  # 도구 사용 필요
    return "end"  # 최종 답변 완성

# 6. 그래프 구성
workflow = StateGraph(AgentState)

workflow.add_node("agent", agent_node)      # Thought 단계
workflow.add_node("tools", tool_node)        # Action 단계

workflow.add_edge(START, "agent")

# ReAct 사이클: agent → tools → agent → tools → ... → end
workflow.add_conditional_edges(
    "agent",
    should_continue,  # ← 종료 조건 검출
    {
        "tools": "tools",  # 도구 실행 후 다시 agent로
        "end": END         # 답변 완성 시 종료
    }
)
workflow.add_edge("tools", "agent")  # Observation 후 다시 Thought

react_agent = workflow.compile()

# 7. 실행 예시
if __name__ == "__main__":
    # 예시 1: 계산 필요
    result = react_agent.invoke({
        "messages": [HumanMessage(content="What is 342 * 67?")]
    })
    print("답변:", result["messages"][-1].content)
    print("\n" + "="*80 + "\n")
    
    # 예시 2: 검색 + 계산 필요
    result = react_agent.invoke({
        "messages": [HumanMessage(
            content="Search for the population of Seoul, then multiply it by 2"
        )]
    })
    print("답변:", result["messages"][-1].content)

이런 방식으로 필요한 도구들, 목적별 LLM 에이전트(예. 조사, 설계, 아이디어 구현, 요약, 보고서 작성 등)를 추가해 나갈 수 있다. 

결론
이 간단한 에이전트 아키턱쳐 예시는 다중 에이전트 간의 협력 체인 구축, 세션 내 인간 피드백 통합, 워크플로우 관찰 가능성(Observability) 강화 등을 통해 더욱 고도화된 에이전트 시스템으로 확장이 가능하다. 본 글에서는 랭그래프를 사용했다. 요즘에는 랭그래프 라이브러리 안전성이 높아져 이런 멀티 에이전트 구조는 큰 문제 없이 개발이 가능해 졌다. 멀티에이전트 개발 개념은 모두 유사하므로 유스케이스 목적에 따라 다른 프레임웍 라이브러리 사용하는 것도 고려할 수 있을 것이다.

부록: AI 에이전트 디자인 패턴 
앞서 본 것처럼 에이전트 개발에는 여러 디자인 패턴이 있을 수 있다. 이는 유스케이스 목적에 따라 결정되어야 한다. 토큰 사용량 대비 효과적인 답을 낼 수 있도록 구현되어야 한다. 다음은 다양한 에이전트 패턴 구현 방법을 보여준다.

import operator
from typing import Annotated, List, TypedDict, Literal
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from dotenv import load_dotenv

load_dotenv()

# 순차적 에이전트(Sequential Agent)는 여러 단계에 걸쳐 내부 추론을 수행. 스크래치패드(scratchpad)라는 방식으로 진행. 별도의 외부 도구를 전혀 사용하지 않음.
# 순수한 분석과 연역적 추론만으로도 충분한 상황에 주로 사용.

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

class State(TypedDict):
    question: str
    steps: Annotated[List[str], operator.add]
    answer: str

def plan_node(state: State) -> dict:
    sys = (
        "당신은 신중한 계획자입니다. 사용자의 질문을 2-4개의 간결한 단계로 나누세요. "
        "문제를 해결하지는 마세요. 번호가 매겨진 단계 목록만 반환하고, 추가 텍스트는 작성하지 마세요."
    )
    messages = [("system", sys), ("user", state["question"])]
    resp = llm.invoke(messages)
    raw = resp.content
    steps = []
    for line in str(raw).splitlines():
        line = line.strip()
        if not line:
            continue
        line = line.lstrip("-• ").split(". ", 1)[-1] if ". " in line[:4] else line.lstrip("-• ")
        steps.append(line)
    return {"steps": steps}

def solve_node(state: State) -> dict:
    """계획된 단계를 사용하여 최종 답변만 도출합니다."""
    sys = (
        "제공된 단계를 사용하여 문제를 해결하세요. "
        "최종 답변만 반환하고, 추론 과정은 포함하지 마세요."
    )
    messages = [
        ("system", sys),
        ("user", f"질문: {state['question']}\\\\n단계: {state['steps']}"),
    ]
    resp = llm.invoke(messages)
    return {"answer": str(resp.content).strip()}

#  Wire up the graph
graph = StateGraph(State)
graph.add_node("plan", plan_node)
graph.add_node("solve", solve_node)

graph.add_edge(START, "plan")
graph.add_edge("plan", "solve")
graph.add_edge("solve", END)

cot_graph = graph.compile()

state = {
    "question": "강의 동영상이 120개이고 하루에 15개를 본다면, 완강하는 데 며칠이 걸릴까요?",
    "steps": [],
    "answer": ""
}
out = cot_graph.invoke(state)
print("최종 답변:", out["answer"])

# 커스텀 에이전트(Custom Agent)는 유연성을 제공. 사용자는 전체적인 로직과 라우팅을 직접 설계할 수 있음. 또한 시스템을 구성하는 개별 노드까지 스스로 정의. 사용자의 요구에 맞춰 자유롭게 맞춤형 제어가 가능.
class CustomState(TypedDict):
    input: str
    task: Literal["math", "capitalize", "count"]
    result: str

def route(state: CustomState) -> str:
    """Deterministic router based on a simple protocol in the input."""
    text = state["input"].strip().lower()
    if text.startswith("math:"):
        return "math"
    if text.startswith("capitalize:"):
        return "capitalize"
    if text.startswith("count:"):
        return "count"
    return "count"

def do_math(state: CustomState) -> dict:
    expr = state["input"].split(":", 1)[-1].strip()
    allowed = set("0123456789+-*/(). ")
    if any(c not in allowed for c in expr):
        return {"result": "Error: unsupported characters in math expression."}
    try:
        res = eval(expr, {"__builtins__": {}})
    except Exception as e:
        res = f"Error: {e}"
    return {"result": str(res)}

def do_capitalize(state: CustomState) -> dict:
    text = state["input"].split(":", 1)[-1].strip()
    return {"result": text.upper()}

def do_count(state: CustomState) -> dict:
    text = state["input"].split(":", 1)[-1].strip()
    tokens = [t for t in text.split() if t]
    return {"result": f"words={len(tokens)} chars={len(text)}"}

graph = StateGraph(CustomState)
graph.add_node("math", do_math)
graph.add_node("capitalize", do_capitalize)
graph.add_node("count", do_count)

graph.add_conditional_edges(
    START,
    route,
    {
        "math": "math",
        "capitalize": "capitalize",
        "count": "count",
    },
)
graph.add_edge("math", END)
graph.add_edge("capitalize", END)
graph.add_edge("count", END)

custom_agent = graph.compile(debug=True)

for user_input in [
    "math: (16 + 3) * 2 + 5",
    "capitalize: hello world from AI agent",
    "count: 여기에 몇 개의 단어가 있나요?",
]:
    out = custom_agent.invoke({"input": user_input, "task": "count", "result": ""})
    print(f"입력: {user_input}\\n결과: {out['result']}\\n---")

# 슈퍼바이저(Supervisor) 패턴은 중앙 제어형 에이전트가 전체 작업을 관리합. 상위 에이전트가 문제를 분석한 뒤 이를 여러 하위 노드에 나누어 배정. 각 하위 노드가 작업을 마치면 그 결과를 다시 취합하고 검토.복잡한 작업을 나누어 병렬로 처리하거나 중앙 통제가 필요할 때 유용.
class SupervisorState(TypedDict):
    """여러 에이전트가 있는 슈퍼바이저 패턴의 상태."""
    topic: str
    messages: Annotated[List[str], operator.add]
    next_agent: str
    final_answer: str


def researcher_agent(state: SupervisorState) -> dict:
    """연구자 에이전트는 주제에 대한 정보를 수집합니다."""
    sys = (
        "당신은 연구자입니다. 주어진 주제에 대한 핵심 사실과 정보를 "
        "수집하는 것이 당신의 임무입니다. 2-3개의 핵심 포인트를 제공하세요. 간결하게 작성하세요."
    )
    messages_for_llm = [
        ("system", sys),
        ("user", f"다음 주제를 조사하세요: {state['topic']}")
    ]
    resp = llm.invoke(messages_for_llm)
    research_msg = f"연구자: {resp.content}"
    return {"messages": [research_msg]}


def expert_agent(state: SupervisorState) -> dict:
    """전문가 에이전트는 연구를 기반으로 분석하고 통찰력을 제공합니다."""
    sys = (
        "당신은 전문 분석가입니다. 제공된 연구를 검토하고 "
        "전문가 분석과 결론을 제공하세요. 구체적이고 통찰력 있게 작성하세요."
    )
    # 이전 메시지에서 컨텍스트 가져오기
    context = "\n".join(state["messages"])
    messages_for_llm = [
        ("system", sys),
        ("user", f"주제: {state['topic']}\n\n이전 조사 내용:\n{context}\n\n전문가 분석을 제공하세요.")
    ]
    resp = llm.invoke(messages_for_llm)
    expert_msg = f"전문가: {resp.content}"
    return {"messages": [expert_msg]}


def supervisor_agent(state: SupervisorState) -> dict:
    """슈퍼바이저는 다음에 어떤 에이전트가 활동할지 또는 토론을 종료할지 결정합니다."""
    sys = (
        "당신은 연구자와 전문가 간의 조사 토론을 관리하는 슈퍼바이저입니다. "
        "지금까지의 대화를 바탕으로 다음에 무엇을 해야 할지 결정하세요:\n"
        "- 초기 조사나 추가 정보가 필요하면 'researcher'를 반환하세요\n"
        "- 조사가 완료되고 전문가 분석이 필요하면 'expert'를 반환하세요\n"
        "- 조사와 전문가 분석이 모두 완료되면 'end'를 반환하세요\n\n"
        "단 하나의 단어만 응답하세요: researcher, expert, 또는 end"
    )

    context = "\n".join(state["messages"]) if state["messages"] else "아직 토론이 없습니다"
    messages_for_llm = [
        ("system", sys),
        ("user", f"주제: {state['topic']}\n\n대화 내용:\n{context}\n\n다음은 무엇인가요?")
    ]
    resp = llm.invoke(messages_for_llm)
    next_step = resp.content.strip().lower()

    # 유효한 응답인지 확인
    if next_step not in ["researcher", "expert", "end"]:
        next_step = "end"

    return {"next_agent": next_step}


def finalize_answer(state: SupervisorState) -> dict:
    """토론에서 최종 답변을 작성합니다."""
    sys = (
        "조사 토론을 명확하고 간결한 최종 답변으로 요약하세요. "
        "핵심 발견 사항과 전문가 통찰력을 포함하세요."
    )
    context = "\n".join(state["messages"])
    messages_for_llm = [
        ("system", sys),
        ("user", f"주제: {state['topic']}\n\n토론 내용:\n{context}\n\n최종 요약을 제공하세요:")
    ]
    resp = llm.invoke(messages_for_llm)
    return {"final_answer": resp.content}

def route_supervisor(state: SupervisorState) -> str:
    """슈퍼바이저의 결정에 따라 라우팅합니다."""
    next_agent = state.get("next_agent", "researcher")
    if next_agent == "end":
        return "finalize"
    return next_agent

supervisor_graph = StateGraph(SupervisorState)

supervisor_graph.add_node("supervisor", supervisor_agent)
supervisor_graph.add_node("researcher", researcher_agent)
supervisor_graph.add_node("expert", expert_agent)
supervisor_graph.add_node("finalize", finalize_answer)

supervisor_graph.add_edge(START, "supervisor")

supervisor_graph.add_conditional_edges(
    "supervisor",
    route_supervisor,
    {
        "researcher": "researcher",
        "expert": "expert",
        "finalize": "finalize"
    }
)

supervisor_graph.add_edge("researcher", "supervisor")
supervisor_graph.add_edge("expert", "supervisor")
supervisor_graph.add_edge("finalize", END)
supervisor_agent_graph = supervisor_graph.compile(debug=True)

topic = "AI 에이전트를 구축하는 데 LangGraph를 사용하는 주요 이점은 무엇인가요?"

initial_state = {
    "topic": topic,
    "messages": [],
    "next_agent": "",
    "final_answer": ""
}

result = supervisor_agent_graph.invoke(initial_state)

print(f"주제: {topic}\n")
print("\n토론 내용:")
for msg in result["messages"]:
    print(f"\n{msg}\n")
print(f"\n최종 답변:\n{result['final_answer']}")

레퍼런스

2025년 12월 25일 목요일

Kaggle 설치 및 사용방법

연구원에서 kaggle, huggingface CLI 도구를 이용해 모델, 데이터셋 다운로드하려면 국가보안법? 규제로 인해 방화벽을 넘지 못해 에러발생하는 경우가 많다(사용 거의 할 수 없는 상황). 특히, 아래한글(이것때문에 기술발전이 안된다는 페친이 많음) 각종 행정이 많은 연구원 업무 특성상 윈도우 버전을 주로 사용하기 때문에 리눅스에서 하루 종일 연구만한다는 것은 꿈도 못꿀일이다. 이런 이유로, 윈도우에서 시간 날때 틈틈히 개발해야 하므로, CLI 도구 호환성 문제는 많이 발생한다.

언급된 kaggle 은 윈도우 호환성이 낮다. 신기하게도 우분투에서는 매우 잘 실행된다. 많은 삽질을 해 봣으나 윈도우에서 캐글 돌리다가는 해골되기 쉽상... 그래서, 어쩔수 없이 포기하고, 리눅스에서 사용법을 정리해 둔다.

다음과 같은 순서로 설치하면 된다.
 
1. kaggle 웹사이트 방문 후 가입한다.
2. kaggle > account > setting > new API key 로 키값을 얻는다. 
여기서 브라우저 설정에 따라 kaggle.json이 다운안되는 경우가 많다. 이 경우 다음과 같이 이 파일을 직접 작성해야 한다.


이 파일은 다음 명령으로 생성할 수 있다. 터미널에 입력한다.
mkdir -p ~/.kaggle
echo '{"username":"<user name>","key":"<your key>"}' > ~/.kaggle/kaggle.json
chmod 600 ~/.kaggle/kaggle.json

그 결과 다음 형식의 파일이 생성될 것이다.
{"username":"","key":""}

3. 터미널에서 kaggle 을 설치한다. 
pip install kaggle

4. 다음과 같이 경진대회 리스트 실행해 본다.
kaggle competitions list

실행 결과는 다음과 같다.
ref deadline category reward teamCount userHasEntered
https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3 2026-04-15 23:59:00 Featured 2,207,152 Usd 976 False
https://www.kaggle.com/competitions/vesuvius-challenge-surface-detection 2026-02-13 23:59:00 Research 200,000 Usd 446 False
https://www.kaggle.com/competitions/google-tunix-hackathon 2026-01-12 23:59:00 Featured 100,000 Usd 97 False
https://www.kaggle.com/competitions/csiro-biomass 2026-01-28 23:59:00 Research 75,000 Usd 2494 False
https://www.kaggle.com/competitions/recodai-luc-scientific-image-forgery-detection 2026-01-15 23:59:00 Research 55,000 Usd 1029 False

2025년 12월 9일 화요일

Google Antigravity 바이브 코딩 도구 사용기

최근 장안의 화재인 Google Antigravity 바이브 코딩 도구 설치 및 사용기를 나눔한다. 아울러, PDF파일에서 텍스트와 이미지 레이아웃을 분리하고, 이를 JSON 과 이미지로 저장하는 간단한 웹앱을 바이브 코딩으로 개발해 본다. 

설치 및 준비
설치는 매우 간단하다. 다음 링크 방문해 다운로드 후 설치, 실행하면 된다.

vscode 통합 개발환경이므로, 설치 후, vscode 설정 방법대로 파이썬 등 애드인 설치하고, 사용하면 된다. 
파이썬 앤드인 설치 모습(vscode와 동일함)

vscode 우측 하단의 setting을 클릭하면 다음처럼 설정 창이 표시된다. 제미니 포함한 다양한 모델 사요 가능하다.

바이브 코딩하기
본 예에서는 간단히 PDF파일에서 텍스트와 이미지 레이아웃을 분리하고, 이를 JSON 과 이미지로 저장하는 간단한 웹앱을 개발해 보도록 한다. 우선, vscode 바이브 도구 설정처럼 LLM 모델을 설정하고, 다음과 같이 프롬프트를 입력한다. 

파이썬으로 주어진 PDF파일을 업로드하면, 여기서 layout, text, image를 분리해서 이를 각 페이지별로 json으로 저장하는 파서 서비스를 개발해. 웹 기반 동작해야 함. 안정적이고 유명한 라이브러리만 사용해.

그럼, 다음과 같이 PRD.md 파일을 우선 생성한다. 


이 파일대로 프로젝트 개발하라 요청한다. 다음은 그 결과이다.

실행해본다. 그리고 웹 접속하면 다음 웹앱이 정상동작될 것이다.

적당한 PDF파일로 테스트해본다.


마무리

지금까지 간단하게 구글 안티그레비티 바이브 코딩 도구를 사용하고, 웹 개발 후, 테스트해보았다. 기존 바이브 도구만큼이나 잘 동작하고, 깔끔하게 실행된다. 참고로, 구글 제미나이 프로 버전을 사용한다면 토큰 제한 그리 신경쓰지 않고 활용 가능하다.

2025년 12월 5일 금요일

독일 뮌헨 공과대학교(TUM) 세계 최대 규모 오픈소스 3D 건물 지도 데이터셋 글로벌 빌딩 아틀라스 기술 개발 이야기

이 글은 독일 뮌헨 공과대학교(TUM) 연구팀이 개발하여 공개한 세계 최대 규모의 3D 건물 지도 데이터셋인 글로벌 빌딩 아틀라스(Global Building Atlas) 프로젝트에 대해 설명한다. 특히, 인공지능과 위성 영상 분석 기술을 결합하여 전 세계에 존재하는 건물을 3차원 모델로 구현한 방법을 기술적 관점에서 이야기나눈다.


이 결과는 오픈소스로 공개되었으며, 기존에 가장 방대하다고 알려진 데이터셋이 포함하던 약 17억 개의 건물 수치를 대폭 상회하는 규모로 개발되었다. 그동안 디지털 지도 데이터에서 소외되었던 아프리카, 남미, 아시아의 농촌 지역 건물들까지 정밀하게 포착해냈다는 점에서 기술적 진보를 보여준다.

개발과정
지도의 기반이 된 데이터는 주로 2019년에 촬영된 플래닛스코프(PlanetScope) 위성 이미지를 활용하였으며, 연구팀은 이를 통해 각 건물의 2D 바닥 면적뿐만 아니라 높이 정보까지 정밀하게 추출했다. 이 지도가 제공하는 높이 데이터의 해상도는 3x3미터 수준으로, 기존의 글로벌 건물 높이 데이터셋들이 주로 90미터 해상도에 그쳤던 것과 비교하면 약 30배 이상 정밀도가 향상된 수치이다. 제공되는 데이터는 건물의 대략적인 형태와 높이를 단순화하여 표현하는 LoD1(Level of Detail 1) 수준의 3D 모델 형식을 따르고 있어, 전 지구적 규모의 방대한 데이터를 다루면서도 활용성을 확보했다.

이 연구는 기존 데이터셋이 가진 커버리지의 한계와 3D 정보의 부재를 해결하기 위해 진행되었으며, 전 세계 약 27억 5천만 개의 건물을 포함하는 방대한 규모의 데이터를 구축하였다. 이는 기존의 가장 포괄적인 데이터베이스보다 10억 개 이상 많은 수치로, 그동안 데이터상에서 누락되었던 전 세계 건물의 약 40% 이상을 메우는 성과이다.

연구팀은 이 데이터셋 구축을 위해 플래닛스코프(PlanetScope) 위성 이미지만을 사용하는 머신러닝 기반 파이프라인을 개발했다.  이 과정은 크게 건물 폴리곤 생성과 높이 추정의 두 단계로 나뉘며, 기존의 오픈소스 건물 데이터(OpenStreetMap, Google, Microsoft 등)와 자체 생성한 데이터를 '품질 기반 융합 전략'을 통해 결합하여 데이터의 완성도를 극대화했다. 이를 통해 완성된 'GBA-Height'는 3x3미터의 공간 해상도를 제공하는데, 이는 기존 글로벌 제품들이 제공하던 90미터 해상도보다 약 30배 더 정밀한 수준이며 이를 통해 지역 및 전 지구 규모에서 신뢰할 수 있는 건물 부피 분석이 가능해졌다.

또한 연구팀은 건물 높이 정보를 포함한 'GBA-LoD1' 모델을 생성하여 약 26억 8천만 건의 건물 인스턴스를 구현했으며, 이는 전체의 97%에 달하는 높은 완성도를 보인다.  높이 추정의 정확도를 나타내는 RMSE(평균제곱근오차)는 대륙별로 1.5미터에서 8.9미터 사이로 나타났으며, 특히 오세아니아와 유럽에서 높은 정확도를 보였다. 데이터 분석 결과, 아시아가 건물 수와 총 부피 면에서 압도적인 비중을 차지하는 반면, 아프리카는 건물 수는 많으나 총 부피가 작아 소규모 또는 비공식 건물이 다수 분포함을 시사했다. 
공개된 GlobalBuildingAtlas LoD1 웹 서비스(선릉역, 뉴욕 근처 생성된 3D건물모델)

AI 모델 개발 접근법
인공지능 모델 개발 및 활용 관점에서 본 GlobalBuildingAtlas(GBA) 프로젝트는 3미터 해상도의 단일 시점(Monocular) 위성 영상인 PlanetScope 데이터를 입력으로 받아 전 지구적 규모의 3D 건물 모델을 생성하는 파이프라인을 구축했다는 점에서 기술적 의미가 있다. 전체 시스템은 크게 2D 건물 폴리곤 생성을 위한 의미론적 분할(Semantic Segmentation) 네트워크와 3D 높이 추정을 위한 단안 높이 추정(Monocular Height Estimation) 네트워크로 이원화되어 설계되었다.

2D 건물 폴리곤 생성 모델의 경우, 연구팀은 UPerNet(Unified Perceptual Parsing Network) 아키텍처를 기반으로 하되 백본(Backbone)으로 ConvNeXt-Tiny를 사용했다.  모델의 성능을 높이기 위해 '추출(Extraction)'과 '정규화(Regularization)'라는 두 단계의 네트워크를 직렬로 구성한 점이 특징이다. 첫 번째 네트워크가 위성 영상에서 1차적인 이진 마스크를 생성하면, 동일한 아키텍처를 가진 두 번째 정규화 네트워크가 이를 입력받아 노이즈를 제거하고 건물 경계를 다듬는다. 특히 정규화 네트워크 학습 시에는 깨끗한 폴리곤 마스크에 인위적인 노이즈를 주입한 것을 입력 데이터로 사용하여, 모델이 거친 마스크를 정제된 형태로 복원하는 일종의 디노이징(Denoising) 기능을 수행하도록 훈련시켰다.

3D 높이 추정 모델은 HTC-DC Net(Hybrid Transformer-CNN with Dynamic Classification)을 채택했다. 이는 CNN 계열인 EfficientNet-B5를 백본으로 사용하여 이미지의 특징을 추출하고, 비전 트랜스포머(ViT) 인코더를 결합하여 지역적 특징과 전역적 특징 간의 관계를 학습하는 하이브리드 구조이다. 높이 예측 방식으로는 단순한 회귀(Regression) 대신 '분류-회귀(Classification-Regression)' 패러다임을 적용했다. 이는 높이 범위를 먼저 구간별로 분류(Classification)한 뒤, 해당 구간 내에서 미세 값을 회귀로 조정하는 방식으로, 이를 통해 예측의 안정성을 높였다. 학습 데이터로는 전 세계 168개 도시의 항공 LiDAR 데이터에서 추출한 정규화된 디지털 표면 모델(nDSM)을 정답 레이블(Ground Truth)로 활용했다.

추론(Inference) 및 배포 단계에서는 모델 예측의 불확실성(Uncertainty)을 정량화하기 위해 테스트 시간 증강(TTA, Test Time Augmentation) 기법을 도입했다. 대용량 위성 영상을 처리할 때 슬라이딩 윈도우 방식을 적용하여 겹치는 영역에 대해 픽셀당 최대 4번의 예측을 수행하고, 이 결과값들의 분산을 계산하여 데이터의 신뢰도 지표로 삼았다. 이러한 딥러닝 파이프라인은 기존 오픈소스 데이터가 커버하지 못하는 지역의 데이터를 생성하는 데 핵심적인 역할을 했으나, 아프리카 등 학습 데이터(LiDAR)가 전무한 지역에 대해서는 도메인 적응(Domain Adaptation)의 한계가 존재함을 명시하고 있다.

기술 연구 및 개발 의미
이 연구는 단순한 데이터 구축을 넘어 유엔의 지속가능발전목표(SDG) 11번, 특히 토지 소비율과 인구 증가율의 비율을 모니터링하는 데 있어 단순 면적보다 '건물 부피' 기반 지표가 도시의 개발 상태와 인구 밀도를 더 정확하게 반영함을 입증했다. 1인당 건축 부피와 GDP의 상관관계를 분석한 결과, 부피 기반 지표가 경제 발전 수준을 더 잘 설명하는 것으로 나타났다. 

상관관계 분석 결과

이러한 고해상도 3D 데이터의 공개는 도시 계획, 재난 위험 관리, 기후 변화 대응 연구 분야에 즉각적인 활용이 가능하다. 구체적으로는 홍수나 지진 발생 시 피해 규모를 건물의 높이와 부피에 기반해 정확하게 시뮬레이션하거나, 도시의 건물 밀도와 부피를 분석하여 에너지 소비 효율을 계산하고 인구 과밀 지역의 주거 환경을 파악하는 기초 자료로 쓰일 수 있다. 해당 연구의 방법론과 데이터셋 구축 과정에 대한 상세한 내용은 과학 저널인 Earth System Science Data에 게재되어 학술적 검증을 마쳤으며, 연구팀은 이 데이터를 오픈 소스로 공개하여 전 세계 연구자와 정책 입안자들이 자유롭게 활용할 수 있도록 했다.

마무리
논문의 주 저자들을 보면 알겠지만, 모두 중국인이다(요즘 놀랍지도 않은...). 논문은 매우 기술적이고, AI 사용 접근 방법은 똑똑하다. 글로벌 연구 분야에서 이런 상황들이 최근 몇 년 사이 크게 많아지고 있다. 이들이 접근한 방법들을 보고 있자면 여러가지 생각이 든다. 사실, 국내 연구 학계(산학연 포함)에서 이렇게 대규모의 데이터셋 수집, 구축, 기술 개발, 성능 분석, 연구 과정의 투명한? 오픈소스 공개, 여러 저자들 간의 협력적 연구를 통한 시너지 효과를 발생하는 경우는 매우 드물다. 좋은 연구 결과를 만든 것은 연구자들의 열정과 노력도 중요하겠지만 연구 핵심기술 개발에 대한 선택과 집중이 가능한 연구 환경이 전제 되어야 한다. 이런 환경의 연구기관 인프라는 참 부럽다는 생각이다.

레퍼런스

2025년 11월 29일 토요일

그래프 구조 지원 FalkorDB와 LLM을 활용한 BIM AI 에이전트 개발 방법

이 글은 건설 인프라 분야에서 정보 교환 시 사용되는 BIM(Building Information Modeling) 산업표준인 IFC(Industry Foundation Classes) 기반 AI 에이전트 개발 과정을 설명한다. 

이 글에서는 IFC 포맷의 BIM 데이터를 FalkorDB 그래프 데이터베이스로 변환하고, 로컬 LLM(Ollama)을 연동하여 온톨로지 모델 자연어 질의가 가능한 AI 에이전트를 구축하는 전체 과정을 기술한다. 또한, 도커(Docker) 기반의 데이터베이스 서버 구성부터 Python 의존성 설치, 데이터 적재 및 애플리케이션 실행 방법을 단계별로 정리한다. FalkorDB는 neo4j에 비해 라이센스 정책이 유연하고, 설치부터 사용이 가벼운 장점이 있다. 참고로 neo4j 기반 그래프 RAG 개발은 그래프 데이터베이스 Neo4J 기반 데이터 질의 서비스 개발하기 글을 참고한다. 

본 글의 모든 실행코드는 다음 github를 참고한다.
코드 clone을 위해 다음 명령을 터미널에서 실행한다. 
git clone https://github.com/mac999/LLM-RAG-Agent-Tutorial.git

개발 환경 및 전제 조건
본 시스템은 온프레미스 환경에서의 실행을 가정하며, 다음 도구, 컴포넌트들을 필요로 한다.
  • Docker: 그래프 데이터베이스(FalkorDB) 실행
  • Python 3.12+: 데이터 변환 및 에이전트 로직 수행
  • Ollama: 로컬 LLM 추론 서버
  • 하드웨어: LLM 구동을 위한 적정 수준의 GPU(NVIDIA VRAM 6GB 이상 권장)
사전 지식
온톨로지 지식 그래프 구조와 Cypher Query에 대한 사전 지식이 있다면, 아래 실습을 이해하기 용이하다. 상세한 내용은 다음을 참고한다.

데이터베이스 서버 구축 (FalkorDB)
FalkorDB는 Redis API 호환 고성능 그래프 데이터베이스다. 

다음과 같이 터미널에서 Docker를 실행하면, FalkorDB 서버가 구동될 것이다. 
docker run -p 6379:6379 -p 3000:3000 -it --rm -v ./data:/var/lib/falkordb/data -e ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef falkordb/falkordb

상세 옵션은 다음과 같다.
-p 6379:6379: FalkorDB(Redis 프로토콜) 접속 포트 바인딩. Python 클라이언트가 이 포트로 통신한다.
-p 3000:3000: (옵션) FalkorDB 시각화 도구 등을 위한 포트 바인딩.
-it --rm: 대화형 모드로 실행하며, 컨테이너 종료 시 자동 삭제.
-v ./data:/var/lib/falkordb/data: 호스트의 ./data 디렉토리를 컨테이너 내 데이터 저장소로 마운트하여 데이터 영속성(Persistence)을 보장한다.
ENCRYPTION_KEY: 이 키는 브라우저가 여러분의 데이터베이스 접속 정보(호스트, 포트, 아이디, 비밀번호 등)를 서버 측에 저장할 때 암호화하기 위해 사용. 로그인을 하면 해당 정보가 .data/api_tokens.json 같은 파일에 저장되는데, 이때 평문이 아닌 이 키로 암호화해서 저장. 반드시 64자. 16진수(Hexadecimal)만 허용 (최근 버전업 기능) 

FalkorDB Browser (http://localhost:3000) 에 접속해 로그인한다. 참고로 로그인 user name은 default 혹은 빈칸 입력, pwd는 없다(나중에 설정 가능). 데쉬보드가 보이면, 좌측 database 추가 [+] 메뉴 클릭 후, bim 이름으로 데이터베이스를 다음 그림처럼 하나 만든다. 

데이터베이스 생성
생성 후 모습

패키지 및 모델 설치
이제 IFC 파싱, 그래프 DB 연결, LLM 체인 구성을 위한 라이브러리를 pip로 터미널에서 설치한다.
pip install Plaintext falkordb langchain langchain-ollama langchain-core ifcopenshell python-dotenv streamlit

자연어를 Cypher 쿼리로 변환(Text-to-Cypher)하는 작업에는 코드 생성 능력이 뛰어난 모델이 필요하다. 본 프로젝트에서는 qwen2.5-coder:7b 모델을 사용한다.

Ollama 설치 후 아래 명령어 실행한다
ollama pull qwen2.5-coder:7b

데이터베이스 연결 정보 및 그래프 네임스페이스 설정을 위해 프로젝트 루트 폴더에 .env 파일을 생성한다.
FALKORDB_HOST=localhost
FALKORDB_PORT=6379
FALKORDB_GRAPH=bim
FALKORDB_USERNAME=default
FALKORDB_PASSWORD=

데이터 적재 (ETL Process)
BIM 데이터(.ifc)를 그래프 구조(노드 및 엣지)로 변환하여 FalkorDB에 적재하는 과정이다.
  1. ifcopenshell을 이용해 IFC 엔티티 파싱.
  2. src.falkordb_graph_converter 모듈이 엔티티를 노드로, 관계(포함, 집합 등)로 구성된 트리플 레이블(객체-관계-행동) 속성그래프(Labeled Property Graph, LPG) 형식으로 변환.
  3. 속성(Property Set) 정보를 JSON 형태로 직렬화하여 노드에 저장.
이 알고리즘이 구현된 다음 소스 파일을 실행한다. 이 파일은 하위 input 폴더 내의 IFC 파일 자동 감지해 앞에서 설정한 .env 의 bim database로 속성정보를 그래프로 변환해 저장한다. 

다음을 실행해 본다.
python import_ifc_to_falkordb.py
실행 결과

그 결과 FalkorDB 에서 질의문 실행해 확인해 보자.

다음과 같이 파싱된 IFC 데이터셋이 그래프DB로 구축된 것을 확인할 수 있다. 

CLI(Command Line Interface) 기반 에이전트 실행
CLI 기반 웹 에이전트를 다음 명령어로 실행한다.
python BIM_graph_agent_falkordb.py

이 모듈은 다음과 같은 방식으로 실행된다.
  1. 질의 입력: 사용자 자연어 질문 수신.
  2. Cypher 변환: LLM(qwen2.5-coder)이 스키마 정보를 바탕으로 질문을 Cypher 쿼리로 변환.
  3. 쿼리 실행: FalkorDB 엔진이 그래프 탐색 수행 후 JSON 결과 반환.
다음과 같이 질문을 해본다.
What IFC files are loaded?
List all properties of space A204
List all elements contained in A204 space
How many IfcWallStandardCase?

결과는 다음과 같다. 환각이 심한 약어, 숫자와 관련된 질의 결과도 정확히 정보를 얻어오는 것을 확인할 수 있다.
실행 결과

각 질의는 LLM에 의해 Cypher Query언어로 변환되어, FalkorDB에 전달되어 결과가 리턴된다. 이를 파싱해 보여주는 파이프라인이 수행된다. 실제 질의언어는 FalkorDB 도커 로그에서 확인할 수 있다.
FalkorDB 도커 로그

웹 기반 에이전트 실행
데이터 적재가 완료되면 Streamlit 기반의 웹 인터페이스를 구동하여 질의응답 시스템을 활성화한다.
  1. 질의 입력: 사용자 자연어 질문 수신.
  2. Cypher 변환: LLM(qwen2.5-coder)이 스키마 정보를 바탕으로 질문을 Cypher 쿼리로 변환.
  3. 쿼리 실행: FalkorDB 엔진이 그래프 탐색 수행 후 JSON 결과 반환.
  4. 답변 생성: LLM이 JSON 결과를 해석하여 자연어 답변 생성.
이제 터미널에서 다음과 같이 명령 실행해본다. 
streamlit run BIM_graph_agent_web_falkordb.py


질의 결과는 다음과 같다.
BIM 기반 AI 에이전트 실행 결과

결론
위 과정을 통해 구축된 시스템은 복잡한 BIM 데이터의 위상학적 관계를 그래프로 표현하고, 별도의 쿼리 언어 학습 없이 자연어만으로 온톨로지 건물 모델 정보를 조회할 수 있는 환경을 제공한다. FalkorDB의 빠른 인덱싱, 로컬 LLM의 보안성, vLLM과 같은 캐쉬 도구를 잘 활용하면 실무에 적용 가능한 수준의 응답 속도와 데이터 프라이버시를 확보할 수 있다. 

단, 온톨로지 모델 구조인 그래프 형식 지식을 적절히 추출할 수 있는 방식은 여러가지가 될 수 있다. 에이전트로 연결하기 위해서는 적절한 템플릿, 데이터 파싱, RAG 및 데이터처리 방식 및 적절한 펑션콜(function call) 모델이 필요하고, 기능과 속도 간 트레이드오프도 고려해야 한다.