AI 에이전트 구축 방법: 계획, 도구 사용, 실행

AI 에이전트 구축 방법: 계획, 도구 사용, 실행

AI 에이전트를 구축하는 것은 간단한 아이디어에서 시작합니다. 모델이 행동을 취하고 결과를 관찰한 후 루프를 반복할 수 있는 능력을 부여하는 것입니다. 모델은 목표에 대해 추론하고, 다음에 무엇을 할지 결정하며, 도구를 호출하고, 결과를 받아 작업이 완료될 때까지 계속합니다. 이 읽기-평가-실행(read-evaluate-act) 루프는 단일 단계 도구 호출기든, 코드를 작성하고 실행하며 자체 오류를 수정하는 다중 단계 시스템이든 여러분이 구축할 모든 에이전트의 핵심입니다.

이 튜토리얼에서는 에이전트를 작동시키는 네 가지 구성 요소인 계획, 도구 사용, 코드 실행, 평가를 살펴봅니다. 여러분은 오늘 바로 실행할 수 있는 작동하는 정신 모델과 최소한의 구현을 얻을 수 있습니다.

AI 에이전트를 만드는 요소는 무엇인가

AI 에이전트는 기본 LLM 호출과 한 가지 중요한 점에서 다릅니다. 바로 행동을 취하고 그 결과를 관찰한다는 점입니다.

일반적인 채팅 완성은 텍스트를 생성합니다. 에이전트는 그 텍스트를 사용하여 함수를 호출하고, API를 쿼리하고, 명령을 실행하거나 파일을 읽은 다음, 그 결과를 다음 단계의 입력으로 다시 제공합니다. 이 피드백 루프가 에이전트를 단일 응답으로 답할 수 없는 작업에 유용하게 만듭니다.

세 가지 기능이 에이전트를 정의합니다.

  • 도구 호출 — 구조화된 인수로 정의된 함수를 이름으로 호출
  • 컨텍스트 축적 — 대화가 진행됨에 따라 도구 결과를 앞으로 전달
  • 목표 지향 루핑 — 단일 응답 후가 아닌 중단 조건이 충족될 때까지 계속

시스템이 이 세 가지를 모두 갖추고 있다면 에이전트를 구축하고 있는 것입니다. 컨텍스트 축적이나 루핑이 없다면 도구가 강화된 챗봇입니다.

동일한 아키텍처로 다양한 작업을 처리할 수 있습니다. 검색하고 종합하는 연구 에이전트, 코드를 작성하고 실행하는 코딩 에이전트, 데이터베이스를 쿼리하고 보고서를 생성하는 데이터 에이전트 등. 각 경우 구현 패턴은 거의 동일합니다.

1단계: 계획 — 추론 계층

계획 계층은 모델이 다음에 무엇을 할지 결정하는 곳입니다. 별도의 계획 모델이 필요하지 않습니다. 텍스트를 생성하는 동일한 모델이 목표에 대해 추론하고 각 단계에서 구조화된 작업(도구 호출)을 생성할 수 있습니다.

시스템 프롬프트가 대부분의 작업을 수행합니다. 좋은 계획 프롬프트는 모델에게 다음을 알려줍니다.

  • 어떤 목표를 향해 작업하고 있는지
  • 어떤 도구를 사용할 수 있고 각 도구가 무엇을 하는지
  • 도구를 호출할 때 어떤 형식을 사용해야 하는지
  • 언제 멈출지(종료 조건)

OpenAI 호환인 Novita의 LLM API를 사용하면 다음과 같습니다.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.novita.ai/v3/openai",
    api_key=os.environ["NOVITA_API_KEY"],
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "run_code",
            "description": "Execute Python code and return stdout/stderr.",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {
                        "type": "string",
                        "description": "Python code to execute"
                    }
                },
                "required": ["code"]
            }
        }
    }
]

messages = [
    {
        "role": "system",
        "content": "You are a coding assistant. Use the run_code tool to execute Python and solve tasks. Stop when you have a final answer."
    },
    {
        "role": "user",
        "content": "Find all prime numbers under 50."
    }
]

response = client.chat.completions.create(
    model="meta-llama/llama-3.1-8b-instruct",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

모델은 일반 텍스트 응답 대신 함수 이름과 인수가 포함된 JSON 객체인 도구 호출을 반환합니다. 여러분의 코드는 이를 추출하고 함수를 실행한 후 결과를 messages에 추가하여 루프백합니다.

2단계: 도구 사용 — 모델을 세상에 연결하기

도구는 에이전트가 실제로 수행하는 작업입니다. 도구가 없으면 모델은 텍스트만 생성할 수 있습니다. 도구가 있으면 파일을 읽고, API를 호출하고, 코드를 실행하고, 데이터베이스를 검색하거나 여러분이 정의한 모든 함수를 트리거할 수 있습니다.

도구 정의

각 도구는 함수를 설명하는 JSON 스키마입니다. 모델은 이 스키마를 읽어 호출을 올바르게 포맷합니다. 몇 가지 실용적인 규칙:

  • 설명을 짧고 정확하게 유지하세요. 모델은 매 단계마다 설명을 읽습니다. 장황한 설명은 토큰을 낭비합니다.
  • 매개변수 이름을 함수가 실제로 받아들이는 이름과 일치시키세요. 스키마와 구현 간의 불일치는 조용한 실패를 유발합니다.
  • 구조화된 출력을 반환하세요. 도구가 원시 HTML이나 포맷되지 않은 산문을 반환하면 모델이 이를 파싱하는 데 토큰을 낭비합니다. 깔끔한 문자열이나 명확한 레이블이 있는 JSON을 반환하세요.

도구 호출 루프

모델이 도구 호출을 만든 후, 여러분의 코드는:

  1. 응답에서 도구 이름과 인수를 파싱합니다.
  2. 일치하는 Python 함수를 찾습니다.
  3. 파싱된 인수로 함수를 호출합니다.
  4. role: "tool"로 결과를 messages에 추가합니다.
  5. 다른 완성 요청을 만듭니다.

루프는 모델이 도구 호출 없이 일반 메시지를 반환할 때까지 계속됩니다(종료 조건).

import json

def run_agent_loop(client, model, messages, tools, tool_fns, max_steps=10):
    for _ in range(max_steps):
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        choice = response.choices[0]

        # 완료 — 도구 호출 없음
        if choice.finish_reason == "stop":
            return choice.message.content

        # 도구 호출 실행
        tool_call = choice.message.tool_calls[0]
        fn_name = tool_call.function.name
        fn_args = json.loads(tool_call.function.arguments)

        result = tool_fns[fn_name](**fn_args)

        # 모델의 도구 호출과 결과를 모두 추가
        messages.append(choice.message)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": str(result),
        })

    return "최대 단계에 도달했지만 최종 답변이 없습니다."

이것이 완전한 에이전트 루프입니다. max_steps 가드는 무한 루프를 방지합니다. 모델이 동일한 도구를 반복적으로 호출하는 데 갇히면 루프는 무한히 실행되지 않고 종료됩니다. 다중 도구 조정, 메모리, 스트리밍 등 다른 모든 것은 이 패턴 위에 구축됩니다.

일반적인 도구 패턴

파일 접근: 에이전트의 작업 디렉터리 내에서 파일을 읽고 씁니다. 파일 내용을 문자열로 반환합니다.

웹 검색: 검색 결과를 {title, url, snippet} 객체 목록으로 반환합니다. 스니펫은 짧게 유지합니다.

코드 실행: 격리된 환경에서 코드를 실행하고 stdout/stderr를 반환합니다(다음 섹션에서 다룹니다).

구조화된 데이터 조회: 데이터베이스나 API를 쿼리하고 레코드를 JSON으로 반환합니다. 모델이 요약을 처리합니다.

각 도구는 한 가지 작업을 수행하고 동일한 입력에 대해 결정론적 출력을 반환해야 합니다. 너무 많은 로직을 단일 도구에 패킹하려는 에이전트는 디버그하고 평가하기가 더 어려워집니다.

3단계: 코드 실행 — 안전하게 코드 실행하기

에이전트가 코드를 생성하고 실행한다면 격리된 환경이 필요합니다. 모델이 생성한 코드를 머신에서 직접 실행하는 것은 보안 위험입니다. 모델이 파괴적인 셸 명령, 무한 루프 또는 접근해서는 안 되는 민감한 파일을 읽는 코드를 생성할 수 있습니다.

표준 접근 방식은 샌드박스 입니다. 호스트 시스템에 영향을 주지 않고 에이전트가 자유롭게 코드를 실행할 수 있는 단기 격리된 Linux 환경입니다. Novita의 Agent Sandbox는 정확히 이 사용 사례를 위해 설계되었습니다. 200ms 이내에 시작되고 에이전트가 생성하는 모든 코드를 실행하며 세션이 종료되면 환경을 폐기하는 클라우드 기반 실행 환경입니다.

계획 섹션의 run_code 도구를 샌드박스를 사용하여 구현하는 방법은 다음과 같습니다.

from novita_sandbox.code_interpreter import Sandbox

sandbox = Sandbox.create()

def run_code(code: str) -> str:
    result = sandbox.run_code(code)
    output = ""
    if result.logs.stdout:
        output += "\n".join(result.logs.stdout)
    if result.logs.stderr:
        output += "\nSTDERR: " + "\n".join(result.logs.stderr)
    return output or "(no output)"

에이전트가 run_code를 호출하면 이 함수는 격리된 환경에서 코드를 실행하고 stdout/stderr를 문자열로 반환합니다. 에이전트는 결과를 보고 다음에 무엇을 할지 결정합니다. 버그를 수정하거나, 출력을 계속 사용하거나, 작업이 완료되었다고 선언하는 것입니다.

이 아키텍처는 추론 계층과 실행 계층을 깔끔하게 분리합니다. LLM은 계획을 세우고 도구를 호출하며, 샌드박스는 컨테이너 외부의 어떤 것도 노출하지 않고 코드를 실행합니다.

샌드박스가 처리하는 것

  • 셸 명령(bash, git, python, node)
  • 샌드박스 내 파일 시스템 작업
  • 컨테이너 내 네트워크 요청
  • 비동기 결과 검색을 통한 장기 실행 백그라운드 작업
  • 다중 세션 워크플로를 위한 일시 중지 및 재개

허용되지 않는 것: 호스트 시스템, 다른 샌드박스 또는 명시적으로 전달하지 않은 자격 증명에 대한 접근. 이러한 격리가 모델이 임의 코드를 작성하고 실행하도록 허용하는 것을 안전하게 만듭니다.

4단계: 평가 — 에이전트가 올바른지 알기

평가는 에이전트 개발에서 가장 자주 건너뛰는 단계입니다. 출력을 생성하는 에이전트와 올바른 출력을 생성하는 에이전트는 다릅니다.

세 가지 평가 수준

1. 결정론적 검사

날짜 파싱, 값 계산, 필드 추출과 같이 알려진 정답이 있는 작업의 경우 에이전트 출력에 대한 단위 테스트를 작성합니다. 이러한 테스트는 실행 비용이 저렴하고 회귀를 빠르게 발견합니다.

2. 실행 기반 검사

코딩 에이전트의 경우 가장 신뢰할 수 있는 신호는 코드가 실제로 실행되고 올바른 결과를 생성하는지 여부입니다. 에이전트의 출력을 샌드박스에서 실행하고 stdout을 예상 값과 비교합니다. 이는 코드 텍스트를 비교하는 것보다 강력합니다. 동작을 테스트하기 때문입니다.

def evaluate_code_task(agent_code: str, expected_stdout: str) -> bool:
    result = run_code(agent_code)
    return result.strip() == expected_stdout.strip()

3. LLM-as-judge

요약 작성, 연구 질문에 답변과 같은 개방형 작업의 경우 두 번째 LLM 호출을 사용하여 루브릭에 따라 출력을 평가합니다. 루브릭을 구체적으로 유지하세요. "답변에 작동하는 코드 예제가 포함되었습니까?"가 "답변이 좋습니까?"보다 낫습니다. 모호한 루브릭은 일관되지 않은 점수를 생성합니다.

실패 모드 추적

에이전트는 반복되는 패턴으로 실패합니다.

실패 원인 수정
도구 호출 루프 모델이 동일한 도구를 계속 호출 max_steps 설정; 중간 단계 로깅
인수 불일치 스키마가 구현과 일치하지 않음 도구 인수를 호출 전에 검증
컨텍스트 오버플로 세션이 모델의 컨텍스트 한도를 초과 이전 도구 결과 요약 또는 자르기
환각된 도구 이름 모델이 존재하지 않는 도구 생성 tool_choice: "auto"로 엄격한 도구 목록 사용

평가는 끝에 있는 별도 단계가 아니라 개발의 일부입니다. 에이전트의 기능을 확장할 때마다 새로운 동작에서 회귀를 잡을 수 있는 테스트를 하나 이상 작성하세요.

에이전트 추론을 위한 모델 선택

대부분의 튜토리얼은 에이전트 추론을 위해 폐쇄형 소스 모델을 기본값으로 사용합니다. 프로토타이핑에는 잘 작동하지만, 에이전트가 작업당 20번의 LLM 호출을 할 수 있는 프로덕션에서는 모델 비용과 속도 제한이 실제 제약이 됩니다.

Novita의 LLM API를 통해 호스팅되는 오픈 소스 모델은 에이전트가 실제로 필요로 하는 작업(함수 호출, 구조화된 출력, 다중 단계 추론)에서 점점 폐쇄형 소스 성능과 일치하고 있습니다. Llama 3.3 70BDeepSeek-V3와 같은 모델은 코딩 및 지시 따르기에서 GPT-4 클래스 수준의 벤치마크를 보여주면서 비용은 훨씬 적습니다. 높은 처리량의 에이전틱 워크로드에서 이 차이는 빠르게 누적됩니다.

Novita API는 완전히 OpenAI 호환이므로 폐쇄형 소스 모델에서 오픈 소스 모델로 전환하는 것은 한 줄 변경입니다.

# 이전
model = "gpt-4o"

# 이후 — 동일한 API 계약, 더 낮은 비용
model = "meta-llama/llama-3.3-70b-instruct"

지연 시간에 민감한 에이전트 루프의 경우 meta-llama/llama-3.1-8b-instruct와 같은 더 작은 모델이 단계당 더 빠르게 응답하고 비용이 덜 듭니다. 이는 각 사용자 작업이 많은 순차적 호출을 포함할 때 중요합니다. 실용적인 패턴: 라우팅 및 분류 단계에는 더 빠르고 작은 모델을 사용하고, 더 많은 용량이 필요한 복잡한 추론 단계에는 더 큰 모델을 사용합니다.

Novita 모델 라이브러리에는 특정 에이전틱 하위 작업을 위한 강력한 옵션도 포함되어 있습니다. 이미지 기반 도구 결과를 위한 비전 모델, 검색을 위한 임베딩 모델, 코딩 에이전트를 위한 코드 특화 모델 등 모두 동일한 API 엔드포인트에서 액세스할 수 있습니다.

모든 것을 통합 — 최소한의 작동하는 에이전트

다음은 수학 문제를 받아 Python 코드를 생성하여 해결하고 샌드박스에서 실행한 후 검증된 결과를 반환하는 완전한 에이전트입니다.

import os
import json
from openai import OpenAI
from novita_sandbox.code_interpreter import Sandbox

client = OpenAI(
    base_url="https://api.novita.ai/v3/openai",
    api_key=os.environ["NOVITA_API_KEY"],
)
sandbox = Sandbox.create()

def run_code(code: str) -> str:
    result = sandbox.run_code(code)
    stdout = "\n".join(result.logs.stdout)
    stderr = "\n".join(result.logs.stderr)
    return stdout or stderr or "(no output)"

tool_fns = {"run_code": run_code}

tools = [
    {
        "type": "function",
        "function": {
            "name": "run_code",
            "description": "Execute Python code. Returns stdout or stderr.",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string"}
                },
                "required": ["code"]
            }
        }
    }
]

messages = [
    {
        "role": "system",
        "content": "Solve the task by writing and running Python code. Use run_code to execute it. Stop when you have a verified answer."
    },
    {
        "role": "user",
        "content": "What is the sum of all even Fibonacci numbers under 1,000,000?"
    }
]

for _ in range(10):
    response = client.chat.completions.create(
        model="meta-llama/llama-3.1-8b-instruct",
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )
    choice = response.choices[0]

    if choice.finish_reason == "stop":
        print(choice.message.content)
        break

    tool_call = choice.message.tool_calls[0]
    fn_args = json.loads(tool_call.function.arguments)
    result = tool_fns[tool_call.function.name](**fn_args)

    messages.append(choice.message)
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": result,
    })

sandbox.kill()

이는 약 50줄의 코드로 완전한 에이전트 루프(계획, 도구 호출, 실행, 컨텍스트 축적, 최대 단계 가드, 깔끔한 샌드박스 정리)를 다룹니다. 에이전트는 코드를 작성하고, 실행하고, 출력을 확인한 후 계속하거나 최종 답변을 제공합니다.

확장하려면: tool_fnstools에 더 많은 도구를 추가하고, 더 어려운 작업을 위해 더 큰 모델로 교체하거나, 샌드박스 run_code를 다른 함수(웹 검색, 데이터베이스 쿼리, API 호출)로 대체합니다. 루프는 동일하게 유지됩니다.

FAQ

AI 에이전트와 챗봇의 차이점은 무엇인가요?

챗봇은 텍스트 응답을 생성합니다. 에이전트는 도구 호출, 코드 실행, API 쿼리와 같은 행동을 취하고 그 결과를 사용하여 목표를 향해 계속 추론합니다. 도구 호출과 피드백 루프의 존재가 에이전트를 만드는 요소입니다.

AI 에이전트를 구축하려면 LangChain과 같은 프레임워크가 필요한가요?

아니요. 핵심 에이전트 루프는 모든 OpenAI 호환 API에 대해 직접 구현할 수 있을 만큼 간단합니다. 프레임워크는 복잡한 오케스트레이션(다중 에이전트 시스템, 영구 메모리, 스트리밍 출력)에 가치를 더하지만, 디버깅을 더 어렵게 만들 수 있는 추상화 계층을 추가합니다. 원시 API로 시작한 다음, 프레임워크가 제공하는 것이 진정으로 필요할 때만 추가하세요.

에이전트가 위험한 코드를 실행하는 것을 어떻게 방지하나요?

샌드박스 실행 환경을 사용하세요. 에이전트의 코드는 호스트 시스템이나 다른 프로세스에 접근할 수 없는 격리된 컨테이너 내에서 실행됩니다. 세션이 종료되면 샌드박스는 폐기되므로 에이전트가 만든 모든 변경 사항은 해당 컨테이너로 범위가 제한됩니다.

오픈 소스 모델이 함수 호출을 안정적으로 처리할 수 있나요?

네 — 모델 크기에 따른 주의 사항이 있습니다. Llama 3.3 70B, DeepSeek-V3, Qwen 2.5 72B는 모두 구조화된 도구 호출을 잘 처리합니다. 더 작은 모델(7B–8B)은 간단하고 잘 정의된 도구 스키마에 대해 작동하지만, 모호한 지시나 복잡한 다중 단계 추론 체인에는 어려움을 겪을 수 있습니다. 프로덕션 에이전트의 경우 모델을 결정하기 전에 실제 작업 분포에서 테스트하세요.

에이전트는 작업당 몇 단계를 수행해야 하나요?

하드 캡을 설정하세요. 대부분의 작업에 일반적으로 10~20단계입니다. 중간 단계를 로깅하여 에이전트가 정확히 무엇을 했는지 확인하고 실패를 디버깅할 수 있습니다. 일관되게 더 많은 단계가 필요한 작업은 일반적으로 루프 제한을 확장하기보다는 하위 에이전트나 하위 작업으로 나누는 것이 좋습니다.

추천 문서