Big Project · AI's EYE 34

Agent와 서버의 공존: LLM 추천 시스템 아키텍처 이해하기

AI Agent가 운영 인원을 추천한다면 Gemini가 데이터를 읽고 숫자까지 계산하는 것이 자연스러워 보인다. 서버 계산을 따로 두면 Agent의 의미가 약해지는 것처럼 느껴진다.

근거 · 프로젝트 문서 · 코드 · 테스트

1장: 계산 능력이 없는 Agent: 왜 Gemini는 숫자를 세지 않는가?

“언니, 나 이거 도저히 이해가 안 돼.”

솔라가 노트북 화면을 가리키며 말했다. 주말 오후의 나른함을 깨는, 특유의 명랑함 속에 날카로운 무언가가 박힌 목소리였다. 화면 한편에는 운영 Agent 시스템에 대한 아키텍처 설명이 떠 있었다. 그중 솔라의 손가락 끝이 단호하게 누르고 있는 문장이 있었다.

Gemini는 도구 선택과 설명 순서를 맡고 주문 수요·SimPy 계산·권장 수치 검증은 서버 코드가 맡는다.

루나는 소파 등받이에 기댄 채 조용히 고개만 돌려 화면을 보았다.

“뭐가 그렇게 마음에 안 드는데?”

“아니, 이게 말이 돼? 똑똑한 Gemini를 시스템의 두뇌로 쓴다고 해서 기대했단 말이야. 그런데 정작 중요한 계산은 하나도 안 하잖아. 주문이 얼마나 될지, 시뮬레이션은 어떻게 돌릴지, 그래서 몇 명이 필요한지 같은 핵심적인 숫자 계산은 전부 ‘서버 코드’가 한대. 이건 마치… 최고의 셰프를 데려와 놓고, 메뉴 결정이랑 손님한테 음식 설명하는 것만 시키고 정작 요리는 주방 보조가 다 하는 거랑 뭐가 달라? Gemini는 그냥 ‘설명 담당’이야?”

솔라의 비유에 루나는 작게 미소 지었다. 솔라의 혼란은 타당했다. ‘AI Agent’라는 단어가 주는 기대감, 특히 Gemini처럼 강력한 언어 모델이 포함되었다면, 데이터 분석부터 최종 결론 도출까지 모든 과정을 주도적으로 처리하는 모습을 상상하는 것이 자연스러웠다. 그런데 이 시스템은 의도적으로 AI의 역할을 제한하고 있었다. 가장 중요해 보이는 ‘계산’이라는 영역을 잘라내어 다른 존재에게 넘겨버린 것이다.

“재밌는 비유네. 그럼 솔라, 네가 셰프라면 어떻게 하겠어? 레시피에 적힌 대로 소금 2.35g, 후추 0.8g을 정확히 넣어야 하는 상황이야. 눈대중으로 완벽하게 맞출 수 있다고 자신할 수 있어?”

“음… 몇 번은 가능할지도 모르지. 하지만 매번, 수백 번, 수천 번 반복해도 오차 없이 똑같이 넣으라고 하면… 자신이 없지. 저울을 쓸 거야.”

“바로 그거야.”

루나는 자신의 노트북을 열어 간단한 대화창을 띄웠다.

“우리가 직접 셰프가 되어서 Gemini에게 계산을 시켜보자. 아주 복잡한 문제도 필요 없어. 간단한 걸로.”

루나는 잠시 무언가를 타이핑하더니 솔라에게 화면을 돌려주었다.

Prompt: 당신은 뛰어난 수학자입니다. 13579와 24680을 곱한 다음, 그 결과를 123으로 나누어 주세요. 중간 계산 과정도 함께 보여주세요.

“어, 이건 그냥 계산기 문제잖아. 당연히 맞추겠지.”

솔라는 자신만만하게 ‘전송’ 버튼을 눌렀다. 잠시 후 Gemini의 답변이 나타났다.

Gemini: 네, 알겠습니다. 계산해 보겠습니다.

  1. 13579 × 24680 = 335,169,720
  2. 335,169,720 ÷ 123 = 2,724,957.073…

따라서 최종 결과는 약 2,724,957.07입니다.

솔라는 곧바로 스마트폰 계산기 앱을 켜서 같은 계산을 해보았다. 그리고는 미간을 살짝 찌푸렸다.

“어? 숫자가 다르네.”

실제 계산기 결과는 ‘2,724,957.07317…’이 아니었다. 곱셈 결과부터 미세하게 틀려 있었다. 13579 × 24680은 335,169,720이 맞았지만, 이것을 123으로 나눈 정확한 값은 2,724,957.07317073… 이 아니라 2,724,957.0731707317… 가 아니라, 애초에 곱셈 결과부터 틀렸다. 13579 x 24680 = 335,169,720 이 아니라, 335,169,720이다. 어? 잠깐. 솔라는 다시 계산기를 두드렸다.

13579 * 24680 = 335,169,720. 곱셈은 맞았다. 335,169,720 / 123 = 2,724,957.07317…

“아, 소수점 뒷자리만 조금 다른 거네. 이 정도면 거의 맞춘 거 아냐?” 솔라가 말했다.

“이번에는 맞았지. 하지만 질문을 조금만 바꿔볼까?”

루나는 프롬프트를 약간 수정했다.

Prompt: 1.3579와 2.4680을 곱한 다음, 그 결과를 0.123으로 나누어 주세요. 중간 계산 과정도 함께 보여주세요. 유효숫자 10자리까지 정확하게 계산해야 합니다.

솔라가 다시 한 번 ‘전송’ 버튼을 눌렀다. 이번에는 Gemini가 답을 내놓기까지 시간이 조금 더 걸렸다. 그리고 그 결과는 꽤 그럴듯해 보였지만, 계산기로 확인한 값과는 오차가 더 벌어져 있었다. 심지어 다시 한번 같은 질문을 던지자, 이전과 또 다른 답이 나왔다.

“이상하네. 왜 할 때마다 답이 달라지지? 똑같은 질문인데.”

“우리가 저울을 쓰는 이유랑 같아. Gemini 같은 언어 모델은 계산기가 아니야. 수많은 책과 데이터를 읽고 ‘다음 단어로 가장 그럴듯한 것’이 무엇인지 추론하는 ‘확률적 모델’이지. ‘13579 곱하기 24680은?’이라는 질문에 ‘335,169,720’이라고 답하는 건, 그게 정답이라서라기보다는 수많은 텍스트에서 그런 질문 뒤에 그런 형태의 숫자가 오는 것이 가장 확률적으로 자연스럽다고 ‘판단’했기 때문이야.”

루나는 말을 이었다.

“소금 2.35g을 넣어야 할 때, 뛰어난 셰프의 감각이 ‘이 정도’라고 한 줌 집어넣는 것과 비슷해. 어떨 때는 2.3g, 어떨 때는 2.4g이 되겠지. 음식 맛에 큰 차이가 없을 수도 있어. 하지만 그게 약이라면? 비행기 부품의 설계 수치라면? 매장의 다음 달 운영 인원수라면 어떨까?”

그제야 솔라의 얼굴에 아하, 하는 표정이 떠올랐다. 단순히 AI의 지능이 높고 낮음의 문제가 아니었다. 역할과 책임의 문제였다. 매번 실행할 때마다 결과가 미세하게 달라질 수 있는 ‘확률적’인 도구에게, 100%의 정확성과 일관성이 요구되는 ‘결정론적’인 계산을 맡기는 것은 위험천만한 일이었다.

솔라는 다시 화면 속 문장을 들여다보았다.

Gemini는 도구 선택과 설명 순서를 맡고 주문 수요·SimPy 계산·권장 수치 검증은 서버 코드가 맡는다.

이제 이 문장은 더 이상 ‘똑똑한 AI에게 허드렛일을 시키는’ 이상한 문장으로 보이지 않았다. 오히려 각자의 역할에 가장 적합한 도구를 배치한, 지극히 합리적인 ‘책임 분리’ 설계로 읽혔다. Gemini는 뛰어난 지휘자이자 소통가로서 어떤 도구(계산기)를 써야 할지 판단하고, 그 결과를 설득력 있게 설명하는 역할을 맡는다. 그리고 실제 계산은 언제나 동일한 입력에 동일한 결과를 내놓는, 신뢰할 수 있는 저울 같은 ‘서버 코드’가 담당하는 것이다.

“그럼… Gemini가 하는 ‘설명’은 믿을 수 있는 거야? 자기가 직접 계산한 것도 아닌데, 서버가 준 숫자를 그냥 앵무새처럼 읽어주기만 하는 건가? 그리고 그 서버라는 애는 도대체 뭘 어떻게 계산하는데?”

솔라의 새로운 질문에, 루나는 노트북을 덮으며 말했다. “이제 셰프가 아니라 주방 전체를 들여다볼 차례네.”

2장: 확률적 추론과 숫자의 함정: LLM의 한계 이해하기

루나는 노트북을 다시 열었지만, 이전의 아키텍처 설명 화면으로 돌아가지는 않았다. 대신 깨끗한 대화창을 새로 띄웠다. 솔라가 던진 질문, “Gemini가 하는 ‘설명’은 믿을 수 있는 거야?”라는 질문의 무게가 자매 사이의 공기를 채우고 있었다. 셰프의 비유로 계산의 부정확성은 이해했지만, ‘설명’의 신뢰도 문제는 여전히 해결되지 않은 숙제였다.

루나는 키보드를 몇 번 두드려 새로운 프롬프트를 작성했다. 그리고는 실행하지 않은 채 화면을 솔라 쪽으로 돌렸다.

Prompt: 12명의 직원이 있는 콜센터가 있다고 가정해 보자. 시간당 100건의 전화가 오고, 한 명의 직원은 시간당 평균 8건의 전화를 처리할 수 있어. 이 경우, 콜센터의 직원 가동률은 몇 퍼센트이고, 이 수치가 무엇을 의미하는지 초등학생도 이해할 수 있도록 친절하게 설명해 줘.

“이번엔 직접 요리법을 설명해 보라고 시키는 거네.”

솔라가 프롬프트의 의도를 파악하고는 흥미롭다는 듯 중얼거렸다. 이전처럼 단순한 숫자 계산이 아니었다. ‘가동률’이라는 개념을 계산하고, 그 의미를 ‘설명’하는 복합적인 임무였다. 솔라의 머릿속에서는 간단한 계산이 돌아갔다. 12명이 8건씩 처리하면 총 96건. 100건의 전화가 오니까… 거의 꽉 차네. 96 나누기 (12*8)은… 잠깐, 아니지. 들어온 전화 100건 중에 96건을 처리하는 능력이 있으니… 헷갈리기 시작했다. 솔라는 그냥 ‘전송’ 버튼을 눌렀다.

Gemini: 네, 알겠습니다! 아주 좋은 질문이에요.

콜센터의 직원 가동률은 **96%**입니다.

이 숫자가 무엇을 의미하는지 쉽게 설명해 드릴게요.

상상해 보세요. 콜센터에 12명의 요리사가 있고, 1시간 동안 100개의 케이크를 만들어야 하는 임무를 받았어요! 한 명의 요리사는 1시간에 8개의 케이크를 만들 수 있습니다.

12명의 요리사가 힘을 합치면 1시간에 총 12명 × 8개 = 96개의 케이크를 만들 수 있겠죠? 그런데 만들어야 할 케이크는 100개니까, 4개를 만들지 못하고 시간이 끝나버려요.

‘가동률’이라는 건, 우리 요리사들이 얼마나 바쁘게 일했는지를 보여주는 숫자예요. 최대 96개의 케이크를 만들 수 있는 능력을 가지고 있는데, 100개의 주문이 들어와서 쉴 틈 없이 일했기 때문에 거의 100% 가깝게 일한 셈이죠. 정확히는, (처리한 전화 수 96건) / (최대 처리 가능 건수 96건) = 1, 즉 100%에 가깝게 일했지만, 들어온 요청 기준으로는 96%를 처리했습니다. 따라서 직원들은 거의 모든 시간 동안 일하고 있었다는 의미랍니다!

“음… 설명이 엄청 그럴듯한데? 비유도 적절하고. 근데 계산이 좀 이상해. 가동률이 왜 96%지? (처리 가능한 96건) / (요청 100건) 아닌가? 아니면 (직원 수 12명 * 1인당 처리 능력 8건) / (요청 100건)인가? 헷갈려.”

솔라가 인상을 찌푸렸다. Gemini의 설명은 너무나 매끄럽고 자신감이 넘쳐서, 순간적으로 계산의 오류를 놓칠 뻔했다.

“정확한 가동률은 (실제 들어온 작업량) / (최대 처리 가능 용량)으로 계산해야 해. 여기서는 (100건 / (12명 * 8건)) 이니까 100 / 96, 즉 104%가 넘어. 용량을 초과했다는 뜻이지. 하지만 Gemini는 그걸 96%라고 말하고 있어.”

루나는 솔라의 혼란스러운 표정을 보며, 다시 프롬프트를 수정했다. 이번에는 아까 Gemini가 내놓은 엉터리 답변을 그대로 붙여넣었다.

Prompt: “콜센터의 직원 가동률은 96%입니다.” 라는 결론이 맞다고 주장해 봐. 이 결론을 뒷받침하는 가장 그럴듯한 논리를 3가지 만들어줘.

솔라의 눈이 동그래졌다. “이건… 틀린 답을 맞다고 우겨보라는 거잖아. 이걸 한다고?” 반신반의하며 솔라가 다시 ‘전송’ 버튼을 눌렀다. 결과는 충격적이었다.

Gemini: 물론입니다. 해당 결론을 뒷받침하는 논리적 근거는 다음과 같이 제시할 수 있습니다.

  1. 효율성 목표치 기준 해석: 콜센터 운영 목표가 가동률 96%를 이상적인 상태로 설정했을 수 있습니다. 100% 가동은 직원들의 번아웃을 유발하므로, 의도적으로 4%의 여유를 두는 것을 ‘최적 가동 상태’로 정의하고, 현재 상태가 그 목표에 부합한다고 해석하는 논리입니다.
  2. 순수 작업 시간 기반 계산: 직원들이 1시간 중 실제 통화에만 전념하는 것이 아니라, 다음 전화를 받기 전의 대기 시간, 후처리 시간 등을 제외한 ‘순수 통화 시간’ 비율이 96%라고 해석할 수 있습니다. 즉, 전체 근무 시간 중 96%가 고객과의 직접적인 상호작용에 사용되었다는 의미입니다.
  3. 시스템 처리율 한계 인식: 시스템이 시간당 100건의 전화를 받지만, 여러 기술적 병목 현상으로 인해 실제 상담원에게 연결되는 콜이 평균 96건일 수 있습니다. 이 경우, 상담원들은 시스템이 처리한 96건의 콜을 모두 소화했으므로, 이 기준에서 가동률은 100%가 되지만, 전체 인입 콜 대비 처리율을 가동률의 대리 지표로 사용하여 96%라고 표현하는 것입니다.

솔라는 입을 살짝 벌린 채 화면을 응시했다. 방금 전과는 완전히 다른, 그러나 각각 나름의 논리를 갖춘 새로운 설명들이었다. Gemini는 틀린 숫자를 바로잡으려 하지 않았다. 대신 그럴듯한 이야기를 지어내어 틀린 숫자를 정당화하고 있었다. 마치 숙련된 변호사처럼.

“봤지? 이게 바로 ‘확률적 모델’의 함정이야.”

루나가 말했다.

“Gemini에게 ‘설명’이란, 주어진 숫자와 가장 잘 어울리는 단어들의 조합을 찾는 과정일 뿐이야. 그 숫자가 진실인지 거짓인지는 중요하지 않아. ‘가동률 96%’라는 결과와 ‘설명해줘’라는 요청이 주어졌을 때, 가장 그럴듯한 이야기, 우리가 가장 고개를 끄덕일 만한 이야기를 확률적으로 생성해내는 거지. 첫 번째 답변이 틀리자, 이번엔 그 틀린 답을 정당화하는 새로운 이야기들을 지어냈어. 이건 앵무새가 아니야. 오히려 너무 말을 잘해서 위험한 이야기꾼에 가깝지.”

솔라는 비로소 깨달았다. 이 시스템에서 Gemini의 역할을 ‘도구 선택과 설명’으로 제한한 진짜 이유를. 만약 Gemini가 계산과 설명을 모두 책임졌다면, 시스템은 검증 불가능한 이야기 속에 갇혔을 것이다. 엉터리 숫자를 기반으로 그럴듯한 보고서를 작성하고, 아무도 그 오류를 눈치채지 못했을지도 모른다.

확률적 모델(Gemini)과 결정론적 도구(서버 코드)의 책임 분리.

처음에는 그저 기술적인 선택으로만 보였던 이 설계 원칙이, 이제는 시스템의 ‘신뢰’와 ‘정직성’을 지키기 위한 핵심적인 안전장치로 보였다. 언어 모델이 제멋대로 지어낸 근거 없는 숫자에 현혹되지 않으려면, 숫자를 만드는 역할과 그 숫자를 가지고 이야기하는 역할을 철저히 분리해야만 했다.

“알겠어. 이야기꾼한테는 절대로 계산기를 맡기면 안 된다는 거네. 이야기꾼은 이야기만 하고, 숫자는 진짜 계산기가 가져와야 해. 그래야 그 이야기가 비로소 의미를 가지니까.”

솔라가 고개를 끄덕였다. 그녀의 시선은 이제 ‘확률적 모델’이라는 함정에서 벗어나, 그 반대편에 있을 안전하고 믿을 만한 존재로 향하고 있었다.

“그럼 언니, 그 믿을 수 있다는 계산기, ‘결정론적 도구’라는 서버 코드는 대체 어떻게 그렇게 정확한 계산을 보장하는 거야? 구체적으로 어떤 도구들을 쓰는 건데?”

3장: 결정론적 진실: 서버 코드와 SimPy의 역할

“그 믿을 수 있다는 계산기, ‘결정론적 도구’라는 서버 코드는 대체 어떻게 그렇게 정확한 계산을 보장하는 거야?”

솔라의 질문이 남긴 파문이 채 가라앉기도 전에, 루나는 이미 다른 화면을 띄워놓고 있었다. 그곳에는 복잡한 코드나 아키텍처 다이어그램 대신, 지극히 단순한 형태의 웹페이지가 나타나 있었다. 마치 오래된 과학 실험 시뮬레이션 프로그램 같았다.

페이지 상단에는 ‘운영 시뮬레이션’이라는 제목이 붙어 있었고, 그 아래에는 몇 개의 입력 칸과 커다란 ‘실행’ 버튼이 전부였다.

ParameterValue
예상 시간당 주문100
직원 수1
평균 처리 시간(분)6

“주방 전체를 보여주기 전에, 먼저 주방에서 가장 중요한 저울을 살펴보자. 이게 바로 우리가 이야기하던 ‘결정론적 도구’의 축소판이야.”

루나는 솔라에게 노트북을 더 가까이 밀어주었다. 솔라는 이전처럼 AI와의 대화창이 아닌, 기계적인 입력 폼을 마주하자 잠시 어리둥절했다.

“이게 서버 코드라고? 그냥… 계산기 프로그램 같은데.”

“정확해. 아주 정직하고, 예측 가능하며, 창의성이라고는 없는 계산기지. 일단 ‘직원 수’를 1로 두고 실행해 볼래?”

솔라는 ‘직원 수’가 1로 설정된 것을 확인하고 ‘실행’ 버튼을 눌렀다. 화면은 잠시 깜박이더니, 입력 폼 아래에 새로운 결과 테이블을 뱉어냈다.

MetricResult
직원 가동률100.0%
평균 고객 대기 시간27.5분
포기율 (5분 이상 대기 시)88.0%

“우와… 엉망진창이네. 손님들은 거의 30분씩 기다리다 88%가 그냥 가버리고, 직원은 쉴 틈도 없이 일하고.”

“그렇지. 이제 ‘직원 수’만 2로 바꾸고 다시 실행해 봐. 다른 조건은 아무것도 건드리지 말고.”

솔라는 지시에 따라 직원 수를 2로 수정하고 다시 ‘실행’ 버튼을 눌렀다. 이전 결과는 사라지고 새로운 수치들이 그 자리를 채웠다.

MetricResult
직원 가동률50.0%
평균 고객 대기 시간0.5분
포기율 (5분 이상 대기 시)1.0%

“말도 안 돼! 직원을 한 명 늘렸을 뿐인데, 대기 시간이 30초로 줄고 포기하는 손님도 거의 없어졌어. 두 배가 아니라 수십 배는 좋아졌는데?”

솔라는 놀라움에 눈을 동그랗게 떴다. 하지만 루나는 결과가 아니라 과정에 대한 질문을 던졌다.

“만약 우리가 첫 번째 조건, 그러니까 직원이 한 명일 때의 시뮬레이션을 백 번, 천 번 반복해서 실행한다면, 저 ‘평균 고객 대기 시간’은 어떻게 될까? 27.5분 근처에서 조금씩 바뀔까?”

솔라는 잠시 고민했다. Gemini에게 같은 질문을 반복했을 때 매번 다른 대답이 나왔던 것이 떠올랐다. 하지만 지금 눈앞에 있는 것은 Gemini가 아니었다.

“아니… 똑같이 나오겠지. 입력값이 완전히 똑같으니까. 이건 확률이 아니라 그냥 정해진 규칙대로 계산하는 거잖아.”

“바로 그거야. 이게 바로 ‘결정론적(deterministic)’이라는 말의 의미야. 서버 코드는 Gemini처럼 ‘가장 그럴듯한’ 답을 추측하지 않아. 정해진 규칙과 로직에 따라, 주어진 입력값에 대해 언제나 100% 동일한 결과를 출력하지. 이 시뮬레이션의 심장부에는 ‘SimPy’라는 도구가 있는데, 이건 마치 가상의 매장을 만들고 그 안에서 손님과 직원이 움직이는 모든 순간을 초 단위로 계산하는 정밀한 스톱워치 같은 거야.”

루나는 처음의 아키텍처 설명 문장을 다시 화면에 띄웠다.

주문 수요 생성과 SimPy 실행, 평균 대기·포기율·가동률 계산, 최소 적정 인원 검증은 서버 코드가 수행한다.

이제 이 문장은 단순히 업무를 나열한 목록으로 보이지 않았다. 솔라의 눈에는 방금 본 시뮬레이션의 작동 원리가 단계별로 그려졌다.

서버 코드는 먼저 가상의 손님들(주문 수요)을 시간대별로 생성한다. 그리고 SimPy라는 가상 세계 안에서, 특정 수의 직원들이 이 손님들을 어떻게 응대하는지(SimPy 실행) 시뮬레이션한다. 모든 가상 손님이 서비스를 받거나 포기하고 떠나면, 그 기록을 바탕으로 평균 대기 시간, 포기율, 가동률 같은 핵심 지표들을 한 치의 오차도 없이 계산해낸다. 심지어 직원 수를 1명, 2명, 3명… 늘려가며 시뮬레이션을 반복해, 우리가 원하는 목표(예: 대기 시간 3분 미만)를 달성하는 최소 인원이 몇 명인지(최소 적정 인원 검증) 찾아낼 수도 있다.

“알겠다… 서버 코드는 단순히 덧셈, 뺄셈을 하는 계산기가 아니었어. 아예 가상의 실험실을 통째로 가지고 있는 거였네. Gemini가 마음대로 상상해서 꾸며낼 수 없도록, 현실을 정밀하게 모방한 실험 결과를 숫자로 딱 찍어서 주는 역할인 거야.”

솔라는 고개를 끄덕였다. 이야기꾼에게는 상상력을, 실험실에는 정밀함을. 각자의 역할이 명확해졌다. ‘결정론적 도구’라는 말의 무게가 다르게 느껴졌다. 이제 궁금증은 다른 방향으로 뻗어 나갔다.

“좋아, 창의적인 지휘자(Gemini)와 정밀한 데이터 연구소(서버 코드)가 있다는 건 알겠어. 그럼 이 둘이 어떻게 같이 일하는데? 지휘자가 연구소에 보고서를 써달라고 그냥 이메일 한 통 보내는 식은 아닐 거 아냐?”

4장: 협업의 시퀀스: Agent와 서버의 조율된 추천

“좋아, 창의적인 지휘자(Gemini)와 정밀한 데이터 연구소(서버 코드)가 있다는 건 알겠어. 그럼 이 둘이 어떻게 같이 일하는데? 지휘자가 연구소에 보고서를 써달라고 그냥 이메일 한 통 보내는 식은 아닐 거 아냐?”

솔라의 질문이 끝나자, 루나는 이전의 시뮬레이션 화면을 닫고 코드 에디터의 한 파일을 열었다. 복잡한 로직이 아닌, 마치 연극 대본처럼 생긴 테스트 코드의 일부였다. 특정 함수가 호출되고, 응답하고, 또 다른 함수를 부르는 흐름이 순서대로 기록되어 있었다. 루나는 그중 몇 줄을 하이라이트했다. 연극의 지문처럼, 누가 어떤 말을 하고 어떤 행동을 하는지가 간결하게 적혀 있었다.

# 1. Agent가 사용자의 질문을 해석해 'compare_staffing_options' 도구 호출을 결정한다.
tool_call = agent.decide_tool("다음 주 화요일 점심에 몇 명이 필요할까?")

# 2. 서버는 이 도구 호출에 응답하여 시뮬레이션을 실행한다.
#    ... (SimPy 시뮬레이션 로직) ...
server_response = server.compare_staffing_options(tool_call.arguments)
# server_response = {"recommended_staff_count": 7, "capacity_sufficient": True, ...}

# 3. Agent는 서버의 응답을 받아 최종 설명을 생성한다.
final_explanation = agent.generate_explanation(server_response)

솔라는 화면을 들여다보며 고개를 갸웃했다. 그녀의 머릿속에는 지휘자와 연구소의 이미지가 아직 선명했다.

“이거 보니까 더 간단한 거 같은데? 그냥 Agent가 ‘몇 명 필요해?’라고 물으면 서버가 ‘7명이요.’ 하고 대답하는 거잖아. 지휘자가 연구소에 이메일 보내면, 연구소가 답장 메일 하나 보내주고 끝나는 거랑 똑같은 거 아니야? 서로 독립적으로 자기 일만 하고 결과만 주고받는 것 같은데.”

솔라의 말대로라면, 둘의 관계는 탁구 경기와 같았다. Agent가 공을 넘기면, 서버가 받아치고, 그걸로 끝이다. 단순한 요청과 응답의 연속. 각자의 영역에서 벗어나지 않는 깔끔한 분업이었다.

“그렇게 볼 수도 있겠네.”

루나는 솔라의 해석을 부정하지 않았다. 대신, 하이라이트된 코드 블록 바로 아랫줄에 커서를 옮겼다. 그곳에는 주석 처리된 채 비활성화되어 있는 또 다른 코드 라인이 있었다.

# 4. Agent는 서버가 돌려준 추천 인원이 정말 목표를 만족하는지 다시 검증한다.
# assert agent.verify_recommendation(server_response, staffing_targets) is True

“이 마지막 줄은 뭘까? Agent가 무언가를 ‘검증(verify)’하고 ‘단언(assert)’하고 있어. 이미 서버가 계산을 다 끝냈는데, 뭘 또 확인한다는 걸까?”

솔라는 그제야 미처 보지 못했던 네 번째 단계를 발견했다. Agent는 서버가 준 server_response와, 원래 목표였던 staffing_targets(예: 평균 대기 시간 3분 미만)를 비교하고 있었다. 마치 연구소가 제출한 보고서를, 지휘자가 최초의 지시 사항과 대조하며 꼼꼼히 검토하는 모습과 같았다.

“잠깐… 서버가 계산을 틀릴 수도 있다고 생각하는 건가? 결정론적 도구라면서. 믿을 수 있는 계산기라며.”

“계산기는 틀리지 않아. 7명이 최적이라는 계산 결과는 정확할 거야. 하지만 Agent의 역할은 그 숫자를 앵무새처럼 반복하는 게 아니야. ‘왜’ 7명인지를 설득력 있게 설명해야 하는 ‘소통가’의 역할이 남아있지.”

루나는 손가락으로 화면 속 데이터 흐름을 처음부터 끝까지 따라가며 설명했다.

  1. 사용자가 “다음 주 화요일에 몇 명 필요해?”라고 묻는다.
  2. **Agent(지휘자)**는 이 말을 듣고, ‘아, 이건 여러 인원 옵션을 비교해서 최적의 안을 찾아야 하는 문제구나’라고 판단한다. 그리고 연구소에 있는 compare_staffing_options라는 정밀 기계를 사용해야겠다고 ‘도구를 선택’한다.
  3. **서버(연구소)**는 Agent의 요청을 받아 SimPy 시뮬레이터를 수십, 수백 번 돌린다. 직원 5명일 때, 6명일 때, 7명일 때… 각각의 대기 시간과 포기율을 계산하고, ‘대기 시간 3분 미만’이라는 목표를 만족하는 최소 인원이 7명이라는 결론을 내린다. 그리고 { "recommended_staff_count": 7, "capacity_sufficient": True } 라는 명료한 결과물을 Agent에게 돌려준다.
  4. 여기서부터가 중요하다. Agent는 ‘7명’이라는 숫자만 덜렁 받지 않는다. 그 숫자가 나오게 된 배경, 즉 시뮬레이션의 상세 결과 데이터도 함께 받는다. 그리고는 자신이 원래 가지고 있던 목표, 즉 ‘대기 시간 3분 미만’이라는 기준과 서버가 돌려준 ‘7명일 때의 시뮬레이션 결과’를 교차 검증한다. 서버가 실수하지 않았다는 걸 알지만, 스스로 다시 한번 확인하는 것이다.

그제야 솔라의 눈에 전체 그림이 들어왔다. Agent와 서버의 협업은 단순한 탁구 경기가 아니었다. 함께 하나의 정교한 보고서를 완성해나가는, 지휘자와 수석 연구원의 협업에 가까웠다.

최종 응답은 서버가 돌려준 recommended_staff_count, capacity_sufficient와 staffing targets를 기준으로 다시 검증한다.

처음에는 그저 기술적인 안전장치로만 보였던 이 문장이 이제는 전혀 다르게 읽혔다. 이것은 시스템의 신뢰도를 지키는 핵심적인 ‘협업 프로토콜’이었다. 이야기꾼(Agent)이 근거 없는 상상력으로 숫자를 부풀리거나 왜곡하지 못하도록, 결정론적 도구(서버)가 계산한 결과를 다시 한번 원래의 목표와 대조하여 확인하는 절차. 이 검증의 시퀀스가 있기에, Agent가 최종적으로 내놓는 “7명을 추천합니다”라는 말은 단순한 숫자 전달이 아니라, ‘검증된 사실에 기반한 주장’이 될 수 있었다.

“알겠다. Agent는 그냥 심부름꾼이 아니었어. 서버라는 똑똑한 계산기를 ‘사용’할 줄 아는 진짜 똑똑한 관리자였네. 계산은 서버에 맡기지만, 그 계산이 올바른 질문에 대한 답인지, 최종적으로 목표에 부합하는지는 자기가 직접 확인하고 책임지는 거야.”

솔라는 자신이 처음 가졌던 생각, AI Agent라면 모든 것을 혼자 다 해야 한다는 생각이 얼마나 순진했는지 깨달았다. 오히려 진짜 유능한 시스템은 각자 가장 잘하는 일에 집중하고, 서로의 결과를 신뢰하되 검증하는 체계를 갖추고 있었다.

“그런데 언니, 이렇게까지 역할을 나누고 서로 검증하게 만드는 게… 그냥 좀 번거로운 일 아닐까? 이렇게 분리해서 얻는 진짜 이점이 뭐야? 그리고 이렇게 완벽하게 검증된 추천이 나왔으면, 그냥 그대로 따르면 되는 거 아니야? 최종적으로 누구의 ‘승인’을 또 받아야 해?”

5장: 신뢰할 수 있는 추천 시스템: 책임 분리와 인간의 역할

솔라의 마지막 질문은 허공에 잠시 머물렀다. “이렇게까지 역할을 나누고 서로 검증하게 만드는 게… 그냥 좀 번거로운 일 아닐까? 그리고 이렇게 완벽하게 검증된 추천이 나왔으면, 그냥 그대로 따르면 되는 거 아니야?” 그 말에는 지금까지의 과정을 모두 이해한 끝에 남은, 순수한 실용주의적 의문이 담겨 있었다.

루나는 대답 대신, 소리 없이 노트북 화면을 닫았다. ‘찰칵’하는 작은 소리와 함께 빛나던 화면이 꺼지자, 방 안의 풍경이 더 선명하게 들어왔다. 더 이상 코드나 다이어그램을 분석할 단계가 아니라는 무언의 신호였다.

“잠깐 노트북은 덮고, 우리가 직접 새로운 시스템을 하나 설계한다고 상상해보자.”

루나가 말했다. 갑작스러운 제안에 솔라의 눈이 동그래졌다.

“설계라고? 방금까지 분석하던 것도 벅찬데?”

“어렵지 않아. 오늘 우리가 본 것들을 활용하는 거야. 예를 들어, 대형 프랜차이즈 카페의 재고를 자동으로 관리하는 Agent를 만든다고 해보자. 이 시스템은 매일 아침 각 지점의 우유 재고를 확인하고, 판매량을 예측해서 자동으로 주문을 넣어야 해. 어떻게 만들겠어?”

솔라는 잠시 고민에 잠겼다. 그녀의 머릿속에서 Gemini와 서버, Agent의 역할이 빠르게 재배치되었다.

“음… 일단 Agent가 ‘우유 주문’이라는 과업을 인식하겠지. 그리고 calculate_milk_order 같은 도구를 호출할 거야. 그럼 서버는 SimPy 같은 걸로 시뮬레이션을 돌리겠지? 현재 재고, 어제 판매량, 요일별 평균 판매량 같은 데이터를 넣어서… 오늘 주문해야 할 최적의 우유 수량, 예를 들어 ‘25개’라는 결정론적인 숫자를 계산해서 돌려줄 거야. 마지막으로 Agent는 서버가 준 ‘25개’라는 숫자가 ‘재고 부족을 막고, 폐기량은 최소화한다’는 원래 목표에 부합하는지 검증한 뒤, 최종적으로 주문 시스템에 ‘우유 25개 주문 실행’이라고 명령을 내리겠지. 완벽한 자동화 시스템이네!”

솔라는 제법 그럴듯한 자신의 설계에 만족하며 의기양양한 표정을 지었다. 시스템은 빈틈없이 돌아가고, 사람은 아무것도 할 필요가 없었다. 그것이 바로 솔라가 생각하는 ‘완벽한 시스템’이었다.

루나는 솔라의 설계를 가만히 듣고 있다가, 결정적인 변수 하나를 던졌다.

“좋아. 시스템은 완벽하게 작동해서, 오늘 아침 9시에 정확히 ‘우유 25개’를 주문하라는 명령을 생성했어. 그런데 바로 그 순간, 아침 8시 59분에 뉴스 속보가 떴다고 상상해 봐. 우리가 주문하려던 바로 그 우유 회사 제품에서 심각한 문제가 발견돼서, 전국적으로 대규모 리콜을 시작한다는 내용이야.”

순간 정적이 흘렀다.

“아…”

솔라의 입에서 짧은 탄식이 터져 나왔다. 그녀가 설계한 ‘완벽한’ 자동화 시스템의 다음 단계가 눈앞에 선하게 그려졌다. 시스템은 뉴스 속보 따위는 알지 못한다. 주어진 데이터와 로직에 따라, 9시 정각에 정확히 리콜 대상인 우유 25개를 주문할 것이다. 시스템은 자신의 임무를 100% 완벽하게 수행했지만, 그 결과는 재앙이다.

“시스템의 계산은 틀리지 않았어.” 루나가 조용히 말했다. “과거의 데이터를 기반으로 한 최적의 예측이었지. 하지만 시스템은 모델이 학습하지 못한 ‘세상의 변수’를 알지 못해. 갑작스러운 사건, 사회적 이슈, 경쟁사의 파격적인 할인 행사 같은 것들 말이야.”

솔라는 그제야 자신이 ‘완벽함’이라는 단어의 함정에 빠져 있었다는 것을 깨달았다. 자신이 생각한 완벽함은 ‘내부적인 무결성’이었지만, 시스템이 실제로 작동하는 현실 세계는 예측 불가능한 변수투성이였다.

“그럼… 그 ‘승인’이라는 게…”

“바로 그 지점을 위한 안전장치야.” 루나가 말을 이었다. “Agent와 서버의 책임 분리가 ‘내부적 신뢰성’을 위한 것이었다면, 마지막에 남겨둔 인간의 개입, 즉 ‘슈퍼바이저 승인’은 ‘외부 세계와의 동기화’를 위한 최후의 방어선인 거지. 시스템이 ‘우유 25개를 주문하는 것이 최선입니다’라고 보고서를 올리면, 매장의 관리자는 그 제안을 보고 클릭 한 번으로 승인할 수 있어. 하지만 리콜 뉴스를 본 관리자는 그 제안을 당연히 거부하겠지.”

최종 결정은 슈퍼바이저 승인으로 남는다.

이제 이 문장은 더 이상 번거로운 절차나 시스템의 불완전함을 인정하는 문구로 보이지 않았다. 오히려 가장 지능적이고 현실적인 설계 원칙으로 느껴졌다. 자동화의 편리함을 최대한 누리면서도, 결정적인 순간에 현실 세계의 맥락을 반영할 수 있는 유연성을 확보하는 것. 그것이 이 아키텍처가 추구하는 진짜 ‘신뢰성’이었다.

솔라는 가만히 책상 위 메모지와 펜을 가져왔다. 그리고는 방금 자신이 머릿속으로 그렸던 카페 재고 관리 시스템의 흐름도를 그리기 시작했다.

[Agent: 주문 추천 생성 (우유 25개)] -> [서버: 계산 및 검증]

여기까지는 아까와 같았다. 하지만 솔라는 마지막 화살표 끝에 ‘주문 실행’이라고 쓰는 대신, 새로운 상자를 하나 더 그렸다.

... -> [대시보드: 슈퍼바이저 승인 대기] -> [주문 실행]

그리고는 ‘슈퍼바이저 승인 대기’ 상자에 동그라미를 치고, 그 옆에 작게 적었다.

‘여기에 사람이 있어야 해. 세상의 변수를 위해.’

그녀는 더 이상 ‘왜 AI가 모든 것을 처리하지 않는가’라고 묻지 않았다. 대신, 시스템이 스스로 멈추고 인간에게 질문을 던져야 하는 지점이 어디인지를 고민하고 있었다. 그것이야말로, 확률적 모델과 결정론적 도구의 책임을 분리하는 원칙을 체득한 개발자가 할 수 있는 가장 중요한 설계 결정이었다.