LLMOps 29

실행 Trace 시점에 Code Evaluator 붙이기: 비동기의 숨겨진 의미

평가 함수를 한번 실행한 것과 운영 trace에 붙인 evaluator를 같은 것으로 보기 쉽다.

근거 · 교안 p107-p110

실행 Trace 시점에 Code Evaluator 붙이기: 비동기의 숨겨진 의미 대표 이미지

1장: 솔라, Code Evaluator의 기본을 배우다

솔라가 노트북을 들고 거실로 나왔다. 화면에는 코드 편집기와 LangSmith 프로젝트 화면이 나란히 떠 있었다. 솔라의 미간이 살짝 찌푸려져 있었다.

“언니, Code Evaluator라는 거, 그냥 내가 만든 평가 함수를 등록만 하면 되는 거 아니었어?”

소파에서 책을 읽던 루나가 고개를 들었다. 솔라는 답답하다는 듯 화면을 가리켰다.

“에이전트가 특정 단어를 포함해서 답하면 1점을 주는 간단한 평가 함수를 만들어서 등록했거든. 그런데 에이전트를 몇 번을 실행해 봐도 아무런 반응이 없어. 평가 결과가 어디에도 안 보여.”

솔라의 말대로 LangSmith의 실행 추적(Trace) 목록에는 방금 실행한 에이전트의 기록이 몇 줄 쌓여 있었지만, 평가 점수를 보여줘야 할 ‘Feedback’ 열은 텅 비어 있었다. 마치 솔라가 정성껏 만든 평가 함수가 세상에 존재하지 않는다는 듯이.

루나는 소파에서 일어나 솔라의 옆으로 다가왔다. 노트북 화면에 떠 있는 솔라의 코드는 간결했다.

# 솔라가 작성한 초기 평가 함수
def check_for_keyword(run, example):
    if "성공" in run.outputs.get("output", ""):
        return 1.0
    else:
        return 0.0

“이 check_for_keyword 함수를 등록한 거고?”

“응. 이름도 명확하고, 로직도 문제없잖아. run 객체에서 출력을 꺼내 ‘성공’이라는 단어가 있는지 확인하는, 아주 간단한 코드야. 이걸 등록했는데 왜 감감무소식인지 모르겠어.”

솔라는 자신이 코드를 잘못 짰을 리는 없다고 생각하는 듯했다. 함수 자체만 놓고 보면 틀린 곳은 없었다.

루나는 말없이 LangSmith의 ‘Evaluators’ 탭을 클릭했다. 솔라가 등록한 평가기 설정 화면이 나타났다. 루나는 아무것도 바꾸지 않고, 단지 화면의 특정 부분을 손가락으로 가리켰다. 평가 함수를 등록하는 코드 입력창 바로 위에 희미한 회색 글씨로 예시 코드가 적혀 있었다.

“솔라, 우리가 우체국에 편지를 부칠 때를 생각해 봐. 편지 내용은 우리가 자유롭게 쓰지만, 편지 봉투에 주소나 우편번호를 적는 칸은 정해져 있잖아. 받는 사람이 그 규칙을 보고 편지를 분류해서 배달하는 것처럼.”

“편지 봉투?”

“응. LangSmith라는 시스템이 솔라의 평가 코드를 ‘실행시켜주려면’, 시스템이 알아볼 수 있는 정해진 양식이 필요해. 우리가 만든 함수 이름이 check_for_keyword이든 my_awesome_evaluator이든, LangSmith는 신경 쓰지 않아. 오직 자기가 찾기로 약속된 이름의 함수만 찾아서 실행할 뿐이야.”

루나의 손가락은 예시 코드의 첫 줄을 정확히 가리키고 있었다.

def perform_eval(run, example=None):

그제야 솔라의 눈에 그 한 줄이 들어왔다. “아… 함수 이름이 perform_eval로 고정되어 있어야 하는구나.” 자신의 함수 이름이 문제였다는 것을 깨달은 솔라는 허탈한 웃음을 지었다. 마치 정성껏 쓴 편지를 주소도 없이 우체통에 넣은 기분이었다.

“하나 더 있어.” 루나가 말을 이었다. “편지를 배달하는 데 성공했다고 치자. 그런데 배달부가 편지 내용을 뜯어보는 게 아니라, 봉투 겉면에 ‘중요’, ‘보통’ 같은 스티커가 붙어 있길 기대한다면 어떨까?”

루나는 이번엔 예시 코드의 마지막 줄, return 부분을 가리켰다.

return {"key": "some_name", "score": 1.0}

“솔라의 함수는 지금 숫자 1.0이나 0.0을 돌려주고 있지. 물론 점수는 맞지만, LangSmith는 조금 더 정돈된 형식, 그러니까 ‘점수(score)’가 몇 점이고, 이 평가 항목의 이름(key)이 무엇인지 알려주는 사전(dictionary) 형태로 결과를 받기를 원해.”

솔라는 고개를 끄덕이며 자신의 코드를 빠르게 수정하기 시작했다. 함수 이름은 perform_eval로 바꾸고, 반환 값은 루나가 알려준 사전 형태로 고쳤다.

# 솔라가 수정한 평가 함수
def perform_eval(run, example=None):
    output_str = run.outputs.get("output", "")
    score = 1 if "성공" in output_str else 0
    
    return {
        "key": "keyword_match", 
        "score": score, 
        "comment": f"Checked for '성공'. Found: {'성공' in output_str}"
    }

“좋아. 이제 함수 이름도, 반환 형식도 LangSmith가 기대하는 ‘편지 봉투 양식’에 맞췄어.”

솔라는 자신감에 찬 목소리로 말했다. 방금 전의 답답함은 사라지고, 이제 곧 문제를 해결하리라는 기대감이 얼굴에 어렸다. 그녀는 수정한 코드를 복사해 LangSmith 평가기 설정에 붙여넣고 저장 버튼을 눌렀다.

“됐어! 이제 진짜 되겠지. 에이전트를 다시 실행하면, 이번에야말로 ‘Feedback’ 탭에 딱 하고 점수가 뜰 거야.”

솔라는 확신에 차서 에이전트 실행 버튼을 클릭했다. 실행은 금방 끝났고, 새로운 추적 기록이 목록 맨 위에 추가되었다. 솔라는 숨을 참고 화면의 ‘Feedback’ 열을 뚫어져라 쳐다보았다.

하지만 1초, 2초, 5초가 지나도 그곳은 여전히 텅 비어 있었다. 마치 아무 일도 없었다는 듯이.

“……어?”

분명히 모든 형식을 정확하게 맞췄다. 평가기의 기본 설계는 이제 완벽했다. 그런데 왜 결과는 바로 나타나지 않는 걸까? 솔라의 머릿속에 새로운 질문이 떠올랐다. 평가 함수를 실행하는 것과, 운영 Trace에 평가기를 붙이는 것은 정말 같은 일일까? 만약 다르다면, 그 차이는 어디에 있는 걸까?

2장: 솔라, ‘비동기’의 벽에 부딪히다

솔라는 노트북 화면의 새로고침 버튼을 무의식적으로 누르고 있었다. 방금 전 에이전트 실행으로 생성된 새로운 추적 기록은 목록 맨 위에 얌전히 자리 잡고 있었지만, 그 옆 ‘Feedback’ 열은 여전히 공백이었다. 5초, 10초, 이제는 거의 30초가 다 되어가고 있었다.

“말도 안 돼. 이번엔 완벽했는데.”

옆에서 지켜보던 루나가 조용히 물었다.

“뭐가 완벽했는데?”

“함수 이름도 perform_eval로 맞췄고, 반환 값도 { "key": ..., "score": ... } 형식으로 정확하게 바꿨잖아. LangSmith가 원하는 ‘편지 봉투 양식’에 모든 걸 다 맞췄다고. 그런데도 점수가 안 나와.”

솔라의 목소리에는 짜증과 함께 약간의 억울함이 섞여 있었다. 그녀는 실행이 끝나면 평가도 즉시 끝나 결과가 나와야 한다고 믿었다. 마치 코드 한 줄을 실행하면 바로 다음 줄로 넘어가는 것처럼, 모든 것이 순서대로 착착 진행되어야 마땅했다.

루나는 화면의 텅 빈 ‘Feedback’ 칸을 잠시 보다가, 그 옆에 있는 ‘Latency’(지연 시간) 열을 손가락으로 가리켰다. ‘2.1s’ 라고 적혀 있었다.

“솔라, 저 에이전트 실행이 2.1초 만에 끝났다는 건 어떻게 알 수 있지?”

“응? 여기 기록이 있으니까. 실행이 끝나야 기록이 남는 거잖아.”

솔라는 당연한 걸 묻는다는 듯이 대답했다. 그녀의 생각은 명확했다. 실행이 완료되었으니, 당연히 평가도 완료되어야 한다.

“그럼 이렇게 생각해 보자.” 루나가 말했다. “우리가 카페에 가서 키오스크로 커피를 주문했어. 그게 ‘에이전트 실행’이야. 잠시 후에 바리스타가 우리 번호를 부르면서 커피를 건네줬지. 그게 ‘실행 완료’고, 영수증과 함께 커피를 받았다는 ‘추적 기록’이 남은 셈이야.”

“응. 거기까진 알겠어.”

“그런데 카페 매니저는 손님들의 만족도를 전부 조사하고 싶어 해. 솔라, 매니저는 어느 시점에 너에게 다가와서 커피 맛이 어떤지 물어볼까? 바리스타가 커피를 건네주는 바로 그 순간에?”

루나의 질문에 솔라는 잠시 상상에 잠겼다. 커피를 받자마자 누군가 다가와 “맛이 어떠십니까!” 하고 묻는 장면. 어색하고, 정신없다.

“아니… 내가 커피를 받아서 자리에 앉아 한두 모금 마시고 있을 때쯤, 조용히 와서 물어보겠지. 커피 만드는 일이랑 만족도 조사는 다른 사람이, 다른 시간에 하는 거니까.”

그 말을 내뱉는 순간, 솔라의 머릿속에 무언가 번뜩였다.

“잠깐만. 설마…”

루나는 고개를 끄덕였다.

“바로 그거야. LangSmith에서 에이전트를 실행하는 것과, 그 결과를 Code Evaluator로 평가하는 건 완전히 분리된 두 개의 작업이야. 바리스타가 커피를 만드는 일과 매니저가 만족도를 조사하는 일이 다른 것처럼.”

루나는 솔라의 화면에 간단한 그림을 그려 보이는 시늉을 했다.

“솔라가 생각한 모델: [주문 → 커피 제작 및 평가 → 서빙]” “실제 LangSmith 모델: [주문 → 커피 제작 → 서빙] 이 먼저 끝나고, 그 후에 별도로 [매니저의 만족도 조사 시작]

“LangSmith 시스템의 최우선 목표는 에이전트의 실행을 안전하게 끝내고 그 기록을 남기는 거야. 그게 커피를 손님에게 건네주는 단계지. 일단 기록이 성공적으로 저장되고 나면, 시스템은 그제야 ‘아, 이 프로젝트에 등록된 평가기가 있었지. 이제 평가 작업을 시작해볼까?’ 하고 두 번째 일을 시작해. 두 작업은 서로를 기다려주지 않아. 이런 방식을 ‘비동기(asynchronous)’ 처리라고 불러.”

‘비동기’. 그 단어를 듣자 모든 조각이 맞춰지는 기분이었다. 평가 함수가 틀린 것도, LangSmith 시스템이 고장 난 것도 아니었다. 단지 에이전트 실행과 평가의 작업 순서에 대한 솔라의 가정이 틀렸던 것이다.

솔라는 반신반의하며 다시 한번 웹페이지 새로고침 버튼을 눌렀다.

그러자 거짓말처럼, 텅 비어 있던 ‘Feedback’ 열에 동그란 초록색 점과 함께 숫자 ‘1’이 나타났다.

“떴다! 진짜네… 지금 보니까 거의 1분 가까이 걸렸어.”

솔라는 허탈한 웃음을 지었다. 이제야 비로소 ‘실행’과 ‘평가’가 별개의 과정이라는 사실을 몸소 체험한 것이다. 그녀는 자신의 새로운 이해를 정리했다.

“그러니까… 내가 에이전트를 실행하면, 그 실행 기록이 먼저 LangSmith에 저장되는 거고. 그 다음에 LangSmith 시스템이 그 기록을 보고 ‘아, 평가할게 있구나’ 하고 내가 등록한 perform_eval 함수를 나중에 따로 실행시켜주는 거구나.”

“정확해. 우리는 그저 ‘이런 종류의 실행이 끝나면 이 코드로 평가해주세요’ 하고 규칙을 등록해둘 뿐이야. 실제 평가는 LangSmith의 다른 컴퓨터가, 다른 시간에 처리하는 거지.”

수수께끼가 풀린 듯 후련해진 솔라가 안도의 한숨을 내쉬었다.

“휴, 그럼 그냥 조금 기다리기만 하면 되는 거였네. 어쨌든 결과는 항상 뜨는 거니까, 문제없는 거겠지?”

3장: 솔라, 운영에서의 지연과 누락을 이해하다

비동기라는 시간차의 비밀을 풀어낸 솔라는 자신감이 붙었다. 그녀는 방금 얻은 깨달음을 확인해보고 싶어졌다. ‘그래, 여러 번 실행해서 평가 결과가 모두 제대로 뜨는지 직접 관찰해 보는 거야.’ 솔라는 마치 작은 과학 실험을 설계하듯, 노트북 화면에 메모장을 띄우고 간단한 표를 그렸다.

실행 순번실행 완료평가 결과 (5분 후)
1O
2O
10O

“좋아. 에이전트를 빠르게 열 번 실행하고, 커피라도 한잔 마시고 와서 보면 되겠지.”

솔라는 가벼운 마음으로 에이전트 실행 스크립트를 연달아 열 번 실행했다. LangSmith 화면의 추적 목록이 순식간에 새로운 기록들로 채워졌다. 이제 남은 것은 ‘기다림’뿐이었다. 그녀는 정말로 부엌으로가 커피를 내리고는, 느긋하게 5분 정도 시간을 보냈다. 이제 모든 ‘Feedback’ 칸이 채워져 있을 터였다.

다시 노트북 앞에 앉은 솔라는 기대하며 화면을 새로고침했다. 예상대로 대부분의 실행 기록 옆 ‘Feedback’ 열에는 초록색 점과 함께 평가 점수가 나타나 있었다. 하지만 표를 채워나가던 솔라의 손이 멈칫했다.

실행 순번실행 완료평가 결과 (5분 후)
6O1
7O-
8O1

“어라? 7번 실행 결과만 평가가 안 붙네.”

다른 기록들은 모두 1분에서 3분 사이에 평가가 완료되었지만, 유독 일곱 번째 실행 기록만 5분이 넘도록 감감무소식이었다. 솔라는 다시 몇 번이고 새로고침을 눌렀지만 결과는 마찬가지였다. 실행 추적 기록은 분명히 있는데, 평가만 이루어지지 않은 것이다.

‘결과는 항상 뜬다’고 안도했던 스스로의 모습이 떠올랐다. 그 안도감이 이제는 당혹감으로 바뀌었다. ‘항상’이라는 가정은 보기 좋게 깨져버렸다.

“언니, 이거 좀 봐. 이상해.”

솔라의 목소리에 루나가 다가왔다. 루나는 텅 비어 있는 일곱 번째 ‘Feedback’ 칸과, 그 외에는 모두 숫자가 채워진 솔라의 작은 표를 말없이 번갈아 보았다.

“이것만 이상하게 누락됐나 봐. 일시적인 버그인가? 아니면 내가 너무 빨리 실행해서 그런가?”

솔라는 원인을 자신이나 시스템의 ‘오류’에서 찾으려 했다. 루나는 지난번 카페 비유를 다시 꺼냈다.

“솔라, 지난번 그 카페, 점심시간처럼 아주 바쁜 피크 타임이라고 생각해 보자. 손님이 1분마다 열 명씩 밀려들어 와. 바리스타들은 커피를 만드느라 정신이 없지.”

“응. 그게 ‘에이전트 실행’이고.”

“그리고 매니저는 여전히 손님 만족도를 조사하고 싶어 해. 그게 ‘평가’ 작업이지. 그런데 매니저가 한 손님에게 다가가 커피 맛을 물어보려는 순간, 주방에서 큰 소리가 나거나 다른 급한 손님 응대가 필요해졌어. 그럼 매니저는 어떻게 할까?”

“음… 일단 더 급한 일부터 처리하겠지. 만족도 조사는 나중에 하거나, 그 손님은 그냥 건너뛸 수도 있고.”

솔라가 대답하는 순간, 그녀의 표정이 미묘하게 변했다.

“설마… LangSmith 시스템도 그렇다는 거야? 평가 요청이 들어왔는데, 시스템이 다른 일로 바쁘거나 부하가 높으면… 그냥 그 평가를 건너뛸 수도 있다고?”

루나는 고개를 끄덕였다.

“정확해. ‘비동기 처리’라는 건 단지 ‘나중에 한다’는 뜻만 있는 게 아니야. ‘실행’이라는 핵심 작업과 ‘평가’라는 부가 작업을 완전히 분리해서, 서로에게 영향을 주지 않겠다는 의미가 더 커. 시스템 입장에서 최우선 과제는 에이전트 실행을 안정적으로 완료하고 기록하는 것이지, 모든 실행을 100% 평가하는 게 아닐 수 있어. 평가는 ‘최선 노력(best-effort)’ 기반으로 동작하는 거야. 그래서 가끔은 추적은 성공했지만 평가가 누락되는 경우도 생기는 거지.”

솔라는 그제야 ‘2~5분 소요’나 ‘가끔 누락될 수 있음’이라는 정보가 단순한 참고사항이 아니라, 시스템의 근본적인 동작 방식을 설명하는 중요한 특징임을 깨달았다. 그것은 버그가 아니라, 의도된 설계의 결과였다. 지연과 누락은 언제든 발생할 수 있는 ‘운영상의 현실’이었던 것이다.

생각이 여기까지 미치자, 솔라는 안도감 대신 가벼운 위기감을 느꼈다.

“만약… 우리가 만든 에이전트의 성능을 매일 아침 보고해야 한다면? 그런데 이렇게 평가가 몇 분씩 늦어지고, 가끔은 누락되기까지 한다면… 보고서의 신뢰도가 떨어질 수밖에 없겠네.”

그녀는 더 이상 ‘내 코드가 왜 안 돌아가지?’를 고민하지 않았다. 대신 ‘이 시스템의 특성을 고려해서 어떻게 안정적으로 운영할까?’를 고민하기 시작했다. 이것이 바로 운영 위험을 예측하는 첫걸음이었다.

솔라는 잠시 생각에 잠기더니, 아까 만들었던 메모장을 지우고 새 문서를 열었다. 그리고 망설임 없이 제목을 입력했다.

[ Agent 시스템 운영 가이드라인 (초안) ]

그녀는 방금 자신이 겪은 시행착오를 바탕으로 첫 번째 원칙들을 써 내려가기 시작했다.

  • 1. 평가 결과는 실시간 데이터가 아니다.

    • Agent 실행 후 평가 결과가 반영되기까지 최소 5분 이상의 지연이 발생할 수 있음을 가정한다.
    • 실시간 대시보드보다는 일일/주간 리포트처럼 시간 여유가 있는 분석에 활용하는 것이 적합하다.
  • 2. 평가 누락은 ‘오류’가 아닌 ‘정상적인 예외’로 간주한다.

    • 시스템 부하 등에 따라 약 1~2%의 평가 누락률이 발생할 수 있음을 인지한다.
    • 전체 실행 건수 대비 평가 완료 건수를 함께 모니터링하여, 평가 시스템 자체의 건강 상태를 점검해야 한다.

키보드를 두드리는 솔라의 얼굴에는 더 이상 혼란이나 답답함이 없었다. 대신, 예측 불가능한 시스템의 특성을 이해하고, 그것을 길들일 구체적인 계획을 세우는 엔지니어의 단단함이 자리 잡고 있었다. 실행과 평가의 숨겨진 의미를 이해한 그녀는 이제 복잡한 Agent 시스템을 운영할 준비가 되어 있었다.