* 이 게시물은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
(폐쇄망 사내 에이전트 ②) 설계
에이전트를 만든다고 하면 다들 모델 얘기부터 한다. 그런데 이 프로젝트에서 가장 먼저, 그리고 끝까지 지킨 결정은 모델이 아니라 이 한 문장이었다. “모델과 실행기를 분리한다.” LLM은 도구 이름과 인자만 반환하고, 실제 실행은 런타임이 정책 검사 → 사용자 승인 → 실행 → 감사 기록 순서로 대행한다. 모델은 손이 없다. 손은 전부 런타임 것이다.
왜 이 분리가 전부인가
모델에게 실행권을 직접 주면 무슨 일이 생기는지는 상상이 아니라 구조의 문제다. 4B 소형 모델은 경로를 지어내고(환각), 지시문에 휘둘리고, 같은 실수를 반복한다. 그 손에 rm을 쥐여줄 수는 없다. 분리하면 얘기가 달라진다 — 모델이 아무리 이상한 호출을 만들어도, 정책이 거르고 승인이 막고 감사가 남는다. 안전의 근거가 ‘모델의 판단력’이 아니라 ‘런타임의 구조’가 되는 것이다. MCP 보안 편에서 정리했던 원칙과 정확히 같은 결이다.
커밋에서 — 프레임워크조차 경계 밖에 둔다
이 분리는 도구 실행에만 적용된 게 아니다. Java 시절 두 번째 커밋이 이랬다.
commit b029b7b (2026-08-01)
feat: LangChain4j 도입, Tool 31종 확장, 경로 제한 해제, Agent Loop 개선
자체 HTTP 클라이언트·응답 파서 4개를 제거하고 LangChain4j로 교체.
ChatModel을 얇은 경계로 남겨 Agent Runtime은 프레임워크를 모른다.
Provider를 Ollama·OpenAI·Azure로 바꿔도
Tool·Policy·승인·감사 코드는 그대로다.
모델 호출 프레임워크마저 얇은 인터페이스 뒤에 가둬서, 런타임 본체는 지금 어떤 모델이 붙어 있는지 모른다. 이 결정 덕분에 나중에 언어를 통째로 갈아탈 때(3편), 추론 엔진을 바꿀 때(4편), 외부 vLLM 서버를 붙일 때(11편) 승인·감사 코드는 한 줄도 건드리지 않았다. 경계를 먼저 그어두면 나중의 큰 수술이 전부 국소 수술이 된다.
경로 제한을 푼 날 — 안전의 근거가 이동했다
같은 커밋에 흥미로운 결정이 하나 더 있다. 처음엔 에이전트가 작업 폴더 밖을 못 건드리게 경로 경계를 뒀는데, 이걸 풀어서 컴퓨터 전체를 다루게 했다. 무모해 보이지만 논리가 있다.
## 경로 제한 해제 (PathScope)
작업 폴더 경계를 없애고 컴퓨터 전체를 다룬다.
안전의 근거가 "경계"에서 "승인"으로 이동했으므로
승인 화면이 해석된 절대경로를 그대로 표시한다.
경계로 지키는 안전은 경계 안에서는 무력하다. 반대로 모든 쓰기·실행이 승인을 거친다면, 어디를 건드리든 사용자가 절대경로를 보고 결정한다. ‘어디까지 허용할까’라는 공간의 문제를 ‘이 실행을 허락할까’라는 행위의 문제로 바꾼 것이다. 물론 이 논리가 성립하려면 승인 화면이 거짓말을 하지 않아야 한다 — 그래서 6편에서 다룰 승인·감사 체계가 이 프로젝트의 심장이 된다.
에이전트 루프도 실측으로 다듬는다
흐름을 그림으로 — 호출 하나의 일생
flowchart LR
M[모델\n4B LLM] -->|"도구 이름 + 인자"| P{정책 검사}
P -->|DENY| X[차단 · 기록]
P -->|APPROVAL| A{사용자 승인}
P -->|ALLOW| E
A -->|거부| X2[거부 · 기록]
A -->|허용| E[실행기 실행]
E --> L[(감사 DB)]
E -->|결과| M
모델이 만든 호출은 반드시 이 관문들을 통과한다. 어느 단계에서 멈추든 감사 DB에 남는다는 것, 그리고 모델은 이 그림의 맨 왼쪽 상자 하나뿐이라는 것 — 이 두 가지가 구조의 전부다.
실제 코드 — 경계가 얼마나 얇은가
“프레임워크를 얇은 경계 뒤에 가둔다”는 말이 추상적이라면, TS판의 실제 인터페이스를 보자. 런타임 본체가 아는 모델의 전부가 이것이다.
// packages/model-api/src/ChatModel.ts — 런타임이 아는 '모델'의 전부
export interface ChatModel {
generate(request: ModelRequest): Promise<ModelResponse>;
// 스트리밍·계측은 선택 — 없는 Provider도 깨지지 않는다
generateStreaming?(request: ModelRequest, onChunk: ModelStreamListener): Promise<ModelResponse>;
metrics?(): ModelMetrics;
}
메서드 하나(+선택 둘). 인프로세스 llama.cpp든, 외부 vLLM이든, Ollama든 이 인터페이스만 구현하면 꽂힌다. 스트리밍과 계측이 선택 메서드인 것도 의도다 — 계약을 넓히면 모든 구현이 무거워지고, 좁게 유지하면 교체가 싸다. 실제로 이 프로젝트에서 Provider가 네 번 바뀌는 동안(자체 클라이언트→LangChain4j→node-llama-cpp→OpenAI 호환 추가) 승인·감사 코드는 그대로였다.
✅ 확인
- 내 에이전트에서 도구를 실행하는 코드가 어디인지 한 파일로 짚을 수 있다 — 여러 곳이면 게이트를 못 세운다.
- 모델 교체 시 바뀌는 파일이 Provider 구현뿐인지 확인했다.
- 승인 화면이 보여주는 정보(절대경로·명령 전문)가 실제 실행될 것과 같은지 검증했다.
이 커밋의 마지막 조각은 루프 개선인데, 방식이 인상적이었다. 프록시로 실제 대화를 캡처해 보니 쓰기 성공 후에도 stat·edit를 반복 호출해 승인 창이 5번 뜨고 있었다. 그래서 동일한 (도구, 인자) 재호출은 실행 없이 차단하고, 연속 3회 실패면 상황 요약과 함께 종료하게 했다. 추측이 아니라 로그를 잡아서 고치는 이 습관은 8일 내내 반복되고, 이 시리즈에서 가장 재미있는 부분들이 전부 여기서 나온다.
정리하면 — 에이전트 설계의 첫 질문은 “어떤 모델을 쓸까”가 아니라 “실행권을 어떻게 가둘까”다. 모델은 갈아끼우는 부품이고, 남는 것은 정책·승인·감사의 구조다.
자주 묻는 질문 (FAQ)
모델과 실행기를 분리하면 뭐가 좋아지나요?
안전의 근거가 모델의 판단력이 아니라 런타임 구조가 됩니다. 모델이 환각으로 이상한 호출을 만들어도 정책·승인·감사 게이트가 걸러내고, 모델·프레임워크를 교체해도 안전 관련 코드는 그대로 유지됩니다.
경로 제한 없이 컴퓨터 전체를 다루게 하면 위험하지 않나요?
모든 쓰기·실행이 사용자 승인을 거치고 절대경로가 승인 화면에 그대로 표시된다면, 경계 기반보다 오히려 투명합니다. 다만 승인 화면의 정직함과 감사 기록이 전제 조건입니다.

답글 남기기