코딩 에이전트 앞에 문서만 읽는 연구 에이전트 넣기

TL;DR
내 자율 코딩 에이전트는 라이브러리 API에 대해 존재하지도 않는 코드를 계속 배포하고 있었습니다. 먼저 코드를 쓰고, 문서는 전혀 읽지 않았기 때문입니다. 저는 구현 전에 실행되는 읽기 전용 연구 서브에이전트를 추가해서 이 문제를 해결했습니다. 이 서브에이전트는 짧고 구조화된 브리프를 반환하고, 그 브리프가 생기기 전까지 구현 에이전트의 작업을 막습니다. 환각된 API 버그는 3개월 동안 대략 5개 작업 중 1개에서 40개 작업 중 1개 정도로 줄었고, 추가 비용은 토큰의 10% 미만이었습니다. 어떻게 동작하는지, 그리고 그 과정에서 제가 무엇을 잘못했는지 소개하겠습니다. 🚀
문제
저는 Mac mini에서 완전 자율 구현 시스템을 운영합니다. 오케스트레이터가 작업을 가져와 Claude Code 위에 구축한 병렬 구현 에이전트들에게 넘기고, 자기복구 에이전트가 실패한 항목을 정리합니다. 이 시스템은 실제 프로젝트에 실제 코드를 배포하며, 대부분 무인으로 동작합니다.
처음 몇 달 동안 가장 짜증 나는 실패 양상은 이것이었습니다:
구현 에이전트가 자신 있게
client.batchUpsert()를 호출합니다. 그런데 해당 라이브러리에는batchUpsert()가 한 번도 없었습니다. 대신 v4.2에서 추가된upsertMany()가 있고, 인자 형태도 다릅니다.
테스트가 실패하고, 자기복구 에이전트가 세 가지 수정을 시도하다가 4만 토큰을 소모한 뒤 결국 포기하고 작업을 차단된 상태로 표시합니다. 저는 같은 근본 원인을 공유하는 “차단됨” 작업들의 더미를 보게 되었습니다. 에이전트가 API를 읽지 않고 추측한 것이었습니다.
실패한 작업 2주치를 감사해 보니 수치는 좋지 않았습니다:
| 실패 원인 | 실패 작업 비율 |
|---|---|
| 환각되었거나 오래된 라이브러리 API | 38% |
| 기존 내부 코드를 잘못 읽음 | 22% |
| 정말 어려운 버그 | 19% |
| 그 외 모두 | 21% |
실패의 60%는 “쓰기 전에 살펴보지 않음”이었습니다. 이건 지능 문제가 아닙니다. 프로세스 문제입니다.
흥미로웠던 점은, 구현 에이전트가 문서를 읽을 수 있었다는 것입니다. 웹 fetch와 파일 도구가 있었습니다. 다만 그렇게 하지 않았습니다. 능력 있는 모델에게 작업을 주는 순간, 그것의 본능은 코드를 생성하기 시작하는 것이기 때문입니다. “문서를 먼저 읽어 달라”라고 프롬프트하면 약 이틀 정도는 도움이 되었지만, 그다음부터는 컨텍스트 윈도우의 다른 내용들에 그 지시가 묻혀 버렸습니다.
해결한 방법
해결책은 프롬프트 기반이 아니라 구조 기반이었습니다. “이게 어떻게 작동하는지 알아내기”와 “그걸 구현하기”를 서로 다른 도구 집합을 가진 두 개의 다른 에이전트로 분리했습니다.
전체 구조
flowchart LR
O[Orchestrator] -->|task + question list| R[Research agent<br/>read-only]
R -->|research brief| O
O -->|task + brief| I[Implementation agent]
I -->|diff| V[Verifier]
V -->|pass / fail| O
O -.->|brief missing or low confidence| R이 구조가 동작하게 만드는 규칙은 세 가지입니다:
- 연구 에이전트는 쓸 수 없습니다. Read, Grep, Glob, 웹 검색, 웹 fetch만 받습니다. Edit, Write, Bash는 없습니다. “그냥 빨리 고치자”를 물리적으로 할 수 없습니다.
- 브리프가 없으면 구현 에이전트는 시작할 수 없습니다. 해당 작업에 대한 연구 브리프 파일이 존재하고 신뢰도 점수가 있어야만 오케스트레이터가 구현 작업을 넘깁니다.
- 브리프는 짧고 구조화되어 있습니다. 문서 덤프가 아닙니다. 계약서입니다.
연구 브리프 계약
모든 브리프는 고정된 형식의 작은 Markdown 파일입니다. 실제 예시는 아래와 같고, 약간 익명화되어 있습니다:
# Research brief: task-0417
## Questions
1. How do we bulk-insert rows with the ORM at the pinned version?
2. Does the existing repo already wrap this anywhere?
## Findings
### Q1
- Pinned version is 4.3.1 (from lockfile).
- Bulk insert is `Model.bulkCreate(rows, { updateOnDuplicate: [...] })`.
- `upsertMany` does NOT exist in this version. It was a community plugin.
- Source: node_modules/<orm>/lib/model.js lines 2210-2290, and the 4.x changelog.
### Q2
- Yes. `src/db/batch.ts` exports `insertBatch()` which already handles chunking at 500 rows.
- Two callers use it. Reuse it; don't add a second path.
## Verdict
Use the existing `insertBatch()` helper. Do not touch the ORM directly.
## Confidence: 0.9
## Sources read: 4 files, 1 changelog, 0 web pages
## Staleness risk: low (pinned version, checked lockfile)중요한 네 가지 필드는 Questions , Verdict , Confidence , Staleness risk 입니다. 나머지는 모두 보조 증거입니다. 구현 에이전트는 verdict를 먼저 읽습니다. 오케스트레이터는 confidence를 읽습니다.
연구 에이전트 정의
저는 서브에이전트를 frontmatter가 있는 Markdown 파일로 정의합니다. 연구용 에이전트는 대략 이렇게 생겼습니다:
name: researcher
description: Read-only investigation. Answers "how does X actually work"
by reading source, lockfiles, changelogs, and official docs. Returns a
research brief, never code.
tools: Read, Grep, Glob, WebSearch, WebFetch
You answer questions about how things actually work. You never propose
an implementation.
Rules:
- Check the pinned version in the lockfile BEFORE reading any docs.
Docs for the wrong major version are worse than no docs.
- Prefer reading node_modules / site-packages source over web docs.
Source can't be out of date for the installed version.
- Search the repo for existing wrappers before answering "how do I call X".
- If two sources disagree, report both and lower your confidence.
- Output ONLY the brief format. No preamble.그 “먼저 lockfile을 확인하라”는 문장은 고통스러운 한 주를 겪은 뒤에 추가되었습니다. 아래에서 더 설명하겠습니다.
오케스트레이터가 브리프로 작업을 막는 방식
게이트 자체는 지루한 껍데기다. 그게 핵심이다. 이건 매번 디스패치하기 전에 실행된다:
brief="state/briefs/${task_id}.md"
if [[ ! -f "$brief" ]]; then
dispatch_agent researcher "$task_id"
exit 0 # come back next tick
fi
confidence=$(grep -E '^## Confidence:' "$brief" | awk '{print $3}')
if (( $(echo "$confidence < 0.6" | bc -l) )); then
# Re-research with the low-confidence sections as new questions
dispatch_agent researcher "$task_id" --refine
exit 0
fi
dispatch_agent implementer "$task_id" --brief "$brief"내가 이 방식에서 좋아하는 점은 두 가지다:
- 결정론적 게이팅. 모델에게 준비됐는지 묻지 않는다. 파일이 있고 그 안에 숫자가 있거나, 아니면 없다.
- 리파인먼트는 상한이 있는 루프다. 낮은 신뢰도는 약한 질문들에 집중한 두 번째 리서치 패스를 유발한다. 두 번의 리파인먼트 후에는 작업이 무한 루프에 빠지지 않도록 "needs human"으로 표시된다.
구현자가 보는 것
구현자의 프롬프트에는 brief가 맨 위에 그대로 주입되고, 그 뒤에 한 줄이 붙는다:
위의 brief가 라이브러리 API와 기존 헬퍼에 대한 단일한 진실의 원천이다. 그것이 틀렸다고 생각하면, 멈추고 task 파일에 이유를 적어라. 그걸 우회하지 말라.
마지막 문장이 중요하다. 이걸 추가하기 전에는, 구현자가 brief를 읽고도 가끔은 말없이 동의하지 않은 채 자기 방식대로 해버리곤 했다. 이제는 의견 불일치가 grep으로 찾을 수 있는 눈에 보이는 사건이 되었다.
3개월 후의 결과
4개 프로젝트에서 약 1,100개의 작업을 기준으로:
| 지표 | 이전 | 이후 |
|---|---|---|
| 환각된 API 때문에 실패한 작업 | ~20% | ~2.5% |
| 작업당 self-healing 재시도(평균) | 1.8 | 0.6 |
| 완료된 작업당 토큰 수 | 기준선 | +8% |
| "needs human"으로 표시된 작업 | 11% | 6% |
8%의 토큰 오버헤드는 놀라웠다. 25% 이상일 거라고 예상했기 때문이다. 알고 보니 제거한 재시도 비용이 추가한 리서치 패스보다 훨씬 더 비쌌다. 실패한 구현 하나에 self-healing 시도 세 번을 붙이는 편이, 파일 네 개를 읽는 것보다 더 많은 비용이 든다.
배운 점
1. "이해"와 "구현"을 프롬프트가 아니라 도구로 분리하라
단일 에이전트에게 "먼저 조사하고, 그다음 코딩하라"고 말하는 건 점점 흐려진다. 컨텍스트가 차기 전까지는 잘 작동하지만, 그다음엔 지시보다 작업이 우선된다. 리서치 에이전트에 쓰기 도구를 주지 않으면 분리가 물리적으로 된다. 구현은 불가능하므로 구현 쪽으로 새어 나갈 수 없다.
이는 일반화된다: 어떤 행동을 안정적으로 원한다면, 친절하게 부탁하는 대신 대안 행동을 할 수 없게 만들어라.
2. lockfile은 마지막 문서가 아니라 첫 번째 문서다
이 시스템에서 내가 가장 힘들었던 주는, 리서치 에이전트가 라이브러리의 최신 웹 문서를 읽고 자신만만한 brief를 작성했는데, 구현자가 우리가 설치하지도 않은 메이저 버전을 기준으로 빌드해버린 때였다. 신뢰도는 0.95였다. brief는 완전히 맞았지만, 그건 잘못된 버전에 대해서였다.
이제 에이전트의 첫 번째 행동은 항상 lockfile이나 pip freeze 출력을 읽는 것이고, brief에는 Staleness risk 필드가 있다. 웹 문서보다 node_modules 안의 소스가 항상 우선한다. 설치된 소스는 설치된 버전에 대해 낡아 있을 수 없기 때문이다.
3. 신뢰도 점수는 그것을 누군가가 실제로 사용할 때만 유용하다
처음에는 신뢰도 필드를 내 읽기용으로만 추가했다. 장식용이었다. 오케스트레이터가 그것을 기준으로 게이팅하기 시작한 날부터 유용해졌다. 어떤 숫자든 소비자가 생기는 순간, 그 숫자를 만드는 에이전트는 보정(calibration)에 훨씬 더 신경을 쓰게 된다. 낮은 신뢰도가 눈에 보이는 재작업을 유발하기 때문이다.
이 교훈은 에이전트가 내놓는 어떤 구조화된 출력에도 똑같이 적용된다. 그 필드를 읽는 것이 아무도 없다면, 지우거나 연결하라.
4. 의견 불일치를 로그가 남는 이벤트로 만들어라
“브리프가 틀렸다고 생각하면 멈추고 이유를 써라”는 조용한 실패 모드를 데이터로 바꿨다. 3개월 동안 구현 담당자는 브리프에 23번 이의를 제기했다. 그중 9번은 맞았다. 그 9가지 사례는 연구 에이전트의 지침에 대한 테스트 케이스가 되었다. 틀린 이의 제기 14건은 거의 항상 구현 담당자가 학습 때 “기억해 둔” 더 최신 API를 쓰려 했기 때문이었다.
5. 작업이 작을 때는 연구를 건너뛰고, 임계값을 명확히 하라
모든 작업에 브리프가 필요한 것은 아니다. 변수를 이름 바꾸기, CSS margin 조정하기, 저작권 연도 올리기 같은 것들 말이다. 그런 일들에 대해 연구를 돌리면 토큰만 낭비했고, 더 나쁘게는 사소한 작업에도 시스템이 느리게 느껴지게 만들었다.
나는 크기 휴리스틱을 추가했다. 작업이 2개 미만의 파일에 영향을 주고 외부 라이브러리를 언급하지 않으면, 오케스트레이터가 신뢰도 1.0의 한 줄짜리 자동 브리프를 작성하고 다음 단계로 넘어간다. 이 임계값은 멍청하다. 내가 만들지 못한 똑똑한 임계값보다, 존재하는 멍청한 임계값이 낫다.
다음에 할 것
지금 작업 중인 것은 두 가지다:
- 작업 간 브리프 재사용. 지금은 마지막 작업이 같은 질문에 이미 답했더라도 매번 새로 연구를 한다. 라이브러리와 버전을 키로 하는 간단한 인덱스를 추가해 브리프를 7일 동안 재사용한 뒤 만료되게 할 예정이다.
- MCP 서버를 일급 시민으로서의 연구. 다른 도구들이 "이 버전의 이 저장소에서 X는 어떻게 동작하는가"를 물어보고, 오케스트레이터를 거치지 않고도 같은 브리프 형식의 응답을 받을 수 있게 하고 싶다.
또한 디버깅에도 같은 분리를 시도해 보고 싶다. 즉, 어떤 수정 에이전트가 코드를 건드리기 전에 가설 브리프를 만드는 읽기 전용 "diagnose" 에이전트다. 초기 징후는 유망하지만, 아직 이에 대해 글을 쓸 만큼 데이터가 충분하지 않다.
마무리
코딩 에이전트가 계속 API를 지어낸다면, 해결책은 아마 더 나은 모델이나 더 긴 프롬프트가 아니다. 그것은 프로세스 경계다. 한 에이전트에게는 눈만 주고 손은 주지 말고, 다른 에이전트에게는 손과 계약을 주며, 오케스트레이터가 첫 단계를 건너뛰지 못하게 만들어라.
재현하려는 사람들을 위한 버전 정보: 2026년 9월 기준 Claude Code 2.x 계열, Node.js 22.x, Python 3.13, 오케스트레이터는 macOS에서 순수 Bash로 구현.
이 글이 유용했다면, Dev.to에서 여기 나를 팔로우해 주세요. 24시간 내내 완전 자율 구현 시스템을 운영하며 쌓은 더 많은 빌드 로그를 공유하겠습니다. 그리고 에이전트를 위한 다른 종류의 연구 또는 계획 게이트를 만들어 본 적이 있다면, 댓글에서 그것이 어떻게 동작하는지 정말로 듣고 싶습니다. 💡
출처
- 원문: https://dev.to/yureki_lab/how-i-built-a-research-agent-that-reads-the-docs-before-my-coding-agent-ships-c9a (2026-09-21 · yureki_lab)