4. 함부로 실행하지 않는 Agent

직접 돌려 보기

이 차시를 마치면

검증을 직접 재현하고, 자기 프로젝트에 옮긴 뒤 다시 확인합니다.

앞 차시의 검증을 직접 재현하고, 자기 프로젝트에 옮기는 방법입니다. 모든 명령은 2026-08-15 에 실제로 실행해 확인했습니다.

이 차시의 명령은 2026-08-15 에 모두 직접 실행해 확인했습니다. 아래 출력은 그때 받은 것을 그대로 옮긴 것입니다. 판이나 환경이 다르면 숫자가 달라질 수 있습니다.

준비물

필요한 것이유없으면
Python 3.10 이상langgraph 가 요구하는 최소 버전Docker 로 대신합니다 (아래 B안)
langgraph그래프 실행에 필요도구 검증만 가능합니다
Docker호스트 파이썬이 낮을 때A안으로 진행합니다

A안 — 호스트에 Python 3.10 이상이 있는 경우

python3 --version          # 3.10 이상인지 확인
python3 -m venv .venv
source .venv/bin/activate  # Windows PowerShell: .\.venv\Scripts\Activate.ps1
pip install -U langgraph
python -m unittest discover -p "test_*.py" -v

B안 — 호스트 파이썬이 3.10 보다 낮은 경우

머신에 파이썬을 새로 설치하지 않고 컨테이너 안에서 실행합니다. 이 교재의 검증도 이 방식으로 했습니다.

Dockerfile 은 다섯 줄입니다.

FROM python:3.12-slim
WORKDIR /work
RUN pip install --no-cache-dir --disable-pip-version-check langgraph
COPY . /work
CMD ["python", "-m", "unittest", "discover", "-p", "test_*.py", "-v"]
docker build -t llmwiki-verify .
docker run --rm -v "$PWD":/work -w /work llmwiki-verify \
  python -m unittest discover -p "test_*.py" -v

Docker 데몬이 꺼져 있으면 failed to connect to the docker API 가 나옵니다. Docker Desktop 을 먼저 실행합니다.

이 방식이 교재 내용과 이어지는 지점

1번 트랙에서 "LLM 이 만든 코드는 격리 실행하라"고 배웠습니다. 여기서 쓰는 컨테이너가 바로 그 격리입니다. 검증 자체가 isolated work directory 원칙을 따르고 있습니다.

파일 배치

verify/
├─ Dockerfile
├─ run_verify.sh          실행 스크립트
├─ sec_state.py           State 설계        (3차시)
├─ sec_tools.py           Tool 권한 최소화  (4차시)
├─ sec_graph.py           HITL 그래프       (5차시)
├─ test_sec_tools.py      ① 권한 최소화 검증 10개
├─ test_sec_graph.py      ②③④ 와 종료 조건 검증 12개
├─ test_audit.py          감사 회귀 테스트 16개
├─ probe_resume.py        재개 시 재실행 계측
├─ probe_concurrency.py   동시 재개 관찰
└─ probe_injection.py     prompt injection 실측 (9차시)

한 번에 실행하기

./run_verify.sh          # 전체 검증 (Docker 필요)
./run_verify.sh tools    # 도구 검증만 (langgraph 불필요)

tools 옵션은 langgraph 없이도 돌아갑니다. Python 3.9 에서도 10개 테스트가 통과합니다. 환경 준비가 끝나기 전에 권한 차단부터 확인하고 싶을 때 씁니다.

기대 출력

== 전체 검증 ==
Ran 38 tests in 0.076s

OK

== 재개 시 재실행 계측 ==
interrupt 앞 코드가 실행된 횟수: 2
→ 확인됨: 재개 시 interrupt 앞의 코드가 다시 실행됩니다.
앞선 별도 node 가 실행된 횟수: 1
→ 이미 끝난 앞 node 는 재실행되지 않았습니다.

== 환경 ==
Python 3.12.14
langgraph 1.2.11

직접 손으로 돌려 보기

테스트 말고 실제 흐름을 보고 싶다면 이렇게 합니다.

from langgraph.types import Command

from sec_graph import build_graph

graph = build_graph()
config = {"configurable": {"thread_id": "demo-1"}}

paused = graph.invoke(
    {
        "request": "고객 목록 삭제",
        "requester_id": "user-01",
        "plan": "",
        "draft": "",
        "evidence_refs": [],
        "risk_level": "low",
        "pending_action": "",
        "approved": False,
        "retry_count": 0,
        "executed_action_id": None,
        "result": "",
        "audit_log": [],
    },
    config,
)

print("멈춤 여부:", "__interrupt__" in paused)
print("사람에게 보여 줄 내용:", paused["__interrupt__"][0].value)

resumed = graph.invoke(Command(resume="approve"), config)
print("결과:", resumed["result"])
print("감사 로그:")
for line in resumed["audit_log"]:
    print("  -", line)

실제 출력입니다.

멈춤 여부: True
사람에게 보여 줄 내용: {'question': '이 작업을 실행할까요?', 'risk_level': 'high',
 'pending_action': "[high] '고객 목록 삭제'에 대한 초안",
 'options': ['approve', 'edit', 'reject']}
결과: 실행 완료: [high] '고객 목록 삭제'에 대한 초안
감사 로그:
  - draft 생성 (requester=user-01)
  - validate 완료 (risk=high)
  - prepare_action 완료 (action_id=539ba09691612c49). 아직 실행하지 않음
  - 사람 결정: approve
  - side effect 실행 (action_id=539ba09691612c49)

승인 화면의 pending_action 과 실행 결과가 정확히 같고, 키(539ba09691612c49)가 prepare 부터 실행까지 유지되는 것을 확인할 수 있습니다.

request 를 "회의록 요약" 으로 바꾸면 risk_levellow 가 되어 멈추지 않고, auto_approve 를 지나 실행까지 끝납니다. 감사 로그에 "저위험으로 자동 승인" 이 남습니다.

자기 프로젝트에 옮길 때

이 코드는 골격입니다. 실제로 쓰려면 네 곳을 바꿉니다.

#위치무엇을 바꾸는가
1make_draft문자열 조립을 실제 LLM 호출로 바꿉니다. 여기서는 side effect 를 만들지 않습니다
2validatehigh_risk_markers업무에 맞는 위험 기준으로 바꿉니다. 가능하면 호출되는 도구 기준을 함께 봅니다
3execute_side_effect 의 주석 자리실제 발송·삭제·결제 코드를 넣습니다. approved 검사 뒤여야 하고, 반드시 state["pending_action"] 을 대상으로 해야 승인한 것과 같아집니다
4InMemorySaver_EXECUTED_ACTION_IDSdurable 저장소로 바꿉니다. 7차시에서 다룬 내용입니다

바꾼 뒤 반드시 다시 돌릴 것

네 곳을 고친 다음에도 38개 테스트가 그대로 통과해야 합니다.

./run_verify.sh

특히 3번을 고친 뒤에는 아래 두 테스트를 확인합니다.

  • test_unapproved_state_never_executes — 미승인 상태에서 실행되지 않는가
  • test_same_action_id_executes_once — 같은 작업이 두 번 실행되지 않는가

이 둘이 깨지면 3번 트랙의 C-5 를 저지른 것입니다. 실제 코드를 approved 검사보다 앞에 두었거나, 중복 방어를 지나쳤다는 뜻입니다.

왜 테스트를 지우면 안 되는가

실제 코드를 넣으면 테스트가 실패할 수 있습니다. 그때 테스트를 고치고 싶은 유혹이 생깁니다. 학습팩은 이를 C-6 으로 금지합니다. 실패하는 테스트를 원인 수정 없이 삭제해서 'PASS' 로 만들면, 다음에 같은 사고가 났을 때 알려 줄 장치가 사라집니다.

문제가 생기면

증상원인과 조치
ModuleNotFoundError: No module named 'langgraph'가상환경을 켜지 않았거나 설치가 안 됐습니다. (.venv) 표시를 확인합니다
failed to connect to the docker APIDocker 데몬이 꺼져 있습니다. Docker Desktop 을 실행합니다
테스트가 SKIPPED실행되지 않았다는 뜻입니다. PASS 가 아니므로 원인을 찾습니다
재개했는데 __interrupt__ 가 계속 나옴같은 thread_id 를 쓰고 있는지 확인합니다. 다른 값이면 새 실행으로 시작됩니다
감사 로그가 마지막 것만 남음audit_logReducer 가 빠졌습니다. Annotated[list[str], operator.add] 를 확인합니다

정리

여기까지 하면 승인 없이는 위험한 일을 하지 못하는 Agent 를 직접 돌려 보고, 고친 뒤에도 그 성질이 유지되는지 확인할 수 있습니다.

확인 필요 로 남은 항목들(durable Checkpointer, 실제 LLM 연결, 승인자 인증)은 각자의 환경에서 이어서 확인해야 합니다.

다음 차시에서 지금까지의 방어로 막을 수 없는 위협 하나를 다룹니다. 구조로 막는 방법을 배웠으니, 구조로 막을 수 없는 것도 알아야 합니다.

이해도 확인

문항을 불러오는 중입니다.