POOOLING FOREST
모든 것을 LLM에게 맡길 수는 없습니다: 코드 기반 워크플로우가 필요한 순간 - AI 에이전트 열풍 속에서 겪은 시행착오를 통해 깨달은 결정적 로직과 확률적 모델의 조화, 그리고 '하이브리
Engineering & Tech

모든 것을 LLM에게 맡길 수는 없습니다: 코드 기반 워크플로우가 필요한 순간

AI 에이전트 열풍 속에서 겪은 시행착오를 통해 깨달은 결정적 로직과 확률적 모델의 조화, 그리고 '하이브리드' 워크플로우 도입기를 공유합니다.

김영태

테크리드

안녕하세요. 풀링포레스트 테크리드 김영태입니다.

요즘 개발자들 사이에서 가장 뜨거운 화두는 단연 'AI 에이전트'입니다. 저 또한 이 흐름에 깊이 매료되어 있었습니다. LLM(거대언어모델)에 적절한 도구(Tools)만 쥐여주면, 그 어떤 복잡한 워크플로우도 알아서 척척 해결해 줄 것이라는 믿음이 있었죠. 실제로 초기 프로토타입을 만들어보면 마법처럼 느껴지기도 합니다. 하지만 막상 실무에 깊숙이 적용해 보니, 뼈저리게 느낀 점이 하나 있습니다. 때로는 우리가 직접 짠 코드가 AI보다 훨씬 낫다는 사실입니다. 오늘은 우리 팀이 내부 에이전트를 구축하며 겪은 시행착오와, 그 결과로 도입하게 된 '하이브리드' 방식에 대해 이야기해 보려 합니다.

이야기는 사내 Slack 채널인 pr-reviews에서 시작되었습니다. 우리 개발팀은 서로의 Pull Request(PR)를 공유하고 리뷰를 요청하는 문화가 있는데, 문제는 PR이 병합(Merge)된 후였습니다. 분명 병합된 코드인데도 채팅방에는 여전히 리뷰 대기 중인 것처럼 남아있어서, 굳이 안 봐도 될 코드를 클릭해 보는 비효율이 발생했죠.

"이거야말로 AI 에이전트로 해결하기 딱 좋은 문제다!"

저는 즉시 Cursor를 켜고 뚝딱뚝딱 에이전트를 만들었습니다. 로직은 간단했습니다. 1. Slack 채널의 최근 메시지 10개를 읽는다. 2. GitHub PR 링크가 있으면 추출한다. 3. GitHub MCP(Model Context Protocol)를 통해 해당 PR의 상태를 확인한다. 4. 상태가 merged 혹은 closed라면 해당 메시지에 :merged: 이모지를 단다.

결과는 어땠을까요? 처음 며칠은 환상적이었습니다. 제가 커피를 마시는 동안 에이전트가 알아서 완료 표시를 달아주니 세상 편하더군요. 팀원들도 "오, 이거 신기한데요?"라며 좋아했습니다.

하지만 문제는 예고 없이 찾아왔습니다. 어느 날, 아직 리뷰가 끝나지도 않은, 심지어 치명적인 버그가 있어 수정이 필요한 PR에 에이전트가 당당하게 :merged: 이모지를 붙여버린 겁니다. 그 바람에 다른 리뷰어들은 "아, 이미 처리됐구나" 하고 넘어가 버렸고, 해당 코드는 릴리즈 직전까지 방치될 뻔했습니다.

등줄기에 식은땀이 흘렀습니다. 기술적으로 분석해 보니, LLM이 도구(Tool)를 사용하는 과정에서 아주 가끔, 하지만 치명적인 확률로 환각(Hallucination)을 일으킨 것이었습니다. 프롬프트를 아무리 깎아도 100%의 신뢰도를 보장할 수는 없었습니다. 여기서 저는 중요한 깨달음을 얻었습니다.

"결정성(Determinism)이 필요한 곳에 확률(Probability)을 쓰지 말자."

PR이 병합되었느냐 아니냐는 True 혹은 False로 명확하게 떨어지는 문제입니다. 이런 결정적인(Deterministic) 문제에 확률적인 LLM을 메인 드라이버로 쓰는 건, 마치 계산기 대신 직감으로 회계를 처리하는 것과 다를 바 없었습니다. 이 문제를 해결하기 위해 소프트웨어적으로 훨씬 더 간단하고, 저렴하고, 빠른 방법인 '코드'로 돌아가기로 했습니다.

그렇다고 에이전트 시스템을 다 뜯어고친 건 아닙니다. 우리는 '코디네이터(Coordinator)'라는 개념을 도입해 유연성을 확보했습니다. 기존의 LLM 기반 워크플로우와 코드 기반 워크플로우를 선택적으로 사용할 수 있게 만든 것이죠.

시스템의 구조를 대략적으로 설명하면 이렇습니다.

우리는 설정 파일에서 coordinator 값을 조정함으로써 에이전트의 '두뇌'를 교체할 수 있게 만들었습니다.

# 기본값: 복잡하고 창의적인 작업은 LLM이 처리
coordinator: llm

# 변경 후: 명확한 규칙이 있는 작업은 스크립트가 처리
coordinator: script
coordinator_script: scripts/check_pr_status.py

coordinatorscript로 설정되면, 이제 LLM이 도구를 선택하는 것이 아니라, 개발자가 작성한 Python 스크립트가 로직을 제어합니다. 중요한 점은 이 스크립트도 LLM과 동일한 도구(GitHub API 접근, Slack 메시지 전송 등)와 컨텍스트에 접근할 수 있다는 것입니다.

즉, "PR 상태 확인"처럼 로직이 명확한 부분은 Python 코드로 if status == 'merged': add_emoji()라고 명시적으로 작성하여 100%의 신뢰도를 확보합니다. 만약 그 과정에서 "이 PR 내용을 요약해 줘" 같은 지능적인 작업이 필요하다면? 그때만 스크립트 내부에서 부분적으로 LLM을 호출(Sub-agent)하면 됩니다.

이 방식은 우리에게 '점진적 향상(Progressive Enhancement)'이라는 새로운 개발 패턴을 선물했습니다.

새로운 워크플로우를 만들 때, 우리는 여전히 LLM으로 시작합니다. 프로토타이핑 속도가 압도적으로 빠르기 때문입니다. 하지만 운영하다가 신뢰성이 떨어지거나 속도가 느리다고 판단되면, Claude나 Gemini 같은 도구의 도움을 받아 해당 프롬프트 로직을 코드로 변환합니다. LLM에게 "이 프롬프트가 하는 일을 Python 스크립트로 짜줘"라고 하면 금방 만들어주니까요.

주니어 개발자분들이나 에이전트 개발에 관심 있는 분들께 꼭 드리고 싶은 말씀이 있습니다. AI 기술이 발전한다고 해서 코딩이 사라지는 것은 아닙니다. 오히려 AI를 '언제 쓰고 언제 쓰지 말아야 할지' 판단하는 엔지니어링 감각이 더 중요해지고 있습니다.

모든 것을 AI에게 맡기려 하지 마세요. AI는 훌륭한 도구지만, 신뢰할 수 있는 시스템을 만드는 건 결국 견고한 코드와 아키텍처입니다. 반복적이고 규칙 기반인 작업은 코드에게, 정말로 '지능'이 필요한 부분은 LLM에게 맡기는 균형 감각을 키우시길 바랍니다. 그것이 바로 현시점에서 가장 실용적이고 강력한 에이전트를 만드는 비결이니까요.

지금 읽으신 내용, 귀사에 적용해보고 싶으신가요?

상황과 목표를 알려주시면 가능한 옵션과 현실적인 도입 경로를 제안해드립니다.