Flask UI · history · 토큰한도 · SQLite · session · 요약 · SSE
박수현 · (사)한국정보공학기술사회 · 2026
1.openai/2~5.chatbot*/ 6개 파일 — 단계별 실행1.openai/6.twobots/ — 두 봇이 자동 대화1.openai/8.streaming/ — SSE 스트리밍 챗봇📂 베이스 소스: 모두 Flask 기반 단일 파일. 한 단계씩 진화하는 구조라 매 단계의 diff 만 보면 됨.
| 단계 | 베이스 코드 | 핵심 진화 |
|---|---|---|
| 1 | 2.chatbot_ui/ |
Flask + 정적 HTML + REST 호출 한 번 |
| 2 | 3.chatbot2_history/app3_history.py |
conversation_history 리스트 누적 |
| 3 | 3.chatbot2_history/app4_historylimit.py |
마지막 N개만 유지 (token window) |
| 4 | 4.chatbot3_historysqlite/app4_historysqlite3.py |
SQLite 로 영속화 |
| 5 | 5.chatbot4_session/app5_session.py |
session_id 도입 |
| 6 | 5.chatbot4_session/app7_session_summary.py |
요약으로 압축 |
| 7 | 8.streaming/1.sse_stream.py |
SSE 청크 전송 |
💡 1~3단계(history·window·temperature)가 이 세션의 핵심 — 멀티턴 챗봇의 본질. 4~7단계(SQLite·세션·요약·SSE)는 서비스화로 가는 확장이니, 시간이 빠듯하면 핵심을 확실히 잡고 나머지는 코드 위치만 짚고 넘어가도 됨.
stateless 위에 대화를 얹는 가장 짧은 코드 — 약 15분
2.chatbot_ui/app2_openailib.py — 한 호출 챗봇from flask import Flask, request, jsonify, send_from_directory
from openai import OpenAI
from dotenv import load_dotenv
import os
load_dotenv('../.env')
app = Flask(__name__, static_folder='public')
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
@app.route('/api/chat', methods=['POST'])
def chat():
user_input = request.json.get('userInput', '')
r = client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': user_input},
],
)
return jsonify({'reply': r.choices[0].message.content})
@app.route('/')
def root():
return send_from_directory('public', 'index.html')
if __name__ == '__main__':
app.run(port=5000)
3.chatbot2_history/app3_history.py — 메모리에 history# 모듈 전역에 history 리스트 — 모든 사용자가 공유 (단점!)
conversation_history = []
@app.route('/api/chat', methods=['POST'])
def chat():
user_input = request.json.get('userInput', '')
# ① 사용자 메시지를 history 에 누적
conversation_history.append({'role': 'user', 'content': user_input})
# ② history 전체를 API 로 전송 (stateless 위의 대화 시뮬)
r = client.chat.completions.create(
model='gpt-4o-mini',
messages=conversation_history,
)
reply = r.choices[0].message.content
# ③ assistant 응답도 history 에 추가 (다음 호출에 같이 전송)
conversation_history.append({'role': 'assistant', 'content': reply})
return jsonify({'reply': reply})
💡 멀티턴의 전부는 이 3단계 — ① user 추가 ② 전체 messages 전송 ③ assistant 응답을 다시 추가. 모델의 지난 답변을
assistant역할로 되넣어야 자기 답을 근거로 맥락을 이어감.
| 모델 | 컨텍스트 한도 | 의미 |
|---|---|---|
| gpt-4o-mini | 128K 토큰 | 약 한국어 책 한 권 |
| gpt-4o | 128K | 동일 |
| gpt-4.1 | 1M 토큰 | 책 7~10권 |
| claude-haiku-4-5 | 200K | 책 1.5권 |
| claude-opus-4-7 | 1M | 책 7~10권 |
openai.BadRequestError: This model's maximum context length is 128000 tokens.
However, your messages resulted in 132847 tokens.
app4_historylimit.py — 마지막 N개만WINDOW_SIZE = 10 # 최근 10턴만 유지 (system 메시지는 별도)
@app.route('/api/chat', methods=['POST'])
def chat():
user_input = request.json.get('userInput', '')
conversation_history.append({'role': 'user', 'content': user_input})
# system 메시지 + 최근 WINDOW_SIZE 만 API 로 전송
system_msg = [{'role': 'system', 'content': '한국어 전문가.'}]
recent = conversation_history[-WINDOW_SIZE:]
messages = system_msg + recent
r = client.chat.completions.create(model='gpt-4o-mini', messages=messages)
reply = r.choices[0].message.content
conversation_history.append({'role': 'assistant', 'content': reply})
return jsonify({'reply': reply})
⚠️ system 메시지 는 항상 윈도우 밖에서 별도 보존. 잘라먹으면 페르소나가 망가짐.
| 파라미터 | 역할 | 범위 |
|---|---|---|
| temperature | 무작위성·창의성 | 0.0 ~ 2.0 |
| max_tokens | 응답 최대 토큰 | 모델 한계 내 |
| top_p | 상위 확률 토큰만 후보 | 0.0 ~ 1.0 |
| frequency_penalty | 같은 단어 반복 억제 | -2.0 ~ 2.0 |
| presence_penalty | 새 주제 도입 장려 | -2.0 ~ 2.0 |
# 1.openai/1.intro/12.sdk_params.py
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role':'system','content':'친절한 AI 도우미.'},
{'role':'user','content':'AI를 한 문장으로 설명해줘.'},
],
temperature=0.7, # 0.0 정확·일관 ~ 2.0 창의·다양
top_p=0.9,
)
💡 일부 추론 특화 모델은 temperature 를 지원하지 않음 — 모델별 지원 파라미터 확인 후 적용. Day 2 RAG 에서는 낮은 temperature 가 기본.

Temperature 샘플링 softmax(zᵢ/T) · T 낮으면 뾰족(결정적), 높으면 평평(창의·다양) · top-p/top-k와 함께 제어
서버 재시작에도 살아남는 대화 — 약 10분
app4_historysqlite3.py — DB 가 historyconn = sqlite3.connect('history.db', check_same_thread=False)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
def init_db():
cursor.execute("""
CREATE TABLE IF NOT EXISTS conversation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
def insert(role: str, content: str):
cursor.execute("INSERT INTO conversation (role, content) VALUES (?, ?)",
(role, content))
conn.commit()
def get_recent(n: int = 20) -> list[dict]:
cursor.execute(
"SELECT role, content FROM conversation ORDER BY id DESC LIMIT ?", (n,))
rows = cursor.fetchall()
return [{'role': r['role'], 'content': r['content']} for r in reversed(rows)]
chat() 가 단순해진다user_input = request.json.get('userInput', '')
insert('user', user_input)
messages = get_recent(20)
r = client.chat.completions.create(model='gpt-4o-mini', messages=messages)
reply = r.choices[0].message.content
insert('assistant', reply)
WHERE session_id = ? 한 줄로 격리 가능해짐| 함정 | 증상 | 해결 |
|---|---|---|
| same_thread 검사 | ProgrammingError: SQLite objects created in a thread can only be used in that same thread |
sqlite3.connect(..., check_same_thread=False) |
| 요청마다 새 connection | 성능 저하 + 잠금 충돌 | Flask g 객체에 connection 저장, teardown_appcontext 로 정리 |
| 트랜잭션 누락 | 데이터 사라짐 | INSERT 후 conn.commit() 잊지 말 것 |
g + teardownfrom flask import g
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect('history.db')
return db
@app.teardown_appcontext
def close_db(exc):
db = getattr(g, '_database', None)
if db is not None:
db.close()
💡 운영 환경: SQLite 는 단일 프로세스용. 동시 접속이 많으면 PostgreSQL/MySQL 또는 Redis 로.
누구의 대화인가 — 약 10분
session_id 라는 한 컬럼CREATE TABLE conversation (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL, -- ← 추가
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_session ON conversation(session_id);
session) — 브라우저 기준app5_session.py — session_id 로 격리from flask import session
import uuid
app.secret_key = 'change-me-in-prod'
@app.before_request
def ensure_session_id():
if 'sid' not in session:
session['sid'] = str(uuid.uuid4())
def insert(sid: str, role: str, content: str):
cursor.execute(
"INSERT INTO conversation "
"(session_id, role, content) VALUES (?, ?, ?)",
(sid, role, content))
conn.commit()
def get_recent(sid: str, n: int = 20) -> list[dict]:
cursor.execute(
"SELECT role, content FROM conversation "
"WHERE session_id = ? ORDER BY id DESC LIMIT ?",
(sid, n))
return [{'role':r['role'],'content':r['content']}
for r in reversed(cursor.fetchall())]
chat() — sid 로 격리@app.route('/api/chat', methods=['POST'])
def chat():
sid = session['sid']
user_input = request.json.get('userInput', '')
insert(sid, 'user', user_input)
messages = get_recent(sid, 20)
r = client.chat.completions.create(
model='gpt-4o-mini', messages=messages)
reply = r.choices[0].message.content
insert(sid, 'assistant', reply)
return jsonify({'reply': reply})
💡 세션 삭제 엔드포인트 (
app6_session_delete.py) 도 추가 — 사용자가 "내 대화 지우기" 기능을 가질 수 있게.
긴 대화를 압축해 토큰을 줄이기 — 약 10분
[system] 한국어 전문가.
[system] (요약) 사용자는 Flask 챗봇을 만드는 중이며,
11턴까지 SQLite 영속화·세션 분리를 마쳤음.
[user] 이제 요약 압축은 어떻게?
[assistant] (최근 응답)
[user] (현재 질문)
app7_session_summary.py 핵심SUMMARY_TRIGGER = 20 # 20턴 이상이면 요약
KEEP_RECENT = 10 # 최근 10턴은 유지
def summarize_old(messages: list[dict]) -> str:
"""오래된 메시지들을 한 문단으로 압축."""
old_text = "\n".join(
f"{m['role']}: {m['content']}" for m in messages)
r = client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role':'system','content':
'다음 대화를 한 문단으로 핵심만 요약하세요.'},
{'role':'user', 'content': old_text},
],
max_tokens=300,
temperature=0.0,
)
return r.choices[0].message.content
def compress_history(sid: str):
rows = get_all_messages(sid)
if len(rows) <= SUMMARY_TRIGGER:
return # 아직 압축 불필요
old = rows[:-KEEP_RECENT] # 오래된 부분
summary = summarize_old(old)
delete_messages_before(
sid, rows[-KEEP_RECENT]['id'])
insert(sid, 'system',
f'(이전 대화 요약) {summary}')
자동 토론·면접·페어 코딩 — 약 8분
6.twobots/1.debate — 두 페르소나 토론client = OpenAI()
PERSONA_A = "당신은 강력한 AI 규제를 주장하는 윤리학자."
PERSONA_B = "당신은 AI 혁신 자유를 옹호하는 기술 기업가."
def speak(persona: str, history: list[dict]) -> str:
r = client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role':'system','content':persona}]
+ history,
max_tokens=200,
temperature=0.7,
)
return r.choices[0].message.content
# A 의 첫 발언
topic = "AI 모델 학습에 저작권 콘텐츠 사용을 금지해야 하는가?"
history = [{'role':'user','content':f'주제: {topic}'}]
say_a = speak(PERSONA_A, history)
history.append({'role':'assistant','content':say_a})
for turn in range(10):
# B 가 응답 (B 시점에서 A 발언이 user)
history_b = swap_roles(history)
say_b = speak(PERSONA_B, history_b)
history.append({'role':'user','content':say_b})
# A 가 응답
say_a = speak(PERSONA_A, history)
history.append({'role':'assistant','content':say_a})
print(f"[Turn {turn}] A: {say_a[:80]}…")
print(f"[Turn {turn}] B: {say_b[:80]}…")
응답 체감 시간을 줄이는 기법 — 약 12분
사용자 입력
↓ 0초
[기다림 ......]
↓ 5초 (생성 완료)
응답 전체가 한 번에 등장
사용자 입력
↓ 0초
[기다림 .....]
↓ 0.5초 → "안" 등장
↓ 0.6초 → "안녕"
↓ 0.8초 → "안녕하세요"
↓ ...
↓ 5초 → 전체 완료
🎯 같은 5초 응답이라도 체감 시간은 1/10. ChatGPT 가 한 글자씩 타이핑되는 그 효과.
| 항목 | SSE | WebSocket |
|---|---|---|
| 방향 | 서버 → 클라이언트 (단방향) | 양방향 |
| 프로토콜 | HTTP/1.1 | upgrade 후 별도 |
| 재연결 | 브라우저 자동 | 직접 구현 |
| 챗봇 적합도 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
8.streaming/1.sse_stream.py 핵심from flask import Flask, Response, request
@app.route('/stream', methods=['POST'])
def stream():
user_message = request.json.get('message', '')
def generate():
response = client.chat.completions.create(
model='gpt-4o-mini',
messages=[
{'role':'system','content':'한국어 친절 도우미.'},
{'role':'user', 'content': user_message},
],
stream=True, # ← 핵심!
temperature=0.7,
max_tokens=1000,
)
for chunk in response: # 청크 단위 iteration
delta = chunk.choices[0].delta.content
if delta:
# SSE 포맷: "data: <payload>\n\n"
yield f"data: {json.dumps({'token': delta})}\n\n"
yield "data: [DONE]\n\n"
return Response(generate(), mimetype='text/event-stream')
mimetype='text/event-stream' → SSE 인식X-Accel-Buffering: no 헤더 권장 (nginx 버퍼링 방지)<input id="msg" />
<button onclick="send()">Send</button>
<div id="out"></div>
<script>
async function send() {
const msg = document.getElementById('msg').value;
const out = document.getElementById('out');
out.textContent = '';
// Fetch 로 POST + stream 응답 읽기
const resp = await fetch('/stream', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: msg}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const text = decoder.decode(value);
// "data: {...}\n\n" 형식 파싱
text.split('\n\n').forEach(line => {
if (!line.startsWith('data: ')) return;
const payload = line.slice(6);
if (payload === '[DONE]') return;
const {token} = JSON.parse(payload);
out.textContent += token;
});
}
}
</script>
💡 React/Vue 사용 시 동일 패턴 —
ReadableStreamreader 로 청크 읽기. 라이브러리 따로 필요 없음.
대상 도메인 챗봇 — 약 25분
5.chatbot4_session/app7_session_summary.py 를 대상 도메인에 맞게 수정 (15분)8.streaming/ 의 SSE 패턴 가져와 위 챗봇에 스트리밍 적용📌 답 안 나오면
system메시지를 더 구체적으로 (역할 + 톤 + 금지 사항).
1.openai/3~5.chatbot*/ 6개 파일 모두 실행해 챗봇 띄우기1.openai/8.streaming/ 의 SSE 챗봇 띄우고 응답 체감 비교bash
python -c "import langchain, langchain_openai; print('ok')"