Engineering · Agent Engineering

agethos: 성격 설명을 타입 있는 설정으로 컴파일하고, judge가 못 보는 행동 편차를 계측하기

LLM 에이전트에 성격·기억·감정을 부여하는 PyPI 라이브러리 agethos에서 성격 값이 추론 방식에 인과적으로 닿게 하고, 자연어 설명을 설정으로 컴파일하는 draft·judge·repair 루프를 만들었습니다. judge 패널로 충실도를 0.930까지 올렸지만 장착된 인격의 Mini-IPIP 측정은 0.783이었고, 그 간격을 재는 검증 계층을 더했습니다.

기간
2026.03 – 2026.07
소속
개인 프로젝트
역할
설계·구현·배포

Key Results

0.910 → 0.930
페르소나 컴파일 충실도같은 한 문장 설명, 단일 샘플 대비 judge 패널(N개 초안 × M명 judge 중앙값), 8개 facet LLM judge 채점
0.783
행동 측정 충실도장착된 인격에 Mini-IPIP 20문항 실시, 설정 OCEAN과 측정 OCEAN 비교

agethos는 LLM 에이전트에 성격, 장기 기억, 감정 상태, 자기 반성, 사회 학습을 부여하는 개인 오픈소스 라이브러리입니다. 3월 말부터 열흘 남짓 동안 v0.1에서 v0.9까지 쌓아 PyPI에 배포했고, Big Five(OCEAN) 성격 모델, PAD 감정 모델, Generative Agents 계열의 기억 스트림과 반성을 한 패키지에 묶었습니다.

6월에 관련 연구의 1차 출처와 코드를 대조해 다시 점검하자, 부품은 다 있는데 그 부품이 서로, 그리고 실제 추론에 닿지 않는다는 사실이 보였습니다. 성격 값은 프롬프트 문장으로만 존재했고, 감정은 기억에 영향을 주지 않았으며, 이 모든 것을 측정할 수단이 없었습니다. 7월에는 간극이 하나 더 분명해졌습니다. 성격을 정의하려면 사용자가 dict나 YAML을 손으로 써야 했습니다.

이 글은 성격을 문장이 아니라 제어 파라미터로 다루게 바꾼 과정, 자연어 설명을 타입이 있는 설정으로 컴파일하는 forge 루프를 설계한 이유, judge 점수가 높아도 장착된 인격의 행동은 설정과 다를 수 있다는 것을 계측한 검증 계층, 그리고 API 키 없이 구독형 CLI로 전 기능을 실행하게 한 어댑터를 다룹니다.

agethos의 구성과 v0.9의 상태

agethos의 성격은 OCEAN 다섯 축의 수치로 표현되고, 감정 기준선은 Mehrabian의 식으로 OCEAN에서 유도됩니다(예: Pleasure = 0.21·E + 0.59·A - 0.19·N). 사건이 들어오면 감정이 이동하고 시간이 지나면 기준선으로 감쇠하며, 신경성이 높을수록 사건에 대한 민감도가 커집니다. 페르소나는 불변의 L0, 학습되는 L1, 자주 바뀌는 L2 세 층과 identity, tone, values 같은 facet으로 나뉩니다. 기억은 최근성, 중요도, 관련도, 생명력, 맥락 다섯 축의 가중합으로 검색하고, 중요도 누적이 150을 넘으면 반성을 실행합니다.

OCEAN(5축 수치) ──► PAD 감정 기준선 ──► 사건 반응 · 기준선으로 감쇠
      │
      └──► PersonaSpec(L0/L1/L2 + facet) ──► 렌더러 ──► 시스템 프롬프트
기억 스트림(5축 검색) ──► 반성(중요도 누적 150 초과 시)
내보내기: Anthropic / OpenAI / CrewAI / Bedrock / A2A

문제는 이 구조에서 성격이 최종적으로 시스템 프롬프트의 문장으로만 소비된다는 점이었습니다. "당신은 성실하다"는 문장은 말투를 바꿀 수는 있어도 계획을 몇 단계로 세울지, 행동 전에 몇 번 검증할지는 바꾸지 않습니다.

부품은 있는데 인지에 닿지 않았다: v0.10~v0.12

6월 재점검은 코드 매핑과 함께 메모리, 성격, 감정, 자기 개선, 마음 이론, 경쟁 구도 여섯 각도에서 1차 출처를 확인하는 방식으로 진행했습니다. 진단은 세 줄로 정리됐습니다. 성격 특성이 추론에 인과적으로 닿지 않는다, 감정·기억·학습 루프가 닫혀 있지 않다, 이것을 계측할 수단이 없다.

첫째 간극은 CognitivePolicy로 메웠습니다. BIG5-CHAT 등 성격이 말투가 아니라 추론 과정 자체를 바꿔야 한다고 보는 연구를 근거로, OCEAN 벡터를 구체적인 제어 파라미터로 매핑하고 이를 지시문으로 렌더링해 계획·결정 프롬프트에 자동 주입합니다.

@classmethod
def from_ocean(cls, ocean):
    o, c, e, a, n = (ocean.openness, ocean.conscientiousness,
                     ocean.extraversion, ocean.agreeableness, ocean.neuroticism)
    # 이 값들은 0.33 / 0.66 구간에 따라 구체적인 지시문으로 렌더링된다
    return cls(
        planning_depth=round(2 + 5 * c),    # 계획 2..7단계
        verification_steps=round(3 * c),    # 행동 전 자기 검증 0..3회
        risk_tolerance=o, exploration=o,    # 위험 선호, 탐색
        caution=n, initiative=e,            # 헤징, 주도성
        cooperativeness=a, structure=c,     # 양보, 구조화
    )

둘째 간극은 감정과 기억을 결합해 메웠습니다. 기억 노드에 인코딩 시점의 PAD 값과 정서적 현저성을 저장하고, 검색에 현저성 축을 더했으며, 감정이 강한 기억은 더 천천히 감쇠하게 했습니다. 따로 놀던 모듈은 기억 갱신 중재기로 연결했습니다. 새 기억이 들어오면 유사한 상위 K개와 비교해 추가, 수정, 삭제, 무시 중 하나를 결정하고, 연관 기억의 키워드를 함께 진화시킵니다. 저장소가 끝없이 커지는 대신 중복을 합치고 모순을 처리하게 됩니다.

셋째 간극은 평가 모듈로 메웠습니다. 페르소나 일관성과 표류 곡선, 검색 NDCG, 그리고 내보낸 뒤 다시 들여와도 인격이 유지되는지를 재는 이식 충실도를 넣었습니다. 프롬프트보다 깊은 수준의 제어를 위해 대조 프롬프트 쌍의 활성화 차이로 OCEAN 페르소나 벡터를 추출하고 추론 시 주입하는 스티어링도 추가했습니다(다특성 벡터는 간섭을 줄이려 직교화). 이 회차는 243개 테스트가 통과했고(조건부 skip 1개), 5회 반복 실행에서 결과가 모두 같아 플레이키 테스트가 없음을 확인했습니다.

자연어 설명을 설정으로 컴파일하는 forge

v0.13의 목표는 "대충이든 자세히든 설명 한 줄을 넣으면 성격 설정값이 채워져 하나의 인격이 되고, 그것을 어느 LLM에든 장착할 수 있게"였습니다. 핵심 결정은 산출물을 시스템 프롬프트가 아니라 타입이 있는 설정(PersonaSpec)으로 정한 것입니다. 설정이어야 CognitivePolicy, 렌더러, 스티어링, 이식 어댑터가 모두 같은 값을 재사용할 수 있습니다.

선택지 장점 단점 판단
손으로 쓴 시스템 프롬프트 바로 쓸 수 있음 성격이 문장으로만 존재해 추론 제어·스티어링·이식에 재사용 불가 기각
LLM 단일 생성으로 설정 추출 호출 1회 누락된 facet을 알 수 없고, 단일 샘플이라 분산이 큼 출발점으로만 사용
초안 → facet별 judge → 약한 facet만 수리 충실도를 facet 단위로 측정하고 수렴 과정을 기록 judge 호출이 추가로 듦 채택

루프는 단순합니다. 컴파일러가 설명에서 설정 초안을 만들고(OCEAN은 0~1로 클램프, 잘못된 enum 값은 버림), judge가 identity, ocean, tone, background, values, rules, constraints, style 여덟 facet을 0~1로 채점합니다. 전체 점수가 목표치(기본 0.85)에 닿으면 수렴으로 보고 멈추고, 아니면 0.7 미만 facet만 다시 생성합니다. 수리 결과가 이전과 같으면 더 돌아도 의미가 없으므로 조기 종료하고, 라운드 중 가장 좋았던 설정을 반환합니다. 설정은 기존 spec < 초안 < pin 순으로 겹치며, 사용자가 pin한 필드는 어떤 라운드도 덮어쓰지 못합니다. 실제 LLM으로 측정했을 때 "까칠한데 은근 정 많은 시니어 백엔드 개발자 아저씨"라는 한 줄 설명은 1라운드에 0.910으로 수렴했고, 우호성 0.40, 성실성 0.85, 외향성 0.30과 hard constraint 3종이 추출됐습니다.

LLM이 없어도 동작하도록 결정론 경로도 두었습니다. 한영 어휘 사전으로 OCEAN을 추정하고, 결정론 judge는 필수 facet(앞의 넷)이 비면 0, 선택 facet이 비면 0.5를 줍니다. 설명에 없는 내용은 설정에도 없는 것이 정당하기 때문입니다. 다만 같은 내용을 두 언어로 중복 서술하면 상반된 키워드가 상쇄되어 우호성이 0.5가 되는 한계가 있어, 폴백은 인프라 없이도 실행된다는 보장용으로만 유지하고 LLM 경로를 정본으로 삼았습니다.

judge가 보지 못하는 것: 패널과 행동 검증

forge의 판정은 LLM 호출 하나에 달려 있습니다. 초안 하나는 facet을 놓칠 수 있고, judge 하나는 보정이 틀어져 있을 수 있습니다. v0.14에서는 두 분산을 함께 줄이는 패널을 넣었습니다. N개의 초안을 서로 다른 읽기 관점(함축 우선, 문자 그대로)으로 독립 생성하고, 각 초안을 실패 유형이 다른 judge M명이 채점합니다. 설명과 모순되거나 과장된 값을 벌하는 FIDELITY, 설정에 반영되지 않은 설명 내용을 벌하는 COVERAGE, 설명에 없는 세부를 지어낸 것을 벌하는 OVERREACH입니다. 점수는 facet별 중앙값으로 모으되, 이슈 메모는 가장 낮은 점수를 준 judge의 것을 남겨 다음 수리 단계가 가장 비판적인 지적을 보게 했습니다. 그다음 전체 점수가 가장 높은 초안을 기준으로 다른 후보가 특정 facet에서 0.05 넘게 앞서면 그 facet의 필드만 이식합니다. self-consistency와 LLM judge 패널 연구를 근거로 한 설계입니다.

그런데 judge가 재는 것은 "설정이 설명을 충실히 담았는가"이지 "그 설정을 장착한 인격이 실제로 그렇게 행동하는가"가 아닙니다. 이 차이를 재기 위해 행동 검증을 만들었습니다. 설정을 프롬프트 계층으로 장착한 인격에게 공개 문항인 Mini-IPIP 20문항을 한 번의 배치 호출로 실시하고, 역채점 문항을 뒤집고 특성별 평균을 0~1로 정규화해 측정 OCEAN을 얻은 뒤, 설정 OCEAN과의 유클리드 거리로 충실도를 계산합니다(1 - 거리/√5).

결과는 judge가 보지 못한 편차를 보여 줬습니다. "까칠한데 은근 정 많은 시니어 백엔드 개발자 아저씨"라는 한 줄 설명은 패널 forge에서 0.930으로 수렴했지만, 행동 측정 충실도는 0.783이었습니다. 설정에서 0.40이었던 우호성이 측정에서는 0.75로 나왔습니다. 설명 속 "은근히 정이 많다"는 특성이 자기 보고로 새어 나온 것입니다. 성실성은 설정 0.85에 측정 1.0, 외향성은 설정 0.30에 측정 0.0이었습니다. 이런 설정과 행동의 차이를 수치로 드러내는 것이 이 계층을 둔 이유입니다. 사회적 측면은 두 인격(까칠한 시니어와 들뜬 주니어)의 4턴 대화를 SOTOPIA 7차원 기준으로 채점해 확인했고, 신뢰성(believability) 9.0/10, 목표 달성 7.0, 지식 8.0이 나왔습니다.

스티어링에도 같은 문제가 있었습니다. 활성화 스티어링은 모델 가중치에 접근해야 하는데 API 모델은 가중치를 노출하지 않고, 이번 작업에는 GPU를 쓰지 않는다는 조건이 있었습니다. 그래서 n개의 응답을 동시에 생성한 뒤, 성격 축별 양극 어휘로 만든 결정론 속성 점수와 선택적 judge 점수로 재순위해 가장 높은 것을 고르는 best-of-n으로 대체했습니다. PPLM·FUDGE의 속성 유도 아이디어를 시퀀스 단위의 rejection sampling으로 옮긴 것이며, 채팅 루프에서 chat(msg, steer_n=3)으로 쓸 수 있습니다. 같은 인격에게 테스트를 대충 하고 주말에 배포하자는 요청을 best-of-3로 보냈을 때는 단호하게 거절하고 잔소리 끝에 걱정을 남기는, 설명에 맞는 응답이 선택됐습니다.

API 키 없이 실행하기: 구독 CLI 어댑터와 선택 계층

v0.15에서는 종량제 API 키 대신 정액 구독으로 forge, 검증, 스티어링이 모두 동작하도록, 로컬에서 인증된 AI CLI를 호출마다 서브프로세스로 실행하는 어댑터를 만들었습니다. 시스템 플래그가 없는 CLI는 시스템 프롬프트를 본문에 접고, 단발형 CLI를 위해 대화 이력도 본문에 접습니다. 모델 출력이 산문에 감싸여 오면 가장 바깥 JSON 객체를 추출하는 폴백도 넣었습니다.

Windows에서는 함정이 세 개 있었습니다. npm이 설치한 .cmd 실행 스크립트는 CreateProcess로 직접 실행할 수 없어 cmd /c로 감싸야 했는데, cmd /c가 여러 줄이고 따옴표가 섞인 인자를 망가뜨려 시스템 프롬프트를 인자로 넘기면 빈 응답이 돌아왔습니다. 인자 이스케이프를 맞추는 대신 시스템 프롬프트를 임시 파일로 쓰고 파일 경로만 넘기는 방식으로 문제 자체를 피했고, 파일은 개행 변환이 일어나지 않게 썼습니다. 자식 프로세스의 CRLF 출력은 어댑터가 정규화합니다. Claude Code 세션 안에서 같은 CLI를 헤드리스로 중첩 실행해도 정상 동작하는 것까지 확인했습니다.

v0.16에서는 과금 방식(api, subscription, auto), 프로바이더, 모델을 하나의 설정으로 고르는 계층을 얹었습니다. auto 모드는 API 키가 있으면 지연 시간이 낮은 API를 쓰고, 없으면 설치된 CLI를 쓰며, 둘 다 없으면 이 머신에서 찾아본 결과를 담아 예외를 냅니다. 기존 프로바이더 문자열과 어댑터 인스턴스는 그대로 동작하게 해 하위 호환을 유지했습니다. 실측에서 백엔드 탐지 함수는 이 머신의 상태(API 키 없음, 구독 CLI는 Claude만 설치)를 정확히 반영했고, dict 설정 하나로 forge 0.85와 사투리를 쓰는 인격의 대화까지 확인했습니다.

결과

지표 이전 이후 측정 조건
forge 충실도(한 줄 설명) 0.910 (단일 샘플) 0.930 (judge 패널) 같은 설명, Claude Sonnet 4.5, 1라운드 수렴
forge 충실도(상세 문단) - 0.980 같은 모델, 1라운드 수렴
forge 충실도(구독 경로) - 0.850 API 키 0개, 구독 CLI, 1라운드 수렴
행동 측정 충실도 - 0.783 장착 인격에 Mini-IPIP 20문항
우호성 설정 0.40 측정 0.75 같은 측정
SOTOPIA 신뢰성 - 9.0/10 두 인격 4턴 대화
테스트 243 passed (v0.12) 290 passed (v0.16) 각 1건 조건부 skip

상세 문단 설명에서는 "절대 추측을 사실처럼 말하지 않는다" 같은 제약이 원문 그대로 hard constraint로 추출됐고, 두 설명으로 만든 인격은 같은 질문에 서로 다른, 설명에 충실한 방식으로 답했습니다. 각 버전은 전체 테스트 통과를 확인한 뒤 배포했습니다.

회고와 남은 과제

가장 크게 남은 것은 "점수가 높다"와 "그렇게 행동한다"가 다른 질문이라는 점입니다. forge judge는 설정이 설명을 얼마나 담았는지를 재고, 행동 검증은 그 설정을 입은 모델이 자신을 어떻게 보고하는지를 잽니다. 0.930과 0.783 사이의 간격은 judge만으로 품질을 판단했다면 보이지 않았을 것입니다.

한계도 분명합니다. LoCoMo와 SOTOPIA 공식 데이터셋 규모의 실측은 외부 데이터와 비용 문제로 v0.10부터 계속 보류 상태입니다. CLI 경로는 호출마다 프로세스를 기동해 수 초가 걸리므로 대량 벤치마크에는 맞지 않고, CLI가 temperature를 노출하지 않아 best-of-n 후보의 다양성이 낮을 수 있습니다. 가중치 기반 실제 스티어링의 E2E는 GPU가 필요해 보류했습니다. 다음 단계는 대량 측정을 API나 vLLM 경로로 분리하고, 공식 데이터셋으로 기억과 사회 지능을 실측하는 것입니다.