DearMate는 사용자가 AI 캐릭터와 계속 대화하며 관계를 만들어 가는 서비스입니다. 같은 캐릭터와 다시 이야기할 때 앞선 대화가 이어져야 하고, 오래된 대화를 참고하더라도 지금의 발화와 섞여 기록되지는 않아야 합니다. 답변이 생성된 뒤에는 그 결과를 다음 대화에서 다시 사용할 수 있는 상태로 남겨야 합니다. 이런 요구를 다루려면 한 번의 모델 호출을 둘러싼 준비와 저장 과정까지 설계해야 했습니다.
기존 Quality Analyst 작업에서는 같은 페르소나와 대화 맥락에서 어떤 응답이 선택되는지 비교했습니다. 이 문서에서는 그 모델을 실제 대화 서비스 안에서 실행하기 위해 구성한 흐름을 다룹니다. 제가 확장한 핵심은 대화 원본을 보존하면서 매 실행에 필요한 맥락을 준비하고, 성공한 결과를 일관되게 저장하는 구조입니다.
1. 기본 대화 루프에서 출발했습니다
팀의 초기 구현은 사용자 메시지에 시스템 프롬프트를 넣고, LiteLLM을 거쳐 PydanticAI로 응답을 받는 경로였습니다. 기본 루프를 구현한 PR #8에는 텍스트 응답과 실제 모델 호출 검증이 기록되어 있습니다. 동시에 당시의 사용자 정의 모델 어댑터가 tool/output tool 요청을 지원하지 않아 구조화 출력을 막는 문제도 남아 있었습니다.
저는 이후 PR #30에서 모델 연결과 페르소나, 대화 저장, 맥락 조립의 경계를 정리했습니다. 사용자 정의 LiteLLM client/model wrapper는 PydanticAI의 OpenAIChatModel과 LiteLLMProvider를 사용하는 경로로 단순화했습니다. 애플리케이션에서는 Persona를 자체 도메인 값으로 다루고, 프레임워크가 사용하는 instructions와 메시지 타입은 모델 호출 경계에서 변환하도록 구성했습니다.
이 변화로 포트폴리오에서 보여줄 부분은 모델 호출 전후에 애플리케이션이 책임져야 할 일을 찾아내고, 그 책임을 코드 구조로 구체화한 과정입니다.
2. 대화 원본과 현재 실행의 맥락을 분리했습니다
사용자가 나눈 대화 전체와 모델이 이번 응답에 읽을 내용은 목적이 다릅니다. 원본 대화는 이후 조회와 검색의 근거로 남겨야 하지만, 실행 맥락은 길이에 따라 요약하거나 일부를 선택할 수 있어야 합니다.
PR #30에서는 발화 원본을 보존하는 canonical conversation history와 교체 가능한 active Agent context session을 분리했습니다. 외부에서 이어지는 대화의 식별자와 내부에서 요약·교체되는 맥락 세션의 식별자도 구분했습니다. 이 구조가 이후 압축과 검색을 연결하는 기반이 됐습니다.
그 결과 압축의 대상은 원본 대화 전체가 아니라 다음 모델 실행에 사용하는 맥락이 됩니다. 오래된 내용을 어떻게 요약하고 최근 발화를 얼마나 남길지는 별도의 정책으로 다룰 수 있습니다. 구체적인 토큰 예산과 검색 전략은 기억·맥락 관리 문서에서 이어서 설명합니다.
3. 한 번의 실행에서 읽고 남기는 순서를 명시했습니다
PR #33에서는 실행 전후 처리를 inbound와 outbound로 나눴고, 이후 PR #53에 포함된 변경에서는 실행 순서를 AgentContext로 모았습니다. 현재 팀 저장소 stage의 흐름은 다음과 같습니다.
- 소유권 확인을 거친 대화의 저장 상태를 조회합니다.
- 현재 맥락 세션을 읽고, 필요한 경우 압축을 마칩니다.
- 해당 캐릭터의 지침과 이번 발화에 필요한 검색 결과를 가져옵니다.
- 지침·사용자 입력·대화 이력을 모델이 받을 입력으로 조립합니다.
- ChatAgent가 준비된 값으로 모델을 호출합니다.
- 응답에서 영구 보존할 메시지를 골라 저장 형식으로 변환합니다.
- 발화와 세션 체크포인트, 후처리 작업을 함께 저장합니다.
ChatAgent.run()은 준비된 입력으로 모델을 실행하고, AgentContext는 서비스 호출 순서를 조정합니다. Inbound와 outbound processor는 메시지의 조립·변환·보존 정책을 담당합니다. 실제 조회와 트랜잭션은 저장 계층이 수행합니다.
이렇게 책임을 나누면 검색 결과를 조립하는 정책이나 저장할 메시지를 선택하는 정책을 변경할 때, 모델 호출 코드와 데이터베이스 처리까지 한꺼번에 수정할 필요가 줄어듭니다. 테스트에서도 모델 입력, 저장할 결과, 실행 순서를 각각 확인할 수 있습니다.
4. 모델이 참고한 자료와 실제 대화 기록을 구분했습니다
이전 발화를 검색해 모델 입력에 붙이는 경우, 그 검색 결과를 새 대화 기록에도 그대로 저장하면 동일한 내용이 반복해서 누적될 수 있습니다. 그래서 검색으로 주입한 내용에는 임시 정보라는 표시를 붙이고, 기본 보존 정책에서는 outbound 처리에서 제거합니다.
모델은 이번 응답을 위해 검색 결과를 읽지만, 대화 원본에는 실제 사용자 발화와 모델이 생성한 새 메시지를 남깁니다. 현재 구현에는 검색 내용의 보존 여부를 바꿔 비교할 수 있는 실험 설정도 있습니다. 따라서 이 문서에서 설명하는 동작은 기본 정책이며, 모든 실험에서 동일하게 제거한다고 단정하지 않습니다.
이 결정은 맥락을 많이 제공하는 것과 대화 기록을 정확히 보존하는 것을 각각 다룬 사례입니다. 장기 대화에서는 어떤 정보를 읽게 할지와 어떤 정보를 새 기록으로 남길지를 함께 설계해야 했습니다.
5. 응답 생성 이후의 성공 조건도 정했습니다
현재 실행 흐름은 준비 단계나 모델 호출이 실패하면 해당 interaction의 발화 저장을 진행하지 않습니다. 성공한 결과는 outbound 처리를 거쳐 발화, 세션 체크포인트, 후처리 작업을 하나의 트랜잭션으로 저장합니다. 여기서 “실패하면 저장하지 않는다”는 것은 해당 대화 턴의 commit을 뜻합니다. 모델 호출 전에 실행되는 맥락 압축처럼 별도 단계에서 수행하는 저장까지 없다는 의미는 아닙니다.
동시에 실행된 요청이 이미 바뀐 세션을 기준으로 결과를 저장하지 않도록 세션 버전을 검사합니다. 초기 PR #33에는 프로세스 내부의 대화 잠금도 있었지만, 후속 변경에서 이 잠금은 제거됐습니다. 현재 구조에서는 프로세스 경계와 무관하게 데이터베이스의 세션 버전 검사가 오래된 실행 결과의 commit을 거부합니다.
발화가 저장된 뒤 필요한 임베딩 처리는 별도 worker가 수행합니다. 이 문서에서는 실행 경계까지만 다루며, 대화 재전송과 이용 재화의 복구는 「재시도해도 대화와 Sand 정산이 어긋나지 않도록」에서 이어서 설명합니다. Social Automation의 생성·승인·게시와 보안 프롬프트 평가도 각각 독립된 설계 사례로 다룹니다.
6. 이 작업에서 보여줄 기여와 검증
초기 기본 Agent Loop는 팀원이 작성한 PR #8에 해당합니다. 제 기여는 그 기반 위에서 수행한 런타임과 대화 영속성 통합(PR #30), inbound/outbound 분리(PR #33), API·맥락·저장·worker 경계 재구성과 압축·검색 연결(PR #53)로 설명할 수 있습니다. 실행 전후 흐름의 context 통합과 프로세스 내부 잠금 제거는 공개 팀 저장소의 개별 커밋에서도 제 작성 기록을 확인할 수 있습니다.
PR에 남은 당시 검증 기록은 #30이 84 passed·2 skipped, #33이 113 passed·2 skipped, #53이 182 passed·3 skipped입니다. 서로 다른 시점의 테스트 집합이므로 합산하거나 성능 향상의 수치로 사용하지 않습니다. 이번 원고 조사에서는 서비스를 실행하거나 테스트를 다시 수행하지 않았습니다.
현재 코드에서 확인되는 결과는 대화 원본과 실행 맥락이 분리되어 있고, 준비·모델 실행·보존·commit의 순서가 명시되어 있으며, 실패와 오래된 세션을 다루는 저장 조건이 있다는 점입니다. 실제 장기 기억 정확도나 대화 만족도가 얼마나 향상됐는지는 별도의 평가 결과로 입증해야 합니다.