
AI가 나처럼 말하도록 가르치는 방법: Oracle AI Database를 활용한 에이전트 메모리 구축
요약
Oracle AI Database를 사용하여 AI 에이전트가 사용자의 고유한 말투를 학습할 수 있도록 세 가지 계층의 메모리 시스템을 구축하는 튜토리얼입니다. 에피소드, 의미론적, 성찰적 메모리를 통해 에이전트가 개인화된 스타일로 콘텐츠를 생성하는 방법을 다룹니다.
핵심 포인트
- AI의 상태 비저장(Stateless) 특성을 극복하기 위한 메모리 구축 필요성
- 에피소드, 의미론적, 성찰적 메모리의 3단계 계층 구조 활용
- Oracle AI Database를 통한 통합 메모리 스택 관리
- 성찰 루프를 통한 지속적인 스타일 프로필 업데이트
- 프롬프트 엔지니어링보다 적절한 메모리 제공이 핵심
AI 에이전트가 당신의 말투로 소셜 미디어 게시물을 작성하는 법을 배울 수 있도록 Oracle AI Database에서 세 가지 계층의 에이전트 메모리를 구축하는 단계별 튜토리얼입니다.
동반 노트북 (Companion notebook): https://github.com/oracle-devrel/oracle-ai-developer-hub/tree/main/apps/oracle-agent-memory
핵심 요약 (Key takeaways)
- 문제는 AI가 글을 못 쓰는 것이 아니라, AI가 아무것도 없는 상태(zero)에서 글을 쓴다는 점입니다. 상태 비저장 (Stateless) 모델은 당신의 이전 게시물, 리듬 (cadence), 특이한 구절, 또는 당신이 절대 사용하지 않는 표현에 대한 기억이 없습니다. 따라서 인터넷의 평균적인 말투를 기본값으로 사용하게 되며, 이것이 수많은 AI 작성 소셜 콘텐츠가 모두 비슷하게 느껴지는 이유입니다.
- 좋은 글쓰기 지원에는 세 가지 종류의 메모리가 필요합니다. 에피소드 메모리 (Episodic memory)는 에이전트에게 당신이 이전에 작성했던 예시들을 제공합니다. 의미론적 메모리 (Semantic memory)는 구조화된 스타일 프로필을 제공합니다. 성찰적 메모리 (Reflective memory)는 당신의 글쓰기가 변함에 따라 해당 프로필이 진화할 수 있게 합니다.
- Oracle AI Database는 메모리 스택을 단순하게 유지합니다. 게시물, 벡터 (vectors), JSON 스타일 프로필, 그리고 성찰 로그 (reflection logs)가 모두 동일한 데이터베이스에 저장됩니다. 이는 에이전트가 별도의 서비스들을 복잡하게 엮을 필요 없이, 유사한 게시물을 검색하고, 당신의 보이스 프로필을 로드하며, 이해도를 업데이트할 수 있음을 의미합니다.
- 성찰 루프 (reflection loop)가 학습하는 듯한 느낌을 만들어냅니다. 몇 개의 새로운 게시물이 올라올 때마다, 에이전트는 현재의 스타일 프로필을 최신 글과 비교하여 보수적인 차이점 (diff)을 생성하고, 단 일주일간의 특이한 게시물에 과잉 반응하지 않으면서 프로필을 업데이트합니다.
- 실제 에이전트는 생각보다 매우 작습니다. 최종
generatePost함수에는 스타일 프로필, 몇 가지 유사한 예시, 그리고 단 한 번의 LLM 호출만 필요합니다. 어려운 부분은 프롬프트 (prompt)가 아닙니다. 어려운 부분은 프롬프트에 적절한 메모리를 제공하는 것입니다.
마지막으로 소셜 미디어를 이용했을 때, 느낌이 어떠셨나요... 진부하지 않았나요? 스크롤을 내리며 지나치는 모든 게시물이 단어만 약간 다를 뿐 다 똑같아 보입니다. "대부분의 X는 Y라고 생각합니다"로 시작하는 일반적인 도입부, 세 가지 핵심 포인트와 행동 유도(call to action), 그리고 똑같은 여섯 개의 이모지까지. 네, 그것들은 AI가 만든 것이었습니다.
모든 AI 생성 콘텐츠가 나쁘다는 말을 하려는 것은 아닙니다. 하지만 요즘 확실히 독창성이 부족하다는 것을 느끼고 있습니다. 이는 안타까운 일입니다. 창의적인 과정에서 생성형 AI (Generative AI)를 도구로 사용하는 것은 놀라운 일이기 때문입니다. 하지만 "AI 에이전트의 보안 문제에 대한 LinkedIn 게시물을 작성해줘"라고 요청한 결과물을 그대로 복사해서 붙여넣는 것은 잘못된 방식입니다.
AI 모델은 상태가 없는 (stateless) 특성을 가집니다. 게시물을 작성해달라고 요청할 때마다 모델은 제로(zero) 상태에서 시작합니다. 모델은 당신이 이전에 무엇을 썼는지, 무엇이 효과적이었는지, 무엇이 반응이 좋지 않았는지, 혹은 당신이 의식하지 않을 때 어떤 말투를 사용하는지 전혀 알지 못합니다. 그래서 모델은 평균적인 값에 의존하게 되며... 이것이 바로 요즘 당신의 피드에서 보고 있는 바로 그 모습입니다.
하지만 반드시 이럴 필요는 없습니다. AI 에이전트를 사용하여 소셜 게시물을 작성하는 데 도움을 받으면서도, 동시에 당신의 자연스러운 목소리처럼 들리게 할 수 있습니다. 단지 에이전트에게 약간의 메모리 (memory)를 제공하기만 하면 됩니다.
이 포스트에서는 Oracle AI Database 26ai를 기반으로 한 3계층 메모리 구조를 가진 AI 에이전트를 구축하는 방법을 살펴봅니다. 또한 시간이 지남에 따라 당신의 말투에 대한 에이전트의 이해도를 업데이트하는 성찰 루프 (reflection loop)도 다룹니다. 기술 스택은 Node.js 백엔드, React + Vite 프론트엔드, 공식 oracledb 드라이버를 사용하는 TypeScript 엔드 투 엔드 (end-to-end) 방식입니다. 만약 Python을 선호하신다면, langchain-oracledb가 직접적인 대응 라이브러리입니다.
에이전트 메모리에 대해 조금 더 자세히 알고 싶다면, Casius Lee가 작성한 이 블로그를 확인해 보세요.
우리가 만드는 것
우리의 에이전트는 한 가지 임무를 가집니다: 주제와 플랫폼(LinkedIn, X 등)이 주어지면, 마치 나처럼 들리는 게시물 초안을 작성하는 것입니다. 단 한 번의 LLM 호출 (one-shot LLM call)만으로도 충분히 가능해 보입니다. 하지만 진짜 멋진 부분은 프롬프트 (prompt)를 변경하지 않고도 시간이 지남에 따라 어떻게 에이전트를 더 개선할 수 있는지에 있습니다.
이를 위해 에이전트에는 세 가지 다른 종류의 메모리 (memory)가 필요합니다:
<table><tbody><tr><td><strong>계층 (Layer)</strong></td><td><strong>저장 내용</strong></td><td><strong>사용 방식</strong></td></tr><tr><td>에피소드 메모리 (Episodic memory)</td><td>벡터 (vector)로 임베딩된 내가 작성한 모든 게시물</td><td>예시로서 <a href="https://www.oracle.com/database/ai-vector-search/similarity-search/#techniques" rel="noopener noreferrer">가장 유사한 K개</a>의 과거 게시물을 검색</td></tr><tr><td>의미론적 메모리 (Semantic memory)</td><td>나의 말투 특성을 설명하는 구조화된 JSON 객체</td><td>명시적인 가이드라인으로서 시스템 프롬프트 (system prompt)에 주입</td></tr><tr><td>성찰적 메모리 (Reflective memory)</td><td>나의 글쓰기 스타일이 시간이 지남에 따라 어떻게 진화하는지에 대한 관찰 내용</td><td>주기적으로 의미론적 메모리 (semantic memory)를 개선</td></tr></tbody></table>이 세 가지 모두 Oracle AI Database에 저장되며, 이는 생각보다 훨씬 간단하게 만들어 줍니다. 벡터 검색 (Vector search), JSON, 그리고 관계형 행 (relational rows)이 모두 동일한 쿼리 엔진 (query engine)을 사용하는 동일한 데이터베이스 내에 존재합니다. 따라서 단일 데이터베이스 내에서 이를 작동시키는 데 필요한 모든 것을 저장할 수 있습니다.
루프 (loop)는 다음과 같습니다:

설정 (Setup)
아직 데이터베이스가 없다면, 리포지토리 (repo)에 Always Free Autonomous AI Database 26ai를 프로비저닝 (provision)하고 데이터가 채워진 .env 파일을 작성하는 Terraform 스택이 포함되어 있습니다. 리포지토리를 클론 (clone)하고 다음 세 명령어를 실행하기만 하면 됩니다:
cd terraform
terraform init && terraform apply
terraform output -raw env_file > ../.env
Always Free covers the cost를 통해 데이터베이스 비용을 영구적으로 지원받을 수 있습니다. OCI Generative AI는 Always Free 계층에 포함되지 않지만, 신규 계정에는 300달러의 체험 크레딧이 제공되며, 우리가 구축하려는 기능의 호출당 비용은 불과 몇 센트에 불과합니다. 이미 Oracle 26ai 인스턴스를 보유하고 있다면, Terraform 단계를 건너뛰고 .env 파일을 수동으로 작성하세요.
그 다음 의존성(dependencies)을 설치합니다:
npm install
의존성 설치가 완료되면 구축을 시작할 준비가 되었습니다! 하지만 그 전에, 에이전트 메모리(agentic memory)의 각 레이어를 나타내는 스키마 내 세 개의 테이블에 대해 이야기해 보겠습니다.
-- 일화적 메모리 (Episodic memory): 당신이 작성한 모든 게시물
CREATE TABLE posts (
id VARCHAR2(36) PRIMARY KEY,
...
VECTOR(1024, FLOAT32)가 OCI의 cohere.embed-english-v3.0 모델과 일치한다는 점에 유의하는 것이 중요합니다. 만약 임베딩 함수(embedding functions)를 교체한다면, 반드시 차원(dimension)을 그에 맞춰 업데이트해야 합니다. 그리고 보너스로, Oracle 26ai에서 JSON은 인덱싱 가능한 경로(indexable paths)를 가진 일급 타입(first-class type)이므로, 스타일 프로필(style profile)을 매번 읽을 때마다 다시 파싱할 필요가 없습니다.
메모리 레이어 자체로 넘어가기 전에, 일관성과 단순성을 위해 OCI SDK를 감싸는 얇은 래퍼(thin wrapper)를 설정하겠습니다. 두 가지 함수가 있습니다: 소셜 게시물로부터 텍스트 임베딩(text embeddings)을 생성하는 역할을 하는 embed(), 그리고 모델과 통신하기 위한 chat()입니다.
// src/server/llm.ts
import * as common from 'oci-common';
import { GenerativeAiInferenceClient } from 'oci-generativeaiinference';
...
ConfigFileAuthenticationDetailsProvider는 ~/.oci/config (DEFAULT 프로필)를 자동으로 읽습니다. servingType: 'ON_DEMAND'는 클러스터를 프로비저닝(provisioning)하지 않고 체험 크레딧을 사용하는 종량제(pay-as-you-go) 모드입니다. 이제부터 모든 호출은 이 embed() 및 chat() 함수를 사용합니다.
일화적 메모리 (Episodic memory)
첫 번째 레이어는 가장 단순합니다. 제가 포스트를 게시할 때마다 이를 저장합니다. 새로운 포스트를 초안으로 작성하고 싶을 때마다, 에이전트는 퓨샷 예시 (few-shot examples)로 사용할 가장 유사한 과거 포스트 K개를 검색합니다.
// src/server/memory.ts
import { randomUUID } from 'node:crypto';
import { withConn, oracledb } from './db';
...
검색(Retrieval)은 조금 더 미묘하며, 여기서는 하이브리드 필터 (hybrid filter)를 최대한 활용합니다. 우리는 삭제되지 않은 것 중, _이 사용자_가 _동일한 플랫폼_에서 작성한 가장 유사한 포스트를 찾고자 합니다. 이를 위해 벡터 검색(vector search)과 WHERE 절을 함께 수행하며, 단일 쿼리로 원하는 결과를 얻을 수 있습니다.
export async function retrieveSimilarPosts(args: {
userId: string; platform: string; topic: string; k?: number;
}) {
...
FETCH APPROX FIRST :k ROWS ONLY는 Oracle이 근사 최근접 이웃 (approximate nearest neighbor)을 위해 HNSW 인덱스 (HNSW index)를 사용하게 해주는 구문입니다. APPROX가 없다면 쿼리는 정확한 스캔 (exact scan)으로 전환되는데, 이는 수천 개의 벡터에는 괜찮지만 수백만 개의 벡터에는 사실상 사용할 수 없습니다.
의미론적 메모리 (Semantic memory)
일화적 검색 (Episodic retrieval)을 통해 "이 주제에 대해 이전에 무엇을 말했는가"를 알 수 있습니다. 하지만 이제는 "글을 쓸 때 내 말투가 어떠한가"를 알아야 합니다. 이것이 바로 의미론적 메모리 (semantic memory)의 완벽한 사용 사례입니다.
우리는 "스타일 프로필 (style profile)"을 구조화된 JSON 객체로 유지합니다. 이는 글 자체에 담기보다는 글에 대한 목소리 특성(voice traits)과 같은 속성들을 저장합니다. 사용자의 말투를 잘 근사하기 위해, 우리의 프로필은 다음과 같은 행동 양식을 찾습니다.
{
"tone": ["direct", "self-deprecating", "slightly skeptical of hype"],
"sentenceLength": {
...
만약 누군가에게 자신에 대해 이런 내용을 써달라고 요청해 본 적이 있다면, 사람들이 이러한 종류의 자기 성찰(self-reflection)에 얼마나 형편없는지 잘 알고 계실 것입니다. 따라서 자연스럽게 우리는 인간 요소를 배제하고, 사용자가 시스템에 추가한 처음 N개의 게시물을 바탕으로 모델이 직접 생성하도록 요청합니다.
import { chat } from './llm';
const SEED_SYSTEM = `당신은 음성 분석가(voice analyst)입니다. 당신은 한 저자의 여러 소셜 미디어 게시물을 읽고 그들이 어떻게 글을 쓰는지 설명하는 JSON 스타일의 프로필을 생성할 것입니다. 구체적이고 구체적이어야 합니다. "톤이 친근함"은 쓸모가 없습니다. "톤이 직설적이며, 때때로 자기 비하적(self-deprecating)이고, 유행에 대해 약간 회의적(slightly skeptical of hype)임"이 유용합니다.
...
MERGE 문은 한 번의 왕복(round trip)으로 삽입(insert)과 업데이트(update)를 모두 처리합니다(삭제도 가능하며, 상당히 인상적입니다). Oracle의 JSON 타입은 삽입 시 값을 검증하며, 만약 LLM이 잘못된 형식의 JSON을 출력하면 삽입이 실패합니다. 이는 정확히 우리가 원하는 바입니다.
다시 읽어오는 과정도 몇 줄의 간단한 코드로 가능합니다:
export async function loadStyleProfile(userId: string) {
return withConn(async (conn) => {
const r = await conn.execute<[unknown]>(
...
성찰적 메모리 (Reflective memory)
저는 에피소드 메모리(episodic memory)와 의미론적 메모리(semantic memory)를 구현하는 것으로 작업을 마치는 에이전트를 많이 만들어 왔습니다. 많은 경우, 그것으로도 충분합니다. 하지만 사람들이 자신이 관심을 두는 것에 대해 글을 쓰는 것과 같은 유형의 워크로드에서는 선호도가 시간이 지남에 따라 변합니다. 개인적으로, 저는 예전에 서버리스 아키텍처(serverless architectures)에 대해 건조하고 차가운 사실만을 써왔습니다. 하지만 오늘날의 저는 꽤 재미있는 사람이며(그렇죠?!), 소프트웨어를 '좋은' 수준에서 '위대한' 수준으로 끌어올리는 것들에 대해 고민합니다. 매우 다른 스타일이지만, 둘 다 저입니다.
성찰적 메모리(reflective memory)를 구축하면 에이전트가 시간이 지남에 따라 스스로를 조정할 수 있습니다. 이것이 바로 에이전트가 실제로 "학습"하고 있다는 인상을 주는 핵심입니다.
매 K개의 새로운 포스트가 작성될 때마다 (저는 K=5를 사용합니다), 우리는 성찰 (reflection)을 트리거합니다. 이 성찰은 현재의 스타일 프로필 (style profile)과 가장 최근의 K개 포스트를 읽은 다음, 무엇이 변했는지, 무엇이 추가되어야 하는지, 그리고 무엇이 제거되어야 하는지를 나타내는 구조화된 차이 (structured diff)를 생성하는 LLM 호출입니다.
우리는 프로필 스래싱 (profile thrashing)을 방지하기 위해 단순히 덮어쓰는 대신 구조화된 차이 (structured diff) 방식을 사용합니다. 당신은 단지 당신의 마지막 5개 포스트가 아닙니다. 당신은 최신성 편향 (recency bias)을 가진, 당신이 지금까지 게시한 모든 것의 요약입니다. 차이 (diff)를 요청함으로써 모델이 작고 의도적인 업데이트를 수행하도록 할 수 있으며, 이를 통해 당신은 당신다움을 유지하면서 매주 격렬한 감정 기복을 겪는 것처럼 보이지 않을 수 있습니다.
const REFLECT_SYSTEM = `당신은 저자의 목소리가 어떻게 진화했을지 검토하고 있습니다. 당신에게는 다음이 주어집니다:
1. 저자의 현재 (CURRENT) 스타일 프로필 (이전 포스트들로부터 구축됨)
...
applyDiff 함수는 간단합니다
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기