에이전트 제품 로그 설계: 답변 품질보다 먼저 남겨야 할 이벤트들
AI 에이전트 제품을 운영하다 보면 “모델이 이상한 답을 했다”는 제보가 들어온다. 그런데 로그를 열어 보면 최종 답변 문자열만 남아 있다. 어떤 도구를 불렀는지, 어떤 검색 결과를 봤는지, JSON 파싱이 실패했는지, 중간에 정책 필터가 개입했는지 알 수 없다. 이 상태에서는 개선이 아니라 재현 운에 기대게 된다.
에이전트는 단일 LLM 호출이 아니다. 계획, 도구 선택, 도구 실행, 관찰, 재계획, 최종 응답이 이어지는 작은 분산 시스템이다. 그래서 로그도 “프롬프트와 응답 저장” 수준에서 끝나면 부족하다. 제품 운영자는 한 run이 왜 성공하거나 실패했는지 사건 순서로 복원할 수 있어야 한다.
이 글은 에이전트 로그를 어떻게 설계해야 디버깅, 비용 관리, 품질 평가, 보안 검토가 가능해지는지 다룬다. 핵심은 많은 텍스트를 저장하는 것이 아니라, 결정 지점을 구조화해서 남기는 것이다.
에이전트 장애는 최종 답변만 봐서는 분류되지 않는다
일반 챗봇의 실패는 대체로 답변 품질 문제로 보인다. 에이전트의 실패는 훨씬 다양하다. 모델이 도구 이름을 잘못 골랐을 수도 있고, 도구는 맞았지만 인자 스키마가 틀렸을 수도 있다. 검색 결과는 정확했는데 재정렬 단계에서 누락됐을 수도 있다. 결제 API가 timeout 되었는데 모델이 성공한 것처럼 말했을 수도 있다.
최종 답변만 저장하면 이 모든 실패가 “hallucination”으로 뭉개진다. 그러면 팀은 모델 교체만 반복한다. 실제로는 tool_args_validation_error를 고치면 해결될 문제였을 수 있다.
운영 로그는 실패를 최소한 네 가지로 나눠야 한다. 첫째, 모델 판단 실패: 잘못된 도구 선택, 불필요한 tool call, instruction 무시. 둘째, 인터페이스 실패: JSON schema 불일치, 필수 필드 누락, enum 오타. 셋째, 외부 시스템 실패: 429, 500, timeout, stale data. 넷째, 정책 실패: 권한 없음, 개인정보 마스킹, 안전 필터 차단. 이 분류가 없으면 개선 우선순위가 흐려진다.
run, step, attempt를 분리해야 재현이 된다
에이전트 로그의 기본 단위는 요청 하나가 아니라 run이다. 사용자가 “지난 분기 매출 변동 원인을 찾아 요약해 줘”라고 요청하면 내부적으로 검색, SQL 조회, 차트 생성, 문서 읽기, 최종 요약이 이어질 수 있다. 이것이 하나의 agent_run_id다.
run 아래에는 step이 있다. plan, retrieve_docs, call_sql, summarize_table, final_answer처럼 작업의 의미 단위다. step 아래에는 attempt가 있다. 같은 도구 호출이 retry되거나, JSON 보정 후 다시 실행되거나, rate limit 이후 backoff될 수 있기 때문이다.
이 구조가 없으면 retry가 원본 실패처럼 보인다. 예를 들어 SQL 도구가 첫 시도에서 timeout 되고 두 번째 시도에서 성공했다면 최종 답변은 정상이다. 하지만 p95 latency는 나빠진다. attempt_index, retry_reason, backoff_ms가 없으면 운영자는 성공 요청 속에 숨어 있는 장애를 보지 못한다.
도구 호출 로그는 입력과 출력을 그대로 저장하면 안 된다
도구 로그를 남기라는 말은 모든 payload를 원문 저장하라는 뜻이 아니다. 특히 B2B SaaS에서는 CRM 메모, 계약서, 주민번호 비슷한 식별자, 고객 이메일이 tool result에 섞일 수 있다. 관측 가능성과 개인정보 최소화 사이의 설계가 필요하다.
도구 호출 이벤트에는 원문 대신 구조화된 요약을 남긴다. 예를 들어 검색 도구라면 query_hash, top_k, document_ids, score_range, reranker_model, result_count를 남긴다. SQL 도구라면 원문 쿼리 전체 대신 query_template_id, tables, row_count, duration_ms, error_code를 남긴다. 외부 API라면 endpoint, status_code, latency_ms, rate_limit_remaining, response_schema_version이 더 중요하다.
원문이 필요한 경우도 있다. 그때는 별도 보관 정책을 둔다. 샘플링 비율, 암호화, 접근 권한, 보존 기간을 정해야 한다. “디버깅 편하려고 전부 저장”은 나중에 보안 사고로 돌아온다.
프롬프트 로그는 해시와 버전을 같이 남겨야 한다
에이전트 품질이 어제와 달라졌을 때 가장 먼저 확인할 것은 모델명이 아니라 프롬프트 버전이다. system prompt 한 문장, tool description 한 줄, few-shot 예시 순서가 도구 선택을 바꿀 수 있다. 그런데 많은 제품은 프롬프트를 코드 문자열로만 관리하고 로그에는 남기지 않는다.
실무적으로는 prompt_template_id, prompt_template_version, system_hash, tool_schema_hash, examples_hash, rendered_prompt_token_count를 남기는 것이 좋다. 전체 프롬프트 원문 저장이 부담스럽다면 해시와 버전만으로도 배포 차이를 추적할 수 있다.
중요한 것은 해시를 덩어리별로 나누는 것이다. 전체 rendered prompt 해시 하나만 있으면 어느 부분이 바뀌었는지 알 수 없다. 도구 스키마가 바뀐 것인지, 검색 문서가 바뀐 것인지, 대화 history truncation이 바뀐 것인지 구분해야 한다. 이 구분이 있어야 롤백도 빠르다.
비용 로그는 모델 호출마다 쪼개야 한다
에이전트 비용은 최종 요청 수와 잘 맞지 않는다. 사용자 요청 하나가 LLM 호출 12번, 검색 5번, 브라우저 3번, 코드 실행 1번으로 불어날 수 있다. 그래서 “요청당 평균 비용”만 보면 문제 step을 찾지 못한다.
각 모델 호출에는 input_tokens, output_tokens, cached_input_tokens, reasoning_tokens처럼 provider가 제공하는 토큰 분해를 최대한 남긴다. 지원하지 않는 provider라면 최소한 tokenizer 기준 추정치라도 남긴다. tool call 전후로 prompt가 얼마나 커졌는지도 기록해야 한다.
예를 들어 보고서 작성 agent에서 비용의 70%가 최종 요약이 아니라 중간 reflect step에서 발생할 수 있다. 모델이 매번 전체 문서와 전체 로그를 다시 읽으며 “다음 행동을 생각”하고 있을 수 있기 때문이다. 이때 해결책은 더 싼 모델로 바꾸는 것이 아니라 reflect step의 context를 줄이거나, summary memory를 별도로 만들거나, prefix cache가 맞도록 프롬프트를 고정하는 것이다.
품질 평가는 user rating보다 사건 라벨이 먼저다
사용자 별점은 중요하지만 에이전트 개선에는 느리고 모호하다. “별점 2점”이 검색 실패인지, 말투 문제인지, 권한 문제인지 알 수 없다. 운영자가 남겨야 할 것은 사건 라벨이다.
라벨 예시는 wrong_tool_selected, tool_args_invalid, tool_result_empty, retrieval_missed_required_doc, answered_without_tool, policy_blocked, final_answer_unsupported, user_goal_ambiguous처럼 구체적이어야 한다. 라벨은 사람이 붙일 수도 있고, 일부는 규칙으로 자동 생성할 수 있다. 예를 들어 tool result가 0건인데 final answer가 확정적으로 작성되면 answered_after_empty_retrieval 후보로 표시한다.
이 라벨은 eval set을 만드는 재료가 된다. 실제 실패 run을 샘플링해서 replay 가능한 테스트 케이스로 바꾼다. 단, replay에는 당시의 prompt version, tool schema version, 검색 index snapshot, 모델명이 필요하다. 로그가 이 정보를 갖고 있지 않으면 회귀 테스트가 아니라 비슷한 상황 재현에 그친다.
고객에게 보여줄 로그와 내부 로그는 다르다
B2B 에이전트 제품은 “왜 이 결론을 냈는가”를 고객에게 설명해야 할 때가 있다. 하지만 내부 디버깅 로그를 그대로 노출하면 안 된다. tool latency, retry, provider error, 내부 prompt는 고객에게 불필요하거나 위험하다.
고객용 trace는 의사결정 근거 중심이어야 한다. “다음 문서 4개를 참고했다”, “CRM에서 2026년 2분기 deal stage 변경 이력을 조회했다”, “권한이 없어 급여 테이블은 조회하지 않았다”처럼 검증 가능한 사건을 보여준다. 내부 로그는 더 세밀하다. span_id, parent_span_id, token 수, cache hit, schema validation error, retry 정책이 들어간다.
이 둘을 같은 저장소와 같은 스키마로 처리하려고 하면 둘 다 어정쩡해진다. 내부 observability pipeline과 고객용 audit trail은 목적이 다르다. 다만 둘을 연결하는 agent_run_id와 evidence_id는 공유해야 한다.
운영자가 남겨야 할 로그
에이전트 제품의 최소 로그 스키마는 다음 질문에 답해야 한다. 이 run은 어떤 목표로 시작했는가. 어떤 step을 거쳤는가. 각 step에서 어떤 모델과 도구를 썼는가. 실패와 retry는 어디서 발생했는가. 최종 답변은 어떤 evidence에 기대고 있는가.
필드로 쓰면 agent_run_id, user_request_id, tenant_id, step_id, attempt_index, model, prompt_template_version, tool_name, tool_schema_version, tool_status_code, input_tokens, cached_input_tokens, output_tokens, latency_ms, error_class, evidence_ids, policy_decision 정도가 출발점이다.
처음부터 완벽한 trace UI를 만들 필요는 없다. 그러나 이 필드 없이 에이전트를 운영하면 매 장애가 추리 게임이 된다. 좋은 로그는 모델을 비난하거나 찬양하기 전에 “어느 결정 지점이 틀렸는가”를 보여준다. 그때부터 프롬프트 수정, 도구 스키마 개선, 검색 인덱스 보강, 모델 라우팅이 각각 다른 작업으로 분리된다.
추가로 로그 수집은 비용 자체도 설계 대상이다. 모든 token, 모든 tool payload, 모든 중간 reasoning을 장기 보관하면 관측 가능성 비용이 제품 비용을 따라잡는다. 그래서 hot storage와 cold storage를 나눈다. 최근 7일의 실패 run은 상세 span을 보관하고, 성공 run은 집계 필드와 샘플만 남긴다. 고객 지원 티켓으로 연결된 run은 보존 기간을 늘리고, 개인정보가 포함된 tool result는 별도 redaction pipeline을 거친다.
알림 규칙도 사건 라벨 기반으로 만들어야 한다. 전체 실패율 1%보다 answered_without_tool이 특정 tenant에서 갑자기 증가하는 것이 더 위험할 수 있다. tool_args_invalid가 새 배포 직후 늘었다면 prompt보다 schema version mismatch를 의심해야 한다. 이런 알림은 최종 답변 텍스트를 읽지 않아도 장애 방향을 알려 준다. 에이전트 운영의 목표는 모든 대화를 사람이 검수하는 것이 아니라, 사람이 봐야 할 run을 정확히 골라내는 것이다.