Big Project · AI's EYE 35

Agent 작업 시각화: 투명성보다 신뢰성

Agent가 실제로 일한다는 것을 보여주려면 생각 과정을 실시간으로 그대로 노출해야 할 것 같다. 또 답이 나올 때까지 계속 tool을 호출하게 두는 편이 유연해 보인다.

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

1장: Agent는 생각을 보이지 않아도 일하고 있다

솔라의 미간이 살짝 찌푸려졌다. 모니터에 떠 있는 팀의 기술 블로그 초안, 그중에서도 새로 만든 Agent 시스템에 대한 문단 때문이었다. “Agent의 작업 흐름을 사용자에게 실시간으로 보여주기 위해 SSE(Server-Sent Events)를 사용합니다.” 여기까지는 명쾌했다. 하지만 바로 다음 문장이 솔라의 발목을 잡았다.

“이벤트 스트림은 run_started부터 run_completed까지, 작업의 각 단계를 명확히 전달하지만, 모델의 내부 추론 과정(internal thoughts)은 포함하지 않습니다.”

솔라는 고개를 갸웃거렸다. “언니, 이거 좀 이상하지 않아?” 옆에서 자신의 코드를 다듬고 있던 루나에게 솔라가 말을 걸었다.

“Agent가 진짜로 똑똑하게 일하고 있다는 걸 보여주려면, 어떻게 생각하고 판단하는지 그 과정을 보여줘야 하는 거 아니야? ‘나는 지금 날씨 정보를 찾아서, 그 결과를 바탕으로 옷을 추천해야겠다고 생각 중이야’ 이런 식으로 말이야. 그런데 그냥 ‘작업 시작’, ‘도구 사용 완료’ 같은 기계적인 신호만 주면, 사용자가 뭘 믿고 기다리겠어? 이건 그냥 로딩 아이콘만 빙글빙글 도는 거랑 근본적으로 뭐가 달라?”

루나는 키보드에서 손을 떼고 솔라의 화면을 잠시 들여다보았다. 그리고는 빙긋 웃으며 옆에 있던 빈 노트를 가져왔다.

“그럼 우리가 그 Agent의 사용자가 되어볼까? Agent가 우리에게 보내주는 신호만 가지고, 정말로 답답하기만 한지 한번 따라가 보자.”

루나는 펜을 들고 노트의 맨 윗줄에 이렇게 적었다.

사용자 질문: 오늘 서울 날씨에 맞는 옷 추천해줘.

“자, 솔라 네가 질문을 했어. Agent가 일을 시작하면 제일 먼저 이런 신호가 도착해.” 루나가 노트에 한 줄을 추가했다.

event: run_started

솔라는 시큰둥하게 대답했다. “응, 시작했구나. 그게 다네. 아직 뭘 하는지는 전혀 모르겠고.”

루나는 아무 말 없이 다음 줄을 적었다.

event: tool_started
data: {"tool_name": "get_current_weather", "location": "서울"}

“어?” 솔라의 눈이 조금 커졌다. “날씨 확인 도구를 쓰네. 서울 날씨를 알아보려는 거구나.”

“조금 더 기다려보자.” 루나는 이어서 썼다.

event: tool_completed
data: {"tool_name": "get_current_weather", "output": "현재 기온 25도, 맑음"}

“오! ‘25도에 맑음’이라는 결과까지 가져왔네.” 솔라는 이제 제법 흥미가 생긴 얼굴이었다. “이제 이 정보를 가지고 옷을 추천해주겠지. 반팔에 얇은 카디건 같은 거.”

루나는 펜을 잠시 내려놓았다. “바로 그거야. 너는 지금 Agent의 ‘생각’을 하나도 보지 않았어. 그냥 Agent가 수행한 ‘작업’과 그 ‘결과’만 봤지. 그런데도 다음에 무슨 일이 일어날지 충분히 예상할 수 있었잖아.”

“그러네…” 솔라는 노트를 다시 들여다봤다. run_started, tool_started, tool_completed. 여기에는 어떤 고민이나 망설임도 없었다. 오직 외부 세계와 상호작용한, 검증 가능한 행동의 기록뿐이었다.

루나가 덧붙였다. “만약 이 과정 중간에 Agent의 속마음이라면서 ‘음… 날씨가 25도면 반팔이 좋겠군. 하지만 저녁엔 쌀쌀할 수도 있으니… 가디건을 추천할까, 아니면 바람막이를 추천할까? 사용자의 이전 구매 기록을 참고해볼까?’ 같은 내부 독백이 계속 흘러나왔다면 어땠을 것 같아?”

솔라는 잠시 상상해보더니 고개를 저었다. “처음엔 신기하겠지만, 금방 시끄럽고 어수선하다고 느꼈을 것 같아. 그리고 저게 진짜 AI의 생각인지, 그냥 개발자가 그럴싸하게 꾸며낸 말인지 의심도 들고. 오히려 지금처럼 ‘get_current_weather라는 도구로 실제 정보를 가져왔다’는 사실만 보여주는 게 훨씬 더 믿음이 가. 이건 꾸며낸 게 아니라 진짜 ‘일’을 했다는 증거니까.”

솔라는 무릎을 탁 쳤다. “아하! 알겠다. 투명성은 모든 걸 다 까서 보여주는 게 아니구나. 사용자에게 중요한 건 Agent의 복잡한 내면세계가 아니라, 지금 내 요청을 해결하기 위해 어떤 ‘관측 가능한 행동’을 하고 있는지, 그 행동이 신뢰할 만한 결과를 만들어내고 있는지를 확인하는 거였어.”

“결국, 우리가 보여줘야 할 건 ‘생각의 흐름’이 아니라 ‘작업의 증거’였네.” 그녀는 잠시 생각에 잠겼다가 다시 루나를 보았다.

“그런데 한 가지 궁금한 게 있어. 이렇게 검증된 행동만 보여주는 게 더 깔끔하고 신뢰를 준다는 건 알겠어. 근데 왜 굳이 ‘안’ 보여주기로 결정했을까? 그냥 보여줘도 되는 거 아냐? 기술적으로 어렵지도 않을 텐데. 이걸 적극적으로 숨기는 데에는 다른 이유가 있는 거 아닐까?“

2장: 무제한 Tool 호출은 왜 유연하지 않은가

솔라의 질문이 채 끝나기도 전에, 루나는 말없이 솔라의 화면으로 손을 뻗어 스크롤을 내렸다. 방금 전까지 들여다보던 ‘내부 추론 비공개’ 문단 바로 아래, 새로운 소제목과 내용이 나타났다.

Agent 실행 제어: 무한 루프 및 비용 제어 Agent가 문제 해결을 위해 자율적으로 Tool을 연쇄 호출하는 과정에서, 의도치 않은 무한 루프에 빠지거나 과도한 비용을 유발할 수 있습니다. 이를 방지하기 위해, 저희 시스템은 모델의 Tool 호출 및 재시도 횟수를 최대 5턴으로 제한합니다.

솔라는 눈앞의 문장을 소리 내어 읽었다. 그리곤 다시 고개를 갸웃했다. 방금 전의 질문은 머릿속에서 희미해지고 새로운 의문이 그 자리를 차지했다.

“이것도 이상한데? 생각을 안 보여주는 건 그렇다 쳐. 근데 왜 행동까지 묶어두는 거야? 5번 안에 답을 못 찾으면 그냥 실패 처리하겠다는 거잖아. 복잡한 문제는 5번 이상의 단계를 거칠 수도 있는 거 아니야? 답이 나올 때까지 시도하게 둬야 진짜 똑똑한 Agent지, 이건 너무 뻣뻣한 제약인데.”

솔라의 목소리에는 실망감이 묻어났다. “우리가 만든 Agent는 왜 이렇게 족쇄가 많아? 생각도 숨기고, 행동도 제한하고.”

루나는 이전 장에서 사용했던 노트를 다시 가져와 새 페이지를 펼쳤다. “그 뻣뻣한 제약이 사실은 왜 필요한 보호 장치인지, 그 ‘유연성’의 함정은 무엇인지 한번 시뮬레이션해 보자.”

루나는 페이지를 반으로 나눠 왼쪽에는 ‘제한된 Agent (최대 5턴)’, 오른쪽에는 ‘유연한 Agent (무제한)‘라고 적었다. 그리고 조금 더 까다로운 질문을 상상해서 던졌다.

사용자 질문: 지금 내 위치에서 제일 가까운 영화관의 상영작 중 평점 제일 높은 영화 추천해줘.

“자, 이 질문을 받은 두 Agent가 어떻게 다르게 행동하는지 따라가 보자.”

루나는 먼저 ‘제한된 Agent’ 쪽부터 채워나가기 시작했다.

[제한된 Agent (최대 5턴)]

1. getLocation() -> '강남역'
2. findNearbyCinemas('강남역') -> ['CGV 강남', '메가박스 강남']
3. getPlayingMovies('CGV 강남') -> ['영화 A', '영화 B']
4. getPlayingMovies('메가박스 강남') -> ['영화 B', '영화 C']
5. getRatings(['영화 A', '영화 B', '영화 C']) -> {'A': 9.1, 'B': 8.5, 'C': 9.5}

=> 최종 추천: '영화 C' (평점 9.5) / 총 5턴 소요. 성공.

“깔끔하네. 5번의 도구 호출로 정확히 답을 찾았어.” 솔라가 말했다.

“그럼 이제 ‘유연한’ Agent 차례야.” 루나는 오른쪽 칸에 똑같이 시나리오를 그려나갔다. 하지만 이번엔 작은 변수를 하나 추가했다. “가끔 findNearbyCinemas 도구가, 명확한 영화관 이름 대신 애매한 지명을 결과로 줄 때가 있다고 가정해 보자.”

[유연한 Agent (무제한)]

1. getLocation() -> '강남역'
2. findNearbyCinemas('강남역') -> ['CGV 강남', '코엑스 몰']
3. getPlayingMovies('CGV 강남') -> ['영화 A', '영화 B']
4. getPlayingMovies('코엑스 몰') -> Error: '코엑스 몰'은 영화관이 아닙니다.

“어? 에러가 났네.” 솔라가 말했다. “하지만 무제한이니까 다시 시도하겠지? ‘코엑스 몰’ 안의 영화관을 다시 찾아보려고 할 거야.”

“그렇지. 바로 그 ‘똑똑한’ 시도가 함정이 될 수 있어.” 루나는 다음 단계를 써 내려갔다.

5. findNearbyCinemas('코엑스 몰') -> ['메가박스 코엑스', '코엑스 몰']
   // Agent는 '코엑스 몰'에서 영화관을 찾으려 했고,
   // 도구는 '메가박스 코엑스'와 함께 다시 '코엑스 몰'을 결과로 줬다.

6. getPlayingMovies('메가박스 코엑스') -> ['영화 D', '영화 E'] // 성공!
7. getPlayingMovies('코엑스 몰') -> Error: '코엑스 몰'은 영화관이 아닙니다.

루나는 7번째 줄에 도착하자 펜을 멈추고 4번, 5번, 7번 단계 사이를 화살표로 연결해 동그라미를 그렸다.

“봐. 이 Agent는 ‘메가박스 코엑스’ 정보를 얻는 데 성공했지만, 여전히 ‘코엑스 몰’이라는 해결되지 않은 문제에 집착하고 있어. 그래서 다시 4번과 똑같은 getPlayingMovies('코엑스 몰')을 시도하고, 실패하고, 또 findNearbyCinemas('코엑스 몰')을 호출하겠지. 영원히.”

솔라는 루나가 그린 순환 고리를 멍하니 바라보았다. “아… 무제한으로 풀어줬더니, 다른 창의적인 방법을 찾는 게 아니라 그냥 한 지점에서 계속 헛도는구나. 사용자는 답을 영원히 못 받고, 우리 서버는 저 쓸모없는 API를 계속 호출하면서 비용만 태우고…”

유연하다고 믿었던 방식이 만들어낸 비효율적인 무한 루프. 솔라는 그제야 깨달았다. 제약이 없는 자유가 항상 더 나은 결과로 이어지는 것은 아니었다.

“결국 5턴 제한은 Agent가 멍청해서가 아니라, 길을 잃고 헤맬 가능성을 원천 차단하는 안전장치였네. 자동차 내비게이션이 막힌 길 앞에서 무한정 ‘경로 재탐색’만 외치는 대신, ‘다른 경로로 안내합니다’라고 말해주는 것처럼.”

그녀는 고개를 들어 루나를 보았다. 머릿속에서 두 가지 사실이 하나로 합쳐지는 순간이었다.

“잠깐만… 그럼 알겠다! 지난번의 ‘내부 추론 비공개’랑 오늘의 ‘턴 제한’은 사실 같은 목적을 위한 두 개의 장치였구나. 만약 저 무한 루프에 빠진 Agent의 ‘생각’을 사용자가 실시간으로 보고 있었다면? ‘코엑스 몰에서 영화를 찾아야지… 어, 안 되네? 다시 코엑스 몰에서 영화를…’ 이 과정을 보면서 신뢰가 완전히 박살 났을 거야.”

솔라는 확신에 찬 목소리로 말했다. “결국 ‘관측 가능한 작업 증거’만 보여주고, 그 작업 횟수도 제한하는 건… 불안정하고 지저분한 실패 가능성을 사용자에게 노출하지 않으려는 거였어. 실패하더라도 깔끔하게 ‘5턴 안에 답을 못 찾았다’고 알려주는 게, 끝없이 헤매는 모습을 보여주는 것보다 훨씬 더 신뢰를 지키는 방법이니까.”

생각을 정리한 솔라의 눈빛이 다시 빛났다. “그렇다면, 이 ‘신뢰를 주는 작업 증거’, 즉 사용자가 보게 될 운영 기록(trace)은 어떤 원칙으로 설계해야 가장 효과적일까? 무엇을, 어디까지 보여주는 게 최선일까?“

3장: Agent 작업 시각화: 검증된 운영 Trace의 가치

솔라는 지난 이틀간의 깨달음을 한 장의 그림으로 정리하고 있었다. 언니 루나와 나눴던 대화의 핵심, 즉 ‘내부 추론 비공개’와 ‘실행 턴 제한’이 어떻게 Agent의 신뢰성을 만드는지에 대한 자신만의 도식이었다. 그녀는 빈 페이지 중앙에 ‘사용자’를 그리고, 그 주위로 Agent가 수행하는 작업들을 배치했다.

“자, 봐봐 언니. 내 생각엔 이게 최선인 것 같아.” 솔라는 자신만만하게 노트를 루나에게 내밀었다. 노트에는 다음과 같은 타임라인이 그려져 있었다.

사용자 질문 → [Agent] run_started → tool_started (get_current_weather) → tool_failed (API timeout, attempt 1/3) → tool_started (get_current_weather, retry) → tool_completed (0.8s) → tool_started (recommend_outfit) → tool_completed (1.2s) → [Agent] recommendation_ready

“어때? Agent의 속마음은 보여주지 않지만, 얘가 얼마나 열심히 일하는지는 보여줄 수 있잖아. API 타임아웃이 나서 재시도하는 모습, 각 도구를 실행하는 데 걸린 시간까지. 이렇게 구체적인 ‘노력의 증거’를 보여주면 사용자는 시스템이 투명하고 정직하다고 느끼지 않을까? 그냥 ‘작업 완료’라고 퉁치는 것보다 훨씬 신뢰가 갈 거야.”

루나는 솔라의 도식을 잠시 들여다보았다. 거기에는 복잡성을 과도하게 노출하는 것이 더 유용할 것이라는 솔라의 선한 의도가 담겨 있었다. 루나는 말없이 첫 번째 장에서 썼던 노트를 가져와 솔라의 그림 옆에 나란히 펼쳐놓았다. 거기에는 오직 네 개의 이벤트만 간결하게 적혀 있었다.

run_started
tool_started
tool_completed
recommendation_ready

“두 개의 운영 기록(trace)이 있네.” 루나가 조용히 말했다. “하나는 솔라 네가 제안한 ‘상세한’ 기록, 다른 하나는 우리가 처음 봤던 ‘단순한’ 기록. 그럼 어제 우리가 마주쳤던 최악의 시나리오, 즉 Agent가 5턴 동안 헤매다 결국 실패하는 상황을 이 두 기록으로 보여주면 어떻게 다를까?”

솔라는 잠시 생각에 잠겼다. 그리곤 자신의 ‘상세한 기록’ 방식이 그 상황을 어떻게 중계할지 상상하기 시작했다.

“내 방식대로라면… tool_started (findNearbyCinemas), tool_completed (결과에 '코엑스 몰' 포함), tool_started (getPlayingMovies, '코엑스 몰'), tool_failed (Error, attempt 1/3), tool_retrying… 이런 식으로 실패와 재시도 기록이 5턴 내내 빼곡하게 찍히겠네. 사용자는 그걸 보면서… ‘어? 왜 자꾸 실패하지? 이 Agent 뭔가 불안한데?’ 하고 초조해지겠구나.”

그녀의 목소리가 점점 작아졌다. 자신이 ‘정직한 정보’라고 믿었던 것들이 사실은 ‘불안감의 증폭기’가 될 수 있다는 사실을 깨달은 것이다.

“반면에… 단순한 기록은?” 루나가 물었다.

”…tool_startedtool_completed가 몇 번 반복되다가, 마지막에 그냥 run_failed (reason: Max turns reached) 하나만 뜨겠지.”

솔라는 두 그림을 번갈아 보았다. 하나는 시스템의 모든 헐떡임을 그대로 노출하는 불안한 심전도 그래프 같았고, 다른 하나는 담담하게 최종 검진 결과만 알려주는 의사의 소견서 같았다.

“알겠다…” 솔라가 긴 한숨과 함께 말했다. “사용자는 우리 Agent의 개발자가 아니야. 시스템의 내부 사투에는 관심 없어. 중요한 건 ‘내 요청이 지금 어디쯤 와 있고, 다음 단계로 넘어갈 만한 확실한 성과가 있었는가’ 뿐이었어. 진행 화면은 Agent의 고군분투를 중계하는 창이 아니라, 외부 세계와 상호작용하여 얻어낸 ‘검증된 결과물’을 보여주는 운영 기록이었던 거야.”

솔라는 자신이 그린 복잡한 타임라인 위로 커다랗게 X자를 그었다. 투명성과 상세함은 동의어가 아니었다. 진정한 신뢰는 모든 것을 보여주는 데서 오는 게 아니라, 보여주기로 약속한 것을 ‘책임감 있게’ 보여주는 데서 온다는 것을 이제야 명확히 알 것 같았다.

“내부 추론을 숨기는 것과 턴 횟수를 제한하는 것. 이 두 가지 원칙은 결국 하나의 목표를 향하고 있었네. 바로 ‘깔끔하고 신뢰할 수 있는 운영 기록’을 만들어내기 위한 전제 조건이었던 거야. 실패하더라도 지저분한 과정을 노출하며 신뢰를 잃는 대신, 정해진 규칙 안에서 깔끔하게 실패를 인정하는 쪽이 훨씬 더 믿음직한 시스템이라는 걸.”

생각을 마친 솔라는 새 노트를 펼쳤다. 더 이상 동료에게 보여줄 UI 시안이 아니었다. 팀 위키에 올릴 ‘Agent 운영 Trace 설계 원칙’ 초안이었다. 그녀는 망설임 없이 첫 줄을 적어 내려가기 시작했다.

1. 검증된 결과만 보여준다 (Show Verified Outcomes Only): 내부 연산, 추론, 재시도 같은 중간 과정은 노출하지 않는다. 사용자가 관심을 갖는 것은 Agent가 외부 도구(Tool)를 사용해 성공적으로 얻어낸 ‘결과’뿐이다. 이 기록은 시스템의 작업 증명서다.

2. 과정을 예측 가능하게 만든다 (Make the Process Predictable): 턴(Turn) 제한 같은 명확한 제어 장치를 통해, Agent가 무한정 헤매지 않을 것이라는 믿음을 준다. 성공하든 실패하든, 정해진 한계 안에서 끝난다는 사실 자체가 신뢰의 기반이 된다.

글을 쓰던 솔라는 고개를 들어 루나를 보며 웃었다. 이제는 동료에게 이 시스템의 설계 의도를 자신 있게 설명할 수 있을 것 같았다. 투명성이라는 이름 아래 모든 것을 드러내야 한다는 강박에서 벗어나, 무엇을 보여주고 무엇을 감출지 선택하는 것이야말로 사용자와 신뢰를 쌓는 핵심이라는 것을 깨달았기 때문이다.