개발/AI

[에이전트] ReAct 도구 순서 가드 read 후 write 강제

jykim23 2026. 6. 25. 21:47
반응형

부제: 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)는 권고라, 모델이 안 지키면 회귀가 반복된다
  • 순서가 중요한 작업(읽고 나서 쓰기)은 도구 레이어에서 강제할 수 있다
  • 거부는 막다른 길이 아니라 모델이 스스로 교정하게 두는 신호로 쓴다
  • 턴마다 상태를 초기화하고, 같은 스레드는 순차 실행해야 게이트가 새지 않는다

"하라고 적었는데 안 한다"가 반복되면, 적는 자리를 프롬프트에서 도구로 옮기는 선택지가 있다.

반응형