AIML / Architecture Deep Dive

국회 OpenAPI 위의 언론사 Agentic AI 지식그래프: ontology-for-assembly 아키텍처 분석

실제 국회 열린데이터광장 공공 데이터를 31개 클래스 온톨로지로 적재하고, 그 위에서 23개 Agentic AI 시나리오를 시연하는 PoC 저장소를 코드 기준으로 분석합니다. 공공 데이터 기반 지식그래프와 다중 에이전트 설계를 검토하는 엔지니어를 위한 문서입니다.

작성 기준일은 2026-08-09입니다.

적용 대상은 공공 데이터 기반 Agentic AI 지식그래프 PoC를 설계하는 아키텍트와 엔지니어입니다.

분석 대상 저장소는 whchoi98/ontology-for-assembly입니다.

주 출처는 저장소의 README, CLAUDE.md, CHANGELOG, docs/, 코드(2026-08-09 로컬 체크아웃)입니다.

요약

01어떤 문제를 푸는가

이 절은 프로젝트가 왜 만들어졌는지를 다룹니다. 국회가 공개하는 데이터와 언론사의 실제 업무 사이에 어떤 간극이 있는지부터 살펴봅니다.

이 프로젝트가 푸는 문제는 "한국 언론사가 국회 공공 데이터를 실제 업무 도구로 쓸 수 있는가"입니다. 국회 열린데이터광장(open.assembly.go.kr)은 의안, 의원, 표결, 위원회, 정당 데이터를 OpenAPI(누구나 호출할 수 있게 공개된 API)로 제공합니다. 하지만 원천 데이터만으로는 편집국의 취재 판단, 독자 서비스, 정책 인텔리전스 같은 뉴스룸 업무와 연결되지 않습니다.

ontology-for-assembly는 이 간극을 지식그래프 + Agentic AI 조합으로 메우는 30-60분 PoC(개념 검증) 데모입니다. 지식그래프는 데이터를 노드와 관계의 그물로 저장하는 방식입니다. Agentic AI는 LLM이 스스로 계획을 세우고 여러 단계를 이어서 수행하는 방식을 말합니다.

1.1 세 가지 목표와 여섯 페르소나

설계 스펙(docs/superpowers/specs/2026-05-13-ontology-assembly-design.md)이 명시한 목표는 세 가지입니다. 편집국 생산성(내부), 독자 서비스 차별화(B2C), 정책 인텔리전스(B2B)를 하나의 데이터 기반 위에서 동시에 시연하는 것입니다.

그래서 페르소나(데모를 바라보는 가상의 사용자 역할)는 내부 3개와 대고객 3개로 나뉩니다. 내부는 편집국, 데이터/AI, 광고/세일즈입니다. 대고객은 일반 독자, 유료 구독자, 기업/B2B 정책 인텔리전스입니다.

여섯 페르소나는 같은 그래프를 서로 다른 KPI 가중치와 어조로 봅니다. 페르소나 정의의 단일 진실원(같은 정보를 한 곳에서만 관리하는 기준 위치)은 api/services/persona.py:PERSONA_REGISTRY이며 6개 항목이 등록되어 있습니다.

1.2 이 문서가 집중하는 부분

저장소는 자매 프로젝트 ontology-for-gcc(plan1-foundation 브랜치)를 차용 베이스로 삼았습니다. README와 CLAUDE.md 모두 디렉토리 구조, 6-stack CDK, 프로젝트 하니스를 그대로 가져왔다고 명시합니다. 도메인만 국회/언론으로 교체했습니다.

그래서 이 문서는 차용된 골격이 아니라 이 프로젝트만의 차별점을 중심으로 분석합니다. 실제 국회 OpenAPI 데이터, 3단계 진화 비교, 광고 매칭 거버넌스, 출처 프로버넌스 배지 네 가지입니다.

핵심: 이 PoC의 주제는 모델 성능이 아닙니다. 민감한 공공 데이터를 다루는 Agentic AI에 출처 투명성과 거버넌스를 코드 레벨에서 강제하는 방법입니다.

02어떻게 동작하는가: 온톨로지와 데이터 출처

이 절은 데이터가 어떤 모양으로 저장되는지를 다룹니다. 어떤 종류의 데이터가 있고, 진짜 데이터와 만든 데이터를 어떻게 구분하는지 살펴봅니다.

2.1 31개 클래스, 코드가 스키마의 단일 진실원

온톨로지 클래스(그래프에 저장되는 데이터 종류의 정의)는 data/schemas.py에 Pydantic(파이썬의 데이터 검증 라이브러리) 모델로 정의되어 있습니다. 클래스는 모두 몇 개일까요? GraphNode base를 상속하는 클래스가 정확히 31개입니다.

Object Explorer가 쓰는 api/services/objects_catalog.py:CLASS_GROUPS의 그룹 합계도 31로 일치합니다. 아래 표에서는 31개 클래스가 어떤 도메인 그룹으로 나뉘는지를 봅니다.

표 1. 31개 온톨로지 클래스 그룹 (api/services/objects_catalog.py:CLASS_GROUPS 기준)
그룹클래스 수클래스
인물, 조직6Person, Party, Staff, Committee, District, Term
입법7Bill, Law, Amendment, Vote, Statement, Session, Budget
주제, 외부6Topic, Policy, Agency, ElectionResult, PollResult, SocialSignal
미디어2Article, Tag
독자 측5Reader, ReaderProfile, SubscriptionTier, ReadingEvent, Bookmark
광고4Advertisement, AdInventory, AdImpression, AdMatchDecision
분석 메타1Cluster
참고 - ontology/ 디렉토리는 스캐폴드입니다

저장소의 ontology/classes, ontology/relations 디렉토리는 현재 비어 있는 스캐폴드이며, CLAUDE.md가 이를 명시합니다. 스키마의 실제 단일 진실원은 data/schemas.py(Pydantic)와 api/services/objects_catalog.py(탐색기 메타)입니다. YAML 온톨로지 파일을 기대하고 열면 빈 디렉토리를 만나게 됩니다.

2.2 데이터 코호트: real / synthetic / external 출처 태깅

이 프로젝트의 두드러진 설계는 데이터 출처를 스키마 레벨에서 강제한다는 점입니다. 모든 노드의 base 클래스가 source 필드를 필수로 요구합니다. 또한 extra="forbid" 설정이 미정의 속성(예: 정치 성향 추론 필드)의 저장 자체를 차단합니다.

data/schemas.py - 모든 Neptune 노드의 base 클래스
class GraphNode(BaseModel):
    # `source` 필드 필수 - DataSourceBadge UI 노출에 사용
    # `extra="forbid"` - 정치 성향 추론 필드 등 미정의 속성 차단
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
    source: Source    # "real" | "synthetic" | "external"

세 코호트(같은 기준으로 묶은 데이터 집단)는 어댑터 디렉토리로도 분리되어 있습니다. data/real/은 국회 OpenAPI 어댑터(bill, member, vote, committee, session, party, agency)로 실 데이터를 가져옵니다. 여기에 22대 의원 286명(members_22.json)이 포함됩니다.

data/synthetic/은 독자, 광고, 기사, 토픽을 합성 생성합니다. data/external/은 네이버 뉴스와 여론조사의 ETL(외부 데이터를 추출하고 변환해 적재하는 파이프라인)을 담당합니다.

페르소나와 시나리오 조합에 따라 어느 코호트를 조회할지는 api/services/cohort.py가 단일 지점에서 결정합니다. 이 결정은 Cypher WHERE 절(n.source IN [...])로 변환됩니다. 프론트엔드에서는 web/components/DataSourceBadge.tsx가 모든 화면에 출처 배지를 그려, 시연 중 어떤 결과가 실 데이터이고 어떤 결과가 합성인지 즉시 구분됩니다.

2.3 아키텍처

인프라는 AWS CDK v2의 6개 스택으로 배포됩니다(infra-cdk/bin/assembly.ts 기준 network, data, ai, compute, edge, observability). FastAPI 백엔드(21개 라우터)와 Next.js 14 프론트엔드가 ECS Fargate ARM64에서 돌고, CloudFront와 Lambda@Edge JWT가 앞단을 막습니다.

그래프 저장소는 Neptune이며 openCypher(그래프 질의 언어)로 조회합니다. 검색은 OpenSearch Serverless에서 Nori BM25(한국어 형태소 기반 키워드 검색)와 Cohere KNN(임베딩 벡터 유사도 검색)을 함께 씁니다. 두 결과는 RRF(서로 다른 검색 순위를 하나로 합치는 융합 방법)로 합친 뒤 rerank-v3로 재정렬합니다.

LLM 계층은 Bedrock Sonnet 4.6과 AgentCore Memory / Code Interpreter입니다. 단, AgentCore Memory는 CDK placeholder이고 Code Interpreter는 mock 응답에만 존재하는 계획 항목으로, 실제 API 통합 코드는 아직 없습니다. B2B 페르소나는 별도로 API Gateway Usage Plan + API Key 경로를 탑니다.

flowchart LR U["브라우저 (6 페르소나)"] --> CF["CloudFront + Lambda@Edge JWT"] CF --> ALB["ALB"] CF --> GW["API Gateway (B2B API Key)"] ALB --> WEB["Next.js 14 (Fargate ARM64)"] ALB --> API["FastAPI 21 라우터 (Fargate ARM64)"] GW --> API API --> NEP[("Neptune openCypher")] API --> OS[("OpenSearch Serverless BM25+KNN")] API --> BR["Bedrock Sonnet 4.6 + Guardrails"] API --> AC["AgentCore Memory / Code Interpreter (계획)"] API --> ADL["Ad Matcher Lambda"]
그림 1. 요청 경로와 AI 계층. 인증이 API 계층이 아니라 edge(Lambda@Edge JWT, API Gateway API Key)에서 강제된다는 점이 운영 관점의 핵심입니다.

03딥다이브 1: 세 가지 AI 방식 비교 (Chatbot vs Agent vs Agentic AI)

이 절은 같은 질문에 세 가지 AI 방식이 어떻게 다르게 답하는지를 다룹니다. 이 비교가 데모의 첫 번째 메인 메시지입니다.

시나리오 B는 같은 질문을 세 가지 모드로 동시에 실행해 한 화면에서 나란히 비교합니다. 모드 분기는 api/services/three_stage.py가 담당합니다. 세 번째 모드의 다중 에이전트 파이프라인은 api/services/multi_agent.py:run_agentic_pipeline이 실행합니다.

용어를 먼저 풀면, RAG는 검색으로 찾은 문서를 근거로 답을 생성하는 방식입니다. Tool Use는 LLM이 미리 정의된 도구(함수)를 호출해 작업하는 방식입니다. 아래 표에서는 각 단계의 실행 흐름과, 데모가 일부러 드러내는 한계를 봅니다.

표 2. 3단계 진화 비교 (api/services/three_stage.py, multi_agent.py 기준)
단계방식실행 흐름한계 시연 포인트
Stage 1 Chatbot RAG OpenSearch top-K 검색 결과를 컨텍스트로 Bedrock 1회 호출 검색된 문서 밖의 질문에 답하지 못함
Stage 2 Agent Tool Use search_bills, get_proposers, cosponsor_network, analyze_votes 등 사전 정의 도구를 질문 의도에 따라 순차 호출 도구 목록 안에서만 행동, 계획 수립 없음
Stage 3 Agentic AI 다중 에이전트 Planner(과제 분해) → Graph(Cypher 변환과 Neptune 조회) → Analyst(해석과 가설) → Editor(기사 초안과 후속 취재 포인트) 에이전트 간 재질의를 포함한 자율 협업

4개 에이전트의 역할 프롬프트는 multi_agent.py:AGENT_PROMPTS에 상수로 정의되어 있습니다. Analyst는 필요 시 "Graph 에이전트에 재질의 필요"라는 형식으로 그래프 단계에 되물을 수 있습니다.

모든 LLM 호출은 api/services/bedrock.py:invoke()/invoke_stream() 단일 진입점을 거칩니다. DEMO_PUBLIC_MODE=true일 때는 결정적 demo mock(항상 같은 답을 돌려주는 가짜 응답)으로 동작합니다. 트리거는 자격증명 유무가 아니라 이 환경 변수이며, 이 모드 덕분에 오프라인에서도 데모와 테스트가 재현됩니다.

스트리밍은 SSE(서버가 응답을 조각 단위로 흘려보내는 방식)로 통일되어 있습니다. 이벤트 어휘(phase|delta|log|result|error|done)는 api/routers/chat.py docstring이 단일 진실원입니다.

주의 - Stage 2의 Tool Use는 아직 시뮬레이션입니다

three_stage.py:stage2_agent의 docstring은 "실 Bedrock Converse tool use API 통합은 Phase 2"라고 명시합니다. 현재 코드는 질문 의도를 규칙 기반으로 판별해 도구를 순차 호출하는 흐름을 재현합니다. LLM이 도구 선택을 스스로 하는 실 Converse 통합은 계획 단계이므로, 이 코드를 참조 구현으로 가져갈 때 혼동하기 쉽습니다.

04딥다이브 2: 광고 매칭과 중립성 가드레일

이 절은 민감한 기사에서 AI가 광고와 답변을 어떻게 조심스럽게 다루는지를 봅니다. 광고 매칭 거버넌스가 데모의 두 번째 메인 메시지입니다.

4.1 광고 매칭 매트릭스 (시나리오 L)

api/services/ad_matcher.py는 같은 기사와 같은 후보 광고 풀에 대해 세 가지 매칭 모드를 실행하고(compare_modes()), 결정 차이를 비교해 보여줍니다. 아래 표에서는 세 모드가 민감 콘텐츠를 어떻게 다르게 처리하는지를 봅니다.

표 3. 광고 매칭 3-way 비교 (api/services/ad_matcher.py 기준)
모드판단 근거민감 콘텐츠 처리
keyword 토픽-카테고리 단순 매핑, 점수 0.3~0.5 수준 감지 불가, 정치인 비위 기사에도 광고 매칭
embedding 임베딩 코사인 유사도(실 환경 Cohere embed-v4, mock은 결정적 해시 벡터) + avoid_topics 감점 의미 유사성은 잡지만 윤리, 평판 판단 불가
agent 민감 패턴 감지 + avoid_topics 엄격 적용 + 카테고리 점수 민감 콘텐츠 감지 시 chosen_ad_id=None으로 광고 노출 자체를 생략

agent 모드가 광고를 생략하면 그 근거 문장(reasoning trace)이 AdMatchDecision 노드에 저장되어 운영 콘솔에서 추적됩니다. 즉 "왜 이 지면에 광고가 안 나갔는가"가 그래프에 기록으로 남습니다.

매칭 실행은 API 응답과 분리된 별도 Ad Matcher Lambda로 동작합니다. 모드는 AD_MATCH_MODE 환경 변수와 UI 토글로 전환합니다. 데모 샘플 기사는 몇 개일까요? Agent가 거절해야 하는 3종(scandal, tragedy, minor_victim)과 안전 매칭 3종으로 총 6개가 준비되어 있습니다.

참고 - 민감 패턴은 README 서술(3종)보다 코드가 1종 많습니다

README와 CLAUDE.md는 자동 광고 생략 대상을 비극, 정치인 비위, 미성년 피해자 3종으로 서술하지만, 코드의 SENSITIVE_PATTERNS에는 controversy(폭로, 의혹, 논란)를 포함해 4종의 정규식 패턴이 정의되어 있습니다. 이 문서는 코드 기준으로 4종으로 봅니다.

4.2 정치 중립성 가드레일 6개 레이어 (ADR-0004)

정치 데이터를 다루는 만큼 중립성은 데모 가능성의 전제 조건으로 취급됩니다. ADR(설계 결정을 남기는 기록 문서)-0004(docs/decisions/0004)는 다층 방어 6개 레이어를 정의합니다.

  1. Layer 1 - Bedrock Guardrails 리소스. 정당명 + 비방 조합, 정치인 모욕을 차단하고 시나리오 B, C, I, K의 입력과 출력 양방향에 적용됩니다.
  2. Layer 2 - 모든 LLM 호출에 자동 첨부되는 중립성 system prompt suffix(NEUTRALITY_GUARD_SUFFIX).
  3. Layer 3 - 응답마다 자동 계산되는 political_balance_score. 한쪽 정당만 부각되면 UI가 경고하고, 평균 0.8 미만이면 CloudWatch 알람이 울립니다.
  4. Layer 4 - 클래스 레벨 필드 금지. Reader.political_leaning 같은 정치 성향 추론 필드는 생성과 저장이 금지되며, extra="forbid"가 이를 스키마에서 강제합니다.
  5. Layer 5 - 라이브 시연은 사전 검증된 질문 풀(scripts/eval_wow_queries.py)만 사용합니다.
  6. Layer 6 - 위 4.1의 광고 매칭 거버넌스입니다.

원칙: 거버넌스를 프롬프트 지시에만 맡기지 않고, 스키마(extra="forbid"), 메트릭(balance score), 인프라(Guardrails, 알람)의 세 층에 나눠 강제하는 것이 이 설계의 요지입니다.

0523개 시나리오 한눈에 보기

이 절은 데모에 들어 있는 시나리오 전체를 한눈에 정리합니다. 23개를 하나씩 나열하는 대신 구현 패턴 8가지로 묶어서 봅니다.

시나리오 목록의 단일 진실원은 web/components/Sidebar.tsx:SCENARIOS 배열이며 정확히 23개(A-W)가 등록되어 있습니다. 설계 스펙은 base 14개(A-N)였습니다. O-S는 사용자 요청 확장으로, T-W는 실 그래프 엣지 위의 고급 인사이트로 구현 과정에서 늘었습니다.

아래 표에서는 23개 시나리오가 어떤 8가지 패턴으로 묶이는지, 각 패턴을 어느 백엔드 파일이 받치는지를 봅니다.

표 4. 23개 시나리오의 구현 패턴 (CLAUDE.md 시나리오 표, api/routers/ 기준)
패턴시나리오백엔드대표 기술
하이브리드 검색, 그래프 탐색 A, O search.py, relations.py BM25(Nori) + Cohere KNN + RRF + rerank-v3, 두 인물 간 그래프 경로
3단계 대화 비교 B chat.py three_stage + multi_agent, SSE 스트리밍 (AgentCore Memory는 계획 항목)
LLM 인사이트, 차트, 타임라인 C, G, K, M, N insights.py 외 시나리오별 라우터 Sonnet 4.6 스트리밍 요약, 표결 이상치 탐지 (Code Interpreter 차트는 계획 항목)
페르소나 매칭, 룩어라이크 D, F persona_match.py, lookalike.py 6 페르소나 KPI 가중치, 클러스터 매칭 + cross-party 신호
통계, 지도, 외부 신호 E, H, J cluster.py, district_map.py, external_signal.py KMeans + 정치 중립 LLM 라벨, 17개 시도 choropleth, 뉴스/SNS/여론조사 융합
중립성 가드레일 가시화 I neutrality.py Bedrock Guardrails + political_balance_score 노출
광고 매칭 거버넌스 L ad_match.py + Ad Matcher Lambda keyword / embedding / agent 3-way, Agent 광고 생략 trace
범용 LLM narrative, 실 엣지 인사이트 P, Q, R, S / T, U, V, W members.py + insight_generic.py / insights_advanced.py 전용 라우터 없이 POST /api/insight 재사용 / 약 74,000개 real 엣지 openCypher (정당 응집도, 영향력 랭킹, 표결 cluster, swing voter)

23개 시나리오가 21개 라우터와 1:1이 아니라는 점도 코드 이해에 중요합니다. P, Q, R, S 4개는 전용 라우터 없이 의원 디렉토리 데이터와 범용 insight_generic.py로 백킹됩니다.

시나리오 페이지 외에 다섯 가지 화면이 함께 배포됩니다. 31클래스 Object Explorer, 온톨로지 관계 그래프(mindmap), 의원 디렉토리(286명, 9개 지표), 운영 콘솔(5 패널), 코드 지식 그래프(codegraph) 페이지입니다.

06어떻게 배포하고 검증하는가

이 절은 이 PoC를 실제로 띄우고 품질을 확인하는 절차를 다룹니다. 배포, 데이터 적재, 자동 평가 순서로 살펴봅니다.

배포는 ap-northeast-2 리전에서 npx cdk deploy --all로 6개 스택을 올리는 것으로 시작합니다. VPC는 새로 만들지 않고 자매 프로젝트들과 공유하는 VPC를 import합니다(ADR-0006). Neptune은 private subnet에 있어 직접 접근이 불가능합니다.

그래서 데이터 적재는 one-shot ECS 태스크(한 번 실행되고 종료되는 컨테이너 작업)로 실행합니다. 이 태스크는 API 컨테이너 이미지를 그대로 재사용합니다.

데이터 적재 - one-shot ECS 태스크 (CLAUDE.md Key Commands)
aws ecs run-task --cluster assembly-dev-cluster --task-definition assembly-dev-api \
  --overrides '{"containerOverrides":[{"name":"api","command":
    ["python","-m","data.load","--source","all","--to","s3",
     "--bucket","assembly-dev-synthetic-data","--neptune","--opensearch"]}]}'

품질 게이트는 두 겹입니다. CI는 push/PR마다 4개 job(python-ast, tsc-check, cdk-synth, pytest)을 돌립니다. 배포된 CloudFront를 대상으로는 make wow-eval이 자동 평가를 실행합니다.

평가 케이스는 몇 개일까요? 핵심 14개 시나리오 A-N에 6개 페르소나를 곱한 84개 케이스입니다. README 배지 기준 현재 상태는 84/84 통과, 평균 balance score 0.98입니다.

통과율이 85% 미만이거나 중립성 메트릭이 임계값에 미달하면 스크립트가 실패 코드로 종료됩니다. 운영 콘솔(/ops)은 ingest, guardrail, memory, eval, trace 5개 패널로 같은 메트릭을 시연 중에 보여줍니다.

주의 - wow-eval의 범위는 23개 시나리오 전체가 아닙니다

84개 평가 케이스는 base 14개 시나리오(A-N)에 대한 것이며, 확장 시나리오 O-W 9개는 이 자동 평가의 대상이 아닙니다. README도 이를 명시합니다. "wow-eval 100%"를 23개 시나리오 전체의 검증으로 읽으면 과대 해석입니다.

07한계와 결론

이 절은 이 PoC를 참조할 때 주의할 점과 최종 평가를 정리합니다. 무엇이 검증되었고 무엇이 아직 아닌지를 구분합니다.

7.1 한계

  • PoC 데모입니다. 인증은 edge 계층(Lambda@Edge, API Gateway)에만 있고 API 계층은 upstream을 신뢰합니다. DEMO_PUBLIC_MODE 같은 데모 전용 우회도 존재합니다.
  • 독자, 광고, 기사 데이터는 전부 합성입니다. 광고 매칭 거버넌스의 사업적 유효성은 실 인벤토리 없이 검증되지 않았습니다.
  • DEMO_PUBLIC_MODE=true에서는 결정적 mock으로 동작하고 Stage 2 Tool Use도 시뮬레이션입니다. 그래서 화면에 보이는 모든 것이 실 LLM 호출은 아니며, 실 데이터 경로(T-W의 Neptune openCypher)와 mock 경로가 혼재합니다.
  • 문서와 코드의 정합은 계속 따라잡는 중입니다. CHANGELOG에 시나리오 수(14 → 23), 클래스 수(25+ → 31), 라우터 수(17 → 21) 정정 기록이 남아 있으며, 이 문서의 수치는 모두 코드 기준입니다.
  • 중립성 가드레일은 한국어 정규식과 사전 검증 질문 풀에 크게 의존합니다. 자유 입력이 늘어나는 production 시나리오에서는 별도 검증이 필요합니다.

7.2 결론

ontology-for-assembly의 가치는 개별 시나리오의 화려함이 아니라 반복 가능한 골격에 있습니다. 실 공공 데이터와 합성 데이터를 스키마 레벨 출처 태깅으로 섞습니다. 같은 질문에 대한 세 가지 AI 패러다임의 차이를 비교 가능하게 만듭니다.

그리고 민감 도메인의 거버넌스를 프롬프트가 아닌 스키마와 메트릭과 인프라에 나눠 강제합니다. 이 구조는 국회 데이터가 아닌 다른 도메인에도 그대로 이식됩니다. 실제로 이 저장소 자체가 GS칼텍스 고객분석 도메인 PoC(ontology-for-gcc)의 골격을 차용해 도메인만 교체한 결과물이라는 점이 그 이식성의 증거입니다.

한 문장으로 요약하면, 이 저장소는 민감한 공공 데이터 위에서 출처 투명성과 거버넌스를 코드로 강제하는 Agentic AI PoC 골격입니다.

권고: 이 저장소를 참조할 때는 시나리오 코드가 아니라 세 개의 단일 진실원, 즉 data/schemas.py(스키마), PERSONA_REGISTRY(페르소나), Sidebar.tsx:SCENARIOS(시나리오 목록)부터 읽기를 권합니다.

인터랙티브 아키텍처 맵 전체 이미지 - 시스템 구성 요소와 흐름을 한 화면으로 보여줍니다
그림 2. 인터랙티브 아키텍처 맵 전체 보기. 이미지를 클릭하면 노드 탐색, 경로 추적, 다크/라이트 테마를 지원하는 인터랙티브 버전 ↗이 열립니다.

--참고 자료

핵심 출처

  • ontology-for-assembly GitHub 저장소 - whchoi98 (2026-08-09 로컬 체크아웃 기준. README, CLAUDE.md, CHANGELOG, docs/decisions ADR 0001-0010, api/, web/, data/, infra-cdk/) https://github.com/whchoi98/ontology-for-assembly
  • 국회 열린데이터광장 - 대한민국 국회 (실 데이터 코호트의 원천 OpenAPI) https://open.assembly.go.kr

공식 문서

관련 저장소

  • ontology-for-gcc - 차용 베이스 (plan1-foundation 브랜치, 디렉토리 / 6-stack CDK / 하니스 골격의 원본) https://github.com/whchoi98/ontology-for-gcc

AIML / Architecture Deep Dive

A Newsroom Agentic AI Knowledge Graph on the Korean National Assembly OpenAPI: An Architecture Analysis of ontology-for-assembly

A code-level analysis of a PoC repository that loads real public data from the National Assembly Open Data Portal into a 31-class ontology and demonstrates 23 Agentic AI scenarios on top of it. Written for engineers evaluating public-data knowledge graphs and multi-agent designs.

This document is written as of 2026-08-09.

The intended audience is architects and engineers designing Agentic AI knowledge-graph PoCs on public data.

The repository under analysis is whchoi98/ontology-for-assembly.

Primary sources are the repository's README, CLAUDE.md, CHANGELOG, docs/, and code (local checkout as of 2026-08-09).

TL;DR

01What problem it solves

This section covers why the project exists. We start with the gap between the data the National Assembly publishes and the daily work of a news organization.

The problem this project tackles is: "can a Korean news organization actually use National Assembly public data as a working tool?" The National Assembly Open Data Portal (open.assembly.go.kr) publishes bills, members, votes, committees, and party data through an OpenAPI (an API anyone can call). But raw data alone does not connect to newsroom work such as editorial judgment, reader-facing services, or policy intelligence.

ontology-for-assembly bridges this gap with a knowledge graph + Agentic AI combination, packaged as a 30-60 minute PoC (proof of concept) demo. A knowledge graph stores data as a web of nodes and relations. Agentic AI means an LLM that plans on its own and carries out several steps in sequence.

1.1 Three goals and six personas

The design spec (docs/superpowers/specs/2026-05-13-ontology-assembly-design.md) states three goals: demonstrating newsroom productivity (internal), reader-service differentiation (B2C), and policy intelligence (B2B) on a single data foundation at the same time.

That is why the personas (imaginary user roles that view the demo) split into three internal and three customer-facing ones. The internal ones are editorial, data/AI, and ads/sales. The customer-facing ones are the general reader, the paid subscriber, and corporate/B2B policy intelligence.

The six personas view the same graph with different KPI weights and tone. The single source of truth (the one place where a definition is maintained) for personas is api/services/persona.py:PERSONA_REGISTRY, which registers six entries.

1.2 What this document focuses on

The repository borrows its skeleton from the sister project ontology-for-gcc (plan1-foundation branch). Both README and CLAUDE.md state that the directory layout, the 6-stack CDK setup, and the project harness were carried over as-is. Only the domain was swapped to the National Assembly / news media.

This document therefore focuses not on the borrowed skeleton but on what is unique to this project. Four things: real National Assembly OpenAPI data, the three-stage evolution comparison, ad-matching governance, and provenance badges.

Key point: the subject of this PoC is not model performance, but how to enforce provenance transparency and governance at the code level in an Agentic AI system that handles sensitive public data.

02How it works: the ontology and data provenance

This section covers the shape the data is stored in. We look at what kinds of data exist and how real data is kept apart from generated data.

2.1 31 classes, with code as the single source of truth for the schema

The ontology classes (the definitions of what kinds of data the graph stores) are defined as Pydantic models (Pydantic is Python's data-validation library) in data/schemas.py. How many classes are there? Exactly 31 classes inherit from the GraphNode base.

The group totals in api/services/objects_catalog.py:CLASS_GROUPS, used by the Object Explorer, also add up to 31. The table below shows how the 31 classes split into domain groups.

Table 1. The 31 ontology class groups (per api/services/objects_catalog.py:CLASS_GROUPS)
GroupClassesClass names
People, organizations6Person, Party, Staff, Committee, District, Term
Legislation7Bill, Law, Amendment, Vote, Statement, Session, Budget
Topics, external6Topic, Policy, Agency, ElectionResult, PollResult, SocialSignal
Media2Article, Tag
Reader side5Reader, ReaderProfile, SubscriptionTier, ReadingEvent, Bookmark
Advertising4Advertisement, AdInventory, AdImpression, AdMatchDecision
Analysis meta1Cluster
Note - the ontology/ directory is a scaffold

The repository's ontology/classes and ontology/relations directories are currently empty scaffolds, and CLAUDE.md says so explicitly. The actual single sources of truth for the schema are data/schemas.py (Pydantic) and api/services/objects_catalog.py (explorer metadata). If you open those directories expecting YAML ontology files, you will find them empty.

2.2 Data cohorts: real / synthetic / external source tagging

The standout design choice is that data provenance is enforced at the schema level. The base class of every node requires a source field. In addition, extra="forbid" blocks the storage of any undefined attribute (for example, an inferred political-leaning field) outright.

data/schemas.py - the base class for every Neptune node
class GraphNode(BaseModel):
    # `source` field is required - used for the DataSourceBadge UI
    # `extra="forbid"` - blocks undefined attributes such as inferred political leaning
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
    source: Source    # "real" | "synthetic" | "external"

The three cohorts (groups of data bundled by a shared criterion) are also separated by adapter directory. data/real/ holds the National Assembly OpenAPI adapters (bill, member, vote, committee, session, party, agency) and pulls real data. That includes the 286 members of the 22nd Assembly (members_22.json).

data/synthetic/ generates synthetic readers, ads, articles, and topics. data/external/ handles the Naver News and opinion-poll ETL (the pipeline that extracts, transforms, and loads external data).

Which cohort a given persona-scenario combination queries is decided at a single point by api/services/cohort.py. The decision is translated into a Cypher WHERE clause (n.source IN [...]). On the frontend, web/components/DataSourceBadge.tsx renders a provenance badge on every screen, so during a demo it is immediately clear which results are real and which are synthetic.

2.3 Architecture

The infrastructure deploys as six AWS CDK v2 stacks (network, data, ai, compute, edge, and observability, per infra-cdk/bin/assembly.ts). A FastAPI backend (21 routers) and a Next.js 14 frontend run on ECS Fargate ARM64, fronted by CloudFront with Lambda@Edge JWT.

The graph store is Neptune, queried with openCypher (a graph query language). Search runs on OpenSearch Serverless and combines Nori BM25 (Korean-morphology keyword search) with Cohere KNN (embedding-vector similarity search). The two result lists are merged with RRF (a method that fuses different search rankings into one) and then reordered with rerank-v3.

The LLM layer is Bedrock Sonnet 4.6 plus AgentCore Memory / Code Interpreter. Note that AgentCore Memory is a CDK placeholder and Code Interpreter exists only in mock responses; both are planned items with no real API integration code yet. The B2B persona takes a separate API Gateway Usage Plan + API Key path.

flowchart LR U["Browser (6 personas)"] --> CF["CloudFront + Lambda@Edge JWT"] CF --> ALB["ALB"] CF --> GW["API Gateway (B2B API Key)"] ALB --> WEB["Next.js 14 (Fargate ARM64)"] ALB --> API["FastAPI, 21 routers (Fargate ARM64)"] GW --> API API --> NEP[("Neptune openCypher")] API --> OS[("OpenSearch Serverless BM25+KNN")] API --> BR["Bedrock Sonnet 4.6 + Guardrails"] API --> AC["AgentCore Memory / Code Interpreter (planned)"] API --> ADL["Ad Matcher Lambda"]
Figure 1. Request paths and the AI layer. The operationally important point is that authentication is enforced at the edge (Lambda@Edge JWT, API Gateway API Key), not in the API layer.

03Deep dive 1: comparing three AI approaches (Chatbot vs Agent vs Agentic AI)

This section covers how three AI approaches answer the same question differently. This comparison is the demo's first headline message.

Scenario B runs the same question through three modes simultaneously and compares the results side by side on one screen. Mode dispatch lives in api/services/three_stage.py. The third mode's multi-agent pipeline is executed by api/services/multi_agent.py:run_agentic_pipeline.

Two terms first. RAG means generating an answer grounded in documents found by search. Tool Use means the LLM calling predefined tools (functions) to do work. The table below shows each stage's execution flow and the limitation the demo deliberately exposes.

Table 2. The three-stage evolution comparison (per api/services/three_stage.py and multi_agent.py)
StageApproachExecution flowLimitation on display
Stage 1 Chatbot RAG One Bedrock call with OpenSearch top-K results as context Cannot answer questions outside the retrieved documents
Stage 2 Agent Tool Use Sequentially calls predefined tools such as search_bills, get_proposers, cosponsor_network, analyze_votes based on question intent Acts only within the tool list, no planning
Stage 3 Agentic AI Multi-agent Planner (task decomposition) → Graph (Cypher translation and Neptune queries) → Analyst (interpretation and hypotheses) → Editor (article draft and follow-up reporting angles) Autonomous collaboration, including agent-to-agent re-queries

The role prompts for the four agents are defined as constants in multi_agent.py:AGENT_PROMPTS. When needed, the Analyst can query the graph stage again using the "re-query the Graph agent" convention.

Every LLM call goes through the single entry point api/services/bedrock.py:invoke()/invoke_stream(). When DEMO_PUBLIC_MODE=true, it runs as a deterministic demo mock (a fake response that always returns the same answer). The trigger is this environment variable, not the presence of credentials, and this mode is what makes demos and tests reproducible offline.

Streaming is unified on SSE (a way for the server to stream a response in pieces). The event vocabulary (phase|delta|log|result|error|done) has its single source of truth in the api/routers/chat.py docstring.

Caution - Stage 2 Tool Use is still simulated

The docstring of three_stage.py:stage2_agent states that "real Bedrock Converse tool use API integration is Phase 2". The current code classifies question intent with rules and replays a sequential tool-calling flow. Real Converse integration where the LLM chooses tools by itself is still a planned item, so this is an easy point to misread if you take this code as a reference implementation.

04Deep dive 2: ad matching and neutrality guardrails

This section looks at how the AI handles ads and answers carefully on sensitive stories. Ad-matching governance is the demo's second headline message.

4.1 The ad-matching matrix (scenario L)

api/services/ad_matcher.py runs three matching modes against the same article and the same candidate ad pool (compare_modes()) and shows how the decisions differ. The table below shows how differently the three modes handle sensitive content.

Table 3. The 3-way ad-matching comparison (per api/services/ad_matcher.py)
ModeDecision basisSensitive-content handling
keyword Simple topic-to-category mapping, scores around 0.3~0.5 Cannot detect it; matches ads even on a politician-misconduct story
embedding Embedding cosine similarity (Cohere embed-v4 in a real environment, deterministic hash vectors in mock) + avoid_topics penalty Captures semantic similarity but cannot make ethics or reputation judgments
agent Sensitive-pattern detection + strict avoid_topics enforcement + category scoring On sensitive content, sets chosen_ad_id=None and withholds the ad placement entirely

When the agent mode withholds an ad, its reasoning trace is stored in an AdMatchDecision node and can be tracked in the ops console. In other words, "why did no ad run on this placement" is left as a record in the graph.

Matching runs in a dedicated Ad Matcher Lambda, decoupled from the API response path. The mode is switched via the AD_MATCH_MODE environment variable and a UI toggle. How many sample articles ship with the demo? Six: three the Agent must refuse (scandal, tragedy, minor_victim) and three safe matches.

Note - the code has one more sensitive pattern than the README's three

README and CLAUDE.md describe three automatic ad-withholding targets - tragedy, politician misconduct, and minor victims - but the code's SENSITIVE_PATTERNS defines four regex patterns, adding controversy (exposes, allegations, disputes). This document counts four, per the code.

4.2 The six political-neutrality guardrail layers (ADR-0004)

Because the data is political, neutrality is treated as a precondition for the demo being possible at all. ADR-0004 (an ADR is a written record of a design decision, here docs/decisions/0004) defines six layers of defense in depth.

  1. Layer 1 - a Bedrock Guardrails resource. Blocks party-name + slander combinations and insults against politicians, applied in both input and output directions for scenarios B, C, I, and K.
  2. Layer 2 - a neutrality system prompt suffix (NEUTRALITY_GUARD_SUFFIX) automatically appended to every LLM call.
  3. Layer 3 - a political_balance_score computed automatically for every response. If only one party is highlighted the UI warns, and if the average drops below 0.8 a CloudWatch alarm fires.
  4. Layer 4 - class-level field bans. Political-leaning inference fields such as Reader.political_leaning are forbidden from being created or stored, enforced at the schema level by extra="forbid".
  5. Layer 5 - live demos use only a pre-vetted question pool (scripts/eval_wow_queries.py).
  6. Layer 6 - the ad-matching governance from 4.1 above.

Principle: the essence of this design is that governance is not left to prompt instructions alone but is enforced across three layers - the schema (extra="forbid"), metrics (the balance score), and infrastructure (Guardrails, alarms).

05The 23 scenarios at a glance

This section summarizes every scenario in the demo at a glance. Instead of listing all 23 one by one, we group them into eight implementation patterns.

The single source of truth for the scenario list is the web/components/Sidebar.tsx:SCENARIOS array, which registers exactly 23 entries (A-W). The design spec started with a base of 14 (A-N). O-S were added on user request, and T-W grew during implementation as advanced insights over the real graph edges.

The table below shows which eight patterns the 23 scenarios group into, and which backend file backs each pattern.

Table 4. Implementation patterns behind the 23 scenarios (per the CLAUDE.md scenario table and api/routers/)
PatternScenariosBackendRepresentative techniques
Hybrid search, graph traversal A, O search.py, relations.py BM25 (Nori) + Cohere KNN + RRF + rerank-v3, graph paths between two people
Three-stage chat comparison B chat.py three_stage + multi_agent, SSE streaming (AgentCore Memory is a planned item)
LLM insights, charts, timelines C, G, K, M, N insights.py plus per-scenario routers Sonnet 4.6 streaming summaries, vote-anomaly detection (Code Interpreter charts are a planned item)
Persona matching, lookalike D, F persona_match.py, lookalike.py KPI weights for 6 personas, cluster matching + cross-party signals
Statistics, maps, external signals E, H, J cluster.py, district_map.py, external_signal.py KMeans + politically neutral LLM labels, a 17-province choropleth, news/SNS/poll fusion
Neutrality guardrails made visible I neutrality.py Bedrock Guardrails + exposing political_balance_score
Ad-matching governance L ad_match.py + Ad Matcher Lambda keyword / embedding / agent 3-way, the Agent's ad-withholding trace
Generic LLM narrative, real-edge insights P, Q, R, S / T, U, V, W members.py + insight_generic.py / insights_advanced.py Reusing POST /api/insight with no dedicated router / openCypher over roughly 74,000 real edges (party cohesion, influence ranking, vote clusters, swing voters)

It also matters for reading the code that the 23 scenarios are not 1:1 with the 21 routers. The four scenarios P, Q, R, and S have no dedicated router and are backed by the member-directory data plus the generic insight_generic.py.

Beyond the scenario pages, the deployment ships five more screens: the 31-class Object Explorer, an ontology relation graph (mindmap), a member directory (286 members, 9 indicators), an ops console (5 panels), and a code knowledge graph (codegraph) page.

06How it is deployed and verified

This section covers how the PoC is actually brought up and how its quality is checked. We go through deployment, data loading, and automated evaluation in order.

Deployment starts by bringing up the six stacks with npx cdk deploy --all in the ap-northeast-2 region. No new VPC is created; the VPC shared with the sister projects is imported (ADR-0006). Neptune sits in a private subnet with no direct access.

Data loading therefore runs as a one-shot ECS task (a container job that runs once and exits). The task reuses the API container image as-is.

Data loading - one-shot ECS task (CLAUDE.md Key Commands)
aws ecs run-task --cluster assembly-dev-cluster --task-definition assembly-dev-api \
  --overrides '{"containerOverrides":[{"name":"api","command":
    ["python","-m","data.load","--source","all","--to","s3",
     "--bucket","assembly-dev-synthetic-data","--neptune","--opensearch"]}]}'

Quality gates come in two layers. CI runs four jobs on every push/PR (python-ast, tsc-check, cdk-synth, pytest). Against the deployed CloudFront, make wow-eval runs the automated evaluation.

How many cases does it evaluate? 84 cases: the 14 core scenarios A-N × 6 personas. Per the README badge, the current state is 84/84 passing with an average balance score of 0.98.

The script exits with a failure code if the pass rate falls below 85% or the neutrality metric misses its threshold. The ops console (/ops) shows the same metrics live during a demo across five panels: ingest, guardrail, memory, eval, and trace.

Caution - wow-eval does not cover all 23 scenarios

The 84 evaluation cases cover the 14 base scenarios (A-N); the nine extension scenarios O-W are outside this automated evaluation, and the README says so. Reading "wow-eval 100%" as validation of all 23 scenarios would be an over-interpretation.

07Limitations and conclusion

This section summarizes what to keep in mind when referencing this PoC, and the final assessment. It separates what has been verified from what has not.

7.1 Limitations

  • It is a PoC demo. Authentication exists only at the edge layer (Lambda@Edge, API Gateway) and the API layer trusts its upstream. Demo-only bypasses such as DEMO_PUBLIC_MODE also exist.
  • Reader, ad, and article data are entirely synthetic. The business validity of the ad-matching governance has not been verified against real inventory.
  • With DEMO_PUBLIC_MODE=true the system runs on deterministic mocks and Stage 2 Tool Use is simulated. So not everything on screen is a real LLM call, and real-data paths (Neptune openCypher for T-W) coexist with mock paths.
  • Documentation-to-code consistency is still catching up. The CHANGELOG records corrections to the scenario count (14 → 23), class count (25+ → 31), and router count (17 → 21); every figure in this document follows the code.
  • The neutrality guardrails rely heavily on Korean-language regexes and the pre-vetted question pool. A production scenario with more free-form input would need separate validation.

7.2 Conclusion

The value of ontology-for-assembly lies not in the flash of individual scenarios but in a repeatable skeleton. It mixes real public data and synthetic data under schema-level provenance tagging. It makes the differences between three AI paradigms comparable on the same question.

And it enforces sensitive-domain governance through schema, metrics, and infrastructure rather than prompts. That structure ports directly to domains other than National Assembly data. The repository itself is evidence of that portability: it borrowed the skeleton of the GS Caltex customer-analytics PoC (ontology-for-gcc) and swapped only the domain.

To sum it up in one sentence, this repository is an Agentic AI PoC skeleton that enforces provenance transparency and governance in code on top of sensitive public data.

Recommendation: when referencing this repository, start not with the scenario code but with the three single sources of truth: data/schemas.py (schema), PERSONA_REGISTRY (personas), and Sidebar.tsx:SCENARIOS (the scenario list).

Full image of the interactive architecture map showing the system components and flows in one view
Figure 2. Full view of the interactive architecture map. Click the image to open the interactive version ↗ with node search, route tracing, and dark/light themes.

--References

Primary sources

  • ontology-for-assembly GitHub repository - whchoi98 (local checkout as of 2026-08-09: README, CLAUDE.md, CHANGELOG, docs/decisions ADR 0001-0010, api/, web/, data/, infra-cdk/) https://github.com/whchoi98/ontology-for-assembly
  • National Assembly Open Data Portal - National Assembly of the Republic of Korea (the source OpenAPI for the real data cohort) https://open.assembly.go.kr

Official documentation

Related repositories

  • ontology-for-gcc - the borrowed base (plan1-foundation branch; origin of the directory layout / 6-stack CDK / harness skeleton) https://github.com/whchoi98/ontology-for-gcc