의존성 없이 약 200줄로 구현한 재개 가능한 Human-in-the-loop AI 에이전트
요약
의존성 없이 약 200줄의 코드로 구현된 Human-in-the-loop AI 에이전트 라이브러리 yieldagent를 소개합니다. 비동기 제너레이터를 활용하여 에이전트 루프를 제어하고, LLM 없이도 결정론적인 테스트가 가능한 구조를 가집니다.
핵심 포인트
- 의존성 없는 가벼운 200줄 규모의 에이전트 루프 구현
- 비동기 제너레이터를 통한 Human-in-the-loop 제어 흐름 제공
- LLM 호출 없이도 가능한 결정론적 유닛 테스트 지원
- 프레임워크 내부 로직을 숨기지 않는 투명한 관찰 가능성
대부분의 "AI 에이전트 (AI agent)" 라이브러리는 두 가지 범주 중 하나에 속합니다. 오후 내내 설정에 매달려야 하는 거대한 프레임워크이거나, 실제 운영 환경에서 정말로 필요한 단 하나의 기능, 즉 에이전트가 되돌릴 수 없는 행동을 하기 전에 멈춰서 사람에게 물어볼 수 있는 능력이 빠져 있는 아주 작은 장난감 같은 라이브러리입니다.
저는 그 중간을 원했습니다. 그래서 yieldagent를 작성했습니다. 이는 처음부터 끝까지 읽을 수 있는 작은 에이전트 루프(agent loop)로, Human-in-the-loop 방식의 일시 중지/재개 기능이 내장되어 있으며 런타임 의존성(runtime dependencies)이 없습니다. 이 포스트에서는 이것이 어떻게 작동하는지, 그리고 왜 이런 방식으로 구축되었는지 설명합니다.
에이전트 루프 (agent loop)의 실제 정체
브랜딩을 걷어내면 "에이전트 (agent)"는 하나의 루프입니다:
- 대화 내용과 에이전트가 호출할 수 있도록 허용된 도구(tools)를 모델에 보냅니다.
- 모델이 도구 호출을 요청하면, 이를 실행하고 결과를 추가합니다.
- 모델이 도구를 요청하지 않고 답변할 때까지 반복합니다.
그게 전부입니다. 모델이 런타임(runtime)에 제어 흐름(control flow)을 결정하며, 여러분의 역할은 도구를 실행하고 그 결과를 다시 전달하는 것입니다. 핵심적인 부분을 가볍게 다듬으면 다음과 같습니다:
for (let step = 0; step < maxSteps; step++) {
const reply = await call(messages, toolSpecs);
messages.push(reply);
...
라이브러리의 나머지 모든 기능은 이 루프를 관찰 가능(observable)하고, 테스트 가능(testable)하며, 실제 환경에서 안전하게 실행할 수 있도록 만드는 데 기여합니다.
왜 비동기 제너레이터 (async generator)인가
yield에 주목하세요. 이 루프는 비동기 제너레이터(async generator)이므로 호출자가 이를 제어합니다:
for await (const step of agent({ call, tools, messages })) {
if (step.type === "tool-start") console.log("->", step.tool, step.args);
if (step.type === "final") console.log(step.text);
...
모든 단계(각 도구 호출, 각 결과, 그리고 최종 답변)가 일반 객체(plain object)로서 여러분에게 전달됩니다. 프레임워크 내부에 숨겨진 것은 아무것도 없습니다. 이를 로그로 남기거나, 렌더링하거나, 테스트에서 검증(assert)할 수 있습니다. 이는 가장 멋진 부수 효과(side effect)로 이어집니다.
LLM 없이 테스트하기
모델 호출은 단순히 (messages, tools) => Promise<Message> 형태의 함수일 뿐입니다. 테스트 시에는 미리 준비된 답변(canned replies)을 반환하는 함수를 전달하면 됩니다. API 키도 필요 없고, 네트워크도 필요 없으며, 결과는 결정론적(deterministic)입니다:
const replies = [
{ role: "assistant", content: null, tool_calls: [{ id: "1", function: { name: "getWeather", arguments: '{"city":"Delhi"}' } }] },
{ role: "assistant", content: "It's 31°C.", tool_calls: [] },
...
라이브러리 전체가 이런 방식으로 테스트됩니다. 반복적인 작업(iteration)을 수행할 때 LLM 없이도 유닛 테스트(unit-test)를 할 수 있는 에이전트 로직은 매우 큰 가치를 지닙니다.
실제로 찾기 어려운 기능: 일시 중지 및 재개 (pause and resume)
실제 에이전트는 이메일을 보내거나, 돈을 쓰거나, 파일을 삭제하는 등 위험한 행동을 수행합니다. 이러한 일이 발생하기 _전_에 루프 내의 인간(human in the loop)이 개입하기를 원하며, 종종 실행을 일시 중지했다가 나중에 다른 요청에서 혹은 재시작 후에 다시 계속하기를 원합니다.
yieldagent는 approve 콜백을 통해 이를 수행합니다. 도구(tool)에 대해 false를 반환하면 도구가 실행되기 전에 루프가 중단되며, 직렬화 가능한(serializable) resumeState를 반환합니다:
const cfg = {
call, tools,
messages: [{ role: "user", content: "Email the Delhi weather to my boss" }],
...
resumeState는 단순한 데이터이기 때문에, 이를 데이터베이스나 작업 큐(job queue)에 기록해 두었다가, 완전히 다른 곳에서 인간이 "승인(approve)"을 클릭할 때까지 기다린 후, 중단되었던 지점부터 다시 시작할 수 있습니다:
import { resume } from "yieldagent";
for await (const step of resume(cfg, paused)) {
if (step.type === "final") console.log(step.text);
...
이 부분은 대부분의 최소형 에이전트들이 생략하는 부분이며, 대부분의 대형 프레임워크들은 이를 하나의 거대한 서브시스템(subsystem)으로 만들어 버립니다. 이 기능을 작고 명시적으로 유지하는 것이 제가 이 라이브러리를 작성한 주요 이유였습니다.
기본적으로 제공자 불가지론적 (Provider-agnostic)
코어(core)는 특정 제공자(provider)에 대해 아무것도 알지 못합니다. 포함된 어댑터(adapter)는 OpenAI의 /chat/completions 형식을 따르는 모든 것과 통신할 수 있습니다. 즉, OpenAI, Anthropic의 호환 엔드포인트, Groq, Together, 또는 Ollama나 vLLM을 통한 로컬 모델과도 통신이 가능합니다:
const call = openaiCompatible({
baseURL: "http://localhost:11434/v1", // Ollama
apiKey: "ollama",
...
또는 어댑터(adapter)를 건너뛰고 직접 call을 작성할 수도 있으며, 이는 약 10여 줄 정도의 분량입니다.
몇 가지 유용한 추가 기능
- 스트리밍 (Streaming):
call대신stream을 전달하면 모델이 텍스트를 생성함에 따라token단계(steps)를 얻을 수 있습니다. 도구(Tools) 사용과 일시 중지/재개(pause/resume) 기능도 여전히 작동합니다. - Zod 도구 (Zod tools): 선택 사항인
yieldagent/zod엔트리를 사용하면 Zod 스키마로부터 JSON 스키마를 도출하고 모델의 인자(arguments)를 검증하며, 오류를 다시 피드백하여 모델이 스스로 수정할 수 있도록 합니다. - 취소 (Cancellation): 타임아웃이나 사용자 취소 시 실행을 중단하려면
AbortSignal을 전달하세요.
사용하지 말아야 할 때
스트리밍 UI 헬퍼(helpers), 방대한 사전 구축 도구 생태계, 또는 즉시 사용 가능한 멀티 에이전트 오케스트레이션 (multi-agent orchestration)이 필요하다면 Vercel AI SDK 또는 LangGraph를 사용하세요. yieldagent는 프레임워크를 채택하는 대신, 몇 분 안에 읽고 이해할 수 있는 루프(loop)를 직접 소유하고 싶을 때를 위한 것입니다. 만약 이 라이브러리가 너무 작게 느껴진다면, 당신은 무엇을 대체해야 할지 정확히 알게 될 것입니다.
사용해 보기
npm install yieldagent
승인 흐름(approval flow)을 확인할 수 있는 브라우저 데모가 있습니다 (API 키 불필요):
https://rahul1368.github.io/yieldagent/
코드 및 문서: https://github.com/rahul1368/yieldagent
이 라이브러리로 무언가를 만드셨거나, 일시 중지/재개(pause/resume) API가 귀하의 사용 사례에서 제대로 작동하지 않는다면, 의견을 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기