두 개의 AI 에이전트와 하나의 Postgres 행: 버전 체크로도 잡아낼 수 없는 버그
요약
AI 에이전트가 긴 추론 시간 동안 데이터의 상태 변화를 인지하지 못해 발생하는 '오염된 작업' 문제를 다룹니다. 단순한 조건부 쓰기(CAS)만으로는 읽기 단계의 데이터 유효성을 보장할 수 없음을 지적하며, 이를 해결하기 위한 코히어런트 로우(Coherent Row) 패턴을 제안합니다.
핵심 포인트
- 버전 체크는 쓰기(write)는 보호하지만, 긴 추론 중인 읽기(reader)는 보호하지 못함
- 에이전트의 긴 추론 시간은 데이터 불일치(stale data)를 유발하는 위험 구간임
- CAS(Compare-And-Swap)만으로는 에이전트가 생성한 중간 작업의 오염을 막을 수 없음
- 캐시된 뷰의 만료를 감지하고 에이전트에게 알리는 메커니즘이 필요함
만약 두 개의 AI 에이전트가 동일한 Postgres 행(row)을 작성하고 있다면, 아마 이미 조건부 쓰기(conditional write)를 추가했을 것입니다:
UPDATE profiles SET value = $1, version = version + 1
WHERE id = $2 AND version = $3
동료(peer)가 먼저 버전을 변경하면, rowcount = 0을 받게 되고 쓰기가 거부됩니다. 이것은 실제 상황이며, 제대로 작동하고 있으므로 계속 유지해야 합니다. 이는 문제의 절반을 해결해 줍니다.
이 포스트는 나머지 절반에 관한 것입니다. 이 문제는 더 조용히 발생하며, 여러분이 모델의 탓으로 돌리게 될 버그로 나타납니다.
실패 사례
저를 괴롭혔던 타임라인은 다음과 같습니다.
- 에이전트 A가 버전 7인 행을 읽고 다단계 편집을 시작합니다. 이를 2분간의 추론(reasoning)이라고 부릅시다.
- 에이전트 B가 버전 8을 커밋(commit)합니다.
- A가 마침내 쓰기를 시도합니다. 조건부 쓰기 결과
rowcount = 0이 반환되고 A의 쓰기는 거부됩니다.
행(row)은 괜찮습니다. 그것이 바로 CAS(Compare-And-Swap)의 목적입니다.
하지만 2단계에서 무슨 일이 일어났는지 보십시오. 그 2분 동안, A는 이미 죽은(dead) 값으로부터 작업을 도출하고 있었습니다. A가 작성한 계획, 저장한 요약, 실행한 도구 호출(tool call) 중 그 어느 것도 행의 CAS를 통과하지 않았기에, 그 무엇도 거부되지 않았습니다. 그 모든 것들이 정상적으로 반영되었습니다.
행은 살아남았습니다. 하지만 작업은 오염되었습니다.
버전 체크는 쓰기(write)를 보호합니다. 하지만 읽기(reader)를 보호하지는 않습니다.
이는 일반적인 CRUD와는 다른, 에이전트만의 특수한 문제입니다. 일반적인 서비스는 읽기와 쓰기를 거의 동시에 수행하므로 그 간격(window)이 밀리초(milliseconds) 단위입니다. 하지만 에이전트는 읽고, 오랫동안 추론한 뒤, 행동합니다. 그 간격은 추론 과정 전체이며, 그 간격 동안 에이전트가 내뱉는 모든 것은 CAS가 실행되기도 전에 빠져나갑니다.
재현하기
데모는 오프라인 상태이며 별도의 인증이 필요하지 않으므로, 약 1분 안에 이 현상이 발생하는 것을 확인할 수 있습니다:
git clone https://github.com/Cohexa-ai/agent-coherence
cd agent-coherence
pip install -e ".[coherent-row]"
...
--baseline 암(arm)은 조정되지 않은(un-coordinated) 에이전트를 먼저 실행하여, 에이전트가 오래된 캐시(stale cache)를 바탕으로 동작하는 것을 보여준 다음, 보호된(guarded) 버전을 실행합니다. 거부(deny) 여부는 단순히 단언되는 것이 아니라, 거부가 없었을 경우와 비교하여 측정됩니다. 종료 코드(Exit code)는 계약(contract)이 유지되었을 때만 0이 됩니다.
코드에서의 해결책
누락되었던 부분은 에이전트(A)가 동작하기 전에, 에이전트가 가진 캐시된 뷰(cached view)가 만료되었음을 에이전트에게 알려주는 것이었습니다. 바인딩(binding)이 추가하는 기능이 바로 이것입니다. examples/coherent_row/main.py에서 축약한 내용은 다음과 같습니다:
from ccs.adapters.coherent_row import CoherentRow
from ccs.adapters.substrate import CoordinatedSubstrate, SubstrateCoordinatorSession
from ccs.core.exceptions import StaleView
...
제가 가장 중요하게 생각하는 세부 사항은 '발생하지 않는 일'입니다. StaleView가 발생하면, 쓰기(write) 작업은 Postgres에 도달하지 않습니다. 바인딩은 서브스트레이트(substrate)에 접근하기 전 코디네이터(coordinator) 단계에서 이를 거부하며, 데모에서는 CAS(Compare-And-Swap)가 호출되지 않았음을 확인하여 바로 그 사실을 단언합니다. 사후에 거절을 포착하는 것이 아니라, 행위 자체를 중단시키는 것입니다.
복구(Recovery)는 reacquire()라는 하나의 동사로 표현되며, 상태가 행(row)이든, S3 객체든, 디스크의 파일이든 동일한 동사를 사용합니다. CoherentObject는 내부적으로 If-Match를 사용하여 S3에 대해서도 동일한 메커니즘을 제공합니다.
데이터는 당신의 데이터베이스에 머뭅니다
이러한 라이브러리를 사용할 때 제가 의구심을 가질 만한 부분은, 이것이 조용히 제2의 저장소(second store)가 되어버리는지 여부입니다. 이 라이브러리는 그렇지 않으며, 설계 단계에서 이를 확인할 수 있도록 되어 있습니다.
코디네이터는 단조 증가하는 버전(monotonic version), 에이전트별 상태(per-agent state), 그리고 고정 너비의 콘텐츠 해시(fixed-width content hash)를 보유합니다. 바인딩은 SENDS_CONTENT_TO_COORDINATOR = False라고 선언하며, 적합성 키트(conformance kit)는 이를 검증하고, 컴포지션(composition)은 이 값을 True로 설정하는 어떠한 바인딩도 거부합니다. 행의 본문(row body)은 결코 Postgres를 떠나지 않습니다. 당신의 데이터베이스는 백업, 권한, 내구성(durability) 스토리를 그대로 유지하며, 시스템 기록(system of record)으로서의 역할을 유지합니다. 내일 라이브러리를 제거하더라도 당신의 데이터는 항상 있던 그 자리에 그대로 있을 것입니다.
정확성 보장(correctness guarantee)을 받기 위해 운영 상태(production state)를 마이그레이션해서는 안 됩니다.
이것이 필요하지 않은 경우
일부 사용자들에게는 이것이 실질적인 해답이 될 수 있으므로, 솔직하게 말씀드릴 가치가 있습니다.
모든 읽기(read)와 쓰기(write)를 pg_advisory_lock으로 감싸고, 그 사이에서 읽은 데이터를 절대 캐싱하지 않는다면, 단순한 CAS (Compare-And-Swap)만으로도 충분하며 여기서 멈춰도 됩니다. 이 바인딩(binding)은 읽고, 추론하고, 행동하는 에이전트(agent)를 위한 것입니다. 만약 읽기와 쓰기 사이의 시간 간격(window)이 짧고 그 사이에서 아무것도 도출하지 않는다면, 이 문제는 발생하지 않습니다.
솔직한 범위 (Honest scope)
홍보 문구보다 한계점을 아는 것이 더 중요하므로, 다음과 같이 정리합니다.
- 단일 호스트 전용. 두 대의 머신에 있는 두 명의 작성자(writer)는 제공되는 보증 범위에 포함되지 않습니다. 크로스 호스트 전송(cross-host transport)은 데모용이며, 프로덕션 환경의 펜싱(fencing)이 아닙니다.
- 바인딩은 협력적(cooperative)입니다. 바인딩 없이 Postgres로 바로 직행하는 작성자는 쓰기 시점에 포착되지 않습니다. 코디네이터(coordinator)는 콘텐츠 해시(content hash)를 통한 다음 중재된 읽기(mediated read) 시점에 불일치를 감지하며, 사후 탐지는 예방이 아닙니다.
- 데모는 메모리 내의 행(row)을 모델링하므로 오프라인에서 실행됩니다. 실제 Postgres CAS는 적합성 키트(conformance kit)의
real_substrate파트에서 실제 Postgres를 대상으로 실행됩니다. 프로덕션에서는 동일한 바인딩을 실제 DSN(Data Source Name)으로 지정하면 됩니다. - 이 바인딩에는 읽기 생성 펜스(read-generation fence)가 없습니다. v1 작성자들은 '부재 시 허용(admit-on-absent)'과 버전 CAS를 사용합니다. 바인딩은 무효화(invalidation)를 드러냅니다. 펜스는 문서화된 로드맵 항목이며, 현재 제공되는 동작은 아닙니다.
- 취약한 서브스트레이트(substrate)를 강하게 만들지는 않습니다. 각 바인딩은 서브스트레이트가 실제로 강제할 수 있는 기능에서 파생된 역량 계층(capability tier)을 선언하며, 적합성 키트는 선언된 계층을 관찰된 동작과 대조하여 확인합니다.
시도해보기
pip install "agent-coherence[coherent-row]" # Postgres
pip install "agent-coherence[coherent-object]" # S3
Repo: https://github.com/Cohexa-ai/agent-coherence
Full write-up: https://agent-coherence.dev/blog/shipped-byo-substrate/
만약 여러분의 시스템에서 '읽기-추론-행동(read-reason-act)' 시간 간격 문제를 겪으셨다면, 그것이 어떻게 나타났는지 듣고 싶습니다. 이 문제는 동시성 버그(concurrency bug)로 보고되는 경우가 거의 없습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기