
에이전트에게 '부탁'이 아닌 '강제'를 수행하기 — Omnigent 가드레일 입문
요약
Omnigent 프레임워크를 활용하여 에이전트의 동작을 프롬프트가 아닌 메커니즘 층에서 제어하는 가드레일 설정 방법을 다룹니다. 모델의 판단에 의존하지 않고 정책 엔진을 통해 툴 호출 전 실행 여부를 강제하는 원리와 구현 사례를 설명합니다.
핵심 포인트
- 프롬프트 지시의 한계를 극복하기 위해 모델 외부의 가드레일 메커니즘 도입
- 가드레일, 정책(Policy), 정책 엔진의 계층적 구조 이해
- ALLOW, ASK, DENY 세 가지 상태를 통한 에이전트 동작 제어
- 비용 상한(cost_budget) 등 실무적인 정책 적용 사례 제시
지금까지 4개의 글을 통해 Omnigent의 구성을 이해하고, Polly로 코드의 병렬 구현과 크로스 리뷰를, Debby로 다중 모델의 토론과 통합을 실행해 왔습니다. 합성, 즉 이종의 에이전트를 섞어서 상호작용하게 하는 것의 가치에 대해서는 어느 정도 납득이 된 상태입니다.
하지만 여러 에이전트를 병렬로 실행하고, 게다가 클라우드나 무인 상태로 구동하게 되면 다음과 같은 걱정이 생깁니다. 멋대로 위험한 명령어를 입력하지 않을까, 예상치 못한 비용을 사용하지 않을까, 건드려서는 안 될 파일을 건드리지 않을까. 이번에는 Databricks가 내세우는 세 가지 가치 중 「제어 (Control)」를 다룹니다. 개념 정리부터 시작하여, 실제로 가드레일을 설정하고 에이전트의 동작이 멈추는 순간까지 지켜보겠습니다.
제어에 관한 이야기는 여기서부터 시작하는 것이 이해하기 쉽습니다. 에이전트에게 "위험한 명령어를 실행하기 전에 확인해 줘"라고 지시하는 것 자체는 누구나 하고 있습니다. 그렇다면 왜 굳이 메커니즘으로서의 제어가 필요한 것일까요.
프롬프트(Prompt)를 통한 지시는 어디까지나 모델에 전달되는 텍스트의 일부입니다. 지침을 지킬지 여부는 모델의 판단에 맡겨집니다. 대화가 길어지면 지시는 희미해지고 해석도 흔들립니다. 그리고 무엇보다 실행 그 자체는 멈추지 않습니다. 모델이 "이것은 확인이 필요 없다"라고 판단하면 그대로 툴(Tool)이 작동합니다.
가드레일(Guardrail)은 이 판단을 모델의 외부로 꺼냅니다. 설정으로서 선언되며, 메타 하네스 층 (Meta-harness layer, 지난 아키텍처 회차에서 본 서버나 러너)이 툴 호출 직전에 이를 평가합니다. 모델이 무엇을 생각하고 있든, 허가되지 않으면 툴은 움직이지 않습니다.
이 차이가 바로 Omnigent가 "프롬프트가 아닌 메커니즘 층에서 강제한다"라고 주장하는 부분입니다.
용어를 정리해 두겠습니다. 이 부분을 알면 이후의 설정 작업이 망설임 없이 진행됩니다.
가드레일은 에이전트 설정 (config.yaml)의 최상위 레벨에 있는 guardrails: 블록입니다. 개별 규칙들을 묶는 그릇이라고 생각하면 됩니다.
정책(Policy)은 그 가드레일 안에 나열되는 개별 규칙입니다. Polly의 설정을 열어보면 파괴적인 조작을 막는 blast_radius, 1턴당 병렬 기동 수를 제한하는 spawn_bounds 등이 나열되어 있습니다.
그리고 정책 엔진(Policy Engine)이 에이전트가 툴을 호출하기 직전에 이것들을 평가하여 세 가지 중 하나를 반환합니다. ALLOW (그대로 실행), ASK (인간의 승인을 기다리며 정지), DENY (실행시키지 않음)입니다.
즉, 이번에 하려는 것은 guardrails:에 정책을 하나 추가하여 ASK를 실제로 발생시키는 것입니다.
어떤 정책이 준비되어 있는지는 가지고 있는 소스 코드를 확인하는 것이 확실합니다. 내장 정책은 omnigent/policies/builtins/에 모듈로서 나열되어 있습니다.
cd ~/src/omnigent
sed -n '1,5p' omnigent/policies/builtins/__init__.py
이 부분을 읽어보면 설계가 보입니다. 각 모듈은 `POLICY_REGISTRY`라는 리스트를 공개하고 있으며, 그곳에 핸들러의 경로, 설명, 파라미터 스키마가 적혀 있습니다. 서버는 기동 시 이것을 discover 하여 API로 목록을 제공합니다. 즉 이것이 "사용 가능한 정책 카탈로그"입니다.
스캔 대상 모듈은 safety, cost, google, github, working_dir, risk_score, routing, cel, prompt, 그리고 nessie까지 총 10개였습니다.
가장 먼저 시도해보고 싶었던 것은 비용 상한 정책 (`cost.cost_budget`) 이었습니다. "이 이상 사용하면 멈춘다"는 제어로서 가장 이해하기 쉽고 실무에서도 수요가 있습니다.
소스를 읽어보니 사양은 명쾌했습니다. 하드 상한 (`max_cost_usd`) 에 도달하면 고비용 모델에서의 툴 호출을 DENY하고, 소프트 체크포인트 (`ask_thresholds_usd`) 를 넘을 때마다 ASK를 내보냅니다. 승인은 session_state에 기록되므로 동일한 체크포인트에서 두 번 묻지 않습니다. 상태 유지형(Stateful) 정책의 좋은 사례입니다.
그런데 여기에 함정이 있었습니다. 이 정책은 누적 비용 (`total_cost_usd`
)를 보고 판정하는데, 이 값은 모델의 토큰 단가로부터 계산됩니다. 그리고 이번 구성, 즉 Claude와 Codex의 구독(Subscription)을 통한 실행에서는 단가 기반의 과금 정보가 오지 않습니다. 값은 0.0인 상태로 유지됩니다. 정책 측은 "비용이 계상되지 않았을 때는 항상 ALLOW"하도록 구현되어 있기 때문에, 아무리 실행해도 발화(Trigger)되지 않습니다.
만약을 위해 실측으로도 확인했습니다. 지난번 스모크(Smoke) 테스트에서 생성된 `mlflow.db`를 조사한 결과, 비용이나 토큰을 포함하는 레코드는 단 한 건도 없었습니다.
cd ~/work/polly-smoke
sqlite3 mlflow.db "select name, substr(content,1,300) from spans where content like '%cost%' or content like '%usd%' or content like '%token%' limit 15;"
결과는 빈 값. 소스 코드의 기술 내용과 실측 결과 모두를 통해, 이 구성에서는 비용 관련 정책을 테스트할 수 없다는 것이 확정되었습니다.
그래서 비용 계상에 의존하지 않는 정책으로 전환합니다. 카탈로그를 다시 살펴보니 `safety.ask_on_os_tools`가 눈에 띄었습니다. 파일이나 쉘(Shell) 도구 호출 전에 반드시 사용자의 승인을 요구하는 정책입니다. Omnigent의 `sys_os_*` 도구뿐만 아니라, Claude Code의 네이티브 도구 (Bash, Read, Write, Edit, Glob, Grep), Codex의 네이티브 도구까지 폭넓게 커버합니다.
게다가 `kind`가 `callable`이고 `params_schema`가 `None`입니다. 즉, 인자(Argument)를 받지 않습니다. 경로(Path)를 한 줄 적는 것만으로 활성화됩니다. Polly는 반드시 쉘과 파일을 다루기 때문에 확실하게 발화됩니다.
Polly의 설정 (`~/agents/polly-dbx/config.yaml`) 내 `guardrails.policies`에 정책을 하나 추가합니다. 기존의 것은 그대로 남겨둡니다.
```yaml
guardrails:
ask_timeout: 86400
policies:
...
여기서 작성 시 주의할 점이 있습니다. 기존의 blast_radius 등은 인자가 필요하기 때문에 function: 아래에 path:와 arguments:를 가지는 형식입니다. 반면 ask_on_os_tools는 인자를 받지 않으므로, function:에 직접 점(Dot)이 포함된 경로를 작성합니다. 정책의 구현 형식 (Factory 인지 Callable 인지)에 따라 YAML 작성 방식이 달라집니다.
ask_timeout: 86400은 Polly 설정에 원래 있던 것으로, 승인 대기가 하루 동안 유지될 수 있음을 의미합니다. 자리를 비워도 괜찮다는 뜻입니다.
설정을 마쳤다면, 스모크용 리포지토리에서 Polly를 실행합니다.
cd ~/work/polly-smoke
omni run ~/agents/polly-dbx/
승인 프롬프트의 수를 줄이기 위해 태스크는 극소화합니다.
calc.py 에 square 함수(a * a)를 하나만 추가해 주세요.
테스트도 하나 추가해 주세요.
전송하자마자 즉시 멈췄습니다.
"Approval required · require_approval (tool_call)"라고 표시되어 있습니다. require_approval은 설정에서 지정한 정책 이름 그 자체입니다. 멈춘 지점은 Polly가 처음에 워커(Worker)의 유무를 확인하려고 시도한 sys_os_shell('command -v claude codex pi || true')였습니다.
실행하려던 명령어가 JSON 형태로 통째로 제시되며, Approve와 Reject 버튼이 나타납니다. 입력창은 "Respond to the pending request above to continue"로 바뀌며, 승인하기 전까지는 다음으로 진행할 수 없습니다. 세션 목록에도 "Needs response" 배지가 붙습니다.
프롬프트로 부탁한 것이 아니라, 도구 호출(Tool call) 자체가 물리적으로 정지된 것입니다. 이것이 제어(Control)의 실체입니다.
승인하면 "Approved · require_approval"이라는 이력이 남고, 에이전트는 다음 단계로 진행합니다. 그리고 다음 쉘 호출에서 다시 멈춥니다.
ask_on_os_tools
ask_on_os_tools는 인자(argument)도 상태(state)도 가지지 않는 단순한 정책(policy)이므로, 승인을 기억하지 않습니다. 동일한 종류의 작업이라도 호출할 때마다 ASK를 발생시킵니다. 앞서 살펴본 비용 상한(cost limit) 설정이 session_state에 승인을 기록하여 "같은 체크포인트는 한 번만 묻는다"라고 작동하는 것과는 대조적입니다. 즉, 정책에 따라 상태를 유지하는 것(stateful)과 그렇지 않은 것이 있다는 뜻입니다.
승인을 거듭하다 보면 이력이 장관을 이룹니다.
square 함수를 하나 추가하는 것뿐인 태스크임에도 승인은 10회를 넘어섰습니다. 내용도 다채롭습니다. git status, python3 버전 확인, pytest 확인, gh auth status, calc.py의 Edit, test_calc.py의 Write, pip install, venv 생성, pytest 실행, 그리고 .polly 탐색까지.
여기서 두 가지 부수적인 발견이 있습니다.
첫 번째는 에이전트가 배후에서 무엇을 하고 있는지 전부 보인다는 점입니다. 평소에는 결과만 보고 있기 때문에 의식하지 못하지만, "함수 하나를 추가한다"는 작업 뒤에서 이만큼의 조작이 실행되고 있습니다. 제어를 가하면 그 전체량이 가시화됩니다.
두 번째가 중요합니다. 승인 목록에 Bash(...), Edit(...), Write(...)가 나열되어 있는데, 이것들은 Claude Code의 네이티브 도구(native tool) 이름입니다. 즉, 부모인 Polly의 sys_os_shell뿐만 아니라, 서브 에이전트인 claude_code의 도구 호출에도 동일한 정책이 적용되고 있습니다. 병렬로 실행되는 여러 에이전트 모두에게 공통된 제어를 횡단적으로 가할 수 있다는 의미입니다. 이 지점이 합성(composition)과 제어가 연결되는 부분입니다.
ASK에는 승인(approve)과 거절(reject)이 모두 있습니다. 한쪽만 보고 끝내기에는 아쉬워서 Reject도 시도해 보았습니다. .polly/registry.json 읽기를 거절해 보겠습니다.
결과는 세션이 중단되지 않았습니다. 에이전트는 registry 읽기를 포기하고 다음 작업(브랜치를 만들고 파일을 스테이징한 뒤, diff를 확인하는 작업)으로 진행했습니다.
즉, Reject는 "해당 도구 호출만을 거부하는" 것입니다. 에이전트는 거절을 받아들이고 다른 수단을 찾거나, 그것 없이 진행하려고 시도합니다. 세션 전체를 종료하는 비상 정지 버튼이 아닙니다.
이때의 승인 프롬프트에는 description으로서 "Create branch and stage only the two intended files"와 같이 에이전트 자신의 의도가 덧붙여져 있었습니다. Claude Code의 네이티브 Bash 도구는 설명과 함께 호출되므로, 승인하는 사람이 판단하기 용이합니다.
여기까지를 통해 제어가 확실히 작동한다는 것을 체감할 수 있었습니다. 동시에 실운용상의 중요한 점도 보입니다.
함수 하나를 추가하는 태스크에서 승인이 10회 이상 발생합니다. Polly와 같은 오케스트레이터(orchestrator)는 워커 확인, 파일 탐색, worktree 생성, 테스트 실행 등을 위해 쉘(shell)을 수십 번 호출합니다. 모든 것에 승인을 요구하면 사람이 계속 버튼을 눌러야 하므로, "에이전트에게 맡기고 방치한다"는 이점이 사라집니다. 제어의 강도와 자동화의 편의성은 정비례 관계가 아닌 트레이드오프(trade-off) 관계에 있습니다.
이 관점에서 Polly의 기본 설정을 다시 읽어보면 설계 사상이 이해됩니다. blast_radius에는 gate_pushes: false라는 옵션이 지정되어 있으며, 주석에는 다음과 같이 적혀 있었습니다. 오케스트레이터는 무인으로 동작하므로, push나 merge, deploy 시에는 ASK하지 않는다. 단, 파멸적인 DENY 세트(force-push, rm -rf /, 원격 참조에 대한 hard-reset)는 여전히 적용한다.
즉, 실무에서의 현실적인 해답은 "모든 것에 승인을 요구하는 것"이 아니라, "정말로 파멸적인 것만 문답무용으로 DENY하고 나머지는 통과시키는 것"입니다. 이번에 테스트한 ask_on_os_tools는 제어 메커니즘을 이해하기에는 최적이지만, 상용하는 용도는 아닙니다. 오히려 익숙하지 않은 에이전트를 처음 구동할 때나, 중요한 리포지토리에서 일시적으로 상황을 지켜보고 싶을 때 사용하는 도구라는 위치가 적절합니다.
참고로, 테스트가 끝난 후에는 설정에서 정책을 제거하는 것을 잊지 마세요. 그대로 두면 다음 세션에서도 매번 승인을 요구받게 됩니다.
제어(Control)를 개념에서부터 실제 구현까지 추적해 보았습니다. 가드레일(Guardrail)은 정책(Policy)들을 하나로 묶는 설정 블록이며, 개별 정책은 도구 호출(Tool call) 직전에 평가되어 ALLOW / ASK / DENY를 반환합니다. 프롬프트(Prompt)를 통한 부탁과는 달리, 모델의 외부에서 강제되므로 대화가 길어져도 효과가 약해지지 않으며, 모델의 판단에 의해 무시되는 일도 없습니다.
실제로 구동하며 알게 된 점도 많았습니다. 정책은 설정한 이름으로 발화(Fire)되며, 실행 내용이 JSON 형식으로 제시된다는 점. 상태(State)를 가지는 정책(비용 상한)과 상태를 가지지 않는 정책(이번 승인 절차)이 있다는 점. 부모 에이전트뿐만 아니라 서브 에이전트(Sub-agent)의 도구 호출에도 횡단적으로 적용된다는 점. 거절(Reject)은 해당 도구 호출만을 중단시키며, 에이전트는 다른 수단을 통해 진행한다는 점. 그리고 모든 도구에 승인을 요구하는 설정은 무인 운영(Unattended operation)과 양립할 수 없다는 점입니다.
그리고 이번에 얻은 가장 큰 수확은, 비용 상한(Cost limit)을 목표로 시도했으나 빗나갔다는 사실일지도 모릅니다. 구독(Subscription)을 통한 실행에서는 비용이 계상되지 않아, 비용 관련 정책이 발화하지 않습니다. 문서를 읽는 것만으로는 깨닫지 못했고, 소스 코드와 실제 측정치를 모두 확인하고 나서야 비로소 알 수 있었습니다. 제어를 도입할 때는 해당 정책이 의존하고 있는 값이 자신의 구성(Configuration)에서 실제로 제대로 가져와지는지 먼저 확인하는 것이 확실합니다.
- omnigent-ai/omnigent (GitHub)
- Omnigent 공식 문서
- 제1탄: Omnigent 개요
- 제2탄: Polly 스모크 테스트
- 제3탄: 아키텍처 (Architecture)
- 제4탄: Debby / 합성 (Synthesis)
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기