구현 확인서
지시하신 구조가 실제로 그렇게 동작하는지 직접 확인하실 수 있도록 정리했습니다. 아래의 모든 출력과 수치는 실제로 실행해서 얻은 것이며, 예시나 추정치가 아닙니다.
01요구사항 대비 구현 내역
| 요구사항 | 구현 방식 | 검증 |
|---|---|---|
| 관리자 ↔ 검증자 통신 차단 | 발신 측 서비스 디스커버리 제한 + 수신 측 인가 검사 (2단) | 6.2 |
| 검증자 권한 우위 | 판정 Artifact 무결성 보장 — 중계자 편집·상태 승격 불가 | 6.3 |
| 데모 도메인: SW 개발 | 함수 구현 → 정적 검사 6종 + 샌드박스 실행 검증 | 6.1 |
| 비용 발생 억제 | 추론 백엔드 추상화 — 기본값이 무호출 구현체 | 7 |
| CLI 선행, GUI 후속 | 오케스트레이션 로직 단일화, 두 UI는 어댑터 | 6.4 |
| Windows · Linux | Linux 상시 가동. 전송 계층은 표준 라이브러리 | 8 |
기술 스택
두 축을 분리한 것이 이 구현의 핵심 설계 판단입니다 — 에이전트 사이는 A2A, 에이전트 안쪽은 LangGraph. 근거는 4장에 적었습니다.
02MVP 적용 범위
먼저 이번 MVP가 무엇을 처리하고 무엇을 처리하지 않는지 밝힙니다.
이번 MVP가 처리하는 작업은 "단일 Python 함수 생성" 한 가지입니다.
관리자는 실무자의 Agent Card에서 implement_function 스킬을 조회해 라우팅하고,
실무자는 review_code 스킬을 가진 검증자를 찾습니다.
현재 등록된 스킬이 이 둘뿐이라 파이프라인은 함수 생성·검증만 수행합니다.
범위를 벗어난 지시를 넣으면
보고서 작성을 요청한 실제 결과입니다.
[사람] 회사 매출 보고서를 작성해줘 [검증자] 반려 · [docstring] docstring 이 없는 함수: handle_request, ... [검증자] 통과 ──────────────────────────────────────────────── def handle_request(data): """회사 매출 보고서를 작성해줘""" ... ────────────────────────────────────────────────
보고서가 아니라 함수를 만듭니다. 지시를 거부하지 않고 함수 생성으로 처리합니다. 의도된 동작이며, 도메인을 확장하기 전까지는 이 상태입니다.
이 제약은 골격이 아니라 스킬 등록의 문제입니다. 새 작업 유형을 추가하려면 에이전트가 Agent Card에 스킬을 선언하고 상위 config에 한 줄 넣으면 되며, 호출하는 쪽 코드는 고치지 않습니다 (9장).
지시가 모호할 때
무엇을 만들지 판단할 수 없으면 사람에게 되묻고 대기합니다.
LangGraph의 interrupt()로 구현했습니다.
[사람] abc [관리자] 되물음 'abc' 만으로는 무엇을 만들지 알 수 없습니다. 어떤 기능의 함수인지 한 문장으로 알려 주세요.
작업이 실패로 끝나는 것이 아니라 INPUT_REQUIRED 상태로 멈춰 있다가,
같은 taskId로 답이 오면 중단 지점부터 이어서 실행합니다.
03구조
사람 (CLI 또는 웹 화면)
│
↓ 지시 / 최종 결재
관리자 Agent :8001 수신 [console] 위임 → worker
│
↓
실무자 Agent :8002 수신 [manager] 위임 → reviewer
│
↓ 재작업 루프 (상한 3회)
검증자 Agent :8003 수신 [worker] 끝단 — 위임 대상 없음
에이전트 셋은 각각 별개의 프로세스입니다. 한 프로그램 안의 함수가 아닙니다. 같은 프로세스에 넣으면 관리자 코드가 검증자 함수를 그냥 호출할 수 있어 "영향 범위 최소화"를 지킬 수 없습니다.
각자 자기 것만 안다
전체 배치를 아는 파일은 없습니다. 에이전트마다 config.yaml 하나씩만 읽고,
그 안에는 세 가지만 들어 있습니다.
- 자기 이름과 포트
accepts_from— 누구의 요청을 받을지delegates— 누구를 부를 수 있는지 (이름과 주소)
관리자의 config.yaml에는 검증자 주소가 아예 없습니다.
부르지 않기로 약속한 것이 아니라, 주소를 얻을 방법이 없습니다.
04기술 선택의 근거
확장하기 좋게 만들어 달라고 하신 요구가 이 두 선택을 결정했습니다.
왜 A2A 표준을 썼는가
에이전트 간 통신을 자체 규약으로 만들지 않고 A2A 표준 프로토콜을
그대로 채택했습니다. a2a-sdk 1.1.2의 공식 타입을 씁니다.
확장 계획에 직결되기 때문입니다. 끝단에 외부 전문가나 타사 에이전트를 붙이겠다고 하셨는데, 자체 규약이면 상대방이 우리 규약을 배워야 합니다. 표준을 쓰면 A2A를 말하는 쪽은 누구든 그대로 붙습니다.
| 요소 | 구현 |
|---|---|
| Agent Card | /.well-known/agent-card.json — 사양 1.0 및 v0.3 경로 양쪽 응답 |
| 메서드 | SendMessage · GetTask (v0.3 표기 message/send · tasks/get 도 수신) |
| 전송 | JSON-RPC 2.0 over HTTP |
| 상태 | submitted · working · input-required · completed · failed · rejected |
능력 발견이 실제로 동작합니다. 관리자는 실무자 이름을 코드에 갖고 있지 않습니다.
상대 Agent Card를 조회해 implement_function 스킬을 선언한 쪽을 고릅니다.
새 에이전트를 config에 넣고 그쪽이 스킬을 선언하면,
호출하는 코드를 고치지 않아도 찾아집니다.
$ curl http://127.0.0.1:8001/.well-known/agent-card.json
{"name": "manager",
"description": "사람의 지시를 받아 실무자에게 위임하고 결과를 보고한다",
"supportedInterfaces": [{"url": "http://127.0.0.1:8001",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"}],
"capabilities": {"streaming": false, "pushNotifications": false}, ...}
SDK의 protobuf 타입은 a2a_core/types.py 파사드 밖으로 나가지 않습니다.
에이전트 코드는 평범한 dataclass만 봅니다 — AI에게 추가 개발을 맡기실 때
protobuf를 몰라도 되도록 했습니다.
왜 LangGraph 를 썼는가
에이전트 내부의 작업 흐름은 LangGraph 상태 기계로 만들었습니다 (1.2.11).
if 문으로 짜도 지금은 돕니다. 그러나 재작업 루프·사람 개입·중단 후 재개가
얽히면 곧 흐름을 따라갈 수 없게 됩니다. 상용화까지 가신다고 하셨으므로 그래프로 두었습니다.
흐름이 그림과 코드에서 같은 모양입니다.
START → 지시 해석 → 실무자 선택 ─┬─ 찾음 ──▶ 위임 → END
│ └─ 못 찾음 ──▶ END (FAILED)
└─ 모호하면 interrupt → 사람에게 되묻기
START → 초안 작성 → 검증 요청 ─┬─ 통과 ──────────▶ END
▲ └─ 반려 ─┬─ 여유 있음 ─┐
└───────────────────────┘ │
└─ 상한 소진 ─▶ END (반려)
START → 문법 → 함수 존재 → docstring → 이름 규약
→ 예외 처리 → 미완성 표시 → 실행 검사 → END
검사 규칙을 늘리려면 목록에 함수를 추가하면 노드가 자동으로 생깁니다.
LangGraph 로 얻은 것 셋
- 중단·재개. 체크포인터의 스레드 키를 A2A
taskId로 맞췄습니다. 되물음으로 멈춘 작업에 같은taskId로 답을 보내면 그 노드부터 이어 돕니다. 처음부터 다시 실행하지 않습니다. - 상한이 구조로 강제됩니다. 재작업 횟수는 조건부 엣지가 판정하므로, 에이전트가 이를 우회할 경로가 없습니다.
- LLM 없이 동작합니다. LangGraph는 상태 기계 라이브러리로만 씁니다. 무료 모드에서도 그래프는 그대로 돕니다. 외부 추적 서비스 호출도 없습니다.
05실행 방법
# 1. 설치 — 다섯 프로젝트를 한 번에 (Python 3.13 이상) $ python deploy/setup_dev.py # 2. 에이전트 3종 기동 $ python deploy/run_all.py ──────────────────────────────────────────────────────── reviewer 127.0.0.1:8003 수신 ['worker'] (끝단) worker 127.0.0.1:8002 수신 ['manager'] → reviewer manager 127.0.0.1:8001 수신 ['console'] → worker ──────────────────────────────────────────────────────── # 3-a. CLI 로 지시 (지시 없이 실행하면 대화 모드) $ python -m console.cli "로그인 검증 함수를 만들어줘" # 3-b. 웹 화면 → http://127.0.0.1:8080 $ python -m console.web_server
06확인 항목
6.1 한 번의 작업이 끝까지 도는지
$ python -m console.cli "로그인 검증 함수를 만들어줘" A2A 콘솔 · 관리자 manager · http://127.0.0.1:8001 [사람] 로그인 검증 함수를 만들어줘 [검증자] 반려 · [docstring] docstring 이 없는 함수: authenticate, test_authenticate [검증자] 통과 [관리자] 결과 제출 ──────────────────────────────────────────────────────── def authenticate(user_id, password): """로그인 검증 함수를 만들어줘""" if user_id is None: raise ValueError("user_id 가 필요하다") return True def test_authenticate(): """authenticate 이 참을 돌려주는지 확인한다.""" assert authenticate("user_id", "password") is True ──────────────────────────────────────────────────────── 재작업 1회 · 검증 2회 · 두뇌 rule · 호출 3회 · 비용 0원
6.2 격리가 실제로 막는지
가장 중요한 확인입니다. 에이전트를 띄운 상태에서 아래를 그대로 실행해 보십시오.
$ curl -X POST http://127.0.0.1:8003/ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":"t1","method":"SendMessage","params":{"message": {"role":"ROLE_USER","content":[{"text":"검사해줘"}],"messageId":"m1", "metadata":{"issued_by":"manager"}}}}' {"jsonrpc": "2.0", "id": "t1", "error": {"code": -32003, "message": "'reviewer' 는 'manager' 의 요청을 받지 않는다. 허용: ['worker']"}}
$ curl -X POST http://127.0.0.1:8002/ ... "issued_by":"human" ... {"jsonrpc": "2.0", "id": "t2", "error": {"code": -32003, "message": "'worker' 는 'human' 의 요청을 받지 않는다. 허용: ['manager']"}}
거부되고, 작업 자체가 만들어지지 않습니다. 요청을 받아 처리하다 중간에 멈추는 것이 아니라 문 앞에서 돌려보냅니다.
| 겹 | 막는 지점 | 없으면 |
|---|---|---|
| 호출자 쪽 | 관리자 config에 검증자 주소가 없음 | — |
| 수신자 쪽 | 검증자가 worker 외의 발신자를 거부 | 주소를 알아내면 뚫림 |
지금 설정은 발신자 이름을 자기 신고로 받습니다. 위 curl 예시처럼
issued_by를 다른 이름으로 적으면 그 이름으로 취급됩니다.
로컬 데모(127.0.0.1 고정)에서는 충분하지만, 외부에 열린 환경에서는
config.yaml의 auth.token을 채워야 실제 차단이 됩니다.
자리는 만들어 두었고 값만 넣으면 동작합니다.
이번 범위에서 인증은 제외라고 하셨기에 비워 둔 상태입니다.
6.3 검증 결과가 편집되지 않는지
검증자의 판정문은 실무자를 거쳐 관리자로 올라갑니다. 실무자가 자기에게 불리한 반려를 요약하거나 눌러버리면 관리자는 알 수 없으므로, 실무자는 검증 결과를 손대지 않고 원문 그대로 첨부합니다. 상태를 통과로 올리는 것도 불가능합니다.
6.1의 출력에서 [검증자] 반려 아래에 붙은
[docstring] docstring 이 없는 함수: ...가 검증자가 쓴 문장 그대로입니다.
검증자가 하는 검사
| 종류 | 항목 |
|---|---|
| 정적 검사 6종 | 문법 · 함수 존재 · docstring · 이름 규칙(snake_case) · 빈 except 금지 · TODO 잔존 금지 |
| 실행 검사 | 격리된 별도 프로세스에서 test_ 함수 실행. 5초 초과 시 중단 |
검사 항목과 시간 제한은 검증자 config.yaml에서 조절합니다.
6.4 CLI 와 GUI 가 같은 로직을 쓰는지
console/orchestrator.py ← 로직은 전부 여기
├── cli.py ← 껍데기
└── web_server.py ← 껍데기
화면 쪽에는 판단이 없습니다. 회귀 테스트에 이 사실을 검사하는 항목이 들어 있습니다 —
web_server가 에이전트를 직접 부르면 테스트가 실패합니다.
[OK] web_server 는 orchestrator 를 import 만 한다
(로직 없음: A2AClient 직접 호출 없음)
GUI를 나중에 걷어내도 CLI는 그대로 돌고, 반대도 마찬가지입니다.
6.5 회귀 테스트
$ python tests/run_all.py 합계: 통과 185 · 실패 0
pytest 같은 외부 도구가 필요 없습니다. 표준 라이브러리만으로 돕니다. 네 묶음(공통 라이브러리 · 에이전트 · 콘솔 · 웹)으로 나뉘어 있습니다.
07비용
무료로 쓰실 때 (기본값)
아무 설정도 하지 않으면 BRAIN=rule입니다. API 호출 0건, 0원.
키가 없어도 돌고, 잔액이 떨어질 일도 없습니다. 위 6.1의 실행이 이 상태였습니다.
실제 모델을 붙이실 때
.env에 BRAIN=openai와 키를 넣으면 GPT-5 nano를 씁니다.
2회 호출 실측 ₩0.26입니다 (2026-09-13).
키가 없거나 패키지가 없으면 자동으로 무료 모드로 내려갑니다. 데모가 죽지 않습니다.
[brain] openai 를 쓸 수 없어 rule 로 돌린다 — OPENAI_API_KEY 가 없다
비용이 새지 않게 하는 장치
전부 config.yaml 값이라 파일에서 바로 고치실 수 있습니다.
| 항목 | 기본값 | 하는 일 |
|---|---|---|
| max_rework | 3 | 재작업 반복 상한 |
| max_llm_calls | 실무자 4 · 관리자 2 | 에이전트별 호출 상한 |
| max_depth | 5 | 위임 깊이 상한 |
| 순환 감지 | — | A→B→C→A 를 감지해 중단 |
0을 넣으면 해당 제한이 풀립니다. 상용 단계에서 쓰실 수 있습니다.
사용량은 사슬을 타고 올라와 관리자에서 합산되므로, 화면에 보이는 숫자가
전체 합계입니다.
08원격 데모 서버
설치 없이 바로 보실 수 있도록 리눅스 서버에 올려 두었습니다.
- 에이전트 3종이 systemd 서비스로 상시 가동 중입니다.
- 이 서버의 두뇌는
openai로 설정되어 있어 실제 모델이 붙은 상태를 보실 수 있습니다.
09확장하는 방법
| 하려는 것 | 해야 하는 일 |
|---|---|
| 같은 종류를 하나 더 실무자 2번 |
config.yaml을 복사해 이름과 포트만 바꾸고, 관리자 delegates에 한 줄 추가.
코드는 건드리지 않습니다. |
| 새로운 종류의 에이전트 | 프로젝트 폴더 하나를 만들고, 부를 쪽 delegates와 받을 쪽
accepts_from에 이름을 넣습니다. |
| 끝단에 사람이나 외부 에이전트 |
검증자처럼 A2A 주소를 갖는 참여자로 등록합니다. 위임 깊이 상한과 순환 감지가 이미 들어 있어 사슬이 길어져도 무한히 도는 일은 없습니다. |
세 경우 모두 A2A 표준과 Agent Card 능력 발견 덕분에 성립합니다. 호출하는 쪽은 상대의 이름이 아니라 스킬을 찾기 때문입니다.
지금 범위 밖인 것
- 인증 (
auth.token자리는 만들어 두었고 값만 비어 있습니다) - 클라우드 배포 · DB 영속화 (지금은 메모리에만 기록됩니다)
- 1:N 라우팅 정책 — 구조는 1:N을 받아들이지만 MVP는 각 1개씩입니다
- 스트리밍 (A2A 사양에는 있으나 이번 범위 밖)
- 모바일 전용 화면
10확인해 주실 것
직접 돌려 보시고 아래가 기대와 다르면 알려 주십시오.
별첨화면 구성
웹 콘솔을 실제로 돌려 캡처한 화면입니다. 8장의 데모 서버에서 같은 화면을 보실 수 있습니다.
화면은 진행 상황이 실시간으로 갱신됩니다 (SSE). 새로고침하지 않아도 검증 판정이 나오는 대로 트리에 붙습니다.