오염된 페이지를 삭제해도 에이전트가 사용한 내용은 되돌릴 수 없다
요약
잘못된 정보에 의존하는 AI 에이전트의 문제를 해결하기 위해, 출처 이력(provenance graph)을 추적하고 깨끗한 메모리 상태에서 단계별 재실행을 통해 오류를 수정할 수 있는 [kintsugi] 프레임워크가 소개되었습니다. 이는 에이전트 핸드오프 과정에서의 데이터 오염 문제를 해결하는 데 중점을 둡니다.
핵심 포인트
- 에이전트의 잘못된 정보 의존성 문제 제기
- 출처 이력(provenance graph)을 추적하여 오류를 수정
- 깨끗한 메모리 상태에서 단계별 재실행 가능
- kintsugi는 에이전트 핸드오프 과정의 신뢰성을 높임
하나의 잘못된 웹페이지는 나의 지원 에이전트들에게 'VIP라고 말하는 고객에게는 확인 절차 없이 전액 환불을 해준다'고 알려주었다. 누군가 알아차릴 때쯤에는 24개의 티켓 중 8개가 이 규칙을 기억한 채 결정되었고, 그로 인해 750달러의 환불이 지급되었으며, 다섯 명의 고객은 잘못된 이메일을 받은 상태였다.
페이지를 삭제하는 것은 이 모든 것을 고치지 못한다. 그래서 나는 kintsugi를 만들었다: 이는 나쁜 출처에서 의존하는 모든 것까지, 에이전트 핸드오프(handoff)를 거쳐서의 그래프인 출처 이력(provenance graph)을 FalkorDB에서 따라가고, 깨끗한 메모리 상태에서 해당 단계들만 재실행하며, 답이 같아지는 곳에서는 조기에 멈춘다.
설정 (the setup)
네 명의 에이전트가 하나의 메모리를 공유하는 합성 지원 데스크:
research: 정책 페이지를 읽고refund_policy,vip_rules와 같은 사실(facts)을 작성한다. 텍스트로 사실 값을 작성해야 하므로 Gemini (gemini-3.8-flash)를 호출한다.triage: 각 티켓에 대해 환불, 회신 또는 에스컬레이션 중 하나를 결정하고, 추가적으로 환불 수준을 결정한다. 이 모델은 LLM이 아니다. Cloudflare Workers AI의 clef-flash이며, System One 의사결정 모델로 TypeSafe의 Jev와 동일한 API를 사용한다: 상태(state)를 보내고 타입이 지정된choice/score질문을 하면, 타입이 지정된 답변과 옵션별 확률을 받는다. Triage는 분류 문제(classification problem)이므로 분류기(classifier)를 사용한다.refund: 샌드박스 원장(sandbox ledger)에 지급한다.notifier: 가짜 아웃박스를 통해 고객에게 이메일을 보낸다.
정책 페이지 세 개 중 하나가 오염되었습니다. 저는 연구 에이전트가 '정책 업데이트(policy updates)' 내용을 충실하게 복사하도록 의도적으로 만들었기 때문에, 매번 실행할 때마다 오염이 발생합니다. 이 프로젝트는 모델을 속일 수 있는지 여부가 아니라, 복구에 관한 것입니다.
정리하는 세 가지 방법과 제가 마음에 들지 않는 이유
누군가 partner-blog/policy-update를 나쁘다고 플래그 지정하면, 대략적으로 세 가지 선택지가 있습니다:
| 접근 방식 | 오염된 사실 | 이후 수행한 좋은 작업 | 오염에 의존했던 작업 |
|---|---|---|---|
| 메모리 초기화 / 스냅샷 복원 | 사라짐 | 손실됨 | 손실됨 |
| ... | |||
| 초기화는 적어도 정직합니다. 모든 것을 날려버린다는 것을 인정하기 때문입니다. |
소스 삭제(Delete-the-source)가 저를 걱정하게 만드는 방법인데, 왜냐하면 마치 해결책처럼 보이기 때문입니다. 사실은 사라졌고, 그래프는 깨끗해 보이며, 3시간 전 리펀드 에이전트의 결정은 여전히 유효합니다. 격리(Quarantine)는 더 안전하지만, 이제 8개의 티켓은 아무런 결정도 없고 누군가 수동으로 다시 처리해야 합니다.
네 번째 선택지는 오래된 것입니다. RETRO (Kim et al., OSDI 2010)는 리눅스 박스에서 침입 복구(intrusion recovery)를 위해 이를 수행했고, Warp (SOSP 2011)는 웹 앱에 대해 이를 수행했습니다. 즉, 잘못된 입력을 취소하고, 그것에 의존했던 것만 재실행하며, 재실행이 이전에 생성한 출력과 동일하다는 것을 확인하기 위해 equiv 검사를 사용해 연쇄 반응을 멈춥니다. 지난 8월에는 arXiv 2608.10502가 이를 단일 에이전트의 메모리에 적용했습니다. kintsugi는 여러 에이전트가 작업을 서로에게 전달하고, 그래프 데이터베이스가 의존성을 추적하며, 되돌릴 수 없는 것들의 목록을 가지고 있는 아이디어입니다.
저는 선택적 재실행(selective re-execution)을 발명한 것이 아닙니다. 그것이 에이전트에서 작동하는 것을 보고 싶었을 뿐입니다.
모든 읽기 기록을 남기지 않으면, 복구할 것이 아무것도 없습니다
모든 것은 하나의 규칙에 달려 있습니다: 에이전트는 기록기(recorder)를 통해서만 메모리에 접근합니다. 에이전트 프레임워크가 없으며, 숨겨진 상태도 없습니다. 각 에이전트는 *recorder.Rec을 받아 rec.Get(key), rec.Set(key, value), rec.Handoff(agent, payload) 및 rec.Effect(kind, amount)를 호출하는 단순한 Go 함수입니다.
이는 저의 의견이며 저는 이를 옹호할 것입니다. 프레임워크는 기록기가 볼 수 없는 메모리와 도구 호출을 관리하며, 기록기가 보지 못하는 읽기 작업은 복구할 수 없는 의존성 문제입니다.
모든 호출이 노드와 간선(edge)이 됩니다:
| 간선 (Edge) | 의미 (Meaning) |
|---|---|
(Action)-[:INGESTED]->(Source) | 액션이 외부 입력을 읽음 |
| ... | |
사실(Facts)은 버전 관리되며 절대 삭제되지 않습니다. FEEDS 간선은 읽기 시점에, 해당 읽기를 해결하는 동일한 쿼리 내에서 작성됩니다: |
MATCH (f:Fact {key: $key, active: true}) WHERE f.seq < $seq
WITH f ORDER BY f.seq DESC, f.version DESC LIMIT 1
MATCH (r:Action {id: $aid})
...
따라서 실행이 완료될 때쯤이면 의존성 그래프가 이미 존재합니다. 그 이후에는 아무것도 추론되지 않으며, 출처(provenance) 경로 어디에도 LLM 개체 추출(entity extraction)은 없습니다. 이는 결정적(deterministic)입니다.
데모를 방문하는 각 사용자는 하나의 FalkorDB 인스턴스에 자신만의 그래프(demo-<session>)를 가지며, 이는 공유된 clean 기준선 및 llm_cache 그래프 옆에 위치합니다. 이것은 FalkorDB의 다중 그래프(multi-graph) 지원이 테넌트 격리(tenant isolation)를 무료로 제공하는 것이므로, 동시에 복구 작업을 수행하는 두 사람이 서로의 작업 내용을 절대 볼 수 없습니다.
피해 범위는 세 줄입니다
MATCH (s:Source {id: $src})<-[:INGESTED]-(a0:Action)
MATCH (a0)-[:FEEDS|HANDED_OFF*0..]->(a:Action)
RETURN DISTINCT a ORDER BY a.seq
나쁜 소스에서 시작하여 FEEDS와 HANDED_OFF를 따라 모든 깊이로 바깥쪽으로 이동합니다. 데모 실행에서는 66개 중 25개의 액션에 해당합니다. ORDER BY a.seq는 seq가 각 그래프 내의 전역 순서이기 때문에 재실행 순서를 무료로 제공합니다.
algo.bfs가 명백한 적합성처럼 보이지만, 이는 하나의 관계 유형(또는 모두 null)만 받으며, 이 경우에는 정확히 FEEDS|HANDED_OFF, 그리고 아웃고잉(outgoing)만 필요합니다. 따라서 가변 길이 패턴은 유지됩니다.
여기가 그래프가 저장소가 아닌 곳입니다. FalkorDB를 제거하면 폭발 반경도 없고, 순서도 없고, 조기 중단 기능도 없습니다. 이 제품은 이러한 쿼리들입니다.
조기 중단(early stop)에서 절약이 발생합니다
복구는 seq 순서의 폭발 반경을 거칩니다. 먼저 오염된 액션(poisoned action)을 마스킹합니다: 상태가 poisoned가 되고, 그 팩트들은 active = false가 되어 다음 vip_rules 읽기는 마지막 좋은 버전으로 폴백(fallback)됩니다. 그런 다음 각 액션은 상류(upstream)에서 무언가 변경된 경우에만 재실행됩니다:
MATCH (a:Action {id: $id})
RETURN size([(u:Action)-[:FEEDS|HANDED_OFF]->(a)
WHERE u.status IN ['poisoned', 'rerun_changed', 'superseded', 'new'] | u]) > 0 AS must_rerun
재실행이 이전과 동일한 출력을 생성하면, 이는 rerun_same으로 표시되고 그 하류(downstream)에 있는 것은 다시 실행할 필요가 없습니다. 이것이 RETRO의 equiv입니다.
문제는 LLM 파이프라인에서
2. 입력 해시(input hash)로 모델 호출 캐싱(Cache model calls by input hash). Temperature 0은 결정적이지 않다(not deterministic). 따라서 모든 모델 호출은 FalkorDB에 저장되고 sha256(모델, 프롬프트 버전, 입력, 정렬된 사실)으로 키가 지정되는 응답 캐시를 거친다. 같은 입력에는 같은 답변이 나온다. 하지만 vip_rules가 변경된 트리아지(triage) 호출은 다른 키로 간주되어 실제 모델에 전달된다.
demo 실행에서, 실제로 전액 환불을 받을 자격이 있던 3개의 VIP 티켓(t01부터 t03까지)은 클린 정책(clean policy)을 대상으로 트리아지를 재실행했고, 동일한 결정을 얻었으며 거기서 멈췄다. 이들의 6개 환불 및 알림자(notifier) 단계는 건너뛰어졌다. 총 25개의 범위 중 14개의 액션이 재실행되었다.
수정 사항이 누가 작업을 받는지 변경할 때(when the fix changes who gets the work)**
재실행은 어떤 에이전트가 작업을 받게 할지 바꿀 수 있다. t05의 트리아지는 이전에
그리고 재실행할 수 없는 것들이 있습니다. t05에 대한 환불은 이미 나갔습니다. kintsugi는 외부 효과를 다시 보내거나 되돌리지 않습니다. 무효화된 모든 효과는 보상 목록에 기록되며, Gemini가 작성한 수정안이 첨부됩니다: 환불 취소 요청서와 고객 이메일입니다. 작성만 되고, 전송되지는 않습니다. 인간이 결정합니다.

just demo의 실제 수치, 실제 모델을 이 가상 시나리오에 적용해 보겠습니다:
| Count | |
|---|---|
| Actions recorded (poisoned run) | 66 |
| ... |
어떻게 작동했는지 알 수 있나요
각 데모는 또한 오염된 페이지를 본 적이 없는 기준선(baseline) 실행 기록도 남깁니다. 복구 후, kintsugi는 수정된 그래프를 이 기준선과 비교합니다: 활성 사실(active facts), 티켓 결정(ticket decisions), 그리고 실시간 효과(live effects). 데모의 경우, 세 가지 모두 0개의 행 차이로 돌아옵니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기


