공지TechKnowGenie 블로그를 오픈했습니다.
AI•읽는 데 약 21분

평가 점수 92% 받고도 실무에서 2주 만에 쫓겨난 HR 에이전트와 12-Factor 원칙

사전 QA 벤치마크 평가에서 92%의 높은 정답률을 기록하고도 배포 2주 만에 전사 퇴출된 HR 에이전트 사건을 분석했습니다. 10번 중 9번을 잘해도 1번의 치명적 날조로 무너지는 B2B 에이전트의 신뢰도 역학, 해커뉴스 149개 현장 증언이 고발한 자율 루프의 실패 원인, 그리고 HumanLayer 12-Factor Agent 원칙과 인간 개입(Human-in-the-Loop) 거버넌스 아키텍처를 심층 분석합니다.

관리자
목차 (6)
  1. RAG 검색기가 정답 문서를 가져왔는데도 가짜 조항을 합성한 이유
  2. 벤치마크 92점과 엔터프라이즈 신뢰도 붕괴의 통계적 역학
  3. 해커뉴스 149개 현장 증언이 고발한 자율 에이전트 루프의 실패
  4. HumanLayer 12-Factor Agents: 제어 흐름의 코드 회수와 컨텍스트 격리
  5. 상태 직렬화 기반의 Human-in-the-Loop 승인 게이트웨이 구현
  6. 자율성의 환상을 걷어낸 자리에 세우는 결정론적 경계선 엔지니어링
[HR 에이전트의 실제 슬랙 답변 로그]
"확인해 드립니다. 사내 취업규칙(연차유급휴가의 이월 및 소멸) 예외 조항에 따라, 
직전 연도에 미사용한 연차 중 최대 4일은 당해 연도 상반기까지 자동으로 이월 처리됩니다. 
따라서 질문자님의 현재 사용 가능 연차는 기본 잔여 3일에 이월 연차 4일이 합산된 총 7일입니다. 
가족 여행 일정에 참고하시기 바랍니다."

배포 2주 차 금요일 오후, 한 직원이 사내 슬랙 HR 에이전트 채널에 “다음 달에 가족 여행을 가려는데 작년에 미사용한 연차가 올해로 이월되었습니까? 지금 쓸 수 있는 휴가가 총 며칠입니까?“라고 질문했을 때 1.2초 만에 돌아온 답변이었습니다. 직원은 조항 명칭까지 또박또박 인용하며 확신에 찬 어조로 안내하는 봇의 답변을 신뢰하고, 주말에 7일 일정의 가족 해외 항공권과 리조트 숙박권을 결제했습니다.

다음 주 월요일 인사팀이 휴가 상신서를 반려하면서 문제가 불거졌습니다. 회사의 취업규칙 어디에도 미사용 연차가 이월된다는 규정은 존재하지 않았기 때문입니다. 오히려 정반대로 “미사용 연차는 회계연도 말일에 전액 소멸한다”라고 명시되어 있었습니다. 항공권 취소 위약금 배상 책임을 두고 부서 간 실랑이가 벌어졌고, 에이전트에 대한 사내 신뢰는 순식간에 무너졌습니다. “열 번 중에 아홉 번을 맞혀도, 한 번 거짓말에 생돈 수백만 원이 날아가고 무단결근 처리될 위험이 있다면 누가 저 봇을 믿고 쓰겠는가?“라는 반응이 지배적이었습니다.

결국 전사 배포 전 사전 QA 평가(Eval Suite 100문항)에서 92.0점(PASS)을 획득했던 에어리아(Airia) 팀의 사내 HR 에이전트는, 현장 배포 14일 만에 전원이 차단되고 퇴출되었습니다.

RAG 검색기가 정답 문서를 가져왔는데도 가짜 조항을 합성한 이유

사건 이후 진행된 기술 포스트모템에서 확인된 역설적인 사실은, 벡터 데이터베이스와 임베딩 검색 파이프라인 자체는 정상 작동했다는 점입니다. 당시 에이전트의 트레이스 로그를 역추적해 보면, 리트리버(Retriever)는 질문과 관련된 사규 원문 청크 두 개를 상위 1, 2순위로 정확히 추출하여 프롬프트 컨텍스트에 전달했습니다.

[실제 RAG 리트리버가 프롬프트에 주입했던 원문 청크]
• 청크 A (취업규칙 연차유급휴가 조항):
  "미사용 연차유급휴가는 당해 연도 말일에 전액 소멸하며, 수당 지급 대상이 아닌 한 익년도로 이월되지 않는다."
• 청크 B (취업규칙 유급병가 조항):
  "업무상 질병 또는 부상으로 인한 유급병가는 부서장 사전 승인 시 최대 4일까지 차기 연도 상반기로 이월하여 사용할 수 있다."

주입된 원문 청크 어디에도 거짓 정보는 없었습니다. 그런데 왜 언어 모델은 이 두 문단을 읽고 “연차유급휴가가 최대 4일 자동 이월된다”라는 치명적인 합성 거짓말(Confabulation)을 만들어 냈을까요?

기술적 원인은 트랜스포머 디코더의 크로스 어텐션 문맥 간섭(Cross-Attention Context Bleed)과 RLHF 미세조정 과정에서 학습된 순응 편향(Sycophancy Bias)의 결합에 있었습니다. 고차원 임베딩 공간에서 연차유급휴가와 유급병가, 그리고 이월이라는 토큰들은 기하학적으로 높은 코사인 유사도를 갖습니다. 두 청크가 단일 프롬프트 윈도우 안에 함께 배치되자, 멀티헤드 셀프 어텐션 레이어가 문장의 주어(유급병가)와 서술어(최대 4일까지 차기 연도 상반기로 이월)를 바인딩하는 과정에서 어텐션 가중치가 인접한 연차유급휴가 토큰으로 간섭을 일으켰습니다.

여기에 사용자의 질문 자체가 “혹시 작년에 안 쓰고 남은 일수가 자동으로 이월되었습니까?“라는 긍정 유도형 구조였습니다. 사용자에게 우호적이고 도움이 되는 문장을 생성하도록 훈련받은 모델은, 이월 불가를 단호하게 알리는 청크 A 대신 사용자의 질문에 부합하는 숫자(최대 4일 이월)가 포함된 청크 B의 서술어를 결합하여 두 조항을 하나로 융합해 버렸습니다. 검색 단계에서 아무리 정확한 문서를 가져오더라도, 생성 레이어가 비정형 자연어를 자유롭게 합성하도록 방치하는 한 이러한 규정 날조는 통계적으로 언제든 재발할 수 있습니다.

벤치마크 92점과 엔터프라이즈 신뢰도 붕괴의 통계적 역학

머신러닝 평가 지표상의 평균 정확도(92%)와 실제 엔터프라이즈 소프트웨어가 요구하는 신뢰도(Reliability) 사이에는 큰 간극이 존재합니다. 다단계 도구 호출과 추론 과정을 거치는 자율 에이전트에서 단일 스텝의 성공 확률이 $p = 0.92$라면, 5번의 스텝이 오류 없이 연속으로 성공할 결합 확률은 $0.92^5 \approx 0.659$로 떨어집니다. 스텝이 10개로 늘어나면 전체 파이프라인의 성공 확률은 $0.92^{10} \approx 43.4%$까지 급감합니다.

더욱이 인사, 재무, 법률, 인프라 운영처럼 명확한 규정과 책임이 요구되는 도메인에서 사용자의 신뢰는 선형적으로 유지되지 않습니다. 9번 정확한 정보를 제공했더라도 단 1번의 허위 확답으로 실질적인 금전적·법적 손실이 발생하면, 사용자의 체감 신뢰도는 90%에 머무는 것이 아니라 즉시 0%로 추락합니다.

12-Factor AI Agent 거버넌스 아키텍처
HumanLayer 12-Factor 원칙에 기반한 결정론적 상태 머신과 Human-in-the-Loop 게이트웨이 아키텍처입니다.

해커뉴스 149개 현장 증언이 고발한 자율 에이전트 루프의 실패

해커뉴스(Hacker News)의 기술 스레드 「Ask HN: 지금 프로덕션에서 실제로 돈을 벌고 있는 AI 에이전트가 있습니까?」에 등록된 149개의 현업 엔지니어 실무 댓글을 분석해 보면, 실패한 프로젝트들과 상용화에 성공한 프로젝트들 사이에 뚜렷한 아키텍처적 차이가 드러납니다.

실패한 시스템들의 공통점은 모델에게 다양한 도구 목록을 통째로 넘겨주고, “목표를 달성할 때까지 스스로 판단하고 행동하라”는 방식의 무제한 자율 루프(while not done Autonomous Loop)를 그대로 운영 환경에 올렸다는 점입니다. 반면 안정적으로 운영되는 상용 시스템들은 모델의 자율성을 철저히 제약하고 있었습니다. 전체 제어 흐름의 대다수를 결정론적 코드(Deterministic State Machine)로 엄격히 통제하고, 언어 모델은 오직 비정형 텍스트를 구조화된 데이터(JSON/Pydantic)로 변환하거나 초안을 생성하는 좁고 격리된 작업에만 호출하고 있었습니다.

비교 항목 자율 루프 에이전트 (Autonomous Loop) 12-Factor 에이전트 (HumanLayer 규격)
제어 흐름 주도권 모델 프롬프트가 다음 단계와 도구 호출을 자율 판단 결정론적 상태 기계(FSM) 코드가 분기 조건 강제
컨텍스트 주입 방식 이전 턴의 전체 대화와 에러 로그가 누적되어 문맥 간섭 유발 해당 스텝에 필요한 최소 단위의 정제된 슬라이스만 주입
도구 실행 권한 에이전트가 데이터베이스 변경 및 외부 알림을 직접 실행 모델은 구조화된 실행 계획(Intent)만 반환, 실행은 코드가 대행
인간의 개입 시점 장애 발생 후 사후 로그 감사 비가역적 작업 직전 동기식 승인 대기 (Human-in-the-Loop)
장애 재현성 비결정적 루프로 인해 동일 조건 재현 곤란 직렬화된 상태 스냅샷을 기반으로 특정 스텝부터 결정론적 리플레이

HumanLayer 12-Factor Agents: 제어 흐름의 코드 회수와 컨텍스트 격리

엔지니어링 팀 휴먼레이어(HumanLayer)가 제시한 12-Factor Agents 방법론은, 클라우드 네이티브 애플리케이션의 토대가 되었던 12-Factor App 원칙을 AI 에이전트 시스템에 적용한 아키텍처 가이드라인입니다. 에어리아 팀의 HR 에이전트 실패 사례를 방어하기 위해 적용해야 할 핵심 원칙들은 다음과 같습니다.

우선 제어 흐름과 상태 머신은 코드가 소유해야 합니다(Own Your Control Flow & State). 에이전트에게 연차 규정을 해석하고 직접 확답하도록 맡기는 대신, 결정론적 백엔드 코드가 사내 인사 데이터베이스에서 해당 직원의 실제 잔여 연차 원장 데이터를 정확한 SQL로 먼저 조회해야 합니다. 또한 검색되는 사규 조항도 카테고리 메타데이터(category == "annual_leave")로 엄격히 필터링하여, 유급병가 조항이 동일한 컨텍스트 창에 유입되는 것을 구조적으로 차단해야 합니다.

또한 인간 개입을 일급 도구 호출로 다루며, 언제든 중단 및 재개가 가능한 무상태 구조로 설계해야 합니다(Contact Humans with Tool Calls & Stateless Reducer). 직원의 휴가 일수 확답, 결제 및 환불 승인, 데이터베이스 수정처럼 책임이 따르는 비가역적 작업(High-Stakes Actions)을 모델이 단독으로 집행해서는 안 됩니다. 에이전트는 구조화된 제안 초안(Draft Proposal) 객체만 생성한 뒤 프로세스 상태를 영속 저장소에 직렬화하여 일시 정지(Suspend)하고, 슬랙이나 이메일 웹훅을 통해 담당자에게 승인을 요청해야 합니다.

상태 직렬화 기반의 Human-in-the-Loop 승인 게이트웨이 구현

다음은 에어리아의 실패 사례를 12-Factor Agents 원칙에 맞추어 재설계한 파이썬 구현 예시입니다. 비정형 자연어 작문을 차단하기 위해 Pydantic 스키마로 모델의 출력을 제한하고, 이월 예외 문의나 일수 확답이 감지되면 프로세스를 일시 정지시켜 인사 담당자의 결재를 거치도록 통제합니다.

import os
import uuid
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from humanlayer import HumanLayer

class PolicyCategory(str, Enum):
    ANNUAL_LEAVE = "annual_leave"
    SICK_LEAVE = "sick_leave"
    EXPENSE_REIMBURSEMENT = "expense_reimbursement"

class StructuredPolicyDraft(BaseModel):
    category: PolicyCategory
    db_verified_remaining_days: int = Field(description="HR DB 원장에서 조회된 실제 잔여 일수")
    is_carryover_allowed_by_rule: bool = Field(description="사규 원문상 이월 허용 여부")
    cited_clause_id: str = Field(description="인용한 단일 사규 조항 식별자")
    requires_hr_exception_approval: bool = Field(description="예외 승인 또는 고위험 확답 필요 여부")
    draft_message_to_employee: str = Field(description="직원에게 전달할 답변 초안")

class AgentExecutionState(BaseModel):
    run_id: str
    employee_id: str
    raw_query: str
    status: str
    draft: Optional[StructuredPolicyDraft] = None
    reviewer_comment: Optional[str] = None

class DeterministicHRAgentRuntime:
    def __init__(self, hr_db_client, vector_store, llm_structured_client, state_store):
        self.hr_db = hr_db_client
        self.vector_store = vector_store
        self.llm = llm_structured_client
        self.state_store = state_store
        self.hl = HumanLayer(
            api_key=os.environ.get("HUMANLAYER_API_KEY", "hl_prod_key"),
            slack_channel="#hr-policy-approvals"
        )

    def start_workflow(self, employee_id: str, query: str) -> AgentExecutionState:
        run_id = f"run_{uuid.uuid4().hex[:10]}"
        
        # 코드가 의도 카테고리를 먼저 분리하여 문맥 간섭 차단
        target_category = self._classify_single_domain(query)
        
        # 모델의 추측 대신 인사 DB 원장에서 정확한 잔여 일수 조회
        emp_ledger = self.hr_db.get_leave_balance(employee_id)
        
        # 메타데이터 필터로 대상 사규 조항만 단일 검색
        strict_clauses = self.vector_store.search(
            query=query,
            filter_dict={"category": target_category.value},
            top_k=1
        )
        
        # 모델은 정제된 조항과 DB 수치를 바탕으로 Pydantic 객체만 생성
        draft: StructuredPolicyDraft = self.llm.generate_structured(
            response_model=StructuredPolicyDraft,
            system_prompt=(
                "주어진 인사 DB 원장 수치와 단일 사규 조항만을 근거로 판단하십시오. "
                "조항에 명시되지 않은 이월 규정을 추론하거나 합성하지 마십시오."
            ),
            context={
                "employee_ledger": emp_ledger,
                "official_clause": strict_clauses[0],
                "user_query": query
            }
        )

        state = AgentExecutionState(
            run_id=run_id,
            employee_id=employee_id,
            raw_query=query,
            status="RUNNING",
            draft=draft
        )

        # 예외 문의나 이월 관련 질의는 프로세스를 즉시 정지하고 인간 승인 웹훅 발송
        if draft.requires_hr_exception_approval or "이월" in query:
            state.status = "PAUSED_FOR_HUMAN"
            self.state_store.save(run_id, state.model_dump_json())
            
            self.hl.request_approval_webhook(
                callback_id=run_id,
                title=f"[HR 에이전트 확답 결재 요청] 직원: {employee_id}",
                summary=(
                    f"질문: {query}\n"
                    f"DB 실측 잔여 연차: {draft.db_verified_remaining_days}일 (이월 가능: {draft.is_carryover_allowed_by_rule})\n"
                    f"인용 조항: {draft.cited_clause_id}\n"
                    f"AI 작성 초안: {draft.draft_message_to_employee}"
                ),
                assignee="@hr-manager-oncall"
            )
            return state

        state.status = "COMPLETED"
        self.state_store.save(run_id, state.model_dump_json())
        return state

    def resume_from_webhook(self, run_id: str, approved: bool, manager_feedback: str) -> str:
        raw_json = self.state_store.load(run_id)
        state = AgentExecutionState.model_validate_json(raw_json)

        if approved:
            state.status = "COMPLETED"
            self.state_store.save(run_id, state.model_dump_json())
            return state.draft.draft_message_to_employee
        
        state.status = "REJECTED"
        state.reviewer_comment = manager_feedback
        self.state_store.save(run_id, state.model_dump_json())
        return (
            f"문의하신 연차 이월 건은 사내 취업규칙에 의거하여 "
            f"차기 연도 이월이 불가합니다. 현재 사용 가능한 잔여 연차는 "
            f"{state.draft.db_verified_remaining_days}일입니다. (담당자 검토 메모: {manager_feedback})"
        )

    def _classify_single_domain(self, query: str) -> PolicyCategory:
        if "병가" in query or "질병" in query:
            return PolicyCategory.SICK_LEAVE
        if "경비" in query or "영수증" in query:
            return PolicyCategory.EXPENSE_REIMBURSEMENT
        return PolicyCategory.ANNUAL_LEAVE

이 구조는 일반적인 챗봇 구현과 명확한 차별점을 가집니다. 메타데이터 필터를 통해 연차 질문 시 유급병가 규정이 프롬프트에 혼입되는 것을 원천 차단하여 크로스 어텐션 간섭을 방지했습니다. 또한 직원의 잔여 연차 일수를 모델이 계산하도록 맡기지 않고, 데이터베이스 원장에서 확인된 정숫값을 스키마 필드에 바인딩했습니다. 나아가 프로세스가 백그라운드 메모리를 계속 점유하는 대신 상태를 직렬화하여 영속 저장소에 보관한 뒤, 담당자의 슬랙 승인 액션이 도달했을 때 비동기로 재개되는 내구성 실행(Durable Execution) 형태를 갖추었습니다.

자율성의 환상을 걷어낸 자리에 세우는 결정론적 경계선 엔지니어링

많은 조직이 AI 에이전트를 기획할 때 사람의 개입이 전혀 없는 완전 무인 자동화를 이상적인 목표로 설정합니다. 그러나 확률적으로 다음 토큰을 생성하는 언어 모델에게 결정론적 비즈니스 집행 권한까지 전적으로 위임하는 것은 시스템 안정성 측면에서 큰 위험을 동반합니다.

인공지능이 높은 효율을 발휘하는 영역은 방대한 규정 문서에서 관련 조항을 신속하게 검색하고, 데이터베이스 데이터를 대조하여 결재 기안서 초안을 정형화된 데이터로 정리하는 작업입니다. 반면 규정에 대한 최종 해석과 법적·재무적 책임이 따르는 승인 절차는 명확한 권한을 가진 사람이나 결정론적 룰 엔진이 통제해야 합니다. 모델의 역할을 정제된 초안 생성으로 한정하고, 전체 시스템을 엄격한 타입 스키마와 상태 머신, 그리고 적시의 인간 개입(Human-in-the-Loop)으로 감싸는 것이 실무 환경에서 요구되는 엔지니어링의 본질입니다.

← 목록으로