
코딩을 하지 않는 코딩 에이전트
요약
직접 코드를 작성하지 않고 사용자의 결정에 이의를 제기하며 피드백을 주는 소크라테스식 코딩 에이전트 'Socreates'를 소개합니다. 에이전트의 핵심 구성 요소인 워크스페이스 정보 수집, 도구 실행, 컨텍스트 제어, 메모리 유지의 개념을 설명합니다.
핵심 포인트
- 코드를 직접 수정하지 않고 비판적 피드백을 제공하는 에이전트 설계
- 에이전트의 핵심: 워크스페이스 정보 수집 및 도구 실행
- LLM의 한계를 극복하기 위한 컨텍스트 크기 제어의 중요성
- 상태 유지를 위한 메모리 제공 및 제어 루프 구조
코딩을 하지 않는 코딩 에이전트
우리는 코딩 에이전트 (coding agents)를 사랑합니다. 적절한 프롬프트 (prompt)를 주고 밤새 실행해 두면, 이들은 풀스택 SaaS를 구축하고 잠재적으로 당신을 백만장자로 만들어 줄 수도 있습니다. 하지만 이들은 당신의 GPU나 예산을 태워버릴 것이고, 요청하지 않은 몇 가지 취약점 (vulnerabilities)을 포함할 것이며, 디버깅을 시작하는 순간 당신의 정신 건강을 위협할 정도로 코드를 비대하게 만들 것이고, 주석에 이모지를 집어넣을 것이며, 궁극적으로는 당신의 인생 선택에 의문을 갖게 만들 것입니다.
그래서 저는 생각했습니다. 이들이 그렇게 멋지다면, 직접 하나를 만들어 Anthropic의 명성을 훔쳐보는 것도 흥미로울 것이라고 말이죠.
하지만 아시다시피, 이 블로그는 종종 황당한 프로그래밍의 경계에 서 있곤 합니다. 따라서 오늘 우리가 만들 에이전트는 아마도 최초의 '코딩을 하지 않는 코딩 에이전트'가 될 것입니다.
이 에이전트의 이름은 Socreates (네, 오타가 포함되어 있습니다)라고 불릴 것이며, 이는 소크라테스식 에이전트 (Socratic agent)입니다. 이 에이전트는 당신의 실수를 잡아내고, 당신의 결정에 이의를 제기하며, 잔혹한 의견을 가진 러버 덕 (rubber duck) 역할을 할 것입니다. 하지만 절대로 당신의 코드를 건드리지 않을 것입니다. 당신이 직접 모든 것을 타이핑해야 합니다. 그리고 저는 많은 개발자가 좋았던 옛 시절에 코드를 작성하는 것을 실제로 즐겼다고 들었습니다. 어떤 이들은 스스로를 "코더 (coders)"라고 부르기도 했죠.
에이전트 (A gent)
코드로 뛰어들기 전에, "코딩 에이전트"가 실제로 무엇인지 명확히 해봅시다.
우리는 LLM (Large Language Model)이 단지 다음 토큰 예측기 (next-token predictor)라는 것을 알고 있습니다. 추론 모델 (reasoning model)은 중간 단계에 더 많은 시간을 할애하도록 훈련된 동일한 LLM입니다. 에이전트 (agent)는 무엇을 조사할지, 어떤 도구 (tools)를 호출할지, 그리고 언제 멈출지를 결정하기 위해 LLM을 사용하는 제어 루프 (control loop)입니다.
이러한 에이전트 루프 (agentic loop) 덕분에 Claude Code가 채팅창에 있는 동일한 모델보다 훨씬 더 유능하게 느껴지는 것입니다.
가장 단순한 형태의 코딩 에이전트는 오직 몇 가지 핵심 작업만을 수행합니다:
- 모델이 사용자를 위해 작업을 시작할 수 있도록 워크스페이스(workspace)에 대한 정보 수집 (파일 트리, git 리포지토리 상태 등)
- 도구 실행 (파일 읽기, 사용자의 머신에서 명령 실행과 같은 구조화된 액션 - 과거의 원격 프로시저 호출 (RPC)과 유사함)
- 컨텍스트 크기 (context size) 제어 – LLM에는 한계가 있으므로 긴 출력물 클리핑 (clipping), 불필요한 도구 호출 방지, 가능한 모든 방법으로 컨텍스트 압축
- 다음 날 에이전트를 재시작하더라도 대화 상태를 유지할 수 있도록 메모리 (memory) 제공
어떤 에이전트들은 특정 작업을 제한된 서브 에이전트 (sub-agents)에게 위임하고, 이들을 오케스트레이션 (orchestrating)하거나 병렬로 처리하는 등 더 많은 일을 수행하기도 하지만, 우리는 단순함을 유지합니다. 하나의 루프 (loop), 네 개의 도구, 의존성 없음.
루프 (The loop)
루프 자체는 거의 사소한 수준입니다:
- 사용자가 메시지를 입력합니다.
- 에이전트가 시스템 프롬프트 (system prompt) + 대화 기록을 LLM에 보냅니다.
- LLM이 텍스트로 응답하거나 도구 호출 (tool calls)을 요청합니다. 최종 답변인 경우 루프를 중단합니다.
- 그렇지 않은 경우, 에이전트는 도구를 실행하고 그 결과를 다음 반복 (iteration)에서 LLM에 다시 전달합니다.
- 3단계로 돌아가며, 반복이 너무 오래 걸릴 경우 응답을 강제합니다.
이것이
Ollama와 OpenAI 호환 API 모두 API를 통한 "네이티브 도구 호출 (native tool calling)"을 지원합니다. 사용 가능한 함수를 JSON Schema로 기술한 tools 배열을 전송하면, 모델은 구조화된 tool_calls로 응답합니다. 대화는 대략 다음과 같은 형태를 띱니다:
> system: "당신은 코딩 동료입니다..."
> user: "내 코드를 리뷰해줘"
< assistant: {tool_calls: [{function: {name: "list_files", arguments: "{}"}}]}
...
각 제공자마다 전송 형식 (wire formats)이 약간 다르기 때문에 각자의 HTTP 클라이언트가 필요합니다:
- Ollama (
/api/chat)는arguments를 이미 JSON 객체로 반환하므로,json.RawMessage를 사용하여 마샬링 (marshal) 합니다. 토큰 사용량은 모든 응답에prompt_eval_count및eval_count로 포함되어 제공됩니다. - OpenAI/DeepSeek (
/v1/chat/completions)는 도구 전용 메시지의 경우content가 JSONnull로 직렬화된다고 가정합니다 (생략되지 않음). 호출 ID (call ID)가 없는 도구 결과는 거부됩니다. 토큰 사용량 또한 제공되지만, 표준usage객체라는 다른 형식으로 제공됩니다.
이것들은 지루한 세부 사항이며 구현 또한 지루합니다. GitHub에서 직접 확인하실 수 있습니다 (링크는 끝에 있습니다).
시스템 프롬프트 (System Prompt)
이제 여러분은 진짜 마크다운 엔지니어가 된 기분을 느낄 수도 있습니다.
코딩 에이전트에서 프롬프트는 보통 여러 계층으로 구성됩니다: 안정적인 접두사 (prefix, 지침 + 도구 스키마 + 워크스페이스 요약), 그리고 변화하는 세션 상태 (최근 기록 + 사용자 요청) 순서입니다. 이렇게 함으로써 접두사에는 캐시된 토큰 (저렴한 토큰)을 사용하고, 나머지 부분에는 비용이 더 드는 토큰을 사용하게 됩니다.
Socreates는 충분히 단순하며, 시스템 프롬프트 ("안정적인 접두사")는 printf를 통해 워크스페이스 경로가 주입된 짧은 메시지일 뿐입니다:
You are a coding companion — a sharp, critical reviewer who catches bugs
and challenges decisions. You NEVER write code. The developer types all code;
you ask questions, spot issues, and verify correctness using tools.
...
이 부분은 LLM들이 서로 논쟁을 벌인 끝에 저를 위해 작성해 준 부분인데, 훌륭한 결과물인지에 대해서는 매우 회의적입니다. 하지만 제 역할을 수행하기는 합니다. 불행히도 이 프롬프트가 제품의 핵심입니다. "성격"이나 규칙을 바꾸면, 숙련된 비평가 대신 뇌가 절제된(lobotomised) 주니어 러버덕(rubber duck)을 얻게 됩니다.
마지막 줄(또는 실제로는 마지막 여러 줄)은 *세션 메모리 (session memory)*입니다. 에이전트는 현재 작업 설명과 최근에 다룬 파일들을 추적하여 시스템 프롬프트 (system prompt)에 추가합니다. 이를 통해 모델은 모든 것을 다시 읽을 필요 없이 도구 호출 (tool rounds) 과정 전반에 걸쳐 연속성을 유지할 수 있습니다. 이것이 프롬프트의 동적인 부분입니다.
도구 스키마 (Tool schemas)는 API의 네이티브 tools 파라미터를 통해 전달되므로, 적어도 이 부분은 잘 구조화되어 있으며 무작위적인 마크다운 (markdown) 형식이 아닙니다. 도구에는 짧은 설명과 호출 방법 예시가 포함되어 있습니다.
Looooop
우리는 루프 (loop)를 통해 LLM을 심문합니다. 모델이 "생각"을 멈추고 최종 답변을 줄 때까지, 누적된 시스템 프롬프트와 지금까지의 대화 내용을 여러 번의 반복 (iterations) 동안 전송합니다. 반복 횟수에는 제한을 두었는데, 그렇지 않으면 어떤 재치 있는 모델들은 단순한 요청마다 당신의 예산을 기꺼이 탕진해 버릴 것이기 때문입니다. 우리는 모델의 요구를 정중하게 따르며 필요한 도구들을 호출하며, 이상적으로는 이를 병렬 (in parallel)로 처리합니다.
func (a *Agent) Chat(ctx context.Context, input string) (string, error) {
a.messages = append(a.messages, Message{Role: RoleUser, Content: input})
for step := range a.maxSteps {
...
사용자 메시지는 루프가 시작되기 전 한 번만 추가되며, 매 반복마다 추가되지 않습니다. 이 실수 때문에 DeepSeek에서 몇 센트를 낭비했습니다.
단계 제한 (Step limit)은 (제 취향에는) 상당히 관대한 편입니다. 저는 10번의 반복 후에 멈춥니다. 대부분의 에이전트는 반복당 단 몇 번의 도구 호출만을 예상하며 3~5회의 반복만을 수행한다고 들었습니다. 하지만 제가 테스트한 모델들은 생각하는 속도가 느렸습니다.
LLM에게 반복 제한에 대해 경고를 주는 것은, 모델이 계속 생각만 하다가 마지막에 제대로 된 답변을 내놓지 못하는 시나리오를 방지하는 데 도움이 되었습니다.
병렬 도구 호출 (Parallel tool calling)은 장난감 수준의 에이전트에게는 그리 중요하지 않을 수 있습니다. 파일을 읽는 것은 빠르니까요. 하지만 Go 언어를 사용하고 있으므로, 최소한 어느 정도의 동시성 (Concurrency)은 기대됩니다.
컨텍스트 압축 (Context compaction)
토큰 예산 (Token budgets)은 저를 잠 못 들게 합니다. 압축 (Compaction) 없이는 긴 대화가 모델의 컨텍스트 창 (Context window)을 초과하여 모델을 멍청하게 만들 뿐만 아니라, 실제 비용도 발생시키기 때문입니다. Socreates는 단순한 2패스 (Two-pass) 접근 방식을 사용합니다:
const maxHistoryTokens = 16000 // 약 64KB의 텍스트, 이 정도면 모두에게 충분하겠죠?
func (a *Agent) truncateHistory() {
// Pass 1: 과거의 도구 출력값 축소 (처음 400자만 유지)
...
어시스턴트 (Assistant)의 응답 메시지를 삭제할 때, 그에 따른 도구 응답 (Tool responses)을 함께 삭제하지 않으면 안 된다는 점에 놀랐습니다. 고립된 (Orphaned) 도구 응답은 OpenAI/DeepSeek 프로토콜에서 유효하지 않은 것으로 보입니다. 하지만 이는 압축을 더 공격적으로 만들 뿐이며, 비용 측면에서는 오히려 우리에게 유리하게 작용할 수도 있습니다.
도구 (Tools)
일반적인 LLM은 마크다운 (Markdown) 형식으로 명령어를 제안할 수 있습니다. 도구를 가진 에이전트는 이를 구조화된 방식으로 전달받아 실행합니다. 이를 통해 입력값을 검증할 수 있으며, 도구 호출 (Tool calling)에 대해 최소한의 안전 경계 (Safety boundaries)를 가질 수 있습니다. 적어도 모델이 우리가 알아차리지 못하는 사이에 임의의 행동을 환각 (Hallucinate)할 수는 없게 됩니다.
우리의 에이전트는 코딩을 하지 않는 에이전트이므로 파일을 작성할 수 없습니다. 저는 단 네 가지의 도구만으로도 꽤 멀리 갈 수 있다고 생각합니다:
list_files
: 워크스페이스 내의 fs.WalkDir 파일 트리 목록화
read_file
: 특정 라인 범위에서 io.Scanner로 텍스트 읽기
search
: grep과 유사하지만 셸 인젝션 (Shell injections)이 없는 재귀적 정규 표현식 (Regex) 검색
run_command
: 가장 위험한 도구로, 무엇이든 실행할 수 있지만 (go test 또는 git diff 등), 항상 사용자에게 확인을 요청해야 함
모든 도구 출력은 16K 글자(~4K 토큰) 이후에 잘립니다 (Truncated). 또한 모델에게 파일 크기를 알려주어 도구를 현명하게 사용할 기회를 제공합니다. 우리의 read_file 도구는 또한 [150 more lines. Use start=501 to continue.]와 같은 계속하기 힌트 (Continuation hint)를 반환하여 모델에게 어떻게 진행해야 할지 알려줍니다. 적어도 DeepSeek에서는 이것이 도움이 되는 것을 확인했습니다.
경로 해석 (Path resolution)은 단순하지만, ./pkg/../../../etc/passwd와 같은 경로를 읽는 것을 방지하기에는 충분합니다.
func resolvePath(root, path string) string {
abs := filepath.Clean(filepath.Join(root, path))
if rel, err := filepath.Rel(root, abs); err != nil || strings.HasPrefix(rel, "..") {
...
그리고 맞습니다, 당신의 편집증적인 수준을 충족하는 격리된 환경 (isolated environment)에서 실행한다면 모든 명령을 자동 승인 (auto-approve) 하는 옵션도 있습니다.
메모리 (Memory)
코딩 에이전트 (coding agent)는 턴 (turns)과 재시작 (restarts) 사이에도 유지되어야 합니다. 우리의 세션 상태 (session state)는 사용자 메시지, 어시스턴트 응답, 도구 호출 (tool calls), 그리고 도구 결과 (tool results)를 포함한 전체 대화입니다. 마치 "전사본 (transcript)"처럼, .socreates/session.json 내부에 JSONL 형식으로 저장됩니다.
한 번에 하나의 세션만 처리합니다. 어차피 저는 멀티태스킹을 잘 못하니까요.
type Session struct {
ID string `json:"id"`
Created time.Time `json:"created"`
...
/reset 명령 시, 현재 세션은 아카이브(이름 변경)되고 새로운 세션이 시작됩니다. 재시작 시, 에이전트는 마지막 세션을 로드하여 중단된 지점부터 계속 진행합니다. Memory는 시스템 프롬프트 (system prompt)에 추가되는 몇 가지 단서들을 제공하여, 모델이 전체 이력을 읽지 않고도 우리가 무엇을 작업하고 있었는지 알 수 있게 합니다.
다시 말해, 저장된 전사본은 완전하지만 (모든 메시지, 전체 내용), LLM에 컨텍스트 (context)로 보내는 내용은 압축되고 잘려 나갑니다 (truncated). 아마도 제한된 윈도우 (bounded window)라고 불리는 방식인 것 같습니다.
REPL
CLI는 단순히 표준 입력 (stdin) / 표준 출력 (stdout)일 뿐이며, TUI도 없고 화려하게 깜빡이는 애니메이션도 없습니다. 줄 편집 (line editing)을 위해 rlwrap을 사용하여 실행하며, 다음과 같은 모습입니다:
> review my error handling
(thinking...)
-> list_files(map[path:.])
...
qwen, llama, gemma, 그리고 deepseek API로 테스트해 보았으며, 물론 결과는 섞여 있었습니다. 하지만 완전히 형편없는 수준은 아니기에 만족합니다.
적어도 멋진 실험이었습니다. 코딩 에이전트를 동료 프로그래머처럼 사용하면서, 키보드를 만지게 하지는 않되 그들의 끙끙거림과 잔소리를 듣는 것은 즐겁게 여긴다면 어떤 모습일지에 대한 실험 말입니다.
직접 시도해 보고 싶다면 Github에서 확인하세요: github.com/zserge/socreates. 제안, 기여 및 피드백은 언제나 환영합니다!
2026년 5월 25일
참고: The old way to the modern web services 및 기타.
AI 자동 생성 콘텐츠
본 콘텐츠는 Lobste.rs AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기