에이전트 데모는 한 번의 성공을 보여 주기 쉽다. 운영자는 같은 작업을 매일 실행하면서 다른 문제를 만난다. 파일 생성은 성공했는데 알림이 실패하고, 재시도한 작업이 같은 게시물을 두 번 만들기도 한다. cron 프로세스가 중간에 종료되면 다음 실행이 이전 상태를 알지 못한다.
OpenClaw 자동화를 운영하면서 모델의 응답보다 실행 상태를 더 자주 고쳤다. LangChain은 에이전트 시스템을 agent, verification, event-driven, hill-climbing 루프로 설명한다. 이 구분에 맞춰 상태와 로그를 나누자 실패한 위치와 재시작 방법을 찾기 쉬워졌다.
실행 전에 run을 만든다
스케줄러가 작업을 시작하면 먼저 run_id와 상태 레코드를 만든다. 모델을 호출한 뒤 기록하면 초기 오류가 로그에 남지 않는다.
{
"run_id": "digest:2026-08-17:morning",
"trigger": "cron",
"status": "running",
"step": "collect_sources",
"attempt": 1,
"started_at": "2026-08-17T09:20:00+09:00",
"updated_at": "2026-08-17T09:20:00+09:00"
}
run_id는 같은 논리 작업에서 변하지 않는다. 재시도 횟수는 attempt로 올린다. 새 UUID만 만들면 스케줄러가 같은 아침 작업을 두 번 실행했는지 알기 어렵다. 날짜와 작업 종류, 입력 범위처럼 결정적인 값으로 ID를 만든다.
단계 이름도 고정된 목록에서 고른다. 자유로운 설명문을 쓰면 collect, collecting, source-search가 같은 상태를 뜻하게 된다. 로그를 집계하고 멈춘 작업을 찾으려면 기계가 비교할 수 있는 값이 필요하다.
에이전트 루프는 산출물로 끝낸다
가장 안쪽에서 모델은 도구를 호출하고 결과를 읽으며 다음 행동을 정한다. URL을 수집하고 요약 파일을 만드는 흐름이 여기에 속한다.
완료 판단은 모델의 마지막 문장에 두지 않는다. 다음 단계가 읽을 산출물과 검사 결과를 상태에 기록한다.
{
"source_count": 3,
"artifact": "runs/digest-2026-08-17.md",
"artifact_sha256": "...",
"source_audit": "passed",
"delivery": "pending"
}
파일 경로만 기록하면 빈 파일이나 이전 실행의 파일을 성공으로 오해할 수 있다. 해시와 생성 시각, 원본 수를 함께 저장한다. 요약 작업은 각 문장에 출처가 있는지 검사하고, 링크를 열 수 없으면 해당 URL을 실패 목록에 넣는다.
도구 호출마다 입력 전체를 로그에 남기지는 않는다. 개인정보와 토큰이 섞일 수 있다. 도구 이름, 입력의 안전한 식별자, 시작과 종료 시각, 종료 코드만 기록한다. 문제를 재현하는 데 필요한 정보와 비밀 값을 분리한다.
검증 루프는 실패를 구조화한다
검증기는 quality low 같은 평가를 반환하지 않는다. 다음 실행이 고칠 대상을 구체적으로 내보낸다.
{
"status": "failed",
"check": "source_links",
"failures": [
{
"url": "https://example.com/report",
"reason": "HTTP 404",
"claim_id": "claim-07"
}
]
}
데이터 작업은 예상 행 수와 실제 행 수, 중복 키를 반환한다. 글 발행은 빠진 frontmatter 필드와 파일명을 출력한다. 테스트가 실패하면 명령과 종료 코드, 관련 로그 구간을 저장한다.
사람이 검토할 때도 같은 형식을 쓴다. 검토자는 문장 위치와 반려 이유, 필요한 출처를 적는다. 다음 에이전트는 “글이 어색하다”는 메모보다 “두 번째 표의 수치 기준 연도가 출처와 다르다”는 기록을 고칠 수 있다.
검증 실패는 발행 상태를 바꾸지 않는다. 블로그 원고는 draft에 남고, 외부 메시지는 전송 대기 상태를 유지한다. 실패 결과가 다음 입력으로 전달되기 전까지 재시도를 시작하지 않는다.
이벤트 루프는 중복 실행을 가정한다
cron과 webhook은 같은 이벤트를 다시 보낼 수 있다. 네트워크 응답을 받기 전에 프로세스가 종료되면 발신자는 실패로 판단하고 재전송한다. 수신자는 첫 요청이 외부 시스템을 이미 바꿨는지 알기 어렵다.
나는 이벤트 처리 전에 run_id 또는 idempotency key를 저장한다. 같은 키가 completed 상태면 기존 결과를 반환한다. running 상태가 오래 유지되면 lease 만료 시각을 확인하고 복구 작업으로 보낸다.
received -> running -> verified -> delivered -> completed
\-> retry_wait
\-> needs_review
\-> failed
작업 성공과 전달 성공을 분리한다. 요약 파일을 만들었지만 Slack 전송이 실패했다면 수집과 요약을 다시 실행하지 않는다. 저장한 artifact를 사용해 전달 단계만 재시도한다. 외부 서비스가 중복 요청을 지원하지 않으면 전송 ID를 로컬에 기록하고 상대 서비스의 응답 ID와 묶는다.
재시도에는 최대 횟수와 간격을 둔다. 인증 실패와 잘못된 스키마는 기다려도 해결되지 않는다. 429 응답이나 짧은 네트워크 장애는 지수 backoff를 사용할 수 있다. 오류 종류에 따라 재시도 가능 여부를 명시하고, 제한을 넘은 작업은 사람이 보는 큐로 보낸다.
멈춘 실행을 찾는 경보
프로세스가 오류를 던지지 않고 멈출 수도 있다. 각 단계가 시작할 때 updated_at과 lease를 갱신한다. 감시 작업은 단계별 허용 시간을 비교한다.
| 단계 | 정상 완료 기준 | 경보 조건 |
|---|---|---|
| 자료 수집 | URL 목록과 응답 상태 저장 | 10분 동안 갱신 없음 |
| 원고 생성 | artifact와 해시 저장 | 파일 없이 모델 호출 종료 |
| 검증 | 검사별 결과 저장 | 검증 상태 누락 |
| 전달 | 외부 응답 ID 저장 | artifact 성공 후 전달 대기 지속 |
경보 메시지에는 run_id, 마지막 단계, 마지막 갱신 시각, 복구 명령을 넣는다. 운영자가 전체 로그를 읽기 전에 재시도와 수동 승인 중 하나를 고를 수 있어야 한다.
성공 알림도 남발하지 않는다. 매일 같은 작업이 성공하면 대시보드에 집계하고, 지연과 실패, 결과 수 급감처럼 행동이 필요한 변화만 보낸다. 알림이 많으면 운영자가 중요한 오류를 넘긴다.
개선 루프는 고정된 샘플로 비교한다
hill-climbing 루프는 실행 기록을 사용해 프롬프트와 도구를 개선한다. 나는 시스템이 자기 프롬프트를 운영 환경에서 곧바로 바꾸게 하지 않는다. 평가 지표가 좁으면 모델이 지표를 맞추면서 독자에게 필요한 정보를 줄일 수 있다.
먼저 실패를 유형별로 집계한다.
source_access 출처를 열지 못함
schema_invalid 출력 형식 위반
claim_unsupported 출처가 주장을 뒷받침하지 않음
human_judgment 맥락 판단이 필요함
external_write 외부 시스템 변경 실패
반복되는 유형에만 변경안을 만든다. 출처 접근 실패가 많으면 검색 도구와 fallback을 고친다. 스키마 오류가 많으면 예시와 validator를 개선한다. 사람 판단 항목이 많다는 이유로 승인 단계를 없애지는 않는다.
변경 전후에는 같은 입력 묶음을 실행한다. 성공률만 보지 않고 검증 시간과 재시도 횟수, 사람이 수정한 문장 수를 비교한다. 새 프롬프트가 한 지표를 개선하면서 출처 누락을 늘리면 배포하지 않는다. 변경한 버전과 평가 결과를 기록해 이전 설정으로 돌아갈 수 있게 한다.
내가 보는 운영 지표
모델 점수 하나로 시스템을 평가하지 않는다. 작업 흐름마다 다음 지표를 본다.
- 단계별 성공률과 소요 시간
- 첫 시도 성공률과 평균 재시도 횟수
- 중복 이벤트를 차단한 횟수
- 사람이 개입한 이유와 대기 시간
- 생성 성공 후 전달에 실패한 비율
- 같은 오류가 다시 발생한 간격
평균만 보면 긴 꼬리 지연이 숨는다. p95 소요 시간과 가장 오래 멈춘 run도 확인한다. 입력 문서 수가 갑자기 줄었는데 성공률이 높다면 수집 단계가 일부 소스를 놓쳤을 수 있다.
OpenClaw 자동화에 필요했던 장치는 실행 위치를 보여 주는 상태와 안전한 재시도, 사람에게 넘길 큐다. 네 루프를 분리하면 모델 호출 실패와 이벤트 중복, 검증 반려를 다른 방식으로 처리할 수 있다. 운영자는 성공한 한 번보다 실패한 다음 실행이 어디에서 이어지는지를 관리한다.