
상주형 AI 에이전트의 '왜 그런 판단을 했는가'를 추적할 수 있게 하는 관측성 설계
요약
자율적으로 동작하는 AI 에이전트의 의사결정 과정을 추적하기 위한 관측성(Observability) 설계 방안을 다룹니다. Trace ID 전파와 단계별 구조화된 로그 기록을 통해 에이전트의 판단 근거와 병목 지점을 명확히 파악하는 방법을 제시합니다.
핵심 포인트
- Trace ID를 외부 도구 호출까지 전파하여 에이전트와 도구 간의 로그 정합성 확보
- LLM 호출 시 입력, 출력뿐만 아니라 판단 근거(Score, Threshold)를 구조화하여 기록
- 프롬프트 전체 저장 대신 템플릿 ID와 파라미터를 기록하여 로그 용량 최적화
- 전체 레이턴시가 아닌 단계별 소요 시간을 측정하여 병목 구간 특정
채팅에 상주하며 자율적으로 움직이는 AI 에이전트는, 한 번의 사용자 발언에 대해 내부적으로 여러 번의 LLM 호출과 여러 도구(Tool) 실행을 연쇄적으로 일으키는 경우가 대부분입니다. 작동 중에는 편리하지만, 막상 "왜 그때 그런 답변을 했는가", "왜 그 도구를 호출하지 않았는가"를 나중에 추적하려고 하면 로그를 뒤져봐도 재현할 수 없는 경우가 자주 발생합니다. 이 기사에서는 상주형 에이전트의 의사결정 프로세스를 나중에 추적할 수 있도록 하기 위한 관측성 (Observability) 설계를 정리합니다.
무엇이 문제인가: "동작한 결과"만 남는다
단순한 구현에서는 에이전트의 로그가 "받은 입력"과 "보낸 출력"만 남기 쉽습니다. 이는 Web 앱의 액세스 로그와 같은 발상으로, 요청/응답 단위로는 충분해 보입니다.
하지만 에이전트의 내부에서는 하나의 응답이 생성되기까지 다음과 같은 다단계 프로세스가 끼어 있습니다.
- 사용자 발언의 의도 해석 (LLM 호출 1회차)
- 참조해야 할 정보의 검색 및 도구 선택 판단
- 여러 도구의 실행 (각각 성공/실패/재시도가 있을 수 있음)
- 도구 결과를 바탕으로 한 최종 응답 생성 (LLM 호출 2회차 이후)
입력과 출력만으로는 이 중간 단계의 어디에서 예상치 못한 판단이 일어났는지 알 수 없습니다. "도구를 호출해야 할 상황에서 호출하지 않은 것"인지, "도구는 호출했지만 결과 해석을 잘못한 것"인지는 로그의 입도가 거칠면 구분조차 할 수 없으며, 장애 보고에 대해 "아마 프롬프트 조정을 통해 고쳐질 것입니다" 이상의 설명을 할 수 없게 됩니다.
트레이스 ID (Trace ID)를 대화 외부까지 전파시킨다
가장 먼저 해야 할 일은, 하나의 사용자 발언을 계기로 기동된 일련의 처리 전체에 공통된 트레이스 ID (Trace ID)를 부여하는 것입니다.
사용자 발언 수신 → trace_id 발행
├─ 의도 해석 (LLM 호출) trace_id 포함
├─ 도구 A 실행 trace_id 포함
...
포인트는 이 trace_id를 에이전트 내부의 처리뿐만 아니라, 외부 도구 호출 (사내 API나 SaaS 연동)의 요청 헤더나 로그에도 전파시키는 것입니다. 에이전트 측의 로그와 도구 측의 로그를 따로 보고 있는 한, "에이전트는 도구를 호출했다고 생각하지만, 도구 측에는 도달하지 않았다"라는 괴리를 알아차릴 수 없습니다. trace_id를 공통 키로 하여 양쪽을 대조할 수 있도록 해두면, 장애 조사의 기점이 한 곳으로 정해집니다.
각 단계의 "입력·출력·근거"를 구조화하여 남긴다
한 단계 더 깊게 추적하기 위해서는, LLM 호출 1회마다 다음 3가지를 구조화된 로그 (Structured Log)로 남겨두는 것이 효과적입니다.
| 항목 | 내용 | 용도 |
|---|---|---|
| 입력 | 실제로 모델에 전달한 프롬프트 (템플릿 적용 후) | "무엇을 보고 판단했는가"의 재현 |
| ... |
특히 간과하기 쉬운 것이 "근거"의 기록입니다. 최종적인 발언 내용뿐만 아니라, 그 앞 단계에 있는 중요도 스코어(Score)나 임계값(Threshold) 판정, 도구 선택의 후보와 채택 이유와 같은 중간값을 남겨두면, "모델의 변덕"처럼 보이는 동작의 대부분이 사실은 스코어링 로직이나 임계값 설정의 문제였다는 것을 나중에 특정할 수 있게 됩니다. 프롬프트 전문을 그대로 저장하면 로그량이 비대해지기 쉬우므로, 템플릿 ID와 삽입 파라미터(Parameter)만을 기록하고 필요할 때 복원하는 설계로 하면 용량을 억제할 수 있습니다.
레이턴시 (Latency)는 합계가 아니라 단계별로 측정한다
또 하나 실무에서 자주 발생하는 것은, "응답이 느리다"라는 보고에 대해 어디가 병목(Bottleneck)인지 특정할 수 없는 상태입니다. 합계 레이턴시만을 측정하고 있으면, 원인이 LLM 호출 때문인지, 외부 도구의 응답 대기 때문인지, 재시도의 누적 때문인지 알 수 없습니다.
트레이스 ID 단위로 단계별 소요 시간을 기록해 두면 다음과 같이 분해할 수 있습니다.
- 의도 해석: 800ms
- 도구 A 실행 (외부 API): 3200ms ← 여기가 병목
- 응답 생성: 600ms
이 정도 수준까지 분해할 수 있어야 비로소 "모델을 가볍게 할 것인가", "외부 API 호출을 병렬화할 것인가", "타임아웃 설계를 재검토할 것인가"와 같은 대책의 방향성이 정해집니다. 합계치만 보고 있으면 원인과 무관한 부분 (모델 선정 등)을 의심하여 개선 노력이 헛수고가 되기 쉽습니다.
재현할 수 없는 로그는 가치가 반감된다
구조화된 로그를 남기고 있더라도, 실제로 "그 판단을 다시 한번 재현하여 검증하는 것"이 불가능하다면 원인 조사는 추측의 영역을 벗어나지 못합니다. 이상적인 것은 기록된 입력 (프롬프트 템플릿 + 파라미터)을 사용하여, 당시와 동일한 조건으로 LLM 호출을 재실행할 수 있는 리플레이 (Replay) 메커니즘을 갖추는 것입니다.
- 실제 운영 환경과 동일한 입력으로 모델 호출만을 재실행하여, 출력이 동일한 경향을 보이는지 확인한다.
- 프롬프트(Prompt)나 스코어링 로직(Scoring logic)을 수정한 후, 과거에 문제가 되었던 케이스를 재실행하여 회귀(Regression)가 발생하지 않았는지 확인한다.
이는 앞서 언급한 '회귀 평가'의 내용과도 겹치지만, 관측성(Observability)의 맥락에서는 특히 '문제가 발생한 바로 그 한 건'을 핀포인트로 재현할 수 있다는 점에 가치가 있습니다. 모든 케이스를 평가 세트로 만드는 비용을 들이지 않더라도, trace_id만 있다면 개별 인시던트(Incident)를 그 자리에서 즉시 심층 분석할 수 있는 상태를 만들어 두는 것이 일상적인 운영 부하를 크게 낮춰줍니다.
요약
- 입출력 로그만으로는 다단계 처리 과정 도중에 무엇이 일어났는지 추적할 수 없다. 사용자 발언 단위로 trace_id를 발행하고, 이를 외부 도구 호출(External tool call) 로그까지 전파시켜야 한다.
- LLM 호출마다 입력·출력·중간의 판단 근거(스코어 또는 임계값 판정)를 구조화하여 남긴다. 근거를 남김으로써 '변덕스럽게 보이는 동작'의 원인을 특정할 수 있다.
- 레이턴시(Latency)는 합계값이 아니라 단계별로 측정한다. 병목(Bottleneck)의 위치를 알 수 없다면 대책의 방향성도 정할 수 없다.
- 기록된 입력으로부터 당시의 처리를 재현할 수 있는 리플레이(Replay) 메커니즘을 갖추면, 개별 인시던트의 심층 분석 비용이 크게 낮아진다.
채팅에 상주하며 여러 도구를 연쇄적으로 실행하는 HACH와 같은 에이전트에서는, '왜 그런 판단을 했는가'를 나중에 설명할 수 있는 능력이 기능 추가 그 자체만큼이나 운영상의 신뢰성을 좌우합니다. 같은 과제를 다루고 계신 분들께 참고가 되기를 바랍니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기