반응형
부제: LLM이 읽기 전에 쓰지 못하게 막기

캘린더를 다루는 에이전트에서 가끔 사고가 났다. 모델이 기존 일정을 읽지 않고 바로 일정을 만들거나 고쳤다. 그러면 두 가지 문제가 생긴다.
- 이미 있는 약속과 겹치는 시간에 새 일정을 덮어쓴다.
- 수정·삭제할 때 event_id를 실제로 모르니 그럴듯한 값을 지어낸다(환각).
문제
프롬프트와 도구 설명(docstring)에 "먼저 조회하라"고 적어도 회귀가 반복됐다. 지시는 권고일 뿐이라, 모델이 안 지키면 그만이다.
여기서 Claude Code의 패턴이 떠올랐다. Claude Code는 파일을 Write하기 전에 그 파일을 Read하지 않았으면 시스템이 거부한다. 지시가 아니라 도구 레이어에서 강제하는 방식이다. 이걸 우리 도구 레이어에 옮겨오기로 했다.
정리한 방식 — 턴 범위의 read→write 게이트
도구마다 (도메인, 역할)을 붙였다. 예를 들어 캘린더 도메인에서 get_events는 READ, manage_event는 WRITE다.
규칙은 단순하다.
- READ 도구가 성공하면, 그 턴 안에서 해당 도메인을 "읽었다"고 표시한다.
- WRITE 도구는 같은 턴에 그 도메인을 읽은 적이 있을 때만 실행한다. 없으면 실행하지 않고 에러 메시지를 돌려준다.
# 도구 실행을 감싸는 미들웨어 (요지)
if role is READ:
result = await execute(request)
if result.status != "error": # 실패한 read는 read로 치지 않음
gate.mark_read(thread_id, domain)
return result
# WRITE
if not gate.has_read_in_turn(thread_id, domain):
return ToolMessage(
content='{"error": "이 턴에서 먼저 해당 도메인을 읽으세요."}',
status="error",
)
return await execute(request)
거부는 막다른 길이 아니다. 모델은 에러 메시지를 보고 "아, 먼저 조회해야겠다"며 READ 도구를 부른 뒤 다시 시도한다. 즉, 강제하되 막진 않는다.
몇 가지 세부도 있었다.
- 턴마다 초기화: 그래프 진입 시 읽음 표시를 비운다. 지난 턴에 읽었다고 이번 턴의 쓰기를 통과시키면 안 되니까. (정상 경로와 체크포인트 복구 경로 둘 다에 초기화를 걸어야 했다.)
- 같은 스레드는 순차 실행: 도구가 병렬로 불려도 같은 스레드의 게이트 도구는 락으로 순서를 지킨다. 안 그러면 read 표시 직전에 write가 끼어든다.
결과
가드를 켠 뒤 24시간 동안, 사전 읽기 없이 쓰기를 시도한 위반은 0건이었다. 프롬프트로는 반복되던 회귀가, 도구 레이어에서 강제하니 멈췄다.
정리
- 지시(프롬프트·docstring)는 권고라, 모델이 안 지키면 회귀가 반복된다
- 순서가 중요한 작업(읽고 나서 쓰기)은 도구 레이어에서 강제할 수 있다
- 거부는 막다른 길이 아니라 모델이 스스로 교정하게 두는 신호로 쓴다
- 턴마다 상태를 초기화하고, 같은 스레드는 순차 실행해야 게이트가 새지 않는다
"하라고 적었는데 안 한다"가 반복되면, 적는 자리를 프롬프트에서 도구로 옮기는 선택지가 있다.
반응형
'개발 > AI' 카테고리의 다른 글
| [프롬프트] Gemini System Prompt 고정 캐싱 전략 (1) | 2026.07.01 |
|---|---|
| [프롬프트] persona drift 대응 생성 지점 context 재주입 (0) | 2026.06.25 |
| [메모리] 추출 중복 차단 context 주입 단일 출처 (0) | 2026.06.25 |
| [메모리] 문맥 전환 감지 임베딩 유사도 한계 매턴 검색 회귀 (0) | 2026.06.25 |
| [Gemini] thought signature 보존 환각 도구 호출 방지 (0) | 2026.06.25 |