페이지 목차
이 페이지에서 검색
Graph Engineering — 에이전트 작업 흐름 설계
이 페이지에 묻기
이 Graph Engineering 강의 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

생성 수정
GRAPH ENGINEERING · 사용자 제공 원고
그래프 엔지니어링: 내 시스템은 스스로의 작업을 검증했다. 하지만 거짓말을 하고 있었다.
나는 그래프 엔지니어링(Graph Engineering)을 배울 생각이 없었습니다. 내가 하려 했던 건, 20분마다 자기가 뭘 하고 있었는지 잊어버리는 에이전트를 계속 붙잡고 감시하는 일에서 벗어나는 것이었습니다.
하나의 에이전트, 하나의 긴 대화, 한 번에 하나의 작업. 수십 개의 파일에 걸친 리팩토링이나 자료 조사처럼 5분을 넘어가는 진짜 작업을 시켜보면, 20분쯤 지났을 때 에이전트는 어김없이 원래 목적에서 벗어나기 시작했습니다. 파일 하나를 고치면서 다른 파일 하나를 조용히 망가뜨리고, 3분 전에 내린 결정을 잊어버렸죠. 재시작을 하면 지난 20분은 존재하지도 않았던 것처럼 0부터 다시 시작했습니다.
처음에는 모델 탓만 했습니다. 지나고 보니 모델 자체가 진짜 문제는 아니었습니다. 적어도 내가 생각했던 방식으로는요. 그저 내가 한 가지 사실을 깨닫지 못했던 것뿐이었고, 그 깨달음은 이후 내가 적용한 모든 해결책을 관통하는 핵심이 되었습니다. 바로 "나는 그동안 범죄 현장만 찾고 있었다. 정작 찾아야 했던 것은 '범죄 자체'였다"는 점입니다.
원고의 사례·수치는 작성자의 서술입니다. 소스코드는 설명용 예시이며 실행 가능한 전체 프로젝트는 아닙니다.
01 · 아무도 경고해주지 않은 형태
나는 모든 작업을 하나의 일직선으로 실행하고 있었습니다. 1단계, 2단계, 3단계... 각 단계는 앞 단계가 완료될 필요가 전혀 없는 상황에서조차 앞 단계가 끝나기만을 점잖게 기다리고 있었습니다.
이 사실은 상관없는 테스트를 하다가 발견했습니다. "이 피드에서 상위 3개 헤드라인을 가져온 다음, 내 도메인의 SSL 증명서가 이번 달에 만료되는지 확인하라"는 작업을 작성했을 때였습니다. 이 두 작업은 서로 아무런 관련이 없었지만, 단지 내가 그 순서대로 타이핑했기 때문에 연쇄적으로 묶여 있었습니다. 인증서 확인 기능은 단 하나의 헤드라인도 열어보지 않은 채, 존재하지도 않는 의존성을 기다리며 가만히 서 있었습니다.
그래서 나는 내 워크플로우 안의 모든 "~하고 나서(and then)" 문장에 한 가지 질문을 던지기 시작했습니다. "이 단계가 정말로 앞 단계의 결과물을 필요로 하는가?" '예'라면 기다림이 타당하므로 순서를 유지합니다. '아니오'라면, 단지 내가 문장을 그렇게 썼다는 이유만으로 존재했던 대기열 때문에 시간을 허비하고 있었던 것입니다.
내가 만든 거의 모든 워크플로우에서 이런 가짜 대기 시간을 2~3개씩 발견했습니다. 모두 공으로 버려진 시간이었습니다. 실수는 단계 자체에 있었던 것이 아니라, 내가 그 단계를 둘러싸고 그어놓은 '선'에 있었습니다.
02 · 내가 실제로 보고 있던 것
워크플로우를 그저 실행하는 대신 직접 그림으로 그려보았을 때, 내 일직선 구조가 이미 기술적으로는 하나의 그래프(Graph)라는 것을 깨달았습니다. 단지 최악의 형태였을 뿐입니다. 모든 단계가 정확히 하나의 입력과 하나의 출력만 가지는 단일 사슬 형태였죠. 실행은 잘 되었지만, 중복성(Redundancy)이 전혀 없었습니다. 3단계가 멈추면 4단계는 절대 실행되지 않고, 1단계가 이미 찾아낸 결과는 갇혀서 멍하게 서 있게 됩니다.
이 구조는 두 개의 실체적인 요소로 나뉘며, 그 개념을 이해한 후에는 더 이상 복잡한 어휘가 필요하지 않았습니다. 노드(Node)는 실제 작업을 수행하는 주체로, 하나의 제한된 작업, 하나의 에이전트, 하나의 입력, 하나의 출력을 가집니다. 노드가 두 가지 일을 하는 순간, 병렬화할 수 없고, 단독으로 검증할 수 없으며, 고장 났을 때 디버깅할 수 없습니다. 엣지(Edge)는 한 노드에서 다음 노드로 실제 데이터가 건너갔다는 증거일 뿐, 그 이상도 이하도 아닙니다.
노드는 모델이 내가 파싱할 수 있는 무언가를 반환해주길 기도하는 대신, 스키마(Schema)를 통해 고정된 입출력 형태의 계약(Contract)을 부여한 순간부터 비로소 쓸만해졌습니다.
소스코드 · JavaScript
const ITEM = {
type: 'object',
properties: {
title: { type: 'string' },
url: { type: 'string' },
impact: { type: 'string', enum: ['high', 'medium', 'low'] },
},
required: ['title', 'url', 'impact'],
};
const result = await agent(source.prompt, {
label: `research:${source.key}`,
schema: ITEM,
agentType: 'general-purpose',
});이전에는 자유 형식 텍스트를 읽고 다음 단계에서 쓸 수 있을지 추측해야 했지만, 이후에는 완벽히 신뢰할 수 있는 데이터 형태를 갖게 되었습니다.
03 · 모델 성능과 무관한 첫 번째 속도 향상
나는 40여 단계가 일렬로 연결된 실제 워크플로우 중 하나를 다시 그렸습니다. 모든 화살표를 지나며 가짜 엣지 질문을 던졌습니다. 대부분은 실제로 순차적이어야 했기에 살아남았지만, 10여 개 정도는 그렇지 않았습니다. 이유 없이 기다리고 있던 작업들이었습니다.
그 가짜 엣지들을 잘라내고 독립적인 조각들이 동시에 실행되도록 만들자, 동일한 40단계 작업이 전체 시간의 합이 아니라 가장 느린 실제 의존성 사슬의 속도로 완료되었습니다. 더 똑똑한 모델을 쓴 것도, 더 나은 프롬프트를 쓴 것도 아니었습니다. 오직 내가 그린 구조의 형태 덕분이었습니다.
지속적으로 나타난 이 패턴에는 기억할 만한 이름이 있습니다: Fan out, reduce, synthesize (확산, 축소, 종합).
소스코드 · JavaScript
// FAN OUT — independent angles, all at once
const raw = await parallel(
angles.map(a => () => agent({
task: `research: ${a}. every claim needs a source url and date.`,
schema: Finding,
model: 'cheap',
}))
);
// REDUCE — plain code, zero model tokens
const findings = dedupeBySource(raw.flat().filter(Boolean));
// VERIFY — a fresh skeptic per finding
const survivors = await parallel(
findings.map(f => () => agent({
task: 'try to disprove this. return keep or drop and why.',
input: f,
freshContext: true,
model: 'strong',
}))
).then(v => findings.filter((_, i) => v[i].verdict === 'keep'));
// SYNTHESIZE — one final agent writes the answer
return agent({
task: 'one report, ranked by confidence, sources attached.',
input: survivors,
model: 'strong',
});축소(Reduce) 단계는 단순한 자바스크립트입니다. 평탄화(Flatten), 중복 제거(Dedupe), 필터링(Filter) - 확정적이고, 즉각적이며, 비용이 들지 않습니다. 그동안 나는 자바스크립트 Set 객체 하나로 공짜로 할 수 있는 배관 작업을 위해 모델에 돈을 지불하고 있었습니다. 또 다른 범죄 현장, 그리고 그 위에 서 있는 또 다른 에이전트. 범죄 자체는 내가 귀찮아서 작성하지 않았던 코드 한 줄이었습니다.

04 · 나를 실제로 겁먹게 만든 실수
여기서부터는 속도의 문제가 아니라 신뢰의 문제로 바뀌었습니다.
나는 모든 작업자가 다음으로 넘어가기 전에 스스로의 결과물을 검증하는 팬아웃(Fan-out) 구조를 만들었습니다. 잘 작동하는 것처럼 보였고 숫자도 깔끔했습니다. 그러다 내 속을 뒤집어 놓는 논문을 하나 읽었습니다. 모델은 우연의 수준을 훨씬 뛰어넘는 확률로 자신이 쓴 글을 알아채며, 그 인식만으로도 자신이 쓴 텍스트를 선호하는 방향으로 눈에 띄게 기울어진다는 내용이었습니다 (Panickssery et al., NeurIPS 2024). 또 다른 연구는 그 결과가 어디로 이어지는지 정확한 수치로 제시했습니다. 모델에게 자신들의 답변이 포함된 세트를 평가하는 심사위원 역할을 맡겼을 때, GPT-4는 자신의 결과물에 10%, Claude는 25% 더 높은 승률을 부여하며 편애했습니다 (Zheng et al., NeurIPS 2023).
나는 일을 수행한 바로 그 에이전트에게 평가까지 맡기고 있었던 것입니다. 에이전트가 나에게 거짓말을 한 것은 아니었습니다. 그저 자신의 맹점을 진심으로 보지 못했을 뿐입니다. 왜냐하면 그 맹점 자체가 채점을 하고 있었기 때문입니다.
그래서 이를 분리했습니다. 엣지에 별도의 노드를 두고 단 하나의 역할만 부여했습니다: 결과물이 하류로 이동하기 전에 어떻게든 기각(Kill)해 볼 것. 살아남으면 통과하고, 그러지 못하면 그 자리에서 폐기됩니다.
처음에 내가 틀렸던 세부 사항이 있습니다. 검증자에게는 완전히 깨끗한 문맥(Clean Context)이 필요하다는 점입니다. 나는 작업자가 사용했던 것과 동일한 대화를 검증자에게 전달했습니다. 당연히 모든 것에 동의했죠. 이미 추론 과정을 보았고 그것을 받아들일 준비가 되어 있었으니까요. 작업자와 문맥을 공유하는 검증자는 아무것도 검증하지 않습니다. 그저 다른 폰트로 스스로에게 고개를 끄덕이고 있을 뿐입니다.
소스코드 · JavaScript
const verdicts = await parallel(
['correctness', 'recency', 'source-validity'].map(lens => () =>
agent({
task: `judge this finding via ${lens}. return keep or drop.`,
input: finding,
freshContext: true,
schema: { verdict: { enum: ['keep', 'drop'] } },
})
)
);신선한 문맥, 세 가지 관점, 그리고 과반수의 동의. 이것이 실제로 의미 있는 문제를 잡아낸 유일한 버전이었습니다. 범죄 현장은 잘못된 점수였고, 범죄는 동일한 손이 에세이 작성과 채점 펜을 모두 쥐게 만든 것이었습니다.
05 · 그것을 고친 후에도 살아남는 함정
나는 이제 다 끝났다고 생각했습니다. 그러다 쌍으로 묶인 검증자, 즉 다른 감사 노드를 검증하는 감사 노드라는 더 정교한 시스템을 구축했습니다. 그리고 그 감사가 완전히 동일한 하위 출처에서 나온 보고서를 바탕으로 숫자를 대조하고 있다는 사실을 발견했습니다.
모든 것이 다른 모든 것과 일치했습니다. 그 무엇도 실제로 검증되지 않았습니다. 내 원래의 단일 에이전트 루프가 실패했던 것과 똑같이 실패할 구조였습니다. 단지 더 나중에, 더 많은 비용을 들여, 밑으로 내려가는 길에 초록색 체크 표시를 더 많이 켜가면서 말이죠.
나에게 필요했던 것은 앵커(Anchor), 즉 진심으로 반박할 수 없는 무언가였습니다. 모델이 통과할 것이라고 말하는 것이 아니라 실제로 실행되어 통과한 테스트, 실제 계좌에 도달한 숫자, 그리고 최적화 도구가 성공한 것처럼 보이기 위해 은밀히 완화하고 싶어할 만한 항목들이기에 일부로 완전히 동결시켜 버린 몇 가지 규칙들이었습니다. 그래프는 그 내부에서 움직이기를 거부하는 부품들의 존재감만큼만 정직합니다.
06 · 이번에는 의도적으로 깨뜨린 부분
작업을 더 넓게 확산(Fan-out)시키자, 모두가 겪는 실패에 직면했습니다. 여러 에이전트가 동시에 같은 파일에 글을 쓰며 서로의 작업을 조용히 덮어쓰는 문제였습니다. 같은 실행에서 나온 두 개의 "완료된" 출력물이 서로 충돌했기 때문에 겨우 잡아낼 수 있었습니다.
해결책은 모든 작업자의 지시 사항에 고정해 둔 한 문장이었습니다:
Never git stash. Never git reset. No git command except committing a specific file.
그리고 에이전트 함대를 Git 워크트리(Worktree)로 분할하여 격리된 사본을 만들고, 각 그룹이 자신의 파일만 건드리도록 했습니다. 64개의 개별 체크아웃 대신 16개의 에이전트로 구성된 4개의 워크트리를 사용하자, 더 이상 그 누구도 타인의 파일을 밟지 않았습니다.
넓게 확산시키면서 발생한 문제는 그것이 끝이 아니었습니다. 다음 문제는 에러처럼 보이지 않았기 때문에 눈치채는 데 더 오랜 시간이 걸렸습니다. 약 1,000개의 파일에 걸쳐 스위프를 실행하고, 1,000개의 개별 결과물 전체를 하나의 최종 종합(Synthesis) 단계에 밀어 넣었습니다. 그리고 에러 메시지도 없이, 그저 실행 범위가 넓어질수록 답변이 점점 더 모호해지고 악화되는 모습을 지켜보았습니다. 종합 단계가 데이터를 제대로 다뤄보기도 전에 문맥 창(Context Window) 한계를 한참 지나쳐 버렸던 것입니다.
해결책은 데이터를 한 단계에 모두 쏟아붓는 대신 팬인(Fan-in) 과정을 계층화하는 것이었습니다. 원본 결과를 배치로 묶고, 각 배치를 개별적으로 요약한 다음, 원본 더미가 아닌 요약본으로부터 최종 종합을 수행하는 방식입니다:
소스코드 · JavaScript
const batches = chunk(results, 40);
const summaries = await parallel(
batches.map(b => () => agent({ task: 'summarize this batch', input: b }))
);
return agent({ task: 'write the answer from the summaries', input: summaries });이제 최종 단계는 1,000개의 원본 항목 대신 25개의 요약본을 읽으며, 작업 범위가 넓어져도 답변이 저하되지 않습니다. 범죄 현장은 지적할 에러조차 없이 조용히 품질이 떨어진 답변이었고, 범죄는 그 많은 양을 담아내도록 설계된 적이 없는 단 하나의 노드에 모든 것을 품으라고 요구한 것이었습니다.
07 · 만들지 않을 뻔했던 라우터
한동안 모든 결과물은 필요 여부와 상관없이 완전히 동일한 전체 감사를 거쳤습니다. 한 줄짜리 오타 수정조차 결제 로직을 건드리는 변경 사항과 동일한 3중 관점 검증을 거쳤습니다. 왜 그런지 말로 표현하기 전에 계산서에서부터 체감될 정도로 낭비였습니다.
해결책은 라우터(Router) 노드였습니다. 작업의 남은 부분이 어떤 경로를 거칠지 결정하는 하나의 작은 분류 호출이었습니다:
소스코드 · JavaScript
const { severity } = await agent(
`Classify this diff's risk:\n${diff}`,
{ schema: { properties: { severity: { enum: ['low', 'high'] } } } }
);
let review;
if (severity === 'high') {
review = await parallel(FILES.map(f => () => agent(`Audit ${f}`)));
} else {
review = await agent(`Quick review of ${diff}`);
}분류 자체는 여전히 모델에서 나옵니다. 하지만 라우팅은 그렇지 않습니다. 심각도가 결정되면 타게 되는 분기점은 일반 코드이며, 동일한 입력에 대해 매번 동일한 분기를 탑니다. 에이전트가 수행했어야 할 감사를 슬그머니 건너뛰는 불상사가 더 이상 발생하지 않았습니다. 그런 건너뛰기가 가능하려면 그래프에 직접 코드로 작성되어 있어야 하는데, 그렇게 하지 않았기 때문입니다.
그러다 또 다른 것을 깨달았습니다. 나는 단순한 분류 호출이든 실제 판단 호출이든 상관없이, 가장 귀한 최고가 모델을 모든 노드에서 동일한 요율로 지불하며 사용하고 있었습니다. 그것을 눈으로 확인한 순간 분리하는 작업은 너무나 당연하게 느껴졌습니다:
소스코드 · Shell
claude -p "..." --model sonnet // repetitive, high-volume nodes
claude -p "..." --model opus // the reviewer, and anything writing rules여기서의 범죄 현장은 타당한 이유도 없이 비싸게 청구된 영수증이었고, 범죄는 관성 때문에 모든 노드에 걸쳐 단 한 번도 변경하지 않았던 설정을 그대로 방치한 것이었습니다.
검증 작업도 모델과 동일한 문제를 가지고 있었고, 이는 이미 비용을 한번 치르고 나서야 눈에 보였습니다. 수 초 만에 반환되는 빠른 검증은 작업 대상 바로 옆, 루프 내부에 위치해야 합니다. 몇 분이 걸리는 느린 검증은 루프 내부에 있으면 안 됩니다. 단 한 번이면 충분한 일을 매 노드마다 실행하여 느린 작업을 수백 번 실행하는 꼴이 되기 때문입니다:
소스코드 · Shell
# fast check, inline
claude -p "..." && ./scripts/judge.sh "output/$name" || echo "$name" >> logs/failed.txt
# slow check, once, at the end, grouped into categories instead of a flat list
./scripts/expensive_check.sh > logs/errors.txt
sort logs/errors.txt | uniq -c | sort -rn | head -20마지막 줄은 처음 사용할 때 내 예상보다 훨씬 더 중요했습니다. 에러를 단순 나열식 목록으로 읽는 대신 카테고리별로 그룹화하자, 400개의 개별 문제처럼 보였던 것이 단 6개의 실제 원인으로 추려졌습니다. 나는 400개의 범죄 현장을 수습하고 있었던 게 아니었습니다. 단 6개의 범죄를 해결하고 있었던 것이며, 에러 개수라는 숫자가 그동안 나에게 거짓말을 하고 있었던 것입니다.
08 · 작업의 규모조차 알지 못했을 때
어떤 작업은 사전 계획에 저항합니다. 버그 하나를 찾으면 3개가 더 발견되는 버그 탐색 작업 같은 것입니다. 직접 들어가 보기 전까지는 전체 양을 알 수 없으며, 이런 작업에는 제어된 루프, 즉 이전 노드로 돌아가는 제어된 엣지가 필요합니다. 순환 구조는 기본적으로 위험합니다. 수렴하지 않는 순환은 내가 보지 않는 동안 전체 예산을 조용히 탕진하는 무한 루프가 됩니다.
실제로 수렴하는 버전은 '마를 때까지 반복(Loop-until-dry)' 방식입니다. 몇 라운드 연속으로 새로운 것이 나오지 않을 때까지 탐색자를 계속 생성하고 멈추는 것입니다. 처음에 이것을 만들었을 때 잘못 만들었는데, 그 실수가 너무 미묘해서 왜 루프가 마르지 않는지 원인을 파악하는 데 따박 하루가 걸렸습니다.
소스코드 · JavaScript
const seen = new Set();
const confirmed = [];
let dry = 0;
while (dry < 2) {
const found = (await parallel(
FINDERS.map(f => () => agent(f.prompt, { schema: BUGS }))
)).filter(Boolean).flatMap(r => r.bugs);
const fresh = found.filter(b => !seen.has(key(b)));
fresh.forEach(b => seen.add(key(b)));
if (!fresh.length) { dry++; continue; }
dry = 0;
const verified = await parallel(fresh.map(b => () =>
agent(`is this real: ${b.desc}`, { schema: VERDICT })
));
confirmed.push(...verified.filter(v => v.real).map((v, i) => fresh[i]));
}처음에 내가 틀렸던 부분이 바로 이것입니다. 탐색된 항목이 내 두 번째 건조 상태 확인을 통과한 '후'에야 seen.add()를 호출하고 있었지, 발견된 '즉시' 호출하지 않았습니다. 그 바람에 탐색자가 동일한 결과를 두 번 연속으로 가져와도 확인된 것으로 표시되기 전이었으므로 매번 새로운 것으로 간주되었습니다. 건조 상태 카운터는 아무런 이유 없이 계속 리셋되었고, 루프는 동일한 결과 몇 개가 매 라운드마다 새것으로 위장해 순환하는 것도 모른 채 원래 실행되어야 할 시간보다 3배나 더 오래 돌아갔습니다.
해결책은 코드 한 줄을 올바른 위치로 옮기는 것이었습니다: 루프가 이제껏 본 모든 것과 대조하여 검증을 받기 전이든 후든 발견된 즉시 중복을 제거하는 것입니다. 이제 나는 이 구조를 연속 건조 라운드, 엄격한 토큰 예산, 최대 반복 횟수라는 3가지 중단 조건을 중첩하여 배포합니다. 어느 것 하나만 단독으로 사용하는 것은 이미 내가 한번 빠져본 적이 있는 함정이기 때문입니다.
범죄 현장은 동일한 버그가 두 번 걸어 들어오는 것이었습니다. 범죄는 한 번도 문을 걸어 잠그지 않은 한 줄의 코드였습니다.
09 · 주말을 날리지 않게 해준 수학
이 모든 것을 확장하기 전에, 나는 그동안 미뤄두었던 계산을 직접 해보았습니다. 16개의 에이전트를 병렬로 돌린다고 해서 16배의 속도를 얻을 수 있는 것은 아닙니다. 암달의 법칙(Amdahl's Law)은 실제 수치를 보여주며, 그 결과는 겸허해지게 만듭니다:
계산식 · Amdahl’s Law
S = 1 / ((1 − p) + p/N)
p = the fraction of the work that's genuinely independent
N = number of agents
p = 0.95, N = 16 → ×9.14 (not ×16)
p = 0.70, N = 16 → ×2.9195%가 독립적인 작업이라 해도, 16개의 에이전트가 가져다주는 속도 향상은 16배가 아니라 약 9배에 불과합니다. 잘못된 숫자로 예산을 잡고 있었던 셈이며, 이는 배움으로 다가오기 전에 당혹스러운 영수증으로 먼저 나타났을 것입니다. 직렬 구간, 최종 병합, 검증 단계 등 모든 실제 엣지들이 확산(Fan-out)을 통해 얻을 수 있는 이점의 한계를 결정짓습니다. 맨 처음 고안했던 가짜 엣지 테스트가 알고 보니 호되게 당해가며 배우기 전에 이 $p$ 값을 추정하는 방법이었습니다.

10 · 이제는 신뢰할 수 있는 빌드: 디스크 우선 구조
1시간이 넘어가는 작업에 대해 내가 실제로 실행하는 버전은 대화 창 안에 존재하지 않습니다. 디스크(Disk) 상에 존재합니다. 실행 3시간 만에 프로세스가 튕기면 처음부터 완전히 다시 시작해야 했던 경험 때문입니다.
나는 실제 작업을 건드리기 전에 심사 프로그램(Judge)부터 만들었습니다. 내가 출력을 읽는 것이 아니라 스크립트로 만들었죠:
소스코드 · Shell
#!/bin/bash
# judge.sh
FILE=$1
[ -s "$FILE" ] || { echo "FAIL: empty"; exit 1; }
grep -q "required_section" "$FILE" || { echo "FAIL: missing section"; exit 1; }
echo "PASS"; exit 0그리고 그 스크립트를 신뢰하기 전에 의도적으로 고장 내 보았습니다:
소스코드 · Shell
cp output/known_good.md /tmp/broken.md
sed -i 's/required_section//' /tmp/broken.md
./scripts/judge.sh /tmp/broken.md # must print FAIL만약 의도적으로 고장 낸 파일이 통과했다면 그 심사 스크립트는 눈이 먼 것이고, 그 이후의 모든 초록색 합격 결과는 아무런 의미도 갖지 못했을 것입니다. 그다음은 규칙서(Rulebook)였습니다. 소리 내어 고심했던 모든 모호한 점들을 규칙서의 문장으로 만들었고, 단 하나의 규칙을 엄격하게 지켰습니다: 규칙서에 적혀 있어야 했던 내용에 맞추려고 출력을 절대로 손으로 직접 수정하지 말 것. 그 짓을 단 한 번이라도 하는 순간, 나는 두 개의 진실을 갖게 되며, 그중 하나는 내 머릿속에만 존재하게 될 테니까요.
큐(Queue)는 완전히 디스크 상에 존재합니다:
소스코드 · Shell
#!/bin/bash
# queue.sh — rebuilds from the filesystem every run
for f in source/*.md; do
name=$(basename "$f")
[ -f "output/$name" ] || echo "$f"
done프로세스가 60% 진행되었을 때 강제 종료하고 재시작해도, 파일시스템이 기억하고 있기 때문에 60% 지점에서 다시 시작합니다. 다시 구축해야 할 문맥 창도 없고, "리가 어디까지 했더라" 하고 되물을 필요도 없습니다.
검토는 문맥을 절대로 공유하지 않는 완전히 독립된 두 개의 패스로 실행됩니다:
소스코드 · Shell
#!/bin/bash
# review.sh
FILE=$1
for n in 1 2; do
claude -p "Read rules/rulebook.md. Review $FILE against those rules only.
For each problem output: RULE: | ISSUE: .
If nothing violates a rule, output PASS." > logs/review_$n.txt
done모든 지적 사항에 대해 규칙 인용을 강제하는 것은 예상보다 훨씬 더 중요했습니다. 모호한 불평이 수정 가능한 명확한 문장으로 바뀝니다. 동일한 규칙이 3개의 파일에 걸쳐 반복 인용될 때, 그것은 3개의 문제가 아니라 하나의 잘못 작성된 규칙을 의미합니다. 그 한 줄을 다시 쓰고 배치를 재실행하면 됩니다.
3개의 범죄 현장, 3개 모두에 남겨진 동일한 필체. 범죄는 파일 안에 있었던 적이 없습니다. 규칙서 안에 적히지 않은 채 빠져 있던 단 한 문장이었을 뿐입니다.
11 · 실제 들어간 비용과 얻은 것
이 모든 것은 공짜가 아닙니다. 이 메커니즘을 실제 현장에 적용한 한 팀은 약 53만 5천 줄에 달하는 프로덕션 자바스크립트 런타임을 11일 만에 다른 언어의 100만 줄이 넘는 코드로 다시 재작성했습니다. 손으로 했다면 1년 가까이 걸릴 일입니다. 약 50개의 워크플로우, 최대 64개의 에이전트 동시 가동, 약 $165,000의 사용 비용이 들었습니다. 사람이 직접 설계하고, 감시하고, 에이전트가 놓친 것을 잡아냈습니다. 그만큼의 AI 생성 코드가 과연 안전하게 검토될 수 있느냐에 대해 공개적인 비판을 받기도 했습니다.
하이라이트 모음집이 아닌, 이것이 솔직한 한계선입니다. 나는 내 첫 실행 한도를 20개 항목으로 제한하고 두 번째 실행을 건드리기 전에 사용량 보고서부터 읽었습니다. 매번 던지는 3가지 질문:
- 팬아웃이 단일 에이전트는 진심으로 찾아낼 수 없었을 무언가를 찾아냈는가?
- 검증자가 작업자가 놓친 것을 잡아냈는가?
- 결과가 들어간 비용을 정당화하는가?
3가지 모두에 '예'라고 답할 때만 제한을 2배로 늘렸습니다. 20개 항목에서 제값을 해내는 그래프는 200개에서도 제값을 해냅니다. 그렇지 못한 그래프는 2,000개에서도 제값을 못 할 것이며, 단지 그것을 증명하는 데 100배 많은 돈이 들 뿐입니다.
12 · 완전히 건너뛸 부분들
나는 여전히 규모가 작은 작업에는 이 모든 방식을 쓰지 않습니다. 단 하나의 함수, 단 하나의 격리된 수정은 조정 오버헤드가 단일 에이전트 혼자 일하는 것보다 더 많은 비용을 발생시킵니다. 모든 단계를 개인적으로 직접 승인하고 싶을 때도 건너뜁니다. 그래프의 존재 목적 자체가 내가 감시하지 않고도 넓게 돌아가는 데 있기 때문입니다.
내가 무엇을 찾고 있는지조차 모르는 탐색적인 작업에서도 건너뜁니다. 내가 조종할 수 있는 단일 에이전트는 내가 사전에 그려두지 못했을 경로를 찾아내며, 그래프의 형태를 너무 일찍 고정하는 것은 나의 잘못된 첫 번째 추측을 고정해 버릴 뿐입니다.
신호는 항상 동일한 가짜 엣지 테스트입니다. 작업에서 실제 독립성을 전혀 찾을 수 없다면, 만들 가치가 있는 그래프는 없습니다. 그것은 그저 루프일 뿐이며, 그런 종류의 작업에는 루프가 정확히 맞습니다.
13 · 실제로 생각을 바꾼 부분
이전에는 에이전트가 무언가를 잘못했을 때 해결책이 들어가서 손으로 직접 출력을 고치는 것이라고 생각했습니다. 3개의 파일에 동일한 실수가 있으면 3개의 파일을 고쳤죠.
그것은 거꾸로 된 방식이었고, 그것을 깨닫는 데 부끄러울 정도로 오랜 시간이 걸렸습니다. 나는 범죄 현장을 하나씩 수습하면서, 누가 계속해서 동일한 범죄를 저지르고 있는지 단 한 번도 묻지 않고 있었습니다. 그 일이 일어나도록 방치한 단 한 문장을 고치고 배치를 다시 생성하면, 범죄 자체는 다음에 숨을 곳이 사라집니다. 이전에는 범죄 현장만 숨을 곳이 있었을 뿐입니다.
작업이 진심으로 순차적일 때는 여전히 일직선으로 큐를 세웁니다.하지만 그렇지 않은 모든 작업에 대해, 나는 에이전트에게 연속해서 더 많은 단계를 수행해 달라고 요청하는 것을 그만두었습니다. 어디가 진짜 분기점(Split)이고 어디가 진짜 병합점(Merge)인지 묻기 시작했습니다. 그 질문이야말로 확장성을 가져다주는 질문입니다. 일직선은 결코 한계선이 아니었습니다. 단지 내가 입력하는 방식과 일치하는 구조였기에 가장 먼저 손이 갔던 모양이었을 뿐입니다. 일단 그것을 깨닫고 나자, 그 습관조차 더 이상 모델 탓으로 돌릴 수 없게 되었습니다.
LangTailor