에이전트 출력을 위한 가드레일: LLM 호출 전후의 플러그형 검증
요약
에이전트 시스템의 출력 품질을 보장하기 위해 LLM 호출 전후에 적용하는 결정론적 가드레일 구현 방식을 소개합니다. AgentEnsemble 프레임워크를 통해 입력 및 출력 가드레일을 함수형 인터페이스로 구현하여 조합 가능성과 테스트 용이성을 확보하는 방법을 다룹니다.
핵심 포인트
- 프롬프트의 확률적 한계를 극복하는 결정론적 가드레일 계층 도입
- InputGuardrail과 OutputGuardrail을 통한 단계별 검증
- 함수형 인터페이스를 활용한 가드레일의 조합 가능성 및 재사용성 증대
- 상태가 없는(Stateless) 설계를 통한 스레드 안전성 및 유닛 테스트 용이성 확보
에이전트 시스템에서 가장 어려운 문제 중 하나는 모든 프롬프트를 명령의 벽으로 만들지 않으면서 출력 품질을 제한하는 것입니다. LLM에게 3,000자 이내로 작성하라거나, 항상 결론 섹션을 포함하라거나, 경쟁사 제품을 절대 언급하지 말라고 요청할 수 있습니다. 하지만 프롬프트 기반의 제약 조건은 확률적(probabilistic)입니다. LLM이 이를 따를 수도 있고, 따르지 않을 수도 있습니다.
가드레일(Guardrails)은 결정론적(deterministic) 계층입니다. 이들은 LLM 호출 전후에 Java 코드로 실행되며, 프롬프트가 보장할 수 없는 규칙들을 강제합니다.
모델 (The Model)
AgentEnsemble은 가드레일을 두 가지 함수형 인터페이스(functional interfaces)인 InputGuardrail과 OutputGuardrail로 구현합니다. 두 인터페이스 모두 성공 또는 이유를 포함한 실패를 나타내는 GuardrailResult를 반환합니다.
입력 가드레일(Input guardrails)은 LLM에 접속하기 전에 실행됩니다. 만약 하나라도 실패하면 실행이 즉시 중단되며 에이전트의 LLM은 호출되지 않습니다. 출력 가드레일(Output guardrails)은 에이전트가 응답을 생성한 후(그리고 설정된 경우 구조화된 출력 파싱(structured output parsing) 후에) 실행됩니다.
InputGuardrail piiGuardrail = input -> {
String desc = input.taskDescription().toLowerCase();
if (desc.contains("ssn") || desc.contains("credit card")) {
...
두 가지 모두 작업(per-task)별로 설정됩니다:
var task = Task.builder()
.description("Write an executive summary")
.expectedOutput("A concise summary")
...
함수형 인터페이스를 사용하는 이유 (Why Functional Interfaces)
가드레일을 어노테이션(annotation) 기반이나 설정(configuration) 기반이 아닌 함수형 인터페이스로 만들기로 한 선택에는 몇 가지 실질적인 결과가 따릅니다.
첫째, 가드레일은 조합 가능(composable)합니다. 람다(lambdas)를 통해 가드레일을 구축하거나, 이를 결합하거나, 유틸리티 메서드로 감쌀 수 있습니다. 개인정보(PII)를 확인하는 가드레일은 프레임워크 특유의 연결 작업 없이도 앙상블(ensemble) 내의 모든 작업에서 재사용될 수 있습니다.
둘째, 독립적으로 테스트가 가능합니다. 가드레일은 입력에서 결과로 이어지는 순수 함수(pure function)입니다. 앙상블을 구축하거나 LLM을 모킹(mocking)할 필요 없이 유닛 테스트(unit test)를 수행할 수 있습니다.
셋째, 이들은 기본적으로 상태가 없는(stateless) 방식입니다. 가드레일(guardrails)은 동시에(병렬 워크플로에서) 실행될 수 있으므로, 상태가 없는 람다(lambdas)는 본질적으로 스레드 안전(thread-safe)합니다. 만약 상태가 있는(stateful) 검증이 필요하다면, 스레드 안전성은 사용자의 책임입니다.
입력 가드레일(Input Guardrails)이 보는 것
GuardrailInput 레코드는 실행 전 결정을 내리는 데 필요한 모든 정보를 담고 있습니다:
taskDescription()-- 작업 설명 텍스트expectedOutput()-- 예상 출력 사양contextOutputs()-- 이전 컨텍스트 작업의 출력값 (불변)agentRole()-- 실행될 에이전트의 역할
이는 현재 작업뿐만 아니라 상위(upstream) 작업의 출력까지 확인하는 가드레일을 작성할 수 있음을 의미합니다. 예를 들어, 상위의 리서치(research) 작업에서 아무런 결과도 도출되지 않았다면 쓰기(writing) 작업을 거부하는 가드레일을 다음과 같이 작성할 수 있습니다:
InputGuardrail requireResearch = input -> {
boolean hasResearch = input.contextOutputs().stream()
.anyMatch(o -> o.getRaw().length() > 100);
...
출력 가드레일(Output Guardrails)과 타입화된 출력(Typed Output)
작업이 구조화된 출력(structured output)을 위해 outputType을 사용하는 경우, 실행 순서는 다음과 같습니다:
- 입력 가드레일 실행 (LLM 호출 전)
- LLM 실행 및 원시 텍스트(raw text) 생성
- 구조화된 출력 파싱 (JSON 추출 + 역직렬화(deserialization))
- 출력 가드레일 실행 (
rawResponse()와parsedOutput()모두 사용 가능)
즉, 출력 가드레일은 타입화된 Java 객체를 직접 검사할 수 있습니다:
record ResearchReport(String title, List<String> findings, String conclusion) {}
OutputGuardrail findingsGuardrail = output -> {
...
이 지점이 바로 가드레일과 타입화된 출력이 서로를 강화하는 부분입니다. 타입 시스템은 파싱된 객체를 제공하고, 가드레일은 해당 객체에 비즈니스 규칙을 적용할 수 있는 공간을 제공합니다.
다중 가드레일과 평가 순서
작업당 여러 개의 가드레일이 있는 경우 순서대로 평가됩니다. 첫 번째 실패가 발생하면 평가가 중단되며, 이후의 가드레일은 호출되지 않습니다.
var task = Task.builder()
.description("Write an article")
.expectedOutput("An article")
...
단락 중단 (short-circuit) 방식 대신 모든 실패 사례를 수집하고 싶다면, 이를 하나의 가드레일로 구성하십시오:
InputGuardrail compositeGuardrail = input -> {
List<String> failures = new ArrayList<>();
for (InputGuardrail g : List.of(piiGuardrail, roleGuardrail)) {
...
예외 전파 (Exception Propagation)
가드레일이 실패하면 GuardrailViolationException이 발생합니다. 이 예외는 워크플로 실행기 (workflow executor)를 통해 전파되며, 다른 작업 실패와 동일한 패턴을 따라 TaskExecutionException으로 래핑(wrapped)됩니다.
이 예외는 가드레일 유형 (INPUT 또는 OUTPUT), 위반 메시지, 작업 설명, 에이전트 역할과 같은 구조화된 정보를 포함하고 있습니다. 따라서 에러 문자열을 파싱할 필요 없이 실패 사례를 메트릭 (metrics)이나 알림 (alerting) 시스템으로 라우팅할 수 있습니다.
try {
ensemble.run();
} catch (TaskExecutionException ex) {
...
트레이드오프 (The Tradeoff)
가드레일은 의미론적 분석 (semantic analysis)이 아닌 결정론적 검사 (deterministic checks)입니다. 길이 제한을 강제하는 것은 쉽습니다. 하지만 독성 검사 (toxicity check)는 더 어렵습니다. 가드레일 내부에서 외부 분류기 (classifier)를 호출해야 하며, 이는 지연 시간 (latency)을 추가하고 그 자체로 실패 모드 (failure modes)를 가질 수 있습니다.
설계상 가드레일은 의도적으로 단순한 동기 함수 (synchronous functions)로 유지됩니다. 비동기 검증 (async validation), 외부 API 호출 또는 재시도 로직 (retry logic)이 필요한 경우, 가드레일 함수 내부에 이를 구현하면 됩니다. 프레임워크는 검증의 복잡도에 대해 어떠한 의견도 강요하지 않습니다.
이는 가드레일이 구조적 및 정책적 검사—길이 제한, 필수 섹션, 개인정보 (PII) 필터, 역할 기반 액세스 (role-based access), 타입화된 출력에 대한 스키마 검증 (schema validation)—에 가장 유용함을 의미합니다. 의미론적 품질 검사 (semantic quality checks)의 경우에는 단계별 검토 (phase review) 및 작업 성찰 (task reflection) 메커니즘(이전 포스트에서 다룸)이 더 적합합니다.
전체 가드레일 가이드는 AgentEnsemble documentation에서 확인할 수 있습니다.
입력/출력 분리가 적절한 추상화(abstraction)라고 느껴지는지, 아니면 이 두 범주 중 어디에도 깔끔하게 들어맞지 않는 검증 요구사항을 본 적이 있는지 궁금합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기