6. 직접 만들기: 회의록 정리 Agent

실제로 돌려 보기

이 차시를 마치면

빈 폴더에서 세 경우를 실제로 돌려 봅니다.

빈 폴더에서 시작해 세 번 돌립니다. 아래는 실제 실행 결과입니다.

아래 출력은 2026-08-15 에 빈 폴더에서 시작해 실제로 세 번 돌려 받은 것입니다. 이 차시는 로컬 모델을 부르므로 5번 트랙에서 띄운 서버가 켜져 있어야 같은 결과가 나옵니다.

실행 스크립트

notes/run_demo.py 를 만듭니다. 파일 전문입니다. 빈 폴더에서 시작해 세 경우를 차례로 보여 줍니다.

"""회의록 정리 Agent 전체 흐름을 한 번 돌린다.

빈 폴더에서 시작해 세 번 실행한다.

  1회차 — 새 파일. 승인 없이 작성
  2회차 — 같은 입력. 멱등성으로 건너뜀 (승인을 묻지 않는다)
  3회차 — 회의록이 늘어난 상태. 덮어쓰기이므로 승인 요청

컨테이너에서 실행한다. LM Studio 서버가 켜져 있어야 한다.

    python -m notes.run_demo

모듈로 실행해야 한다. `python notes/run_demo.py` 로 실행하면
notes/ 가 경로에 들어가 최상위의 sec_tools 를 찾지 못한다.
"""
import shutil
import sys

from langgraph.types import Command

import sec_write_tools
from notes import notes_graph
from sec_tools import ALLOWED_ROOT

FIRST_NOTE = """# 8월 2주차 회의록
참석: 김, 이
- 김: 3분기 매출 자료 8월 20일까지 정리
- 이: 신규 채널 후보 3곳 조사, 8월 25일
"""

SECOND_NOTE = """# 8월 3주차 회의록
참석: 박
- 박: 다음 회의 일정 공지 8월 18일
"""


def base_input():
    return {
        "meeting_dir": "meetings",
        "target_path": "actions/TODO.md",
        "requester_id": "demo-user",
        "note_names": [],
        "note_texts": [],
        "actions": [],
        "pending_content": "",
        "diff": "",
        "target_exists": False,
        "risk_level": "low",
        "approved": False,
        "executed_action_id": None,
        "backup_name": None,
        "result": "",
        "audit_log": [],
    }


def show(title, state):
    print("--- %s ---" % title)
    print("  위험 등급 :", state.get("risk_level"))
    print("  멈춤 여부 :", "__interrupt__" in state)
    print("  결과      :", state.get("result") or "(아직 없음)")
    for line in state.get("audit_log", []):
        print("    -", line)
    print()


def main():
    shutil.rmtree(ALLOWED_ROOT, ignore_errors=True)
    (ALLOWED_ROOT / "meetings").mkdir(parents=True, exist_ok=True)
    (ALLOWED_ROOT / "meetings" / "0812.md").write_text(FIRST_NOTE, encoding="utf-8")

    graph = notes_graph.build_graph()

    print("== 1회차: 새 파일 ==")
    try:
        first = graph.invoke(base_input(), {"configurable": {"thread_id": "demo-1"}})
    except Exception as error:
        print("실패:", type(error).__name__, error)
        print("LM Studio 서버가 켜져 있는지 확인하세요.")
        return 2
    show("1회차", first)
    print("만들어진 파일:")
    print((ALLOWED_ROOT / "actions/TODO.md").read_text(encoding="utf-8"))

    print("== 2회차: 같은 입력 ==")
    second = graph.invoke(base_input(), {"configurable": {"thread_id": "demo-2"}})
    show("2회차", second)

    print("== 3회차: 회의록이 늘어난 뒤 ==")
    (ALLOWED_ROOT / "meetings" / "0819.md").write_text(SECOND_NOTE, encoding="utf-8")
    config = {"configurable": {"thread_id": "demo-3"}}
    paused = graph.invoke(base_input(), config)
    show("3회차 (승인 대기)", paused)

    if "__interrupt__" in paused:
        payload = paused["__interrupt__"][0].value
        print("사람에게 보여 주는 차이:")
        print(payload["diff"])
        print()
        resumed = graph.invoke(Command(resume="approve"), config)
        show("3회차 (승인 후)", resumed)
        print("최종 파일:")
        print((ALLOWED_ROOT / "actions/TODO.md").read_text(encoding="utf-8"))
        print("남은 백업:", sec_write_tools.list_backups("actions/TODO.md"))

    return 0


if __name__ == "__main__":
    sys.exit(main())

실행

cd webapp/verify
docker run --rm -v "$PWD":/work -w /work llmwiki-verify python -m notes.run_demo

1회차 — 새 파일

--- 1회차 ---
  위험 등급 : low
  멈춤 여부 : False
  결과      : 작성 완료: actions/TODO.md
    - 회의록 1건 읽음: 0812.md
    - 실행항목 2건 추출
    - 쓸 내용 확정 (risk=low, action_id=0bc7f85dfd8126ca). 아직 쓰지 않음
    - 새 파일이므로 자동 승인 (사람 개입 없음)
    - actions/TODO.md 에 씀. 백업 없음 (새 파일)

만들어진 파일입니다.

# 실행항목

회의록에서 자동으로 뽑았습니다. 사람이 확인한 뒤 쓰십시오.

- [ ] **김** — 3분기 매출 자료 정리 (기한 8월 20일)
- [ ] **이** — 신규 채널 후보 3곳 조사 (기한 8월 25일)

새 파일이라 저위험이고, 자동 승인되었으며, 그 사실이 기록에 남았습니다. 백업은 없습니다. 지울 원본이 없기 때문입니다.

2회차 — 같은 입력

--- 2회차 ---
  위험 등급 : high
  멈춤 여부 : False
  결과      : 이미 처리됨
    - 회의록 1건 읽음: 0812.md
    - 실행항목 2건 추출
    - 쓸 내용 확정 (risk=high, action_id=0bc7f85dfd8126ca). 아직 쓰지 않음
    - 같은 입력이 이미 처리되어 건너뜀 (action_id=0bc7f85dfd8126ca)

두 가지를 봅니다.

  • action_id1회차와 같습니다(0bc7f85dfd8126ca). 입력이 같으니 키도 같습니다
  • 위험 등급은 high 인데 멈추지 않았습니다. 중복 확인이 승인보다 앞에 있기 때문입니다

3회차 — 회의록이 늘어난 뒤

--- 3회차 (승인 대기) ---
  위험 등급 : high
  멈춤 여부 : True
  결과      : (아직 없음)
    - 회의록 2건 읽음: 0812.md, 0819.md
    - 실행항목 3건 추출
    - 쓸 내용 확정 (risk=high, action_id=fa5eaa6ef4b993c8). 아직 쓰지 않음

action_id바뀌었습니다(fa5eaa6ef4b993c8). 회의록이 늘어 입력이 달라졌으니 다른 작업입니다. 그래서 건너뛰지 않고 승인을 요청합니다.

사람에게 보여 준 차이입니다.

--- actions/TODO.md (현재)
+++ actions/TODO.md (바뀔 내용)
@@ -5,2 +5,3 @@
 - [ ] **김** — 3분기 매출 자료 정리 (기한 8월 20일)
 - [ ] **이** — 신규 채널 후보 3곳 조사 (기한 8월 25일)
+- [ ] **박** — 다음 회의 일정 공지 (기한 8월 18일)

한 줄이 추가된다는 것이 한눈에 보입니다. 전체 문서를 읽지 않아도 판단할 수 있습니다.

--- 3회차 (승인 후) ---
  결과      : 작성 완료: actions/TODO.md
    - 사람 결정: approve
    - actions/TODO.md 에 씀. 백업 actions_TODO.md.20260815-061341-208193

남은 백업: ['actions_TODO.md.20260815-061341-208193']

이번에는 백업이 남았습니다. 덮어쓰기였기 때문입니다.

되돌리기

덮어쓴 것을 되돌립니다.

import sec_write_tools

backups = sec_write_tools.list_backups("actions/TODO.md")
print("백업 목록:", backups)

sec_write_tools.restore_from_backup("actions/TODO.md", backups[0])

가장 최근 백업이 목록의 첫 번째입니다. 되돌리면 파일이 덮어쓰기 직전 상태로 돌아갑니다.

확인 필요 되돌리기는 현재 파일을 덮어씁니다. 이 교재의 데모에서는 직접 함수를 부르지만, 실제로 쓸 때는 이 동작도 승인 게이트를 지나게 해야 합니다.

테스트는 다음 차시에서

여기까지 하면 Agent 가 돕니다. 그런데 돌아간다는 것과 앞으로도 계속 옳게 돈다는 것은 다릅니다.

다음 차시에서 테스트 28개를 씁니다. 코드를 고친 뒤에도 승인 게이트와 백업이 살아 있는지 확인하는 장치입니다.

바꾼 뒤 반드시 다시 돌릴 것

자기 업무에 맞게 고칠 곳입니다.

#위치무엇을
1ACTION_SCHEMA뽑을 항목의 모양. 담당자·할 일·기한 말고 다른 것이 필요하면
2render_todo결과 문서의 형식
3plan_write 의 위험 판정지금은 파일 존재 여부. 다른 기준이 필요하면
4_EXECUTED_ACTION_IDS메모리 집합을 durable 저장소로. 4번 트랙 7차시

고친 뒤 28개 테스트가 그대로 통과해야 합니다. 특히 세 가지를 확인합니다.

  • test_reject_leaves_the_file_untouched — 반려했는데 파일이 바뀌면 승인 게이트가 뚫린 것입니다
  • test_approve_writes_and_backs_up — 백업 없이 덮어쓰면 되돌릴 수 없습니다
  • test_same_input_executes_once — 중복 방어가 깨진 것입니다

여기까지 오면

빈 폴더에서 시작해 파일을 만들고 고치는 Agent 를 만들었습니다. 그 과정에서 여섯 트랙의 내용이 전부 쓰였습니다.

트랙이 실습에서
1. 설치격리 실행. 자료가 나가는 지점 확인
2. 입문State · Node · 조건부 Edge · Reducer · Interrupt
3. 코드 구조화파일 분리 · 규칙을 두 벌로 만들지 않기
4. 보안 특화승인 게이트 · 멱등성 · 감사 로그 · 검증 범위 밝히기
5. 로컬 LLM회의록이 기기 밖으로 나가지 않음
6. 실습이 전부를 한 번에

마지막으로 남길 것은 이것입니다. 구조로 막는다는 것은 공격을 막는다는 뜻이 아니라, 잘못될 수 있는 일이 넘을 수 없는 선을 코드로 그어 두는 것입니다.

이해도 확인

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