All notes

에이전트는 안목 있는 while 루프다

프레임워크를 걷어내면 프로덕션 에이전트는 여든 줄 남짓이다. 우리 것은 구조를 한 번도 안 바꾸고 모델 4세대를 살아남았다.

3 분 읽기서울과 나트랑에서 씁니다

우리 일상 업무의 상당 부분은 AI 에이전트가 처리한다. 조사, 코드 수정, QA, 보고서 작성. 다들 여기에 오케스트레이션 프레임워크가 필요하다고 여긴다. 정작 필요한 건 루프 하나다.

let messages = [system, task];
for (let step = 0; step < MAX_STEPS; step++) {
  const res = await llm(messages, TOOLS);
  if (res.type === 'text') return res.text;        // the model decided it is done
  const tool = TOOLS[res.name];                     // no dynamic anything
  const result = await tool.run(res.args);          // sandboxed, permission-gated
  messages.push(res, clip(result, TOOL_BUDGET));    // clip. always.
}
return escalate(messages);                          // never loop forever

다음에 무엇을 할지는 모델이 정한다. 할지 말지와 얼마나 오래는 코드가 쥔다. 이 분담이 설계의 전부다.

루프가 가르쳐준 것

  • 도구 결과는 무조건 잘라라. 우리는 2,000 토큰 언저리에서 자른다. 전체 출력은 디스크로 보내고, 모델에는 앞부분과 뒷부분, 그리고 경로만 준다. 이 상한을 두기 전에는 장황한 빌드 로그 하나가 컨텍스트 윈도우를 통째로 잡아먹고 나머지 세션을 바보로 만들어 버리곤 했다.
  • 겹치지 않는 도구 몇 개가 영리한 도구 여러 개를 이긴다. 우리 에이전트는 엄격한 스키마를 가진 도구를 여섯에서 아홉 개 들고 다닌다. 기존 도구와 기능이 겹치는 편의성 도구를 더할 때마다 도구 선택 정확도가 떨어졌다. 모델은 딱 사람이 헷갈릴 자리에서 똑같이 갈팡질팡한다.
  • MAX_STEPS는 튜닝 손잡이가 아니라 안전장치다. 예산이 바닥나면 상태를 그대로 담은 기록이 사람에게 넘어간다. 영원히 돌 수 있는 에이전트는 언젠가 진짜로 그렇게 된다. 새벽 4시에, 레이트 리밋 걸린 API를 상대로.
  • 기록은 JSONL이고, JSONL은 다시 재생할 수 있다. 모든 단계는 로그에 덧붙고, 이 로그는 새 모델이나 고친 도구에 대고 다시 돌려볼 수 있다. 우리 디버깅의 절반은 어제의 기록을 오늘의 코드에 다시 재생해 보는 일이다.
  • 읽기는 자유, 쓰기는 관문을 거친다. 루프는 샌드박스 안에서 뭐든 읽을 수 있다. 배포, 삭제, 결제, 외부 발송은 루프를 벗어나 우리 크론 작업이 쓰는 것과 똑같은 기계적 관문으로 들어간다. 모델에게 조심하라고 당부하는 법이 없다.

모델을 네 번 갈아치우는 동안 루프는 바이트 하나 안 바뀌었다. 그 주변은 전부 좋아졌다. 좋은 추상화 경계란 이런 느낌이다. 빠르게 바뀌는 것이, 절대 바뀌면 안 되는 것으로부터 격리되어 있다.