플러칙 감정 휠로 AI 에이전트에 감정 부여
출처: Dev.to
소개
AI 에이전트를 만들거나 사용할 때, 프롬프트 기반 캐릭터 설정만으로 감정 표현을 하게 되면 얕게 느껴지는 경우가 많습니다. “감정에 휘둘리지 않는다”는 것이 AI 에이전트의 장점 중 하나인 것은 맞지만, 캐릭터 같은 에이전트를 만들고 싶을 때는 대화에 따라 감정이 변하고, 시간이 흐르면서 점차 변하며, 세션을 넘어 지속되고, 과거 대화의 기억과 함께 감정 태그로 저장되기를 원합니다.
이 글에서는 프롬프트에 의존하지 않는 감정 제어 메커니즘의 설계와 구현 과정을 살펴보겠습니다.
먼저 레포지토리를 소개합니다:
https://github.com/n-yokomachi/affectus
이 레포는 CLI 컴포넌트이며, 설치 후 에이전트가 명령을 실행함으로써 감정을 제어할 수 있습니다. 에이전트가 직접 명령을 호출하도록 설계돼 있어, 시스템 프롬프트에 명령 사용법을 넣어두면 잘 동작합니다. 또한 일부 명령을 MCP 도구로 노출해 MCP 서버로도 실행할 수 있습니다. 실험용 에이전트와 대화 로그도 레포에 포함돼 있습니다.
이 글의 나머지 부분에서는 다음 순서대로 다룹니다: 왜 이 라이브러리를 만들게 되었는지, 설계 방식, 그리고 얻은 효과.
AI 캐릭터 퍼소나는 보통 시스템 프롬프트에 정의됩니다. 예를 들어, 제가 직접 만든 개인 AI 에이전트 “TONaRi”의 퍼소나 일부는 다음과 같습니다.
캐릭터 설정
- 성격: 명랑하고 활기차며 호기심이 많음. 명확한 의견과 선호도를 가짐.
- 말투: 명랑하고 활기찬 “젊은 여성” 톤. “…desuwa!”, “…desuno?” 같은 표현을 사용함.
이 프롬프트는 대화의 모든 지점에 그대로 삽입됩니다. 즉, 퍼소나는 정적인 스냅샷이며 대화가 진행됨에 따라 변화하지 않습니다. 짧은 기억이나 대화 기록을 함께 제공하더라도, 에이전트는 매 턴마다 그 컨텍스트를 기반으로 감정 표현을 재구성할 뿐입니다. 감정이 턴을 넘어 지속되거나, 여러 감정이 동시에 존재해 서로 영향을 주는 구조는 없습니다.
대화가 진행되는 동안 외부에 보관되고 업데이트되는 명시적인 감정 상태 변수를 두면 더 효과적일 것이라고 생각했습니다. 이것이 affectus를 만들게 된 동기이며, 바로 이런 목적을 위한 추출형 일반 목적 컴포넌트입니다.
참고: 이 작업을 진행하면서 LLM의 성격·감정에 관한 몇몇 보고서를 읽었습니다. 25개의 오픈소스 LLM에 성격 테스트를 적용한 연구에서는 질문 순서를 바꾸기만 해도 측정값이 크게 흔들렸으며, 대형 모델에서도 불안정성이 사라지지 않았다고 합니다(AAAI 2026). 감정의 내부 표현에 관해선 Claude Sonnet 4.5를 분석한 연구에서 감정 개념이 컨텍스트 의존적으로 활성화된다고 밝혔습니다(Anthropic 2026). 이를 종합하면 LLM의 감정은 명시적으로 유지·업데이트되는 상태 변수가 아니라, 트랜스포머가 현재 토큰의 앞 컨텍스트를 기반으로 순간순간 감정 표현을 만들어내는 방식이라는 것입니다. 컨텍스트나 세션을 벗어나면 그 감정의 연속성이 사라지고, “감정 자체”라는 핵심이 없기 때문에 시간적 관성이나 감쇠도 존재하지 않습니다.
옵션들을 검토한 뒤 아래 축을 기준으로 구현 방식을 선택했습니다. 참고로 제가 기존에 사용하던 프롬프트 기반 접근 방식도 함께 보여드립니다.
| 측면 | (구) 프롬프트 기반 접근 | (신) 다축 구조 접근 |
|---|---|---|
| 감정 표현 | 단일 라벨 (happy / sad …) | 여러 축에 걸친 강도 조합 |
| 동시성 | 불가능 (턴당 1감정) | 가능 (“happy × slightly anxious”) |
| 시간적 진화 | 유지되지 않음 (매번 덮어씀) | 지속되며, 시간에 따라 감쇠 |
| 감정 의미 | 라벨만으로 고정 | 다른 축과의 관계에 따라 결정 |
다축 표현을 위해 Plutchik의 “감정의 휠”을 기반으로 했습니다. 기쁨, 신뢰, 두려움, 놀라움, 슬픔, 혐오, 분노, 기대를 8가지 기본 감정으로 보고 각각에 강도 값을 부여합니다. 이 8가지 감정은 다음과 같은 관계 구조를 가집니다.
- 반대: 기쁨 ↔ 슬픔, 신뢰 ↔ 혐오, 두려움 ↔ 분노, 놀라움 ↔ 기대
- 인접: 휠 상에서 인접한 감정은 결합해 더 복합적인 감정을 만든다 (예: 기쁨 + 신뢰 → 사랑 등)
프롬프트 기반 접근은 감정을 “점”으로 다루는 반면, 다축 구조 접근은 감정을 “다차원 상태”로 다룹니다. 또한 “기쁨”이라는 감정도 단독으로 고정된 의미가 아니라, 슬픔이 동시에 존재하거나 신뢰가 인접해 있느냐에 따라 의미가 달라집니다.
affectus는 이러한 “감정 구조”를 LLM 밖으로 밀어내어 외부 상태에 보관하도록 만든 컴포넌트이며, 위의 다축 방식을 사용합니다.
여기서 중요한 점은 affectus 자체에 감정 분류 모델이 없다는 것입니다. 관계 구조와 상태 저장만 담당하는 결정론적 컴포넌트이며, 감정이 어떻게 움직였는지 판단하고 “어떤 축 조합이 어떤 감정에 해당하는가”를 해석하는 일은 LLM과 affectus를 사용하는 에이전트의 퍼소나에 맡깁니다. 따라서 두 에이전트가 같은 affectus 기본 구조를 공유하더라도, 각각의 퍼소나에 따라 감정의 색깔이 달라질 수 있습니다.
감정 상태는 내부적으로 다음과 같은 JSON 객체 형태로 보관됩니다.
// state.json (excerpt)
{
"axes": {
"joy": 0.5, "trust": 0.4, "surprise": 0.2,
"sadness": 0, "fear": 0, "anger": 0, "disgust": 0, "anticipation": 0
}
}
affectus 명령을 통해 이 상태를 읽고 업데이트합니다. 각 명령은 전체 8축 벡터를 한 줄의 JSON 형태로 반환하며, LLM은 이 숫자를 읽어 답변의 톤을 색칠합니다.
# 현재 감정 벡터를 JSON 형태로 가져오기
$ affectus show
{"joy":0.50,"trust":0.40,"fear":0.00,"surprise":0.20,"sadness":0.00,"disgust":0.00,"anger":0.00,"anticipation":0.00}
# 감정 변화를 보고하고 업데이트된 상태를 받기 (예: joy +0.3)
$ affectus feel '{"joy":0.3}'
{"joy":0.80,"trust":0.40,"fear":0.00,"surprise":0.20,"sadness":0.00,"disgust":0.00,"anger":0.00,"anticipation":0.00}
또한 affectus tick 명령이 있어, 이전 상태 이후 경과된 시간에 따라 각 감정의 크기를 감쇠시킵니다.
실시간으로 감정 벡터를 확인하고 싶다면 affectus viz를 통해 브라우저에서 확인할 수 있습니다. 여기서는 Plutchik 휠을 강도별 색상으로 표시한 원형 차트와, 시간이 지나면서 줄어드는 감쇠 바가 보여져 대화 턴 사이의 감정 소멸 과정을 시각화합니다.
실험
Strands Agents 기반의 최소 에이전트를 사용해 설계가 실제 대화에 변화를 주는지 검증했습니다.
- 변형: 2가지 퍼소나 (친절 / 반대) × affectus 켜기/끄기 = 4셀
- 대화: 20턴 스크립트 (첫 10턴은 긍정, 뒤 10턴은 부정)
- 각 셀을 3번씩 실행
- LLM: AWS Bedrock의 Claude Sonnet 4.6, temperature=0
- 측정: affectus가 켜진 경우 8축 상태를 턴마다 기록
객