
Go에서의 선언적 에이전트 워크플로우(Declarative Agent Workflows): Microsoft가 출시한 것과 직접 구축하는 것
요약
Microsoft의 Agent Framework 1.0 출시와 함께 도입된 선언적 워크플로우의 개념을 설명합니다. Python과 .NET에서는 YAML 기반의 선언적 방식이 지원되지만, Go SDK에서는 아직 미지원 상태이므로 이를 직접 구현하는 방법을 다룹니다.
핵심 포인트
- 선언적 워크플로우는 워크플로우를 코드가 아닌 YAML 설정 파일로 기술하는 방식입니다.
- YAML 기반 방식은 버전 관리, 차이점 비교, 비개발자와의 협업에 유리합니다.
- Microsoft의 선언적 에이전트는 PowerFx 표현식 엔진을 사용하여 런타임에 동작을 평가합니다.
- 현재 Go SDK는 선언적 워크플로우를 지원하지 않으므로 직접 구현이 필요합니다.
2026년 6월, Microsoft는 자사의 Agent Framework를 위한 선언적 워크플로우(declarative workflows) 1.0 버전을 출시했습니다. 제안하는 핵심은 매우 좋습니다. 멀티 에이전트 워크플로우를 코드 내에서 직접 연결하는 대신 YAML 파일로 기술함으로써, 워크플로우를 읽고, 차이점(diff)을 비교하고, 버전을 관리하며, 코드를 작성하지 않는 사람에게도 전달할 수 있는 설정 아티팩트(config artifact)로 만드는 것입니다. agent-framework-declarative 패키지는 PyPI에서 1.0.0 버전으로 출시되었으며, 이미 안정화된 .NET의 Microsoft.Agents.AI.Workflows.Declarative에 합류했습니다.
하지만 문서를 열고 언어를 Go로 전환하면, 방법(how-to)이 보이지 않습니다. 선언적 워크플로우는 Python과 .NET 기능입니다. Go SDK인 github.com/microsoft/agent-framework-go는 퍼블릭 프리뷰(public preview) 상태이며, 자체 릴리스 노트에서도 이러한 격차에 대해 솔직하게 밝히고 있습니다: 선언적 에이전트(declarative agents), RAG, CodeAct, 그리고 기능적 워크플로우(functional workflows)는 "아직 사용할 수 없습니다."
따라서 만약 여러분이 Go로 에이전트 시스템을 구축하고 있다면, 흥미로운 상황에 놓여 있는 것입니다. Microsoft가 판매하려는 개념은 진정으로 유용합니다. 이를 가능하게 할 런타임(runtime)은 여러분의 언어에 이미 존재합니다. 단지 그 위에 얹혀질 얇은 YAML-to-graph 레이어가 아직 Go용으로 작성되지 않았을 뿐입니다. 이는 약 100줄 정도의 코드로 메울 수 있는 격차이며, 이 글을 다 읽을 때쯤이면 여러분은 그것을 갖게 될 것입니다. 하지만 그 전에, "선언적(declarative)" 방식이 실제로 무엇을 제공하는지 정확히 짚고 넘어가겠습니다. 왜냐하면 이를 도입하려는 팀 중 절반은 그것이 필요하지 않기 때문입니다.
여기서 "선언적(declarative)"이 실제로 의미하는 것
프로그래밍 방식(programmatic)의 워크플로우는 코드입니다. 빌더(builder)를 임포트하고, 실행기(executors)를 인스턴스화하며, 이들을 연결한 뒤 Build()를 호출합니다. 선언적 워크플로우는 동일한 그래프를 데이터로 표현한 것입니다. 다음은 문서에서 가져온 Python 스타일의 버전으로, 이름을 불러 인사를 건네는 워크플로우입니다:
name: greeting-workflow
description: A simple workflow that greets the user
...
주목할 만한 두 가지가 있습니다. 첫째, 모든 단계에는 kind가 있습니다. 이 프레임워크는 고정된 어휘(vocabulary)를 제공하며, 이 어휘 자체가 핵심입니다: SetVariable, If, ConditionGroup, Foreach, InvokeAzureAgent, InvokeFunctionTool, InvokeMcpTool, HttpRequestAction, Question, SendActivity 등 수십 가지가 있습니다. 사용자가 동작(behavior)을 작성하는 것이 아니라, 사전에 승인된 빌딩 블록 메뉴에서 선택하는 것입니다. 이러한 제약이야말로 기능입니다. 개발자가 아닌 사람도 파일을 안전하게 편집할 수 있게 해주는 요소입니다.
둘째, =로 시작하는 value 필드를 보세요. 이것들은 표현식(expressions)이며, 임베디드 표현식 엔진에 의해 런타임에 평가됩니다. Microsoft는 여기서 PowerFx를 사용하는데, 이는 Power Apps의 기반이 되는 것과 동일한 공식 언어입니다. 따라서 `=Concat(
2026년 중반 기준 현황은 다음과 같습니다. 선언적 워크플로우 (Declarative workflows)는 Python과 .NET에서는 1.0 단계에 도달했습니다. Go에서는 아직 존재하지 않습니다. Go SDK는 퍼블릭 프리뷰 (public preview) 상태이며, 현재 출시된 기능 목록은 에이전트 (agents), 프로바이더 (providers), 도구 (tools), 그리고 프로그래밍 방식의 워크플로우 (programmatic workflows)를 포함합니다. 선언적 레이어 (declarative layer), RAG 헬퍼 (RAG helpers), CodeAct, 그리고 함수형 워크플로우 (functional workflows)는 모두
함수에 대한 for ... range를 주목하세요. 이것은 Go 1.23의 range-over-func 반복자 (iterator)입니다. SDK의 go.mod는 실제로 Go 1.25를 요구하므로, 매우 최신 버전의 Go를 위해 작성되었으며 새로운 언어 기능들을 적극적으로 활용하고 있습니다. 또한, 이벤트를 수집하여 나중에 run.NewEvents()로 살펴볼 수 있는 비스트리밍 (non-streaming) inproc.Default.Run 방식도 존재합니다.
그것은 직선 형태입니다. 흥미로운 케이스는 분기 (branches)입니다. 빌더 (builder)는 switch를 통해 조건부 라우팅 (conditional routing)을 노출하므로, 메시지 내용에 따라 서로 다른 경로로 메시지를 보낼 수 있습니다. 프리뷰 (preview) 단계에서의 형태는 다음과 같습니다 (정확한 메서드 이름은 프리뷰 단계의 것으로 간주하십시오. 리포지토리 (repo)가 진실의 원천입니다):
wb := workflow.NewBuilder(classify)
wb.AddSwitch(classify).
...
동일한 계열의 기능들이 더 있습니다: 여러 실행기 (executors)에게 브로드캐스트 (broadcast)하기 위한 AddFanOutEdge, 이들을 다시 결합하기 위한 AddFanInBarrierEdge, 직선 시퀀스 (sequence)를 위한 AddChain, 그리고 워크플로우가 일시 중지되고 외부에서 응답할 수 있는 요청 이벤트 (request event)를 발생시키는 인간 참여형 (human-in-the-loop)을 위한 RequestPort가 있습니다. 그리고 Build()는 단순히 승인 도장이 아닙니다. 이는 그래프 (graph)를 검증합니다: 연결된 실행기 간의 타입 호환성 (type compatibility), 모든 실행기가 시작 지점으로부터 도달 가능한지 여부, 바인딩 (bindings)이 해결되는지 여부, 그리고 중복된 엣지 (edges)를 선언하지 않았는지 등을 확인합니다. 프로덕션 환경에서 메시지가 조용히 어디론가 사라지는 대신, 빌드 타임 (build time)에 연결되지 않은 노드 (node)를 발견하게 됩니다.
따라서 Go SDK는 완전한 오케스트레이션 (orchestration) 인터페이스를 갖추고 있습니다. 다만 부족한 점은 그 인터페이스를 YAML 파일로 표현할 방법이 없다는 것입니다. 이를 추가하기 전에, 우리는 무엇으로 컴파일되는지를 이해해야 합니다. 왜냐하면 실행 모델 (execution model)에는 날카로운 모서리 (sharp edge)가 있기 때문입니다.
내부 동작: 그것은 슈퍼스텝 엔진 (superstep engine)입니다
대부분의 사람들이 이 부분을 건너뛰었다가 나중에 낭패를 보는 지점이 바로 여기입니다. 에이전트 프레임워크 (Agent Framework)는 단순히 그래프를 노드 단위로 하나씩 따라가는 것이 아닙니다. 이 프레임워크는 "수정된 Pregel 실행 모델, 즉 슈퍼스텝 (superstep) 기반 처리를 사용하는 벌크 동기식 병렬 (Bulk Synchronous Parallel, BSP) 방식"으로 작동합니다. Pregel은 Google이 2010년에 거대 그래프 연산을 위해 발표한 그래프 처리 모델이며, BSP는 그 기반이 되는 수십 년 된 병렬 컴퓨팅 (parallel-computing) 원칙입니다. 이를 에이전트 워크플로우 (agent workflows)에 도입한 것은 정말 탁월한 선택이며, 이는 그래프를 추론하는 방식을 완전히 바꿔 놓습니다.
실행은 불연속적인 슈퍼스텝 (supersteps) 단위로 이루어집니다. 각 슈퍼스텝은 다음의 다섯 가지 동일한 과정을 거칩니다:
- 이전 슈퍼스텝에서 대기 중인 모든 메시지를 수집합니다.
- 엣지 (edges)와 그 조건에 따라 각 메시지를 대상 실행기 (executors)로 라우팅합니다.
- 대상이 된 모든 실행기를 동시에 (concurrently) 실행합니다.
- 모든 실행기가 완료될 때까지 동기화 장벽 (synchronization barrier)에서 대기합니다.
- 실행기들이 방출한 새로운 메시지들을 큐 (queue)에 넣고, 다음 슈퍼스텝을 시작합니다.
4단계의 장벽 (barrier)이 핵심입니다. 하나의 슈퍼스텝 내에서는 모든 것이 병렬로 실행됩니다. 하지만 슈퍼스텝 사이에는 아무것도 병렬로 실행되지 않습니다. 현재 단계에서 가장 느린 실행기가 결과를 반환할 때까지 워크플로우는 진행되지 않습니다.
이 장벽은 프로덕션 에이전트 (production agents)에 있어 중요한 세 가지 이점을 제공합니다. 첫째, 실행이 결정론적 (deterministic)입니다. 즉, 매번 동일한 입력에 대해 동일한 순서로 실행됩니다. 둘째, 체크포인팅 (Checkpointing)이 신뢰할 수 있습니다. 슈퍼스텝 경계는 전역적으로 일관된 스냅샷 (snapshot)이기 때문에, SDK가 상태를 저장하고 다른 머신에서 재개할 수 있는 것입니다. 셋째, 추론이 더 간단합니다. 하나의 단계 내에서는 경합 조건 (races)이 발생하지 않으며, 모든 실행기가 메시지의 동일한 고정된 뷰 (frozen view)를 보기 때문입니다.
이제 날카로운 모서리 (sharp edge)에 대해 이야기해 보겠습니다. 예를 들어, 워크플로우가 두 개의 분기로 갈라진다고 가정해 봅시다. 하나는 30초가 걸리는 단일 장기 실행 에이전트 호출이고, 다른 하나는 빠른 3단계 체인 (chain)입니다. 빠른 체인이 앞서 나갈 것이라고 예상하겠지만, 그렇지 않습니다. 그 체인의 각 단계는 자체적인 슈퍼스텝이며, 모든 슈퍼스텝은 30초짜리 에이전트가 끝날 때까지 장벽 (barrier)에서 대기합니다. 즉, 당신의 "빠른" 분기는 "느린" 분기와 발맞추어(lockstep) 움직이게 됩니다.
문서에서는 이 해결책을 미리 명시하고 있으며, 처음 읽을 때는 직관에 어긋날 수 있습니다. 만약 두 분기(branch)가 진정으로 독립적으로 실행되기를 원한다면, 그중 하나를 체인(chain)으로 만들지 마세요. 해당 분기의 세 단계를 하나의 실행기(executor)로 통합하십시오. 그러면 두 분기 모두 하나의 슈퍼스텝(superstep) 내에서 완료되며, 중간 경계(intermediate boundaries)에서 서로를 차단(block)하지 않게 됩니다. 슈퍼스텝 엔진에서 노드(node)가 더 많다는 것이 더 많은 병렬성(parallelism)을 의미하지는 않습니다. 때로는 오히려 더 적어질 수도 있습니다.
Go에서 자신만의 선언적 레이어 구축하기
이제 결실을 볼 차례입니다. 우리는 YAML로 기술된 워크플로우가 Go에서 실행되는 선언적 경험을 원합니다. 우리는 PowerFx를 재구현하거나 .NET을 우리 프로세스에 바인딩(bind)하지 않을 것입니다. 대신 작고 정직한 엔진을 구축할 것입니다. 즉, 고정된 액션 어휘(action vocabulary), 수식 언어 대신 간단한 ${var} 보간(interpolation), 그리고 agent-framework-go, OpenAI SDK 또는 테스트를 위한 페이크(fake)를 연결할 수 있는 플러그형 에이전트 인터페이스를 갖춘 엔진입니다. 이는 한 번에 읽을 수 있는 규모로 구현된 실제 제품의 형태를 띠고 있습니다.
스키마(schema)부터 시작합니다. 워크플로우는 이름과 액션(action)의 목록입니다. 각 액션은 자신의 kind가 필요로 하는 필드만 가집니다. Go에서는 이는 omitempty 필드와 Kind 판별자(discriminator)를 가진 하나의 구조체(struct)를 의미합니다.
package flow
// Spec은 YAML 파일에서 파싱된 전체 워크플로우입니다.
...
*Cond가 포인터라는 점은 중요합니다. nil인 Cond는 해당 액션이 조건부(conditional)가 아님을 의미하며, 이는 추측해야 하는 제로 값(zero-value) 구조체보다 더 깔끔합니다. 이것은 Microsoft의 액션 어휘를 Go 버전으로 만든 것으로, set, if, agent, send의 네 가지 종류로 축소되었습니다. 실제 작동하기에 충분하면서도, 따라가기에 충분히 작습니다.
다음은 상태(state)입니다. 워크플로우 변수와 출력 싱크(output sink)가 있으며, PowerFx 표현식을 대신하여 ${name} 보간을 사용합니다.
package flow
import "strings"
...
에이전트(Agent)는 인터페이스이며, 바로 그 단일한 결정이 실제 프로젝트에서 이를 유용하게 만드는 핵심입니다. 엔진은 에이전트가 무엇인지 알 필요도, 신경 쓸 필요도 없습니다. 오직 당신이 프롬프트(Prompt)를 전달하면 텍스트를 돌려받을 수 있다는 사실만 알면 됩니다.
package flow
import "context"
...
이제 엔진(Engine)입니다. 엔진은 액션(Action) 리스트를 재귀적으로 순회(Recursive walk)하며, 어휘(Vocabulary)를 반영하는 Kind에 대한 switch 문을 실행합니다. 모든 분기(Branch)는 실패한 액션의 이름을 지정할 수 있을 만큼 충분한 컨텍스트(Context)와 함께 에러를 래핑(Wrap)합니다. 워크플로우(Workflow)가 폭발(Blow up)할 때, 세 프레임 아래에서 발생하는 단순한 nil map 패닉(Panic)이 아니라 메시지 안에 스텝 ID(Step ID)가 포함되어 있기를 원하기 때문입니다.
package flow
import (
...
로딩(Loading)은 지루한 부분입니다. os.ReadFile 한 번과 yaml.Unmarshal 한 번이 수행되며, 둘 다 래핑되어 있습니다.
package flow
import (
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기