
해당 코드는 프롬프트가 업데이트 되었으나 반영되지 못함.
전체적인 코드 구조를 공유하기 위한 코드로
목표
메모리
툴
검증
로직을 살펴볼것.
추후 내용 검수 및 업데이트 예정
"""
====================================================================
AI 기반 회의 지식 관리 시스템 - 음성 STT 변환 & 정제 에이전트 (main.py)
====================================================================
[이 코드가 'AI 에이전트'로 불릴 수 있는 이유]
1) Goal : build_system_prompt() 가 에이전트의 역할/목표/출력 형식을 명문화한다.
2) Memory : AgentMemory 가 (a) 이번 회의 배경, (b) 회의를 거듭할수록 쌓이는
도메인 용어집(glossary_memory.json)을 영속적으로 저장/재사용한다.
3) Tool : tool_lookup_glossary(), tool_transcribe() 를 ReAct 방식
(Thought -> Action -> Observation) 으로 호출한다.
4) Verify : LLM Self-Critique 로 결과물에 0~10점을 매기고, 기준 미달 시
피드백을 프롬프트에 더해 최대 MAX_RETRY 회까지 재시도한다.
[전제 조건]
- Ollama 가 로컬에서 실행 중이어야 함 (기본값: ollama serve, 포트 11434)
- OLLAMA_MODEL 로 지정한 모델이 이미 pull 되어 있어야 함
- pip install faster-whisper requests
"""
import os
import json
import time
import requests
from dataclasses import dataclass, field
from typing import Dict, List
from faster_whisper import WhisperModel
# --------------------------------------------------------------
# 0. 설정값
# --------------------------------------------------------------
OLLAMA_URL = "http://localhost:11434/api/chat"
OLLAMA_MODEL = "llama3.1:8b" # 이미 pull 되어 있는 로컬 모델 재사용 (속도 개선 핵심 1)
WHISPER_MODEL_SIZE = "small" # "base"보다 한국어 인식률이 좋음. 급하면 "base"로 되돌려도 됨
GLOSSARY_PATH = "glossary_memory.json" # 회의를 거듭할수록 누적되는 장기 기억(용어집)
MAX_RETRY = 3
QUALITY_THRESHOLD = 7 # 10점 만점 기준 통과선
# --------------------------------------------------------------
# 1. GOAL: 목표를 프롬프트로 명문화
# --------------------------------------------------------------
def build_system_prompt(meeting_context: str, glossary_hint: str) -> str:
"""
에이전트의 '목표'를 담은 시스템 프롬프트.
- STT 결과물의 특성(구어체, 반복, 오인식)을 명시적으로 알려줘야 정제 품질이 올라간다.
- 배경 정보(meeting_context)와 도메인 용어(glossary_hint)를 함께 주입해
"정문 -> 전문(메시지 규격)" 같은 문맥 기반 오인식 교정이 가능해진다.
- 출력 형식을 강하게 고정해야 매번 재현성 있는 회의록이 나온다.
"""
#return f"""당신은 사내 회의 녹취록을 정리하는 10년차 회의록 작성 전문가입니다.
return f"""
당신은 음성인식(STT) 결과를 정제하는 전문가입니다.
[이번 회의 배경]
{meeting_context or "제공된 배경 정보 없음"}
[자주 사용하는 도메인 용어]
{glossary_hint or "저장된 용어집 없음"}
[목표]
입력은 Whisper가 추출한 STT 원문입니다.
당신의 역할은 사람이 읽기 편하도록 STT를 정제하는 것입니다.
[할 수 있는 작업]
1. STT 오인식을 수정한다.
2. 반복되는 문장을 제거한다.
3. "어", "음", "그..." 같은 의미 없는 추임새를 제거한다.
4. 띄어쓰기와 문장부호를 자연스럽게 수정한다.
5. 용어집을 참고하여 잘못 인식된 단어를 수정한다.
[절대로 하면 안 되는 작업]
- 회의록으로 재작성하지 않는다.
- 내용을 요약하지 않는다.
- 새로운 정보를 추가하지 않는다.
- 문장의 순서를 변경하지 않는다.
- 원문의 의미를 변경하지 않는다.
출력은 정제된 STT 텍스트만 출력한다.
"""
# --------------------------------------------------------------
# 2. MEMORY: 회의 배경 + 누적 도메인 용어집
# --------------------------------------------------------------
@dataclass
class AgentMemory:
meeting_context: str = ""
glossary: Dict[str, str] = field(default_factory=dict)
history: List[str] = field(default_factory=list) # 이번 실행의 시도별 결과
@classmethod
def load(cls, meeting_context: str) -> "AgentMemory":
glossary = {}
if os.path.exists(GLOSSARY_PATH):
with open(GLOSSARY_PATH, "r", encoding="utf-8") as f:
glossary = json.load(f)
return cls(meeting_context=meeting_context, glossary=glossary)
def save_glossary(self) -> None:
with open(GLOSSARY_PATH, "w", encoding="utf-8") as f:
json.dump(self.glossary, f, ensure_ascii=False, indent=2)
def glossary_hint_text(self) -> str:
if not self.glossary:
return ""
return "\n".join(f"- {k} -> {v}" for k, v in self.glossary.items())
# --------------------------------------------------------------
# Ollama 호출 헬퍼 (transformers pipeline 대신 사용 -> 속도 개선 핵심 2)
# --------------------------------------------------------------
def call_ollama(system_prompt: str, user_prompt: str, json_mode: bool = False) -> str:
payload = {
"model": OLLAMA_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"stream": False,
"options": {
"temperature": 0.2, # 정제/평가는 창의성보다 일관성이 중요
"num_predict": 700,
},
}
if json_mode:
payload["format"] = "json"
try:
res = requests.post(OLLAMA_URL, json=payload, timeout=180)
res.raise_for_status()
return res.json()["message"]["content"]
except requests.exceptions.ConnectionError:
raise RuntimeError(
"Ollama 서버에 연결할 수 없습니다. 'ollama serve' 실행 여부와 "
f"'{OLLAMA_MODEL}' 모델 pull 여부를 확인하세요."
)
# --------------------------------------------------------------
# 3. TOOL: ReAct 스타일 Thought -> Action -> Observation
# --------------------------------------------------------------
def tool_transcribe(audio_path: str, stt_model: WhisperModel) -> str:
print(" [Action] tool_transcribe 호출 (faster-whisper)")
segments, _ = stt_model.transcribe(
audio_path,
language="ko", # 언어 자동감지 스킵 -> 속도 개선 핵심 3
beam_size=1, # beam search 축소 -> 속도 개선 핵심 4 (정확도와 트레이드오프)
vad_filter=True, # 무음 구간 스킵 -> 속도 개선 핵심 5
)
text = " ".join(seg.text for seg in segments)
print(f" [Observation] STT 원문 {len(text)}자 확보")
return text
def tool_lookup_glossary(memory: AgentMemory) -> str:
print(" [Action] tool_lookup_glossary 호출 (저장된 용어집 조회)")
hint = memory.glossary_hint_text()
print(f" [Observation] 용어집 {len(memory.glossary)}건 로드")
return hint
# --------------------------------------------------------------
# 4. VERIFY: LLM Self-Critique + 재시도
# --------------------------------------------------------------
def verify_result(raw_text: str, cleaned_text: str) -> Dict:
system = "당신은 STT 정제 결과를 검토하는 품질 평가자입니다. 반드시 JSON으로만 답하세요."
user = f"""
[STT 원문]
{raw_text}
[정제 결과]
{cleaned_text}
다음 JSON 형식으로만 답하세요.
{{"score": 0부터 10 사이 정수, "reason": "감점 이유(한 문장)"}}
평가 기준
1. 원문의 의미를 유지했는가?
2. 새로운 내용을 추가하지 않았는가?
3. 요약하지 않았는가?
4. 오인식만 수정했는가?
5. 반복과 추임새만 제거했는가?
6. 문장 순서를 유지했는가?
정제 결과가 원문보다 길어졌거나 새로운 정보가 추가되었다면 감점하세요.
"""
raw = call_ollama(system, user, json_mode=True)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"score": 0, "reason": "평가 결과 JSON 파싱 실패"}
# --------------------------------------------------------------
# 5. 에이전트 메인 루프 (Thought -> Action -> Observation -> Verify -> Retry)
# --------------------------------------------------------------
def run_agent(audio_path: str, memory: AgentMemory, stt_model: WhisperModel) -> str:
print(f"[Thought] '{memory.meeting_context}' 회의이므로 용어집 도구로 오인식 힌트를 먼저 확보한다.")
glossary_hint = tool_lookup_glossary(memory)
raw_text = tool_transcribe(audio_path, stt_model)
system_prompt = build_system_prompt(memory.meeting_context, glossary_hint)
feedback = ""
cleaned_text = ""
for attempt in range(1, MAX_RETRY + 1):
print(f"\n--- 정제 시도 {attempt}/{MAX_RETRY} ---")
user_prompt = f"[STT 원문]\n{raw_text}"
if feedback:
user_prompt += f"\n\n[이전 시도 피드백 - 반드시 개선할 것]\n{feedback}"
cleaned_text = call_ollama(system_prompt, user_prompt)
verdict = verify_result(raw_text, cleaned_text)
print(f" [Verify] score={verdict.get('score')} reason={verdict.get('reason')}")
if verdict.get("score", 0) >= QUALITY_THRESHOLD:
memory.history.append(cleaned_text)
return cleaned_text
feedback = f"""이전 결과의 문제점:
{verdict.get("reason", "")}
원문의 의미는 유지한 채 다시 정제하세요.
"""
time.sleep(0.5)
print("최대 재시도 횟수 초과. 마지막 결과물을 반환합니다.")
return cleaned_text
# --------------------------------------------------------------
# 실행부
# --------------------------------------------------------------
if __name__ == "__main__":
print("=== 회의 STT 정제 에이전트 ===")
meeting_context = input(
"이번 회의 주제를 간략히 입력하세요 (예: DB 락으로 메시지 발송 실패 이슈 논의): "
).strip()
extra_terms = input(
"자주 등장할 전문용어가 있다면 '오인식어:정답어' 형태로 콤마 구분 입력 (없으면 엔터): "
).strip()
memory = AgentMemory.load(meeting_context)
if extra_terms:
for pair in extra_terms.split(","):
if ":" in pair:
wrong, correct = pair.split(":", 1)
memory.glossary[wrong.strip()] = correct.strip()
memory.save_glossary()
print(f"\nWhisper 모델({WHISPER_MODEL_SIZE}) 로딩 중...")
stt_model = WhisperModel(WHISPER_MODEL_SIZE, device="cpu", compute_type="int8")
file_path = input("음성 파일 경로 (엔터 시 기본값 '에러건.m4a'): ").strip() or "에러건.m4a"
if not os.path.exists(file_path):
print("파일 경로를 확인해주세요.")
else:
result = run_agent(file_path, memory, stt_model)
print("\n[최종 정제된 STT]\n")
print(result)
AI Agent를 공부하면서 가장 먼저 만들어 보고 싶었던 것은 STT(Speech To Text) 정제 Agent였다.
처음에는 단순히 "음성을 텍스트로 변환하는 프로그램"을 만들려고 했지만, 구현을 진행하면서 한 가지를 깨달았다.
Whisper는 음성을 텍스트로 변환(STT) 하는 역할만 한다.
하지만 실제 업무에서는 Whisper가 만들어 준 결과를 그대로 사용하는 경우는 거의 없다.
예를 들어
어.. 그 그러니까 오늘은 DB 락 때문에 메세지 전송이 안됐고...
처럼
- 추임새
- 반복
- 오인식
- 띄어쓰기 오류
등이 그대로 포함되어 있다.
내가 만들고 싶었던 것은
"텍스트를 생성하는 AI"가 아니라 "STT 결과를 사람이 읽기 좋은 형태로 정제하는 AI Agent"였다.
최종 목표
내가 정의한 Agent의 역할은 다음과 같다.
음성 파일
│
▼
Whisper(STT)
│
▼
STT 원문
│
▼
AI Agent
│
▼
오인식 수정
반복 제거
추임새 제거
띄어쓰기 수정
│
▼
정제된 STT
여기서 중요한 점은
원문의 의미는 절대로 변경하지 않는 것이다.
따라서
- 회의록 작성
- 내용 요약
- 새로운 내용 생성
은 모두 금지하도록 설계했다.
왜 AI Agent라고 부를 수 있을까?
이번 프로젝트에서는 일반적인 프로그램이 아니라 AI Agent의 핵심 요소를 모두 포함하도록 설계했다.
Goal
↓
Memory
↓
Tool
↓
Verify
↓
Retry
이 다섯 단계가 반복되면서 최종 결과를 만들어낸다.
이제 각각을 살펴보자.
1. Goal - Agent의 목표 정의
Agent에서 가장 중요한 것은 Goal이라고 생각한다.
LLM은 같은 모델이라도 어떤 목표를 주느냐에 따라 완전히 다른 결과를 만든다.
이번 프로젝트에서는 System Prompt를 이용하여
Agent의 역할을 명확하게 정의했다.
당신은 STT 결과를 정제하는 전문가입니다.
할 수 있는 작업
- 오인식 수정
- 반복 제거
- 추임새 제거
- 띄어쓰기 수정
절대로 하면 안 되는 작업
- 회의록 작성
- 내용 요약
- 새로운 내용 추가
- 원문의 의미 변경
즉
AI가 "회의록을 작성"하는 것이 아니라
"원문을 최대한 유지하면서 STT만 정제"하도록 목표를 설정했다.
Goal을 명확하게 정의해 주는 것만으로도 결과가 크게 달라졌다.
2. Memory - 이전 회의 정보를 기억하기
다음은 Memory이다.
이번 프로젝트에서는 두 가지 정보를 기억하도록 설계했다.
Memory
├── 회의 배경
└── 도메인 용어집
회의 배경은
예를 들어
Kafka 장애 분석 회의
처럼
현재 회의가 어떤 내용인지 알려주는 역할을 한다.
그리고
용어집은
메세지 → 메시지
카프카 → Kafka
정문 → 전문
처럼
회사에서 자주 사용하는 단어를 저장한다.
새로운 용어가 추가되면
JSON 파일에 저장되고
다음 회의에서도 그대로 사용할 수 있다.
즉
Agent는 실행이 끝나도
용어집이라는 형태의 장기 기억(Long-Term Memory)을 유지한다.
3. Tool - Agent가 직접 사용하는 기능
AI Agent는 생각만 하는 것이 아니라
필요한 Tool을 호출한다.
이번 프로젝트에서는 두 개의 Tool을 만들었다.
STT Tool
tool_transcribe()
Whisper를 이용하여
음성을 텍스트로 변환한다.
음성
↓
Whisper
↓
STT 원문
Glossary Tool
tool_lookup_glossary()
저장된 용어집을 읽어와
LLM에게
"이 단어들은 회사에서 자주 사용하는 용어입니다."
라는 힌트를 제공한다.
Agent 내부에서는
Thought
↓
Action
↓
Observation
순서로 Tool을 호출한다.
Thought
이번 회의이므로 용어집을 먼저 조회한다.
↓
Action
tool_lookup_glossary()
↓
Observation
용어집 24건 로드 완료
이처럼 단순히 함수를 호출하는 것이 아니라
Agent가 필요한 Tool을 선택하여 사용하는 구조를 만들었다.
4. STT 정제
Tool에서 가져온 정보를 이용하여
LLM이 실제 정제를 수행한다.
입력은
어... 그러니까
오늘은 메세지 락 때문에...
출력은
오늘은 메시지 Lock 때문에...
정도로만 수정된다.
여기서 중요한 점은
새로운 내용을 만드는 것이 아니라
오직
- 오인식 수정
- 반복 제거
- 추임새 제거
만 수행한다는 것이다.
5. Verify - 결과를 AI가 다시 검사하기
이번 프로젝트에서 가장 넣어 보고 싶었던 기능이 Verify이다.
보통은
LLM
↓
결과 출력
으로 끝난다.
하지만
이번 Agent는
한 번 더 자기 결과를 평가한다.
STT 원문
↓
정제 결과
↓
LLM 평가
평가 기준은 다음과 같다.
- 원문의 의미를 유지했는가?
- 새로운 내용을 추가하지 않았는가?
- 요약하지 않았는가?
- 오인식만 수정했는가?
- 반복과 추임새만 제거했는가?
그리고
0~10점으로 점수를 매긴다.
{
"score": 8,
"reason": "원문의 의미는 유지되었지만 일부 표현이 과도하게 수정되었습니다."
}
6. Retry - 기준 미달이면 다시 수행
만약 Verify 결과가
기준 점수보다 낮다면
Agent는 그대로 끝나지 않는다.
정제
↓
Verify
↓
6점
↓
피드백 생성
↓
다시 정제
피드백도 함께 전달한다.
이전 결과의 문제점
- 원문의 의미가 일부 변경되었습니다.
원문의 의미를 유지한 채 다시 정제하세요.
이 과정을 최대 3회까지 반복한다.
즉
Agent는
스스로 결과를 확인하고
다시 개선하는 구조를 갖는다.
전체 처리 흐름
전체 로직은 아래 순서로 동작한다.
사용자 입력
↓
Memory 로드
↓
Glossary Tool 호출
↓
Whisper(STT)
↓
STT 원문 생성
↓
Goal(System Prompt)
↓
LLM 정제
↓
Verify
↓
점수 확인
↓
통과
↓
최종 STT 출력
각 단계는 하나의 역할만 수행하도록 분리했다.
덕분에
Goal, Memory, Tool, Verify를 각각 독립적으로 수정하거나 확장하기도 쉬운 구조가 되었다.
테스트하면서 가장 크게 느낀 점
처음 구현했을 때는 Goal을 잘못 정의하여 회의록 작성 Agent처럼 동작했다.
예를 들어
## 논의 안건
## 결정 사항
## 담당자
형태로 결과를 생성했고,
일부 내용은 요약하거나 새롭게 작성하기도 했다.
하지만 내가 원했던 것은
회의록 생성이 아니라
STT 정제였다.
그래서 Goal과 Verify 기준을 다시 설계했다.
특히
"요약 금지"
"새로운 내용 추가 금지"
"원문의 의미 변경 금지"
를 명확하게 정의한 이후에는
Agent가 의도한 방향으로 훨씬 안정적으로 동작했다.
이번 프로젝트를 통해 가장 크게 느낀 점은 같은 LLM을 사용하더라도 Goal을 어떻게 정의하느냐에 따라 완전히 다른 AI Agent가 만들어진다는 것이었다.
또한 단순히 LLM을 한 번 호출하는 것만으로는 AI Agent라고 보기 어렵고, 명확한 목표(Goal), 기억(Memory), 도구(Tool), 검증(Verify), 그리고 재시도(Retry)가 유기적으로 연결될 때 비로소 하나의 Agent다운 동작을 하게 된다는 점을 직접 확인할 수 있었다.
'스터디 > AI-Agent' 카테고리의 다른 글
| [AI-Agent] LLM 에이전트 설계, 더 똑똑하게 일하게 만드는 6가지 핵심 설계 요소 (0) | 2026.07.05 |
|---|---|
| [AI-Agent] 재설계/요구사항 분석 - AI 기반 회의 지식 관리 시스템 (0) | 2026.06.28 |
| [작성중/AI-Agent] Claude.md 분석 (0) | 2026.06.15 |
| [작성중/AI-Agent] 끔직한 결말, 그리고 방향성을 잡기위한 재설계의 필요성 (0) | 2026.06.14 |
| [작성중/AI-Agent] Claude가 작성한 프로젝트, 어떤 순서로 만들어졌을까? (0) | 2026.06.14 |