jacobhan.me

Engineering · AI Agents

왜 시니어 엔지니어일수록 AI 에이전트를 못 만드는가

지난 수십 년간 좋은 소프트웨어 공학은 모호함을 제거하는 일이었다. 그런데 에이전트는 모호함 위에서 작동한다. 구글 딥마인드의 한 발표가 짚은 다섯 가지 사고 충돌을 따라가 본다.

2026년 6월 3일 · 원발표: Philipp Schmid (Google DeepMind) · 읽는 데 약 12분

소프트웨어 엔지니어가 오래 갈고닦는 직업적 본능이 하나 있다. 모호함을 없애는 것이다. 인터페이스를 엄격하게 정의하고, 타입을 강제하고, "입력 A에 코드 B를 넣으면 항상 출력 C가 나온다"는 등식을 보장한다. 이 등식이 깨지면 그것은 버그이고, 버그는 고쳐야 한다. 좋은 코드란 예측 가능한 코드다.

구글 딥마인드(Google DeepMind)에서 개발자 대상 기술 전파를 맡고 있는 필립 슈미트(Philipp Schmid)는 최근 한 발표와 글에서 흥미로운 역설을 제기했다. 인공지능 에이전트(AI agent)를 만들 때, 경력이 짧은 주니어 엔지니어가 오히려 시니어보다 작동하는 결과물을 더 빨리 내놓는 경우가 잦다는 것이다.

경력이 쌓일수록 엔지니어는 에이전트의 추론 능력과 지시 수행 능력을 덜 신뢰하는 경향이 있다. 그래서 모델과 싸우고, 확률적인 성질을 코드로 "없애려" 든다. Philipp Schmid, "Why (Senior) Engineers Struggle to Build AI Agents" (2025. 11.)

여기서 핵심 단어는 확률적(probabilistic)이다. 전통적 소프트웨어는 결정론적(deterministic)이다. 같은 입력이면 항상 같은 출력이 나온다. 반면 대규모 언어 모델(LLM, Large Language Model)을 두뇌로 삼은 에이전트는 같은 입력에도 매번 조금씩 다른 경로를 밟고, 다른 결과를 낼 수 있다. 시니어가 더 잘 아는 그 모든 "올바른 공학 습관"이, 확률적 시스템 앞에서는 도리어 발목을 잡는다.

슈미트는 이 충돌을 운전에 빗댄다. 전통적 소프트웨어를 만들 때 우리는 교통 관제원이었다. 도로와 신호등과 법규를 직접 소유하고, 데이터가 정확히 어디로 어떻게 흐를지 결정했다. 그런데 에이전트를 다룰 때 우리는 배차원에 가깝다. 운전자(LLM)에게 "런던으로 가달라"고 목적지만 일러주는 사람이다. 그 운전자는 지름길로 갈 수도, 길을 잃을 수도, "이게 더 빨라 보여서"라며 인도로 차를 몰 수도 있다.

비유로 이해하기 · 관제원과 배차원

관제원은 차가 어느 차선으로, 어떤 속도로, 어떤 신호에 멈추는지를 전부 통제한다. 경로의 모든 칸이 그의 손안에 있다. 배차원은 다르다. "이 손님을 강남역까지 모셔다 드리세요"라고만 지시한다. 어느 길로 갈지는 운전자가 그때그때 판단한다. 막히면 돌아가고, 사고가 나면 우회한다.

에이전트를 만든다는 것은 관제원에서 배차원으로 역할을 바꾸는 일이다. 목적지(목표)는 우리가 정하지만, 거기 도달하는 정확한 단계는 더 이상 우리가 정하지 않는다. 코딩 도구를 써본 사람이라면 익숙할 것이다. 중간에 가끔 이상한 짓을 하지만, 결국 원하던 결과에는 도달하는 그 경험 말이다.

아래 그림은 두 패러다임의 차이를 한 장으로 보여준다.

결정론적 소프트웨어 · 관제원 입력 A 코드 B 출력 C 항상 같다 확률적 에이전트 · 배차원 목표 (자연어 지시) LLM 두뇌 경로 ① 경로 ② 경로 ③ 목표 경로는 매번 달라도 도착
위: 결정론적 소프트웨어는 입력에서 출력까지 경로가 고정돼 있다. 아래: 에이전트는 같은 목표라도 매번 다른 경로를 밟지만, 결국 목표에 도달한다.

아래는 슈미트가 든 다섯 가지 사례다. 전통적 공학 습관이 에이전트 시대의 현실과 정면으로 부딪치는 지점들이다. 각각은 "왜 우리가 무심코 잘못된 방식을 택하는가"라는 함정과, "그 대신 무엇을 해야 하는가"라는 처방의 짝으로 이루어져 있다.


사례 1텍스트가 곧 상태다

Text is the New State

전통적 공학에서 우리는 세계를 데이터 구조로 모델링한다. 스키마를 정의하고, 인터페이스를 만들고, 타입을 엄격하게 박는다. 이 방식이 안전하게 느껴지는 이유는 예측 가능하기 때문이다. 그래서 우리는 에이전트도 본능적으로 이 상자 안에 욱여넣으려 한다.

함정  현실의 의도·선호·설정은 이진적이거나 구조적인 경우가 드물다. 사람이 입력하는 것은 구조화된 필드(이산값)가 아니라 자연어(연속값)다.

예를 들어 사용자가 심층 조사 계획을 검토하고 이렇게 말한다고 하자. "이 계획 좋네요. 다만 미국 시장에 집중해 주세요." 결정론적 시스템은 이 문장을 승인 여부: 참/거짓이라는 칸 하나로 욱여넣는다. 그 순간, "미국 시장에 집중"이라는 핵심 맥락은 통째로 잘려나간다. 슈미트는 이를 두고 맥락을 "절제(lobotomize)해버린다"고 표현한다.

결정론적 방식 — 뉘앙스가 사라진다
{
  "plan_id": "123",
  "status": "APPROVED"   // "미국 시장" 지시는 어디로?
}
에이전트 방식 — 의미를 보존한다
{
  "plan_id": "123",
  "text": "계획 좋네요. 다만 미국 시장에 집중해 주세요."
}

텍스트를 그대로 보존하면, 다음 단계의 에이전트가 그 피드백을 읽고("승인하되, 미국 시장 집중") 행동을 동적으로 조정할 수 있다. 사용자 선호도 마찬가지다. 결정론적 시스템은 섭씨 사용: 참이라는 플래그를 저장한다. 반면 에이전트 시스템은 "날씨는 섭씨로 보지만, 요리할 때는 화씨가 편해요"라는 문장을 저장한다. 그러면 에이전트는 작업의 종류에 따라 단위를 알아서 바꾼다.

처방  boolean의 안락함을 버리고 의미(semantic meaning) 자체를 상태로 삼아라. 이제 애플리케이션의 상태를 담는 그릇은 잘 정의된 데이터 구조가 아니라 텍스트(때로는 이미지·음성·영상까지 포함하는 맥락)다.

비유로 이해하기 · 식당 주문서

분식집 키오스크에는 "곱빼기"와 "보통" 버튼이 있다. 그런데 손님이 "면은 곱빼기인데 국물은 좀 적게, 그리고 파는 빼주세요"라고 한다면? 체크박스로는 담을 수 없다. 결국 주방으로 가는 주문서에 손으로 메모를 적게 된다.

에이전트에게 그 메모지가 곧 데이터다. 미리 만들어 둔 칸에 손님을 맞추는 대신, 손님의 말을 그대로 적어 두고 주방(다음 단계)이 알아서 해석하게 한다. 칸을 늘리는 게 아니라, 칸을 없애는 발상의 전환이다.


사례 2통제권을 넘겨라

Hand over Control

마이크로서비스 구조에서 사용자의 의도는 곧 경로(route)에 대응한다. "구독을 해지하겠다"는 의도는 POST /subscription/cancel이라는 종착점으로 연결된다. 엔지니어는 이 경로를 직관적으로 손수 코딩한다. 그런데 에이전트는 다르다. 자연어로 된 단일 진입점이 있고, 두뇌(LLM)가 가용한 도구·입력·지침을 종합해 제어 흐름을 스스로 결정한다.

함정  우리는 그 흐름을 에이전트 안에 하드코딩하려 든다. 하지만 실제 상호작용은 직선으로 흐르지 않는다. 루프를 돌고, 되돌아가고, 방향을 튼다. 해지하려던 사용자가 결국 갱신에 동의하기도 한다.

예전: 의도 분류 + 고정 흐름
"해지 의향"으로 분류 → 미리 정해 둔 해지 절차 실행. 도중에 사용자 마음이 바뀌어도 흐름은 정해진 길로만 간다.
지금: 맥락 기반 동적 판단
사용자: "해지할게요" → 에이전트: "50% 할인 드릴게요" → 사용자: "오, 그럼 유지할게요" → 의도가 통째로 바뀌어도 자연스럽게 따라간다.

예전 방식이라면 이 모든 분기와 예외를 일일이 상태 기계(state machine)로 모델링해야 했다. 사용자가 보일 수 있는 모든 변심과 특수 상황을 미리 그려두는 일은 사실상 불가능에 가깝다.

처방  전체 맥락을 바탕으로 현재 의도를 이해하도록 에이전트를 신뢰하라. 모든 예외 경우를 하드코딩하고 있다면, 그것은 더 이상 AI 에이전트를 만드는 것이 아니다. 통제권을 모델에 넘긴다는 것은, 우리가 더는 순수하게 결정론적인 환경에서 일하지 않는다는 사실을 받아들이는 일이다.


사례 3오류도 입력이다

Errors are just Inputs

전통적 소프트웨어에서 API 호출이 실패하거나 변수가 비어 있으면, 우리는 예외(exception)를 던진다. 프로그램이 즉시 멈추기를 바란다. 그래야 버그를 빨리 찾아 고칠 수 있기 때문이다. 예전에는 이 방식이 합리적이었다. HTTP 요청은 값쌌다. 상품 검색 한 번이 실패하면 그냥 요청을 다시 보내면 됐다. 다시 해도 손해가 크지 않았다.

함정  에이전트는 사정이 다르다. 한 번의 실행이 5분, 15분씩 걸리고 비용도 만만치 않다(슈미트는 한 번에 0.5달러가 들 수 있다고 예시한다). 5단계 중 4단계까지 와서 입력 하나가 잘못돼 실패했다고 전체 실행을 처음부터 다시 돌린다면, 그동안 쌓아 온 맥락도 날아가고 연산 비용도 다시 치러야 한다. 이는 받아들이기 어렵다.

처방  오류를 또 하나의 입력으로 취급하라. 멈춰 세우는 대신, 오류를 붙잡아 에이전트에게 다시 먹이고 복구를 시도하게 한다. 슈미트는 이것이 Go 언어가 이미 하는 방식과 닮았다고 짚는다. Go에서 함수 호출은 값을 반환할 수도, 오류를 반환할 수도 있으며, 둘은 거의 대등하게 다뤄진다. 에이전트도 그렇게 다뤄야 한다.

전통적 방식 — 실패하면 전체 중단 1단계 2단계 3단계 4단계실패! 전체 크래시 → 처음부터 다시 (맥락·비용 소실) 에이전트 방식 — 오류를 되먹여 복구 1단계 2단계 3단계 4단계오류 오류 메시지를 입력으로 되먹임 4단계'복구 5단계 완료
실패 지점에서 전체를 버리는 대신, 오류 메시지 자체를 다음 입력으로 되먹여 에이전트가 스스로 복구하도록 설계한다.
비유로 이해하기 · 길 막힌 내비게이션

내비게이션을 따라 운전하는데 앞길이 갑자기 통제됐다고 하자. 내비가 "경로 실패"라며 차를 멈춰 세우고 출발지로 돌아가라고 하지는 않는다. "전방 통제"라는 새 정보를 입력으로 받아들여, 그 자리에서 우회로를 다시 계산한다.

에이전트의 오류 처리도 이래야 한다. 실패는 여정의 끝이 아니라, 다음 판단에 쓰일 새로운 정보다. 멈추지 말고, 그 정보를 들고 앞으로 나아가도록 설계하는 것이 핵심이다.


사례 4유닛 테스트에서 평가로

From Unit Tests to Evals

테스트 주도 개발(TDD, Test-Driven Development)은 더 견고한 코드를 짜는 데 큰 도움을 준다. 입력 A를 넣으면 출력 C가 나온다는 것을 단언(assert)하고, 그 단언이 깨지지 않도록 코드를 다듬는다. 그런데 에이전트는 유닛 테스트로 검증할 수 없다. 엔지니어가 확률적 시스템에서 이진적 정답을 찾느라 몇 주를 허비하기도 한다.

함정  창의적이거나 추론이 필요한 작업에는 이진적 단언을 쓸 수 없다. "이 이메일을 요약하라"는 작업에는 유효한 정답이 무한히 많다. 그렇다고 LLM을 가짜(mock)로 대체해 테스트하면, 그것은 에이전트를 테스트하는 게 아니라 우리가 짜놓은 문자열 결합을 테스트하는 꼴이 된다.

처방  테스트 대신 평가(eval)로 옮겨가라. 추론은 유닛 테스트할 수 없으니, 신뢰성과 품질을 검증하고 중간 단계를 추적한다. 슈미트는 세 축을 제시한다.

① 신뢰성 — "작동했나?"가 아니라 "얼마나 자주 작동하나?"

에이전트는 같은 입력에도 매번 같은 결과를 보장하지 않는다. 그래서 한 번 성공했는지가 아니라, 여러 번 돌렸을 때 몇 번 성공하는지를 본다. 슈미트는 이를 Pass^k(여러 번 시도했을 때의 통과율) 개념으로 설명한다. 고객 응대 에이전트가 같은 질문에 열 번 중 한 번만 제대로 답한다면, 그것은 프로덕션에 올릴 물건이 아니다.

에이전트를 50번 돌렸을 때 유닛 테스트 사고방식 — "1번 돌려보고 통과" → "통과! 배포하자" (위험한 착각) 평가 사고방식 — "50번 돌려서 성공률 측정" 성공 45회 실패 5 통과율 90% · 품질 점수 4.5 / 5 → 위험을 "관리"할 수 있는 수준
변동성을 0으로 없애는 것이 목표가 아니다. 충분히 높은 통과율과 품질을 확보해 위험을 관리 가능한 범위로 끌어내리는 것이 목표다. 슈미트는 50번 중 45번 성공, 품질 4.5/5면 프로덕션에 올릴 만하다고 본다.

② 품질 — 사람 대신 모델이 채점한다

결과가 주관적인 작업에서는 정성적 평가가 필요하다. 답이 도움이 되는가? 어조가 적절한가? 요약이 정확한가? 이런 판단을 자동화하는 방법이 LLM as a Judge, 즉 또 다른 언어 모델을 심사위원으로 세우는 것이다. 사람 전문가의 평가와 병행하면 신뢰도가 높아진다.

③ 추적 — 정답만 보지 말고 과정을 보라

최종 답만 확인하는 것으로는 부족하다. 중간 단계를 추적(tracing)해야 한다. 답하기 전에 정말 지식 베이스를 검색했는가? 한 사용자에게는 추가 조사 네 단계를 더 밟고, 다른 사용자에게는 토큰을 조금 더 쓰더라도, 우리가 측정하고 싶은 것은 결국 결과의 성공 여부다.

비유로 이해하기 · 채용 면접

지원자가 면접에서 답 하나를 완벽하게 말했다고 바로 채용하지는 않는다. 진짜 궁금한 것은 "이 사람이 열 번 중 몇 번 좋은 결정을 내리는가", 그리고 "어떻게 그 결론에 이르렀는가"이다. 한 번의 정답보다 일관된 성공률과 사고 과정이 중요하다.

에이전트 평가도 같다. 정답 한 번을 단언(assert)하는 대신, 반복 통과율과 추론 경로를 함께 본다. 변동을 없애려 애쓰기보다, 충분히 믿을 만한지를 가늠하는 일이다.


사례 5에이전트는 진화하고, API는 그대로다

Agents Evolve, APIs Don't

지난날 우리는 응용 프로그래밍 인터페이스(API, Application Programming Interface)를 사람 개발자를 위해 설계했다. 암묵적 맥락에 기대고, "깔끔한" 인터페이스를 추구했다. 백엔드를 다뤄본 사람이라면 delete_item 같은 종착점이 너무나 자명하게 느껴질 것이다. 그런데 결정적 차이가 하나 있다. 사람은 맥락을 추론하지만, 에이전트는 문자 그대로 받아들인다. 에이전트는 직역가(literalist)다. 식별자(ID) 형식이 모호하면, 에이전트는 그것을 환각으로 지어낸다.

함정  우리는 "사람 등급" API를 만든다. id라는 변수가 우리에겐 당연히 사용자의 범용 고유 식별자(UUID, Universally Unique Identifier)지만, 에이전트는 그 배경 지식이 없다. 에이전트는 함수의 스키마와 설명 문서(docstring), 도구 정의만 본다. 코드를 직접 보지 못하고, 우리가 그 API를 만들며 쌓은 수년치 맥락도 알지 못한다. 그래서 get_user(id)에 엉뚱하게 이메일이나 이름을 넣을 수 있다.

사람 등급 API — 에이전트가 헷갈린다
delete_item(id)
# id가 정수? UUID? 못 찾으면 무슨 일이?
에이전트 등급 API — 의미를 명시한다
delete_item_by_uuid(uuid: str)
# "아이템을 삭제한다. 못 찾으면 서술적
#  오류 문자열을 반환한다."

처방  에이전트를 위해 쓰인 도구를 만들어라. 장황하더라도 "바보 방지(idiot-proof)" 수준으로 의미를 명시한 타이핑(가령 email 대신 user_email_address)과, 맥락 역할을 하는 서술적 설명 문서가 필요하다. 사람 개발자의 전문성이나 API를 만든 사람의 사정을 전제하지 않는, 스스로를 설명하는(self-documenting) 인터페이스다.

여기서 한 가지 반전이 있다. API는 본래 개발자와 맺은 약속이다. 우리는 그 약속에 기대어 코드를 짜놓고 떠난다. 그래서 get_user_by_id(id)get_user_by_email(email)로 바꾸면 약속이 깨지고, 그에 의존하던 모든 것이 즉시 무너진다. 그런데 에이전트는 다르다. 바뀐 도구 정의를 읽고 그 자리에서 적응한다. 슈미트는 이를 적시 적응(JIT, Just-In-Time adaptation)이라 부른다. 사람은 깨지지만, 에이전트는 다시 읽고 맞춰간다.

비유로 이해하기 · 새 직원에게 건네는 업무 매뉴얼

오래 일한 직원에게는 "그 서류 처리해줘"라고만 해도 통한다. 어느 서랍에서, 어떤 양식으로, 누구에게 보내야 하는지 몸으로 안다. 하지만 첫 출근한 직원에게는 그렇게 말할 수 없다. "왼쪽 서랍의 노란 양식을 꺼내, 이름과 사번을 적고, 3층 총무팀에 제출. 양식이 없으면 비품실에 요청"이라고 하나하나 적어줘야 한다.

에이전트는 매번 새로 출근한 직원과 같다. 우리 머릿속에만 있는 맥락을 도구 설명서에 글로 다 풀어 적어야 한다. 다만 이 신입에게는 특별한 재주가 하나 있다. 매뉴얼이 갱신되면, 다시 읽고 즉시 새 절차대로 일한다.


맺으며 — 신뢰하되, 검증하라

결정론적 시스템에서 확률적 에이전트로 넘어가는 일은 불편하다. 확실성을 의미적 유연성과 맞바꾸는 일이기 때문이다. 우리는 더 이상 정확한 실행 경로를 알지도, 소유하지도 못한다. 제어 흐름을 비결정론적 모델에 넘기고, 애플리케이션 상태를 자연어로 저장한다. 엄격한 인터페이스로 단련된 머리에는 이 모든 것이 잘못된 일처럼 느껴진다.

하지만 에이전트를 결정론적 상자에 욱여넣으려는 시도는, 애초에 에이전트를 쓰는 목적 자체를 무너뜨린다. 확률은 코드로 없앨 수 없다. 평가와 자기수정으로 관리해야 한다. 그렇다고 "신뢰"가 방치를 뜻하지는 않는다. 중간 지점을 찾아야 한다. 슈미트가 다른 글에서 강조하듯, 언제 에이전트 대신 정해진 워크플로우(workflow)를 써야 하는지를 아는 것도 그 균형의 일부다.

슈미트는 발표를 여섯 개의 원칙으로 갈무리한다.

1
신뢰하되 검증하라통제권은 넘기되, 평가로 그 결과를 늘 확인한다.
2
모델과 싸우지 마라"1단계엔 이것, 2단계엔 저것" 식으로 하나의 흐름에 모델을 가두려 하지 않는다.
3
의미를 보존하라이제 모든 것이 맥락이다. 잘 정의된 데이터 구조의 시대가 아니다.
4
복구를 전제로 설계하라모델도 에이전트도 완벽하지 않다. 오래 도는 에이전트일수록 이상한 일이 벌어진다.
5
단언하지 말고 평가하라100% 신뢰할 수는 없다. 몇 번 성공해야 사용자에게 내놓을지, 그 균형점을 찾는다.
6
버릴 것을 전제로 만들어라소프트웨어는 일회용이다. 더 나은 모델과 에이전트가 나오면, 같은 것을 몇 번이고 다시 만들게 된다.

마지막 원칙, "버릴 것을 전제로 만들어라(build to delete)"에서 슈미트는 쓰라린 교훈(The Bitter Lesson)을 언급한다. 이 표현은 본래 강화학습의 선구자 리처드 서튼(Richard Sutton)이 2019년에 쓴 짧은 에세이의 제목이다. 서튼의 원래 주장은 "지난 70년의 인공지능 연구에서 얻은 가장 큰 교훈은, 연산을 활용하는 일반적 방법이 인간이 손수 설계한 지식 기반 방법을 결국 압도한다는 것"이었다. 사람의 영리한 설계는 단기적으로 앞서 보여도, 무어의 법칙을 타고 늘어나는 연산량 앞에서는 결국 밀린다는 이야기다.

슈미트는 이 교훈을 소프트웨어 개발의 관점으로 끌어와 적용한다. 모델이 계속 강해지는 한, 오늘 우리가 모델의 약점을 메우려 정성껏 짠 보조 코드와 우회 장치는 머지않아 불필요해진다는 것이다. 그러니 코드에 과하게 애착을 두지 말고, 다시 만들 것을 전제로 가볍게 짓는 편이 낫다. 엄밀히 말하면 이는 서튼의 원문이 펼친 논지 그 자체라기보다, 그 정신을 엔지니어의 일상으로 옮겨온 해석에 가깝다.

에이전트는 앞으로도 예상치 못한 방식으로 숱하게 실패할 것이다. 그러나 방향은 분명하다. 모호함을 코드로 없애려는 노력을 멈추고, 그 모호함을 견뎌낼 만큼 회복력 있는 시스템을 설계하는 쪽으로. 시니어 엔지니어가 에이전트 앞에서 머뭇거리는 이유는 능력이 부족해서가 아니다. 오히려 너무 잘 단련된 그 본능이, 새로운 규칙의 게임에서는 한 번 내려놓아야 하는 짐이기 때문이다.