항상 대화할 수 있는 환경 만들기 — Python 봇에서 Claude Code 채널로
AI 사고 파트너의 전제조건은 '항상 거기 있는 것'. Python 2000줄을 걷어내고 MD 12개로 대체한 전환 과정과, 누구나 따라 만들 수 있는 채널 인프라 청사진.
- 배경: AI 사고 파트너를 만들었는데, 행동을 고치려면 코드를 수정해야 했어요. “왜 Claude한테 시킬 일을 Python으로 짜고 있지?”라는 질문에서 시작된 전환 이야기예요.
- 핵심 인사이트: 중간 레이어(Python 코드)를 걷어내고 MD 파일로 AI의 행동을 직접 정의하면, 코드 유지보수 없이 에이전트를 운영할 수 있어요.
- 이런 분에게: AI 봇을 만들어봤는데 유지보수가 점점 무거워지는 분, 또는 Claude Code로 나만의 에이전트를 운영하고 싶은 분
”왜 Claude한테 시킬 일을 Python으로 짜고 있지?”
텔레그램으로 생각을 던지면 AI가 정리해주는 봇을 만들었어요. 메모를 분류하고, 하루를 돌아보는 회고를 돕고, 아침마다 브리핑을 보내주는 — 나름 괜찮은 사고 파트너였거든요.
근데 어느 날 회고 질문의 톤을 바꾸고 싶었어요. “오늘 어땠어?”를 좀 더 구체적으로 바꾸고 싶었을 뿐인데, Python 코드를 열어서 프롬프트 조립 로직을 찾고, 테스트하고, 배포 스크립트를 돌려야 했어요.
그때 든 생각이요 — “잠깐, 이거 Claude가 잘하는 일인데. 내가 왜 코드로 짜고 있지?”
Sullivan은 Selforge 프로젝트의 사고 파트너 에이전트예요. 텔레그램으로 생각을 보내면 분류하고, 정기적으로 회고를 돕고, 아침 브리핑을 보내는 시스템이었어요.
초기 아키텍처는 이랬어요:
[Telegram] → [Python bot.py (launchd 데몬)]
→ scheduler.py (cron 트리거)
→ agents/capture.py (Anthropic API 호출)
→ agents/reflection.py (Anthropic API 호출)
→ obsidian.py (파일 읽기/쓰기)Python이 텔레그램 메시지 수신부터 전처리, Claude API 호출, 후처리, Obsidian 저장, 텔레그램 응답까지 모든 단계를 관리하고 있었어요. 각 에이전트마다 프롬프트 조립, 에러 핸들링, fallback 로직을 전부 Python으로 짜야 했고요.
세 가지 마찰
봇이 커질수록 불편이 쌓였어요.
첫째, 행동을 바꾸기가 너무 번거로웠어요. 회고 질문 하나 바꾸려면 Python 코드를 수정하고, 로컬에서 테스트하고, deploy.sh로 배포해야 했어요. MD 파일에 에이전트 행동을 정의해두긴 했는데, 실제 동작은 Python 코드가 결정했거든요. MD는 그냥 참고용 문서에 가까웠어요.
둘째, 코드가 점점 무거워졌어요. bot.py, capture.py, reflection.py, obsidian.py 등 합치면 약 2000줄. URL에서 콘텐츠를 추출하는 로직만 해도 API 호출, 특수문자 처리, 폴백 로직이 붙으면서 점점 복잡해졌어요.
셋째, 비용이 신경 쓰였어요. Anthropic API 종량제로 월 $5~15. 금액 자체보다, 호출을 최적화해야 한다는 인지적 비용이 더 컸어요. “이 맥락을 다 보내면 비용이…” 하면서 프롬프트를 줄이게 되더라고요.
운영하면서 세 가지 마찰이 명확해졌어요.
1. 개선 병목: 에이전트 행동을 바꾸려면 코드 수정 → 테스트 → deploy.sh 파이프라인을 매번 타야 했어요. MD 파일에 에이전트 행동을 서술해뒀지만, 실제 동작은 Python 코드가 결정했어요. MD와 코드 사이의 괴리가 점점 벌어졌고요.
2. 코드 복잡성: Python 약 2000줄. 각 에이전트마다 프롬프트 조립, API 호출, 에러 핸들링, 응답 파싱 로직이 있었어요. LinkedIn에서 콘텐츠를 추출하는 로직만 해도 Voyager API, surrogate character 처리, agent_mode fallback 등이 겹겹이 쌓여 있었고요.
3. API 비용 구조: Anthropic API 종량제로 월 $5~15 소요. 금액보다 “호출 최적화”라는 인지적 비용이 컸어요. 충분한 컨텍스트를 줘야 좋은 결과가 나오는데, 비용을 의식하면 프롬프트를 줄이게 돼요.
이 세 마찰의 공통 원인은 하나였어요 — Python이라는 중간 레이어. Claude가 직접 할 수 있는 일(파일 읽기/쓰기, URL 추출, 텍스트 분석)을 Python이 대신하고 있었어요.
”MD가 곧 코드”라는 발상
Claude Code에 채널(Channels) 기능이 나왔어요. 텔레그램 플러그인을 통해 Claude Code 세션이 직접 텔레그램 메시지를 주고받을 수 있게 된 거예요.
이게 의미하는 바가 컸어요. 예전에는 “파일 경로 조립 → glob → 파일 읽기 → 프롬프트 조립 → API 호출”을 전부 Python으로 코딩했잖아요. 근데 Claude Code는 Read, Write, Glob, WebFetch 같은 도구를 이미 갖고 있어요. 에이전트 정의 MD 파일에 “vault에서 오늘 캡처를 읽고 맥락 기반 질문을 던져”라고 쓰면, Claude가 그대로 실행해요.
MD 파일이 곧 코드가 되는 구조. Python 배관이 필요 없어지는 거예요.
Claude Code Channels에 텔레그램 플러그인이 추가되면서, 아키텍처 전환의 가능성이 열렸어요.
Channels의 핵심은 이거예요:
- Claude Code 세션이 직접 텔레그램 메시지를 수신/발신
- 파일 I/O는 네이티브 도구(Read, Write, Glob)로 처리
- URL 콘텐츠 추출은 WebFetch로 처리
- 스케줄링은 CronCreate로 처리
- API 호출 비용이 없음 (구독 내 포함)
가장 근본적인 변화: 에이전트의 행동을 MD 파일이 직접 결정해요. Agent MD에 “vault에서 오늘 Captures를 읽고 맥락 기반 질문을 던진다”라고 쓰면, Claude가 Read, Glob 등 네이티브 도구를 활용해서 그대로 수행해요. Python으로 “파일 경로 조립 → glob → 파일 읽기 → 프롬프트 조립 → API 호출”을 코딩할 필요가 없어요.
Before와 After
전환 결과를 한마디로 요약하면, Python 2000줄이 사라지고 MD 파일 12개만 남았어요.
전에는 봇의 행동을 바꾸려면 코드를 고치고, 테스트하고, 배포해야 했는데 — 이제는 MD 파일을 수정하면 다음 메시지부터 바로 반영돼요. API 비용도 없어졌고요. 구독 안에 다 포함되니까, 프롬프트를 줄여야 한다는 걱정 없이 충분한 맥락을 줄 수 있게 됐어요.

| 항목 | Python 봇 (Before) | Channel 모드 (After) |
|---|---|---|
| 아키텍처 | Python 데몬 + Anthropic API | Claude Code 세션 + 네이티브 도구 |
| 코드량 | Python ~2000줄 + MD 12개 | MD 12개만 |
| 행동 수정 | 코드 수정 → 테스트 → deploy.sh | MD 수정 → 즉시 반영 |
| 비용 | $5~15/월 (API 종량) | $0 추가 (구독 내) |
| 파일 처리 | obsidian.py (Python) | Read/Write/Glob (네이티브) |
| URL 추출 | requests + 정규식 | WebFetch (네이티브) |
| 스케줄링 | APScheduler (영구) | CronCreate (7일 갱신) |
| 배포 | launchd + deploy.sh | launchd + tmux |
에이전트 정의와 채널 하네스 — what과 how의 분리
채널 모드로 전환한 뒤, 두 번째 에이전트를 만들면서 중요한 걸 배웠어요.
사고 파트너(Sullivan) 채널은 잘 돌아가고 있었거든요. 그래서 PM 에이전트(셀피)도 같은 방식으로 만들면 되겠다고 생각했어요. 에이전트가 뭘 하는지 정의한 문서를 만들었고요.
근데 그걸로는 부족했어요. “이 에이전트는 PM이야”라고 적어놓는 것과, “이 세션에서 해요체로 말하고, /status 커맨드에 반응하고, 매일 아침 9시에 브리핑을 보내”라고 적는 건 다른 차원이더라고요.
전자는 에이전트가 무엇인가(what)를 정의하는 거고, 후자는 채널에서 어떻게 동작하는가(how)를 정의하는 거예요. Sullivan 채널이 잘 돌아간 건 이 두 층이 모두 갖춰져 있었기 때문이었어요. PM 채널은 what만 있고 how가 비어 있었던 거고요.
이걸 깨닫고 나서 “채널 하네스”라는 개념을 만들었어요 — 에이전트 정의와는 별도로, 그 세션의 운영 방식을 정의하는 문서예요.
두 번째 채널(PM)을 구축하면서 에이전트 정의와 채널 하네스가 서로 다른 설계 레이어라는 걸 알게 됐어요.
에이전트 정의 (.claude/agents/selforge-pm.md): “이 에이전트는 무엇인가” — 역할, 가치 계층, 정보 수집 프로토콜, 질문 전략. 서브에이전트로 호출될 때의 행동 명세예요.
채널 하네스 (.claude/skills/pm-channel.md): “이 세션에서 어떻게 동작하는가” — 페르소나 정체성, 톤, 메시지 라우팅, 크론 스케줄, 위임 규칙. 메인 세션이 페르소나를 입을 때의 운영 명세예요.
Sullivan 채널이 잘 돌아간 이유는 이 두 층이 모두 갖춰져 있었기 때문이에요. PM 채널에는 에이전트 정의만 있었고 하네스가 없었어요.
여기서 추가로 설계 결정이 필요했어요 — 세션에서 사용자와 소통하는 주체를 어떻게 구성할 것인가.
| 패턴 | 방식 | 장단점 |
|---|---|---|
| Delegate-every-message | 매 메시지를 서브에이전트로 위임 | 컨텍스트 매번 리셋, 대화 흐름 단절 |
| Orchestrator-as-persona | 메인 세션이 페르소나를 입고 동작 | 대화 맥락 유지, 무거운 작업만 위임 |
Orchestrator-as-persona 패턴을 선택했어요. 메인 Claude Code 세션 자체가 “나는 셀피다”로 동작하고, 코드 구현 같은 무거운 작업만 서브에이전트에 위임하는 구조예요. Sullivan 채널도 이미 이 패턴이었고, Anthropic 공식 가이드도 이걸 추천하더라고요.
실제 PM 하네스에 들어간 핵심 요소:
## 셀피의 정체성
셀피는 Selforge의 전략적 PM이자 흐민의 시스템 파트너.
## 메시지 라우팅
/status → 시스템 전체 상태 점검
/plan → 이번 주 태스크 현황 + 제안
/daily → 오늘 태스크 브리핑
기본값 → 셀피로서 직접 응답
## 역할 경계
사고/감정 관련 메시지 → "이건 설리반이 더 잘 도와줄 수 있을 것 같아요."역할 경계를 문서에서 명시적으로 강제하지 않으면, LLM은 맥락에 따라 역할이 흐려져요. “이것은 하지 않는다”를 구체적으로 나열하는 게 효과적이었어요.
Init Prompt — 같은 프로젝트에서 여러 에이전트 운영하기
에이전트 정의와 채널 하네스를 분리한 건 좋았는데, 현실적인 문제가 하나 더 있었어요. Sullivan과 셀피가 같은 프로젝트에서 돌아가야 한다는 거예요.
둘 다 같은 파일(Obsidian vault, 설정 파일)에 접근해야 하거든요. 프로젝트를 분리하면 파일 접근이 끊어지고, 하나의 봇으로 합치면 페르소나가 섞이고요.
Claude Code는 세션을 시작할 때 CLAUDE.md를 자동으로 읽어요. 근데 이건 프로젝트 레벨이지 세션 레벨이 아니에요. 같은 디렉토리에서 세션 두 개를 띄우면 둘 다 같은 CLAUDE.md를 읽어요. 여기에 “너는 PM이야”라고 쓰면 Sullivan도 PM이 되어버리는 거예요.
해결책은 의외로 간단했어요. 각 세션을 띄우는 스크립트에서 “이 파일을 읽고 따라해”라는 첫 메시지를 보내는 거예요. Sullivan 스크립트는 agents/sullivan.md를 읽으라고 하고, PM 스크립트는 agents/pm.md를 읽으라고 해요. 이 첫 메시지가 init prompt이고, 같은 프로젝트에서 여러 에이전트를 분기시키는 핵심이에요.

CLAUDE.md는 프로젝트 레벨이라 모든 세션이 공유해요. CLAUDE.md에 세션 분기 로직(“세션 이름을 확인하고 해당 파일을 로드하라”)을 넣는 것도 가능하지만, Claude가 이 라우팅을 따르지 않는 경우가 간헐적으로 발생했어요.
init prompt 방식이 더 안정적이에요. 세션의 첫 번째 사용자 메시지이므로 Claude가 반드시 처리하고, 읽어야 할 파일 경로를 명시적으로 전달하니 모호함이 없어요.
selforge/
├── CLAUDE.md ← 모든 세션이 공유 (프로젝트 규칙)
├── .claude/agents/
│ ├── sullivan.md ← 사고 파트너 에이전트 정의
│ └── selforge-pm.md ← PM 에이전트 정의
├── .claude/skills/
│ ├── sullivan-channel.md ← Sullivan 채널 하네스
│ └── pm-channel.md ← PM 채널 하네스각 Runner 스크립트의 핵심 차이는 INIT_PROMPT 하나예요:
# sullivan-runner.sh
INIT_PROMPT="Read .claude/skills/sullivan-channel.md and follow the instructions"
# pm-runner.sh
INIT_PROMPT="Read .claude/skills/pm-channel.md and follow the instructions"이 패턴은 Claude Code Channels 세션이 영속적으로 운영되는 환경을 전제로 해요. tmux + Runner(while true) + LaunchAgent + Watchdog의 영속성 체인이 세션의 크래시 복구와 자동 재시작을 담당하고요. 세션이 (재)시작되면:
- Claude Code가 Channels 모드로 기동 → 텔레그램 봇 연결
CLAUDE.md자동 로딩 — 프로젝트 공통 규칙 적용- init prompt 수신 → 해당 채널 하네스 파일을 읽음
- 에이전트 역할로 전환 + 크론 스케줄 등록
- 이제 이 텔레그램 봇은 해당 에이전트로 동작
두 에이전트가 같은 프로젝트 파일에 접근하니까 유기적 협력도 가능해요. Sullivan이 vault에 저장한 인사이트를 셀피가 주간 리뷰에서 참조하고, tmux send-keys로 세션 간 직접 통신도 할 수 있어요.
감수할 리스크와 완화
이 구조가 만능은 아니에요. Python 봇에 비해 감수해야 할 부분이 있어요.
세션이 죽으면 봇도 죽어요. Python 데몬은 한번 띄워놓으면 알아서 돌아갔는데, 채널 모드는 Claude Code 세션이 살아있어야 해요. 세션이 크래시하면 텔레그램 메시지에 응답을 못 하거든요. tmux + watchdog + LaunchAgent로 자동 복구 체인을 구축해서 보완했는데, 맥이 완전히 꺼져 있으면 당연히 안 돼요.
크론이 7일마다 만료돼요. Python의 APScheduler는 한번 등록하면 영구적이었는데, Claude Code의 CronCreate는 7일 후 자동 만료돼요. 매일 자정에 크론 목록을 점검하고 누락된 걸 재등록하는 “하트비트” 크론을 만들어서 해결했어요. 크론이 크론을 지키는 구조예요.
결과가 매번 달라요. Python은 같은 입력에 같은 출력을 보장했지만, Claude는 매번 다른 답을 할 수 있어요. 근데 돌이켜보니 이건 오히려 장점이더라고요. 회고 질문이 매번 같으면 지루하잖아요.
Python 봇 대비 세 가지 리스크가 있어요.
1. 세션 의존성
- 문제: Claude Code 세션이 죽으면 봇이 멈춤
- 완화: tmux + Runner(while true) + LaunchAgent + Watchdog 4레이어 영속성 체인
- 한계: 맥이 완전히 꺼져 있으면 불가능
2. CronCreate 7일 만료
- 문제: recurring 작업이 7일 후 자동 만료
- 완화: 하트비트 패턴 — 매일 00:05에 CronList 점검 + 누락 시 재등록
- 구조: 각 크론 프롬프트 파일(
.claude/skills/cron/*)에 CronList 확인 로직이 포함되어, 누락 시 자동 재등록
## Step 0: 크론 점검 (세션 시작 시 1회)
CronList를 실행한다. 6개 작업이 모두 있는지 확인한다.
누락이 있으면 scheduler.md를 읽고 누락된 작업만 재등록한다.3. 비결정성
- Python: 동일 입력 → 동일 출력 (결정적)
- Claude: 동일 입력 → 다른 출력 가능 (비결정적)
- 판단: 회고 질문, 인사이트 발견처럼 “매번 다른 관점”이 가치인 영역에서는 비결정성이 오히려 장점
추가로, 텔레그램 환경에서 채널을 여러 개 운영할 때 환경변수 문제를 겪었어요. Claude Code가 MCP 서버를 스폰할 때 환경변수를 세정(sanitize)해서 TELEGRAM_STATE_DIR이 전달되지 않았거든요. 세 채널이 같은 봇 토큰으로 폴백해서 하나만 동작하는 상황이었어요. 부모 프로세스의 환경변수를 복원하는 브릿지 스크립트로 해결했고요. “당연히 전달될 것”을 가정하지 말고 항상 확인해야 한다는 걸 배웠어요.
핵심 원칙: “중간 레이어의 정당성을 주기적으로 질문하라”
이 전환에서 가장 큰 교훈은 기술적인 게 아니었어요.
Python 봇을 처음 만들 때, Python은 당연히 필요한 레이어였어요. Claude API를 호출하려면 코드가 있어야 했고, 파일을 읽고 쓰려면 라이브러리가 필요했고, 스케줄링도 코드로 해야 했으니까요.
근데 도구가 발전하면서 그 “당연함”이 사라졌어요. Claude Code가 파일을 직접 읽고 쓸 수 있게 됐고, 텔레그램 플러그인이 나왔고, CronCreate가 생겼어요. Python이 하던 일을 도구가 직접 할 수 있게 된 거예요.
문제는 그 변화를 알아차리기까지 시간이 걸린다는 거예요. “예전부터 이렇게 했으니까”라는 관성이 있거든요. 그래서 주기적으로 물어봐야 해요 — “이 코드가 하는 일을 도구가 직접 할 수 있나?”
이건 AI 도구에만 해당하는 이야기가 아닌 것 같아요. 우리가 만드는 모든 중간 레이어 — 자동화 스크립트, 래퍼 라이브러리, 추상화 계층 — 는 만들어진 시점에서는 정당하지만, 환경이 바뀌면 불필요해질 수 있어요.
이 전환의 핵심 레슨을 정리하면 이래요.
중간 레이어가 정당한지 주기적으로 질문한다. “이 코드가 하는 일을 도구가 직접 할 수 있는가?”
Python 봇의 모든 코드는 본질적으로 “배관(plumbing)“이었어요. 메시지를 전달하고, 파일을 읽고 쓰고, API를 호출하는 — Sullivan의 핵심 가치인 포착, 분류, 요약, 회고와는 무관한 코드였어요. Claude Code가 그 배관을 네이티브로 제공하게 되면서, Python 레이어의 존재 이유가 사라졌어요.
추가로 이 과정에서 얻은 설계 원칙들:
- 에이전트 정의(what)와 채널 하네스(how)는 별개의 레이어다. 에이전트가 무엇인지 정의하는 것과, 세션에서 어떻게 동작하는지 정의하는 건 다른 문서에요. 이 구분이 없으면 “에이전트를 만들었는데 왜 채널에서 안 되지?”라는 혼란이 생겨요.
- MD가 곧 코드인 세계에서, 하네스 파일은 행동 명세서다. 채널 하네스에 두 가지 동작 모드가 혼재하면 Claude의 행동이 예측 불가능해져요. 한 하네스에는 한 모드만 기술해야 해요.
- 두 번째를 만들 때 첫 번째의 구조가 보인다. Sullivan 채널을 만들 때는 자연스럽게 진화했던 구조가, PM 채널을 만들면서 비로소 명시적인 패턴으로 정리됐어요. 시스템을 이해하는 가장 좋은 방법은 두 번째 인스턴스를 만들어보는 거예요.
마무리
이 전환의 38일 여정은 빌드 로그 #1에서 전체 맥락을 볼 수 있어요.
돌이켜보면, Python 봇이 나빴던 건 아니에요. 그 시점에서는 최선의 선택이었고, 잘 돌아갔어요. 다만 도구가 변하면서 더 나은 방법이 생긴 거예요. 중요한 건 그 변화를 알아차리고, “예전에 이렇게 했으니까”에 갇히지 않는 거더라고요.
AI 사고 파트너를 만드는 데 있어서, 기술적인 구현보다 더 중요한 전제조건이 하나 있어요 — 항상 거기 있는 것. 대화하고 싶을 때 언제든 말을 걸 수 있어야, 그때부터 진짜 파트너가 되거든요. Python 2000줄을 MD 12개로 바꾼 건, 결국 그 “항상 거기 있는 것”을 더 가볍게 유지하기 위한 선택이었어요.
다음 편에서는 이 채널 안에서 페르소나를 어떻게 분리하고, 사고 파트너와 PM이 서로 다른 목소리로 말하게 만들었는지 이야기할게요.
이 시리즈의 다른 글
- 항상 대화할 수 있는 환경 만들기 — Python 봇에서 Claude Code 채널로
- 채널 시스템으로 페르소나 분리하기 — 같은 프로젝트, 다른 역할
- AI 사고 파트너의 대화 규칙 설계 — '왜?'가 아니라 '뭐?'를 묻는 이유
- 포착 → 회고 → 인사이트 루프 — 하루의 리듬이 시스템이 되는 과정
- 영속성 체인으로 세션 안정성 확보 — AI가 꺼지지 않게 만드는 4레이어