
Strands에서의 Agents as Tools: Gemma 4 및 Ollama를 이용한 로컬 계층적 오케스트레이션
요약
Strands Agents 프레임워크를 활용하여 Gemma 4와 Ollama 기반의 로컬 계층적 에이전트 오케스트레이션 패턴을 구현하는 방법을 설명합니다. 에이전트를 도구(Tool)로 취급하여 중첩된 루프 구조를 형성함으로써 복잡한 IT 지원 자동화 작업을 수행하는 개념 증명을 다룹니다.
핵심 포인트
- Agents-as-Tools 패턴을 통한 계층적(Hub and Spoke) 멀티 에이전트 구조 구현
- 에이전트와 일반 도구를 동일한 인터페이스로 통합하여 재귀적 구성 가능
- 오케스트레이터와 하위 에이전트 간의 독립적인 중첩 루프 작동 방식
- LLM의 라우팅 결정을 돕는 @tool 함수의 docstring 중요성 강조
- Sliding Window 방식을 이용한 오케스트레이터의 컨텍스트 관리
꽤 오래전부터 다양한 프레임워크의 에이전트 오케스트레이션 (Agent Orchestration) 강의를 수강해 왔으며, 로컬 환경에서 실행되는 중간 정도의 복잡성을 가진 Strands Agents의 다양한 패턴을 탐구해 보기로 했습니다 (추후 AWS 배포를 염두에 둔 계획입니다). 이 글은 단 16GB RAM을 가진 노트북에서 Ollama를 통해 실행되는 Agents-as-Tools에 관한 내용입니다 💻.
서론
Strands Agents 프레임워크를 사용하여 에이전트를 개발하고 이해하려면 기초 과정을 수강하거나 문서(내용이 방대합니다)를 읽어볼 것을 제안합니다. 기초는 매우 중요합니다! 에이전트에 복잡성을 부여하는 방법을 아는 것이 필수적이며, 설계의 모든 책임을 AI에 위임하려 해서는 안 됩니다 (관련 리소스는 이 노트 하단에 있습니다).
개념 증명 (Proof of Concept)을 로컬에서 실행할 수 있도록, Docker에서 실행되는 Ollama와 소형 모델인 gemma4:e2b-it-qat (용량 4GB 남짓)를 사용했습니다.
**이 패턴을 개발하기 위해 채택한 유스케이스 (Use Case)**는 기본적인 IT 지원 자동화입니다:
🛠 지원 티켓을 수신하면 그것이 청구, 기술 또는 보안 티켓인지 판단합니다. 그 다음, 문제를 해결할 수 있도록 전문 에이전트에게 전달합니다.
핵심 개념
➡ Agents As Tools는 멀티 에이전트 오케스트레이션 (Multi-agent Orchestration) 패턴으로, 문서에 설명된 바와 같이 계층적 (Hub and Spoke) 구조를 가집니다. 여기서 오케스트레이터 (Orchestrator)는 문제를 해결하기 위한 다양한 도구 (Tools)를 가진 전문 에이전트들에게 작업을 위임합니다. 오케스트레이터와 각 에이전트는 격리된 컨텍스트 (Context)를 가지며, 서로 간섭하지 않습니다.
작동 방식을 명확히 하자면, 위임 시 하나의 공유된 루프가 아닌, 두 개의 중첩된 에이전트 루프 (Agent Loops)가 존재합니다: 오케스트레이터는 전문가의 답변을 기다리며 자신의 추론을 "일시 정지"하는 것이 아니라, 단순히 텍스트를 반환하는 도구 호출 (Tool Call)을 시각화할 뿐입니다. 그 이면에서 해당 도구 호출은 이미 완료된 후 돌아오기 전까지 독립적으로 작동하는 완전한 루프를 트리거한 것입니다.
Orchestrator loop (오케스트레이터 루프)
└─ llama billing_agent(query) → 새로운 독립적 루프 실행
└─ Billing loop (결제 루프): check_invoice_status → process_refund → 최종 응답
...
➡ @tool은 단순 작업(simple operation)과 API를 통한 통합, 그리고 완전한 에이전트(complete agent)를 구분하지 않습니다. Agents-as-Tools 패턴은 에이전트가 도구(tool)와 동일한 인터페이스(interface)(입력을 받고 출력을 반환함)를 충족하도록 활용하여 재귀적으로 구성될 수 있게 합니다. 이를 통해 상위 계층이 하위 계층의 내부 복잡성을 알 필요 없이 에이전트 내부에 에이전트를 중첩(nesting)할 수 있습니다.
⭐ 각 @tool 함수의 설명(docstring)은 단순한 장식용 주석이 아닙니다. 이는 오케스트레이터 역할을 하는 LLM이 실행 시간(runtime)에 어떤 전문가에게 위임할지 결정하기 위해 읽는 입력값입니다. 이는 정보 제공용 메타데이터로서 라우팅(routing)에 관여하지 않는 Agent 자체의 description= 파라미터와는 다릅니다.
➡ 오케스트레이터 메모리 (ConversationManager). 연속적인 상호작용 사이의 컨텍스트를 유지하기 위해, 세션의 마지막 10개 메시지 또는 티켓을 에이전트가 기억하도록 하는 압축(compression) 개념을 사용했습니다: SlidingWindowConversationManager(window_size=10)
프로세스 실행 동안 RAM 내의 Python 객체 리스트에 메시지를 유지합니다.
➡ 도메인 훅 (SteeringHandler). Strands의 "HookProvider"를 구현하며, 이 유스케이스에서는 보안 사고 대응 프로토콜의 단계 순서를 강제합니다. 예를 들어, LLM이 IP를 먼저 감사하지 않고 revoke_access_token을 호출하려고 시도하면, 훅은 함수가 실행되기 전에 event.cancel_tool을 통해 이를 차단합니다. 프레임워크는 모델의 판단에 의존하지 않고 결정론적인 비즈니스 로직을 강제합니다. BeforeInvocationEvent, AfterToolCallEvent, BeforeToolCallEvent 세 가지 이벤트를 정확하게 설명합니다.
훅(hooks)은 일반적으로 다음과 같은 용도로 사용됩니다:
- 무한 루프 또는 과도한 도구 호출 (tool calls) 방지 (비용/토큰 제어)
- 보안/비즈니스 가드레일 (Guardrails) (예: n개 이상의 트랜잭션 허용 안 함, rate limit이 있는 API에 대한 n개 이상의 쿼리 제한 등)
- 에이전트 루프 (agent loop)의 코어를 수정하지 않고도 제어 로직을 주입하기 위한 Strands의 표준 메커니즘입니다.
➡ 평가 (Evaluations, Evals): 에이전트의 행동을 평가합니다 (전통적인 assert 방식이 아닌 Contains 방식 사용).
Ollama를 통해 모델을 실행하는 로컬 환경에서 평가(evals)가 멈추지 않고 실행될 수 있도록 제가 고려한 몇 가지 사항은 다음과 같습니다: OpenTelemetry의 OTLP를 비활성화했습니다. 이 기능은 내부적으로 엔드포인트(localhost:4318)에 도달하지 못할 경우 무한정 대기 상태(hang)에 빠지기 때문입니다.
해당 리포지토리의 유스케이스(use case)에서는 단일 LLM만 사용하므로 결정론적 평가(deterministic evaluations)를 수행합니다. 모든 평가를 다루지는 않지만, 아래에 설명된 두 가지 중요한 평가를 수행합니다.
출력 평가 (Output Evaluation). 에이전트가 그 결과에 도달하기 위해 취한 내부 경로와 상관없이, 최종 결과(사용자가 받는 텍스트)를 평가합니다. 이는 **키워드 매칭 (keyword matching)**을 통한 결정론적 체크입니다. 즉, 응답에 기대되는 특정 키워드(INV-1002, USR-884 등)가 포함되어 있어야 합니다.
궤적 평가 (Trajectory Evaluation). 에이전트가 무엇이라고 응답했는지가 아니라, 어떻게 그 결과에 도달했는지, 즉 추론 과정에서 어떤 도구(tools)/서브 에이전트(sub-agents)를 호출했는지를 평가합니다. 코드에서 extract_called_tools()는 agent.messages를 검사하여 toolUse 블록을 찾고, evaluate_trajectory()는 기대되는 서브 에이전트(예: BillingAgent)가 실제로 위임되었는지 확인합니다. Ollama를 이용한 로컬 환경 실행을 위해, extract_called_tools 함수는 agent.messages에서 도구 이름을 직접 추출하여 case.expected_agent가 호출되었는지 밀리초 단위로 확인합니다. 이 방식은 추가적인 판사 모델(judge model)을 인스턴스화하거나 Bedrock/Ollama에 추가 요청을 보낼 필요가 없어 비동기적 멈춤 현상(asynchronous hangs)이 발생하지 않습니다.
⭐ 차원 수준에서의 PASS / FAIL, 출력물 기준: 각 EvalCase는 두 차원이 모두 통과했을 때(traj_eval.test_pass 및 out_eval.test_pass)만 승인(case_passed)으로 표시됩니다. 에이전트가 올바른 전문가에게 작업을 위임할 수는 있지만(경로(trajectory) OK), 예상되는 키워드 없이 응답할 경우(출력(output) FAIL), 해당 케이스는 동일하게 실패로 표시됩니다. 단일 평가 차원만으로는 불충분합니다: 에이전트가 "올바른 행동"을 하고도 "잘못 전달"하거나, 그 반대의 경우가 발생할 수 있기 때문입니다.
솔루션 설명
📦 GitHub 저장소: github.com/reinalau/strands-agents-as-tools
LLM을 호스팅하기 위해 Docker Desktop을 사용하였고, 이후 Ollama 이미지를 다운로드하여 gemma4:e2b-it-qat(용량 4GB 이상)를 설치했습니다. 모든 gemma 모델은 여기에서 찾을 수 있지만, 특히 이 모델은 16GB RAM을 탑재한 노트북에서도 잘 작동합니다.
필수 조건으로 Docker에 충분한 메모리를 할당했는지 확인하십시오.
# 지속성 볼륨(persistent volume)과 함께 Ollama 서버 실행
docker run -d --name ollama -p 11434:11434 -v ollama_data:/root/.ollama ollama/ollama
# 모델 다운로드
...
중요! 질문을 던져 모델이 응답하는지 확인하며 터미널에서 gemma 4 모델을 테스트해 보세요.
멀티 에이전트(multi-agent) 코드는 Python으로 작성되었으며, 프로젝트 구조는 로직을 더 쉽게 설명할 수 있도록 설계되었습니다:
strands-agents-as-tools
├── README.md
├── requirements.txt
...
로컬 실행
소스 코드 저장소를 클론(clone)하고 gemma 4 모델이 포함된 Ollama Docker가 실행 중이라면, 환경 구축을 시작합니다.
requirements.txt에서 발견하게 될 요구 사항(requirements)은 다음과 같습니다:
strands-agents
strands-agents-evals
ollama
python-dotenv
pytest
pip install -r requirements.txt
.env 파일에는 다음 설정이 필요합니다:
OLLAMA_HOST=http://localhost:11434
MODEL_NAME=gemma4:e2b-it-qat
LOG_LEVEL=INFO
cp .env.example .env
테스트는 결정론적 단위 테스트 (deterministic unit tests) (tests/)입니다. 이는 LLM 추론을 요구하지 않고도 비즈니스 도구(billing, technical support, security)의 순수 코드 로직을 빠르고 격리된 방식으로 검증할 수 있게 해줍니다. 이를 통해 보조 함수들이 멀티 에이전트 시스템에 @tool 기능으로 노출되기 전에 예상된 상태와 형식을 반환하는지 보장합니다.
python -m pytest tests/
이제 오케스트레이터가 Ollama로 처리되는 전문가들에게 티켓을 위임하는 모습을 볼 수 있는 주요 데모 플로우를 실행합니다. 선택한 Ollama 모델에 따라 속도가 느릴 수 있지만, 결국 티켓 예시들과 함께 플로우가 종료됩니다. main 내부에서는 from examples.sample_tickets import SAMPLE_TICKETS를 사용하여 지원(support) 예시들을 사용하고 있습니다... 직접 변경해 보시는 것을 추천합니다!
python -m src.main
마지막으로, "Evals" 개념을 사용하여 실제 에이전트에 대해 실행되는 품질 및 정확도 테스트 평가를 수행합니다. 이는 여러분이 구축할 모든 미래의 에이전트에게 중요한 사항입니다. Strands의 문서를 참조하여 우리의 에이전트를 어떻게, 그리고 무엇을 위해 평가해야 하는지 파악해야 합니다.
또한 OpenTelemetry 텔레메트리 수집기(Key Concepts에서 설명됨)를 비활성화하려면 OTEL_SDK_DISABLED=true를 설정하는 것을 권장합니다.
export OTEL_SDK_DISABLED="true"
python -m evals.run_evals
이 마지막 스크립트는 Strands Agents: Output / Keyword Evaluation 및 Tool-Call Trajectory Evaluation (Key Concepts에서 설명됨) 명세에 따라 두 가지 평가 방법론을 실행합니다.
📝 참고: 저장소의 코드 설명과 단계별 실행 방법은 README.md에서 확인할 수 있습니다.
결론
Ollama를 통해 로컬 모델로 Agents-as-Tools를 구현한 것은 비용을 들이지 않고 오케스트레이션 (Orchestration) 패턴을 이해할 수 있는 좋은 방법이었습니다. 하지만 gemma4:e2b-it-qat와 같은 작은 모델들은 더 큰 모델(Bedrock/Claude/GPT-4)이 아마 더 정확할 것과 달리, 추론 과정에서 일관성이 없을 수 있음(동일한 도구에 대한 중복 호출, 위임 전 망설임 등)을 발견했습니다. 이는 패턴을 무효화하는 것이 아니라 오히려 그 반대입니다. 즉, 왜 결정론적 가드레일 (Deterministic Guardrails, 보안 훅)과 자동 평가가 프로덕션에 배포되기 전 비정상적인 동작을 감지하는 방법인지를 재확인시켜 줍니다.
예제의 한계점
이 저장소는 교육용이며, 다음과 같은 여러 단순화된 부분이 포함되어 있습니다:
- 모의 데이터(Mock Data)로 하드코딩된 도구들:
check_invoice_status,check_system_status,audit_ip_address등은 코드 내에 고정된 데이터를 반환하며, 실제 시스템을 조회하지 않습니다. - 단일 로컬 LLM: 서로 다른 크기의 모델 간의 라우팅 (Routing)/도구 호출 (Tool-calling) 일관성을 비교하기 위해 다른 모델로 동작을 테스트하지 않았습니다.
- 평가: 출력 (Output) 및 궤적 (Trajectory)만을 결정론적인 방식으로 다루었습니다. 카오스 테스팅 (Chaos Testing), LLM-as-judge, 또는 멀티 턴 (Multi-turn) 시뮬레이션은 테스트되지 않았습니다 (공식 문서를 참조하세요).
이와 동일한 프로덕션급 멀티 에이전트(Multi-agent)를 고려한다면 🤯
-
실제 API/시스템에 도구 연결: 모의 데이터(mocks)를 실제 결제 API, 인프라 상태 엔드포인트(예: AWS CloudWatch, Datadog), 그리고 토큰 취소를 위한 실제 ID 관리 서비스(예: IAM, Okta) 호출로 교체합니다. 이 중 다수는 커뮤니티나 AWS에서 유지 관리하는 MCP 서버로 이미 존재합니다 (
MCPClient를 통해 연결하는 것이 각 도구를 수동으로 작성하는 것보다 더 빠를 것입니다). -
실제 세션 지속성 (Persistence): 이 예제에서는 각 티켓이
main.py실행 간에 상태를 유지하지 않는 stateless 방식입니다. 동일한 사용자의 실제 멀티 턴(multi-turn) 대화를 시뮬레이션하려면SessionManager(FileSessionManager또는DynamoDBSessionManager)를 추가하십시오. -
모델 비교: 동일한 테스트 스위트(test suite)를 Bedrock 모델이나 Anthropic API에 대해 실행하고, 로컬 모델과 라우팅(routing)/도구 호출(tool-calling) 오류율을 비교하여 비용/지연 시간(latency) 대 신뢰성 사이의 트레이드오프(trade-off)를 정량화하십시오.
-
Chaos testing:
ChaosPlugin을 사용하여 (연결된 후의) 실제 API 장애를 시뮬레이션함으로써, 에이전트가 답변을 환각(hallucination)하는 대신 우아하게 성능을 저하시키며(degrade with grace) 대응하는지 검증하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기