Java에서 에이전트 워크플로우 (Agentic Workflows) 구축하기
요약
Java 환경에서 LLM을 활용한 에이전트 워크플로우를 구축하는 방법과 설계 원칙을 다룹니다. 단순 호출, 코드 기반 워크플로우, 에이전트 방식의 차이점을 설명하며, 에이전트 도입이 필요한 시점과 안전한 제어 장치 구축의 중요성을 강조합니다.
핵심 포인트
- 에이전트는 모델이 도구 호출과 순서를 스스로 결정하는 개방형 루프 구조임
- 단순 요약이나 고정된 시퀀스는 에이전트 대신 단일 호출이나 코드 오케스트레이션을 권장
- 작업이 다단계이며 순서를 사전에 알 수 없는 개방형 작업에만 에이전트 도입 고려
- Java에서 수동 루프 구현 또는 SDK의 도구 러너를 통한 에이전트 실행 가능
서론
"에이전트 (Agent)"는 LLM (Large Language Model)을 한 번 이상 호출하는 모든 프로그램을 지칭하는 단어가 되었으며, 이는 정확한 정의가 필요한 단어입니다. 이 포스트에서 사용하는 의미의 에이전트는 하나의 루프 (loop)입니다. 모델이 다음에 호출할 도구 (tool)를 결정하면, 여러분의 코드가 이를 실행하고, 그 결과가 다시 피드백되는 과정이 모델이 완료를 결정할 때까지 반복됩니다. 이는 단일 요청/응답 (request/response) 호출과는 진정으로 다른 (그리고 더 위험한) 형태입니다.
이 포스트는 Java에서 신뢰할 수 있는 LLM 애플리케이션 구축하기를 기반으로 합니다. 재시도 (retries), 타입화된 출력 (typed output), 그리고 평가 (evaluation)에 대해 그곳에서 언급된 모든 내용은 루프를 추가한 후에도 여전히 적용됩니다. 다만 이제는 _모든 반복 (iteration)_에 적용되며, 모델이 어떤 부작용 (side effects)을 트리거할지도 직접 선택하게 됩니다. 우리는 에이전트가 실제로 정당화되는 시점, 루프 자체 (수동 및 SDK 지원 방식), 그리고 모델에게 운전대를 맡기는 것을 방어 가능하게 만드는 안전 제어 장치들에 대해 다룰 것입니다.
에이전트를 구축해야 할 때 — 그리고 구축하지 말아야 할 때
작업이 진정으로 다단계(multi-step)이고 개방형(open-ended)일 때만 에이전트를 고려하십시오. 즉, 작업의 수와 순서를 사전에 알 수 없어 고정된 파이프라인 (pipeline)으로 표현할 수 없는 경우입니다. 에이전트처럼 느껴지는 대부분의 작업은 사실 더 단순하고 디버깅하기 쉬운 방법으로 처리하는 것이 더 낫습니다. 단계(ladder)가 존재하며, 작업이 충족되는 즉시 올라가는 것을 멈춰야 합니다:
- 단일 LLM 호출 (A single LLM call). 이 티켓을 분류하세요. 이 문서를 요약하세요. 하나의 프롬프트(prompt)가 입력되어 하나의 답변이 출력되는 것으로 문제가 해결된다면, 그것이 전체 시스템입니다.
- 코드로 오케스트레이션된 워크플로우 (A code-orchestrated workflow). LLM 호출과 결정론적 단계(deterministic steps)로 이루어진 고정된 시퀀스입니다. 모델을 호출하여 필드를 추출하고, 코드에서 이를 검증하며, 다시 모델을 호출하여 답장 초안을 작성합니다. 단계의 *순서(order)*는 사전에 알려져 있으며 모델의 머릿속이 아닌 Java 코드 내에 존재합니다.
- 에이전트 (An agent). 모델 스스로가 각 결과로부터 배운 내용을 바탕으로 어떤 도구(tool)를 호출할지, 몇 번 호출할지, 그리고 어떤 순서로 호출할지를 결정합니다. 이러한 개방성(open-endedness)이 핵심인 작업에만 에이전트를 사용하세요. 예를 들어, 검색이 몇 번 필요할지 미리 알 수 없는 연구 보조원이나, 마지막 명령의 출력 결과에 따라 반응해야 하는 디버깅 도우미 같은 작업이 이에 해당합니다.
3단계를 구축하기 전에, 해당 작업을 네 가지 기준으로 검토하십시오. 만약 하나라도
에이전트가 필요하다고 판단되면, 관련된 도구(tools)가 무엇이든 그 형태는 동일합니다. 사용 가능한 도구 목록과 함께 모델을 호출합니다. 만약 모델이 도구 사용을 요청하며 응답한다면(stop_reason이 tool_use인 경우), 여러분의 코드에서 해당 도구를 실행하고 그 결과를 tool_result로 다시 보냅니다. 모델이 end_turn으로 응답할 때까지 이 과정을 반복합니다. Java에서 이 루프를 실행하는 두 가지 방법은 — 완전한 제어를 위해 직접 작성하거나, SDK의 도구 러너(tool runner)가 대신 실행하도록 하는 것입니다.
수동 루프 (The Manual Loop) — 완전한 제어
루프를 직접 작성한다는 것은 모든 도구 호출이 실행되기 전에 여러분의 코드를 거친다는 것을 의미하며, 이 단계에서 인자(arguments)를 검증하고, 결정을 로그로 남기며, 되돌릴 수 없는 작업에 대해 게이트(gate)를 설정할 수 있습니다.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.*;
...
편의를 위한 러너(runner)가 숨겨버리는 두 가지 요소가 여기서 제 역할을 합니다. 바로 MAX_ITERATIONS 제한과 도구 결과가 왕복(round-trip)하기 직전의 로그 포인트입니다. 이 두 가지는 추가하는 비용은 적지만, 에이전트가 운영 환경에서 한 시간 동안 루프를 돌고 난 뒤에 사후에 적용하려면 비용이 매우 많이 듭니다.
SDK 도구 러너 (The SDK Tool Runner) — 편의성
모든 호출을 가로챌 필요가 없는 경우 — 예를 들어 위험 부담이 적은 읽기 전용 에이전트나 프로토타입의 경우 — 베타 도구 러너가 동일한 루프를 대신 실행해 줍니다. 각 도구를 Supplier를 구현하는 작은 클래스로 정의하고, SDK가 JSON 스키마(JSON schema)를 유도할 수 있도록 어노테이션(annotation)을 지정합니다.
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.helpers.BetaToolRunner;
...
트레이드오프(trade-off)는 명확합니다. 러너를 사용하면 코드 줄 수는 줄어들지만, 검증 및 승인 로직이 모델과 실행 사이의 단일 통제 지점(choke point)이 아닌 도구의 get() 메서드 내부에 존재해야 합니다. 읽기 전용 데모 이상의 단계라면, 수동 루프의 명시적인 체크포인트(checkpoint)가 추가 코드의 가치를 충분히 합니다.
중요한 지점에서의 결정론 (Determinism Where It Matters)
루프의 형태(shape) — 즉, 몇 번의 반복(iteration)이 허용되는지, 무엇을 완료로 간주할지, 실패한 도구 호출(tool call)을 어떻게 재시도할지 — 는 모델에게 "작동할 때까지 계속 시도하세요"라고 요청하는 시스템 프롬프트(system prompt)가 아니라 Java 코드에 포함되어야 합니다. Building Reliable LLM Applications in Java에서 다룬 바와 같이, 모델은 판단(어떤 도구를, 어떤 인자(argument)로, 언제 멈출지)을 위해 사용하고, 코드(루프, 재시도 정책(retry policy), 상한선, 감사 로그(audit log))는 장부 기록(bookkeeping)을 위해 사용하십시오. 자연어로 재시도 로직을 스스로 추론하는 에이전트는 일시적인 오류(transient failure)에 대해 무엇을 해야 할지 이미 알고 있는 catch 블록보다 더 느리고, 비용이 많이 들며, 예측 불가능합니다.
단계 간의 구조화된 핸드오프 (Structured Hand-offs Between Steps)
에이전트 단계 사이의 자유 형식 텍스트(free-text) 핸드오프는 오류가 조용히 누적되는 지점입니다. 예를 들어, 2단계에서의 약간 잘못된 필드 형식이 3단계 도구 호출의 잘못된 인자가 될 수 있습니다. 한 단계의 출력이 (단순히 사람에게 보여지는 것이 아니라) 다음 단계에서 _사용_되어야 하는 경우, 다시 파싱(re-parse)해야 하는 산문(prose) 대신 타입이 지정되고 스키마 검증(schema-validated)이 된 객체로 결과값을 받으십시오:
import com.anthropic.models.messages.StructuredMessageCreateParams;
record PlanStep(String action, boolean done) {}
...
타입이 지정된 PlanStep은 역직렬화(deserialize)가 되거나 되지 않거나 둘 중 하나입니다. 모델이 "완료(done)"를 의미했는지 아니면 "거의 다 끝났습니다(we're basically done)"를 의미했는지 추측하려는 정규 표현식(regex)은 필요하지 않습니다.
안전성과 비용 — 선택이 아닌 필수
에이전트는 읽은 텍스트를 바탕으로 런타임(runtime)에 당신의 함수 중 무엇을 어떤 인자로 호출할지 결정하는 프로그램입니다. 이에 따라 모든 도구를 공격 표면(attack surface)으로 취급하십시오:
- 도구 입력을 검증하고 화이트리스트(whitelist)를 적용하십시오.
toolUse.input()은 모델이 제공하는 데이터이며, 네트워크로부터 전달된 요청 본문(request body)과 마찬가지로 신뢰할 수 없는 것으로 취급해야 합니다. 실행 코드에 도달하기 전에 허용된 값을 화이트리스트로 관리하고, 숫자 범위를 제한하며, 도구의 규약(contract)에 맞지 않는 모든 것을 거부하십시오. 모델이 제공한 인자를 셸 명령어나 SQL 쿼리에 절대 문자열 보간(string-interpolate)하지 마십시오. - 되돌릴 수 없거나 외부로 향하는 작업 전에는 반드시 사람의 승인을 요구하십시오. 파일을 읽거나 API를 쿼리하는 것은 하나의 위험 단계이지만, 이메일을 보내거나, 레코드를 삭제하거나, 돈을 이동하는 것은 또 다른 단계입니다. 후자의 작업은 명시적인 승인 단계 — 즉, 사람의 확인, 실행 전 미리보기(dry-run preview), 또는 최소한 안전한 작업에 대한 하드코딩된 허용 목록(allowlist) — 를 통해 통제하십시오. 모델의 판단 자체가 되돌릴 수 없는 효과를 내기 전의 마지막 확인 단계가 되도록 방치해서는 안 됩니다.
- 루프 반복 횟수를 제한하십시오. 모든 에이전트 루프(agentic loop)에는 엄격한
MAX_ITERATIONS(또는 실제 시간 기준 타임아웃)가 필요합니다. 제한이 없다면, 혼란에 빠진 모델이 무한 루프를 돌며 토큰을 낭비하고, 실패하는 도구 호출을 영원히 재시도할 수도 있습니다. - 토큰 비용과 지연 시간(latency)을 일급 메트릭(first-class metrics)으로 추적하십시오. 에이전트의 비용은 단일 호출이 아니라 모든 반복의 합계입니다. 턴(turn)당
usage()를 계측하고, 재시도 폭풍(retry storm)에 대해 경고를 설정하는 것과 동일한 방식으로 제어되지 않는 루프에 대해 경고를 설정하십시오. - API 키를 절대 하드코딩하지 마십시오. 위의 모든 예제는
AnthropicOkHttpClient.fromEnv()를 통해ANTHROPIC_API_KEY를 읽어옵니다. 소스 코드, 버전 관리 시스템에 커밋된 설정 파일, 또는 로그에 키가 절대 나타나지 않도록 하십시오.
실무적 시사점 (Practical Takeaways)
실무적 시사점 (Practical Takeaways)
- 필요한 만큼만 계단을 오르세요: 단일 호출(single call) → 코드 기반 워크플로우(code-orchestrated workflow) → 에이전트. 대부분의 작업은 첫 번째 또는 두 번째 단계에서 멈춥니다.
- 루프를 구축하기 전에 네 가지 검사—복잡성, 가치, 실행 가능성, 오류 비용—를 수행하세요. '아니오'라는 답변은 더 단순하게 유지해야 할 이유가 됩니다.
- 수동 루프는 상세함(verbosity)을 포기하는 대신 모든 도구 호출을 검증하고, 기록하며, 게이트할 수 있는 단일 병목 지점(choke point)을 제공합니다. SDK 도구 러너는 그 통제권을 편리성으로 교환하므로, 기본값에 의존하지 말고 신중하게 선택하세요.
- 제어 흐름(루프, 재시도, 반복 횟수 제한)은 Java에 유지하고, 모델에게 판단을 맡기세요.
- 산문(prose)을 구문 분석하는 대신 단계 간 구조화되고 타입이 지정된 핸드오프를 사용하세요.
- 도구 입력은 신뢰할 수 없는 데이터로 검증하고, 되돌릴 수 없는 작업은 승인 절차 뒤에 게이트를 설정하며, 반복 횟수를 제한하고 비용을 측정(instrument)하세요. 이러한 장치 없이 에이전트는 기능이 아니라 책임(liability)입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기