주니어 개발자가 시니어보다 AI 에이전트를 더 빨리 만들어낸다면 믿으시겠어요? 실제로 일어나는 일입니다. 이유는 간단합니다. 시니어일수록 LLM의 추론 능력과 지시 수행 능력을 믿지 않고, 확률적 특성을 “코드로 제거하려” 하기 때문이죠.

Hugging Face의 ML 엔지니어 Philipp Schmid가 전통적인 소프트웨어 엔지니어링 습관이 AI 에이전트 개발과 충돌하는 5가지 지점을 분석한 글을 발표했습니다. 수십 년간 개발자들이 배워온 “명확한 인터페이스, 타입 안정성, 결정론적 실행”이 오히려 AI 에이전트 개발에서는 걸림돌이 된다는 내용입니다.
출처: Why (Senior) Engineers Struggle to Build AI Agents – Philipp Schmid
교통 관제사에서 디스패처로
전통적인 소프트웨어 엔지니어링은 결정론적입니다. 우리는 교통 관제사 역할을 하죠. 도로도, 신호등도, 법규도 우리가 정합니다. 데이터가 언제 어디로 가는지 정확히 통제합니다.
AI 에이전트 엔지니어링은 확률론적입니다. 우리는 디스패처가 됩니다. 운전사(LLM)에게 지시를 내리지만, 그 운전사는 지름길을 택할 수도, 길을 잃을 수도, 더 빠를 것 같다는 이유로 인도로 운전할 수도 있습니다.
이 근본적인 차이를 받아들이지 못하면 AI 에이전트의 본질과 싸우게 됩니다.
1. 텍스트가 새로운 상태(State)다
전통적인 개발에서 우리는 세상을 데이터 구조로 모델링합니다. 스키마를 정의하고 타입을 엄격히 지정하죠. 안전하고 예측 가능해서 좋습니다. 본능적으로 에이전트도 이 틀에 끼워 맞추려 하게 됩니다.
함정: 실제 세계의 의도, 선호, 설정은 거의 이진적이거나 구조화되어 있지 않습니다. 사용자 입력은 연속적(자연어)이지 불연속적(구조화된 필드)이지 않아요.
딥 리서치 계획 승인 사례를 볼까요? 사용자가 “이 계획 좋은데, 미국 시장에 집중해줘”라고 말했다고 해봅시다. 결정론적 시스템은 이걸 is_approved: true/false로 강제 변환하면서 맥락을 잃어버립니다.
전통적 방식:
{
"plan_id": "123",
"status": "APPROVED" // 뉘앙스가 사라짐
}
에이전트 방식:
{
"plan_id": "123",
"text": "이 계획 좋은데, 미국 시장에 집중해줘"
}
텍스트를 보존하면 다음 에이전트가 피드백(“승인됨, 단 미국 시장 집중”)을 읽고 동적으로 행동을 조정할 수 있습니다.
또 다른 예로 사용자 선호도가 있습니다. 결정론적 시스템은 is_celsius: true를 저장하지만, 에이전트 시스템은 “날씨는 섭씨로, 요리는 화씨로 표시해줘”라는 텍스트를 저장합니다. 에이전트는 작업에 따라 맥락을 동적으로 전환할 수 있죠.
2. 제어권을 넘겨라
마이크로서비스에서는 사용자 의도가 라우트(POST /subscription/cancel)와 매칭됩니다. 에이전트에서는 단일 자연어 진입점이 있고, 뇌(LLM)가 사용 가능한 도구, 입력, 지시사항을 기반으로 제어 흐름을 결정합니다.
함정: 우리는 에이전트에 흐름을 하드코딩하려 하지만, 실제 상호작용은 직선을 따르지 않습니다. 루프를 돌고, 되돌아가고, 방향을 바꿉니다.
- 사용자: “구독 취소하고 싶어요.” (의도: 이탈)
- 에이전트: “50% 할인을 드릴 수 있는데 어떠세요?”
- 사용자: “음, 그럼 괜찮네요.” (의도: 유지)
에이전트가 흐름을 탐색하도록 신뢰하세요. 모든 엣지 케이스를 하드코딩하려 한다면 AI 에이전트를 만드는 게 아닙니다. 에이전트가 전체 맥락을 기반으로 현재 의도를 이해하도록 믿어야 합니다.
3. 에러는 그냥 입력이다
전통적인 소프트웨어에서 API 호출이 실패하거나 변수가 누락되면 예외를 던집니다. 버그를 고칠 수 있도록 프로그램이 즉시 중단되길 원하죠.
함정: 에이전트는 실행에 5분이 걸리고 비용이 $0.50일 수 있습니다. 5단계 중 4번째에서 입력 누락이나 오류로 실패한다면, 전체 실행을 중단시키는 건 받아들일 수 없습니다.
에러는 또 다른 입력일 뿐입니다. 중단 대신 에러를 잡아서 에이전트에게 피드백으로 제공하고 복구를 시도합니다. 에이전트는 에러 메시지를 읽고 “아, 이 필드가 필요하구나”라고 이해한 뒤 다시 시도하거나 사용자에게 명확히 물어볼 수 있습니다.
4. 유닛 테스트에서 Eval로
테스트 주도 개발(TDD)은 견고한 코드 작성에 도움이 되지만, 에이전트는 유닛 테스트할 수 없습니다. 엔지니어들은 확률적 시스템에서 이진적 정확성을 찾으려 몇 주를 낭비하곤 합니다. 우리는 행동을 평가해야 합니다.
함정: 창의적이거나 추론이 필요한 작업에 이진 어서션을 작성할 수 없습니다. “이 이메일을 요약해줘”는 무한히 많은 유효한 답이 있습니다. LLM을 Mock한다면 에이전트를 테스트하는 게 아니라 문자열 연결을 테스트하는 거죠.
테스트보다 Eval을. 추론을 유닛 테스트할 수 없습니다. 신뢰성과 품질을 검증하고 중간 단계를 추적해야 합니다.
- 신뢰성 (Pass^k): “작동했나?”가 아니라 “얼마나 자주 작동하나?”를 물어야 합니다
- 품질 (LLM as a Judge): “답변이 도움이 되나? 톤이 적절한가? 요약이 정확한가?”
- 추적: 최종 답만 확인하지 마세요. 중간 단계를 확인하세요. 답변 전에 지식베이스를 검색했나요?
에이전트가 50번 중 45번 성공하고 품질 점수가 4.5/5라면 프로덕션 준비가 된 겁니다. 우리는 변동성을 제거하는 게 아니라 리스크를 관리하는 겁니다.
5. 에이전트는 진화하고, API는 그렇지 않다
과거에 우리는 인간 개발자를 위한 API를 설계했습니다. 암묵적 맥락과 “깔끔한” 인터페이스에 의존했죠. 인간은 맥락을 추론하지만 에이전트는 그렇지 않습니다. 에이전트는 문자주의자입니다. ID 형식이 애매하면 에이전트는 하나를 환각으로 만들어냅니다.
함정: 우리는 종종 “인간급” API를 만듭니다. 암묵적 맥락에 의존하는 엔드포인트들이죠. 예를 들어 id라는 변수는 우리에게는 명백히 user_unique_identifier(UUID)이고 get_user(id)에 사용할 수 있습니다. 에이전트는 이 맥락이 없어서 get_user(id)에 이메일이나 이름을 넣으려 할 수 있습니다.
에이전트는 장황하고 “바보도 알 수 있는” 시맨틱 타이핑이 필요합니다. 예를 들어 "email" 대신 "user_email_address", 그리고 “맥락” 역할을 하는 매우 상세한 독스트링이 필요합니다.
- 나쁨:
delete_item(id)(ID가 정수? UUID? 없으면 어떻게 됨?) - 좋음:
delete_item_by_uuid(uuid: str)+ 독스트링: “아이템을 삭제합니다. 아이템을 찾을 수 없으면 설명적인 에러 문자열을 반환합니다.”
게다가 에이전트는 적시 적응(Just-in-Time adaptation)을 가능하게 합니다. 일반 API는 개발자에게 하는 약속입니다. 그 API에 의존하는 코드를 커밋하고 떠납니다. API를 get_user_by_id(id)에서 get_user_by_email(email)로 바꾸면 약속을 깨는 거고 모든 게 즉시 망가집니다. 하지만 에이전트는 새 도구 정의를 읽고 거기에 맞춰 조정할 수 있습니다.
신뢰하되 검증하라
결정론적 시스템에서 확률론적 에이전트로의 전환은 불편합니다. 확실성을 시맨틱 유연성과 맞바꿔야 하니까요. 더 이상 정확한 실행 경로를 알지도, 소유하지도 못합니다. 사실상 제어 흐름을 비결정론적 모델에게 넘기고 애플리케이션 상태를 자연어로 저장하는 겁니다.
엄격한 인터페이스로 훈련받은 마음에는 이게 틀리게 느껴집니다. 하지만 에이전트를 결정론적 틀에 억지로 끼워 맞추려는 건 에이전트를 사용하는 목적 자체를 무너뜨립니다. 확률을 코드로 제거할 수 없습니다. Eval과 자기 수정을 통해 관리해야 합니다.
그렇다고 에이전트를 “신뢰한다”는 게 마음대로 두는 걸 의미하진 않습니다. 중간 지점을 찾아야 합니다. 에이전트는 예상치 못한 많은 방식으로 실패할 겁니다. 하지만 궤적은 명확합니다. 모호함을 코드로 제거하려는 시도를 멈추고, 그것을 다룰 만큼 탄력적인 시스템을 엔지니어링해야 합니다.
참고자료:
- Agentic Patterns – Philipp Schmid

답글 남기기