카카오톡 AI 챗봇 누렁이 구축하기(안드로이드)

카카오톡 AI 챗봇 ‘누렁이’ 구축 가이드

AI 관련 단체 채팅방이 많아졌다. 거의 10개 되는 듯한데… 그 중 2개의 단톡방에서는 운영자가 AI 봇을 갖고 있었습니다.

주로 카카오톡 요약, 시시콜콜한 이야기를 나누는 것이었습니다.

그러다보니 나도 저게 어떻게 하는지 궁금했고, 만들어보고 싶은 시도가 있었다.

구축 방법은 맥OS가 있어야 하지만 맥이 없다면, 안드로이드 폰이 있어야 가능하다.

나는 맥이 없기 때문에 안드로이드폰으로 구축을 해보았습니다.

요금제는 알뜰폰 중 제일 저렴한 3,300원짜리 요금제 선택하고 유심 구매했습니다. 어차피 봇 용이라서 집에 와이파이 켜놓고 둘 폰이었습니다.

그리고 카카오 아이디를 하나 새로 만들어야 합니다.

그리고 이걸 절대 보면서 따라하려고 하지 마시고, 혹시나 만들고 싶으시면 이걸 참고해서 만드는 방법을 알려달라고 하는 것이 더 효과적입니다.


1. 전체 구조

카카오톡 일반방 / 단체방
│
▼
Android 봇폰
– 누렁이 전용 카카오톡 계정
– Kakao Bridge 앱
– Termux
│
│ HTTP
▼
Windows PC
C:\nureongi\server.py
│
├─ DeepSeek API : 최종 답변/라우팅
├─ Brave Search API : 일반 웹검색 1순위
├─ Tavily API : 웹검색 보조
├─ GNews API : 최근 24시간 세계 뉴스
└─ SQLite : 사람별 장기기억
│
▼
Android 봇폰
│
▼
카카오톡 자동답장


2. 준비물

Android 폰(봇용도)

  • Android 폰 1대
  • 별도 카카오톡 계정 권장
  • Termux
  • Termux:API
  • Kakao Bridge APK
  • 카카오톡 알림 접근 권한

Windows PC

  • Python 3.13
  • PowerShell 7 권장
  • 같은 공유기/LAN에 연결
  • AI/검색 API 키

API

현재 시점에서 구성한 api key

  • DeepSeek API
  • Brave Search API
  • Tavily API
  • GNews API

참고로 로컬 모델로 운영할 계획이 있으나, 지금은 n100 미니 pc로 돌리기 때문에 일단 api key를 사용하기로 했다.

딥시크 api key가 가성비가 좋아보여서 일단 그걸 선택했다.

나중에는 gemma4 아니면 qwen 3.8 정도 되는 모델을 사용할까 생각중이다.

각종 검색을 위해서 api를 선택했는데 이건 조금씩 늘려가거나 필요 없는 것은 제외할 생각이다.


3. Android 봇 (폰) 구축

3.1 Termux 설치

https://github.com/dextune/kakao-x-termux

위 사이트로 이동했다.

나는 일단 PC의 원하는 폴더로 이동 후 powershell을 실행시키고 아래 내용을 입력했다.

git clone https://github.com/dextune/kakao-x-termux.git

그러면 apk 파일을 받을 수 있다. 이것을 안드로이드 폰으로 이동시키는 방법을 선택했다.

com.termux_1022.zip을 해지하면(나는 반디집으로 했음) 1002.apk 파일이 나온다.

이것을 안드로이드 폰에 전송시켜 설치한다.

apk 파일을 핸드폰에 설치하면 Termux 앱을 실행할 수 있다.

그럼 이제 핸드폰에서 작업을 해야 합니다.

3.2 kakao bridge apk 설치

PC에서 받은 kakao-bridge-app-debug.apk를 안드로이드 폰에 옮기고

해당 apk를 실행했습니다. 다만 이 과정에서 google play 프로텍트가 앱을 차단했는데 이것은

google play 스토어 → 프로필 사진 → play 프로텍트 → 앱 검사 off

이것으로 해결을 봤습니다. apk 설치 이후에는 다시 프로텍트를 on 시켰습니다.

Kakao Bridge 초기 상태는 아래와 같아집니다.

3.2.1 지금 1단계: 알림 접근 권한 켜기

화면 아래쪽의 설정 카드, 즉

설정
백엔드 주소, 권한, 운영 스위치

를 눌러보세요.

설정 화면 안에 알림 접근 권한, 알림 권한, Notification access 같은 항목이 있을 겁니다.

그 항목을 눌러 Android 설정 화면으로 이동합니다.

만약 앱 안에서 바로 이동하지 않는다면 갤럭시에서 직접:

휴대폰 설정
→ 애플리케이션
→ 오른쪽 위 ⋮
또는 '특별한 접근'
→ 특별한 접근
→ 알림 접근

으로 들어갑니다.

기종에 따라서는:

설정
→ 알림
→ 고급 설정
→ 알림 접근

처럼 보일 수도 있습니다.

거기에서 Kakao Bridge를 찾습니다.

Kakao Bridge OFF

를

Kakao Bridge ON

으로 바꿉니다.

경고창이 뜨면서 대략:

Kakao Bridge가 알림을 읽도록 허용하시겠습니까?

라고 하면 허용을 누릅니다. 그러면 아래처럼 변경됩니다.

그 다음 단계는

화면 아래쪽의:

알림 접근 권한 열기

를 누릅니다.

그러면 안드로이드 설정 화면으로 이동할 텐데, 거기서:

Kakao Bridge

를 찾아서 ON으로 바꿉니다. 경고창이 뜨면 허용을 누릅니다.

그다음 다시 Kakao Bridge 앱으로 돌아옵니다.

그리고 현재 화면 맨 아래의:

설정 저장

을 한 번 눌러줍니다.

그다음 오른쪽 위:

뒤로

를 눌러 메인 화면으로 돌아갑니다.

메인 화면에서 아까 보이던:

권한 필요
알림 접근 허용 대기

가 사라지거나 정상 상태로 바뀌면 성공입니다.

현재 다른 설정은 그대로 두시면 됩니다.

백엔드 주소 http://129.0.0.1:8787 ← 그대로
브릿지 가동 ON ← 그대로
자동 재전송 ON ← 그대로
원문 로그 저장 OFF ← 지금은 그대로

그럼 아래처럼 카카오 브릿지가 변경됩니다.

화면 왼쪽 위가 이제

작동 중
카카오톡 수신 대기

로 바뀝니다. 이건 Kakao Bridge가 카카오톡 알림을 받을 준비가 됐다는 뜻입니다.

이제 다음 목표는 오른쪽 위의

백엔드
확인 전

을 정상 연결 상태로 바꾸는 것입니다.

지금은 백엔드 확인 버튼부터 누르지 말고, 먼저 Termux에서 서버를 실행해야 합니다. 프로젝트 공식 설치 절차도 run_termux_server.sh로 서버를 먼저 실행한 뒤 상태를 확인하도록 되어 있습니다.

지금 할 일 1개

휴대폰에서 Termux 앱을 여세요. 검은 터미널 화면에서 아래를 입력합니다.

cd ~/kakao-x-termux

엔터.

그다음 아래를 입력합니다.

ls

정상이라면 대략 이런 것들이 보여야 합니다.

README.md
INSTALL.md
app
app-apk
docs
requirements.txt
scripts

특히 scripts가 보이면 됩니다.


3.3 Termux에서의 작업

이제 Termux 앱을 엽니다. Termux:API가 아니라 검은 터미널 화면이 나오는 Termux를 엽니다.

문제가 발생했는데 핸드폰 Termux의 Python이 3.14.6인데, 이 프로젝트 설치 스크립트는 pydantic 2.13.3 / pydantic-core 2.46.3을 전제로 만들어져 있어 Python 3.14에서 막힌 상태입니다.

그래서 Python을 억지로 3.13으로 내리지 않고 현재 3.14.6을 그대로 쓰는 방향으로 고쳐봤습니다.

termux에서 아래를 입력합니다.

cd ~/kakao-x-termux

그 다음 가상환경을 켭니다.

source .venv/bin/activate

성공하면 앞부분이 대략:

(.venv) ~/kakao-x-termux $

처럼 바뀝니다.

이제 프로젝트의 pydantic 버전만 2.13.4로 바꿉니다.

sed -i 's/pydantic==2.13.3/pydantic==2.13.4/' requirements.txt

다음으로 Android용 pydantic-core wheel을 설치합니다.

python -m pip install --extra-index-url https://pypi.flet.dev pydantic-core==2.46.4

여기서 정상이라면 마지막쯤:

Successfully installed pydantic-core-2.46.4

가 나옵니다.

그 다음:

python -m pip install pydantic==2.13.4

정상이라면:

Successfully installed pydantic-2.13.4

비슷하게 나옵니다.

그다음 나머지 프로젝트 패키지를 설치합니다.

python -m pip install --extra-index-url https://pypi.flet.dev -r requirements.txt

여기까지 성공하면 마지막 확인을 합니다.

python -c "import pydantic, pydantic_core; print(pydantic.__version__, pydantic_core.__version__)"

정상 결과는:

2.13.4 2.46.4

입니다.

Termux 쪽에는 아예 Android용 python-pillow 패키지가 따로 있으므로, pip으로 억지로 컴파일하지 않고 Termux 패키지를 쓰는 게 가장 깔끔합니다. Termux 개발자도 Pillow 설치 문제에서는 pkg install python-pillow을 권장하고 있습니다.

  1. 현재 (.venv)가 보이므로 먼저 가상환경에서 나옵니다.
deactivate

그러면 앞의 (.venv)가 사라져야 합니다.

  1. Termux용 Pillow를 설치합니다.
pkg install python-pillow -y

끝날 때까지 기다립니다.

  1. 기존 가상환경에서도 Termux가 설치한 Pillow를 볼 수 있게 설정합니다.
sed -i 's/include-system-site-packages = false/include-system-site-packages = true/' .venv/pyvenv.cfg
  1. 프로젝트가 구버전 Pillow를 요구하지 않도록 바꿉니다.
sed -i 's/Pillow>=10.3,<11.0/Pillow>=12.0,<13.0/' requirements.txt
  1. 다시 가상환경을 켭니다.
source .venv/bin/activate

다시:

(.venv) ~/kakao-x-termux $

처럼 보여야 합니다.

  1. Pillow가 제대로 보이는지 확인합니다.
python -c "import PIL; print(PIL.__version__)"

정상이면 대략:

12.3.0

처럼 12.x.x가 나옵니다. 현재 Termux 저장소에도 python-pillow 12.3.0 패키지가 제공되고 있습니다.

일단 여기까지 정상으로 나왔습니다.


파이썬 패키지 설치

나머지 Python 패키지 설치만 이어가겠습니다. 지금 화면 앞에 (.venv)가 붙어 있는 상태에서 먼저 아래 한 줄을 입력하세요.

python -m pip install -r requirements.txt

이번에는 이전과 달리 Pillow에서 빌드 오류가 나지 않아야 합니다. 설치가 끝나면 다시:

(.venv) ~/kakao-x-termux $

가 나타납니다.

그다음 설치가 제대로 됐는지 확인합니다.

python -c "import fastapi, uvicorn, httpx, pydantic, PIL; print('OK', pydantic.__version__, PIL.__version__)"

정상이라면 대략:

OK 2.13.4 12.3.0

여기까지 정상적으로 진행이 되었습니다. 모르면 gpt나 클로드에게 캡쳐화면 보여주면 됩니다.


백엔드 설치

이제 백엔드 서버를 실행합니다.

현재 화면이 대략:

(.venv) ~/kakao-x-termux $

라면 아래 한 줄을 입력합니다.

bash scripts/run_termux_server.sh

엔터를 누릅니다.

정상이면 마지막에 대략 이런 문구가 나옵니다.

INFO: Started server process [...]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://129.0.0.1:8787

이 상태에서는 Termux 화면이 다시 $로 돌아오지 않는 게 정상입니다. 서버가 계속 실행 중이기 때문입니다. 프로젝트의 실행 스크립트도 .venv를 활성화하고 DB를 초기화한 뒤 uvicorn app.main:app을 129.0.0.1:8787에서 실행하도록 되어 있습니다.

서버가 떴다면 바로 Kakao Bridge로 이동해서

Termux를 종료하지 말고 홈 버튼으로 빠져나와 Kakao Bridge를 엽니다.

아까 화면의:

백엔드
확인 전

아래에 있는 백엔드 확인 버튼을 누릅니다.

성공하면 오른쪽 위 상태가 대략:

백엔드
정상

또는 연결 성공을 의미하는 상태로 바뀌어야 합니다.


다음 단계: 실제 카카오톡 메시지 수신 테스트

먼저 “카카오톡 메시지를 Bridge가 읽을 수 있는가”부터 확인합니다.

  1. Termux는 지금 화면 그대로 둡니다.
  2. 홈 화면으로 나갑니다. Termux 앱을 종료할 필요는 없습니다.
  3. 봇폰에서 카카오톡을 열어 테스트용 채팅방에 들어간 다음, 그 채팅방의 알림이 켜져 있는지 확인합니다. 방 알림이 꺼져 있으면 Bridge가 받을 카카오톡 알림 자체가 생기지 않습니다.
  4. 확인이 끝나면 카카오톡은 화면에서 빠져나옵니다. 테스트할 때 봇폰에서 해당 카톡방을 계속 열어놓지 않는 게 좋습니다. 카카오톡 알림이 실제로 발생해야 하기 때문입니다.
  5. 이제 다른 휴대폰/다른 카카오 계정에서 봇 계정이 들어 있는 테스트 방으로 아주 단순하게 보냅니다.
테스트123
  1. 봇폰 상단에 카카오톡 알림이 오는지 확인했습니다.
  2. 그다음 Kakao Bridge 앱을 엽니다. 필요하면 새로고침을 한 번 눌러봅니다.

아까:

받은 메시지
0건

이었다면 성공 시:

받은 메시지
1건

이렇게 증가해야 합니다.

일단 저는 여기까지 성공하였습니다.

그리고 다른 카카오 계정에서 테스트방에 이것을 보냅니다

/추첨 테스트

/까지 포함해야 합니다.

정상이라면 봇 계정이 자동으로 답합니다

대략 이런 답변이 와야 합니다.

추첨 [테스트]이(가) 생성되었습니다!
/추첨 참여로 참가하고 /추첨 실행으로 추첨을 진행하세요. (5분 제한)

실제 프로젝트 코드가 /추첨 제목을 받으면 이 메시지를 반환하도록 되어 있습니다.

그러면 우리가 원했던 전체 경로가:

내 계정
↓
/추첨 테스트
↓
카카오톡 알림
↓
Kakao Bridge
↓
Termux FastAPI
↓
Random Draw Bot
↓
답장 생성
↓
Kakao Bridge
↓
카카오톡 자동답장
↓
봇 계정

까지 모두 성공한 겁니다.

Kakao Bridge 숫자도 확인합니다.

성공했다면 메인 화면에서 아마:

받은 메시지
2건
보낸 답장
1건

처럼 바뀌는데 여기까지 성공하였습니다.

성공한 화면입니다.


본격적인 봇 만들기

Termux 서버를 CTRL + c로 끈 뒤, 아래 순서대로 입력합니다.

먼저 봇 폴더를 만듭니다.

mkdir -p app/bots/nureongi-bot

그다음 봇 파일을 만듭니다. 아래 전체를 그대로 붙여넣습니다.

cat > app/bots/nureongi-bot/bot.py <<'PY'
from __future__ import annotations
from app.chatbot.base_bot import ObserverBot
from app.chatbot.context import ChatContext
from app.chatbot.decision import ChatDecision
from app.chatbot.module import MatchPattern
class NureongiBot(ObserverBot):
key = "nureongi_bot"
name = "누렁이"
version = "0.1.0"
description = "누렁이 호출어에 반응하는 AI 비서 테스트 봇"
patterns = [
MatchPattern(type="mention", value=["누렁이"])
]
priority = 300
terminal = True
def can_handle(self, context: ChatContext) -> bool:
if self.skip_command(context):
return False
return context.replyToken is not None
def run(self, context: ChatContext) -> ChatDecision:
return self.reply(
"안녕하세요! 무엇을 도와드릴까요?",
reason="누렁이 호출 감지"
)
def create_bot() -> NureongiBot:
return NureongiBot()
PY

설명 파일도 만듭니다.

cat > app/bots/nureongi-bot/bot.md <<'MD'
# 누렁이 Bot
## 목적
카카오톡 일반 메시지에서 "누렁이"라는 호출어를 감지해 답장한다.
## 트리거
- 누렁이
## 예시
사용자:
누렁이 안녕
봇:
안녕하세요! 무엇을 도와드릴까요?
MD

그다음 파일이 생겼는지 확인합니다.

ls app/bots/nureongi-bot

정상이면:

bot.md bot.py

가 나옵니다.

문법 검사:

python -m compileall -q app/bots/nureongi-bot

아무것도 안 나오면 정상입니다.

마지막으로 서버를 다시 켭니다.

bash scripts/run_termux_server.sh

그리고 다른 카카오 계정에서:

누렁이 안녕

이라고 보내보세요.

성공하면 봇 계정이 자동으로:

안녕하세요! 무엇을 도와드릴까요?

라고 답합니다.

그런데 저는 누렁이라고 부르지 않고 누렁아라고 부를 때 답하길 원했습니다. 그래서 아래와 같이 실행했습니다.

지금 서버가 켜져 있다면 Termux에서 먼저 CTRL → c로 서버를 껐습니다. 그다음 아래 한 줄을 복붙했습니다.

sed -i 's/value=\["누렁이"\]/value=["누렁아"]/' app/bots/nureongi-bot/bot.py

그리고 서버를 다시 켭니다.

bash scripts/run_termux_server.sh

이제 이렇게 동작해야 합니다.

누렁이 안녕
→ 반응 안 함
누렁아 안녕
→ 안녕하세요! 무엇을 도와드릴까요?

즉 이름은 누렁이, 부를 때는 누렁아입니다.

먼저 이 변경만 해보고 누렁아 안녕이 정상적으로 답하는지 확인했더니 성공했습니다.


4. PC와 핸드폰 연결

1단계: Windows PC에 테스트 서버 만들기

아직 AI를 붙이지 않았습니다. 먼저 휴대폰 ↔ Windows PC 통신부터 성공시키려고 했습니다.

Windows에서 PowerShell을 엽니다.

그리고 아래 두 줄을 차례대로 입력합니다.

mkdir C:\nureongi
notepad C:\nureongi\server.py

메모장이 열릴 겁니다.

메모장에 아래 내용을 통째로 복붙했습니다.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, data):
body = json.dumps(
data,
ensure_ascii=False
).encode("utf-8")
self.send_response(status)
self.send_header(
"Content-Type",
"application/json; charset=utf-8"
)
self.send_header(
"Content-Length",
str(len(body))
)
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_json(
200,
{
"ok": True,
"name": "nureongi-pc"
}
)
else:
self.send_json(
404,
{"error": "not found"}
)
def do_POST(self):
if self.path != "/ask":
self.send_json(
404,
{"error": "not found"}
)
return
length = int(
self.headers.get(
"Content-Length",
"0"
)
)
raw = self.rfile.read(length)
try:
data = json.loads(
raw.decode("utf-8")
)
except Exception:
self.send_json(
400,
{"error": "invalid json"}
)
return
prompt = str(
data.get("prompt", "")
).strip()
self.send_json(
200,
{
"text":
f"PC 연결 성공! 질문: {prompt}"
}
)
print("누렁이 PC 서버 시작")
print("http://0.0.0.0:8080")
server = ThreadingHTTPServer(
("0.0.0.0", 8080),
Handler
)
server.serve_forever()

저장하고 메모장을 닫았습니다.


그다음 Windows PowerShell

입력합니다.

cd C:\nureongi
python server.py

만약 python 명령을 못 찾는다고 나오면:

py server.py

정상이면:

누렁이 PC 서버 시작
http://0.0.0.0:8080

이라고 나오고 PowerShell이 멈춰 있는 것처럼 보입니다.

정상입니다. 서버가 실행 중인 겁니다.

Windows 방화벽 창이 뜨면서 Python의 네트워크 접근을 허용할지 물어보면 개인 네트워크는 허용합니다.


이제 Windows PC의 IP 주소 확인

서버 PowerShell은 그대로 놔두고 PowerShell을 하나 더 엽니다.

입력:

ipconfig

그러면 여러 내용이 나오는데 현재 사용하는 Wi-Fi 또는 Ethernet 항목에서:(예시)

IPv4 Address . . . . . . : 123.168.0.123

비슷한 걸 찾습니다.

예를 들어:

123.168.0.123

이 나왔다고 가정하겠습니다.

중요합니다.

129.0.0.1

이건 사용하면 안 됩니다.

휴대폰에서 129.0.0.1은 Windows가 아니라 휴대폰 자기 자신을 뜻합니다.


이제 휴대폰에서 Windows 연결 테스트

여기서 다시 봇폰 Termux로 갑니다.

현재 누렁이 서버가 Termux에서 실행되고 있으므로 끄지 말고, Termux에서 새 세션을 하나 여는 게 좋습니다.

Termux 화면의 왼쪽 가장자리에서 오른쪽으로 스와이프하면 Sessions 메뉴가 나옵니다.

거기서:

NEW SESSION

을 눌러 새 창을 만듭니다.

새 Termux 창에서, 아까 Windows IP가 123.168.0.123이었다면:

curl http://123.168.0.123:8080/health

를 입력합니다.

정상이라면:

{"ok": true, "name": "nureongi-pc"}

비슷하게 나옵니다.

그러면 봇폰 → Windows PC 연결 성공입니다.

그리고 한 번 더:

curl -X POST http://123.168.0.123:8080/ask -H "Content-Type: application/json" -d '{"prompt":"누렁아 안녕"}'

정상이면:

{"text":"PC 연결 성공! 질문: 누렁아 안녕"}

이 나옵니다.

이것까지 성공하면 아주 중요한 고비를 넘긴 겁니다.

Android
↓
Wi-Fi
↓
Windows PC :8080
↓
응답
↓
Android

그다음 제가 현재 누렁이의 bot.py를 바꿔서,

누렁아 서울 날씨 알려줘

라고 카카오톡에서 말하면 Termux가 Windows로:

{
"prompt": "누렁아 서울 날씨 알려줘"
}

그런데 이 과정에서 저는 근데 이더넷 어댑터 vEthernet이 있고 vEthernet (WSL (Hyper-V firewall)이 있어 어떤 건지 헷갈렸습니다.

헷갈리니까 ipconfig 화면에서 찾지 말고, PowerShell에 아래 한 줄을 복붙했습니다.

Get-NetIPConfiguration | Where-Object {$_.IPv4DefaultGateway -ne $null} | Select-Object InterfaceAlias,IPv4Address,IPv4DefaultGateway

그러면 예를 들어 이런 식으로 나오게 되었습니다.

InterfaceAlias IPv4Address IPv4DefaultGateway
-------------- ----------- ------------------
Ethernet {123.168.0.15} {123.168.0.1}

N100 상태는 Windows PC에 실제 Python이 설치되어 있지 않거나, python 명령이 Microsoft Store용 가짜 실행 별칭만 가리키는 상태로 보였습니다.

지금은 Termux 건드리지 말고 Windows PowerShell에서만 했습니다.

먼저 아래 한 줄을 복붙합니다.

winget install -e --id Python.Python.3.13

설치가 끝나면 PowerShell 창을 완전히 닫았다가 새로 엽니다. 그다음:

python --version

정상이면 예를 들어:

Python 3.13.x

처럼 나옵니다.

그다음 다시:

cd C:\nureongi
python server.py

정상이라면 이번에는 바로 끝나지 않고:

누렁이 PC 서버 시작
http://0.0.0.0:8080

그리고 python 명령이 설치된 Python이 아니라 Windows의 가짜 실행 별칭(App Execution Alias) 을 계속 잡고 있었습니다.

그래서 아래처럼 입력했습니다.

PowerShell에서 아래 한 줄만 먼저 실행합니다.

Get-ChildItem "$env:LOCALAPPDATA\Programs\Python" -Recurse -Filter python.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName

정상이면 대략 이런 경로가 나옵니다.

C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe

그 경로가 나오면, 그다음 이렇게 직접 실행합니다.

& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" --version

예를 들어:

Python 3.13.x

가 나오면 바로 서버도:

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

정상이면:

누렁이 PC 서버 시작
http://0.0.0.0:8080

이 나오고 PowerShell 프롬프트가 다시 안 돌아옵니다.


지금 이 상태에서

누렁이 PC 서버 시작
http://0.0.0.0:8080

이면 PowerShell 창은 그대로 둡니다. 닫지 않습니다.

이제 핸드폰 Termux의 새 세션에서 다시 아래 한 줄을 실행했습니다.

curl http://123.168.1.128:8080/health

정상이면:

{"ok": true, "name": "nureongi-pc"}

가 나옵니다.

그다음 한 번 더:

curl -X POST http://123.168.1.128:8080/ask -H "Content-Type: application/json" -d '{"prompt":"누렁아 안녕"}'

정상이면:

{"text":"PC 연결 성공! 질문: 누렁아 안녕"}

가 나옵니다.

저는 여기까지 성공했습니다.


이제 카카오톡의 누렁아 ...를 Windows PC로 보내고, PC가 돌려준 답을 카카오톡에 다시 쓰게 만들 차례입니다.

아직 AI는 붙이지 않고, 지금 만든 PC 테스트 서버를 먼저 연결합니다. 이 프로젝트도 봇에서 http_post_json()으로 외부 HTTP 서버를 호출하는 방식을 공식 지원합니다.

  1. Windows의 server.py 실행 PowerShell은 그대로 켜두세요. 그리고 핸드폰 Termux에서 현재 카카오 백엔드 서버가 실행 중인 세션으로 가서 CTRL → c를 눌러 잠깐 끕니다. 다시 이런 프롬프트가 나오면 됩니다.
(.venv) ~/kakao-x-termux $

그 상태에서 아래 전체를 복사해서 Termux에 한 번에 붙여넣고 엔터 치세요. 기존 bot.py를 새 버전으로 교체합니다.

cat > app/bots/nureongi-bot/bot.py <<'PY'
from __future__ import annotations
from app.chatbot.base_bot import ObserverBot
from app.chatbot.context import ChatContext
from app.chatbot.decision import ChatDecision
from app.chatbot.module import MatchPattern
from app.chatbot.io_helpers import ExternalIOError
class NureongiBot(ObserverBot):
key = "nureongi_bot"
name = "누렁이"
version = "0.2.0"
description = "누렁아 호출을 Windows PC 서버로 전달하는 봇"
patterns = [
MatchPattern(type="mention", value=["누렁아"])
]
priority = 300
terminal = True
timeout_millis = 10000
default_options = {
"endpoint": "http://123.168.1.128:8080/ask",
"timeoutSeconds": 5,
}
def can_handle(self, context: ChatContext) -> bool:
if self.skip_command(context):
return False
return context.replyToken is not None
def run(self, context: ChatContext) -> ChatDecision:
text = context.normalizedText.strip()
if text.startswith("누렁아"):
prompt = text[len("누렁아"):].strip(" ,!?")
else:
prompt = text
if not prompt:
prompt = "안녕"
endpoint = self.str_option(
context,
"endpoint",
"http://123.168.1.128:8080/ask"
)
try:
response = self.http_post_json(
endpoint,
{
"prompt": prompt,
"room": context.room,
"sender": context.sender,
},
timeout_seconds=5,
ref_id=context.eventId,
error_prefix="nureongi pc backend unavailable",
)
except ExternalIOError:
return self.reply(
"PC 서버에 연결할 수 없어요."
)
data = response.json()
reply_text = str(
data.get("text")
or data.get("message")
or ""
).strip()
if not reply_text:
return self.reply("PC에서 답변을 받지 못했어요.")
return self.reply(reply_text)
def create_bot() -> NureongiBot:
return NureongiBot()
PY

프로젝트의 BaseBot에는 실제로 http_post_json()이 들어 있어서 Observer 봇에서도 이 방식으로 외부 서버를 호출할 수 있습니다.

붙여넣은 뒤 먼저 문법 검사:

python -m compileall -q app/bots/nureongi-bot

아무것도 안 나오면 정상입니다.

그다음 Termux 서버를 다시 켭니다.

bash scripts/run_termux_server.sh

Uvicorn running on http://129.0.0.1:8787이 나오면 다른 카카오 계정에서 이렇게 보내세요.

누렁아 오늘 뭐해?

이번에는 예전 고정 답변이 아니라 PC가 만든 답변:

PC 연결 성공! 질문: 오늘 뭐해

가 카카오톡에서 누렁이 계정의 답장으로 와야 합니다.

이게 성공하면 구조가 실제로:

카카오톡
→ 안드로이드
→ Windows PC
→ Windows 답변 생성
→ 안드로이드
→ 카카오톡

까지 완성된 겁니다.

저는 여기까지 성공했습니다.


API KEY 사용하기

딥시크 API 키를 만든 다음 Windows PowerShell에서 이렇게 저장합니다.

setx DEEPSEEK_API_KEY "여기에_발급받은_API_KEY"

그 후 PowerShell을 완전히 닫았다가 다시 열고:

echo $env:DEEPSEEK_API_KEY

로 환경변수가 잡혔는지 확인합니다.

다만 보안을 위해 키가 화면에 그대로 노출되는 게 싫다면 확인은 이렇게 하는 게 낫습니다.

if ($env:DEEPSEEK_API_KEY) { "DeepSeek API key 설정됨" } else { "설정 안 됨" }

정상이면:

DeepSeek API key 설정됨

이라고 나옵니다.

그 다음 제가 C:\nureongi\server.py를 DeepSeek API 호출용으로 바꿔드릴 겁니다. 최종 구조는:

누렁아 오늘 뭐 먹지?
↓
Termux
↓
Windows server.py
↓
DeepSeek V4 Flash API
↓
AI 답변
↓
카카오톡 누렁이

입니다.


이제 Windows의 테스트 응답을 DeepSeek 실제 답변으로 바꾸면 됩니다. 현재 공식 DeepSeek API는 https://api.deepseek.com에서 deepseek-v4-flash와 deepseek-v4-pro를 제공하고 있고, 카카오톡 비서 용도라면 빠르고 저렴한 deepseek-v4-flash가 적합합니다. DeepSeek API Docs

먼저 Windows에서 지금 실행 중인 server.py 창에서 Ctrl+C를 눌러 서버를 끄세요. 그리고 중요하게도 새 PowerShell 창을 하나 여세요. setx로 저장한 API 키는 새로 연 프로그램에서 적용되기 때문입니다.

새 PowerShell에서 이것부터 확인합니다.

if ($env:DEEPSEEK_API_KEY) { "DeepSeek 키 확인됨" } else { "키 없음" }

DeepSeek 키 확인됨이 나오면 다음으로 갑니다.

이제 메모장을 엽니다.

notepad C:\nureongi\server.py

기존 내용을 전부 지우고, 아래 내용을 통째로 복붙해서 저장하세요.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
import urllib.request
import urllib.error
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
MODEL = "deepseek-v4-flash"
def ask_deepseek(prompt):
if not DEEPSEEK_API_KEY:
raise RuntimeError("DEEPSEEK_API_KEY가 설정되어 있지 않습니다.")
payload = {
"model": MODEL,
"messages": [
{
"role": "system",
"content": (
"너는 카카오톡 AI 비서 '누렁이'다. "
"한국어로 자연스럽고 친근하게 답한다. "
"카카오톡 대화에 적합하게 너무 장황하지 않게 답한다. "
"확실하지 않은 사실은 단정하지 않는다."
)
},
{
"role": "user",
"content": prompt
}
],
"thinking": {
"type": "disabled"
},
"max_tokens": 1000,
"stream": False
}
data = json.dumps(
payload,
ensure_ascii=False
).encode("utf-8")
request = urllib.request.Request(
DEEPSEEK_URL,
data=data,
method="POST",
headers={
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
}
)
with urllib.request.urlopen(
request,
timeout=90
) as response:
result = json.loads(
response.read().decode("utf-8")
)
return result["choices"][0]["message"]["content"].strip()
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, data):
body = json.dumps(
data,
ensure_ascii=False
).encode("utf-8")
self.send_response(status)
self.send_header(
"Content-Type",
"application/json; charset=utf-8"
)
self.send_header(
"Content-Length",
str(len(body))
)
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_json(
200,
{
"ok": True,
"name": "nureongi-pc",
"model": MODEL
}
)
else:
self.send_json(
404,
{"error": "not found"}
)
def do_POST(self):
if self.path != "/ask":
self.send_json(
404,
{"error": "not found"}
)
return
length = int(
self.headers.get(
"Content-Length",
"0"
)
)
raw = self.rfile.read(length)
try:
data = json.loads(
raw.decode("utf-8")
)
prompt = str(
data.get("prompt", "")
).strip()
if not prompt:
self.send_json(
400,
{"error": "prompt is empty"}
)
return
answer = ask_deepseek(prompt)
self.send_json(
200,
{"text": answer}
)
except urllib.error.HTTPError as e:
error_body = e.read().decode(
"utf-8",
errors="replace"
)
print(
"DeepSeek HTTP 오류:",
e.code,
error_body
)
self.send_json(
502,
{
"error":
f"DeepSeek API 오류 ({e.code})"
}
)
except Exception as e:
print(
"오류:",
repr(e)
)
self.send_json(
500,
{
"error":
"AI 답변 생성 중 오류가 발생했습니다."
}
)
print("누렁이 DeepSeek 서버 시작")
print("모델:", MODEL)
print("http://0.0.0.0:8080")
server = ThreadingHTTPServer(
("0.0.0.0", 8080),
Handler
)
server.serve_forever()

DeepSeek 공식 Chat Completions API는 messages 배열과 model을 보내는 OpenAI 호환 형식이고, thinking.type="disabled"로 비추론 모드를 지정할 수 있습니다. 기본값은 thinking 활성화이므로, 카카오톡의 일상 질답은 일단 꺼서 응답 속도를 우선하겠습니다. DeepSeek API Docs

저장 후 PowerShell에서 서버를 실행합니다. python 별칭 문제가 있었으므로 이번에도 실제 Python 경로로 실행하세요.

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

정상이면:

누렁이 DeepSeek 서버 시작
모델: deepseek-v4-flash
http://0.0.0.0:8080

이렇게 나오고 멈춰 있는 것처럼 보입니다.

그 상태에서 카카오톡에서 바로 테스트하세요.

누렁아 대한민국 수도가 어디야?

이제 예전처럼:

PC 연결 성공! 질문: ...

이 나오면 안 되고, 누렁이가 DeepSeek가 생성한 실제 답변을 해야 합니다.

예를 들면:

대한민국의 수도는 서울이야.

정도로 올 겁니다.

참고로 지금은 대화 1회마다 독립 질문입니다. 즉 아직:

누렁아 내 이름 기억해
→ 다음 메시지에서 기억
→ 방 전체 대화 기억

같은 기능은 없습니다. DeepSeek 연결이 성공하면 바로 다음 단계에서 카카오톡 방별 대화기억 + 누렁이 성격 + 필요할 때만 호출되는 구조를 붙이는 게 좋습니다.

우선 지금 누렁아 대한민국 수도가 어디야?를 보내보고 어떤 답이 오는지만 확인해보세요.

여기까지는 성공했다.


만약 새로운 딥시크 코드가 실제 서버에 반영되지 않았다면

Windows에서 예전 서버가 떠 있는 PowerShell 창으로 가서 Ctrl + C를 눌러 서버를 끄세요.

그다음 새 PowerShell을 열고, 먼저 파일이 새 코드로 저장됐는지 확인합니다.

Select-String -Path C:\nureongi\server.py -Pattern "DeepSeek"

여기서 DeepSeek 관련 줄들이 나오면 파일은 잘 저장된 겁니다.

그다음 새 PowerShell에서 키도 확인하세요.

if ($env:DEEPSEEK_API_KEY) { "키 설정됨" } else { "키 없음" }

키 설정됨이 나오면 서버를 다시 실행합니다.

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

이번에는 시작 문구가 예전의

누렁이 PC 서버 시작

이렇게 다시 시작해보면 된다.


만약 테스트 서버가 살아있다면 그것을 해결해야 한다.

8080 포트에 예전 테스트 서버가 아직 살아 있습니다. 새 DeepSeek 서버는 시작 문구까지만 찍고, 실제로는 8080을 못 잡았을 가능성이 큽니다.

Windows의 새 PowerShell에서 먼저 이걸 실행하세요.

Get-NetTCPConnection -LocalPort 8080 -State Listen | Select-Object LocalAddress,LocalPort,OwningProcess

예를 들어 이렇게 나오면:

LocalAddress LocalPort OwningProcess
------------ --------- -------------
0.0.0.0 8080 12345

그다음 그 PID가 뭔지 확인합니다.

Get-Process -Id 12345

그리고 그 예전 서버 프로세스만 종료합니다.

Stop-Process -Id 12345 -Force

물론 12345는 실제 나온 숫자로 바꿔야 합니다.

그다음 DeepSeek 서버를 다시 실행하세요.

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

이번에는 실행 직후 오류 없이 그대로 대기 상태여야 합니다.

그 상태에서 새 PowerShell에서 다시 테스트:

Invoke-RestMethod -Method Post `
-Uri "http://129.0.0.1:8080/ask" `
-ContentType "application/json" `
-Body '{"prompt":"대한민국 수도는?"}'

프로세스 문제가 발생할 수 있다. 이런 경우

15940 프로세스를 확인만 했고, 아직 종료하지 않았습니다. 그래서 예전 서버가 계속 8080을 잡고 있을 가능성이 큽니다.

지금 새 server.py를 실행한 PowerShell이 멈춰 있다면 먼저 Ctrl+C로 끄세요. 그다음 아래 순서대로 하시면 됩니다.

Stop-Process -Id 15940 -Force

그리고 정말 8080이 비었는지 확인합니다.

Get-NetTCPConnection -LocalPort 8080 -State Listen -ErrorAction SilentlyContinue

아무것도 안 나오면 정상입니다.

그다음 새 DeepSeek 서버를 다시 실행하세요.

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

이 창은 그대로 켜두고, 새 PowerShell 창에서 테스트합니다.

Invoke-RestMethod -Method Post `
-Uri "http://129.0.0.1:8080/ask" `
-ContentType "application/json" `
-Body '{"prompt":"대한민국 수도는?"}'

이번에 PC 연결 성공! 질문:이 아니라 DeepSeek 실제 답변이 나와야 합니다.


15940을 죽였는데도 8080 리스너가 그대로 남아 있습니다. 즉, 8080을 잡고 있는 다른 Python 프로세스가 또 하나 있습니다.

지금은 다른 거 하지 말고, PowerShell에서 아래 두 줄만 실행해 주세요.

Get-NetTCPConnection -LocalPort 8080 -State Listen | Select-Object LocalAddress,LocalPort,OwningProcess

여기서 새 OwningProcess 숫자가 나올 겁니다. 그다음 그 숫자를 넣어서:

Get-CimInstance Win32_Process -Filter "ProcessId=여기PID" | Select-Object ProcessId,ExecutablePath,CommandLine

예를 들어 PID가 23184면:

Get-CimInstance Win32_Process -Filter "ProcessId=23184" | Select-Object ProcessId,ExecutablePath,CommandLine

이렇게요.

그리고 하나만 더 확인합시다. 지금 server.py 안에 옛 문구가 남아 있는지도 봅니다.

Select-String -Path C:\nureongi\server.py -Pattern "PC 연결 성공","deepseek-v4-flash","ask_deepseek"

결과를 그대로 보내주세요.

잘 해결됐다.


윈도우 업데이트로 인한 재부팅 후 자동 실행하기

1. Windows 재부팅 후 누렁이 자동실행

먼저 현재 수동으로 실행 중인 PowerShell 서버는 그대로 두고, 새 PowerShell에서 아래를 입력하세요.

notepad C:\nureongi\start_nureongi.ps1

메모장이 열리면 아래 내용을 통째로 붙여넣고 저장합니다.

$python = "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe"
$server = "C:\nureongi\server.py"
$log = "C:\nureongi\nureongi.log"
Set-Location "C:\nureongi"
while ($true) {
$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content $log "[$time] 누렁이 서버 시작"
& $python $server *>> $log
$time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content $log "[$time] 서버 종료됨. 10초 후 재시작"
Start-Sleep -Seconds 10
}

이 스크립트는 서버가 오류로 꺼져도 10초 뒤 자동으로 다시 살립니다.

이제 Windows 검색에서 작업 스케줄러를 실행하고 작업 만들기를 누릅니다.

설정은 아래대로 하시면 됩니다.

  • 이름: Nureongi_Server
  • 사용자의 로그온 여부에 관계없이 실행
  • 가장 높은 수준의 권한으로 실행
  • 트리거: 시작할 때
  • 가능하면 1분 지연
  • 동작 → 프로그램 시작
    • 프로그램: powershell.exe
    • 인수:-NoProfile -ExecutionPolicy Bypass -File "C:\nureongi\start_nureongi.ps1"
  • 설정 탭에서 “작업이 다음 시간 이상 실행되면 중지”는 반드시 해제
  • 이미 실행 중이면 새 인스턴스를 시작하지 않음

저장할 때 Windows 암호를 요구할 수 있습니다. Microsoft 계정이면 PIN이 아니라 실제 Microsoft 계정 암호를 넣어야 합니다.

이렇게 하면 Windows Update 때문에 새벽에 PC가 재부팅되어도:

Windows 재부팅
↓
약 1분 대기
↓
Nureongi_Server 자동 실행
↓
server.py 자동 실행
↓
누렁이 복구

가 됩니다. 업데이트 중 몇 분간은 당연히 답장을 못 하지만, 사람이 다시 PowerShell을 열 필요는 없어집니다.

2. 더 중요한 문제: 설정된 아이피 바뀌면 안 됩니다

현재 Termux에:

http://아이피/ask

가 박혀 있습니다.

그런데 공유기가 DHCP로 IP를 배정하고 있기 때문에, Windows 재부팅이나 공유기 재부팅 후 123.168.1.128이 다른 주소로 바뀔 가능성이 있습니다.

가장 좋은 해결책은 공유기에서 Windows PC의 MAC 주소에 123.168.1.128을 DHCP 예약하는 겁니다.

Windows PowerShell에서:

Get-NetAdapter | Where-Object {$_.Status -eq "Up"} | Select-Object Name,MacAddress

를 실행하면 현재 이더넷의 MAC 주소가 나옵니다.

예:

Name MacAddress
---- ----------
이더넷 AA-BB-CC-DD-EE-FF

이 MAC 주소에 대해 공유기에서:

항상 123.168.1.128 배정

으로 예약하면 됩니다.

공유기 모델을 알려주시면 KT/SKT/LG/개인 공유기별로 어디에서 설정하는지 정확히 안내해드릴 수 있습니다.

3. Windows 절전도 꺼야 합니다

PC가 절전모드에 들어가면 누렁이도 사실상 죽습니다.

Windows에서:

설정 → 시스템 → 전원 및 배터리 → 화면 및 절전

으로 들어가서 전원 연결 시:

장치를 절전 모드로 전환
→ 안 함

으로 두는 게 좋습니다.

모니터 화면은 꺼져도 상관없습니다. PC 본체만 잠들지 않으면 됩니다.


여기까지 하면 운영 구조는 꽤 안정적입니다.

Android 봇폰
↓
Termux
↓
아이피
↓
Windows 자동실행 서버
↓
DeepSeek API

그다음부터가 재미있는 부분입니다. 현재 누렁이는 매 질문을 완전히 새 질문으로 처리합니다.

우리는 링크시스 공유기여서 그걸 기준으로 설정했다.


누렁이를 “단순 질답봇”에서 “단톡방 맥락을 보는 봇”으로 바꾸는 단계로 가면 됩니다.

이 프로젝트는 이미 같은 방의 최근 메시지를 읽는 self.messages.recent(room_key, limit=...) 기능을 갖고 있습니다. 즉 별도 DB를 새로 만들 필요 없이, 누렁이가 호출될 때 최근 메시지 몇 개를 꺼내 DeepSeek에 같이 보내면 됩니다. 최근 메시지는 최신순으로 조회되므로 우리가 코드에서 다시 시간순으로 뒤집어 보내면 됩니다.

먼저 중요한 점 하나만: 이렇게 바꾸면 누렁아라고 호출하는 순간, 그 방의 최근 대화 일부도 DeepSeek API로 전송됩니다. 단톡방 참여자들에게 이 점을 알고 쓰는 게 좋습니다.

1단계 — Termux의 bot.py 교체

휴대폰 Termux에서 현재 서버를 CTRL → c로 끄세요. 그다음 아래 전체를 복붙합니다.

cat > app/bots/nureongi-bot/bot.py <<'PY'
from __future__ import annotations
from app.chatbot.base_bot import ObserverBot
from app.chatbot.context import ChatContext
from app.chatbot.decision import ChatDecision
from app.chatbot.module import MatchPattern
from app.chatbot.io_helpers import ExternalIOError
class NureongiBot(ObserverBot):
key = "nureongi_bot"
name = "누렁이"
version = "0.3.0"
description = "최근 단톡방 대화를 참고해 답하는 AI 비서"
patterns = [
MatchPattern(type="mention", value=["누렁아"])
]
priority = 300
terminal = True
timeout_millis = 30000
default_options = {
"endpoint": "http://123.168.1.128:8080/ask",
"timeoutSeconds": 20,
"historyCount": 12,
}
def can_handle(self, context: ChatContext) -> bool:
if self.skip_command(context):
return False
return context.replyToken is not None
def run(self, context: ChatContext) -> ChatDecision:
text = context.normalizedText.strip()
if text.startswith("누렁아"):
prompt = text[len("누렁아"):].strip(" ,!?")
else:
prompt = text
if not prompt:
prompt = "안녕"
# 같은 방의 최근 메시지 가져오기
records = self.messages.recent(
context.roomKey,
limit=12
)
history = []
# recent()는 최신순이므로 시간순으로 뒤집는다.
for record in reversed(records):
# 현재 "누렁아 ..." 메시지는 중복 전송하지 않는다.
if record.eventId == context.eventId:
continue
message_text = (record.text or "").strip()
if not message_text:
continue
history.append({
"sender": record.sender or "알 수 없음",
"text": message_text,
})
endpoint = self.str_option(
context,
"endpoint",
"http://192.168.1.128:8080/ask"
)
try:
response = self.http_post_json(
endpoint,
{
"prompt": prompt,
"room": context.room,
"sender": context.sender,
"history": history,
},
timeout_seconds=20,
ref_id=context.eventId,
error_prefix="nureongi pc backend unavailable",
)
except ExternalIOError:
return self.reply(
"지금 AI 서버 연결이 잠깐 안 돼. 조금 있다 다시 불러줘."
)
data = response.json()
reply_text = str(
data.get("text")
or data.get("message")
or ""
).strip()
if not reply_text:
return self.reply("답변을 만들지 못했어.")
return self.reply(reply_text)
def create_bot() -> NureongiBot:
return NureongiBot()
PY

문법 검사:

python -m compileall -q app/bots/nureongi-bot

아무것도 안 나오면 정상입니다.

아직 Termux 서버는 다시 켜지 마세요. Windows 쪽도 먼저 바꿉니다.


2단계 — Windows server.py 교체

현재 DeepSeek 서버 PowerShell에서 Ctrl+C로 서버를 끕니다.

그다음:

notepad C:\nureongi\server.py

기존 내용을 전부 지우고 아래를 통째로 붙여넣으세요.

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import os
import urllib.request
import urllib.error
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
MODEL = "deepseek-v4-flash"
SYSTEM_PROMPT = """
너는 카카오톡 단체채팅방에 함께 있는 AI 캐릭터 '누렁이'다.
규칙:
- 이름은 누렁이다.
- 사람들이 '누렁아'라고 부르면 대답한다.
- 한국어로 자연스럽고 친근하게 말한다.
- 지나치게 길게 말하지 않는다.
- 최근 단톡방 대화가 제공되면 그 내용을 참고해서 대답한다.
- 누가 어떤 말을 했는지 구분한다.
- 대화에 없는 내용을 누가 말했다고 지어내지 않는다.
- 모르는 내용은 모른다고 말한다.
- Markdown의 **굵게**, ### 제목 같은 표현은 쓰지 않는다.
- 카카오톡에서 사람이 자연스럽게 말하는 것처럼 답한다.
""".strip()
def ask_deepseek(prompt, sender, room, history):
if not DEEPSEEK_API_KEY:
raise RuntimeError("DEEPSEEK_API_KEY가 설정되어 있지 않습니다.")
history_lines = []
for item in history:
name = str(item.get("sender", "알 수 없음")).strip()
text = str(item.get("text", "")).strip()
if text:
history_lines.append(f"{name}: {text}")
if history_lines:
recent_context = "\n".join(history_lines)
else:
recent_context = "(최근 대화 없음)"
context_message = f"""
현재 카카오톡 방: {room}
최근 단톡방 대화:
{recent_context}
현재 누렁이를 부른 사람:
{sender}
현재 질문:
{prompt}
""".strip()
payload = {
"model": MODEL,
"messages": [
{
"role": "system",
"content": SYSTEM_PROMPT
},
{
"role": "user",
"content": context_message
}
],
"thinking": {
"type": "disabled"
},
"max_tokens": 900,
"stream": False
}
data = json.dumps(
payload,
ensure_ascii=False
).encode("utf-8")
request = urllib.request.Request(
DEEPSEEK_URL,
data=data,
method="POST",
headers={
"Authorization": f"Bearer {DEEPSEEK_API_KEY}",
"Content-Type": "application/json"
}
)
with urllib.request.urlopen(
request,
timeout=60
) as response:
result = json.loads(
response.read().decode("utf-8")
)
return result["choices"][0]["message"]["content"].strip()
class Handler(BaseHTTPRequestHandler):
def send_json(self, status, data):
body = json.dumps(
data,
ensure_ascii=False
).encode("utf-8")
self.send_response(status)
self.send_header(
"Content-Type",
"application/json; charset=utf-8"
)
self.send_header(
"Content-Length",
str(len(body))
)
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/health":
self.send_json(
200,
{
"ok": True,
"name": "nureongi-pc",
"model": MODEL
}
)
else:
self.send_json(
404,
{"error": "not found"}
)
def do_POST(self):
if self.path != "/ask":
self.send_json(
404,
{"error": "not found"}
)
return
length = int(
self.headers.get("Content-Length", "0")
)
raw = self.rfile.read(length)
try:
data = json.loads(
raw.decode("utf-8")
)
prompt = str(
data.get("prompt", "")
).strip()
sender = str(
data.get("sender", "")
or "알 수 없음"
).strip()
room = str(
data.get("room", "")
or "알 수 없는 방"
).strip()
history = data.get("history", [])
if not isinstance(history, list):
history = []
if not prompt:
self.send_json(
400,
{"error": "prompt is empty"}
)
return
answer = ask_deepseek(
prompt,
sender,
room,
history
)
self.send_json(
200,
{"text": answer}
)
except urllib.error.HTTPError as e:
error_body = e.read().decode(
"utf-8",
errors="replace"
)
print(
"DeepSeek HTTP 오류:",
e.code,
error_body
)
self.send_json(
502,
{"error": f"DeepSeek API 오류 ({e.code})"}
)
except Exception as e:
print("오류:", repr(e))
self.send_json(
500,
{"error": "AI 답변 생성 중 오류가 발생했습니다."}
)
print("누렁이 DeepSeek 서버 시작")
print("모델:", MODEL)
print("최근 대화 기억 기능: ON")
print("http://0.0.0.0:8080")
server = ThreadingHTTPServer(
("0.0.0.0", 8080),
Handler
)
server.serve_forever()

저장하고 서버를 실행합니다.

cd C:\nureongi
& "C:\Users\JS\AppData\Local\Programs\Python\Python313\python.exe" server.py

이렇게 나오면 정상:

누렁이 DeepSeek 서버 시작
모델: deepseek-v4-flash
최근 대화 기억 기능: ON
http://0.0.0.0:8080

3단계 — Termux 서버 다시 켜기

휴대폰 Termux에서:

bash scripts/run_termux_server.sh

그리고 테스트방에서 일부러 누렁이를 부르지 않고 먼저 대화를 해보세요.

예를 들어:

반: 오늘 저녁 뭐 먹지?
철수: 나는 삼겹살
영희: 난 치킨이 좋아
반: 나는 회도 괜찮은데

그다음:

반: 누렁아 지금까지 나온 메뉴가 뭐야?

정상적으로 작동하면 누렁이는 대략:

삼겹살, 치킨, 회가 나왔네.
철수는 삼겹살, 영희는 치킨을 골랐고 너는 회도 괜찮다고 했어.

처럼 답해야 합니다.

이게 성공하면 엄청 중요한 단계가 끝난 겁니다.

현재 누렁이는 이제:

단톡방 대화 수신
→ DB 저장
→ 평소에는 침묵
→ "누렁아" 호출
→ 같은 방 최근 12개 메시지 조회
→ DeepSeek에 맥락 + 현재 질문 전달
→ 자연어 답변

이 됩니다.

다만 이것은 아직 “최근 대화 기억”입니다. 장기 기억은 아닙니다. 다음 단계에서는 예를 들어:

누렁아 어제 철수가 여행 어디 간다고 했지?
누렁아 지난주에 우리가 얘기했던 노트북 뭐였어?
누렁아 오늘 대화 전체 요약해줘.

처럼 DB 검색까지 하는 기억 기능을 붙이면 됩니다. 프로젝트에 이미 메시지 검색 기능도 들어 있어서 이것도 충분히 가능합니다. ReadOnlyMessageStore는 최근 메시지뿐 아니라 search(query, room_key=...)도 제공합니다.


글이 길어져서 일단 여기까지 합니다.

봇의 성격과

필요한 기능이 있다면 API KEY를 이용해서 추가하면 되리라 생각합니다.

코멘트

답글 남기기

syn2bloom에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기