• **CLI 기반 에이전트 설계 (Agent CLI Architecture)**는 LLM 에이전트에게 여러 개의 typed tool 대신 단일 실행 인터페이스(run)를 제공하는 설계 패턴
  • Manus 백엔드 리드의 프로덕션 경험에서 도출된 운영 중심 아키텍처 제안
  • 에이전트의 툴 선택 부담을 줄이고, 출력/에러/권한을 표준화하는 것이 핵심 목표

한 줄 정의: 에이전트가 할 일을 스크립트로 만들고, 출력을 LLM 친화적으로 설계해서, SKILL.md에 Bash 실행으로 명시하는 패턴

왜? Claude가 매 턴마다 툴 선택/변환/판단하면 토큰과 시간이 낭비되니까, 정답 루틴을 스크립트에 박아서 호출 1번으로 줄인다

어떻게? scripts/에 Python 스크립트를 만들고, 출력은 성공 1줄 / 실패 시 원인+힌트로 설계하고, SKILL.md에 Bash("python scripts/xxx.py")로 명시한다

스크립트 에러 처리 핵심 — B와 C의 차이가 여기서 갈림:

# wiki-publish.py — 개발자가 직접 코딩
try:
    response = requests.post(...)
    print(f"Created: {url}")                           # 성공: 1줄
except HTTPError as e:
    print("ERROR(401): Authentication failed.", file=sys.stderr)
    print("Fix: export CONFLUENCE_TOKEN=<your-token>", file=sys.stderr)
    print(f"[exit:1 | {elapsed}ms]", file=sys.stderr)  # 메타데이터
    sys.exit(1)

B (LLM 친화적 출력):

ERROR(401): Authentication failed.
Fix: export CONFLUENCE_TOKEN=<your-token>
[exit:1 | 340ms]

C (사람용 출력 — 모델에게 불친절):

Traceback (most recent call last):
  File "publish.sh", line 47, in upload
    response = requests.post(url, ...)
requests.exceptions.HTTPError: 401 Client Error

해당 개념이 필요한 이유

  • LLM 에이전트에 툴을 많이 줄수록 툴 선택 오류, 파라미터 실수, 불필요한 호출이 증가
  • 중간 결과(긴 텍스트, JSON 덩어리)가 컨텍스트를 오염시켜 이전 대화가 밀려남
  • 프로덕션에서 바이너리 읽기, stderr 누락, 대용량 출력 같은 예측 가능한 실패 패턴이 반복됨
  • 기존 typed tool 방식은 KV cache를 깨뜨려 비용/지연이 커짐

AS-IS: 툴 N개 방식 (Tool Catalog)

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude (LLM)
    participant T1 as read_file
    participant T2 as search_page
    participant T3 as create_page

    U->>C: "spec.md를 위키에 올려줘"
    C->>T1: read_file("spec.md")
    T1-->>C: md 전체 내용 (3000토큰)
    Note over C: 변환 로직 직접 수행 (2000토큰 생성)
    C->>T2: search_page(space, title)
    T2-->>C: 검색 결과
    Note over C: "없네, create 해야지" 판단
    C->>T3: create_page(space, title, body)
    T3-->>C: 성공 응답
    C->>U: "완료했습니다"

TO-BE: run 1개 방식 (Agent CLI)

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude (LLM)
    participant R as run()
    participant S as wiki-publish (스크립트)
    participant API as Confluence API

    U->>C: "spec.md를 위키에 올려줘"
    C->>R: run("wiki-publish spec.md --space ABC")
    R->>S: 스크립트 실행
    S->>S: md 읽기 + 변환
    S->>API: 검색 → 없으면 create
    API-->>S: 201 Created
    S-->>R: "Created: https://wiki.com/12345"
    R-->>C: 출력 1줄 (~20토큰)
    C->>U: "완료: https://wiki.com/12345"

3가지 방식 비교: Skill vs Agent CLI vs Shell Script

같은 작업(md → Confluence 업로드)을 세 방식으로 수행할 때의 차이.

md-to-wiki-converter 스킬의 목적:

  • Markdown 파일을 Confluence 페이지로 변환/업로드
  • Mermaid 다이어그램을 PlantUML로 변환 (Confluence에 Mermaid 플러그인 미설치)
  • 기존 페이지가 있으면 update, 없으면 create

A) Skill 방식: Claude가 MCP 툴을 단계별로 직접 호출

SKILL.md가 **“이 순서로 이 툴들을 호출해라”**라는 지시서 역할을 하고, Claude가 매 단계마다 어떤 MCP 툴을 호출할지 판단하며 실행한다.

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude
    participant R as Read
    participant M1 as confluence_search
    participant M2 as confluence_create

    U->>C: "spec.md를 위키에 올려줘"
    C->>R: Read("spec.md")
    R-->>C: md 전체 내용 (3000토큰)
    Note over C: Mermaid→PlantUML 직접 변환 (800토큰 생성)
    Note over C: md→Storage Format 직접 변환 (2000토큰 생성)
    C->>M1: confluence_search(space, title)
    M1-->>C: 결과: 페이지 없음
    Note over C: "없으니 create 해야지" 판단
    C->>M2: confluence_create_page(space, title, body)
    M2-->>C: 성공
    C->>U: "완료했습니다"
    Note over C: 총 5턴, ~5800토큰 누적

B) Agent CLI 방식: 스크립트가 처리하고 Claude는 run() 1번

개발자가 만든 전용 커맨드(wiki-publish)가 읽기/변환/업로드를 모두 처리. Claude는 명령 한 줄만 실행하고, 출력은 LLM 친화적으로 표준화되어 있다.

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude
    participant R as run()
    participant S as wiki-publish
    participant API as Confluence API

    U->>C: "spec.md를 위키에 올려줘"
    C->>R: run("wiki-publish spec.md --space ABC")
    R->>S: 스크립트 실행
    S->>S: md 읽기
    S->>S: Mermaid→PlantUML 변환
    S->>S: md→Storage Format 변환
    S->>API: search → 없으면 create
    API-->>S: 201 Created
    S-->>R: "Created: https://wiki.com/12345"
    R-->>C: 출력 1줄 (20토큰)
    C->>U: "완료: https://wiki.com/12345"
    Note over C: 총 1턴, ~50토큰 누적

실패 시 출력 (LLM 친화적):

ERROR(401): Authentication failed.
Fix: export CONFLUENCE_TOKEN=<your-token>
[exit:1 | 340ms]

C) Shell Script 방식: 스크립트가 처리하지만 출력이 사람용

구조는 B와 동일하지만, 출력이 개발자(사람)용으로 설계되어 있다.

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude
    participant R as run()
    participant S as publish.sh
    participant API as Confluence API

    U->>C: "spec.md를 위키에 올려줘"
    C->>R: run("bash publish.sh spec.md")
    R->>S: 스크립트 실행
    S->>S: md 읽기 + 변환
    S->>API: search → create
    API-->>S: 201 Created
    S-->>R: 로그 + JSON 80줄 (~500토큰)
    R-->>C: 장황한 출력
    Note over C: JSON 파싱해서 핵심 찾아야 함
    C->>U: "완료한 것 같습니다"
    Note over C: 총 1턴이지만 출력 노이즈 큼

실패 시 출력 (사람용, 모델에게 불친절):

Traceback (most recent call last):
  File "publish.sh", line 47, in upload
    response = requests.post(url, ...)
requests.exceptions.HTTPError: 401 Client Error

비교표

A) Skill (MCP)B) Agent CLIC) Shell Script
모델이 호출하는 것MCP 툴 여러 개run() 1번 + 전용 커맨드run() 1번 + bash 스크립트
중간 판단매 단계 Claude가 판단스크립트 내부스크립트 내부
출력툴마다 구조화된 응답모델 친화적 (1~2줄)사람용 (로그+JSON)
실패 시 복구Claude 재량stderr에 힌트 내장스택트레이스 그대로
보안/권한툴 단위 제한allowlist로 제한제한 없음
토큰 소모~5800~50~50 + 출력 노이즈
턴 수5턴1턴1턴

B 방식과 모델 자율성의 관계

B 방식은 모델의 자율성을 단순히 “줄이는” 것이 아니라, 자율성이 발휘되는 범위를 바꾸는 것이다. N개 툴 중 뭘 고를지, 어떤 순서로 조합할지를 모델이 매번 결정하는 부담은 줄어들지만, 어떤 명령을 어떤 옵션으로 실행할지, 결과를 보고 다음 명령을 어떻게 바꿀지 같은 문제 해결 전략에 대한 자율성은 그대로 남는다.

B와 C의 차이: 구조가 아니라 출력 설계

B와 C는 **구조(스크립트를 실행한다)**는 완전히 같다. 차이는 오직 출력을 누구를 위해 설계했느냐에 있다. 쉘 스크립트는 사람이 터미널에서 보기 좋게 만들고, Agent CLI는 모델이 읽고 즉시 다음 행동을 결정할 수 있게 만든다. 이 차이가 아래 “LLM 프레젠테이션 레이어” 섹션의 핵심이다.

B와 C의 핵심 차이: LLM 프레젠테이션 레이어

B가 “그냥 쉘 스크립트”와 다른 이유는 출력이 모델을 위해 설계되었다는 점이다.

설계 요소B (Agent CLI)C (Shell Script)
성공 출력URL 한 줄로그 + JSON 덩어리
실패 출력ERROR(401): ... Set CONFLUENCE_TOKEN스택트레이스 20줄
대용량 출력200줄 초과 시 자동 truncation + 파일 저장 안내전부 출력
바이너리 감지ERROR: binary file. Use: see <file>깨진 문자 출력
메타데이터[exit:0 | 12ms] footer없음

B 방식의 두 가지 층위

“서비스 전용 CLI를 만든다”가 전부가 아니다. B 방식에는 두 가지 층위가 있다.

층위 1: 기존 OS 명령어를 run()으로 사용

run("cat app.log | grep ERROR | wc -l")     # 이미 있는 도구 조합
run("sed -n '120,160p' server.log")         # 이미 있는 도구

층위 2: 서비스 전용 CLI를 만들어서 사용

run("wiki-publish spec.md --space ABC")      # 직접 만든 커맨드
run("wiki-preview spec.md")                  # 직접 만든 커맨드

핵심: 두 층위 모두를 감싸는 가드레일 (Presentation Layer)

run(command)
  └─ Presentation Layer (글의 핵심 제안)
       ├─ Binary Guard: 바이너리면 차단 + 대안 안내
       ├─ Overflow Mode: 200줄/50KB 초과 시 truncation + 파일 저장
       ├─ stderr 보존: 실패 시 원인 항상 포함
       └─ 메타데이터 footer: [exit:0 | 12ms]

가드레일이 들어가는 위치는 층위마다 다르다

**층위 2 (직접 만든 스크립트)**에서는 개발자가 try/except로 에러를 잡아서 LLM 친화적으로 출력하도록 스크립트에 직접 코딩한다.

**층위 1 (기존 OS 명령어)**에서는 cat, grep은 남이 만든 것이라 출력을 바꿀 수 없다. Reddit 저자는 이 문제를 run() 자체를 감싸는 래퍼로 해결했다. subprocess 출력을 가로채서 Binary Guard, Overflow 등을 처리하는 방식이다. 이건 에이전트 런타임을 직접 만들었기 때문에 가능한 것이다.

Claude Code 환경에서는 Bash 도구를 Anthropic이 만들었기 때문에 우리가 수정할 수 없다. 따라서 직접 만든 스크립트(층위 2)에만 가드레일을 넣는 것이 현실적이다.

run()과 CLI라는 용어에 대해

여기서 말하는 run()은 Claude Code의 Bash 도구와 같은 것이다. 이미 존재하며 개발자가 새로 만들 필요가 없다. Reddit 저자는 Manus의 에이전트 런타임을 직접 구축했기 때문에 run()이라는 도구를 정의한 것이고, Claude Code에서는 Bash가 그 역할을 한다.

CLI(Command Line Interface)는 터미널에서 명령어를 치는 방식 자체를 뜻한다. git status, python script.py 같은 것이 CLI 사용의 예시다. “Agent CLI 방식”이란, Claude가 사람이 터미널에서 명령어 치듯이 Bash("python scripts/xxx.py")로 작업하는 패턴을 의미한다.

프로덕션 실패 사례 (수치 포함)

Manus 운영에서 실제 발생한 실패들.

사례 1: 바이너리 cat — 20회 반복 후 강제 종료

sequenceDiagram
    autonumber
    participant C as Claude
    participant R as run()
    participant F as 182KB PNG 파일

    C->>R: run("cat architecture.png")
    R->>F: 읽기
    F-->>R: 바이너리 데이터 (182KB)
    R-->>C: 깨진 문자열 (컨텍스트 오염)
    Note over C: 의미 없는 토큰... cat -f 시도?
    C->>R: run("cat -f architecture.png")
    R-->>C: 실패
    Note over C: cat --format? cat --binary?
    C->>R: run("cat --format architecture.png")
    R-->>C: 실패
    Note over C: ... 20회 반복 후 강제 종료

해결: Binary Guard — control character ratio > 10%면 차단

ERROR: binary file detected (182KB PNG)
Use: see architecture.png (image viewer)
[exit:1 | 5ms]

사례 2: stderr 누락 — 10회 블라인드 재시도

sequenceDiagram
    autonumber
    participant C as Claude
    participant R as run()
    participant OS as OS

    C->>R: run("pip install pymupdf")
    R->>OS: pip install pymupdf
    OS-->>R: exit 127 + stderr "pip: command not found"
    R-->>C: exit 127 (stderr 버림!)
    Note over C: 왜 실패? 다른 방법 시도...
    C->>R: run("pip3 install pymupdf")
    R-->>C: exit 127
    C->>R: run("python -m pip install pymupdf")
    R-->>C: exit 127
    Note over C: ... 10회 반복 × ~5초 추론 = ~50초 낭비

해결: stderr를 항상 포함

pip: command not found
Fix: install pip or use python3 -m pip
[exit:127 | 12ms]

사례 3: 5000줄 로그 — 컨텍스트 폭발

sequenceDiagram
    autonumber
    participant C as Claude
    participant R as run()
    participant F as server.log

    C->>R: run("cat server.log")
    R->>F: 읽기
    F-->>R: 5000줄, 198.5KB
    R-->>C: 전체 내용 (컨텍스트 폭발!)
    Note over C: attention 망가짐, 이전 대화 유실

해결: Overflow Mode (200줄 / 50KB 기준)

sequenceDiagram
    autonumber
    participant C as Claude
    participant R as run() + Overflow Guard
    participant F as server.log

    C->>R: run("cat server.log")
    R->>F: 읽기
    F-->>R: 5000줄, 198.5KB
    Note over R: 200줄 초과 감지! Overflow Mode
    R-->>C: 앞 200줄 + 안내 메시지
    Note right of C: --- truncated (5000 lines, 198.5KB) ---<br/>Saved: /tmp/server.log<br/>Use: grep/tail to explore
    C->>R: run("grep ERROR /tmp/server.log | tail -20")
    R-->>C: ERROR 라인 20줄
    C->>R: run("grep ERROR /tmp/server.log | wc -l")
    R-->>C: "42"
    Note over C: 3번 호출로 문제 좁힘, 컨텍스트 <2KB

MCP 환경에서의 현실적 제약

순수 B 방식은 스크립트가 API를 직접 호출해야 하므로, MCP만 있고 직접 API 접근이 불가능하면 순수 B는 불가능하다. MCP 툴은 Claude만 호출할 수 있고, 스크립트에서는 호출할 수 없기 때문이다.

sequenceDiagram
    autonumber
    participant S as 스크립트
    participant MCP as MCP Server
    participant API as Confluence API
    participant C as Claude

    Note over S,API: 순수 B가 원하는 것
    S->>API: REST API 직접 호출
    API-->>S: 응답
    Note over S,API: ❌ MCP만 있으면 불가

    Note over C,API: MCP 환경의 현실
    C->>MCP: confluence_create_page(...)
    MCP->>API: REST API 호출
    API-->>MCP: 응답
    MCP-->>C: 결과
    Note over C,API: ⭕ 이것만 가능

하이브리드 접근법: 스크립트 + MCP 조합

MCP 환경에서 B의 장점을 최대한 살리는 현실적 방법. 스크립트로 뺄 수 있는 것(변환 로직)은 스크립트로 빼고, MCP로만 가능한 것(API 호출)은 MCP로 남긴다.

sequenceDiagram
    autonumber
    participant U as 사용자
    participant C as Claude
    participant R as run()
    participant S as wiki-convert.py
    participant M1 as confluence_search
    participant M2 as confluence_create

    U->>C: "spec.md를 위키에 올려줘"

    rect rgb(230, 245, 230)
        Note over R,S: 스크립트 영역 (토큰 절감)
        C->>R: run("python wiki-convert.py spec.md")
        R->>S: 실행
        S->>S: md 읽기
        S->>S: md→Storage Format 변환
        S->>S: Mermaid 블록 추출 → placeholder 삽입
        S-->>R: "Converted: body.html / Mermaid: 2 blocks"
        R-->>C: 출력 1줄 (20토큰)
    end

    rect rgb(255, 240, 230)
        Note over C: Claude 영역 (스크립트로 불가능한 부분)
        C->>C: mermaid_1.mmd 읽고 PlantUML로 변환 (800토큰)
        C->>C: body.html의 placeholder를 PlantUML 매크로로 교체
    end

    rect rgb(230, 235, 255)
        Note over M1,M2: MCP 영역 (API 접근은 MCP로만 가능)
        C->>M1: confluence_search(space, title)
        M1-->>C: 결과: 페이지 없음
        C->>M2: confluence_create_page(space, title, body)
        M2-->>C: 성공
    end

    C->>U: "완료: https://wiki.com/12345"
    Note over C: 총 4턴, ~900토큰 (A 대비 85% 절감)

하이브리드의 토큰 절감 효과

순수 B만큼은 아니지만 충분한 효과가 있다. md 읽기 + Storage Format 변환을 스크립트가 처리하면서 md 원문(3000토큰) + 변환 결과(2000토큰)가 컨텍스트에 들어오지 않게 된다. MCP 호출(search, create/update)은 여전히 Claude가 하므로 그 부분의 토큰은 줄지 않지만, 전체적으로 A(~5800토큰) → 하이브리드(~900토큰)으로 ~85% 절감된다. md 파일이 길수록 효과가 커진다.

Mermaid→PlantUML을 스크립트로 못 하는 이유

Mermaid→PlantUML 방향의 Python 라이브러리가 존재하지 않는다. 오픈소스 도구는 대부분 반대 방향(PlantUML→Mermaid)이고, Chrome 확장과 온라인 도구만 존재한다.

대안으로 Mermaid→이미지(PNG) 변환은 mmdc(Mermaid CLI)로 가능하지만, 이미지로 첨부하면 이후 다이어그램을 수정할 수 없어 실용성이 떨어진다. Confluence Mermaid 플러그인(공식, 무료)이 있으면 변환 자체가 불필요하지만, 회사에서 플러그인을 설치하지 않는 경우 Claude가 직접 변환하는 수밖에 없다. 그래서 하이브리드에서 Mermaid→PlantUML은 Claude 영역으로 남긴다.

실제로 무엇을 개발하는가

결국 하는 일은 기존 스킬의 scripts/ 디렉토리에 Python 스크립트를 추가하고, SKILL.md에 Bash 실행으로 명시하는 것이다. 새로운 프레임워크나 런타임을 만드는 게 아니라, 이미 하고 있는 패턴을 더 적극적으로 활용하는 것이다.

기존 패턴 (이미 하고 있는 것)

claude/skills/pr-creator/
  ├── SKILL.md
  └── scripts/
      ├── analyze_commits.py      ← Claude 대신 커밋 분석
      ├── generate_pr_body.py     ← Claude 대신 PR body 생성
      └── auto_fix_compile.py     ← Claude 대신 컴파일 에러 수정

SKILL.md에서:

Phase 1: run("python scripts/analyze_commits.py --changed-files ...")
Phase 4: run("python scripts/generate_pr_body.py --ticket ...")

pr-creator는 이미 “Claude가 직접 분석하는 대신 스크립트에 맡기는” Agent CLI 패턴을 부분적으로 쓰고 있다.

같은 패턴을 md-to-wiki-converter에 적용

claude/skills/md-to-wiki-converter/
  ├── SKILL.md
  └── scripts/
      └── wiki-convert.py    ← 이 파일 하나가 핵심

SKILL.md 변경:

Before: "Read로 md 읽어 → 직접 변환해 → MCP 호출해"
After:  "run(wiki-convert.py) 실행해 → Mermaid만 변환해 → MCP 호출해"

스크립트의 역할은 md 읽기 + Storage Format 변환 + Mermaid 블록 추출(placeholder 삽입)이고, 출력은 LLM이 바로 다음 행동을 결정할 수 있도록 핵심 정보만 1~2줄로 설계한다.

언제 어떤 방식을 쓸 것인가

상황추천 방식
작업이 유동적이고 탐색적일 때A (Skill/MCP)
작업이 정형화/반복적이고 API 직접 접근 가능B (Agent CLI)
작업이 정형화/반복적이고 MCP만 가능하이브리드 (스크립트 + MCP)
사용자와 핑퐁이 많은 워크플로A (Skill/MCP)
실패 복구를 예측 가능하게 통제하고 싶을 때B 또는 하이브리드

핵심 수치 요약

항목수치
툴 수 변화N개 → 1개(run)
호출 수 절감 예시3 calls → 1 call
체인 연산자4개 (|, &&, ||, ;)
바이너리 사고182KB PNG, 20 iterations 후 종료
stderr 누락 사고10 calls × ~5s 추론 (exit 127)
Overflow 기준200 lines / 50KB
대용량 해결 결과3 calls, <2KB context
시스템프롬프트 힌트명령당 ~50 tokens
하이브리드 토큰 절감~5800 → ~900 (~85%)

참고 문서