코딩 에이전트(Coding Agent) 구축 시 파일 편집이 가장 어려운 이유
요약
자율 코딩 에이전트 Grinta를 구축하며 경험한 파일 편집의 기술적 난제와 해결 과정을 다룹니다. LLM이 코드의 들여쓰기, 이스케이프, 정확한 타겟팅을 유지하며 안정적으로 파일을 편집하게 만드는 것이 왜 어려운지 분석합니다.
핵심 포인트
- LLM의 파일 편집은 단순한 쓰기 작업을 넘어 높은 인지 부하를 요구함
- JSON 구조화 방식은 이스케이프 및 줄바꿈 문제로 인해 코드 손상 위험이 있음
- XML/Raw 블록 방식은 이스케이프 문제를 줄이나 모델의 컨텍스트 스위칭 비용을 발생시킴
- 결정론적이어야 하는 파일 작업에서 LLM의 환각과 형식 오류 제어가 핵심 과제임
처음부터 자율 코딩 에이전트 런타임(autonomous coding agent runtime)인 Grinta를 구축하며 얻은 교훈입니다. 제가 저의 자율 코딩 에이전트 런타임인 Grinta를 만들기 시작했을 때, 파일 편집(file editing)은 비교적 쉬운 부분 중 하나일 것이라고 생각했습니다. 에이전트가 파일을 읽고, 무엇을 변경할지 결정한 다음, 그 결과를 다시 쓰는 것 말이죠. 간단해 보이지 않나요? 하지만 그렇지 않았습니다. 파일 편집은 전체 시스템에서 가장 고통스러운 부분 중 하나가 되었습니다. 파일을 쓰는 것이 어려워서가 아니라, LLM(Large Language Model)이 파일을 안정적으로 편집하게 만드는 것은 완전히 다른 문제였기 때문입니다.
순진한 가정
처음에는 문제가 단순하다고 생각했습니다. 모델에게 파일 편집 도구를 주고, 변경 사항을 요청한 뒤, 그 결과를 적용하면 된다고 말이죠. 하지만 현실은 훨씬 더 험난했습니다. 모델은 단순히 무엇을 편집해야 하는지만 알아야 하는 것이 아닙니다. 또한 다음과 같은 사항들을 처리해야 합니다:
- 들여쓰기(indentation) 유지
- 콘텐츠의 올바른 이스케이프(escaping)
- 정확한 파일 타겟팅
- 정확한 심볼(symbol) 또는 문자열 타겟팅
- 패치(patch)에 대한 환각(hallucination) 방지
- 코드 블록 손상 방지
- 일반 텍스트와 도구 호출(tool calls)의 혼합 방지
- 편집 실패 시 복구
- 다른 편집 전략보다 특정 전략을 사용해야 하는 시점 파악
이는 결정론적(deterministic)이어야 하는 파일 작업치고는 너무나 많은 인지 부하(cognitive load)를 요구하는 작업입니다.
JSON만으로는 부족했습니다
저의 첫 번째 본능은 일반적인 구조화된 도구 호출(structured tool calls)에 의존하는 것이었습니다. 다음과 같은 방식 말이죠:
{ "path" : "src/app.py" , "old_string" : "old code" , "new_string" : "new code" }
이것은 깔끔하게 들립니다. 문제는 JSON 내부의 코드 콘텐츠 역시 여전히 JSON 내부의 코드 콘텐츠라는 점입니다. 모델은 이스케이프된 줄바꿈(
), 이스케이프된 따옴표, 유효한 JSON 문자열, 정확한 들여쓰기, 그리고 유효한 소스 코드를 동시에 생성해야 합니다. 바로 이 지점에서 문제가 발생하기 시작했습니다.
때때로 모델은 실제 줄바꿈 대신 리터럴(literal) \n 시퀀스를 생성했습니다. 때때로 따옴표를 잘못 이스케이프했습니다. 때때로 콘텐츠는 기술적으로는 유효한 JSON이었지만 유효하지 않은 코드였습니다. 때때로 페이로드(payload)에 마크다운(markdown) 스타일의 서식을 섞어 넣기도 했습니다. 좌절스러운 부분은 모델이 의도한 편집 내용은 이해하고 있었음에도 불구하고, 전송 형식(transport format)이 실패 지점이 되었다는 것입니다.
XML/raw 블록이 도움이 되었으나, 다른 방식으로 실패했습니다. 그 후 저는 XML 스타일의 편집 블록(editing blocks)과 raw 콘텐츠 블록(raw content blocks)을 실험했습니다. 아이디어는 간단했습니다. 메타데이터(metadata)는 구조화된 상태로 유지하되, 코드 페이로드(code payload)는 raw 텍스트로 두는 것이었습니다. 이를 통해 일부 JSON 이스케이프(escaping) 문제를 줄일 수 있었습니다. 하지만 새로운 문제가 발생했습니다. 모델이 이제 사고 방식(mental formats)을 전환해야 한다는 점이었습니다. 대부분의 도구는 일반적인 네이티브 도구 호출(native tool calls) 방식이었지만, 파일 편집은 다른 XML/raw 형식을 사용했습니다. 이러한 컨텍스트 스위칭(context switch)은 놀라울 정도로 비용이 많이 들었습니다. 때때로 모델은 XML 경계를 준수했습니다. 때때로 모델은 XML 블록 내부에서 어쨌든 JSON 이스케이프를 혼용했습니다. 때때로 모델은 블록 주변에 설명을 작성했습니다. 때때로 모델은 raw 블록을 마크다운(markdown)처럼 취급했습니다. 따라서 문제는 완전히 해결되지 않았습니다. 단지 다른 곳으로 옮겨졌을 뿐입니다.
패치(Patches)와 범위 편집(range edits) 또한 마법은 아닙니다. 그다음 저는 패치 방식의 편집(patch-style editing)과 범위 교체(range replacement)를 살펴보았습니다. 패치는 압축적이고 개발자들에게 익숙하기 때문에 매력적입니다. 라인 범위(Line ranges)는 이전 문자열을 검색할 필요가 없기 때문에 매력적입니다. 하지만 자율 에이전트 루프(autonomous agent loop) 내에서 두 방식 모두 약점이 있습니다. 패치는 주변 컨텍스트가 변경되거나, 모델이 컨텍스트를 지어내거나, 패치 형식이 약간 잘못되었을 때 실패할 수 있습니다. 라인 범위는 파일을 읽고 쓰는 사이에 파일이 변경되거나, 모델이 오래된 라인 번호(stale line numbers)에 의존할 때 실패할 수 있습니다. 이러한 방식들은 내부적으로는 유용하지만, 이러한 저수준(low-level) 편집 스타일을 모델에게 직접 너무 많이 노출하면 '도구 쇼핑(tool-shopping)' 현상이 발생합니다. 모델은 다음과 같이 질문하기 시작합니다: '패치를 사용해야 할까? 범위를 교체해야 할까? 파일을 다시 써야 할까? XML을 사용해야 할까? 쉘(shell)을 사용해야 할까? 문자열 치환을 사용해야 할까? AST 편집을 사용해야 할까?' 이것은 정확히 잘못된 사고 모델(mental model)입니다.
진짜 문제는 형식이 아니었습니다. 여러 접근 방식을 시도한 후, 저는 중요한 사실을 깨달았습니다. 문제는 단지 JSON 대 XML 대 패치의 문제가 아니었습니다. 더 깊은 문제는 제가 에이전트에게 너무 많은 편집 사고 모델을 노출하고 있었다는 점이었습니다.
저는 모델에게 무엇을 변경해야 하는지뿐만 아니라, 편집 시스템 자체가 어떻게 작동해야 하는지까지 결정하도록 요구하고 있었습니다. 이는 주객전도된 방식입니다. 코딩 에이전트 (Coding Agent)가 로우 파일 쓰기 (raw file writes), 패치 형식 (patch formats), XML 블록 (XML blocks), 쉘 헤레독 (shell heredocs), 섹션 편집 (section edits), 범위 편집 (range edits), 그리고 AST 편집 (AST edits)의 관점에서 생각할 필요는 없어야 합니다. 모델이 마주하는 API는 의도 (intent)를 기술해야 하며, 런타임 (runtime)이 구현을 처리해야 합니다.
전환점: 의도 기반 편집 도구 (intent-based editing tools)
그래서 저는 Grinta의 편집 인터페이스를 단순화하기 시작했습니다. 많은 편집 메커니즘을 노출하는 대신, 다음과 같은 더 작은 규모의 의도 기반 도구 세트로 이동하고 있습니다:
- read
- create
- edit_symbols
- replace_string
- multiedit
아이디어는 간단합니다. read는 컨텍스트(파일, 범위 또는 심볼)를 조사하기 위한 것입니다. create는 새로운 것(파일 또는 코드 심볼)을 생성하기 위한 것입니다. edit_symbols는 기존 코드 심볼을 수정하거나 삭제하기 위한 것입니다. replace_string은 단일 파일 내에서 정확한 텍스트를 교체하기 위한 것입니다. multiedit은 원자적 다중 파일 리팩터링 (atomic multi-file refactoring)을 위한 것입니다.
이 방식은 모델에게 훨씬 더 단순한 결정 트리 (decision tree)를 제공합니다. 모델은 더 이상 10가지의 편집 형식 사이에서 고민할 필요가 없습니다. 대신 의도를 선택합니다. 경로 안전성 (path safety), 검증 (validation), 디프 (diffs), 구문 체크 (syntax checks), 원자적 쓰기 (atomic writes), 그리고 롤백 (rollback)은 런타임이 처리합니다.
읽기는 유연할 수 있지만, 쓰기는 고정되어야 합니다
매우 중요해진 한 가지 규칙은 다음과 같습니다: 읽기는 검색할 수 있지만, 쓰기는 대상을 지정해야 합니다 (Reads may search. Writes must target).
예를 들어, 심볼을 읽는 것은 유연할 수 있습니다. 모델이 심볼 읽기를 요청했을 때 정확히 일치하는 것이 하나라면, 런타임은 이를 자동으로 해결하여 내용을 반환할 수 있습니다. 일치하는 것이 여러 개라면 후보군을 반환합니다. 일치하는 것이 없다면 유용한 피드백을 반환합니다. 읽기는 프로젝트를 변형시키지 않기 때문에 안전합니다.
하지만 쓰기는 다릅니다. 심볼을 편집할 때 런타임은 추측해서는 안 됩니다. 대상이 모호하다면 편집은 실패해야 하며 후보군을 반환해야 합니다. 그러면 모델은 더 정확한 대상을 가지고 다시 시도할 수 있습니다. 이 한 가지 규칙이 많은 위험한 동작들을 제거해 줍니다.
런타임의 책임이 중요합니다
이 경험은 에이전트 아키텍처 (agent architecture)에 대한 저의 생각 또한 바꾸어 놓았습니다.
에이전트의 신뢰성 중 상당 부분은 프롬프트(prompt)에서 오는 것이 아닙니다. 그것은 런타임(runtime)에서 옵니다. 런타임은 다음 사항들을 강제해야 합니다:
경로 안전성 (path safety)
정확한 매칭 (exact matching)
모호성 거부 (ambiguity rejection)
원자적 쓰기 (atomic writes)
커밋 전 검증 (validation before commit)
구조화된 에러 (structured errors)
롤백 동작 (rollback behavior)
디프 생성 (diff generation)
교착 상태 탐지 (stuck detection)
콘텐츠 가드 (content guards)
예를 들어, 모델이 모든 곳에 리터럴 \n을 포함하는 전체 함수 본문처럼 명백히 직렬화된(serialized) 것처럼 보이는 코드 콘텐츠를 보낸다면, 런타임은 파일이 손상되기 전에 이를 거부해야 합니다. 해결책은 모델에게 더 간절히 부탁하는 것이 아닙니다. 해결책은 위험한 상태를 불가능하게 만드는 것입니다.
교훈
지금까지 얻은 가장 큰 교훈은 이것입니다: 신뢰성은 모델에게 행동할 수 있는 더 많은 방법을 제공한다고 해서 생기지 않습니다. 신뢰성은 모델에게 더 적고 명확한 선택지를 제공하고, 복잡성을 결정론적 코드(deterministic code)로 옮길 때 생깁니다. 코딩 에이전트는 구현의 복잡성을 제품의 표면(product surface)으로 노출해서는 안 됩니다. 모델은 전송 형식(transport formats), 패치 형식(patch formats), 에디터 블록(editor blocks), 또는 쉘 이스케이핑(shell escaping)에 대해 고민할 필요가 없어야 합니다. 모델은 의도(intent)의 관점에서 생각해야 합니다: 문맥 읽기(read context), 새로운 것 생성(create something new), 기존 심볼 수정(edit existing symbols), 정확한 텍스트 교체(replace exact text), 원자적 다중 파일 리팩터링 수행(perform an atomic multi-file refactor). 그 외의 모든 것은 런타임의 책임이어야 합니다.
여전히 구축 중
Grinta는 여전히 개발 진행 중입니다. 저는 여전히 파일 편집 신뢰성, 상태 머신(state machines), 종료 탐지(finish detection), 서킷 브레이커(circuit breakers), TUI 통합, 비동기 실행(async execution), 크래시 복구(crash recovery), 그리고 문맥 관리(context management)와 싸우고 있습니다. 하지만 이 구체적인 교훈은 자율 코딩 에이전트(autonomous coding agents)에 대해 제가 생각하는 방식을 바꾸어 놓았습니다. 어려운 점은 단순히 모델을 똑똑하게 만드는 것이 아닙니다. 진짜 어려운 점은 모델이 틀릴 기회를 더 적게 갖도록 시스템을 설계하는 것입니다. 그것이 진정한 엔지니어링 과제입니다.
저는 여기서 Grinta를 공개적으로 구축하고 있습니다: https://github.com/josephsenior/Grinta-Coding-Agent
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기