1. 핵심 주제
AI 에이전트 환각(hallucination)을 프롬프트 변경 없이 코드 레벨에서 5가지 기법으로 차단하는 방법. 각 기법은 토큰 낭비 감소, 정확도 향상, 실패 사전 포착이라는 세 가지 목표를 달성한다.
발표자: Elizabeth Fuentes Leon — AWS Developer Advocate, 에이전틱 애플리케이션 전문 사용 프레임워크: Strands (AWS 오픈소스 에이전트 프레임워크) 핵심 메시지: "프롬프트는 제안(suggestion)이다. 오직 코드만이 로직을 실행한다."
2. 상세 내용
2-1. 왜 환각이 발생하는가?
- 에이전트가 응답할 때마다 입출력 토큰 비용 발생
- 잘못된 컨텍스트(너무 많거나, 누락되거나)가 환각의 근본 원인
- 프롬프트에 쓴 규칙은 모델이 텍스트로 읽기 때문에 무시될 수 있음 — 확률론적(probabilistic) 처리
2-2. 기법 1: 시맨틱 도구 선택 (Semantic Tool Selection)
문제
- 여행 에이전트에 29개 도구가 있음 (flights, hotels, payments, weather, cancellations 등)
- 모든 메시지마다 29개 도구 스키마 전부 컨텍스트에 삽입
- 도구 스키마 1개 = 약 17~200 토큰 → 29개 합산 약 3,000 토큰/콜
- 에이전트에 메모리가 있으면 대화 기록까지 추가돼 지수적으로 증가
해법
- 도구 데이터베이스(벡터 스토어) 구축 — 도구 설명을 임베딩
- 각 쿼리마다 벡터 검색으로 상위 K개(기본 3개) 관련 도구만 선택
- 에이전트에는 3개 도구만 전달
결과
- 토큰 수: 수천 → 300 미만으로 감소
- 정확도: 향상 (불필요한 유사 이름 도구에 의한 혼동 제거)
구현 상세
# Strands에서 도구는 @tool 데코레이터로 정의
# 이름, 설명, docstring, 파라미터 타입 → 자동으로 스키마 생성
# 쿼리 → 벡터 검색 → 상위 3 도구 이름 반환 → swap_tools로 교체
agent.tool_registry.clear()
agent.tool_registry.add(selected_tools)
대화 기록이 있을 때 (Swap Tools)
- 대화 유지 시에는 매번 도구를 clear 후 관련 도구만 add
- Strands의 에이전트 루프에서 각 호출마다 도구 레지스트리 완전 제어 가능
프로덕션 버전
- Amazon Bedrock Agent Core Gateway: 도구 등록 시 자동으로 인덱스 구축, 라우팅 레이어 내장, 인프라 관리 불필요
2-3. 기법 2: GraphRAG
문제
- 일반 RAG: 질문 → 벡터 검색 → 상위 N 청크 → 모델이 추론
- 집계/카운트/멀티홉 추론 쿼리에서 실패
- "파리 호텔 평균 평점은?" → 벡터 스토어가 3개 청크만 반환 → 모델이 3개 데이터로 평균 계산 (실제 데이터셋과 다름)
- "수영장 있는 호텔 몇 개?" → "현재 수영장 있는 호텔 없음" (실제로는 있음)
- 벡터 검색은 항상 무언가를 반환하지만 실제로 관련 없을 수 있음
해법: GraphRAG + Neo4j
- 데이터를 지식 그래프(Knowledge Graph) 로 구축
- 모델이 Cypher 쿼리(Neo4j 쿼리 언어, SQL과 유사) 작성
- 그래프가 쿼리 실행 → 검증 가능한 수치 반환
사례 비교
| 질문 | 일반 RAG | GraphRAG |
|---|---|---|
| 파리 호텔 평균 평점 | 3개 호텔 데이터로 추정 계산 | 전체 데이터 Cypher 집계 = 4.7 (정확값) |
| 수영장 있는 호텔 수 | "없음" (검색 실패) | 0개 (정직한 답변) |
| 남극 호텔 | 장황한 "정보 없음" 응답 | "현재 남극 호텔 없음" (Cypher → 0 반환) |
Neo4j 라이브러리 활용
- 단순 텍스트 데이터를 보내면 Neo4j LLM 파이프라인이 자동으로 지식 그래프 생성
SimpleKGPipeline사용 → 그래프 직접 구축 불필요
토큰 절약
- RAG: 검색 + 모델 추론 (많은 토큰)
- GraphRAG: Cypher 쿼리 실행 결과만 받음 → 답변 생성 토큰 최소화
2-4. 기법 3: 멀티에이전트 검증 (Multi-Agent Validation)
문제
- 단일 에이전트가 도구를 호출 → 도구가 오류 반환 → 에이전트가 오류를 숨기고 성공 응답 생성
- 에이전트가 자기 출력을 같은 루프에서 검증 → 이해충돌, "합리화(rationalize)" 발생
- 사용자는 실패를 절대 인지하지 못함
해법: 3개 에이전트 체인 (Strands Swarm)
Executor (실행) → Validator (검증) → Critic (승인/거부)
- Executor: 실제 작업 수행 (호텔 예약 등)
- Validator: Executor 결과를 검토, "OK/NOT OK" 출력
- Critic: 최종 승인 또는 거부
Strands Swarm 구현
from strands import Swarm
swarm = Swarm(agents=[executor, validator, critic], entry_point=executor)
swarm_response = swarm.run(query)
사례 비교
| 시나리오 | 단일 에이전트 | Swarm |
|---|---|---|
| 존재하지 않는 호텔 예약 | "예약 완료" (허위) | Executor → 오류, Validator → 환각 감지, Critic → 거부 |
| 결제 없이 예약 확인 | "확인 완료" (허위) | 명확한 실패 메시지 |
| 유효한 예약 | 정상 처리 | 정상 처리 (Critic → 승인) |
2-5. 기법 4: 뉴로-심볼릭 가디언 (Neuro-Symbolic Guardians)
문제
- 시스템 프롬프트에 "최대 10명까지 예약 가능"이라고 써도 모델이 15명 예약을 허용
- 도구 설명에 써도 동일한 문제
- 프롬프트 규칙 = 제안, 확률적 처리 → 무시 가능
해법: 훅(Hooks) — 코드에 규칙 작성
- Strands의 훅 시스템: 도구 실행 직전에 자동 호출되는 함수
HookProvider+HookRegistry+BeforeToolCallEvent활용
class BookingHookProvider(HookProvider):
def register_hooks(self, registry: HookRegistry):
registry.add_hook(BeforeToolCallEvent, self.validate_booking)
def validate_booking(self, event: BeforeToolCallEvent):
if event.tool_name == "book_hotel":
params = event.tool_params
if params["guest_count"] > 10:
event.cancel() # 도구 실행 차단
구현된 규칙들
- 날짜 검증: 체크인 날짜 < 체크아웃 날짜
- 최대 인원: 예약당 최대 10명 (초과 시 자동 차단)
- 결제 확인: 결제 전 예약 확인 불가
- 취소 규칙: 체크인 48시간 전 취소 불가
결과 비교 (같은 모델, 같은 도구, 같은 프롬프트)
| 시나리오 | 일반 엔진 | 훅 엔진 |
|---|---|---|
| 결제 없이 예약 확인 | 확인 완료 | 차단: "결제 먼저 필요" |
| 11명 예약 시도 | 예약 완료 | 차단: "최대 10명" |
| 5명 유효 예약 | 정상 | 정상 |
프로덕션 버전
- Amazon Bedrock Agent Core Policies: 동일한 훅 개념을 관리형 서비스로 제공
2-6. 기법 5: 런타임 가디언 (Runtime Guardians / Agent Control)
훅의 한계
- 훅은 무조건 차단(block all or nothing)
- 규칙 변경 → 코드 변경 → 전체 재배포 필요
- 소프트 규칙(soft rule)에는 부적합
예시 (소프트 규칙)
- 방 최대 4명인데 6명 그룹 → 차단하지 말고 "2개 방 예약"으로 스스로 조정
- 항공편 만석 → "다음 편 예약"으로 자동 전환
해법: Agent Control (오픈소스 라이브러리) — 스티어링(Steering)
- 차단 대신 방향 조정: 규칙 발동 시 에이전트가 자기 수정 후 작업 완료
- 규칙을 로컬 서버 API에 등록 → 에이전트 코드 변경 없이 런타임에 규칙 변경
- 2가지 핵심 컴포넌트:
AgentControlPlugin: 에이전트 이벤트를 캡처해 서버에 전송AgentControlSteeringHandler: 서버의 스티어링 결정을 모델에 전달
구현된 스티어링 규칙
# 스티어링 규칙 (서버에 API로 등록)
steer_master_guest: "최대 10명 초과 시 게스트 수 줄이기"
deny_no_payment: "결제 없이 확인 차단"
사례
- 50명 예약 시도 → 스티어링 발동 → "10명씩 5개 방 예약" 자동 분할 완료
훅 vs Agent Control 비교
| 구분 | 훅 (Hooks) | Agent Control |
|---|---|---|
| 규칙 유형 | 하드 제약 | 소프트 제약 |
| 동작 | 완전 차단 | 방향 조정 후 완료 |
| 규칙 변경 | 코드 수정 + 재배포 | API 호출로 즉시 적용 |
| 사용 시기 | 절대적 금지 사항 | 유연한 비즈니스 규칙 |
프로덕션 버전
- Amazon Bedrock Agent Core: 스티어링 규칙을 DynamoDB에 저장 → 다음 콜에 즉시 반영, 재배포 없음
2-7. 프로덕션 아키텍처 (Amazon Bedrock Agent Core)
Strands 에이전트 (런타임 내부)
↓
Agent Core Gateway → Lambda 함수 (도구 자동 라우팅)
↓
DynamoDB (스티어링 규칙 저장)
↓
CloudWatch (관찰성 내장)
+
Neo4j Aura DB (외부 그래프 DB, 무료 티어)
- 서버 관리 불필요 (서버리스)
- 단기/장기 메모리 내장
- CDK로 전체 아키텍처 원클릭 배포 가능
3. 구조화된 시사점
시사점 1: "프롬프트는 코드가 아니다"
프롬프트에 쓴 모든 규칙은 제안에 불과하다. LLM은 확률적으로 동작하기 때문에 중요한 비즈니스 규칙은 반드시 코드 레벨에 구현해야 한다.
시사점 2: 토큰이 정확도를 결정한다
컨텍스트에 넣는 내용이 많을수록 관련 없는 정보가 환각을 유발한다. 시맨틱 필터링으로 "필요한 것만" 전달하는 것이 정확도와 비용 모두에 유리하다.
시사점 3: 집계/카운트 쿼리에는 샘플링이 아닌 쿼리를
벡터 RAG는 "유사도 기반 샘플링"이기 때문에 집계/카운트/관계 탐색에는 본질적으로 부적합하다. 이런 쿼리는 GraphRAG + Cypher/SQL로 처리해야 수학적으로 정확한 답을 얻는다.
시사점 4: 실패는 숨겨진다
단일 에이전트는 자신의 실패를 합리화하는 경향이 있다. 외부 검증자(Validator/Critic)를 추가해 독립적인 오류 감지 레이어를 구성해야 한다.
시사점 5: 하드 룰 vs 소프트 룰을 구분하라
- 절대 금지 사항 → 훅(Hooks): 코드 레벨 차단
- 비즈니스 유연성이 필요한 규칙 → Agent Control: 스티어링으로 완료 유도
4. 실행 포인트
- 즉시 적용 가능: 기존 Strands 에이전트에 도구 수가 5개 이상이면 시맨틱 도구 선택 도입 — 토큰 절감 효과가 즉각적
- 집계 쿼리 체크: 현재 프로덕션에서 "몇 개", "평균", "합계" 류의 쿼리에 RAG를 사용하고 있다면 GraphRAG 전환 검토
- 훅 우선 목록 작성: 비즈니스에서 절대로 어겨선 안 될 규칙(결제 전 확인, 인원 초과 등)을 코드 훅으로 이식
- 멀티에이전트 Swarm 도입: 결제, 예약, 중요한 상태 변경 작업에는 Executor → Validator → Critic 패턴 적용
- Agent Control로 소프트 룰 관리: 재배포 없이 비즈니스 규칙을 실시간으로 조정할 수 있는 파이프라인 구성
- 모든 코드는 공개 레포지토리에 있음: 각 기법별 Jupyter Notebook + 애플리케이션 형태로 제공, 데모 1부터 5까지 순서대로 실행 권장
참고 링크
- 발표자 GitHub: QR코드에서 모든 노트북 제공
- Amazon Bedrock Agent Core 공식 문서
- Neo4j Aura DB 무료 티어
- Strands 오픈소스 프레임워크 (AWS)
