Go 언어로 LLM 에이전트를 구축하고 오케스트레이션하는 방법
요약
Go 언어 기반의 LLM 에이전트 프레임워크인 Galdor를 사용하여 단일 에이전트부터 멀티 에이전트 시스템까지 구축하는 방법을 설명합니다. 도구 호출 루프를 구현하고, 라우터를 통해 전문 에이전트에게 작업을 배분하는 오케스트레이션 구조를 다룹니다.
핵심 포인트
- Go 언어의 구조체를 활용해 JSON 스키마를 자동으로 생성하는 도구 등록 방식
- Anthropic, OpenAI 등 다양한 프로바이더를 쉽게 교체할 수 있는 유연성
- 단일 에이전트의 한계를 극복하기 위한 슈퍼바이저(라우터) 기반 멀티 에이전트 구조
- 작업자(Worker)를 단순 함수 시그니처로 추상화하여 복잡한 에이전트 설계 가능
요약(TL;DR): 저는 Galdor라는 LLM 에이전트용 Go 프레임워크를 구축했습니다. 이 글에서는 이를 통해 에이전트를 구축하는 과정을 살펴봅니다. 도구(tools)를 가진 하나의 에이전트부터, 앞에 라우터(router)를 둔 여러 전문 에이전트까지 다룹니다. 코드는 실제이며 어떤 프로바이더(provider)와도 연동되어 실행됩니다.
제 에이전트 대부분은 하나의 형태로 시작합니다. 모델 하나, 몇 개의 도구, 그리고 답을 얻을 때까지 해당 도구들을 호출할 수 있게 해주는 루프(loop)가 그것입니다. 이것만으로도 놀라울 정도로 많은 것을 처리할 수 있습니다. 문제는 나중에 발생합니다. 단일 에이전트가 15개의 도구를 가지게 되고 시스템 프롬프트(system prompt)가 단편 소설 길이만큼 길어지며, 계속해서 잘못된 도구를 호출하기 시작할 때 혼란이 나타납니다. 따라서 이 포스트에서는 제가 반복해서 도달하게 되는 두 가지 단계와 Galdor에서 그것들이 어떻게 구현되는지를 다룹니다.
하나의 에이전트
여기서 에이전트란 프로바이더(provider), 도구 세트, 그리고 설정(config)입니다. 도구는 단순한 Go 함수입니다. 입력 구조체(input struct)가 스키마(schema)가 되므로, JSON 스키마를 직접 작성할 필요가 없습니다:
type weatherIn struct {
City string `json:"city" jsonschema:"City to look up"`
}
...
도구를 등록하고, 프로바이더를 선택한 뒤 실행합니다 (공간 확보를 위해 에러 처리는 생략함):
p, _ := anthropic.New(anthropic.Config{APIKey: os.Getenv("ANTHROPIC_API_KEY")})
reg, _ := tool.NewRegistry(
...
이것은 ReAct 루프를 실행합니다. 모델이 weather를 호출하고, 결과를 돌려받고, math를 호출한 다음, 답변을 내놓는 방식입니다. Anthropic을 OpenAI, Gemini, Bedrock 또는 Ollama를 통한 로컬 모델로 교체하는 것은 해당 한 줄의 프로바이더만 바꾸는 작업입니다. 다른 부분은 변경할 필요가 없습니다.
라우터가 있는 여러 에이전트
하나의 에이전트에 과부하가 걸리면, 이를 전문가(specialists)로 나누고 앞에 라우팅 에이전트(routing agent)를 배치합니다. Galdor에서는 이를 슈퍼바이저(supervisor)라고 부릅니다. 이는 요청을 읽고, 작업자(worker)를 선택하며, 답변을 얻으면 멈추는 작은 LLM입니다. 각 작업자는 자신만의 도구를 가진 독립적인 에이전트이므로 규모를 작게 유지할 수 있습니다.
이 구조를 깔끔하게 만드는 핵심은 작업자가 단순히 string in, string out 함수라는 점이며, 전체 ReAct 에이전트가 해당 시그니처(signature) 뒤에 배치될 수 있다는 것입니다.
// 각 전문가(specialist)는 일반적인 ReAct 에이전트입니다.
billing, _ := agent.NewReAct(agent.Config{Provider: p, Tools: billingTools, Model: model})
technical, _ := agent.NewReAct(agent.Config{Provider: p, Tools: techTools, Model: model})
...
관리자(supervisor)는 결제(billing) 관련 질문을 결제 에이전트에게 전달하며, final.History를 통해 정확히 누구를 호출했고 무엇을 물었는지 확인할 수 있습니다. 경로(route)가 잘못되었을 때 제가 뚫어지게 쳐다보는 것이 바로 그 히스토리입니다. 왜냐하면 버그는 에이전트 자체보다는 거의 항상 모호한 작업자(worker) 설명에서 발생하기 때문입니다.
중앙 라우터(router)를 거치지 않고 에이전트들이 서로 직접 업무를 인계하는 스웜 모드(swarm mode)도 있습니다. 개념은 같지만 형태가 다를 뿐입니다.
왜 이렇게 구성(compose)되는가
단일 에이전트와 관리자(supervisor) 모두 동일한 것, 즉 고루틴(goroutines)과 채널(channels) 위에서 실행되는 그래프(graph)로 컴파일됩니다. 따라서 이들은 동일한 종류의 값이며, 그래프 기능은 어느 쪽에도 적용됩니다. Human-in-the-loop를 위한 체크포인트(checkpoint) 및 재개(resume) 기능을 사용할 수 있으며, 진행 과정에서의 모든 모델 호출(model call)과 도구 호출(tool call)은 나중에 열어볼 수 있는 OpenTelemetry 스팬(span)이 됩니다. 이 모든 과정은 에이전트마다 별도로 연결(wired)할 필요가 없습니다.
Go를 작성하지 않고도 동일하게 수행하기
모든 것이 Go 프로그램일 필요는 없습니다. 저의 실제 워크플로 중 상당수는 YAML 파일과 몇 번의 CLI 호출로 이루어집니다. 이 방식을 사용하면 커스텀 Go 도구는 포기해야 하지만, 내장 도구와 MCP 도구는 그대로 유지할 수 있으며 모든 실행은 여전히 트레이스(trace)를 기록합니다.
단일 에이전트는 하나의 agent: 블록입니다. 이 에이전트는 들어오는 이슈를 분류(triage)하며, 프로젝트의 컨벤션(conventions)을 추측하는 대신 직접 읽어 들입니다. file_read는 base_dir로 제한되어 있으므로, 에이전트는 무언가를 결정하기 전에 실제 CONTRIBUTING.md 파일을 가져옵니다.
# triage.yaml — 실행: galdor cast triage.yaml "$(cat issue.txt)"
version: 1
agent:
...
몇 개의 에이전트를 체이닝(chain)하려면 각 단계가 자체적인 cast가 되며, 셸(shell)이 한 단계의 출력을 다음 단계로 전달합니다. 제가 실행하는 릴리스 파이프라인(release pipeline)은 원시 커밋(raw commits)을 불렛 포인트(bullets)로 변환한 다음, 이를 공지사항(announcement)으로 변환합니다. 알아두면 유용한 까다로운 부분들은 다음과 같습니다:
DB=./pipeline.db
# 1단계: 커밋 -> 분류된 불렛 포인트
...
답변은 stdout (표준 출력)으로 전달되고 추적 로그(trace logs)는 stderr (표준 에러)로 전달되므로, $(galdor cast ...)는 답변만을 캡처합니다. 각 단계가 동시에 SQLite 추적 DB에 기록되지 않도록 | 파이프를 통한 실행 대신 순차적으로 단계를 실행하세요. 그리고 두 로그 모두 동일한 --db를 사용하되 각각 고유한 --run-id를 부여해야 합니다. 그렇지 않으면 galdor scry list에서 로그가 나란히 나열되지 않고 충돌하게 됩니다. 만약 고정된 순서 대신 라우터(router)가 실행 주체를 결정하게 하고 싶다면, 하나의 YAML 파일에 여러 워커(worker)를 포함하는 supervisor 또는 swarm 방식의 galdor council을 사용하면 됩니다. (주의: council은 아직 대시보드로 추적을 보내지 않으므로, 현재 scry에서 모든 단계를 확인할 수 있는 방식은 체인(chained) 버전입니다.)
어떤 방식이든, galdor scry show <run-id>와 galdor ui는 로컬 SQLite DB로부터 각 에이전트가 무엇을 읽고 수행했는지를 정확하게 재생(replay)합니다. 별도의 수집기(collector)나 계정, 호스팅되는 서비스는 전혀 필요하지 않습니다.
프로젝트에 대해 간단히 소개하자면, Galdor는 Go 언어로 작성되었으며, Apache 2.0 라이선스를 따르고, 단일 바이너리(single binary) 형태의 셀프 호스팅(self-hosted) 도구입니다. 프로바이더(Anthropic, OpenAI, Ollama 또는 vLLM과 같이 OpenAI와 호환되는 모든 것, Gemini, Bedrock)와 메모리 백엔드(memory backends)는 별도의 모듈로 분리되어 있어 코어(core)를 작게 유지할 수 있습니다. 현재 이 프로젝트는 저 혼자 진행하고 있습니다.
YasserCR / galdor
OpenTelemetry 관측성(observability)이 내장된 LLM 에이전트용 Go 네이티브 프레임워크.
galdor
galdor (명사, 고대 영어, 약 9세기): 주문, 마법, 현실을 굴절시키는 읊조리는 단어.
AI 에이전트를 구축, 오케스트레이션(orchestrating) 및 관측(observing)하기 위한 Go 네이티브 프레임워크. 네이티브 OpenTelemetry. 내장된 대시보드. 단일 바이너리. 외부 SaaS 없음. Apache 2.0.
Why galdor
아래 표는 2026년 5월에 각 프로젝트의 리포지토리(repo), 릴리스(releases) 및 공식 문서를 기준으로 마지막으로 검증되었습니다. 출처는 표 아래에 링크되어 있으며, 내용이 달라질 경우 PR(Pull Request)을 환영합니다.
| galdor | LangChain Python + LangSmith | LangChainGo | Eino | Genkit Go |
|---|---|---|---|---|
| 최신 릴리스 (Latest release) | v1.0.0 (2026년 6월) | langchain-core v1.4.0 (2026년 5월) | v0.1.14 (2025년 10월) | v0.8.13 stable, v0.9.0-alpha active (2026년 5월) — pre-1.0 |
| ... | ||||
| … |
만약 여러분이 Go 언어로 멀티 에이전트 (multi-agent) 시스템을 구축한다면, 어떤 기준으로 경계선을 긋는지 궁금합니다. 수많은 도구 (tools)를 가진 하나의 거대한 에이전트로 만들 것인지, 아니면 앞에 라우터 (router)를 둔 전문가 에이전트들로 구성할 것인지, 그리고 그러한 선택으로 인해 겪었던 어려움은 무엇이었는지 궁금합니다. 이 코드 스니펫 (snippets)들은 저장소(repo) 내에서 직접 실행 가능한 버전으로 제공됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기