feel free to

Contact us

The IMC의 사업과 솔루션, 기술지원, 채용정보에
관련한 여러분의 문의사항을 알려주세요.

  • Tell 053-744-0707
  • E-mail theimc@theimc.co.kr
프로필 정보
프로필 이미지

by Ji

AI 혁신팀 주임연구원

LLM 기반 AI 서비스 개발을 위한 데이터 파이프라인 및 모델 활용 구조를 설계·구현하는 엔지니어입니다.

AI 엔지니어링

00
리스트가 없습니다.

위험 최소화를 위한 Intent-Driven DB 정적조회 + LLM 필터링 아키텍처

2025-12-10

본 개발은 제주 지역 관광 안전 정보를 이용자 질의 기반으로 제공하는 AI 기반 안전 안내 시스템을 구축하는 것을 목표로 하였습니다.
사용자의 질문에서 장소·시점 등 핵심 정보를 추출하고, 정적 SQL을 통해 검증된 데이터만 조회하여 LLM이 필요한 정보만 활용해 자연어로 응답하도록 설계하였습니다.
이를 통해 공공 서비스에서 요구되는 신뢰성·안전성·설명가능성을 갖춘 책임형 AI 질의응답 서비스를 구현하였습니다.

1. 사용자 질문 카테고리·코드 분류

사용자의 자연어 질문이 들어오면, 의도 분석(분류 모델, 규칙 기반 혼합)을 통해 질문을 다음 네 가지 카테고리 중 하나로 분류합니다.

  • 관광정보
  • 기상정보
  • 교통정보
  • 사건사고

카테고리 분류 결과에 따라 내부 처리 코드를 부여하여 조회 대상 DB 스키마(tour / weather / traffic / issue 등)를 결정합니다.
이 과정에서 “어떤 DB 도메인을 조회해야 하는가?”를 선제적으로 확정함으로써 LLM이 임의 영역의 데이터를 조회하지 않도록 검색 범위를 구조적으로 제한합니다.

2. 장소·장소타입, 시점·시점타입 추출

질문 문장 내 NER(개체명 인식), 토큰 패턴 분석을 통해 다음을 추출합니다.

항목설명
장소병원명, 도로명, 행정동, 관광지명 등
장소타입1: 특정 장소 / 2: 인근 지역 / 3: 현재 위치
시점“지금”, “오늘 오후”, “내일”, “2025.12.09 14시” 등
시점타입1: 실시간 정보 / 2: 예측 정보 / 3: 과거 데이터 조회
Q: “한라병원 지금 응급실 가능?”
user_state["user_question"] = {
  "장소": "한라병원",
  "장소 타입": "1",
  "시점1": "지금",
  "시점2": "2025.12.09. 14:00",
  "시점 타입": "1"
}

3. 정적 SQL 기반 DB 조회

사용자 질문 분석을 통해 추출된 카테고리 / 내부 처리 코드 / 장소 / 장소타입 / 시점 / 시점타입을 조합하여 DB 조회에 필요한 정적 WHERE 조건 파라미터를 구성합니다.
일반적인 Text-to-SQL 방식은 JOIN 오류, 컬럼명 불일치, 조건 해석 오류 등으로 인해 공공 안전 서비스 관점에서 오작동 리스크가 크기 때문에 본 사업에서는 사용하지 않습니다.대신, 카테고리·질문 유형별로 조회 대상 테이블을 사전에 고정하고 SELECT * FROM 대상테이블 WHERE 기본 식별 조건 형태의 검증된 정적 SQL만 사용합니다.

# SQL 일부분
if qtype == 1:
    # value와 정확히 일치하는 1건 (한글 컬럼들만 SELECT)
    sql = text(f"""
        SELECT
            {SELECT_COLS}
        FROM {SCHEMA}.{TABLE} AS t
        WHERE t."{NAME_COL}" = :value
        LIMIT 1
    """)
    rows = conn.execute(sql, {"value": value}).fetchall()

elif (qtype == 2)&(value != '없음'):
    # (lon, lat)에서 가까운 순 5건 (Haversine 근사)
    sql = text(f"""
        WITH ranked AS (
            SELECT
                {SELECT_COLS},
                6371000 * acos(
                    LEAST(1, GREATEST(-1,
                        sin(radians(:lat)) * sin(radians(t."{LAT_COL}")) +
                        cos(radians(:lat)) * cos(radians(t."{LAT_COL}")) *
                        cos(radians(t."{LON_COL}") - radians(:lon))
                    ))
                ) AS distance_m
            FROM {SCHEMA}.{TABLE} AS t
        )
        SELECT *
        FROM ranked
        ORDER BY distance_m ASC
        LIMIT 5
    """)
    rows = conn.execute(sql, {"lat": lat, "lon": lon}).fetchall()

elif (qtype == 2)&(value == '없음'):
    # user_info의 (lon, lat)을 기준으로 가장 가까운 5건
    ui = user_state.get('user_info', {})
    lat3 = float(ui.get('lat', 0.0))
    lon3 = float(ui.get('lon', 0.0))

    sql = text(f"""
        WITH ranked AS (
            SELECT
                {SELECT_COLS},
                6371000 * acos(
                    LEAST(1, GREATEST(-1,
                        sin(radians(:lat)) * sin(radians(t."{LAT_COL}")) +
                        cos(radians(:lat)) * cos(radians(t."{LAT_COL}")) *
                        cos(radians(t."{LON_COL}") - radians(:lon))
                    ))
                ) AS distance_m
            FROM {SCHEMA}.{TABLE} AS t
        )
        SELECT *
        FROM ranked
        ORDER BY distance_m ASC
        LIMIT 5
    """)
    rows = conn.execute(sql, {"lat": lat3, "lon": lon3}).fetchall()

else:
    # 지정된 규칙 외 타입은 조회하지 않음
    rows = []

WHERE 절에는 최소한의 식별 조건만 포함하고 SELECT *로 데이터를 모두 가져온 뒤 이후 LLM이 필요한 필드만 필터링하여 활용하도록 설계합니다.
LLM에게는 조회된 데이터를 JSON 형태로 제공하며, 프롬프트에서 “질문과 직접적으로 관련된 필드만 사용하라”는 규칙을 명시하여 불필요한 정보가 포함되지 않도록 조정합니다.

  • 장점과 설명
장점설명
SQL 오류 방지Text-to-SQL처럼 LLM이 SQL을 생성하지 않아 잘못된 조회 원천 차단
감사/검증 용이실행 SQL 패턴이 제한되어 데이터 출처 추적이 명확
공공 서비스 적합위험한 임의 쿼리 실행 및 정보 누락 문제 제거

결과적으로, 잘못된 SQL로 인한 오답 생성 가능성을 구조적으로 차단하여 공공∙안전 서비스가 요구하는 높은 수준의 신뢰성과 책임성을 확보합니다.

4. 조회 결과 한글 및 서술형 컬럼 매핑

DB에 저장된 원본 데이터는 uv, air, addr_num, contenttypeid와 같은 영문 축약 컬럼명과 0, 1, G001, A002 등의 숫자/코드값 중심 구조로 되어 있어 LLM이 바로 이해하거나 자연스럽게 설명하기 어렵습니다.

이를 해결하기 위해, 내부에 LLM 친화적 매핑 레이어를 구현하여 아래와 같이 데이터 의미를 사람 기준으로 자동 변환합니다.

  • 컬럼명 매핑 → 한글 기반 의미 컬럼명 적용
COLUMN_KO_MAP_EMERGENCY = {
  # 기본 정보
  "contact_number": "응급실 전화",
  "hvamyn": "구급차 가용 여부",

  # 기관 정보
  "dutyEmclsName": "응급의료기관분류명",
  "road_address": "주소",
  "name": "기관명",
  "longitude": "병원경도",
  "latitude": "병원위도",
  "dutyEryn": "응급실운영여부(1/2)",

  # 진료시간 C (진료시간(월~일/공휴일) C)
  # 실제 컬럼명은 DB에 맞게 수정해 주세요 (예: monday_c, tuesday_c ...)
  "dutyTime1c": "진료시간(월요일)C",
  "dutyTime2c": "진료시간(화요일)C",
  "dutyTime3c": "진료시간(수요일)C",
  "dutyTime4c": "진료시간(목요일)C",
  "dutyTime5c": "진료시간(금요일)C",
  "dutyTime6c": "진료시간(토요일)C",
  "dutyTime7c": "진료시간(일요일)C",
  "dutyTime8c": "진료시간(공휴일)C",

  # 진료시간 S (진료시간(월~일/공휴일) S)
  "dutyTime1s": "진료시간(월요일)S",
  "dutyTime2s": "진료시간(화요일)S",
  "dutyTime3s": "진료시간(수요일)S",
  "dutyTime4s": "진료시간(목요일)S",
  "dutyTime5s": "진료시간(금요일)S",
  "dutyTime6s": "진료시간(토요일)S",
  "dutyTime7s": "진료시간(일요일)S",
  "dutyTime8s": "진료시간(공휴일)S",
  }
  • 코드값 → 단계/분류 명칭(서술형 의미)
HVAMYN_MAP = {  # 구급차 가용 여부
  "Y": "가능",
  "N": "불가능",
  }

DUTYERYN_MAP = {  # 응급실운영여부(1/2)
  "1": "운영",
  "2": "미운영",
  }

이러한 매핑 정보는 Python 내 사전(dict) 기반 매핑 테이블 또는 DB 내 별도 코드 매핑 테이블로 관리되며, 데이터가 조회되는 즉시 user_state['db_result']에 이미 한글+서술형 의미가 반영된 상태로 저장됩니다.
이 방식은 LLM이 별도 프롬프트 설명 없이도 최소한의 토큰으로 정확한 의미 해석 및 응답 생성을 할 수 있도록 도와주며, 컬럼 인식 오류를 구조적으로 제거합니다.

5. LLM 기반 필터링 및 핵심 정보 추출

LLM은 DB 결과 중 질문 의도와 직접적으로 연결되는 필드(컬럼)만 선별합니다.
불필요한 정보는 자동 제거하며, 현재 서비스 품질 기준에 따라 최대 5건까지 결과 제한 적용합니다.

sql = text(f"""
      WITH ranked AS (
          SELECT
              {SELECT_COLS},
              6371000 * acos(
                  LEAST(1, GREATEST(-1,
                      sin(radians(:lat)) * sin(radians(t."{LAT_COL}")) +
                      cos(radians(:lat)) * cos(radians(t."{LAT_COL}")) *
                      cos(radians(t."{LON_COL}") - radians(:lon))
                  ))
              ) AS distance_m
          FROM {SCHEMA}.{TABLE} AS t
      )
      SELECT *
      FROM ranked
      ORDER BY distance_m ASC
      LIMIT 5
  """)

PASS/FAIL 안전성 검증 로직을 적용하여 정보 부족 혹은 모호성 존재 시 추측 답변을 생성하지 않도록 차단합니다.

def user_state_evaluation(user_state):
    instruction = "너의 임무는 주어진 사용자 state를 분석하여 DB 조회 결과가 사용자 질문에 대한 답변 생성에 적합한지 판정하는 것이다."

    prompt = """다음 규칙에 따라 [사용자 정보]의 user_state를 평가하여 최종 결과를 PASS 또는 FAIL 중 하나로만 출력하시오.

# 역할
    - 너는 '제주 관광 안전 AI Agent'의 안전성 검증자이다.
    - 절대 사용자 질문에 대한 실제 답변을 생성하지 말고, 오직 'DB 조회 결과가 답변 생성에 적합한지 여부'만 판단한다.
    - DB에 조금이라도 의미 있게 활용할 수 있는 정보가 있으면 가능한 한 PASS를 선택하고, 정말로 거의 활용할 수 없는 경우에만 FAIL을 선택한다.

[규칙 0: 비어 있는 경우 (무조건 FAIL)]
    - user_state['db_result']가 비어 있으면 무조건 FAIL을 출력한다.
        - 키 자체가 없거나, 빈 딕셔너리/리스트인 경우
        - '검색 결과 없음', '조회 결과 없음', '데이터 없음' 등 사실상 데이터가 없다는 의미의 문자열만 있는 경우
    - 이 경우에는 PASS를 선택하는 것이 금지된다.

[규칙 1: PASS 기본 원칙]
    - DB에 실제 데이터가 한 항목이라도 존재하면 기본적으로 PASS를 우선적으로 고려한다.
    - 아래 항목 중 하나라도 만족하면 PASS로 판단한다.

    1) DB 결과에 실제 값이 있는 경우
        - 예: 주소, 전화번호, 병상 수, 날짜/시간, 기상 수치, 사건/사고 내용, 예방 활동 내용, 시설 정보 등
        - 일부 필드가 비어 있더라도, 몇 개라도 채워진 필드가 있으면 PASS 후보이다.
...        

이 단계는 임의 생성(Hallucination)을 차단하는 안전장치 역할을 수행합니다.

6. 최종 자연어 응답 생성

선택된 핵심 정보를 기반으로 사용자가 이해하기 쉬운 자연어 문장으로 답변합니다.

'result': {'
  응급실 전화': '064-740-5159',
  '구급차 가용 여부': '가능',
  '응급의료기관분류명': '권역응급의료센터',
  '주소': '제주특별자치도 제주시 도령로 65 (연동)',
  '기관명': '제주한라병원',
  '병원경도': 126.48507635686154,
  '병원위도': 33.489946745300735,
  '응급실운영여부(1/2)': '운영',
  '진료시간(월요일)C': 1700,
  '진료시간(화요일)C': 1700,
  '진료시간(수요일)C': 1700,
  '진료시간(목요일)C': 1700,
  '진료시간(금요일)C': 1700,
  '진료시간(토요일)C': 1200,
  '진료시간(일요일)C': 0,
  '진료시간(공휴일)C': 0,
  '진료시간(월요일)S': 900,
  '진료시간(화요일)S': 900,
  '진료시간(수요일)S': 900,
  '진료시간(목요일)S': 900,
  '진료시간(금요일)S': 900,
  '진료시간(토요일)S': 900,
  '진료시간(일요일)S': 0,
  '진료시간(공휴일)S': 0
  }

응급상황, 위험 요소가 포함된 질문의 경우 안전 가이드 문구를 자동 포함합니다.

  • 최종 답변 예시:
제주한라병원 응급실 알려드리겠습니다.

기관명 : 제주한라병원
응급의료기관분류명 : 권역응급의료센터
주소 : 제주특별자치도 제주시 도령로 65 (연동, (연동))
응급실운영여부 : 운영
응급실 전화 : 064-740-5159
구급차 가용 여부 : 가능
진료시간 : 목요일: 09:00 ~ 17:00

가슴 통증, 호흡곤란, 의식 저하 등 심한 증상이 있으면 지체하지 말고 119에 신고하시거나 가장 가까운 응급실을 즉시 방문해 주세요.
빠른 쾌유를 빌며, 더 필요하신 정보가 있으면 언제든지 다시 물어봐 주세요.

최종 답변은 데이터 근거 기반 + 안전 중심 정책 준수를 원칙으로 합니다.

Release Note

2025-12-10 ― ver 0.1

  • 초기 작성

대구본부 : (42250) 대구광역시 수성구 알파시티1로 35길 17(텍스톰 베이스, 1층)| 서울본부 : (04534) 서울특별시 중구 을지로 50(을지한국빌딩, 20층)|상주 스마트팜 연구소 : 경상북도 상주시 사벌국면상풍로 604-61(빅데이터센터, 2층)|대표이사 : 전채남|Tel(개발문의) : 053-744-0707|Email : theimc@theimc.co.kr

©COPYRIGHT 2018 The IMC Inc. ALL RIGHTS RESERVED.