|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "OpenAI Agent SDK - 간단하게 Agent를 구축하자" |
| 4 | +subtitle: "많은 Agent 툴을 사용해보고 느낀점" |
| 5 | +feature-img: "assets/img/2025-10-08-openai-agent-sdk/1737661646125-download6.webp" |
| 6 | +tags: [LLMOps] |
| 7 | +--- |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +2025년이 되면서 뉴스 피드를 보면 하루가 멀다 하고 "Agent"라는 단어가 등장한다. n8n·Make 같은 노코드 워크플로 툴부터, 구글의 ADK, 랭체인의 Agent, 그리고 오늘 이야기할 OpenAI Agent SDK까지 선택지가 급격히 늘어났다. 그 중에서도 OpenAI가 Agent Builder를 공개하며 Agent SDK와의 연동을 시작하고, 해당 SDK는 더욱 주목 받을 수 있을 것 같은 분위기다. Builder에서 만든 플로우를 그대로 코드로 가져와 버전 관리·배포까지 이어갈 수 있기 때문이다. |
| 12 | + |
| 13 | +> Agent Builder 화면에서 **Code** 버튼을 누르면 바로 Agent SDK 코드가 출력된다. |
| 14 | +
|
| 15 | + |
| 16 | + |
| 17 | +물론 Google ADK나 Microsoft Autogen처럼 다른 접근을 제공하는 툴도 많다. 결국 팀의 요구사항과 사용 중인 인프라에 따라 최적의 선택은 달라질 것이다. 이 글에서는 OpenAI Agent SDK가 어떤 지향점을 갖고 있는지, 어떤 기능이 돋보이는지 정리해 본다. |
| 18 | + |
| 19 | +먼저 눈에 들어온 장점은 다음과 같다. |
| 20 | + |
| 21 | +- 최소한의 추상화로 학습 곡선이 낮고, OpenAI API에 익숙한 사람이라면 바로 감을 잡을 수 있다. |
| 22 | +- 멀티 에이전트·핸드오프·가드레일·세션 등 핵심 구성 요소만 깔끔하게 제공한다. |
| 23 | +- 기본으로 트레이싱이 켜져 있어 실행 이력을 추적하기 쉽다. |
| 24 | +- Agent Builder와 코드를 오가며 실험-배포 사이클을 빠르게 돌릴 수 있다. |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +### Agent SDK 알아보기 |
| 29 | + |
| 30 | +일단, Agent SDK는 과거 베타 단계였던 Swarm에서 출발해 정식 제품군으로 편입된 버전이다. 공식 문서에 명시된 [두 가지 원칙](https://openai.github.io/openai-agents-python)이 특히 인상 깊다. |
| 31 | + |
| 32 | +1. 사용할 가치가 있을 만큼 충분한 기능을 제공하되, 빠르게 배울 수 있도록 기본 구성 요소는 최소화한다. |
| 33 | +2. 기본 설정만으로도 훌륭하게 동작하지만, 원하는 대로 커스터마이즈할 수 있게 열린 구조를 유지한다. |
| 34 | + |
| 35 | +결국 "가볍지만 필요한 건 다 있는" 도구가 되겠다는 선언처럼 읽힌다. 실제로 SDK가 다루는 범위는 에이전트, 핸드오프, 가드레일, 세션, 트레이싱 정도로 명확하게 잘라 놓았다. 덕분에 복잡한 설정 없이도 에이전트 시스템의 뼈대를 바로 세울 수 있다. |
| 36 | + |
| 37 | + |
| 38 | + |
| 39 | +가장 기본이 되는 코드를 보면 심플함이 더 분명해진다. 에이전트를 선언하는 데 아래의 코드면 충분하다. 오히려 GPT API를 직접 다룰 때보다 선언적이다. |
| 40 | + |
| 41 | +```python |
| 42 | +from agents import Agent, Runner |
| 43 | + |
| 44 | +agent = Agent(name="Assistant", instructions="You are a helpful assistant") |
| 45 | + |
| 46 | +result = Runner.run_sync(agent, "Write a haiku about recursion in programming.") |
| 47 | +print(result.final_output) |
| 48 | +``` |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | + |
| 53 | + |
| 54 | +### 빠르게 시작하기 |
| 55 | + |
| 56 | +Agent SDK를 처음 실험해 볼 때 자주 쓰는 흐름을 정리했다. |
| 57 | + |
| 58 | +설치는 openai-agents 라이브러리를 설치하는 것이 전부이다. |
| 59 | +```bash |
| 60 | +pip install openai-agents |
| 61 | +``` |
| 62 | + |
| 63 | +### Handoffs |
| 64 | + |
| 65 | +멀티 에이전트를 엮는 가장 쉬운 방법이 핸드오프다. 각 에이전트를 선언하고 `handoffs` 배열에 넘겨 주면 라우팅을 담당하는 에이전트가 자연스럽게 분기해 준다. 다른 툴에서 오케스트레이션·라우터라고 부르는 개념과 유사하지만, SDK에서는 의도적으로 구성을 단순화했다. |
| 66 | + |
| 67 | +```python |
| 68 | +spanish_agent = Agent( |
| 69 | + name="Spanish agent", |
| 70 | + instructions="You only speak Spanish.", |
| 71 | +) |
| 72 | + |
| 73 | +english_agent = Agent( |
| 74 | + name="English agent", |
| 75 | + instructions="You only speak English", |
| 76 | +) |
| 77 | + |
| 78 | +triage_agent = Agent( |
| 79 | + name="Triage agent", |
| 80 | + instructions="Handoff to the appropriate agent based on the language of the request.", |
| 81 | + handoffs=[spanish_agent, english_agent], |
| 82 | +) |
| 83 | + |
| 84 | + |
| 85 | +async def main(): |
| 86 | + result = await Runner.run(triage_agent, input="Hola, ¿cómo estás?") |
| 87 | + print(result.final_output) |
| 88 | +``` |
| 89 | + |
| 90 | +위 예시는 triage_agent가 입력 언어에 따라 스페인어·영어 에이전트로 작업을 넘겨준다. `Runner.run_sync` 대신 비동기 `Runner.run`을 사용하면 체이닝 도중 외부 API 호출이나 툴 실행을 기다릴 때도 자연스럽게 확장된다. 실제 서비스에서는 핸드오프 체인을 깊게 가져가기보다는, 언어·도메인·권한과 같은 명확한 기준으로만 분기하는 편이 유지보수에 유리했다. |
| 91 | + |
| 92 | +물론 다른 툴들처럼, [멀티에이전트를 툴](https://github.com/openai/openai-agents-python/blob/main/examples/agent_patterns/agents_as_tools.py)들처럼 사용하는 방법도 있으니 참고하면 좋을 듯 하다. |
| 93 | + |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | + |
| 98 | +### Tools |
| 99 | + |
| 100 | +Agent SDK가 제공하는 툴 시스템은 함수 한두 개만으로 정의할 수 있을 만큼 단순하다. 기본적인 형식은 아래와 같다. |
| 101 | + |
| 102 | +```python |
| 103 | +from agents import Agent, Runner, function_tool |
| 104 | + |
| 105 | +@function_tool |
| 106 | +def get_weather(city: str) -> str: |
| 107 | + return f"The weather in {city} is sunny." |
| 108 | + |
| 109 | + |
| 110 | +agent = Agent( |
| 111 | + name="Hello world", |
| 112 | + instructions="You are a helpful agent.", |
| 113 | + tools=[get_weather], |
| 114 | +) |
| 115 | +``` |
| 116 | + |
| 117 | +`@function_tool` 데코레이터로 함수를 감싸면 JSON 스키마가 자동으로 생성되고, `tools` 파라미터에 그대로 넘겨 쓸 수 있다. GPT API와 동일하게, `tool_choice="required"`처럼 호출 정책을 조정해 세밀한 통제가 가능하다. |
| 118 | + |
| 119 | +Agent SDK에서 제공하는 함수형 툴 외에도 MCP 같은 외부 툴 프로토콜을 붙일 수 있다. |
| 120 | + |
| 121 | +```python |
| 122 | +async def main(): |
| 123 | + async with MCPServerStdio( |
| 124 | + params={ |
| 125 | + "command": "uv", |
| 126 | + "args": ["run", "-m", "openai_agent_sdk.mcp_server"], |
| 127 | + }, |
| 128 | + ) as server: |
| 129 | + agent = Agent( |
| 130 | + name="test", |
| 131 | + instructions="test", |
| 132 | + model=settings.OPENAI_MODEL, |
| 133 | + mcp_servers=[server], |
| 134 | + ) |
| 135 | + |
| 136 | + result = await Runner.run(agent, "삼성전자 주가 얼마야?") |
| 137 | + print(result) |
| 138 | +``` |
| 139 | + |
| 140 | +위 예시는 MCP 서버를 stdio 방식으로 붙인 뒤 해당 리소스를 툴로 노출하는 과정이다. 테스트해보면 리소스 조회(`list`·`get`)부터 실행까지 무리 없이 지원해 MCP 생태계와의 궁합이 매우 좋다는 느낌을 받았다. |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | +### Tracing |
| 145 | + |
| 146 | +Agent SDK는 에이전트가 남긴 메시지와 툴 호출, 핸드오프 이력까지 기본값으로 [OpenAI Dashboard - Logs](https://platform.openai.com/logs)에 적재한다. 별도의 옵저버빌리티 스택을 붙이지 않아도 즉시 실행 흐름을 시각화할 수 있다는 점이 강력하다. |
| 147 | + |
| 148 | +다만 모든 트래픽이 OpenAI로 전송되는 만큼 민감한 데이터에 대해서는 사전에 정책을 정해 두는 편이 좋다. 로그 수집을 끄고 싶다면 라이브러리를 import하기 전에 아래와 같이 환경 변수를 지정하면 된다. |
| 149 | + |
| 150 | +```python |
| 151 | +os.environ["OPENAI_AGENTS_DISABLE_TRACING"] = "1" |
| 152 | +``` |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +다음과 같이 트레이싱이 된다고 보면 된다. |
| 157 | + |
| 158 | + |
| 159 | + |
| 160 | +OpenAI 모델이 아니더라도 트레이싱은 유지할 수 있다. LiteLLM 같은 어댑터로 다른 모델을 붙이고, 별도로 발급한 OpenAI API Key를 `set_tracing_export_api_key`에 전달하면 된다. 비용 없이 로그만 수집하는 용도로 사용할 수 있다는 뜻이다. |
| 161 | + |
| 162 | +```python |
| 163 | +import os |
| 164 | +from agents import set_tracing_export_api_key, Agent, Runner |
| 165 | +from agents.extensions.models.litellm_model import LitellmModel |
| 166 | + |
| 167 | +tracing_api_key = os.environ["OPENAI_API_KEY"] |
| 168 | +set_tracing_export_api_key(tracing_api_key) |
| 169 | + |
| 170 | +model = LitellmModel( |
| 171 | + model="your-model-name", |
| 172 | + api_key="your-api-key", |
| 173 | +) |
| 174 | + |
| 175 | +agent = Agent( |
| 176 | + name="Assistant", |
| 177 | + model=model, |
| 178 | +) |
| 179 | +``` |
| 180 | + |
| 181 | + |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | +### 가드레일 |
| 186 | + |
| 187 | +에이전트가 점점 복잡해질수록 정책을 코드로 강제할 방법이 필요하다. Agent SDK는 가드레일을 장착하는 방식을 명확히 두 가지로 나눈다. |
| 188 | + |
| 189 | +1. 입력 가드레일: 최초 사용자 입력을 검사해 필요 시 실행을 중단한다. |
| 190 | +2. 출력 가드레일: 최종 에이전트 응답을 검토해 민감한 내용이 있는지 확인한다. |
| 191 | + |
| 192 | +핵심은 가드레일도 결국 하나의 에이전트 혹은 함수라는 점이다. 입력 가드레일을 예로 들면, 사용자가 입력한 내용을 가드레일 함수에 전달해 차단(tripwire) 여부를 결정한다. |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | +예시 코드는 다음과 같이 소개하고 있다. |
| 197 | + |
| 198 | +```python |
| 199 | +from pydantic import BaseModel |
| 200 | +from agents import ( |
| 201 | + Agent, |
| 202 | + GuardrailFunctionOutput, |
| 203 | + InputGuardrailTripwireTriggered, |
| 204 | + RunContextWrapper, |
| 205 | + Runner, |
| 206 | + TResponseInputItem, |
| 207 | + input_guardrail, |
| 208 | +) |
| 209 | + |
| 210 | +class MathHomeworkOutput(BaseModel): |
| 211 | + is_math_homework: bool |
| 212 | + reasoning: str |
| 213 | + |
| 214 | +guardrail_agent = Agent( |
| 215 | + name="Guardrail check", |
| 216 | + instructions="Check if the user is asking you to do their math homework.", |
| 217 | + output_type=MathHomeworkOutput, |
| 218 | +) |
| 219 | + |
| 220 | + |
| 221 | +@input_guardrail |
| 222 | +async def math_guardrail( |
| 223 | + ctx: RunContextWrapper[None], agent: Agent, input: str | list[TResponseInputItem] |
| 224 | +) -> GuardrailFunctionOutput: |
| 225 | + result = await Runner.run(guardrail_agent, input, context=ctx.context) |
| 226 | + |
| 227 | + return GuardrailFunctionOutput( |
| 228 | + output_info=result.final_output, |
| 229 | + tripwire_triggered=result.final_output.is_math_homework, |
| 230 | + ) |
| 231 | + |
| 232 | + |
| 233 | +agent = Agent( |
| 234 | + name="Customer support agent", |
| 235 | + instructions="You are a customer support agent. You help customers with their questions.", |
| 236 | + input_guardrails=[math_guardrail], |
| 237 | +) |
| 238 | + |
| 239 | +async def main(): |
| 240 | + # This should trip the guardrail |
| 241 | + try: |
| 242 | + await Runner.run(agent, "Hello, can you help me solve for x: 2x + 3 = 11?") |
| 243 | + print("Guardrail didn't trip - this is unexpected") |
| 244 | + |
| 245 | + except InputGuardrailTripwireTriggered: |
| 246 | + print("Math homework guardrail tripped") |
| 247 | +``` |
| 248 | + |
| 249 | +위 코드에서 `math_guardrail` 함수는 별도의 에이전트를 호출해 "수학 숙제를 대신 풀어 달라는 요청인지" 판단하고, 참일 경우 `InputGuardrailTripwireTriggered` 예외를 발생시킨다. Guardrail 로직을 별도 에이전트로 분리하면 정책을 재사용하거나 다른 프로젝트에 쉽게 이식할 수 있다는 장점이 있다. 출력 가드레일 역시 `@output_guardrail` 데코레이터로 동일한 패턴을 구현할 수 있다. |
| 250 | + |
| 251 | + |
| 252 | + |
| 253 | +### 세션과 상태 관리 |
| 254 | + |
| 255 | +에이전트를 실서비스에 붙이려면 대화 맥락과 사용자별 상태를 꾸준히 보존해야 한다. Agent SDK는 `Session` 개념을 제공해 이런 요구를 깔끔하게 분리한다. 해당 내용은 다루지는 않지만 다른 툴들에서 지원하는 메모리, db등 지원하고 있어서 무리없이 사용할 수 있을 것 같다. |
| 256 | + |
| 257 | + |
| 258 | + |
| 259 | +### 단점 |
| 260 | + |
| 261 | +사실 다른 툴들과 비교했을 때, 크게 장,단점이 느껴지지 않는 툴이라고 생각한다. 굳이 뽑자면 OpenAI 생태계와의 결합이 강하다. LiteLLM을 통해 다른 모델을 붙일 수 있지만, 결국 트레이싱·권한 관리는 OpenAI 계정에 의존하고, 기본 트레이싱이 OpenAI Dashboard로만 향한다. 사내 모니터링을 이미 쓰고 있다면 별도 파이프라인을 구축해야한다. |
| 262 | + |
| 263 | + |
| 264 | + |
| 265 | +### 마치며 |
| 266 | + |
| 267 | +Agent SDK는 MS Autogen처럼 심플함을 추구하면서도, OpenAI Builder와의 연동을 통해 실험-배포 루프를 짧게 만들어 줄 수 있을 것 같다는 생각이 든다. 무엇보다 필요한 구성 요소만 남겨 학습 비용을 크게 줄여 준다는 점이 마음에 든다. |
| 268 | + |
| 269 | + |
| 270 | + |
| 271 | +추가 예제와 패턴은 [openai-agents-python](https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns) 저장소에서도 자세히 볼 수 있다. 팀의 요구에 맞는 아키텍처를 직접 실험해 보길 권한다. |
0 commit comments