단 하나의 명령어도 직접 입력하지 않고, 컴퓨터와 대화하는 것만으로 Bash 터미널 작업을 수행할 수 있다면 어떨까요? NVIDIA Nemotron-Nano-v2 오픈 모델을 사용하면, 최소한의 의존성만으로 약 200줄의 Python 코드를 작성해 1시간 이내에 자연어 Bash 에이전트를 바닥부터 손쉽게 구축할 수 있습니다.
본 게시물은 핵심 구성요소와 고려사항을 단계별로 안내하여 누구나 쉽게 따라 할 수 있도록 구성되었습니다. 전체 과정을 처음부터 직접 구현해 보는 것은 물론, LangGraph를 활용해 구조를 한층 더 간소화하는 방법까지 함께 소개합니다.
모델 및 코드 바로 가기
- 🧠 HuggingFace의 NVIDIA Nemotron Nano v2
- ☁️ 클라우드 엔드포인트: OpenRouter, build.nvidia.com
- 🛠️ GitHub의 에이전트 코드
사전 요구사항
- NVIDIA Nemotron Nano 9B v2 (로컬 또는 클라우드 배포)
- 로컬 배포 시 필요 사항:
- 약 20GB의 디스크 공간
- 최소 24GB VRAM을 갖춘 NVIDIA GPU
- 로컬 배포 시 필요 사항:
- Ubuntu, Mac OS, 또는 WSL(Windows Subsystem for Linux)과 같은 Bash를 지원하는 운영 체제
- Python v3.10 이상 환경 및 다음 패키지 설치:
- 여유 시간 1시간
최종 결과물: 컴퓨터 사용 에이전트
이 튜토리얼을 통해 구축할 에이전트와의 실제 상호작용 예시입니다.
['/home/sandbox' 🙂] Make a new directory called "system-info," then create a file called "info.txt" and populate it with current system config (disk space, memory), then briefly summarize the contents.
▶️ Execute 'mkdir system-info'? [y/N]: y
▶️ Execute 'touch system-info/info.txt'? [y/N]: y
▶️ Execute 'df -h >> system-info/info.txt'? [y/N]: y
▶️ Execute 'free -h >> system-info/info.txt'? [y/N]: y
▶️ Execute 'cat system-info/info.txt'? [y/N]: y
[🤖] Here's a summary of the system information in `system-info/info.txt`:
**Disk Space**
- Total: 984 GB (`overlay`), 2.6 TB (`/home/sandbox`)
- Used: ~518 GB (56%) on `/`, ~2.1 TB (83%) on `/home/sandbox`
- Available: ~416 GB (42%) on `/`, ~432 GB (17%) on `/home/sandbox`
**Memory**
- Total: 62 GB RAM
- Used: 20 GB
- Free: 14 GB
- Swap: 63 GB available (0 GB used)
Let me know if you'd like to refine this further.
범례: 🙂: 사용자 메시지 🤖: 에이전트 응답 ▶️: 명령어 실행 확인
챗봇에서 AI 에이전트로
요청-응답 방식으로 작동하는 챗봇과 달리, AI 에이전트는 고수준 목표를 입력받아 그 목표를 달성하기 위해 자율적으로 추론하고 계획하며 작업을 실행하는 시스템입니다. 이 과정의 핵심 기술은 도구 호출(tool calling)(함수 호출이라고도 함)입니다. 텍스트만으로 응답하는 것에 그치지 않고, 에이전트는 외부 도구나 API를 직접 호출하여 실제 행동을 수행하고, 결과를 확인하며, 다음 단계를 계획할 수 있습니다.
이번 가이드에서 구축할 Bash 컴퓨터 사용 에이전트는 에이전트의 작동 방식을 명확히 보여주는 좋은 사례입니다. 사용자가 고수준의 지시를 내리면, 에이전트는 도구 호출을 통해 실행할 Bash 명령어를 스스로 판단하고 결정합니다. 약 200줄의 코드로 바닥부터 구현할 수 있을 만큼 간결하지만, 대규모 시스템에서 쓰이는 고도화된 에이전트의 기본 원리를 그대로 보여줍니다.
모든 현대 에이전트의 핵심은 사용자 의도를 추론하고 이를 구체적인 행동으로 변환하는 거대 언어 모델(LLM)입니다. 복잡한 과제를 완수하기 위해서 LLM은 효율성과 빠른 반응성, 그리고 뛰어난 추론 능력을 갖추어야 합니다. NVIDIA Nemotron Nano 9B v2는 이러한 요구사항을 완벽히 충족하는 소형 모델입니다. 설정이 간단하면서도 강력한 추론 성능을 자랑하며, 빠른 응답 속도로 사용자 경험을 원활하게 유지해 줍니다. 이러한 특징 덕분에 본 가이드에서 구축할 경량 에이전트에 매우 적합합니다.
AI 에이전트의 네 가지 주요 구성요소에 대한 기초 내용은 이 블로그에서 확인하실 수 있습니다.
핵심 고려사항
이제 에이전트 구축을 위한 몇 가지 핵심 고려사항을 하나씩 알아보겠습니다.
- 도구 호출을 통한 Bash 사용: 에이전트가 직접 명령어를 실행하고 그 결과(성공·실패 여부, 터미널 출력 내용 등)를 받아볼 수 있도록 Bash CLI를 하나의 ‘도구’로 연결해 주어야 합니다. 이때 현재 작업 디렉터리를 계속 추적하는 것이 중요합니다. 에이전트가 파일 시스템을 자유롭게 탐색하고, 각 Bash 명령어를 올바른 위치에서 실행할 수 있어야 하기 때문입니다.
- 명령어 안전성: 에이전트가 위험하거나 시스템을 파괴할 수 있는 명령어를 실행하지 못하도록 방지해야 합니다. 이를 위해
ls,cat,grep처럼 안전하고 예측 가능한 명령어만 실행할 수 있도록 ‘허용 목록(Whitelist)’을 적용합니다. 이에 더해 최종 확인 단계도 도입합니다. 어떤 명령어든 실제로 실행하기 전에 사용자에게 승인을 요청하는 방식입니다. 이러한 ‘인간 참여(Human-in-the-Loop)’ 패턴을 활용하면 사용자가 터미널에서 실행되는 모든 과정을 완벽하게 제어할 수 있습니다. - 오류 처리: 신뢰할 수 있는 에이전트 시스템을 구축하려면 항상 실패 상황에 대비해야 합니다. Bash 에이전트의 경우, 잘못된 구문이나 누락된 파일, 예상치 못한 출력 등으로 인해 명령어가 실패하는 일이 빈번하게 일어납니다. 에이전트는 이러한 오류 메시지를 단순히 출력하는 데 그치지 않고, 에러를 스스로 포착하고 해석하여 올바른 다음 행동을 판단할 수 있어야 합니다.
시스템 구성요소
고려사항을 바탕으로 하면 아키텍처는 매우 단순해집니다. 시스템에는 두 가지 주요 구성요소가 있습니다.
- Bash 클래스: Python의
subprocess모듈을 감싸는 경량 래퍼(wrapper)로, 작업 디렉터리를 관리하고 명령어 허용 목록을 적용하며 명령어를 실행하고 실행 결과(또는 오류)를 에이전트에 반환합니다. - 에이전트: NVIDIA Nemotron 모델을 사용하여 사용자 의도를 파악하고 행동 방식을 결정하는 동시에 대화 전반에 걸쳐 컨텍스트를 유지합니다. 에이전트의 동작은 정교하게 설계된 시스템 프롬프트에 의해 안내되며, 이 프롬프트는 경계를 설정하고 Bash 어시스턴트로서의 역할을 정의하며 허용된 명령어를 알려줍니다.
아래 그림은 시스템의 아키텍처 다이어그램을 보여줍니다. 워크플로우는 다음과 같습니다.
- 사용자가 디렉터리 변경, 파일 복사, 문서 내용 검사 등의 고수준 지시를 내립니다.
- Nemotron이 요청을 해석하고 구체적인 단계로 분류하여 명령어 실행이 필요할 때 Bash 클래스를 사용합니다. 일부 작업은 실행이 전혀 필요 없을 수도 있고, 여러 명령어가 필요할 수도 있습니다. 각 실행 후 모델은 출력을 받고 다음 단계를 결정하거나 중단 여부를 판단합니다.
- 성공적으로 완료되거나 오류로 인해 중단된 경우, 에이전트는 결과를 사용자에게 반환하고 다음 지시를 기다립니다.

먼저 두 구성요소를 처음부터 구현한 다음, 복잡한 설정을 간소화할 수 있도록 LangGraph로 두 요소를 매끄럽게 연결하는 방법을 안내해 드리겠습니다.
Bash 클래스
허용 명령어 목록과 현재 작업 디렉터리를 저장하는 간단한 클래스를 생성합니다. 이 클래스의 요약 코드 스니펫은 다음과 같습니다.
class Bash:
"""
An implementation of a tool that executes Bash commands
"""
def __init__(self, cwd: str, allowed_commands: List[str]):
self.cwd = cwd # The current working directory
self._allowed_commands = allowed_commands # Allowed commands
def exec_bash_command(self, cmd: str) -> Dict[str, str]:
"""
Execute the bash command after getting confirmation from the user
"""
if cmd:
# Check the allowlist
allowed = True
for cmd_part in self._extract_commands(cmd):
if cmd_part not in self._allowed_commands:
allowed = False
break
if not allowed:
return {"error": "Parts of this command were not in the allowlist."}
return self._run_bash_command(cmd)
return {"error": "No command was provided"}
def to_json_schema(self) -> Dict[str, Any]:
"""
Convert the function signature to a JSON schema for LLM tool calling.
"""
return {
"type": "function",
"function": {
"name": "exec_bash_command",
"description": "Execute a bash command and return stdout/stderr and the working directory",
"parameters": {
"type": "object",
"properties": {
"cmd": {
"type": "string",
"description": "The bash command to execute"
}
},
"required": ["cmd"],
},
},
}
def _run_bash_command(self, cmd: str) -> Dict[str, str]:
"""
Runs the bash command and catches exceptions (if any).
"""
stdout = ""
stderr = ""
new_cwd = self.cwd
try:
# Wrap the command so we can keep track of the working directory.
wrapped = f"{cmd};echo __END__;pwd"
result = subprocess.run(
wrapped, shell=True, cwd=self.cwd,
capture_output=True, text=True,
executable="/bin/bash"
)
stderr = result.stderr
# Find the separator marker
split = result.stdout.split("__END__")
stdout = split[0].strip()
# If no output/error at all, inform that the call was successful.
if not stdout and not stderr:
stdout = "Command executed successfully, without any output."
# Get the new working directory, and change it
new_cwd = split[-1].strip()
self.cwd = new_cwd
except Exception as e:
stdout = ""
stderr = str(e)
return {"stdout": stdout, "stderr": stderr, "cwd": new_cwd}
이 클래스는 두 가지 공개 함수를 제공합니다.
exec_bash_command(cmd: str) -> Dict[str, str]: 에이전트가 명령어를 실행하기 위해 호출하는 함수입니다.stdout,stderr, 업데이트된 작업 디렉터리를 포함한 딕셔너리를 반환하거나, 명령어가 유효하지 않거나 허용되지 않는 경우 오류를 반환합니다. 이 신호를 통해 에이전트는 문제가 발생했을 때 적절히 대응할 수 있습니다.to_json_schema(self) -> Dict[str, Any]: LLM에게 이 도구의 사용 방법을 알려주는 데 사용됩니다(LangGraph에서는 필요하지 않음).
실행 전에 함수는 허용 목록과 명령어를 비교하여 검증합니다. 실제 실행은 내부적으로 Python의 subprocess.run()을 호출하는 비공개 함수 _run_bash_command()에서 처리됩니다. 모든 실패 상황을 적절히 처리할 수 있도록 예외 처리 블록이 포함되어 있습니다. 에이전트가 cd 명령어를 사용하는 등 디렉터리가 변경되는 상황을 추적하기 위해 모든 명령어 뒤에 고유한 텍스트 마커와 pwd를 추가합니다. 실행이 끝나면 출력 결과에서 이 마커를 찾아 새로운 작업 디렉터리를 추출하며, 실행 결과와 현재 작업 디렉터리를 에이전트에 반환하기 전에 도구의 상태를 업데이트합니다.
에이전트
에이전트 구성을 위해 Nemotron을 추론 엔진으로 초기화하고 exec_bash_command를 명령어 실행을 위한 호출 가능 도구로 등록합니다. 모델의 구체적인 행동은 Bash 어시스턴트로서의 역할을 정의하고 허용 명령어를 나열하며 사용자를 언제 어떻게 지원하거나 도구 호출을 실행할지 안내하는 시스템 프롬프트(아래 참조)를 바탕으로 결정됩니다. Bash 클래스가 허용 목록을 적용하는 것과 동시에 프롬프트가 이 규칙을 한 번 더 강조해 주는데, 이는 모델이 지침을 벗어나지 않도록 제어하는 좋은 방법입니다. 또한 프롬프트는 /think 플래그를 사용하여 사고 모드를 활성화하고 모델의 추론 능력을 높여줍니다.
SYSTEM_PROMPT = f"""/think
You are a helpful Bash assistant with the ability to execute commands in the shell.
You engage with users to help answer questions about bash commands, or execute their intent.
If user intent is unclear, keep engaging with them to figure out what they need and how to best help
them. If they ask question that are not relevant to bash or computer use, decline to answer.
When a command is executed, you will be given the output from that command and any errors. Based on
that, either take further actions or yield control to the user.
The bash interpreter's output and current working directory will be given to you every time a
command is executed. Take that into account for the next conversation.
If there was an error during execution, tell the user what that error was exactly.
You are only allowed to execute the following commands:
{LIST_OF_ALLOWED_COMMANDS}
**Never** attempt to execute a command not in this list. **Never** attempt to execute dangerous commands
like `rm`, `mv`, `rmdir`, `sudo`, etc. If the user asks you to do so, politely refuse.
When you switch to new directories, always list files so you can get more context.
"""
에이전트 루프 (처음부터 구현)
에이전트 루프를 구축하는 과정은 간단합니다. OpenAI 클라이언트를 초기화하고, 메모리와 상태 관리 역할을 할 대화 기록을 유지합니다. 루프 내부의 동작 방식은 다음과 같습니다.
- 사용자 입력을 받아 시스템 프롬프트와 함께 모델에 전송합니다.
- 모델의 응답을 받아 대화 기록에 저장하고 도구 호출 여부를 확인합니다.
- 도구 호출이 있으면 사용자에게 실행 확인을 요청합니다. 승인 시
exec_bash_command()를 실행하고 결과를 반환하여 다음 응답을 받습니다. 거부 시 모델에 알립니다. - 도구 호출이 없으면 모델의 응답을 표시하고 사용자에게 제어권을 반환합니다.
- 도구 호출이 있으면 사용자에게 실행 확인을 요청합니다. 승인 시
- 애플리케이션이 종료될 때까지 이 사이클이 반복됩니다.
코드를 깔끔하게 유지하기 위해 대화 기록 저장 기능을 담당하는 Messages 클래스와, 클라이언트를 통해 모델에 요청을 보내고 응답을 받는 기능을 담당하는 LLM 클래스로 추상화를 정의합니다. 이러한 구조적 추상화 덕분에 전체 에이전트 루프를 간결하고 직관적으로 구성할 수 있습니다.
bash = Bash(...)
# The model
llm = LLM(...)
# The conversation history, with the system prompt
messages = Messages(SYSTEM_PROMPT)
# The main agent loop
while True:
# Get user message.
user = input(f"['🙂] ").strip()
messages.add_user_message(user)
# The tool-call/response loop
while True:
response, tool_calls = llm.query(messages, [bash.to_json_schema()])
# Add the response to the context
messages.add_assistant_message(response)
# Process tool calls
if tool_calls:
for tc in tool_calls:
function_name = tc.function.name
function_args = json.loads(tc.function.arguments)
# Ensure it's calling the right tool
if function_name != "exec_bash_command" or "cmd" not in function_args:
tool_call_result = json.dumps({"error": "Incorrect tool or function argument"})
else:
if confirm_execution("cmd"):
tool_call_result = bash.exec_bash_command(function_args["cmd"])
else:
tool_call_result = {"error": "The user declined the execution of this command."}
messages.add_tool_message(tool_call_result, tc.id)
else:
# Display the assistant's message to the user (without the thinking part).
print(f"\n[🤖] {response.strip()}")
break
내부 while 루프는 에이전트가 주어진 작업을 완료하는 과정에서 여러 번 도구를 호출해야 할 수 있기 때문에 필수적입니다. 이는 그림 1의 2단계에 해당하는 과정입니다.
보너스: 에이전트 루프 (LangGraph 사용)
LangGraph를 사용하면 에이전트 루프가 훨씬 더 간단해집니다. 이 라이브러리의 [create_react_agent()]를 사용하면 루프를 관리하고 모델, 도구, 대화 상태를 연결하며 도구 호출과 결과 전달을 라이브러리가 자동으로 처리하도록 할 수 있습니다. 또한 수동으로 예외 코드를 작성하는 대신 제어된 흐름 내에서 에이전트가 실패나 재시도에 대응할 수 있어 오류 처리가 더욱 체계적으로 바뀝니다. 바닥부터 직접 구현한 버전과 마찬가지로 시스템 프롬프트가 Bash 어시스턴트의 역할을 정의하고 안전한 명령어 실행을 강제하는 한편, 작은 헬퍼 함수가 인간 참여(Human-in-the-loop) 확인을 위해 bash.exec_bash_command()를 감싸주는 구조입니다. 이 최소한의 설정만으로도 사용자의 의도를 정확히 파악하고, 올바른 도구를 호출하며, 그 결과를 대화형으로 반환하는 완벽한 기능의 에이전트가 완성됩니다.
요약된 코드 스니펫은 다음과 같습니다.
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain_openai import ChatOpenAI
class ExecOnConfirm:
"""
A wrapper around the Bash class to implement human-in-the-loop
"""
def __init__(self, bash: Bash):
self.bash = bash
def _confirm_execution(self, cmd: str) -> bool:
"""Ask the user whether the suggested command should be executed."""
return input(f" ▶️ Execute '{cmd}'? [y/N]: ").strip().lower() == "y"
def exec_bash_command(self, cmd: str) -> Dict[str, str]:
"""Execute a bash command after confirming with the user."""
if self._confirm_execution(cmd):
return self.bash.exec_bash_command(cmd)
return {"error": "The user declined the execution of this command."}
# Instantiate the Bash class
bash = Bash(...)
# Create the agent
agent = create_react_agent(
model=ChatOpenAI(model=...),
tools=[ExecOnConfirm(bash).exec_bash_command], # Wrap for human-in-the-loop
prompt=SYSTEM_PROMPT,
checkpointer=InMemorySaver(),
)
# Create the user/agent interaction loop
while True:
user = input(f"[🙂] ").strip()
# Run the agent's logic and get the response.
result = agent.invoke({"messages": [{"role": "user", "content": user}]}, config=...)
# Show the response (without the thinking part, if any)
response = result["messages"][-1].content.strip()
if "</think>" in response:
response = response.split("</think>")[-1].strip()
if response:
print(f"\n[🤖] {response}")
다음 단계
이제 단 몇 줄의 코드로 나만의 컴퓨터 사용 에이전트를 완성했습니다. 이제 직접 테스트해 볼 차례입니다. 다른 오픈 Nemotron 모델로 교체해 보거나 시스템 프롬프트를 조금씩 수정하면서 에이전트가 어떻게 적응하는지 확인해 보세요. 기능을 이것저것 실험하다 보면, 지금 사용한 원리가 더 고도화된 멀티 에이전트 시스템(Multi-agent systems)으로도 자연스럽게 확장된다는 점을 쉽게 이해하실 수 있을 것입니다.
NVIDIA 개발자 포럼에 방문해 다양한 대화에 참여해 보세요. 여러분이 어떤 실험을 하셨는지, 어떤 궁금증이 생기셨는지, 그리고 앞으로 무엇을 만들어 가실지 무척 기대됩니다.
NVIDIA Nemotron 관련 최신 소식을 빠르게 받아보시려면 NVIDIA 뉴스를 구독하고, LinkedIn, X, Discord, YouTube에서 NVIDIA AI 채널을 팔로우해 주세요.
- 동영상 튜토리얼 및 라이브스트림을 통해 NVIDIA Nemotron을 최대한 활용하세요.을 시청하며 NVIDIA Nemotron을 100% 활용해 보세요.
- Nemotron 시작에 필요한 모든 정보가 필요하시다면 Nemotron 개발자 페이지를 방문해 보세요.
- Hugging Face, build.nvidia.com의 NIM 마이크로서비스 및 블루프린트(Blueprints)에서 새로운 오픈 Nemotron 모델과 데이터셋을 살펴보세요.
- 멋진 아이디어를 공유하고 필요한 기능에 투표하며 Nemotron의 미래를 함께 만들어보세요.