본문에 앞서 semantic web 계열 용어와 COA 고유 용어의 최소 정의를 정리했다. 상세한 맥락은 각 절에서 다룬다.
| 용어 | 정의 |
|---|---|
| RDF | 모든 지식을 주어-술어-목적어 3요소(트리플)로 표현하는 W3C 데이터 모델. "클레임#123, 제기자, 딜러A"가 트리플 하나다. |
| Turtle (.ttl) | RDF를 사람이 읽기 쉬운 텍스트로 쓰는 직렬화 포맷. COA가 발행하는 ontology.ttl이 이 형식이다. |
| OWL | RDF 위에서 class, property, 계층, 제약을 정의하는 온톨로지 언어. "모든 Order는 정확히 하나의 Customer를 가진다" 같은 규칙을 기계가 추론할 수 있는 형태로 표현한다. |
| 온톨로지 | 도메인의 개념(class), 속성(property), 관계, 제약을 담은 공식 지식 모델. ERD와 달리 표준 포맷(OWL)으로 존재해 기계가 직접 읽고 추론한다. |
| T-Box / A-Box | 온톨로지 지식의 두 층. T-Box는 개념 정의("Customer라는 class가 있다"), A-Box는 인스턴스 사실("김철수는 Customer다"). COA는 정형 데이터에 대해 T-Box만 만들고 A-Box는 만들지 않는다. |
| SPARQL | RDF 그래프를 조회하는 W3C 표준 질의 언어. SQL의 그래프 대응물이다. |
| R2RML | 관계형 테이블과 RDF 개념의 대응 규칙을 기술하는 W3C 표준 언어. "이 class의 인스턴스는 이 테이블의 행, 이 property는 이 컬럼"이라는 대응표다(예시는 2.3절). |
| OBDA / VKG | Ontology-Based Data Access / Virtual Knowledge Graph. 데이터를 그래프로 복제하지 않고 관계형 DB 위에 온톨로지 뷰를 씌운 뒤, 질의 시점에 SPARQL을 SQL로 번역하는 접근. |
| Ontop | OBDA/VKG를 구현한 오픈소스 번역 엔진. COA의 VKG 컨테이너가 Ontop 5.5.0이다. |
| Reasoner (HermiT) | 온톨로지의 논리적 결함(모순, 만족 불가능한 class, 순환 상속)을 수학적으로 검사하는 추론기. COA의 proposal 검증(온디맨드 실행)에 쓰인다. |
| SHACL | RDF 데이터가 지켜야 할 형태 규칙("모든 class에는 label이 있어야 한다")을 선언하고 검사하는 W3C 표준. |
| GraphRAG | 문서에서 entity와 fact를 추출해 지식 그래프를 구축하고, 벡터 검색과 그래프 탐색으로 맥락을 모아 LLM이 답을 서술하는 기법. COA에서는 Tier 3와 문서 파이프라인이 이 계열이다. |
| Induction (COA) | 승인된 테이블 스키마로부터 온톨로지 초안과 R2RML mapping을 자동 유도하는 COA의 파이프라인. |
| Grounding (COA) | 유도된 class를 표준 온톨로지(Schema.org, FIBO 등)와 embedding 매칭으로 정렬하는 induction의 후속 단계. |
| Governed metric (COA) | 이름, 설명, SQL expression으로 등록되며 등록 시 검증을 거치는 공식 비즈니스 계산 정의. Tier 1의 답변 재료다. |
LLM에 테이블 스키마만 제공하고 자연어 질의를 SQL로 변환하게 하면 세 가지 문제가 반복된다. 첫째, 스키마에는 비즈니스 의미가 없어 LLM이 컬럼의 의미를 추측하다 틀린다. 둘째, "총 매출"과 같은 비즈니스 계산을 질의할 때마다 LLM이 새로운 SQL을 생성하므로 답이 재현되지 않는다. 셋째, AI agent에 DB 접근을 부여할 때 테이블·컬럼 수준의 통제 수단이 없다.
Context Ontology Accelerator(이하 COA)는 데이터와 AI 소비자 사이에 semantic context layer를 두어 이 문제에 대응한다. 승인된 온톨로지가 의미 해석을 고정하고, governed metric이 비즈니스 계산을 고정하며, Cedar 정책과 SQL Firewall이 실행을 통제한다. Apache 2.0 라이선스의 CDK 기반 self-hosted 에셋이며, CloudFormation 스택 16개로 배포된다.
운영은 Scan, Model, Serve 세 단계로 진행된다.
| 단계 | 목적 | 산출물 | 사람의 개입 |
|---|---|---|---|
| Scan | 소스 연결, 스키마 발견, AI 메타데이터 생성 | 검증된 테이블 명세(DataZone asset), Athena 쿼리 경로 | steward가 테이블·컬럼 승인, 메타데이터 교정, PK/FK 지정 |
| Model | 온톨로지 유도, 검증, 발행. metric 등록 | T-Box(OWL) + R2RML mapping + schema.sql, governed metric | induction 트리거, proposal 검토 후 accept |
| Serve | 자연어 질의 응답 | 표, 숫자, 서술형 답변, 실행 trace | 없음(질의만) |
| 특징 | 내용 |
|---|---|
| Virtual Knowledge Graph | 정형 데이터의 인스턴스(A-Box)를 그래프로 복제하지 않는다. 온톨로지 정의(T-Box)와 R2RML mapping(개념과 물리 테이블의 대응 규칙, 2.3절 참조)만 유지하고, 질의 시점에 Ontop 엔진이 SPARQL을 SQL로 번역한다. 데이터는 원본 DB에 남는다. |
| 3-tier fall-through | 질의를 신뢰도 내림차순으로 처리한다. Tier 1은 등록·검증된 metric SQL을 그대로 실행하고, Tier 2는 SQL을 생성하며, Tier 3는 검색된 맥락으로 서술형 답변을 합성한다. 상위 tier가 확신하지 못하면 다음 tier로 넘긴다. |
| 생성·검증·승인의 분리 | LLM이 생성하고(메타데이터, 온톨로지, 질의문), 발행 게이트의 검증은 결정적 도구가 수행하며(HermiT reasoner, SHACL, SQL Firewall), 최종 승인은 사람이 담당한다(scan 리뷰, proposal accept). 생성 단계 내부에 품질 보강용 LLM 상호 평가(grounding reranker, RIGOR 전략의 심판 LLM)가 있지만, 게이트 통과 여부는 결정적 도구와 사람만 결정한다. |
| Provenance 우선순위 | 모든 메타데이터에 출처가 기록된다. 사람이 지정한 값(STEWARD_*)은 DB constraint(DETERMINISTIC)보다, DB constraint는 AI 산출물보다 우선한다. AI 산출물은 서술 메타데이터가 AI_GENERATED, 추론된 PK/FK가 AI_INFERRED로 구분 마킹되며 둘은 같은 최하위 등급이다. 재스캔이 사람의 편집을 덮어쓰지 못한다. |
| 2-layer 권한 | Layer 1(Cedar)은 API 행위를 role 기반으로 허용하고, Layer 2(SQL Firewall)는 실행 시점에 테이블 allowlist와 컬럼 denylist를 강제한다. Layer 2는 platform-admin도 우회할 수 없다. |
| Agent 위임 모델 | MCP agent는 별도 자격증명 없이 사용자의 OIDC 토큰으로만 동작한다. agent가 조회할 수 있는 범위는 위임한 사용자의 grant와 동일하며, 감사 로그에는 사용자 ID가 남는다. |
| 실행 경로 자동 선택 | 단일 소스 질의는 native driver로 직접 실행하고(약 20~50ms), 크로스 소스 질의와 Glue 소스는 Athena federation으로 실행한다(약 500~800ms). 설정이 필요 없다. |
| 카탈로그 이원화 | Glue Data Catalog는 물리 카탈로그(실행용)로 다룬다. 스캔은 읽기만 하고 federation은 쿼리 통로(연결, federated catalog) 등록만 하며, 비즈니스 메타데이터와 리뷰 이력은 DataZone에 별도 기록한다. 고객의 운영 카탈로그를 AI 생성물로 오염시키지 않는다. |
사용자의 질문은 비즈니스 개념으로 구성된다. "우수 딜러", "클레임", "지난달 매출"이 그 어휘다.
데이터는 다른 공간에 있다. 물리 테이블과 컬럼(clm_mst, dlr_cd),
그리고 문서 더미다. 자연어 질의 시스템의 일은 이 두 공간의 간극을 메우는 것이고,
접근 방식들의 차이는 결국 하나의 질문으로 수렴한다.
개념과 데이터의 대응, 곧 "의미"를 무엇으로 표현해서 어디에 보관하는가.
| 접근 | 의미가 존재하는 곳 | 귀결 |
|---|---|---|
| Text-to-SQL | LLM 내부. 질의 순간의 확률적 출력으로만 존재 | 검토·버전·감사 불가, 재현성 없음 |
| Text-to-SPARQL (트리플 스토어 실행) | 온톨로지로 명시화 | 실행을 위해 데이터까지 트리플로 복제해야 함 |
| GraphRAG | 문서에서 통계적으로 추출한 그래프 | 추출물이지 승인된 모델이 아님. 집계 부적합 |
Text-to-SQL은 간극을 질의마다 LLM이 즉석에서 메운다. "우수 딜러"가 어느 테이블의 어느 조건에 해당하는지에 대한 판단이 그 순간의 생성 결과로만 존재하고, 질의가 끝나면 사라진다. 이 판단은 자산이 아니므로 검토할 수 없고, 버전이 없으며, 어제와 오늘이 다를 수 있다. JOIN 경로 추론은 hop이 늘수록 오류가 누적된다. 조직 관점에서 보면 수백 번의 질의는 수백 번의 서로 다른 의미 해석이다.
Text-to-SPARQL은 의미를 온톨로지로 명시화한다는 점에서 방향이 같다. 문제는 실행이다. SPARQL을 실행하려면 인스턴스 데이터가 트리플 스토어에 있어야 하므로 원본 DB를 트리플로 변환해 적재해야 한다. 이중 저장과 동기화 파이프라인, 신선도 문제가 따라오고, 트리플 저장 구조는 대규모 집계에 불리하다. 의미의 명시화라는 이득을 데이터 복제라는 비용으로 지불하는 셈이다.
GraphRAG는 문서 지식에 대한 답이다. entity와 fact를 추출해 그래프를 만들고, 벡터 검색과 그래프 탐색으로 맥락을 모아 LLM이 서술한다. 그러나 이 그래프는 LLM이 추출한 진술의 집합이지 사람이 검토하고 승인한 모델이 아니다. 검색이 조각을 놓치면 답이 틀리고, 틀렸는지 확인할 방법도 없다. "딜러별 클레임 건수" 같은 집계 질문에는 구조적으로 부적합하다. 정형 데이터를 GraphRAG 방식으로 그래프에 물질화하는 선택지도 같은 이유로 배제된다. 동기화 비용에 더해, LLM이 대량 변환에 개입하는 순간 오류 검출이 불가능해진다.
COA의 답은 의미를 일급 자산으로 승격시키는 것이다. 이 선택은 세 가지 판단으로 나뉜다.
첫째, 의미가 자산이어야 거버넌스가 성립한다. "WarrantyClaim은 Dealer가 제기하며 Part를 참조한다"는 해석을 OWL 문서로 명시하면, LLM이 제안하되 reasoner가 검증하고(HermiT, SHACL) 사람이 승인하는 파이프라인을 만들 수 있다. LLM 내부의 잠재적 해석은 리뷰할 수 없지만 온톨로지는 리뷰, 검증, 버전 관리, 감사가 가능하다. Text-to-SQL이 원리적으로 제공할 수 없는 성질이다.
둘째, 의미만 자산화하고 데이터는 복제하지 않는다.
COA는 T-Box(개념 정의)만 생성하고 A-Box(인스턴스)는 만들지 않으며, 둘 사이를 R2RML mapping이 잇는다.
R2RML은 관계형 데이터와 RDF 개념의 대응 규칙을 기술하는 W3C 표준 언어로,
"이 class의 인스턴스는 이 테이블의 행이고, 이 property는 이 컬럼이다"라는 대응표를
기계가 읽을 수 있는 형태로 담는다. 다음 mapping은 claims 테이블의 각 행을
dd:WarrantyClaim의 인스턴스로, created_at 컬럼을
dd:claimDate property로 선언한다.
<#ClaimMap> rr:logicalTable [ rr:tableName "claims" ] ;
rr:subjectMap [ rr:class dd:WarrantyClaim ; rr:template "urn:claim/{id}" ] ;
rr:predicateObjectMap [
rr:predicate dd:claimDate ;
rr:objectMap [ rr:column "created_at" ]
] .
COA에서 이 mapping은 사람이 손으로 쓰지 않는다. induction 단계에서 코드가 스키마로부터 기계적으로 생성하고(4.2절), proposal의 일부로 사람이 승인한다. VKG는 이 원칙의 실행 메커니즘이다. 질의 시점에 Ontop이 SPARQL을 SQL로 번역해 원본에서 조회하므로(packages/vkg), 트리플 스토어 방식의 이중 저장 없이 온톨로지 기반 질의가 성립한다. LLM의 오류 가능 범위도 함께 줄어든다. 수백만 행의 변환이 아니라 사람이 검토 가능한 크기의 T-Box(수십~수백 class)만 LLM이 다루기 때문이다.
셋째, LLM의 역할을 재배치한다. 온톨로지가 있으면 LLM은 닫힌 어휘 안에서 짧은 SPARQL만 쓰면 된다. 노출되는 어휘는 R2RML mapping이 존재하는 class로 제한되고(tier2/ontop/tbox_context.py의 isMapped 게이트), 테이블 선택과 JOIN 경로는 사람이 승인한 mapping이 결정하는 번역 규칙으로 처리된다. 환각이 가능한 구간이 "물리 스키마 전체 위의 SQL 작성"에서 "승인된 개념 위의 의미 질의 작성"으로 좁아지고, 같은 SPARQL은 항상 같은 SQL이 된다.
이 에셋에서 온톨로지의 역할은 Tier 2의 번역 재료에 그치지 않는다. 발행된 온톨로지 하나를 아키텍처의 구성요소들이 서로 다른 방식으로 소비한다.
| 소비자 | 온톨로지의 사용 방식 |
|---|---|
| Tier 2 (VKG) | T-Box가 SPARQL 어휘를, R2RML이 SQL 번역 규칙을 제공한다 |
| Tier 3 (지식 검색) | class·property embedding이 그래프 탐색의 시작점(seed)이 되고, 탐색 자체가 온톨로지 그래프 위에서 이루어진다 |
| Governed metric | 지표 정의가 온톨로지와 같은 Neptune 그래프에 저장된다. 계산이 개념 체계에 부착된다 |
| AI agent (MCP) | describe_schema가 물리 스키마 대신 온톨로지 스키마를 노출한다. REST와 같은 소스를 읽는다 |
| 의미 라우팅 | class·property embedding(OpenSearch)이 자연어 질문과 개념을 매칭한다 |
| 문서 지식 | grounding이 표준 온톨로지(Schema.org, FIBO 등)와의 정렬을 만들고, 문서에서 추출한 lexical graph가 같은 그래프 저장소에서 개념과 만난다 |
온톨로지를 제거하면 이 에셋은 metric 카탈로그, text-to-SQL 도구, 문서 검색기의 서로 접점 없는 묶음으로 남는다. 온톨로지가 있어서 "이 지표는 어느 개념의 계산인지, 이 문서는 어느 개념에 관한 것인지, 이 SQL은 어느 개념을 조회한 것인지"가 하나의 좌표계에서 답해진다. 이 에셋의 이름이 text-to-SQL accelerator가 아니라 Context Ontology Accelerator인 이유이며, semantic context layer라는 규정의 실체다.
| Text-to-SQL | Text-to-SPARQL (트리플 스토어) | GraphRAG | COA (온톨로지 + VKG) | |
|---|---|---|---|---|
| 의미의 형태 | 암묵적(LLM 내부) | 명시적(온톨로지) | 통계적 추출물 | 명시적 + 검증 + 사람 승인 |
| 데이터 위치 | 원본 | 트리플로 복제 | 그래프·인덱스로 적재 | 정형은 원본, 문서만 적재 |
| 집계 정확성 | 생성된 SQL 품질에 종속 | 가능하나 성능 불리 | 부적합(근사) | SQL 실행으로 정확 |
| JOIN 결정 | LLM 추론, hop마다 오류 누적 | 그래프 구조 내장 | 해당 없음 | R2RML 규칙, 결정적 |
| 재현성 | 낮음 | 생성부는 동일 문제 | 낮음 | 번역·실행부는 결정적 |
| 거버넌스(검토·버전·감사) | 불가 | 가능 | 제한적 | 검증·승인·버전이 파이프라인에 내장 |
| 주 대상 | 정형 | 정형 | 비정형 | 정형과 비정형을 tier로 병용 |
온톨로지는 공짜가 아니다. 모델링과 리뷰에 사람의 시간이 들어간다. COA는 induction 자동화와 자동 검증으로 이 비용을 줄이지만, steward와 승인자의 공수는 남는다. 따라서 효용은 질의의 복잡도에 비례해서 나타난다. 단일 테이블 조회라면 온톨로지 없는 text-to-SQL과의 차이가 작고, 다중 hop 조인, class 계층, 다중 소스 통합, agent 접근 통제, 감사 요구가 있을 때 격차가 벌어진다.
DEFAULT_STRATEGY = NL_TO_SQL_FIRST, tier2/strategy.py:46),
VKG는 폴백으로 동작한다. 단순 질의에는 직접 생성이 빠르고 충분하다는 실용적 선택이다.
options.strategy로 VKG 우선(ontop_first)이나 병렬 경쟁(best)으로
전환할 수 있으며, 어떤 질의 유형에서 어느 전략이 우세한지는 워크로드별 실측 대상이다.
| 유형 | 엔진 | discovery 방식 | 서빙 시 쿼리 방식 |
|---|---|---|---|
| JDBC | PostgreSQL, Redshift, MySQL, SQL Server | DB 직접 접속, information_schema 조회 | 직접 driver 또는 Athena federation |
| JDBC (discovery만) | Oracle, Snowflake | 동일 | Athena federation만 (jdbc.py:61 DIRECT_QUERY_ENGINES) |
| Glue Data Catalog | Athena로 쿼리 가능한 모든 것 | Glue GetTables API(메타데이터만) | Athena |
| Documents | S3, 파일 업로드(PDF, DOCX 등) | 별도 파이프라인. 본 가이드 범위 외 | |
SAP, Salesforce 등 애플리케이션 커넥터는 없다. 이런 소스는 추출 파이프라인(Glue connector, AppFlow 등)으로
S3에 내리고 Glue Catalog에 등록한 뒤 GLUE_DATABASE 소스로 연결하는 구성이 표준이다.
Glue 소스는 등록 시점에 이미 Athena로 쿼리 가능한 상태여야 한다.
소스를 등록하면 Step Functions state machine이 Discovery, Federation, Enrichment 세 단계를 순차 실행한다.
상태는 DynamoDB에 기록되며 REGISTERED → SCANNING → ENRICHING → PENDING_REVIEW로 전이한다.
단계 간 데이터 전달은 Step Functions payload가 아니라 공유 저장소를 경유하는데, 저장소는 단계마다 다르다.
Discovery의 산출물(테이블·컬럼 명세)은 DataZone catalog에 asset으로 모이고 Enrichment가 이를 읽어
같은 asset에 덧쓴다. 이 구간이 catalog hub 구조다. 반면 Federation은 DataZone을 읽거나 쓰지 않는다.
입력(발견된 스키마 목록)은 Discovery가 DynamoDB 소스 레코드에 남긴 값이고, 산출물(Glue connection과
federated catalog 참조, queryable 플래그)도 DynamoDB와 Glue/Lake Formation에 기록된다(database/pipeline/federation_handler.py).
Discovery Lambda는 소스 유형에 따라 다르게 동작한다(database/pipeline/discovery_handler.py).
JDBC 소스에는 Secrets Manager에서 자격증명을 받아 DB에 직접 접속해 information_schema와 native catalog를 읽는다.
테이블, 컬럼, 타입, comment, PK/FK constraint를 수집하고, 저카디널리티 문자열 컬럼은
distinct 값을 샘플링한다(COUNT(DISTINCT)로 enum성 판별 후 수집).
Glue 소스에는 데이터에 접근하지 않고 GetTables API로 카탈로그 메타데이터만 읽으며,
enum 샘플링만 Athena를 경유한다(실패해도 스캔은 계속된다).
출력은 테이블당 1개의 DataZone asset이다. 컬럼 메타데이터는 form으로 저장되고,
재스캔 시 asset revision으로 idempotent하게 갱신된다.
DB constraint에서 나온 PK/FK는 DETERMINISTIC provenance로 기록된다.
asset 쓰기는 병렬화되어 있고(기본 10)(database/metadata_writer.py), 테이블 수 상한(MAX_TABLES_PER_SOURCE)을 넘으면 스캔이 조기 실패한다(discovery_handler.py).
Federation Lambda는 서빙 시점의 쿼리 통로를 미리 등록한다(database/pipeline/federation_handler.py). DB에는 접속하지 않으며 AWS 관리 API만 호출한다.
MANAGED_CONNECTION=true, connector Lambda 배포 불필요), Lake Formation에 register_resource(WithFederation=True)로 등록한 뒤 serve runtime role에 SELECT/DESCRIBE를 grant한다. federated catalog는 메타데이터 사본이 아니라 라이브 DB를 가리키는 포인터이며, 쿼리 시점에 Athena가 connection을 통해 원본을 조회한다.이 Lambda를 Discovery와 분리한 이유는 권한 격리다. federated catalog 생성에는 Lake Formation data-lake-admin 권한이 필요한데, 이 권한을 고객 DB에 접속하는 Discovery Lambda가 아닌 단일 목적 함수의 role에만 부여한다. 또한 자격증명 secret을 connector role로 assume하여 읽기 사전 점검을 수행해, 등록은 성공했지만 쿼리 시점에 조용히 실패하는 유형의 장애를 스캔 단계에서 조기 발견한다.
Enrichment는 ECS Fargate task로 실행된다. 테이블 수백 개 규모에서 Bedrock 호출이 Lambda의 15분 제한을 초과하기 때문이다. 원본 DB에는 재접속하지 않고 DataZone의 명세만으로 작업한다. 두 pass로 구성된다(database/enrichment/table_enricher.py, relationship_inferrer.py).
customer_id → customers.id 유형). 각 추론에 confidence를 부여하고, 테이블 250개 초과 시 배치로 나눈다. 데이터 값의 overlap 분석은 수행하지 않는다. 순수하게 이름 기반이므로 무의미한 legacy naming에서는 추론이 약하며, 이 경우 steward의 직접 지정이 해법이다.
DETERMINISTIC 또는 steward 지정 FK는 "재추론 금지" 목록으로 프롬프트에 전달되고,
guard 로직이 AI 결과의 덮어쓰기를 차단한다. Pass 2가 실패해도 Pass 1 결과는 보존된다.
LLM이 형식은 유지한 채 테이블 설명만 누락하는 조용한 실패를 잡기 위해
TablesMissingDescription CloudWatch metric을 별도 발행한다.
결과는 asset revision에 기록되고 소스는 PENDING_REVIEW 상태가 된다. 서술 메타데이터(설명, synonym, 용어, tag)는 AI_GENERATED로, AI가 추론한 PK/FK는 AI_INFERRED로 마킹된다(table_enricher.py:360·377, relationship_inferrer.py:211).
steward는 웹 UI에서 세 종류의 입력을 제공한다.
STEWARD_EDITED로 마킹된다.STEWARD_SPECIFIED로 마킹된다. 실제로 변경된 키만 재스탬프되고 나머지는 기존 provenance(DETERMINISTIC 또는 AI_INFERRED)를 유지한다(api/database_routes.py:935 이하).
provenance 우선순위는 STEWARD_* > DETERMINISTIC > AI_GENERATED·AI_INFERRED로 고정이며,
이후 어떤 재스캔·재추론도 상위 provenance를 덮어쓰지 못한다.
Glue Data Catalog에는 COA가 필요로 하는 구조화 메타데이터(provenance, confidence, 리뷰 상태, synonym)를 담을 자리가 없고, JDBC 소스는 스캔 시점의 Glue에 존재하지 않는다(federation이 나중에 등록하는 것은 메타데이터 사본이 아니라 라이브 DB 포인터다). 또한 Glue Catalog는 Athena와 ETL이 사용하는 운영 인프라이므로 검증 전 AI 생성물을 쓰는 것이 부적절하다. COA는 Glue Catalog에 비즈니스 메타데이터를 쓰지 않고(스캔은 읽기, federation은 쿼리 통로 등록만), 작업 카탈로그로 DataZone을 사용한다. namespace가 DataZone project와 1:1로 대응하므로 테넌트 격리도 이 구조에서 나온다.
ontology-engine은 상시 실행되는 ECS Fargate service다(FastAPI, desiredCount 1). induction 실행, proposal 관리, 온톨로지 카탈로그 CRUD, 서빙용 스키마 응답(DescribeSchema)을 모두 이 서비스가 담당한다. induction과 같은 장시간 작업은 서비스 프로세스 내 background thread로 실행되고 진행 상태는 DynamoDB job 레코드로 추적된다(coa_ontology/main.py, induce_catalog.py). 스캔마다 새로 뜨는 Enrichment task(batch형)와 실행 형태가 다르다.
기본 전략(table_to_ontology)에서 induction의 골격은 코드가 결정적으로 수행한다(induce_catalog.py:215 기본값, inducer/strategies/base.py).
테이블을 owl:Class로, 컬럼을 owl:DatatypeProperty로, FK를 owl:ObjectProperty로 변환하고,
class와 테이블을 잇는 R2RML mapping을 생성한다(소스 라우팅용 coa:datasourceId 주석 포함).
class와 property의 설명·synonym은 새로 생성하지 않고, 3.5절 enrichment가 만들고 steward가 확정한
메타데이터를 이어받는다. 이 경로에서 LLM 판단이 개입하는 지점은 4.3절 grounding의 reranker다.
생성 LLM과 심판 LLM이 협업하는 RIGOR 전략(rigor_ontology)은 API로만 선택할 수 있는 대안이다(inducer/strategies/rigor_ontology.py).
테이블별 온톨로지 조각을 생성 LLM이 제안하고 심판 LLM이 정제하며, FK 위상 순서(참조되는 테이블 먼저)로 진행한다.
단 이 전략은 grounding 옵션을 무시하므로 4.3절의 정렬은 실행되지 않는다.
생성된 class를 grounding pool의 온톨로지와 정렬한다. pool은 induction 요청에서 사용자가 선택한
온톨로지로 한정되며(grounding_ontology_ids), Schema.org, Dublin Core, PROV-O, FIBO 같은
표준 온톨로지는 선택한 경우에만 정렬 대상이 된다. 서버가 표준 온톨로지를 자동으로 추가하지 않으므로
선택이 없으면 모든 class가 신규(novel)로 처리되는데, 이는 오류가 아니라 스코프 선택의 결과다(induce_catalog.py:800-808).
정렬은 class·property의 이름과 설명을 Cohere Embed v4로 embedding하여 후보를 벡터 매칭으로 찾고,
기본 모드(ENHANCED)에서 LLM reranker가 최종 후보를 판정해 rdfs:subClassOf,
skos:*Match 링크를 추가하는 순서로 진행된다.
grounding 인프라 오류(벡터 검색 실패 등)는 job 실패로 처리된다. 오판된 "전부 신규 class"를
조용히 반환하는 대신 실패를 드러내는 fail-loud 설계다.
induction은 proposal을 저장할 때 검증을 실행하지 않는다. 검증은 proposal 생성 후 리뷰어가
웹 UI 또는 POST /proposals/{id}/validate로 실행하는 선택적 비동기 job이고,
결과는 job report로 조회한다(proposals.py, ontology-induction.smithy: ValidateProposal).
검증 자체에는 LLM이 개입하지 않으며 결정적 도구만 사용한다(validation/validators/).
| 검증 | 도구 | 검사 내용 |
|---|---|---|
| Tier 1 (심각도 분류: blocking) | HermiT reasoner(owlready2), pyshacl | 논리 일관성, 만족 불가능 class, subclass 순환, 연결성, SHACL 형태 규칙, XSD datatype 위생 |
| Tier 2 | OntoQA | 구조적 풍부도 지표 |
| Tier 3 | OoPS! 원격 API 등 | 온톨로지 설계 pitfall, 라벨 누락 |
"blocking"은 심각도 분류의 이름일 뿐 시스템이 무언가를 차단하지는 않는다. Tier 1 error는 report에 passed=false로 기록될 뿐이고, 검증을 실행하지 않아도 accept는 가능하다. 발행의 게이트는 사람의 accept 하나이므로, "accept 전에 검증을 실행하고 report를 확인한다"는 규칙은 운영 절차로 정해야 한다.
사람이 proposal을 accept하면 ontology-engine이 세 곳에 기록한다(proposals.py).
ontology.ttl, mappings.r2rml, schema.sql
기록 후 ontology.published 이벤트를 EventBridge에 발행하고, VKG reload Lambda가 이를 받아
해당 namespace의 Ontop ECS service를 생성하거나 강제 재배포한다(deployment circuit breaker 포함)(infra/lambda/vkg-reload/index.py).
VKG service는 기동 시 S3에서 아티팩트를 내려받아 번역 엔진을 조립한다.
온톨로지를 발행한 적 없는 namespace에는 VKG service가 존재하지 않는다.
metric은 이름, 설명, synonym, SQL expression으로 구성된 governed 계산 정의다. 등록 시 hard 검증(소스·테이블 존재, DML/DDL 차단)과 soft 검증(SQL 파싱, 컬럼 존재)을 거치며, Trino·PostgreSQL·Redshift 등 dialect별 expression을 병기할 수 있다. 발행되면 Neptune에 저장되고 OpenSearch에 embedding된다. OSI 포맷의 대량 import를 지원한다.
스키마에 기록되지 않은 지식, 곧 암묵지를 COA가 어떻게 다루는지는 도입 판단에서 자주 나오는 질문이다. 결론부터 말하면 COA는 암묵지를 자동으로 발견하는 시스템이 아니다. 흔적이 있는 지식은 AI가 표면화하고, 나머지는 사람이 주입할 통로를 제공한다. provenance 체계(STEWARD가 AI를 항상 이긴다)가 이 설계의 증거다.
| 수단 | 커버 범위 | 근거 |
|---|---|---|
| LLM 표면화 | 스키마에 흔적이 남은 의미. 이름과 comment에서 설명·synonym을 생성하고, naming pattern으로 FK를 추론하며, enum성 컬럼의 값 목록을 샘플링한다 | enrichment 2-pass |
| Grounding | 보편적 도메인 지식. Schema.org, FIBO 등 표준 온톨로지와의 정렬 | inducer + LLM reranker |
| 제약 추론 | proposal 온톨로지로부터 LLM이 semantic constraint를 추론하는 비동기 작업이 별도 API로 존재한다 | ontology-induction.smithy: InferConstraints, CompileConstraints |
| Proposal 직접 편집 | 사람의 지식 전반. accept 전에 T-Box Turtle과 R2RML을 각각 독립적으로 수정해 업로드할 수 있다 | UpdateProposal (smithy:552), proposals.py:1484 |
| 사람 입력 채널 | steward의 메타데이터 편집과 PK/FK 지정(Scan), metric 수동 등록, 온톨로지 업로드 | 3.6절, 4.6절 |
gubun, col1)과 코드 값의 의미(status=3이 무엇을 뜻하는지)는 추론할 수 없다. enum 샘플링은 값 목록만 제공하고 각 값의 의미는 주지 못한다.rr:class로만 서버가 파생하며 업로드로 위조할 수 없다(ontologies.md의 신뢰 경계). 암묵지를 담은 OWL을 업로드해도 mapping이 없으면 Tier 2 SQL 질의에 반영되지 않고 Tier 3 검색 재료로만 쓰인다. 암묵지를 SQL 질의까지 반영하는 경로는 induced proposal의 편집(위 표)에 한정된다.따라서 결과 품질의 상한은 사람 개입 지점(steward 리뷰, proposal 편집, metric 등록)의 충실도가 정한다. 도입 계획에서 이 세 지점의 담당자와 공수를 명시적으로 배정하는 것이 합리적이다.
| 인터페이스 | 대상 | 특징 |
|---|---|---|
| Playground | 웹 사용자 | SSE 스트리밍 채팅. 해석 단계, 생성된 SQL, 결과, trace를 실시간 표시. 세션은 DynamoDB에 지속 |
| REST API | 애플리케이션 | POST /namespaces/{id}/query. execute:false(SQL만 반환), tierOverride, maxResults 등의 옵션 |
| MCP | AI agent | AgentCore Runtime 위 Streamable HTTP. tool 6종: query, translate_sparql, list_metrics, describe_schema, rag_retrieval, graph_traversal |
세 인터페이스 모두 사용자의 OIDC ID token으로 인증하며 같은 권한 경로를 지난다. MCP tool 호출도 Cedar 사전 인가를 통과해야 하고, grant가 없으면 하드 에러로 처리된다(fail-closed)(coa_mcp/auth/grant_resolver.py). 로컬 IDE(Claude Desktop, Kiro, Cursor)는 stdio를 Streamable HTTP로 변환하는 mcp-proxy로 접속한다.
질의 오케스트레이터인 Context Manager는 Bedrock AgentCore Runtime에서 실행된다. ECS가 아니다. JWT 검증을 런타임이 처리하고 세션 격리와 AgentCore Memory(대화 이력)를 제공하므로, agent 워크로드에 필요한 기반을 자체 구현하지 않는다. MCP server는 별도의 AgentCore Runtime에 배포된다.
| Layer 1: Cedar | Layer 2: SQL Firewall | |
|---|---|---|
| 시점 | API Gateway authorizer(요청 진입) | SQL 실행 직전 |
| 단위 | 행위(query, manageSource 등) | 데이터(테이블 allowlist, 컬럼 denylist, metric allowlist) |
| 결합 규칙 | 여러 role 중 하나라도 permit이면 허용 | 여러 grant의 제한이 누적. 완화되지 않음 |
| 우회 | role 추가로 확장 가능 | 불가. platform-admin도 적용 대상 |
firewall은 sqlglot AST 수준에서 검사한다. SELECT 외 구문은 차단되고, grant에 명시된 테이블·컬럼 제한이 어느 tier에서 생성된 SQL이든 동일하게 적용된다.
Context Manager는 질의 해석을 총괄하는 오케스트레이터로, Python 컨테이너로 빌드되어 Bedrock AgentCore Runtime에서 실행된다(packages/context-manager). ECS가 아니다. AgentCore Runtime을 실행 기반으로 선택한 이유는 agent형 워크로드에 필요한 기반이 내장되어 있어서다. 사용자 JWT의 검증을 런타임이 수행하고, 세션 격리와 AgentCore Memory(멀티턴 대화 맥락), Observability를 플랫폼이 제공하므로 이를 자체 구현하지 않는다.
세 인터페이스가 모두 이 하나의 컨테이너로 수렴한다. REST 질의는 data-layer Lambda가
AgentCore endpoint로 프록시하고, Playground는 AgentCore Runtime의 /invocations endpoint에
HTTP POST로 직접 연결되어 응답을 SSE로 스트리밍 받으며, MCP는 별도 AgentCore Runtime의
MCP server가 실행형 tool(query 등) 호출을 Context Manager 호출로 변환한다.
탐색형 tool(list_metrics, describe_schema)은 해당 서비스 API를 직접 호출한다.
어느 경로든 사용자의 ID token이 함께 전달되어 권한 검사의 주체가 된다.
tier의 진행과 폴백은 LLM이나 agent의 판단이 아니다. orchestrator는 Tier 1, 2, 3을 순차 실행하는 결정적 코드이며(coa_serve/orchestrator.py), 전이 조건이 전부 규칙으로 정의되어 있다. Tier 1의 사양은 매칭 개수와 잔여 한정어 검사로, Tier 2의 폴백은 전략 수준 실패 판정 4종(생성 실패, unsafe SQL, 재시도 후 실행 에러, 저신뢰 0행)과 tier 수준 저신뢰 판정(행이 있어도 confidence < 0.3이면 Tier 3로)으로, Tier 3 진입은 상위 tier의 miss로 결정된다. LLM은 각 tier의 내부에서 도구로만 호출된다. SQL과 SPARQL을 생성하고 답변을 합성하지만, 어느 tier로 갈지를 결정하지 않는다.
이 구분은 실무적 결과를 낳는다. 라우팅이 결정적이므로 같은 질문은 같은 경로를 지나고, 모든 전이 사유가 trace에 남아 재현과 디버깅이 가능하다. LLM 라우터가 tier를 고르는 설계였다면 "오늘은 왜 다른 경로로 갔는가"를 설명할 수 없다. 예외는 agentic 모드 하나다. Tier 3 내부의 옵션이 아니라 요청 전체를 인수하는 실행 정책으로, 켜면(tierOverride가 없을 때) Tier 1·2 cascade를 아예 실행하지 않고 planner LLM이 구조적 도구(nl_to_sql, nl_to_sparql)를 포함한 도구 집합에서 다음 행동을 선택한다(orchestrator.py:241-270). 이때도 controller가 도구 우선순위(검증된 전략, 템플릿, 즉석 질의 순)를 결정적으로 강제하고 시간·스텝·분기 예산을 공유 clock으로 통제한다(tier3/agentic/controller.py).
tier 순서 자체는 신뢰도의 내림차순이다. Tier 1은 LLM이 아무것도 생성하지 않고,
Tier 2는 질의문만 생성하며, Tier 3는 답변 내용을 생성한다.
아래로 갈수록 정확성 보증이 약해지므로 보증이 강한 경로를 먼저 소진한다.
tierOverride 옵션으로 특정 tier를 고정할 수 있고, 소스 구성에 따른 자동 생략이 있다.
문서 전용 namespace는 Tier 1·2를 건너뛰고, DB 전용 namespace에서는 Tier 3의 문서 chunk 검색만
자체 생략된다(온톨로지 그래프 탐색과 합성은 동작한다).
metric resolver는 기동 시 Neptune에서 metric 정의를 읽어 인메모리 인덱스 세 개(by_id, by_name, by_synonym)를 구성하고 주기적으로 refresh한다. 질의 시점의 매칭은 이름·synonym의 정확 일치와, 오타·복수형 수준의 fuzzy 매칭(confidence < 1.0)이다. 의미 기반 벡터 검색은 의도적으로 수행하지 않는다(tier1/metric_resolver.py). 애매한 매칭을 허용하면 잘못된 지표를 확신 있게 반환할 수 있기 때문이다.
Tier 1이 응답하는 조건은 두 가지다.
residual_qualifier_bypass로 기록된다. 인사말 등은 한정어로 취급하지 않는다.
options.dimensions로 필터 값을 파라미터로 전달하면 Tier 1 경로를 유지한 채 필터를 적용할 수 있다.
매칭된 SQL도 실행 전 firewall과 metric allowlist 검사를 통과한다.
Tier 2는 전략 두 개를 가진 플러거블 구조다(tier2/strategy.py).
실행 정책은 best(병렬), ontop_first, nl_to_sql_first, 단일 고정 중
호출자가 선택하며, 기본값은 nl_to_sql_first다.
벡터 검색으로 질문과 관련된 테이블(steward가 검증한 설명·synonym 포함)을 찾아 프롬프트에 넣고, Bedrock이 대상 dialect의 SQL을 직접 생성한다. 실행 에러가 나면 에러 메시지를 포함해 1회 재생성한다(two-shot)(tier2/nl_to_sql/strategy.py).
다음 경우 NL-to-SQL을 포기하고 VKG 경로로 넘어간다.
전략과 별개로 tier 수준의 판정이 하나 더 있다. 어느 전략이 성공해 행을 반환했더라도
최종 confidence가 0.3 미만이면 orchestrator가 결과를 버리고 Tier 3로 넘긴다(orchestrator.py:648).
tierOverride: 2로 고정한 경우에만 저신뢰 결과가 그대로 반환된다.
어느 경로로 생성됐든 SQL은 firewall과 Cedar 데이터 인가를 거친 후 실행된다.
단일 소스이고 직접 driver가 가능한 엔진이면 native driver로(약 20~50ms),
그 외에는 Athena federation으로(약 500~800ms) 실행한다. Glue 소스가 queryEngine=REDSHIFT로 등록된 경우 Redshift Data API 경로도 있다(clients/composite_executor.py).
Tier 3는 두 개의 검색 소스를 병렬로 실행하고 결과를 한 번의 LLM 호출로 합성한다(tier3/knowledge_retriever.py:184-206). 각 소스는 10초 timeout을 가지며, 하나가 실패하거나 비어도 나머지로 진행한다.
| 소스 | 내용 |
|---|---|
| 1. 문서 chunk | OpenSearch chunk index에 k-NN 검색(tier3/vector_retriever.py). 문서 소스가 없는 namespace에서는 자체 생략된다. |
| 2. 그래프 탐색 | 라우팅 벡터 검색에서 매칭된 온톨로지 class·property의 URI를 시작점(seed)으로 Neptune에서 1~2 hop 탐색한다(entity당 관계 최대 10개). 맥락 수집용이며 집계가 아니다. |
합성은 Bedrock Converse 1회 호출이다. 수집된 조각들이 하나의 프롬프트로 조립되며, 문서 chunk에는 datamark를 부여해 프롬프트 주입을 방어하고, 질문의 언어로 답변하도록 강제하는 지시가 프롬프트 마지막에 배치된다(tier3/synthesizer.py). 설정으로 agentic 모드를 켜면 Tier 1·2 cascade를 거치지 않고 planner LLM이 tool을 선택하는 반복 탐색 루프가 요청 전체를 처리한다. 이 루프는 nl_to_sql과 nl_to_sparql을 도구로 보유하므로 sub-question 단위로 구조적 질의와 문서 검색을 조합할 수 있다. 이때도 controller가 tool 우선순위(검증된 전략, 템플릿, 즉석 질의 순)를 결정적으로 강제하고 시간·스텝·분기 예산을 공유 clock으로 통제한다(tier3/agentic/controller.py).
아래 네 예시는 제조사 시나리오(딜러, 보증 클레임, 부품, 로트)를 가정한다.
응답 구조는 Smithy 정의의 QueryResult를 따른다(models/src/main/smithy/serve.smithy:364).
Playground에서는 같은 내용이 SSE로 스트리밍되어 해석 단계가 실시간으로 표시되고,
REST에서는 아래 JSON이 한 번에 반환된다.
synthesizedAnswer는 Tier 3 전용 필드다. Tier 1·2는 결과 표(resultRows)와 실행 질의문을
반환하고 자연어 문장을 만들지 않으며, 직렬화가 null 필드를 생략하므로 아래 예시에는
각 경로에서 실제로 채워지는 필드만 담았다(response_assembler.py, main.py의 exclude_none).
| 단계 | 컴포넌트 | 동작 |
|---|---|---|
| 1 | Metric Resolver | 인메모리 인덱스에서 "총 클레임 건수"가 metric total_claim_count의 synonym과 정확 일치. 매칭 1개, 잔여 한정어 없음 |
| 2 | SQL Firewall | 등록 SQL 검사. 사용자 grant의 metric allowlist 확인 |
| 3 | Executor | 등록·검증된 SQL을 그대로 실행. LLM 개입 없음 |
{
"tier": 1,
"confidence": { "score": 1.0, "rationale": "exact synonym match: total_claim_count" },
"queryUsed": "SELECT COUNT(*) AS claim_count FROM warranty_claims",
"resultRows": [ { "claim_count": "18342" } ],
"trace": [
{ "step": "t1.metric_match", "status": "success", "durationMs": 3 },
{ "step": "t1.firewall", "status": "success", "durationMs": 1 },
{ "step": "t1.execute", "status": "success", "durationMs": 41, "toolUsed": "postgres" }
]
}
사용자에게는 결과 표의 숫자가 반환된다. Playground는 "governed metric으로 응답했다"는 해석 단계와 실행된 SQL을 함께 표시하므로, 사용자는 이 숫자가 즉석 생성이 아니라 조직이 승인한 정의에서 나왔음을 확인할 수 있다.
| 단계 | 컴포넌트 | 동작 |
|---|---|---|
| 1 | Metric Resolver | total_claim_count가 매칭되지만 "지난달"(기간)과 "제품군별"(그룹핑)이 잔여 한정어로 남음. residual_qualifier_bypass를 기록하고 Tier 2로 |
| 2 | NL-to-SQL(기본 전략) | 벡터 검색으로 관련 테이블 조회(warranty_claims, products). steward 검증 설명과 함께 프롬프트 구성, Bedrock이 SQL 생성 |
| 3 | SQL Firewall | SELECT 확인, 테이블 allowlist·컬럼 denylist 검사 |
| 4 | Executor | 단일 PostgreSQL 소스이므로 직접 driver로 실행 |
{
"tier": 2,
"confidence": { "score": 0.87 },
"queryUsed": "SELECT p.product_line, COUNT(*) AS claim_count
FROM warranty_claims c JOIN products p ON c.product_id = p.id
WHERE c.filed_at >= date_trunc('month', now()) - interval '1 month'
AND c.filed_at < date_trunc('month', now())
GROUP BY p.product_line ORDER BY claim_count DESC",
"resultRows": [
{ "product_line": "트랙터 X", "claim_count": "212" },
{ "product_line": "콤바인 C", "claim_count": "97" }
],
"trace": [
{ "step": "t1.metric_match", "status": "failed", "detail": "residual_qualifier_bypass: 지난달, 제품군별" },
{ "step": "t2.sql.generate", "status": "success", "durationMs": 1830, "toolUsed": "bedrock",
"detail": "confidence: 0.87, tables: [warranty_claims, products]" },
{ "step": "t2.sql.firewall", "status": "success", "durationMs": 4 },
{ "step": "t2.sql.execute", "status": "success", "durationMs": 38, "toolUsed": "postgres" }
]
}
사용자에게는 결과 표가 반환된다. trace의 첫 항목이 Tier 1의 사양 사유를 담고 있어, 같은 질문에서 "총 클레임 건수는?"과 다른 경로를 탄 이유를 추적할 수 있다.
클레임, 부품, 로트, 출하, 딜러의 5개 테이블을 잇는 multi-hop 조인이 필요한 질문이다. NL-to-SQL이 조인 경로를 잘못 구성해 폴백이 발생하는 시나리오를 보인다.
| 단계 | 컴포넌트 | 동작 |
|---|---|---|
| 1 | Metric Resolver | 매칭 없음. Tier 2로 |
| 2 | NL-to-SQL | SQL 생성했으나 lots와 shipments의 조인 키를 잘못 추론. 실행 에러 발생, 에러 메시지로 1회 재생성했으나 재차 실패. 전략 포기 |
| 3 | VKG: class 선별 | OpenSearch에서 관련 class 조회. 이 namespace는 class 47개로 200개 미만이므로 선별을 생략하고 전체 T-Box 사용 |
| 4 | VKG: T-Box 조립 | Neptune에서 WarrantyClaim, Part, Lot, Shipment, Dealer의 정의와 관계 조회. Lot.status의 distinct 값("RECALLED", "NORMAL")이 컨텍스트에 포함됨 |
| 5 | VKG: SPARQL 생성 | Bedrock이 온톨로지 어휘로 SPARQL 작성. 조인 경로는 SPARQL에 등장하지 않음(관계명만 사용) |
| 6 | VKG: 번역 | Ontop이 R2RML 규칙으로 5-테이블 JOIN SQL을 결정적으로 생성. 조인 키는 사람이 승인한 mapping에서 나옴 |
| 7 | Firewall, Executor | 검사 후 Athena federation으로 실행(클레임은 PostgreSQL, 출하 이력은 S3 curated의 크로스 소스 조인) |
{
"tier": 2,
"confidence": { "score": 0.91 },
"sparqlGenerated": "SELECT ?dealerName (COUNT(?claim) AS ?claimCount) WHERE {
?claim a dd:WarrantyClaim ; dd:refersToPart ?part .
?part dd:producedInLot ?lot .
?lot dd:status \"RECALLED\" ; dd:shippedVia ?shipment .
?shipment dd:receivedBy ?dealer .
?dealer dd:name ?dealerName .
FILTER(?filedAt >= \"2026-04-01\"^^xsd:date && ?filedAt < \"2026-07-01\"^^xsd:date)
} GROUP BY ?dealerName",
"queryUsed": "SELECT d.name, COUNT(c.id) FROM claims c
JOIN parts p ON c.part_id = p.id
JOIN lots l ON p.lot_id = l.id AND l.status = 'RECALLED'
JOIN shipments s ON s.lot_id = l.id
JOIN dealers d ON s.dealer_id = d.id
WHERE c.filed_at >= DATE '2026-04-01' AND c.filed_at < DATE '2026-07-01'
GROUP BY d.name",
"resultRows": [ { "name": "A딜러", "count": "34" }, { "name": "B딜러", "count": "19" } ],
"trace": [
{ "step": "t2.sql.generate", "status": "success", "toolUsed": "bedrock" },
{ "step": "t2.sql.execute", "status": "error", "detail": "column lots.shipment_id does not exist" },
{ "step": "t2.sql.failed", "status": "failed", "detail": "correction shot also failed" },
{ "step": "t2.vkg.translate", "status": "success", "durationMs": 2140, "toolUsed": "bedrock" },
{ "step": "t2.vkg.compile", "status": "success", "toolUsed": "ontop" },
{ "step": "t2.vkg.authorize", "status": "success", "toolUsed": "cedar" },
{ "step": "t2.vkg.firewall", "status": "success", "toolUsed": "sql-firewall" },
{ "step": "t2.vkg.execute", "status": "success", "durationMs": 640, "toolUsed": "sql-engine", "detail": "engine: athena" }
]
}
응답에 sparqlGenerated와 queryUsed가 모두 채워진 점이 이 경로의 특징이다.
사용자에게는 결과 표와 함께 두 질의문이 제공되므로, LLM이 쓴 것(SPARQL)과
기계 번역이 만든 것(SQL)을 구분해 검증할 수 있다. trace에는 첫 전략의 실패와 폴백 전 과정이 남는다.
"왜"는 하나의 SQL로 답할 수 없다. Tier 1은 매칭이 없고, Tier 2는 두 전략 모두 질문을 질의문으로 표현하지 못해 miss가 된다. Tier 3가 두 검색 소스를 병렬로 수집한다.
| 단계 | 컴포넌트 | 동작 |
|---|---|---|
| 1 | 라우팅 | 질문 embedding으로 OpenSearch 온톨로지 인덱스 검색. seed entity: dd:Dealer, dd:WarrantyClaim |
| 2a | 문서 chunk | chunk index k-NN. 품질 회보 문서에서 관련 조각 2건 검색(문서 소스가 없다면 이 소스는 자체 생략) |
| 2b | 그래프 탐색 | seed에서 1~2 hop. Dealer, Claim, Part, Lot의 연결 구조와 인접 개념 수집 |
| 3 | Synthesizer | 수집된 조각을 datamark와 함께 조립, Bedrock 1회 호출로 한국어 서술 생성 |
{
"tier": 3,
"confidence": { "score": 0.68, "rationale": "synthesis over document and graph retrieval" },
"synthesizedAnswer": "A딜러의 클레임은 부품과 로트 L-2231을 거쳐 출하·딜러와
연결되는 개념이다. 2026년 5월 품질 회보는 해당 로트의 유압 부품 결함
가능성을 언급했다. 증가 원인의 정량 확인은 '딜러별 X제품군 클레임 건수'
같은 집계 질의로 구체화할 수 있다.",
"supportingContent": [
{ "chunkId": "doc-qb-0526#c12", "text": "L-2231 로트 유압 실린더의 조기 마모 사례가 보고되어…", "relevanceScore": 0.83 }
],
"graphContext": {
"entities": [ { "uri": "dd:Dealer", "label": "딜러" }, { "uri": "dd:WarrantyClaim", "label": "보증 클레임" } ]
},
"partial": false,
"trace": [
{ "step": "t3.vector_search", "status": "success", "parallelGroup": "t3.retrieve", "durationMs": 420, "toolUsed": "opensearch" },
{ "step": "t3.graph_traverse", "status": "success", "parallelGroup": "t3.retrieve", "durationMs": 610, "toolUsed": "neptune" },
{ "step": "t3.synthesize", "status": "success", "durationMs": 3900, "toolUsed": "bedrock" }
]
}
사용자에게는 근거가 첨부된 서술형 답변이 반환된다. supportingContent가 인용된 문서 조각을,
graphContext의 entities가 탐색된 개념을 담으므로 답변의 출처를 역추적할 수 있다
(Smithy 계약의 최상위 relationships 리스트는 현 런타임이 채우지 않는다).
Tier 1·2와 달리 이 답변은 LLM이 작성한 것이며, 정확한 집계가 아니라 수집된 맥락의 종합이라는 점이
confidence rationale과 trace에 드러난다. 후속으로 정확한 수치가 필요하면 사용자는
"A딜러의 제품군별 클레임 건수는?"과 같이 Tier 2가 처리할 수 있는 형태로 재질의하면 된다.
| 항목 | 내용 |
|---|---|
| 리전 | 검증된 리전은 us-east-1뿐이다. AgentCore Runtime과 DataZone의 가용성이 제약이며, Bedrock 모델 ID가 US cross-region inference profile로 대부분 하드코딩되어 있어 타 리전 배포에는 코드 수정이 필요하다. |
| 유휴 비용 | Neptune(db.r8g.large 프로비저닝)과 OpenSearch Serverless로 유휴 시에도 월 약 $930이 발생한다. namespace마다 VKG Fargate service 비용이 추가된다. |
| 처리량 | 스캔·유도·서빙의 병목은 컴퓨팅이 아니라 Bedrock 모델별 TPM quota다. |
| 스키마 변경 | drift 감지와 정기 재스캔은 미지원이다. 재스캔은 SCAN_FAILED 상태의 복구 동작으로만 허용된다. |
| 질의 시간 | API Gateway 경유 질의는 29초 제한이 있다. |
| 버전 | 릴리즈는 v0.2.x로 진행 중이며(2026-08-21 v0.2.1), 본 문서의 코드 기준은 main mirror 2026-08-18이다. 운영 배포에는 tagged release 사용이 권장된다. 저장소는 read-only mirror로 PR을 받지 않는다. |