AI가 무언가를 작성해 달라고 요청하고 사람이 그것을 승인해야 하는 상황이 있습니다.
이 문장에는 두 가지 종류의 "만료(expired)" 상태가 존재하며, 이들은 서로 다른 개념입니다:
- 대화가 계속해서 승인을 기다려야 하는지, 아니면 승인 없이 진행할 수 있는지
- 이 행(row) 자체가 여전히 결정될 수 있는 건지, 아니면 이미 끝난 것인지
우리는 이 두 가지를 하나의 타임아웃으로 처리했습니다. 그 결과, 클릭을 해도 실패가 확실한 행에 대해 "당신을 기다리고 있음(waiting on you)"이라는 목록이 표시되는 문제가 발생했습니다.
이 글은 이 두 가지 개념을 각각의 기능을 가진 두 개의 창(window)으로 분리하는 것과, 결정이 내려져야 하는 단일 지점(single point)에 관한 것입니다.
1. 왜 하나의 질문처럼 보이는가
두 질문 모두 "확인이 얼마나 오래 지속되는가"로 읽히기 때문에, 숫자가 하나 주어지면 본능적으로 한 개의 열(column)로 처리하려는 경향이 있습니다.
하지만 이들은 서로 다른 질문에 답하며, 각기 다른 것들이 그 답변을 소모합니다:
| 질문 | 누가 소비하는가 | 잘못된 답변의 비용 |
|---|
| 대화가 계속 기다려야 하는가? | 기다리고 있는 하나의 스트림(stream) | 너무 오래 기다리는 것, 또는 너무 일찍 진행하는 것 |
| 이 행이 여전히 결정될 수 있는가? | 데이터 계층과 모든 결정 항목 | 실행할 수 없는 클릭, 또는 이미 끝났지만 여전히 조치 가능한 행 |
첫 번째 질문에 대한 잘못된 답변은 **경험(experience)**을 비용으로 지불합니다. 두 번째 질문에 대한 잘못된 답변은 **일관성(consistency)**을 비용으로 지불합니다. 그리고 이들이 값이 공유될 때, 두 번째 질문의 답변은 첫 번째 질문의 필요성에 의해 끌려다니게 됩니다.
2. 시나리오: 두 곳에서 계산되고 주석 하나로 연결되는 경우
이것이 우리에게 발생했습니다.
오프라인 창(offline window)의 경계는 두 번 계산되었습니다. 읽기 경로(read path)에서는 메모리 내에서 타임스탬프를 비교했고, 유지보수 스윕(maintenance sweep)에서는 SQL 내에서 이를 비교했습니다.
이 둘을 일치하게 유지했던 것은 코드에 있는 **주석(comment)**이었는데, 대략적으로 여기의 조건(predicate)이 저쪽의 조건과 일치한다고 되어 있었습니다.
주석은 조건(predicate) 자체가 아닙니다. 이 두 가지가 불일치하는 날에는 다음과 같은 행이 나타납니다:
목록에는 "당신을 기다리고 있음"이라고 표시되지만, 스윕 과정에서는 이미 만료된 상태입니다.
따라서 클릭은 필연적으로 실패합니다. 그리고 이것은 가끔 발생하는 실패가 아닙니다. 이 목록 자체가 대기 중인 작업으로 데드 로우(dead row)를 제시함으로써 이를 만들어냅니다.
찾기 어려운 점은, 마치 '클릭이 때때로 실패하는 것'처럼 보이지만 실제 원인은 같은 순간에 대해 두 곳이 서로 다른 정의를 가지고 있기 때문입니다.
3. 해결책 하나: 두 개의 창과 경계마다 하나의 함수
해결책에는 두 가지 부분이 있습니다.
첫째, 그것들이 두 개의 창이라는 것을 인정해야 합니다.
- '대화 중 대기(in-conversation wait)'는 기본적으로 60초로 짧으며, 만료는 단지 그 대기를 끝내어 대화를 계속할 수 있게 할 뿐입니다. 데이터에는 영향을 주지 않습니다.
- '오프라인 창(offline window)'은 기본적으로 24시간으로 길며, 그 안에서는 해당 로우가 결정 가능한 상태로 유지됩니다. 오직 만료만이 이 로우를 '타임아웃' 상태로 전환시킵니다.
둘째, 모든 경계는 정확히 하나의 함수에 의해 계산되어야 합니다.
읽기 경로(read path)와 스윕(sweep) 모두 같은 함수를 호출하고 동일한 순간을 얻습니다. 따라서 '조치 가능하게 표시됨'과 '이미 만료됨'이 동시에 참인 구조적 불가능성을 갖게 됩니다.
핵심은 산술 계산이 맞다는 것이 아닙니다. 핵심은 그것을 수행하는 곳이 하나라는 것입니다. 두 군데라면 남는 것은 그들이 일치한다고 약속하는 주석뿐입니다.
4. 해결책 둘: 결정을 위한 하나의 중재 지점
두 개의 창이 생겼다면, 두 개의 결정 항목이 존재합니다. 즉, 대화 속의 버튼과 이후의 대기 목록입니다. 이들은 서로 경쟁할 수 있습니다.
따라서 두 번째는 이들이 같은 것을 두고 다투게 만드는 것입니다.
우리가 하는 것은
읽고 결정하지 않는 이유? 읽는 행위와 쓰는 행위 사이에 간극이 존재하기 때문에, 두 결정 모두 '아직 보류 중(still pending)'으로 읽힐 수 있습니다. 이는 조치 이전에 내려진 결정이지만, 그 결정 이전에 조정 과정(arbitration)이 배치되지 않은 경우입니다. 조건부 업데이트(conditional update)는 원자적(atomic)입니다: 가장 먼저 행(row)을 변경하는 사람이 그 결정을 소유합니다.
두 개의 항목과 하나의 조정 지점(arbitration point). 이것이 이 섹션 전체 내용입니다.
5. 두 번째 시나리오: '모르겠다'는 읽기를 '예스'로 간주할 때
또 다른 사례입니다.
결정은 카운트되기 전에 쓰여져야 합니다. 만약 그 쓰기 과정(write)에서 오류가 발생한다면 어떨까요?
이전 동작 방식은 오류를 흡수하고 메모리 내 대기 상태(in-memory wait)를 어쨌든 해제했습니다. 이로 인해 다음과 같은 연쇄 반응이 발생했습니다:
- 데이터베이스에 문제가 생겨 쓰기가 실패함
- 메모리 내 대기 상태가 해제되어 도구(tool)가 실행됨
- 해당 행은 여전히 보류 중임(still pending)
- 대화나 보류 목록에서 다음 결정이 '아직 보류 중'임을 보고 이를 두 번째로 실행함
'결정이 데이터베이스에 기록되었는지 알 수 없음'을 '예스'로 읽어버린 것입니다.
해결책은 명확합니다: 결정이 데이터베이스에 도달했다는 증거가 없으면, 대기 상태를 해제해서는 안 됩니다. 불확실성을 실행으로 번역하기보다는, 이 경우 실패 처리하여 사용자가 재시도하도록 해야 합니다.
정확히 알아둘 점이 하나 있습니다: 식별성 키(idempotency key)가 없는 외부 도구의 경우, 두 번째 실행은 실제 재생(real replay)이며 대상 시스템은 실제로 이를 두 번 받게 됩니다. 이 경로에서는 실패를 감지적으로 처리하는 것(failing closed)이 단순한 주의 사항이 아닙니다. 필수적인 요구사항입니다.
6. 이 설계가 넘지 않는 경계
조정 지점(arbitration point)은 최대 하나의 결정만을 보장합니다.
도구가 최대 한 번만 실행된다는 것을 보장하지는 않습니다. 만약 동일한 요청이 모델 재시도나 재시작 등으로 인해 **재전송(resent)**된다면, 이는 다른 문제이며 다른 계층에서 해결해야 합니다.
두 가지를 분리해서 유지해야 합니다: 하나는 '동일한 결정은 두 번 내려질 수 없다'이고, 다른 하나는 '동일한 호출은 두 번 전송될 수 없다'입니다. 이것들을
7. 직접 실행해 볼 수 있는 점검 사항
네 가지 질문입니다.
- 타임아웃은 몇 가지 질문에 답할 수 있습니까? 하나의 값이 'UI가 계속 대기하는지'와 '이 행을 여전히 사용할 수 있는지' 두 가지를 결정한다면, 이는 공유 값(shared value)입니다.
- 경계는 몇 가지 구현 방식을 가집니까? 두 개라는 것은 한 개의 주석만으로도 이탈할 수 있습니다.
- 레이싱 상황에서의 결정을 조정하는 것은 무엇입니까? 메모리 내 확인(in-memory check)은 조정이 아닙니다. 조건부 업데이트(conditional update) 또는 모든 원자적 비교-교환(atomic compare-and-swap) 방식이 그렇습니다.
- 결정이 영구적으로 저장되지 못할 때의 기본값은 무엇입니까? 쓰기 실패 후 다음 단계가 '어쨌든 실행한다'라면, 기본값은 허용(allow)입니다.
마무리
다시 서론으로 돌아갑니다: 하나의 타임아웃으로는 두 질문에 답할 수 없습니다.
우리가 하는 일은 다음과 같습니다. 이를 두 개의 창으로 분리합니다. 대화 중 대기 시간 60초와 결정 가능한 보류(pending) 시간 24시간입니다. 각 경계는 정확히 하나의 함수로 계산하고, 모든 결정이 하나의 조건부 업데이트를 위해 경쟁하도록 만듭니다. 그리고 결정이 저장되었다는 증거가 없으면 아무것도 방출하지 않습니다.
위의 메커니즘은 KeelBase라는 오픈 소스 런타임에서 가져온 것입니다. 이 런타임은 AI 에이전트와 비즈니스 시스템 사이에 위치하며, 도구가 실제로 실행되기 전에 신원(identity), 권한(authorization), 범위(scope), 위험(risk) 및 확인 상태를 재확인합니다.
저장소 (중국 내부에서는 Gitee 미러가 더 빠른 경로입니다):
두 가지를 분명히 말합니다. 위의 경계는 우리가 보장하는 것은 실행 횟수가 가장 많지 않다는 것이 아니라, 결정(decision)이 가장 많지 않다는 것입니다. 그리고 이 전체 설계의 전제 조건입니다. 이는 결정이 영구적으로 저장될 수 있다고 가정합니다. 데이터베이스를 사용할 수 없다면 허용하기보다는 거부하며, 일부 시스템에서는 이것이 확인 기능 자체가 완전히 작동하지 않는다는 것을 의미합니다. 이것은 의도된 것입니다. 만약 확인이 '확인되지 않은 것은 실행되지 않는다'는 의미라면, 아무것도 확인할 수 없을 때는 아무것도 실행되어서는 안 됩니다.
AI 어시스턴트의 도움을 받아 작성됨. 시스템, 입장 및 실수는 저의 것입니다.