1. 교육·국방 AI 인프라의 양면성: 클라우드 파워와 폐쇄망 보안의 공존
인공지능을 도입하는 초·중·고 교육 현장과 군부대 전산망은 상반된 두 가지 요구사항을 동시에 만족해야 합니다.
- 최신 초거대 AI 활용: 공개된 일반 교육 자료나 외국어 번역, 코딩 실습에서는 GPT-4o나 Claude 같은 최상위 클라우드 LLM의 방대한 지식이 필요합니다.
- 철저한 망분리(Air-Gap) 보안: 학생 생활기록부, 시험 문제 출제, 작전 지침 등 외부 유출이 법적으로 엄격히 금지된 데이터는 반드시 학교 컴퓨터실 내부나 군부대 온프레미스 서버(Local sLM)에서만 처리되어야 합니다.
이를 위해 애플리케이션 코드를 클라우드용과 폐쇄망용으로 따로 분기하여 개발하는 것은
유지보수 비용을 폭증시키고 파편화를 유발합니다.
ZeliDesk.Core는 단 하나의 통일된 추론 클라이언트인
AiService를 통해 이 문제를 우아하게 해결했습니다.
2. 단일 인터페이스의 위력: OpenAI 호환 BaseUrl 라우팅 거버넌스
오늘날 오픈소스 로컬 AI 생태계(llama.cpp의 llama-server, Ollama, vLLM, LocalAI 등)는
사실상의 표준(De-facto Standard)으로 OpenAI 호환 REST API(/v1/chat/completions)를 제공합니다.
AiService는 이러한 산업 표준을 적극 수용하여 BaseUrl과 ApiKey 두 가지 매개변수만으로
추론 대상을 자유자재로 라우팅할 수 있도록 설계되었습니다.
| 배치 시나리오 | BaseUrl 설정값 |
구동 환경 및 보안 수준 |
|---|---|---|
| 일반 공교육 수업 | https://api.openai.com/v1 |
클라우드 초거대 모델 연결, 고성능 범용 지식 질의 |
| 컴퓨터실 로컬 sLM | http://localhost:8080/v1 |
llama-cli 또는 llama-server 온디바이스 직결 (인터넷 0바이트) |
| 국방·행정 온프레미스 | https://internal-ai.mil.local/v1 |
부대 내부망 전용 GPU 서버 연동 (외부 인터넷 물리적 단절) |
클라이언트 애플리케이션 입장에서는 상대방이 수천 개의 GPU를 묶은 클라우드 클러스터인지, 자신의 PC 로컬 메모리에서 돌고 있는 양자화 모델인지 전혀 알 필요 없이 완전히 동일한 C# 코드로 비동기 대화를 수행합니다.
3. ResponseHeadersRead와 SSE(Server-Sent Events) 무지연 토큰 디코더
AI 챗봇 인터페이스에서 사용자가 첫 번째 글자를 보기까지 걸리는 시간(Time To First Token, TTFT)은 체감 응답 속도를 결정짓는 핵심 지표입니다. 수백 토큰의 긴 답변이 완전히 끝날 때까지 HTTP 소켓을 블로킹한 채 기다리면 사용자는 앱이 멈췄다고 판단합니다.
AiService.ChatStreamAsync는
HttpCompletionOption.ResponseHeadersRead 옵션을 사용하여
서버 응답 헤더가 수신되는 즉시 스트림을 개방하고,
SSE(Server-Sent Events) 텍스트 청크를 실시간 디코딩합니다.
using var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();
var sb = new StringBuilder();
using var stream = await response.Content.ReadAsStreamAsync(token);
using var reader = new StreamReader(stream);
while (!reader.EndOfStream && !token.IsCancellationRequested)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrEmpty(line)) continue;
if (!line.StartsWith("data: ")) continue;
var data = line.Substring(6);
if (data == "[DONE]") break; // 스트리밍 정상 종료 플래그
try
{
using var doc = JsonDocument.Parse(data);
var choices = doc.RootElement.GetProperty("choices");
if (choices.GetArrayLength() > 0)
{
var delta = choices[0].GetProperty("delta");
if (delta.TryGetProperty("content", out var content))
{
string chunk = content.GetString();
if (!string.IsNullOrEmpty(chunk))
{
sb.Append(chunk);
onChunk?.Invoke(chunk); // UI 스레드로 0ms 즉시 디스패치
}
}
}
}
catch { /* 부분 파손된 SSE 청크 무시 */ }
}
data: [DONE]이 도착할 때까지 한 줄(Line) 단위로 파싱하며,
choices[0].delta.content가 도착할 때마다 onChunk 콜백을 즉각 호출합니다.
이를 통해 타자기를 치듯 부드럽게 글자가 출력되는 고품질 UX를 0.01초의 딜레이도 없이 구현합니다.
4. CancellationToken 기반 리소스 누수 및 GPU 연산 0초 차단
로컬 PC에서 온디바이스 모델을 돌릴 때 가장 조심해야 할 부분은 사용자가 답변 생성을 취소했는데도 백그라운드에서 GPU가 계속 전력을 소모하며 토큰을 연산하는 '고아 태스크(Orphan Task)' 현상입니다.
1. HTTP 연결 즉시 차단:
SendAsync(..., token)과 ReadAsStreamAsync(token)에 토큰을 전달하여 사용자가 [정지]를 누른 즉시 TCP 소켓을 닫음.2. 루프 탈출:
while (!reader.EndOfStream && !token.IsCancellationRequested) 조건으로 스트림 파싱 루프를 0ms 만에 중단.3. 로컬 추론 서버 부하 절감: 클라이언트 소켓이 닫히면 로컬
llama-server 또한 즉시 다음 토큰 생성을 중단하여 VRAM 및 CPU 자원을 즉시 회수.
5. LocalAppData 로컬 격리 설정 영속화(ai_settings.json) 아키텍처
API 키와 엔드포인트 URL은 보안상 소스 코드나 공용 레지스트리에 평문으로 노출되어서는 안 됩니다.
AiService는 윈도우 사용자별 격리 저장소인
%LOCALAPPDATA%\ZeliDesk\ai_settings.json을 활용합니다.
private static readonly string _settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ZeliDesk", "ai_settings.json");
public static void SaveSettings()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath));
var obj = new Dictionary<string, object>
{
["api_key"] = ApiKey ?? "",
["model"] = Model,
["base_url"] = BaseUrl,
["max_tokens"] = MaxTokens,
["temperature"] = Temperature
};
File.WriteAllText(_settingsPath,
JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true }));
}
catch (Exception ex)
{
Debug.WriteLine($"[AiService] Settings save error: {ex.Message}");
}
}
이 방식은 컴퓨터실에서 윈도우 다중 계정을 사용하는 학생들 간의 설정 충돌을 방지하며, 선생님이 지정한 로컬 sLM 엔드포인트 설정을 재부팅 후에도 안전하게 유지시켜 줍니다.
6. 프로덕션 소스 코드 정밀 해부: ZeliDesk.Core/Services/AiService.cs
스트리밍과 일반 동기 호출을 모두 지원하는 AiService의 정적 클래스 및 메시지 모델 구조입니다.
public class ChatMessage
{
public string Role { get; set; } // "user", "assistant", "system"
public string Content { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
}
public static async Task<string> ChatAsync(
List<ChatMessage> messages,
string systemPrompt = null,
CancellationToken token = default)
{
if (!IsConfigured)
throw new InvalidOperationException("OpenAI API 키 또는 엔드포인트가 설정되지 않았습니다.");
var msgList = new List<object>();
if (!string.IsNullOrEmpty(systemPrompt))
msgList.Add(new { role = "system", content = systemPrompt });
foreach (var msg in messages)
msgList.Add(new { role = msg.Role, content = msg.Content });
var requestBody = new
{
model = Model,
messages = msgList,
max_tokens = MaxTokens,
temperature = Temperature,
stream = false
};
var jsonBody = JsonSerializer.Serialize(requestBody);
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/chat/completions");
request.Headers.Add("Authorization", $"Bearer {ApiKey}");
request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
using var response = await _http.SendAsync(request, token);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync(token);
using var doc = JsonDocument.Parse(responseJson);
return doc.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
}
7. 결론: 언제 어디서나 끊김 없는 미래형 하이브리드 AI의 표준
인공지능 소프트웨어의 생명력은 환경의 변화에 흔들리지 않는 유연성에 있습니다. 기가비트 초고속 인터넷이 연결된 본사 연구실이든, 외부 통신이 철저히 차단된 군사 벙커와 산간 벽지의 교실 컴퓨터실이든, 동일한 사용자 경험과 동일한 API 파이프라인으로 지능을 제공할 수 있어야 진정한 솔루션입니다.
ZeliDesk.Core의 AiService는
OpenAI 호환 표준 엔드포인트 거버넌스, ResponseHeadersRead 기반 초고속 SSE 스트리밍 디코더,
그리고 완벽한 취소 토큰 제어를 통해
클라우드와 온디바이스를 자유자재로 넘나드는 차세대 하이브리드 AI의 든든한 기술적 교두보를 완성했습니다.