Building AI-Friendly CLIs
JSON-First Design with Schema Commands
요즘 AI 에이전트가 코드를 짜고, 도구를 호출하고, 심지어 배포까지 하는 세상이 되면서 한동안 조용했던 CLI가 다시 주목받고 있습니다. GUI나 웹 대시보드는 사람에겐 편하지만, AI 에이전트 입장에서는 CLI가 훨씬 다루기 좋은 인터페이스거든요.
그런데 기존 CLI들은 사실 사람을 위해 설계되었습니다. 예쁜 테이블 출력, 컬러 코드, 축약된 플래그 등 사람이 읽기엔 좋지만 에이전트가 파싱하기엔 꽤 고통스러운 부분이 많습니다. 출력 포맷이 버전마다 미묘하게 바뀌기도 하고, 정확한 사용법을 알려면 문서를 따로 읽어야 하는 경우도 많죠.
이번주 동안 회사에서 팀 내부용으로 사용하는 CLI를 JSON I/O와 schema 명령어 기반으로 대폭 수정했고, 에이전트의 작업 성공률이 눈에 띄게 올라갔습니다. 이 경험을 바탕으로 AI-Friendly CLI를 만드는 방법에 대해 정리해보려 합니다.
Why JSON-First Input / Output Matters for AI Agents
Human vs AI CLI Usage Patterns
사람은 CLI를 쓸 때 --help를 보고, man page를 읽고, 에러 메시지를 눈으로 확인하면서 반복적으로 시도합니다. 출력이 좀 달라져도 맥락을 파악해서 적응하죠. 반면 AI 에이전트는 출력을 문자열 그대로 받아서 처리합니다. 테이블 형태의 출력을 정규식으로 파싱해야 하고, 컬럼 순서가 바뀌거나 줄바꿈이 달라지면 바로 깨집니다.
# 사람에겐 이게 편하지만...
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
my-app-7d4b8c6f5-x2k9z 1/1 Running 0 3d
# 에이전트에겐 이게 필요합니다
$ kubectl get pods -o json
{
"items": [{
"metadata": {"name": "my-app-7d4b8c6f5-x2k9z"},
"status": {"phase": "Running", "containerStatuses": [{"ready": true}]}
}]
}
The Pain of Unstructured Text Output
구조화되지 않은 텍스트 출력이 에이전트에게 주는 고통은 생각보다 큽니다. 실제로 제가 겪었던 패턴들을 몇 가지 나열해보면 아래와 같습니다.
- 파싱 불안정: 출력에 헤더가 있을 때도 있고 없을 때도 있음
- 로케일 의존: 날짜/숫자 포맷이 시스템 로케일에 따라 달라짐
- 색상 코드 오염: ANSI escape code가 섞여 들어와서 문자열 비교 실패
- Progress bar 충돌: stderr와 stdout이 섞이면서 출력이 꼬임
- 암묵적 truncation: 긴 값이
...으로 잘리는데 이를 알 방법이 없음
이런 문제를 하나하나 예외 처리하다 보면 에이전트 코드가 CLI 파싱 로직으로 뒤덮이게 됩니다. 본질적인 작업보다 출력을 해석하는 데 더 많은 시간을 쓰게 되는 거죠.
Advantages of JSON
JSON 중심 입출력으로 전환하면 이 문제들이 대부분 사라집니다.
- 타입 안전성: 숫자는 숫자, 문자열은 문자열.
"3"vs3을 구분할 수 있음 - 스키마 기반 검증: JSON Schema로 입출력 형태를 사전 정의하고 검증 가능
- 체이닝 용이성:
jq, 파이프라인, 프로그래밍 언어에서 바로 파싱 가능 - 일관성: 로케일, 터미널 설정과 무관하게 동일한 출력 보장
- 에러 구조화: 에러도 JSON으로 반환하면 에이전트가 에러 유형을 판단하고 적절히 대응 가능
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Pod 'my-app' not found in namespace 'default'",
"suggestions": ["Check namespace with --namespace flag"]
}
}
Real-World Test Results from Our Project
CLI에 --json 플래그를 추가하고 에이전트에게 동일한 태스크를 수행시켰을 때의 변화를 간단히 정리하면:
| 지표 | 텍스트 출력 | JSON 출력 |
|---|---|---|
| 태스크 성공률 | 60% 정도 | 90% 정도 |
| 평균 재시도 횟수 | 2.3회 | 0.4회 |
| 파싱 관련 에러 | 전체 에러의 41% | 거의 0% |
물론 이건 제가 테스트한 특정 태스크들 기준이라 일반화하긴 어렵지만, JSON 전환만으로 이 정도의 차이가 나온다는 건 꽤 의미 있는 결과였습니다.
The Schema Command – Letting AI Learn and Adapt at Runtime
JSON I/O만으로도 큰 개선이 되지만, 여기서 한 단계 더 나아갈 수 있는 방법이 있습니다. 바로 schema 명령어입니다.
Inspiration from Google Workspace CLI (gws)
이 아이디어는 Google Workspace CLI(gws)에서 영감을 받았습니다. gws는 리소스별로 스키마 정보를 런타임에 조회할 수 있는 구조를 가지고 있는데요, 이를 보면서 "에이전트가 문서를 읽는 대신 CLI에 직접 물어보면 되지 않을까?"라는 생각이 들었습니다.
How the Schema Subcommand Works
개념은 단순합니다. CLI에 schema라는 서브커맨드를 추가하고, 리소스와 액션을 지정하면 해당 명령어의 입출력 JSON Schema를 반환하는 겁니다.
$ mytool schema user.create
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Create a new user",
"input": {
"type": "object",
"required": ["email", "role"],
"properties": {
"email": {"type": "string", "format": "email"},
"role": {"type": "string", "enum": ["admin", "member", "viewer"]},
"name": {"type": "string", "maxLength": 100}
}
},
"output": {
"type": "object",
"properties": {
"id": {"type": "string", "format": "uuid"},
"email": {"type": "string"},
"role": {"type": "string"},
"created_at": {"type": "string", "format": "date-time"}
}
}
}
리소스 목록 자체도 조회할 수 있으면 더 좋습니다.
$ mytool schema --list
["user.create", "user.delete", "user.get", "user.list", "project.create", ...]
Benefits for Agents
이 구조가 에이전트에게 주는 이점은 상당합니다.
- 외부 문서 불필요: 에이전트가 CLI에 직접 물어보고 정확한 입력을 구성할 수 있음
- API 변경에 자동 적응: CLI가 업데이트되면 schema도 함께 바뀌므로 에이전트가 항상 최신 스펙으로 동작
- Dry-run과 결합: schema로 입력을 만들고
--dry-run으로 검증한 뒤 실행하는 안전한 워크플로우 가능 - Self-describing: 별도의 AGENTS.md나 tool description 없이도 CLI 스스로 자신을 설명할 수 있음
Our Implementation Overview
저희 프로젝트에서는 다음과 같은 구조로 구현했습니다.
- 각 명령어 핸들러에 input/output schema를 정의 (Pydantic 모델 기반)
schema서브커맨드에서 이를 JSON Schema로 직렬화하여 반환--list옵션으로 전체 리소스/액션 트리를 탐색 가능하게 구성- schema 응답에
examples필드를 포함시켜 에이전트가 참고할 수 있도록 함
구현 자체는 크게 어렵지 않았습니다. 이미 Pydantic으로 입출력 모델을 정의하고 있었기 때문에 .model_json_schema()를 호출하는 것만으로 대부분 해결됐거든요.
Example Agent Workflow Using Schema
실제 에이전트가 schema를 활용하는 흐름을 보면 이런 식입니다:
1. Agent: mytool schema --list
→ 사용 가능한 명령어 목록 확인
2. Agent: mytool schema user.create
→ 입력 스키마 확인 (필수 필드: email, role)
3. Agent: mytool user create --json '{"email":"new@example.com","role":"member"}' --dry-run
→ 실행 전 검증
4. Agent: mytool user create --json '{"email":"new@example.com","role":"member"}'
→ 실제 실행, JSON 응답 수신
5. Agent: 응답의 id 필드를 다음 작업에 활용
이 흐름에서 에이전트는 단 한 번도 문서를 참조하지 않습니다. CLI 자체가 문서 역할을 하는 거죠.
Practical Design Patterns
JSON I/O와 Schema를 실제로 적용할 때 고려할 패턴들을 정리해봤습니다.
Input Design Choices
입력 방식은 크게 세 가지가 있고, 상황에 따라 선택하면 됩니다.
| 방식 | 예시 | 적합한 경우 |
|---|---|---|
| stdin JSON | echo '{"key":"val"}' | mytool create |
큰 페이로드, 파이프라인 체이닝 |
| argument JSON | mytool create --json '{"key":"val"}' |
단일 명령 실행, 히스토리에 남기고 싶을 때 |
| Mixed | mytool create --name foo --json '{"extra":"opts"}' |
자주 쓰는 옵션은 플래그로, 나머지는 JSON으로 |
개인적으로는 argument JSON 방식을 기본으로 하되, stdin도 지원하는 형태를 추천합니다. 에이전트 입장에서는 하나의 명령어로 완결되는 게 가장 다루기 쉽거든요.
Output Design Best Practices
출력 설계에서 몇 가지 중요한 원칙들이 있습니다.
--json플래그는 필수: 기본은 사람이 읽기 좋은 형태를 유지하되,--json을 넣으면 구조화된 출력을 반환- NDJSON 지원: 스트리밍이 필요한 경우 (로그, 이벤트 등) 줄 단위 JSON 지원
- 에러도 JSON으로:
--json모드에서는 에러 역시 JSON 형태로 반환. exit code와 함께 사용 - 메타데이터 포함: pagination 정보, 요청 ID, 타임스탬프 등을 응답에 포함
# 일반 모드
$ mytool user list
EMAIL ROLE CREATED
alice@example.com admin 2026-01-15
bob@example.com member 2026-02-20
# JSON 모드
$ mytool user list --json
{
"data": [
{"email": "alice@example.com", "role": "admin", "created_at": "2026-01-15T00:00:00Z"},
{"email": "bob@example.com", "role": "member", "created_at": "2026-02-20T00:00:00Z"}
],
"meta": {"total": 2, "page": 1, "per_page": 50}
}
결과적으로 저는 json 출력을 기본으로 사용하고 --no-json을 추가하는 형태가 되었네요. 사람이 쓰지 않는 도구라면 입출력을 모두 JSON으로 통일하는게 작업 히트율이 가장 좋았습니다.
Versioning & Backward Compatibility
JSON 출력의 버저닝은 신경 써야 할 부분입니다. 몇 가지 전략을 공유하면:
- 필드 추가는 자유, 삭제/변경은 신중하게: 새 필드를 추가하는 건 하위 호환을 깨지 않지만, 기존 필드를 제거하거나 타입을 바꾸면 에이전트가 깨질 수 있음
- 버전 필드 포함: 응답에
"api_version": "v1"같은 필드를 넣어두면 에이전트가 버전에 따라 분기 가능 - Deprecation 경고: 제거 예정인 필드는 별도 warnings 배열에 알림
Using Pydantic / Zod / JSON Schema for Validation
스키마 정의에 사용할 수 있는 도구들입니다.
- Python: Pydantic이 가장 편합니다. 모델 정의 → JSON Schema 자동 생성 → 입력 검증까지 한 번에
- TypeScript/Node: Zod로 스키마를 정의하고
zod-to-json-schema로 변환 - Go/Rust 등: JSON Schema 파일을 직접 작성하거나, 코드에서 생성하는 라이브러리 활용
핵심은 코드에서 사용하는 타입 정의와 schema 명령어가 반환하는 스키마가 동일한 소스에서 나와야 한다는 겁니다. 이 둘이 따로 관리되면 결국 싱크가 깨집니다.
사실 진행하던 개발 프로젝트에선 이 부분을 크게 신경쓰진 않았습니다. 덕분에 여러번의 실패가 있었죠.
AI-Friendly Helper Flags
schema와 JSON 외에도 에이전트 친화적인 플래그들을 추가하면 좋습니다.
--dry-run: 실제 실행 없이 결과를 미리 확인. 에이전트가 안전하게 시도해볼 수 있음--explain: 명령어가 수행할 작업을 자연어로 설명. 에이전트의 계획 수립에 도움--output-format: json, yaml, csv 등 출력 포맷 선택--quiet: 불필요한 배너, 경고를 제거하고 핵심 출력만 반환--no-color: ANSI escape code 제거 (이건 사실 모든 CLI에 있어야 합니다)
Results, Lessons, and Caveats After Adoption
Quantitative & Qualitative Outcomes
앞서 JSON 전환 결과를 공유했는데, schema 명령어까지 추가한 뒤의 변화도 정리해보면 아래와 같습니다.
| 지표 | JSON만 | JSON + Schema |
|---|---|---|
| 태스크 성공률 | 90% 정도 | ~97% (거의 대부분 성공) |
| 에이전트의 첫 시도 정확도 | ~70% | ~90% ( 솔직히 이 구간이 좋아졌습니다) |
| 문서 참조 필요 횟수 | 태스크당 평균 1.2회 | 거의 0회 |
빈도가 크진 않아서 의미 있는 수치는 아닐 수 있습니다. 다만 정성적으로 더 크게 느낀 부분은 에이전트 코드의 복잡도가 확 줄었다는 점입니다. 파싱 로직이 사라지고 비즈니스 로직에 집중할 수 있게 됐거든요.
Common Failure Patterns We Observed
물론 만능은 아닙니다. 에이전트가 자주 실패하는 패턴과 해결책을 정리하면:
- 너무 큰 JSON 응답: 리스트 API에서 수천 개의 항목을 반환하면 에이전트의 컨텍스트 윈도우를 넘어감 → pagination과 필터링 필수
- 중첩이 깊은 구조: 5단계 이상 중첩된 JSON은 에이전트가 정확히 탐색하기 어려움 → 가능하면 flat하게
- enum 값 오류: schema에 enum이 정의되어 있어도 에이전트가 가끔 유사하지만 다른 값을 넣음 → 입력 검증 + 명확한 에러 메시지
- optional vs required 혼동: 에이전트가 optional 필드를 빠트리는 건 괜찮지만, required를 빠트리는 경우 발생 → schema에 required를 명확히 표시하고 에러 메시지에서 누락된 필드를 알려주기
Remaining Challenges
아직 해결하지 못한 부분도 있습니다.
- 복잡한 pagination: cursor 기반 pagination을 에이전트가 자연스럽게 처리하는 건 여전히 까다로움
- Binary data: 파일 업로드/다운로드 같은 바이너리 데이터 처리는 JSON으로 깔끔하게 표현하기 어려움
- Long-running operations: 수 분 이상 걸리는 작업의 상태 추적과 타임아웃 처리
이런 부분들은 아직 좋은 답을 찾지 못해서 계속 고민 중입니다. 특히 Long-running 부분은 대부분 에이전트가 sleep을 자체적으로 걸면서 체크하고 있던데 상당히 비 효율적으로 보입니다. 반대로 agent가 callback 받을 수 있어야 할텐데, 이게 쉽진 않네요.
Conclusion
정리하자면, 제가 AI-Friendly CLI를 만드는 핵심은 두 가지입니다.
- JSON-First I/O: 입출력을 구조화하여 에이전트가 안정적으로 파싱하고 활용할 수 있게 하기
- Schema Command: CLI 스스로 자신의 인터페이스를 설명할 수 있게 하여 문서 의존성 제거
이 두 가지만 갖춰도 에이전트 성능이 크게 향상되는 걸 직관했습니다. 당장 적용해보고 싶다면 가장 쉬운 시작점은 기존 CLI에 --json 플래그 하나를 추가하는 것입니다. 그것만으로도 에이전트와의 연동이 훨씬 수월해집니다.
다시 한번 CLI의 시대가 열렸습니다.
