티스토리 뷰
작성일: 2026년 1월 9일
카테고리: GraphRAG, Knowledge Graph, LLM
키워드: GraphRAG, NL2SPARQL, 하이브리드 검색, Graph-aware Prompting
시리즈: 온톨로지 + AI 에이전트: 세무 컨설팅 시스템 아키텍처 (16부/총 20부)
대상 독자: 온톨로지에 입문하는 시니어 개발자
요약
15부에서 SHACL로 세무 분석 규칙을 정의했다. 이제 Knowledge Graph의 구조화된 지식을 LLM의 자연어 처리 능력과 결합한다. 이 조합을 GraphRAG라 부른다. 사용자는 "A노무법인의 부채비율이 업계 평균보다 높나요?"라고 자연어로 질문하고, 시스템은 SPARQL로 정확한 데이터를 조회한 후 LLM이 해석을 덧붙인다.
핵심 질문
구조화된 지식을 LLM 컨텍스트로 어떻게 활용하는가?
RAG(Retrieval Augmented Generation)는 외부 문서를 검색해서 LLM에 제공하는 기법이다. 그런데 일반적인 RAG는 비정형 텍스트에서 유사한 청크를 찾는다. GraphRAG는 여기서 한 걸음 더 나아간다.
일반 RAG: 문서 → 벡터화 → 유사도 검색 → LLM
GraphRAG: 지식그래프 → SPARQL 쿼리 → 정확한 데이터 + 관계 → LLMKnowledge Graph의 장점은 정확성과 관계 탐색이다. "A노무법인의 부채비율"을 묻는다면, 벡터 검색으로 "부채비율이 높은 회사들..."이라는 문서를 찾는 것보다, SPARQL로 fin:debtToEquityRatio를 직접 조회하는 것이 정확하다.
이전 내용 복습
- 11부: 벡터 기반 RAG (문서 검색 + LLM 응답)
- 15부: SHACL로 세무 분석 규칙 정의
11부의 RAG는 문서를 벡터로 변환하여 유사도 검색을 수행했다. 하지만 구조화된 재무 데이터에는 정확한 쿼리가 더 효과적이다.
GraphRAG 아키텍처
graph TB
subgraph Input["사용자 입력"]
Q["자연어 질문"]
end
subgraph Routing["Intent Routing"]
IR["질문 분류기"]
KG["KG 쿼리 필요?"]
RAG["문서 검색 필요?"]
BOTH["둘 다 필요?"]
end
subgraph KGPath["Knowledge Graph 경로"]
NL2S["NL2SPARQL"]
SPARQL["SPARQL 실행"]
Result["구조화된 결과"]
end
subgraph RAGPath["Document RAG 경로"]
VEC["벡터 검색"]
DOC["관련 문서"]
end
subgraph Fusion["컨텍스트 융합"]
CTX["Graph + Document\n통합 컨텍스트"]
end
subgraph LLM["LLM 응답 생성"]
GAP["Graph-aware Prompting"]
GEN["응답 생성"]
VAL["응답 검증"]
end
Q --> IR
IR --> KG
IR --> RAG
IR --> BOTH
KG --> NL2S
NL2S --> SPARQL
SPARQL --> Result
RAG --> VEC
VEC --> DOC
Result --> CTX
DOC --> CTX
CTX --> GAP
GAP --> GEN
GEN --> VAL
style NL2S stroke:#2563eb,stroke-width:2px
style CTX stroke:#16a34a,stroke-width:2px
style GAP stroke:#ea580c,stroke-width:2px
질문 유형 분류 (Intent Routing)
모든 질문이 Knowledge Graph를 필요로 하지 않는다. 먼저 질문의 성격을 파악해야 한다.
질문 유형별 처리 전략
| 질문 유형 | 예시 | 처리 경로 |
|---|---|---|
| 수치 조회 | "부채비율이 얼마인가요?" | KG만 |
| 관계 탐색 | "계열사가 있나요?" | KG만 |
| 규정 해석 | "접대비 한도 규정은?" | Document만 |
| 복합 분석 | "R&D 세액공제 받을 수 있나요?" | KG + Document |
| 비교 분석 | "업계 평균 대비 어떤가요?" | KG + Document |
라우팅 판단 기준
수치/비율 관련 키워드 → KG 쿼리 필요
"얼마", "몇 퍼센트", "비율", "금액", "매출"
관계 탐색 키워드 → KG 쿼리 필요
"계열사", "관계사", "모회사", "자회사"
법령/규정 키워드 → Document 검색 필요
"규정", "법", "조문", "요건", "~인 경우"
분석/판단 키워드 → 둘 다 필요
"~할 수 있나요", "~에 해당하나요", "비교", "검토"NL2SPARQL: 자연어를 SPARQL로 변환
변환 전략
자연어 질문을 SPARQL로 변환하는 방법은 크게 세 가지다.
| 전략 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| 템플릿 기반 | 정확함, 빠름 | 유연성 부족 | 정형화된 질문 |
| LLM 직접 생성 | 유연함 | 오류 가능성 | 복잡한 질문 |
| 하이브리드 | 균형 잡힘 | 복잡도 증가 | 실서비스 |
템플릿 기반 NL2SPARQL
정형화된 질문에는 템플릿이 효과적이다.
# 질문 패턴: "{회사}의 부채비율"
# 매칭: 정규식 "(.+)의 부채비율"
PREFIX fin: <http://example.org/financial/>
PREFIX acc: <http://example.org/account/>
PREFIX tax: <http://example.org/tax/>
SELECT ?debtRatio ?totalLiabilities ?totalEquity
WHERE {
?company a tax:Company ;
tax:corporateName "{회사명}" ;
fin:hasBalanceSheet ?bs .
?bs acc:totalLiabilities ?totalLiabilities ;
acc:totalEquity ?totalEquity .
OPTIONAL {
?bs fin:debtToEquityRatio ?debtRatio .
}
# 추론된 값이 없으면 계산
BIND(IF(BOUND(?debtRatio), ?debtRatio,
?totalLiabilities * 100.0 / ?totalEquity) AS ?debtRatio)
}
온톨로지 스키마 활용
LLM이 SPARQL을 생성할 때, 온톨로지 스키마를 컨텍스트로 제공한다. 그래야 존재하는 클래스와 프로퍼티만 사용한다.
## 온톨로지 스키마 (요약)
### 주요 클래스
- tax:Company - 기업
- fin:BalanceSheet - 재무상태표
- fin:IncomeStatement - 손익계산서
- tax:TaxReturn - 세무신고서
### 주요 속성 (DataProperty)
- tax:corporateName - 회사명 (string)
- acc:totalAssets - 자산총계 (decimal)
- acc:totalLiabilities - 부채총계 (decimal)
- acc:totalEquity - 자본총계 (decimal)
- acc:revenue - 매출액 (decimal)
- fin:debtToEquityRatio - 부채비율 (decimal)
### 주요 관계 (ObjectProperty)
- fin:hasBalanceSheet - 재무상태표 보유
- fin:hasIncomeStatement - 손익계산서 보유
- tax:hasSubsidiary - 자회사 보유
- tax:isAffiliatedWith - 계열 관계SPARQL 검증 및 수정
LLM이 생성한 SPARQL은 실행 전에 검증이 필요하다.
검증 체크리스트:
1. 문법 오류 (Syntax Error)
2. 존재하지 않는 PREFIX
3. 존재하지 않는 클래스/프로퍼티
4. 무한 결과 방지 (LIMIT 없음)
5. 보안 위험 (DELETE/INSERT 등)하이브리드 검색: Vector + Graph
왜 하이브리드인가?
| 검색 방식 | 잘하는 것 | 못하는 것 |
|---|---|---|
| Vector | 의미적 유사성, 애매한 질문 | 정확한 수치, 관계 탐색 |
| Graph (SPARQL) | 정확한 수치, 명시적 관계 | 문맥 이해, 동의어 처리 |
"부채비율이 높은 회사"라는 질문:
- Vector 검색: "재무 구조가 불안정한", "차입금 비중이 큰" 등 유사 표현 문서 검색
- Graph 쿼리:
FILTER(?debtRatio > 200)정확히 200% 초과 회사 조회
두 방식을 결합하면 정확성 + 유연성을 모두 확보한다.
검색 결과 융합 (RRF)
두 검색 결과를 통합할 때 Reciprocal Rank Fusion을 사용한다.
RRF 점수 = 1 / (k + rank_vector) + 1 / (k + rank_graph)
- k: 상수 (보통 60)
- rank_vector: 벡터 검색 순위
- rank_graph: 그래프 검색 순위
양쪽에서 모두 상위권이면 최종 점수가 높아진다.검색 파이프라인 설계
graph LR
Q["사용자 질문"]
subgraph Vector["Vector 검색"]
V1["임베딩 생성"]
V2["유사도 검색"]
end
subgraph Graph["Graph 검색"]
G1["NL2SPARQL"]
G2["SPARQL 실행"]
end
subgraph Fusion["결과 융합"]
R1["RRF 점수 계산"]
R2["중복 제거"]
R3["Top-K 선택"]
end
Q --> V1
Q --> G1
V1 --> V2
G1 --> G2
V2 --> R1
G2 --> R1
R1 --> R2
R2 --> R3
style Fusion stroke:#16a34a,stroke-width:2px
Graph-aware Prompting
일반 RAG vs Graph-aware
일반 RAG는 검색된 텍스트를 그대로 프롬프트에 넣는다. Graph-aware Prompting은 그래프 구조를 활용하여 더 풍부한 컨텍스트를 제공한다.
일반 RAG 프롬프트:
---
관련 문서:
[문서1] 부채비율은 부채를 자본으로 나눈 비율이다...
[문서2] A노무법인의 재무 현황...
---
Graph-aware 프롬프트:
---
[엔티티 정보]
- 회사: A노무법인
- 업종: 서비스업 (노무사 사무실)
- 기업유형: 중소기업
[재무 데이터 (2023)]
- 부채총계: 118,000,000원
- 자본총계: 413,000,000원
- 부채비율: 28.5%
[관계 정보]
- 계열사: 없음
- SHACL 검증: 위험 경고 없음
[관련 규정]
[문서1] 중소기업 세액공제 요건...
---프롬프트 구조화
## 시스템 지시문
당신은 세무 전문가입니다. 아래 정보를 바탕으로 질문에 답변하세요.
출처가 불분명한 정보는 "확인 필요"라고 명시하세요.
## 엔티티 컨텍스트 (Knowledge Graph)
[회사 기본 정보]
{company_info}
[재무 데이터]
{financial_data}
[분석 결과 (SHACL 추론)]
{shacl_inferences}
## 문서 컨텍스트 (Document RAG)
{retrieved_documents}
## 질문
{user_question}
## 응답 형식
1. 핵심 답변 (수치 기반)
2. 근거 설명
3. 권고 사항 (있을 경우)엔티티 컨텍스트 구성
Knowledge Graph에서 질문과 관련된 엔티티를 추출하여 구조화한다.
# 예: "A노무법인의 세액공제 적격 여부" 질문 시
# 관련 엔티티: 회사, 재무제표, 세액공제 조건
SELECT ?companyName ?industryType ?smeStatus
?revenue ?rndExpense ?employeeCount
?eligibleCredits
WHERE {
?company a tax:Company ;
tax:corporateName "A노무법인" ;
tax:corporateName ?companyName .
OPTIONAL { ?company tax:industryType ?industryType }
OPTIONAL { ?company a tax:SmallMediumEnterprise . BIND("중소기업" AS ?smeStatus) }
OPTIONAL {
?company fin:hasIncomeStatement ?is .
?is acc:revenue ?revenue .
OPTIONAL { ?is acc:rndExpense ?rndExpense }
}
OPTIONAL { ?company tax:employeeCount ?employeeCount }
# 추론된 세액공제 적격성
OPTIONAL {
GRAPH <http://example.org/inferred> {
?company tax:eligibleFor ?eligibleCredits .
}
}
}
응답 검증 (Hallucination 방지)
검증 전략
LLM이 생성한 응답에 포함된 수치와 사실을 Knowledge Graph와 대조한다.
graph TB
Resp["LLM 응답"]
subgraph Extract["정보 추출"]
E1["수치 추출\n예: 부채비율 28.5%"]
E2["사실 추출\n예: 중소기업 해당"]
end
subgraph Verify["KG 대조"]
V1["SPARQL 검증"]
V2["일치 여부 확인"]
end
subgraph Result["결과"]
R1["검증 통과"]
R2["불일치 경고"]
end
Resp --> E1
Resp --> E2
E1 --> V1
E2 --> V1
V1 --> V2
V2 -->|일치| R1
V2 -->|불일치| R2
style V1 stroke:#dc2626,stroke-width:2px
검증 규칙
| 검증 항목 | 방법 | 허용 오차 |
|---|---|---|
| 금액 | KG 데이터와 직접 비교 | 0% |
| 비율 | KG 계산값과 비교 | 0.1% |
| 분류 | KG 클래스 확인 | 정확 일치 |
| 관계 | KG 관계 존재 확인 | 존재 여부 |
불확실성 표현 강제
검증되지 않은 정보에는 불확실성 표현을 강제한다.
검증된 정보:
"A노무법인의 부채비율은 28.5%입니다."
검증되지 않은 정보:
"업계 평균 부채비율은 약 50% 수준으로 추정됩니다." (확인 필요)실제 질의응답 흐름
예시: "A노무법인이 R&D 세액공제를 받을 수 있나요?"
Step 1: Intent Routing
키워드 분석: "R&D 세액공제", "받을 수 있나요"
→ 복합 질문 (KG + Document 모두 필요)Step 2: KG 쿼리
SELECT ?smeStatus ?rndExpense ?industryType ?eligibility
WHERE {
?company tax:corporateName "A노무법인" .
# 중소기업 여부
OPTIONAL {
?company a tax:SmallMediumEnterprise .
BIND("Y" AS ?smeStatus)
}
# R&D 비용
OPTIONAL {
?company fin:hasIncomeStatement ?is .
?is acc:rndExpense ?rndExpense .
}
# 추론된 적격성
OPTIONAL {
GRAPH <http://example.org/inferred> {
?company tax:eligibleFor tax:RnDTaxCredit .
BIND("적격" AS ?eligibility)
}
}
}
결과:
smeStatus: "Y"
rndExpense: NULL (R&D 비용 없음)
eligibility: NULL (적격 아님)Step 3: Document 검색
쿼리: "R&D 세액공제 요건 중소기업"
검색 결과:
[1] 조세특례제한법 제10조 - 연구인력개발비 세액공제
"연구개발비를 지출한 경우 일정 비율을 세액공제..."Step 4: Graph-aware Prompt 구성
## 엔티티 정보
- 회사: A노무법인
- 기업유형: 중소기업
- R&D 비용: 없음
## 관련 규정
[조세특례제한법 제10조]
연구개발비를 지출한 경우 25% 세액공제 가능...
## 질문
A노무법인이 R&D 세액공제를 받을 수 있나요?Step 5: LLM 응답
A노무법인은 현재 R&D 세액공제를 받을 수 없습니다.
[근거]
- 중소기업 여부: 해당 (조건 충족)
- R&D 비용 지출: 없음 (조건 미충족)
R&D 세액공제를 받으려면 연구개발 인력 채용, 연구전담부서 설립,
또는 외부 연구용역 등을 통해 R&D 비용을 지출해야 합니다.
[출처: Knowledge Graph - 재무 데이터, 조세특례제한법 제10조]Step 6: 응답 검증
검증 항목:
- "중소기업 해당" → KG 확인 → 일치
- "R&D 비용 없음" → KG 확인 → 일치
- "25% 세액공제" → 문서 확인 → 일치
검증 결과: 통과성능 최적화
쿼리 캐싱
자주 묻는 질문 패턴의 SPARQL 결과를 캐싱한다.
캐싱 대상:
- 동일 회사의 기본 정보 (TTL: 1일)
- 재무비율 계산 결과 (TTL: 분기)
- 업종별 통계 (TTL: 연간)프리페칭
회사 ID가 확인되면 관련 데이터를 미리 로드한다.
graph LR
Q["회사명 입력"]
P["프리페칭"]
subgraph Prefetch["병렬 로드"]
P1["기본 정보"]
P2["최근 재무제표"]
P3["세액공제 적격성"]
P4["위험 분석 결과"]
end
Q --> P
P --> P1
P --> P2
P --> P3
P --> P4
style P stroke:#2563eb,stroke-width:2px
벡터 RAG vs GraphRAG
| 구분 | 벡터 RAG | GraphRAG |
|---|---|---|
| 데이터 형태 | 비정형 문서 | 구조화된 그래프 |
| 검색 방식 | 유사도 | 정확한 쿼리 |
| 관계 표현 | 암시적 | 명시적 |
| 수치 계산 | 어려움 | SPARQL로 가능 |
| 적합한 경우 | 법령 해석, 사례 검색 | 재무 분석, 지표 계산 |
질문 유형별 적합한 RAG
| 질문 유형 | 적합한 RAG |
|---|---|
| "부채비율은?" | GraphRAG |
| "법인세 규정은?" | 벡터 RAG |
| "부채비율이 높으면 법적으로?" | 하이브리드 |
| "전년 대비 매출 변화는?" | GraphRAG |
| "유사 판례는?" | 벡터 RAG |
핵심 정리
GraphRAG 파이프라인
graph LR
Q["자연어 질문"]
SG["SPARQL 생성"]
V["쿼리 검증"]
E["쿼리 실행"]
C["컨텍스트 구성"]
R["응답 생성"]
A["자연어 답변"]
Q --> SG
SG --> V
V --> E
E --> C
C --> R
R --> A
style SG stroke:#2563eb,stroke-width:2px
style E stroke:#16a34a,stroke-width:2px
style R stroke:#ea580c,stroke-width:2px
NL2SPARQL 전략
| 전략 | 적용 상황 |
|---|---|
| 템플릿 기반 | 정형화된 질문 (부채비율, 매출액 등) |
| LLM 생성 | 복잡한 조건, 집계 질의 |
| 하이브리드 | 템플릿 매칭 실패 시 LLM 폴백 |
프롬프트 구성 원칙
- 엔티티 컨텍스트 우선: KG 데이터를 먼저 제공
- 구조화된 정보: 표 형식으로 수치 정리
- 출처 명시: KG/문서 구분 표시
- 불확실성 표현: 검증 안 된 정보 명시
다음 단계 미리보기
17부: 세무 분석 에이전트 구축
GraphRAG를 활용하는 자율 에이전트를 설계한다:
- ReAct 패턴으로 도구 활용
- 다단계 분석 자동화
- 사용자 피드백 반영
참고 자료
GraphRAG
NL2SPARQL
관련 시리즈
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| 월간 리포트 자동 생성 파이프라인 (0) | 2026.01.09 |
|---|---|
| 세무 분석 에이전트 구축 (0) | 2026.01.09 |
| 세무 분석 규칙 SHACL로 정의하기 (0) | 2026.01.09 |
| 회계 ERP 데이터를 RDF로 변환하기 (0) | 2026.01.09 |
| 에이전트 도구 설계: LLM이 사용하는 도구를 어떻게 만드는가 (1) | 2026.01.09 |
- Total
- Today
- Yesterday
- imprun.dev
- Next.js
- GPT-5.1
- authorization
- frontend
- react
- AGENTS.md
- backend
- Tax Analysis
- Ontology
- AI
- authentication
- Rag
- knowledge graph
- Go
- Claude Opus 4.5
- claude code
- troubleshooting
- Kubernetes
- AI agent
- Tailwind CSS
- CLAUDE.md
- EnvironmentAgnostic
- api gateway
- LLM
- security
- Developer Tools
- SHACL
- architecture
- Development Tools
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
