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_level 이 low 가 되어 멈추지 않고, auto_approve 를 지나 실행까지 끝납니다. 감사 로그에 "저위험으로 자동 승인" 이 남습니다.
자기 프로젝트에 옮길 때
이 코드는 골격입니다. 실제로 쓰려면 네 곳을 바꿉니다.
| # | 위치 | 무엇을 바꾸는가 |
|---|---|---|
| 1 | make_draft | 문자열 조립을 실제 LLM 호출로 바꿉니다. 여기서는 side effect 를 만들지 않습니다 |
| 2 | validate 의 high_risk_markers | 업무에 맞는 위험 기준으로 바꿉니다. 가능하면 호출되는 도구 기준을 함께 봅니다 |
| 3 | execute_side_effect 의 주석 자리 | 실제 발송·삭제·결제 코드를 넣습니다. approved 검사 뒤여야 하고, 반드시 state["pending_action"] 을 대상으로 해야 승인한 것과 같아집니다 |
| 4 | InMemorySaver 와 _EXECUTED_ACTION_IDS | durable 저장소로 바꿉니다. 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 API | Docker 데몬이 꺼져 있습니다. Docker Desktop 을 실행합니다 |
테스트가 SKIPPED | 실행되지 않았다는 뜻입니다. PASS 가 아니므로 원인을 찾습니다 |
재개했는데 __interrupt__ 가 계속 나옴 | 같은 thread_id 를 쓰고 있는지 확인합니다. 다른 값이면 새 실행으로 시작됩니다 |
| 감사 로그가 마지막 것만 남음 | audit_log 의 Reducer 가 빠졌습니다. Annotated[list[str], operator.add] 를 확인합니다 |
정리
여기까지 하면 승인 없이는 위험한 일을 하지 못하는 Agent 를 직접 돌려 보고, 고친 뒤에도 그 성질이 유지되는지 확인할 수 있습니다.
확인 필요 로 남은 항목들(durable Checkpointer, 실제 LLM 연결, 승인자 인증)은 각자의 환경에서 이어서 확인해야 합니다.
다음 차시에서 지금까지의 방어로 막을 수 없는 위협 하나를 다룹니다. 구조로 막는 방법을 배웠으니, 구조로 막을 수 없는 것도 알아야 합니다.