그래서 Agent에 데이터를 어떻게 입히나요? — 데이터가 작을 때의 온톨로지 설계
GraphDB와 RAG 사이에서 고민하다 도달한 결론. 데이터가 작을 때는 온톨로지를 YAML 문서로 정의해 컨텍스트에 통째로 넣는 것이 더 정직한 선택인 이유와, 언제 다음 단계로 넘어가야 하는지를 정리합니다.
Agent를 만들다 보면 반드시 이 질문에 도달합니다. “모델은 똑똑한데, 우리 도메인은 모른다. 그럼 우리 데이터를 어떻게 입히지?”
저도 여기서 꽤 오래 멈춰 있었습니다. 온톨로지를 만들어야 한다는 건 알겠는데, GraphDB에 넣을지, RAG로 검색하게 할지, 둘을 섞은 GraphRAG로 갈지. 아키텍처 다이어그램을 몇 장 그리고 나서야 더 근본적인 사실이 눈에 들어왔습니다.
데이터가 생각보다 훨씬 작았습니다.
그리고 데이터가 작을 때는, 가장 단순한 방식이 가장 정직하게 동작했습니다. 이 글은 그 과정의 기록입니다.
TL;DR
- 온톨로지는 Agent에게 주는 도메인 지도다. 개념·관계·속성·규칙을 명시적으로 정의한 것.
- 데이터가 작을 때 GraphDB는 운영 비용과 쿼리 생성 실패라는 새 실패 지점을, RAG는 관계 파편화와 검색 누락이라는 새 실패 지점을 만든다.
- 이 규모에서는 온톨로지를 YAML로 정의해 시스템 프롬프트에 통째로 넣는 것이 누락 없고, 고치기 쉽고, 디버깅하기 쉽다.
- 이건 최종 답이 아니라 출발점이다. 컨텍스트 비중·인스턴스 증가·multi-hop 탐색이 신호가 되면 “섹션 조회 도구 → GraphDB/RAG” 순으로 넘어간다.
- 저장소보다 먼저 온톨로지 자체를 잘 설계하라. 잘 정의된 YAML은 나중에 그대로 그래프 스키마가 된다.
온톨로지, Agent에게 주는 도메인 지도
온톨로지는 어떤 도메인에 무엇이 존재하고(개념), 서로 어떻게 연결되며(관계), 각각 어떤 특징을 갖는지(속성)를 명시적으로 정의한 것입니다. 여기에 “이런 조건이면 이렇게 해석한다” 같은 규칙이 붙기도 합니다.
Agent 입장에서 온톨로지는 지도입니다. 사용자가 도메인 용어로 질문하면, Agent는 그 용어가 무엇을 가리키고 무엇과 연결돼 있는지 알아야 제대로 판단할 수 있습니다. LLM은 일반 지식은 풍부하지만, 우리 조직에서 이 용어가 정확히 무엇을 뜻하는지는 모릅니다. 예를 들어 “설비 상태가 이상하다”는 말이 우리 현장에서 어떤 센서의 어떤 경보를 뜻하는지는 모델이 알 방법이 없습니다. 그 간극을 메우는 게 온톨로지입니다.
문제는 이 지도를 어디에, 어떤 형태로 두느냐입니다.
처음에 고민한 선택지들
처음엔 “제대로” 하고 싶었습니다. 그래서 흔히 거론되는 방식들을 하나씩 따져봤습니다.
GraphDB. 온톨로지는 본질적으로 개념과 관계의 네트워크이니, 그래프 데이터베이스가 가장 자연스러워 보입니다. 노드와 엣지로 개념과 관계를 그대로 표현할 수 있고, “A와 연결된 B에 연결된 C” 같은 multi-hop 탐색에 강합니다. Agent가 그래프 쿼리를 생성해 필요한 부분만 조회하는 구조를 상상했습니다.
RAG (벡터 검색). 도메인 문서를 청크로 나눠 임베딩하고, 질문과 유사한 청크를 찾아 컨텍스트에 넣는 방식입니다. 이미 익숙한 패턴이고, 비정형 문서를 다루기에 좋습니다.
GraphRAG. 관계는 그래프로, 설명 텍스트는 벡터로 두고 함께 검색하는 방향입니다.
어느 쪽이든 그럴듯했습니다. 그런데 설계를 구체화할수록 한 가지가 계속 걸렸습니다.
부딪힌 현실: 데이터가 작았다
정리해보니 온톨로지로 정의해야 할 개념과 관계의 규모가, 저장소를 따로 둘 만큼 크지 않았습니다. 그리고 데이터가 작을 때 위의 방식들은 각자 다른 이유로 얻는 것보다 잃는 것이 컸습니다.
GraphDB는 운영 비용이 먼저 옵니다. 데이터베이스를 띄우고, 스키마를 관리하고, 적재 파이프라인을 만들어야 합니다. 여기에 Agent가 그래프 쿼리를 정확하게 생성해야 한다는 부담이 더해집니다. 쿼리 생성이 틀리면 Agent는 틀린 결과 위에서 자신 있게 판단합니다. 전체 데이터가 작다면, 이 인프라와 새로운 실패 지점을 감수할 이유가 약합니다.
RAG는 관계를 잘게 부숩니다. 온톨로지의 핵심은 개념 사이의 관계인데, 청크로 자르는 순간 관계가 여러 조각에 흩어집니다. 유사도 검색은 “질문과 비슷한 조각”을 찾아올 뿐, “이 개념과 연결된 모든 것”을 보장하지 않습니다. 필요한 관계가 담긴 청크가 top-k에서 빠지면, Agent는 지도의 일부가 찢겨나간 채로 판단합니다.
그리고 둘 다 ‘검색’이라는 단계를 추가합니다. 데이터가 작다는 건, 사실 전체를 한 번에 보여줄 수도 있다는 뜻입니다. 그런데 굳이 검색을 거치면 “검색이 놓치는 경우”라는 새로운 오류 지점이 생깁니다. 작은 데이터에서 retrieval은 도움보다 위험이 될 수 있습니다.
정리하면 이렇습니다.
| GraphDB | RAG | 문서 + 컨텍스트 주입 | |
|---|---|---|---|
| 새로 생기는 실패 지점 | 쿼리 생성 오류 | 검색 누락, 관계 파편화 | 거의 없음 |
| 인프라 | DB + 적재 파이프라인 | 임베딩 + 벡터 스토어 | 파일 하나 |
| 온톨로지 수정 비용 | 스키마 마이그레이션, 재적재 | 재청킹, 재임베딩 | 파일 수정 + 커밋 |
| 강점이 살아나는 규모 | 크고 관계가 깊을 때 | 크고 비정형일 때 | 작을 때 |
작을 때의 선택: 온톨로지를 문서로, 컨텍스트에 통째로
그래서 방향을 바꿨습니다. 온톨로지를 구조화된 문서로 정의하고, 컨텍스트에 통째로 넣는 방식입니다.
예를 들어 온톨로지를 YAML로 이렇게 정의할 수 있습니다. (구조를 보여주기 위한 예시입니다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
concepts:
Equipment:
description: "생산 라인에 설치된 설비"
attributes: [equipment_id, line, status]
Sensor:
description: "설비에 부착되어 상태를 측정하는 장치"
attributes: [sensor_id, metric, unit]
Alarm:
description: "센서 값이 임계치를 벗어날 때 발생하는 경보"
attributes: [level, threshold, triggered_at]
relations:
- from: Sensor
to: Equipment
type: attached_to
description: "센서는 하나의 설비에 부착된다"
- from: Alarm
to: Sensor
type: raised_by
description: "경보는 특정 센서의 측정값에서 발생한다"
rules:
- "경보 level이 critical이면 해당 설비의 status를 점검 필요로 해석한다"
그리고 Agent의 시스템 프롬프트를 구성할 때 이 문서를 그대로 넣습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import yaml
from pathlib import Path
ontology = yaml.safe_load(Path("ontology.yaml").read_text(encoding="utf-8"))
SYSTEM_PROMPT = f"""당신은 이 도메인의 데이터를 해석하는 Agent입니다.
아래 온톨로지에 정의된 개념, 관계, 규칙을 기준으로 판단하세요.
온톨로지에 없는 개념을 임의로 만들어내지 마세요.
<ontology>
{yaml.dump(ontology, allow_unicode=True, sort_keys=False)}
</ontology>
"""
몇 가지 작은 디테일이 효과를 키웁니다.
<ontology>태그로 감싸기. 지시문과 참조 데이터의 경계를 분명히 해서, 모델이 온톨로지를 “지켜야 할 기준”으로 읽게 합니다.- “없는 개념을 만들지 마라”는 한 줄. 지도 바깥으로 나가는 환각을 줄이는 가장 값싼 가드레일입니다.
- YAML을 한 번 로드했다가 다시 덤프하기. 파일을 그대로 붙여도 되지만, 이렇게 하면 문법 오류가 프롬프트에 들어가기 전에 터집니다. 온톨로지를 고치다 깨뜨리는 일이 생각보다 잦습니다.
처음엔 이게 너무 단순해서 “이래도 되나?” 싶었습니다. 그런데 데이터가 작다는 조건 아래에서는 오히려 이 방식이 가장 정직하게 동작했습니다.
왜 작을 때는 이 방식이 괜찮은가
누락이 없습니다. 검색을 거치지 않으니 필요한 개념이 빠질 일이 없습니다. Agent는 항상 지도 전체를 봅니다.
관계가 온전히 보존됩니다. 청크로 쪼개지 않으니 개념과 관계가 정의된 그대로 전달됩니다. “센서는 설비에 부착되고, 경보는 센서에서 발생한다”는 연결이 한 문서 안에서 끊기지 않습니다. 그래서 “critical 경보가 난 설비”를 묻는 질문에도 Alarm → Sensor → Equipment를 한 번에 따라갈 수 있습니다. 작은 규모의 multi-hop은 GraphDB 없이도 모델이 해냅니다.
변경이 쉽습니다. 온톨로지를 고치는 건 파일 하나를 수정하는 일입니다. 스키마 마이그레이션도, 재적재도, 재임베딩도 없습니다. Git으로 관리되니 언제 무엇이 바뀌었는지, 그 변경이 Agent 동작을 어떻게 바꿨는지도 PR 단위로 추적됩니다.
디버깅이 쉽습니다. Agent가 이상하게 판단했을 때, 원인을 찾는 범위가 좁습니다. “검색이 잘못 가져왔나? 쿼리가 틀렸나?”를 의심할 필요 없이, 프롬프트에 들어간 온톨로지와 모델의 판단만 보면 됩니다. 평가 글에서 이야기한 것처럼 실패를 층별로 나눠 보는 게 중요한데, 층 자체가 하나 줄어드는 셈입니다.
무엇보다 도메인을 빠르게 검증할 수 있습니다. 초기에는 온톨로지 자체가 계속 바뀝니다. 개념을 합치고, 관계를 고치고, 규칙을 추가합니다. 이 단계에서 무거운 저장소를 먼저 깔아두면, 온톨로지가 바뀔 때마다 인프라까지 같이 흔들립니다. 문서 기반은 이 빠른 반복을 가능하게 합니다.
물론 한계도 분명하다
이 방식이 만능은 아닙니다.
- 컨텍스트 비용. 온톨로지가 커질수록 매 호출마다 들어가는 토큰이 늘고, 비용과 지연도 함께 늘어납니다. 다행히 온톨로지는 매번 동일하므로 프롬프트 캐싱과 궁합이 좋습니다. 캐시는 보통 프롬프트 앞부분(prefix)이 일치할 때 적중하므로, 온톨로지를 시스템 프롬프트 앞쪽에 두고 매번 바뀌는 내용(날짜, 사용자 정보 등)은 뒤로 미루는 게 좋습니다. 토큰 비용 이야기는 멀티에이전트 글에서도 다뤘습니다.
- 긴 컨텍스트의 주의력. 컨텍스트가 길어지면 모델이 중간에 있는 정보를 덜 활용하는 경향이 있습니다(흔히 lost in the middle이라 부릅니다). 문서가 커질수록 “다 넣었으니 다 볼 것”이라는 가정이 약해집니다.
- 스키마와 인스턴스는 다르다. 이 방식이 잘 맞는 건 온톨로지, 즉 개념과 관계의 틀입니다. 그 틀에 담기는 실제 데이터(개별 설비, 개별 경보 기록)가 계속 쌓인다면, 인스턴스까지 통째로 넣는 건 금방 한계에 부딪힙니다. 틀은 컨텍스트에, 인스턴스는 도구로 조회하는 식으로 나누는 게 자연스럽습니다.
언제 다음 단계로 넘어가야 할까
그래서 저는 이 방식을 “최종 답”이 아니라 출발점으로 봅니다. 다음 신호가 보이면 다음 단계를 고민할 때입니다.
- 온톨로지가 컨텍스트에서 차지하는 비중이 부담스러울 만큼 커졌을 때
- 인스턴스 데이터가 계속 늘어나서, 매번 전체를 넣을 수 없게 됐을 때
- 여러 단계를 건너는 관계 탐색이 판단의 핵심이 됐을 때
- 온톨로지가 자주, 여러 사람에 의해 갱신되기 시작했을 때
넘어가는 길도 한 번에 GraphDB로 점프할 필요는 없습니다. 중간에 징검다리가 있습니다. 온톨로지를 섹션별로 나누고, Agent가 필요한 섹션만 도구로 조회하게 하는 방식입니다.
1
2
3
4
5
6
7
8
9
def get_ontology(concept: str) -> dict:
"""개념 하나의 정의와, 그 개념이 등장하는 관계를 함께 돌려준다."""
return {
"concept": ontology["concepts"].get(concept),
"relations": [
r for r in ontology["relations"]
if concept in (r["from"], r["to"])
],
}
시스템 프롬프트에는 개념 이름 목록만 두고, 상세 정의는 이 도구로 꺼내 보게 하는 식입니다. 검색이 아니라 이름으로 정확히 조회하기 때문에 RAG의 누락 문제를 피하면서, 컨텍스트는 필요한 만큼만 씁니다. 관계를 함께 돌려주는 게 핵심인데, 개념만 떼어 주면 결국 RAG처럼 관계가 끊기기 때문입니다. (도구 설계 글의 관점에서 보면, 이 도구의 선택 정확도가 새 평가 대상이 됩니다.)
그다음에야 관계 탐색이 중요하면 GraphDB로, 비정형 문서 검색이 중요하면 RAG로 가면 됩니다. 이때 YAML의 concepts는 노드 레이블로, relations는 엣지 타입으로 거의 그대로 옮겨집니다.
정리
이번 고민에서 얻은 가장 큰 교훈은 이것입니다. 저장소를 고르기 전에, 온톨로지 자체를 먼저 잘 설계하라.
GraphDB냐 RAG냐는 결국 “온톨로지를 어디에 둘 것인가”의 문제입니다. 더 중요한 건 “온톨로지가 무엇인가“입니다. 개념과 관계와 규칙이 잘 정의돼 있다면, 그걸 YAML에 두든 그래프에 두든 옮기는 건 어렵지 않습니다.
반대로 온톨로지가 정리되지 않은 상태에서 저장소부터 도입하면, 복잡한 인프라 위에 흐릿한 지도를 올려놓는 셈이 됩니다.
데이터가 작다면, 작은 방식으로 시작해도 괜찮습니다. 규모가 방식을 정하게 하고, 방식이 규모를 앞서가지 않게 하는 것. 그게 Agent에 데이터를 입히는 첫걸음이었습니다.
📚 시리즈: AI 에이전트, 만들고 평가하기
- Workflow와 Agent, 그리고 하네스 — LLM에게 얼마만큼의 자유를 줄 것인가
- 에이전트의 도구(Tool)와 세 층의 평가 — 선택·실행·결과
- 멀티에이전트의 두 숙제 — 토큰 비용과 안정성, 그리고 평가라는 해법
- ReAct와 Plan-and-Execute를 LangGraph로 직접 구현하며 배운 것
- 그래서 Agent에 데이터를 어떻게 입히나요? — 데이터가 작을 때의 온톨로지 설계 ← 지금 글
- 제조업 AI 에이전트 프로젝트 1차 회고 — 평가, 저장소, 코드 정리에 대한 세 가지 교훈
- AI 에이전트의 예시 데이터는 어떻게 다룰까 — 컨텍스트 엔지니어링부터 파인튜닝 준비까지