
Foundry Local과 PowerShell을 이용한 로컬 AI: 채팅, 메모리, 도구; 하네스 구축하기
요약
Foundry Local을 사용하여 로컬 환경에서 오픈 웨이트 모델을 실행하고, PowerShell을 통해 모델의 상태 유지, 도구 실행 등을 관리하는 하네스(harness)를 구축하는 방법을 다룹니다.
핵심 포인트
- Foundry Local을 통한 로컬 AI 모델 실행 및 하드웨어 가속 활용
- API 비용 절감, 개인정보 보호, 데이터 거주성 문제 해결
- PowerShell을 이용한 모델과 실제 세계를 잇는 하네스 구축 방법
- Mistral, Phi, Qwen 등 다양한 오픈 웨이트 모델 지원
2026년 6월 이후, 프론티어 연구소(frontier labs)들은 멈추지 않고 제품을 출시해 왔습니다: Fable, Sonnet 5, GPT-5.6 (Sol, Terra, Luna), Cursor 메트릭으로 학습된 Grok 4.5, 그리고 중국 모델인 DeepSeek R4, Kimi K3, GLM 5.2는 말할 것도 없습니다. 차세대 프론티어 모델을 향한 경쟁은 활짝 열려 있습니다.
그와 동시에 더 조용한 신호가 흐르고 있습니다: 바로 오픈 웨이트 (open-weight) 모델로의 이동입니다. Microsoft는 DeepSeek를 테스트하고 있으며, Uber Eats와 Airbnb는 Qwen을 실행하고 있고, Cursor는 Kimi를 사용합니다. 이러한 모델들은 당신이 제어하는 하드웨어에서 실행되며, 프론티어 API 호출 비용의 아주 일부분만으로 실제 작업을 수행합니다.
세 가지 이유가 같은 방향을 향하고 있습니다: API 비용, 개인정보 보호 및 데이터 거주성 (EU의 GDPR 및 Cloud Act 논의), 그리고 지난 몇 달 동안 오픈 웨이트 모델과 프론티어 모델 사이의 격차가 급격히 좁혀졌다는 사실입니다.
이 지점에 Foundry Local이 적합합니다. Foundry Local은 100MB 미만의 크기로 SDK (Python, C#, Rust, JS, C++)와 REST API를 노출하는 CLI를 제공합니다. 이는 모델 획득과 하드웨어 가속 (hardware acceleration)을 대신 처리해 줍니다. 큐레이션된 카탈로그에서 모델(Mistral, Phi, Qwen 등)을 선택하기만 하면, Foundry Local이 당신의 기기(CPU, GPU, NPU)에 맞는 적절한 백엔드 (backend)를 선택합니다.
Foundry Local이 제공하지 않는 것은 바로 하네스 (harness)입니다: 상태를 유지하고, 도구를 실행하며, 무엇이 반환될지 결정하는 모델과 실제 세계 사이의 작은 프로그램 말입니다. 이 포스트는 이를 PowerShell로 작성하는 것에 관한 것입니다.
Foundry Local CLI를 설치하려면 프로젝트의 GitHub 페이지에서 최신 릴리스를 사용하세요.
아래의 모든 내용은 프리뷰 (preview) 단계입니다. 이 포스트는 Foundry Local CLI 0.1.1을 사용하며 CLI 0.1.2에서 테스트되었습니다.
이 포스트에서 사용된 스크립트는 제 GitHub 리포지토리에서 찾을 수 있습니다.
CLI는 로컬 서비스 (local service)를 실행합니다. 이 포스트의 나머지 모든 내용은 이를 다루므로, 여기서부터 시작하세요.
>Foundry server status
다음과 같은 결과가 나와야 합니다:
StateReady
PID60900
Started2026-07-13 18:58:02Z
...
만약 그렇지 않다면, 서버를 시작할 수 있습니다.
>foundry server start
이 명령어를 통해 사용자의 플랫폼에서 사용 가능한 모델 목록을 확인할 수 있습니다.
>foundry model list
특정 모델에 대한 더 자세한 정보를 얻으려면 다음 명령어를 사용하세요:
>foundry model info mistral-7b-v0.2
모델은 별칭 (alias)으로 식별되며, 각 별칭은 여러 변체 (variant)로 연결될 수 있습니다. 변체란 하나의 가속 백엔드 (TensorRT 또는 CUDA (NVIDIA), QNN (Qualcomm), 또는 범용 (generic))와 하나의 장치 유형 (CPU, GPU, NPU)에 맞춰 빌드된 모델을 의미합니다.
Foundry Local은 사용자를 대신해 변체를 선택하지만, 그 선택의 품질은 사용자의 하드웨어 성능에 달려 있습니다. 적절한 GPU가 없는 머신은 NPU로 대체됩니다.
별칭에는 보통 파라미터 (parameter) 수가 인코딩되어 있습니다. 예를 들어 mistral-7b-v0.2의 경우 70억 개 (7 billion)입니다. 한 가지 예외는 Phi 모델들로, 이들은 크기 계층 (mini, small...)을 사용합니다. 파라미터가 더 많을수록 메모리 점유율 (memory footprint)이 커지고 일반적으로 성능이 더 뛰어나지만, 아키텍처 (architecture)와 학습 품질 (training quality) 또한 그만큼 중요합니다.
"model info" 명령어는 토큰 (token) 단위의 컨텍스트 윈도우 (context window)도 보고합니다. 이는 보기보다 중요합니다. 한계치에 도달하면 이전 대화 내용이 누락되며, 모델은 더 이상 존재하지 않는 이력을 바탕으로 자신 있게 답변하기 시작합니다.
마지막 속성은 모델이 도구 호출 (tool calling)을 지원하는지 여부입니다. 모델은 스스로 아무것도 실행하지 않습니다. 대신 사용자의 코드가 실행하고 다시 입력할 수 있도록 구조화된 요청 (structured request)을 생성합니다. 실제로 파라미터가 약 3B 미만인 경우, 도구 호출의 신뢰도가 급격히 떨어집니다.
모델을 사용하려면 로컬 캐시 (local cache)에 사용 가능해야 하며 로드되어 있어야 합니다.
모델을 다운로드할 수 있습니다:
>foundry model download qwen3.5-4b
그 다음 모델을 로드합니다.
>foundry model load qwen3.5-4b
최신 명령어를 사용하면 모델이 캐시 (cache)에 없을 경우 다운로드하고 모델을 로드합니다.
다음과 같이 입력하여 모델을 실행할 수 있습니다.
>foundry run qwen3.5-4b
모델과 채팅을 할 수 있습니다. 사용 가능한 명령어를 확인하려면 /help를 입력하면 됩니다.
하지만 이것은 단순한 채팅일 뿐입니다. 이는 제한적입니다. 컨텍스트 (context), 메모리 (memory), 도구 (tools), 하네스 (harness)가 없습니다.
더 많은 제어 기능을 추가하기 위해, 포함된 REST API를 사용하여 PowerShell에서 솔루션을 구축하기 시작할 수 있습니다.
포트는 시작 시 할당되며 서비스가 재시작될 때 변경됩니다. 포트를 하드코딩 (hardcode)하지 마세요. 각 세션 전에 foundry 서버 상태 명령어나 SDK를 통해 다시 읽어오십시오.
>foundry server status
만약 서비스가 시작되지 않았다면, 다음 명령어를 사용해야 합니다:
>foundry server start
PowerShell에서 foundry CLI를 사용하려면, 명령어가 제공하는 URI를 변수에 저장해야 합니다:
$restApiUri = "http://127.0.0.1:56172"
첫 번째 스크립트에서는 간단한 채팅, 즉 모델에게 간단한 질문을 던지는 작업을 수행할 것입니다.
$body = @{
model = "qwen3.5-4b"
messages = @( @{ role = "user"; content = "Give the name of the capital of Belgium" } )
...
Foundry Local의 REST API는 OpenAI와 호환됩니다. 이것이 핵심입니다. 여기서 구축하는 바디 (body)는 api.openai.com으로 보내는 바디와 동일합니다. 로컬 모델을 호스팅된 모델로 교체하는 것은 URL 변경만으로 가능해집니다.
결과에는 서로 다른 속성을 가진 객체가 포함됩니다. 그중에는 모델(model), 모델과의 대화가 담긴 HashMap 배열인 choices, 그리고 사용된 토큰을 나열하는 맵인 total_tokens, prompt_tokens, completion_tokens, total_tokens가 있습니다:
usage : @{prompt_tokens=11; completion_tokens=7; total_tokens=18}
또한 모델과의 상호작용을 포함하는 배열인 Choices 속성도 포함되어 있습니다. 일반적으로 모델의 응답은 첫 번째(인덱스 0)에 위치합니다. 여기에는 역할(role, assistant), 내용(content, 모델 응답), 그리고 도구 호출(tool_call)을 포함하는 맵인 message가 들어 있습니다.
메시지를 가져오려면:
$result.choices[0].message.content
지금까지의 스크립트는 CLI 채팅이 하지 못했던 일을 수행하지는 않습니다. 첫 번째 실질적인 이득은 시스템 프롬프트(system prompt)입니다. 이는 사용자가 말을 걸기 전에 행동을 형성하는 지침입니다. 이는 당신이 가질 수 있는 가장 저렴한 제어 인터페이스(control surface)입니다.
이를 수행하려면 $message 변수를 수정하고 system 역할을 가진 해시 테이블(hash table)을 추가해야 합니다:
$messages = @(
@{ role = "system"; content = "You are a PowerShell coding assistant. Only give code if requested." }
@{ role = "user"; content = "Give me a PowerShell script to list all files in a given directory." }
...
이 방식은 작동하지만, 모델은 메모리(memory)가 없습니다. 모든 호출은 콜드 스타트(cold start)이며, 아무것도 이어지지 않습니다. 여기서 메모리는 모델의 기능이 아니라, 매 상호작용마다 대화 기록(transcript)을 다시 보내는 당신의 역할입니다.
즉, 대화 기록은 삽입 순서를 유지해야 하며 저렴한 비용으로 확장되어야 합니다. PowerShell 배열은 추가(append)할 때마다 재할당(reallocate)되므로, 제네릭 리스트(generic list)를 사용하십시오:
[System.Collections.Generic.List[hashtable]] $Messages = @(
@{ role = "system"; content = "You are a science popularizer specializing in physics." }
@{ role = "user"; content = "Explain quantum computing in plain English in less than 200 words" }
...
첫 번째 상호작용은 처음 두 예시와 유사합니다. 하지만 모델과의 다음 상호작용을 위해, 결과가 메시지에 추가됩니다.
$assistantResponse = $result.choices[0].message.content
$messages.Add(@{ role = 'assistant'; content = $assistantResponse })
그다음 새로운 메시지를 생성할 수 있습니다.
$messages.Add(@{ role = 'user'; content = "이제 이것을 5살 아이에게 설명하듯이 말해줘" })
그리고 대화 기록(history)과 함께 대화를 시작합니다.
위의 모든 과정은 마지막 조각을 설정합니다. 도구 호출 (Tool calling)이 작동하는 이유는 트랜스크립트 (transcript), 모델의 요청, 사용자의 결과, 그리고 모델의 최종 답변이 동일한 대화 기록 내에서 세 번의 턴 (three turns)으로 이루어지기 때문입니다.
모델은 도구를 직접 실행하지 않습니다. 대신 모델은 도구가 외부에서 실행되도록 요청을 보내고 그 결과를 반환받습니다.
이 함수는 스텁 (stub)이며, Azure 관련 사항이 아닌 프로토콜 (protocol)에 집중할 수 있도록 정해진 답변을 반환합니다.
function Get-VnetPeeringStatus {
param (
[string]$VnetName,
...
이 함수를 도구 호출 (tool calling)로 사용하려면, 모델은 JSON 배열 형태인 함수의 정의를 알아야 합니다. 하나 또는 여러 개의 도구를 정의할 수 있습니다.
$tools = @(
@{
type = 'function'
...
그다음 메시지가 필요합니다:
[System.Collections.Generic.List[hashtable]] $Messages = @(
@{ role = "system"; content = "당신은 Azure 인프라 어시스턴트입니다. 당신은 실시간 Azure 상태를 조회하는 도구에 접근할 수 있습니다. 사용자가 리소스 상태에 대해 물으면 추측하는 대신 적절한 도구를 호출하십시오. 오직 도구 결과로만 답변하고, 리소스 상태를 절대 조작하지 마십시오." }
@{ role = "user"; content = "rg-network-prod에 있는 hub-vnet이 올바르게 피어링(peered)되어 있나요?" }
...
여기서 시스템 프롬프트 (system prompt)가 얼마나 더 무거워졌는지 주목하십시오. 이는 모델에게 추측하는 대신 도구를 호출하고, 오직 도구 결과로만 답변하도록 지시합니다. 작은 모델의 경우 이는 선택 사항이 아닙니다. 실시간 인프라에 대해 질문을 받은 4B 모델은 그럴듯한 피어링 상태를 기꺼이 지어낼 것입니다 (hallucinate). 프롬프트는 이러한 종류의 환각 (hallucination)에 대한 첫 번째 방어선입니다.
그다음 바디 (body)를 생성해야 합니다:
$body = @{
model = $modelID
messages = $messages
...
“tool_choice”는 모델이 호출할 수 있는 도구를 고정(pins)합니다. 여기서는 데모에 유용하도록 Get-VnetPeeringStatus를 강제하는데, 이는 비결정론적(nondeterminism) 요소 중 하나를 제거하기 때문입니다. 실제 사용 시에는 'auto'로 설정하여 모델이 결정하도록 하겠지만, 바로 이 지점에서 소형 모델들의 성능이 실망스러워지기 시작합니다.
모델에 요청을 보낸 후에는 “tool_calls”가 반환되었는지 확인하기 위해 응답을 스캔해야 합니다.
$choice = $result.choices[0]
if ($choice.finish_reason -eq 'tool_calls') {
...
히스토리를 유지하기 위해 어시스턴트(assistant) 콘텐츠를 $message 변수에 추가합니다.
그 다음 모델 메시지의 각 호출(call)에 대해 도구(PowerShell 함수)를 실행하고, 역할(role)을 'tool'로, tool_call_id와 함수의 결과값을 포함하는 새로운 해시맵(hashmap)을 추가합니다.
마지막으로 업데이트된 메시지를 모델에 다시 보내면, 이번에는 첫 번째 상호작용에서 보냈던 질문에 대해 모델이 답변하게 됩니다.
이것들이 기본적인 상호작용들입니다: 채팅(chat), 시스템 프롬프트(system prompt), 메모리(memory), 도구(tools). 여기서 PowerShell에 특화된 것은 아무것도 없으며, 동일한 단계가 Python이나 C#에서도 똑같이 작동합니다.
하지만 지금까지 구축된 것을 다시 살펴보십시오. 모델은 아무것도 기억하지 못했습니다. 우리는 대화 기록(transcript)을 다시 보냈을 뿐입니다. 모델은 함수를 호출한 것이 아닙니다. 모델은 이름을 내뱉었고, 우리가 그것을 따를지 결정했습니다. 모델은 자신의 출력을 스스로 확인하지 않았습니다. switch 문이 확인했습니다. 이것을 유용하게 만든 모든 것은 모델 외부에서, 모델과 세상 사이를 가로막고 있는 수백 줄의 PowerShell 코드에서 일어났습니다.
그것이 바로 하네스(harness)의 기초입니다. 하네스는 상태(state)를 유지하고, 허용 목록(allowlist)을 소유하며, 호출을 실행하고, 무엇을 다시 보낼지 결정하는 작은 프로그램입니다. 호스팅된 제품을 사용할 때는 하네스가 제품과 함께 제공되므로 사용자가 볼 일이 없습니다. Foundry Local은 모델과 포트(port)만을 제공합니다. 하네스는 직접 작성해야 하며, 이것이 비용이자 동시에 핵심입니다. 즉, 당신의 코드가 허용하지 않는 한 그 어떤 것도 당신의 인프라에 도달할 수 없다는 점입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

