Skip to content

에이전트보다 실행 생명주기가 먼저였다 ​

처음에는 에이전트가 똑똑하면 대부분 해결될 거라고 생각했다.

모델이 좋은 판단을 하고, 적절한 도구만 호출하면 된다고 봤다.

그런데 실제로 상태를 가진 업무를 붙이기 시작하니 다른 문제가 더 오래 걸렸다.

모델이 무엇을 할지 결정하는 순간보다,

그 실행을 어디서 시작하고 어디서 끝낼지 정하는 일이 더 어려웠다.

한 번의 요청은 생각보다 길다 ​

겉으로는 사용자가 한 문장을 보낸다.

안에서는 여러 일이 일어난다.

text
요청 수신
  ↓
기존 세션 로드
  ↓
이 요청이 이미 처리됐는지 확인
  ↓
메모리 조회
  ↓
필요한 외부 정보 검색
  ↓
컨텍스트 크기 계산
  ↓
필요하면 압축
  ↓
모델 호출
  ↓
도구 호출
  ↓
다시 모델 호출
  ↓
결과 저장
  ↓
세션 revision 갱신
  ↓
이벤트 / 텔레메트리 기록

여기까지 오니 run()이라는 함수 하나가 사실상 작은 트랜잭션처럼 보이기 시작했다.

하지만 데이터베이스 트랜잭션처럼 모든 외부 동작을 롤백할 수는 없다.

메시지를 이미 보냈거나, 외부 API에서 이슈를 만들었거나, 브라우저에서 버튼을 눌렀다면 되돌리기 어려울 수 있다.

그래서 실행의 시작과 끝을 대충 잡으면 바로 애매한 상태가 생긴다.

세션 저장은 마지막에 하면 되는 줄 알았다 ​

처음에는 단순했다.

모델 응답이 끝나면 세션을 저장하면 된다고 생각했다.

그런데 중간에 사용자 확인이 필요한 경우가 생겼다.

예를 들어 컨텍스트가 너무 커져서 자동 압축 대신 사용자에게 선택을 물어야 한다고 하자.

text
현재 세션 revision = 10

압축 필요
  ↓
사용자 선택 필요
  ↓
resume state 저장
  ↓
실행 중단

여기서 상태만 저장하고 revision을 그대로 두면 이상해진다.

같은 revision을 읽고 있던 다른 요청도 여전히 저장에 성공할 수 있다.

겉으로는 Compare-And-Swap을 쓰고 있는데 실제로는 변경이 있었음을 revision이 표현하지 못한다.

이런 종류의 버그는 정상 응답 테스트만 돌리면 잘 안 보였다.

세션 저장소를 만들면서 오히려 happy path보다 중단, 재개, 동시 요청 쪽 테스트가 중요해졌다.

idempotency는 API 옵션이 아니었다 ​

업무 실행을 HTTP로 붙이면 중복 요청은 반드시 생긴다.

사용자가 버튼을 두 번 누를 수도 있고, Client가 timeout 뒤 재시도할 수도 있고, Gateway가 같은 요청을 다시 보낼 수도 있다.

처음에는 operationId 같은 키 하나 저장하면 끝인 줄 알았다.

그런데 질문이 계속 생겼다.

text
operationId는 언제 기록할 것인가?

모델 호출 전에 기록하면 실패한 요청을 다시 실행하기 어렵다.
도구 실행 뒤에 기록하면 중간 실패 시 도구가 두 번 실행될 수 있다.
세션 저장과 operation 기록이 따로 실패하면 어떻게 할 것인가?

결국 중요한 것은 키의 존재가 아니었다.

어느 시점까지 실행된 것을 완료로 볼지 정의하는 것이 먼저였다.

이때부터 실행 생명주기를 하나의 계약으로 보고 싶어졌다.

컨텍스트 압축도 단순 요약이 아니었다 ​

처음에는 토큰이 많으면 오래된 메시지를 요약하면 된다고 생각했다.

실제로 해보면 무엇을 버릴지 결정하기가 어렵다.

사용자 요구사항은 오래됐어도 계속 중요할 수 있다.

도구 결과는 길지만 다시 만들 수 있는 데이터일 수 있다.

반대로 짧은 승인 한 줄은 이후 실행 권한을 결정하는 핵심일 수 있다.

text
전체 대화
  ├─ 현재 작업에 꼭 필요한 상태
  ├─ 장기 기억으로 보내야 할 정보
  ├─ 다시 검색 가능한 외부 정보
  ├─ 이미 끝난 작업의 세부 로그
  └─ 버려도 되는 중간 추론 흔적

전부 같은 메시지가 아니었다.

그래서 컨텍스트 압축을 모델에게 "적당히 줄여줘"라고 맡기는 것만으로는 불안했다.

압축이 언제 발생했고, 압축 전후에 무엇이 유지됐는지 최소한 추적할 수 있어야 했다.

연결이 끊기면 작업도 끝나야 하는가 ​

스트리밍을 붙이면서 또 하나를 놓쳤다.

모델 클라이언트에는 이미 AbortSignal이 있었다.

내부 실행기도 signal을 받을 수 있었다.

그런데 HTTP 계층에서 사용자가 브라우저를 닫았다는 사실을 내부 signal에 연결하지 않았다.

결과는 단순하다.

text
Browser
  ↓ SSE 연결 종료
HTTP Server
  X
Runtime
  ↓ 계속 실행
Model / Tool
  ↓ 계속 비용과 작업 발생

각 계층에 취소 기능이 있어도 연결하지 않으면 없는 것과 같았다.

이런 문제를 겪고 나니 cancellation도 기능 목록의 체크박스가 아니라 end-to-end 계약이라는 생각이 들었다.

text
Client disconnect
  ↓
HTTP abort
  ↓
Runtime abort
  ↓
Model abort
  ↓
Tool abort
  ↓
partial state를 commit하지 않음

중간 하나라도 끊기면 의미가 달라진다.

시간 하나도 런타임 문제였다 ​

모델에게 오늘 날짜를 알려주려고 간단히 UTC 날짜를 잘라 넣은 적이 있다.

서버에서는 멀쩡해 보였다.

그런데 한국 기준 새벽에는 하루 전 날짜가 들어갔다.

text
한국: 2026-09-19 04:00
UTC:  2026-09-18 19:00

모델에게는 "오늘은 2026-09-18"이라고 알려준 셈이다.

프롬프트에 현재 날짜를 명시해 놓고 정작 런타임이 틀린 날짜를 넣었다.

이 문제를 보고 하드코딩으로 특정 타임존을 넣고 싶었지만, 범용 실행 계층이라면 그것도 맞지 않았다.

결국 현재 시각과 타임존 역시 호출자 또는 배포 환경이 명시해야 하는 실행 컨텍스트였다.

작아 보이는 값 하나에도 "이 책임은 누구 것인가"가 숨어 있었다.

모델 루프보다 주변이 더 커졌다 ​

도구를 호출하는 루프 자체는 생각보다 단순했다.

text
모델 호출
  ↓
tool call 있음?
  ├─ 아니오 → 종료
  └─ 예
       ↓
     도구 실행
       ↓
     결과 추가
       ↓
     다시 모델 호출

그런데 이걸 실제 서비스로 만들려면 주변이 붙는다.

text
                model ↔ tool
                     │
       ┌─────────────┼─────────────┐
       │             │             │
    session       context       cancellation
    revision      memory        timeout
    CAS           search        telemetry
    resume        compression   dataset

어느 순간 깨달았다.

내가 만들고 싶은 것은 "에이전트 객체"가 아니라 이 전체 실행의 바닥이었다.

에이전트는 이 위에서 바뀔 수 있다.

모델도 바뀔 수 있고, 도구도 바뀔 수 있고, 도메인도 바뀔 수 있다.

하지만 실행 중간 상태를 어떻게 보존하고, 중복을 어떻게 막고, 실패를 어떻게 복구할지는 계속 남았다.

그래서 하나의 lifecycle로 합치기 시작했다 ​

한 번은 같은 일을 하는 orchestration 코드가 두 군데 생겼다.

둘 다 세션을 읽고, 메모리를 조회하고, 압축하고, 저장하고, revision을 올렸다.

처음에는 재사용처럼 보였는데 시간이 지나니 서로 조금씩 달라졌다.

그리고 한쪽에서만 동시성 버그가 생겼다.

이때부터 기능을 더 넣는 것보다 실행 순서를 하나로 고정하는 게 먼저라고 판단했다.

지금 머릿속의 형태는 대략 이렇다.

text
prepare
  ├─ load
  ├─ idempotency
  ├─ memory
  ├─ search
  └─ compression

execute
  └─ model / tool loop

commit
  ├─ revision
  ├─ CAS
  ├─ operation record
  └─ telemetry / dataset

별것 아닌 구조처럼 보인다.

그런데 이 순서를 한 곳에서만 책임지게 만드는 데까지 꽤 오래 걸렸다.

AX에서 중요한 건 데모 이후였다 ​

모델 API를 호출해서 답을 받는 데모는 금방 만들 수 있다.

업무 하나를 실제로 맡기기 시작하면 질문이 달라진다.

  • 서버가 재시작돼도 이어지는가
  • 같은 요청이 두 번 실행되지 않는가
  • 동시에 수정해도 상태가 깨지지 않는가
  • 사용자가 취소하면 실제 실행도 멈추는가
  • 컨텍스트가 넘쳐도 중요한 상태가 남는가
  • 나중에 왜 이런 결과가 나왔는지 확인할 수 있는가

이 질문들에 답하지 못하면 에이전트가 아무리 똑똑해도 운영하기 어려웠다.

직접 만들면서 가장 크게 바뀐 생각은 이것이다.

AX에서 LLM은 실행의 중심이지만, 운영 가능한 AX를 만드는 일은 LLM 호출 바깥에서 훨씬 많이 일어난다.

다음에는 여기까지 만들고 나서 생긴 더 허무한 순간을 적어보려고 한다.

다 만들고 나니 비슷한 기능이 이미 여러 프레임워크에 있었다.

마지막 수정:

Boundary · Execution · Verification · Operation