AWS 기술 블로그
Smart Agentic AI 구축을 위한 데이터베이스 설계
들어가며
에이전트 서비스 특히, MCP(Model Context Protocol) 서버가 새로운 메인 트렌드로 부상하고 있습니다. 많은 것들이 빠르게 변하고, 그 변화에 적응하기 조차 어려운 시대가 되었습니다. 이 블로그에서는 에이전트 서비스들의 근간에 흐르는 데이터에 초점을 맞추고자 합니다. 에이전트 아키텍쳐는 여러가지 종류가 있습니다. 비동기 Pub/Sub 메시징 기반의 멀티에이전트 프레임워크인 AutoGen, 오늘 중점적으로 논의할 그래프 기반의 워크플로우를 자유롭게 정의하는 LangGraph, 빠른 Poc에 적합한 코드중심의 미니멀 에이전트인 Smolagent 등등 여러가지가 있습니다. 특히 Strands Agent 와 같이 동적으로 도구(Tool)를 선택하고 노드를 붙여나가는 에이전틱 워크플로우는 이미 정의되어 있는 정적인 워크플로우에 비해서, 많은 자유도와 사용자의 다양한 요구사항에 유연하게 대처할수 있습니다. 최근에는 Manus Agent도 나왔습니다. Strands와 Manus Agent는 기존에 비해 훨씬 진일보한 아키텍쳐임에는 틀림 없습니다. 다만, 늘 반복되는 프로세스들조차도 실행하는 사람들마다 다른 결과를 얻기도 하고, 한개의 단순한 명령을 수행하기 위해서도 여러번의 시도와 에러가 반복되는 경우들도 있습니다.
그러면 그때마다 반복적인 실패를 하고 원하는 결과를 얻어야만 하는 걸까요? 그 뿐만 아닙니다. 사용자가 요청했을 때, 생성형 AI가 추론하고 처리했던 여러가지 내용들도 다음 비슷한 요청을 위한 컨텍스트로 활용되지 않는 경우도 많습니다. 새로운 누군가가 유사한 질문을한다면 어떻게 될까요? 같은 대답을 생성하느라 토큰과 시간을 소모할 필요가 있을까요? 특히, 프로세스가 잘못된 경우 잘못된 응답을 반복하고 토큰과 시간도 그대로 낭비해야 할까요? 이것은 프롬프트 캐싱을 사용하더라도 결국은 마찬가지가 될것입니다. 그렇다면, 에이전트의 사용자의 요청 처리내역을 저장해서, 그 데이터를 분석하고 최적의 프로세스를 정립하면 어떨까요? 이것이 이제부터 말씀드리려는 내용입니다. 에이전트의 데이터 저장을 위한 데이터베이스는 구체적으로 어떤 데이터베이스가 어떤 용도로 사용되야 하고, 데이터베이스 내의 스키마 설계는 어떻게 해야할까요? 이 블로그에서는 여러 에이전트 아키텍쳐 가운데, LangGraph와 Strands Agent를 기반으로 데이터베이스 설계를 이야기해보려고 합니다.
에이전트 서비스를 위한 데이터베이스 아키텍쳐
에이전트는 그 안에 여러가지 도구(Tool)들을 가지고 있습니다. (이하 툴이라고 하겠습니다) 이러한 툴을 미리 사전에 정의한 순서에 따라 처리되도록 하면 에이전트 자동화가 이루어집니다. Strands Agent와 같은 아키텍쳐에서는 고정된 워크플로우가 아니라 필요에 따라 에이전트가 판단하고 동적으로 워크플로우를 상황에 맞게 변경할수 있습니다. 매우 편리한 기능이지만, 한편으로는 원하는 결과를 얻기까지 통제가 어렵고 원하지 않는 결과를 얻는 경우 자칫하면 큰 문제가 생길수도 있습니다. 사실 이러한 부분때문에 PoC(Proof Of Concept)와 같은 프로젝트는 많지만, 실제 운영서비스에 에이전트 자동화 서비스를 적용하기는 쉽지 않습니다. 그래서 에이전트가 안정화된 자동화 프로세스를 정립하기까지 책임있는 사람의 개입과 통제(Human-in-the-Loop)가 부분적으로 필요하게 됩니다. 에이전트가 프로세스를 자동으로 실행하기전에 실행계획을 시각적으로 보여주면, 사람이 먼저 리뷰하고, 프로세스를 수정하거나 추가 또는 삭제하도록 합니다. 사후 리뷰도 수행하게 합니다. 주기적인 배치 리뷰 프로세스를 두어, 에이전트 스스로, 처리한 프로세스들을 평가하게 하고, 이것을 최종적으로 사람이 리뷰하여 프로세스를 보완하게 합니다. 실행된 내용들은 저장했다가 비슷한 요청이 들어오면, 이 프로세스들의 실행이력을 컨텍스트로 생성형 AI서비스에 제공하거나, 아예 기존 프로세스 그대로 재사용할 수도 있습니다. 바로 여기에 데이터베이스의 역할이 있습니다. 이렇게 사람의 승인과 검토를 거친 에이전트의 검증된 프로세스는 더욱 신뢰할수 있고 안정적으로 처리되며, 궁극적으로는 에이전트의 자동화 목표를 이루는데 도움을 줄 수 있습니다. 자, 그렇다면 어디에 무엇을 저장하면 될까요?
Amazon DynamoDB : 에이전트가 실행한 모든 프로세스와 툴 실행 이력을 저장
에이전트가 처리한 내역들 성공한 프로세스와 툴 실행이력들은 DynamoDB에 저장하도록 합니다. DynamoDB는 Key-Value(키-값) 형태의 NoSQL Database로서, 사용 트래픽의 많고 적음에 영향받지 않고, 빠른 성능을 일관되게 유지하면서, 한편으로는 다양한 형태의 비정형 데이터를 유연하게 저장하는 장점을 가지고 있습니다. DynamoDB를 에이전트 서비스의 메인데이터를 저장하는 가장 큰 이유는, 바로 LangGraph의 각 노드들이 가지고 있는 키-밸류 값을 저장하는데 적합한 구조를 가지고 있기 때문이기도 합니다.
Amazon Redshift : 에이전트의 실행이력을 분석하여, 최적의 프로세스를 제안
이렇게 저장된 프로세스와 툴 실행이력을 저장하는 것으로 끝나지 않습니다. 에이전트가 실행한 많은 프로세스들에는 여러가지 성공, 실패와 그 이유들이 저장되어있습니다. 이 정보들을 Redshift로 보내서 분석하게 하여, 가장 안정적이면서 검증된 프로세스를 사용자에게 제안해줄수 있습니다. DynamoDB는 ZeroETL로 Redshift에 데이터를 준실시간으로 연동할수 있습니다. 실행된 사용자의 프로세스 이력을 Redshift에서 분석한 내용을 바탕으로, 사용자의 프로세스 워크플로우에 스코어를 매겨, 가장 신뢰할수 있는 안정적인 최적의 프로세스 워크플로우를 사용자에게 제안하게 합니다. 그래서 이전에 처리된 프로세스와 비슷한 요청이 올 때, 성공적으로 처리된 프로세스를 먼저 리스트 업하여, 생성형 AI가 같은 답이나 결과를 처리하기 위해 불필요한 토큰과 시간을 낭비하지 않도록 합니다. 스코어의 기준을 정하는 것은 다양한 방식으로 정할수 있습니다. 에이전트의 특정 프로세스의 성공/실패건수, 최근 실행한 내용인지, 사용자들의 조회/실행횟수를 스코어에 더할수도 있습니다. 아래는 스코어를 정하는 하나의 예시로 여러분들의 상황에 맞춰서 다양한 방식으로 스코어의 기준을 정하고 테스트하면서 수정 또는 추가할 수 있습니다.
- 스코어 = 총실행건수 × exp{-0.1 ×(오늘일시-최근실행일시) } × (성공건수/실행건수)²
위 스코어 계산 방식은 지수감쇠를 통해서 가장 최신 프로세스가 자연스럽게 상위로 올라오게 하고 성공률은 제곱하여 높은 신뢰도를 가진 워크플로우가 상위에 랭크되게 합니다. 이외에도 여러가지 방식을 고려해볼 수 있을 것입니다.
Amazon Elasticache for Valkey : 자주 사용된 프로세스를 메모리에 올려서 성능 최적화
사용자가 채팅창에서 문의나 요청을 하게 되면, 먼저 생성형AI 서비스가 사용자의 요청을 판단하는데, 이때 먼저 Amazon ElastiCache for Valkey에서 해당내용이 있는지 확인하고, 없으면 DynamoDB를 조회하고 유사한 프로세스들이 있는지를 확인합니다. 만약 없다면, 새로운 프로세스를 만들고 이 프로세스들의 집합을 DynamoDB에 저장하고 처리결과도 DynamoDB에 저장합니다. Valkey 역시 Key-Value형태의 구조를 가지고 있어, 에이전트의 각 노드들의 상태값을 저장하기에 적합한 형태입니다. 또한 모든 노드의 상태값을 일일이 저장하지 않아도, 사용자가 요청을 중단하는등의 불필요한 임시 데이터는 버림으로써, 불필요한 데이터 저장공간도 절약할수 있습니다. 그리고 자주 사용하는 프로세스의 컨텍스트를 Valkey에서도 활용합니다. Redshift에서 분석하여 업데이트한 스코어값이 높은 에이전트의 프로세스 이력도 메모리에 올리고 계속 유지시킴으로써 사용자들이 가장 많이 요청하는 프로세스를 성능/비용 효율적으로 처리합니다.
에이전트를 위한 데이터베이스 아키텍쳐의 활용방법
위에서 열거한 여러 데이터베이스로 구성된 아키텍쳐는 다음과 같습니다.
- 먼저 사용자 세션의 대화 및 에이전트의 실행이력은 Valkey에서 먼저 처리합니다.
- Valkey 에서 조회할 데이터가 없는 경우는 DynamoDB를 조회합니다.
- Valkey 에서 처리한 내역은 비동기 방식으로 DynamoDB에 저장합니다.
- DynamoDB가 저장한 에이전트의 처리내역들은 ZeroETL로 Redshift로 보내집니다.
- Redshift에서는 그동안 처리한 사용자 세션의 대화 및 에이전트의 실행이력을 다각도로 분석하고 스코어를 매깁니다.
- Lambda가 Redshift에서 프로세스의 스코어를 다시 DynamoDB 테이블과 Valkey의 통계정보에 업데이트 합니다.
- Valkey와 DynamoDB 에서 스코어가 낮은 실행이력들은 디폴트로 설정된 TTL값(예시:30일)에 따라 자연스럽게 삭제됩니다.
- Valkey와 DynamoDB 에서 스코어가 높은 실행이력들은 TTL값(예시:60일)로 업데이트 하는 방식으로 계속 유지시킵니다.
- 이 작업이 순환/반복되며, 성공횟수가 높은 프로세스만 살아남고, 스코어가 높은 에이전트가 먼저 조회됩니다.
전체적인 아키텍쳐에 대한 설명은 여기까지 말씀드리고, 이제부터는 블로그의 가장 핵심 주제인 에이전트 서비스에 대한 데이터베이스의 스키마 설계에 대해서 구체적으로 설명하겠습니다.
에이전트 서비스 실행이력을 저장하는 데이터베이스 스키마 설계
데이터베이스 스키마 설계에서는 개념 정립이 중요합니다. 해당 비즈니스나 서비스에 대한 이해와 배경지식이 깊을수록, 유연하고 효과적인 스키마 를 설계할 수 있습니다. 먼저 에이전트 서비스는 어떤 일을 하는지 정리해보겠습니다. 에이전트 내부에는 각각의 역할을 담당하는 노드들로 구성이 되어있고, 이 노드들은 워크플로우로 순서대로 각 노드들이 연결됩니다. 에이전트의 각 노드들은 사용자의 요청을 처리하고 다음 단계의 노드에게 상태값을 전달합니다. LangGraph의 아키텍쳐를 기반으로 좀 더 세부적인 역할을 설명하면 다음과 같습니다.
- 에이전트 서비스가 사용자의 요청을 처리하려면 사용자의 요청이 어떤 도메인, 카테고리에 속해있는지부터 알아야 합니다.
- 도메인 밑에 에이전트 서비스가 하위 단위로 들어가서 사용자의 요청에 따라 도메인이 결정되면 에이전트들을 불러올수 있습니다.
- 에이전트 밑에는 업무를 처리하기 위한 하위단위의 툴(노드)들이 필요하고 툴들이 무슨 기능을 하는지 알아야 합니다.
- 에이전트의 각 툴들을 어떤 순서로 실행할지에 대한 순서, 즉 여러개의 노드로 구성된 에이전틱 워크플로우를 구성할수 있습니다.
- LangGraph 의 각 노드는 Key-Value(키-값) 값을 가지고, 각 노드별로 그 상태값을 저장하고 전달할수 있습니다.
- LangGraph의 각 노드가 처리한 상태값에 따라서 좋은 결과 또는 나쁜 결과를 내고 노드와 프로세스, 에이전트의 통계값을 업데이트 합니다.
에이전트 서비스의 각 노드들의 상태값을 잘 알고 있다면, 해당 에이전트를 디버깅하고 더 좋은 서비스로 만들기 위해 도움이 됩니다. Strands Agent SDK로 생성된 에이전트와 같이 동적인 에이전틱 워크플로우에서는 에이전트의 경로 최적화, 사용자에 최적화된 프로세스 정립이 중요합니다. 에이전트의 서비스의 세부적인 역할을 수행하기 위한 엔터티들을 정의해보면 아래와 같습니다.
도메인(Domain)
생성형 AI 서비스에게 무엇인가 질문이나 요청을 한다고 생각해봅시다. 여러 에이전트가 있을수 있는데, 사용자의 질문은 어떤 에이전트가 실행할지를 결정하는게 첫번째 단계입니다. 에이전트가 어떤 일을 하는지, 다시 말해 에이전트가 속한 업무 카테고리, 도메인의 성격을 알아야 하는 것이 가장 첫번째 임무가 될 것입니다 . 에이전트의 도메인을 통해서 어느 에이전트를 선택할지가 판단할수 있고, 그 도메인 또는 카테고리에 속하는 여러개의 에이전트 서비스 중에서 우선순위를 매겨 가장 좋은 것을 선택할수 있습니다. 이렇게 도메인을 정한다면 수많은 에이전트 서비스중에 원하는 에이전트를 빠르게 찾을수도 있습니다. 예시로서 금융, 여행, 통신, 게임과 같은 최 상위 업무 카테고리가 될수도 있고, 데이터베이스가 도메인이 된다면 에이전트로서는 MySQL,PostgreSQL,Oracle, MS-SQL 와 같은 DB에이전트가 하위 단위로 속할수도 있을것 입니다.
에이전트(Agent)
하나의 MCP서버, 하나의 에이전트를 의미합니다. 에이전트에는 해당 에이전트가 무슨 역할을 하는지 설명이 필요합니다. 이 설명을 생성형AI 서비스가 읽고 이 에이전트가 사용자의 요청에 적합한지 판단할 수 있습니다. 그래서 에이전트 엔터티에는 Description과 같은 설명과 키워드를 저장할 곳이 필요합니다. 이것들을 통해 생성형AI 가 어떤 에이전트를 선택할지 결정하게 합니다. 또한 세부적인 특징들을 여러가지로 추가할수 있습니다. 에이전트가 실행될때마다 성공, 실패 횟수를 기록하거나 스코어나 중요도를 저장합니다. 이를 통해서 가장 성공 횟수나 스코어가 높은 에이전트가 검색의 상위에 배치되어 더 많이 사용되게 합니다. 예시로서 프런트 웹 서비스 생성 에이전트, 특정 지역의 카페 검색 서비스, 특정게임의 스테이지 클리어 튜토리얼 서비스 같은것들이 있을수 있습니다.
툴(Tool)
Tool은 Agent 의 하위 단위 입니다. LangGraph에서는 하나의 노드를 의미하며, 노드가 여러개 연결되면 에이전틱 워크플로우가 만들어집니다. 사용자의 요청에 실질적으로 대답을 하거나 업무를 수행하는 하나의 실행단위, 함수를 의미하기도 합니다. Tool , 즉 노드는 각각을 의미하는 키 값과 처리한 상태값들을 저장합니다. 이 상태값을 통해서 어떤 툴이 어떤 값으로 처리되었는지 디버깅하면서, 사용자의 요청에 맞게 잘 수행되었는지 판단하고 개선할수 있는 지표로 사용합니다. 예시로서 프런트 웹 서비스 에이전트의 S3버킷생성 함수, EC2인스턴스생성 같은 것들이 있고, 게임에서는 Attack_zombie와 같은 함수가 여기에 해당됩니다.
사용자 프로파일(UserInfo)
지금까지는 에이전트 입장에서 엔터티를 정의했다면, 실제 사용하는 사용자 입장에서의 엔터티도 필요합니다. 그래야 어떤 유저가 어떤 요청을 했으며, 유저별로 자신만의 최적화된 프로세스를 가질수 있기 때문입니다. User는 이러한 에이전트 서비스를 사용하려는 주체를 말합니다. 유저정보는 유저아이디 및 유저명, 그리고 유저의 신상정보와 같은 프로파일 정보들을 저장합니다.
사용자 세션(UserSession)
UserSession은 User가 AI서비스를 사용할때 유지하는 세션을 말합니다. 생성형AI 서비스와 어떤 대화나 요청을 했고, 그 요청에서 어떤 에이전트가 무슨 작업을 했는지 알기 위한 근거 데이터로서 역할을 합니다. UserSession 엔터티에서는 유저와 생성형AI 대화내용과 에이전트트의 처리결과나 답변등을 저장할수 있어야 합니다. 그러나 모든 내역을 다 저장하기에는 데이터가 길어질 수 있으므로, 자세한 내역은 UserSessionDetail 엔터티에서 자세하게 기록합니다. 또한 UserSession은 사용자 세션의 기본정보 및 시작시간과 끝시간도 기록해서 언제 어떤 대화가 오갔는지를 시점별로 확인하고, 가장 최근 세션기록이나 특정시점의 세션기록을 가져 올 수 있습니다.
사용자 세션 프로세스(UserSessionProcess)
UserSession에서 다루지 못한 상세내역으로, User가 질문한 내용 또는 요청한 내역들의 상제정보를 저장합니다. 상세정보가 길어지고 특정파일들이 업로드되거나 처리된 경우에, 파일위치 정보를 저장합니다. 파일로 저장하는 경우에 장점은 대화내용이 계속 수정되고 추가되는 상황에서도 UserSessionDetail 엔터티를 수정하지 않고 파일에만 저장하면 됩니다. UserSessionDetail에서는 사용자의 개별요청, 질문들을 세세하게 기록합니다. 그럼으로써, 사용자의 어떤 구체적인 요청이 생성형AI에게 User Prompt로 전달되었는지 확인할 수 있습니다.
사용자 세션프로세스 툴 매핑이력 (UserSessPrcToolMappHist)
이 엔터티에서는, 바로 위의 UserSessionProcess 엔터티의 논리적인 프로세스와 매핑되는 에이전트 서비스의 Tool정보를 저장합니다. 이 정보를 통해서 에이전트의 어떤 툴들이 실행되었고, 처리된 값은 무엇이었는지 확인하여, 유저가 요청한 질문에 올바른 대답과 결과를 가져왔는지를 확인하게 됩니다. 즉 UserSession에서 사용자가 요청한 내용을 Agent가 수행한 실제 수행이력이 여기에 저장됩니다. 이력들은 DynamoDB에서 조회할수도 있겠지만, 주로 Redshift로 보낸 다음, 거기서 유저의 요청에 가장 적합하게 수행을 한 프로세스와 에이전트-툴 조합을 정리해서 다시 아래 테이블인 유저 프로세스 툴 매핑 테이블에 업데이트 합니다.
사용자 프로세스 툴 매핑 (UserPrcToolMapp)
이 엔터티에서는, 바로 위의 UserSessPrcToolMappHist 엔터티를 Redshift로 ZeroETL로 보내서 어떤 유저의 요청에 가장 적합한 에이전트-툴 매핑조합이 적합한지를 정리한 데이터가 들어갑니다.. 사용자 자신만의 최적화된 프로세스를 가져올수도 있고, 이 정보를 사용자 스스로 판단해서 순서를 바꾸거나 추가로 에이전트나 툴 을 추가하는 등의 편집 작업을 할 수 있습니다. 대부분의 경우는 유저의 어떤 요청에 가장 적합한 프로세스와 에이전-툴 정보를 미리 제공함으로써, 사용자 응답에 대한 레이턴시를 줄이는 한편, 가능하다면 해당 프로세스를 그대로 사용하게 하여 반복적인 동일하거나 유사한 요청에 따른 추론 토큰 수를 줄이도록 합니다. 바로 이 유저 세션 프로세스 툴 매핑 엔터티야 말로 에이전트 서비스 아키텍쳐 중에서 가장 중요한 부분이기도 합니다. 위와 같은 개념 정리를 통해 ERD를 도출해보면 아래 그림과 같습니다.
위에서 설명드린 엔터티들의 구조를 바로 위에 있는 ERD 이미지에서 확인할 수 있습니다. ERD에는 DynamoDB의 엔터티이면서도 Redshift에도 동일하게 생성되는 엔터티가 하나 있습니다. UserPrcToolMapp(사용자 툴 매핑정보) 입니다. 맨 아래 가운데에는 UserSessPrcToolMappHist (유저세션프로세스 툴 매핑이력)이 있습니다. 유저의 요청에 적합한 툴을 실행한 이력을 ETL로 집계해서 바로 위에 있는 UserPrcToolMapp(사용자 툴 매핑정보)에 넣습니다. 사용자는 생성형AI 서비스에게 가장 적합한 툴을 어떤 순서로 실행할지 요청할 수도 있지만, 이 사용자 툴 매핑정보를 참조해서 직접 실행할 툴들을 선택할수 있습니다. 또는 이 사용자 툴 매핑정보를 생성형AI서비스에게 Context에게 전달해서 참조하도록 할 수도 있습니다. 왼쪽에 있는 Tool 엔터티는 한 에이전트에 있는 여러가지 툴 정보가 있습니다. 여러 툴정보를 하나의 어트리뷰트안에 순서대로 넣어서, 하나의 아이템을 만들었는데, 이렇게 여러개의 아이템으로 구성된 데이터를 하나 또는 여러 어트리뷰트에 넣는 것을 피보팅(Pivot)한다고 이야기합니다. 위에서 설명한 논리적인 엔터티들을 물리적인 데이터베이스의 설계와 특히 DynamoDB를 기준으로한 스키마 설계로 매핑하면 아래와 같습니다.
DynamoDB 스키마 설계
DynamoDB 단일 테이블 설계는 다양한 엔티티 타입을 하나의 테이블에 저장하면서도 효율적인 쿼리를 가능하게 합니다. 이 설계에서는 다음과 같은 여러개의 주요 엔티티 타입을 하나의 테이블에 저장합니다. 테이블을 저장하는데 있어서, 체계적인 파티션 키 및 소트 키를 정하는 코드 체계도 중요하겠지만, 먼저 액세스 패턴을 정하는 것이 중요합니다. 무엇을 저장하는지를 정의하기전에 우리가 원하는 정보를 어떻게 가져올지부터 정의해야 액세스 패턴을 좀 더 쉽게 정의할 수 있습니다.
- 사용자의 요청에 따른 적절한 도메인을 찾을수 있습니다.
- 하나의 도메인에 속한 여러 에이전트를 불러올수 있습니다.
- 하나의 도메인에 속한 여러 에이전트 중에서 스코어가 높은 에이전트를 불러올수 있습니다.
- 하나의 도메인에 속한 여러 에이전트 중에서 사용자의 요청에 가장 적합한 툴을 가진 에이전트를 불러올수 있습니다.
- 특정 사용자의 모든 세션을 최신일자별로 특정 건수만큼 조회합니다.
- 특정 사용자의 특정 세션의 프로세스 및 툴 실행이력을 조회합니다. 프로세스의 순서대로 데이터를 가져옵니다.
- 특정 사용자 또는 다른 사용자의 유사한 요청에 최적화된 툴 매핑정보를 가져옵니다.
이와 같은 기본 구조를 가지고 각 엔터티를 정의하였습니다. 엔터티의 어트리뷰트 구조로 보는것보다, 매장을 검색하는 과정을 샘플데이터와 같이 보여주면 좀 더 이해하기 쉬울 것입니다. 아래 엔터티에서 GSI1_PK, GSI1_SK와 같은 어트리뷰트는 Global Secondary Index를 위한 어트리뷰트입니다. 이 두개의 어트리뷰트를 가지고 GSI의 Partition Key와 Sort Key를 구성하는 것입니다. 그리고 엔터티의 어트리뷰트의 데이터 타입은 모두 String 로 구성했습니다. 만약 숫자를 String으로 데이터를 넣는 경우 주의하실 점이 있습니다. String타입으로 들어간 숫자데이터를 정렬하면 1,11,112 와 같은 순서로 읽어옵니다. 순수하게 숫자만 들어가 있고 Sort key로 활용하시려면 Number 타입으로 바꾸는 것이 좋습니다. 숫자 데이터에 String 타입을 꼭 사용해야 한다면, ‘00001’,‘00002’ 와 같은 형태로 입력하는 방법도 있습니다.
도메인(Domain)
PK | SK | DomainNM | Description | |
1 | DMN001 | METADATA | 매장 검색서비스 | 지역 기반의 매장 검색 서비스로 사용자가 요청한 지역의 카페, 레스토랑을 다양한 카테고리별로 검색하여 찾아주는 서비스 |
에이전트(Agent)
PK | SK | AgentNM | Score | Description | GSI1_PK | GSI1_SK | |
1 | AGT001 | METADATA | 매장 검색 에이전트 | 60 | 지역기반 매장 검색서비스로 최근매장 리스트업이 잘되있음 | DMN001 | AGTSCORE#60 |
2 | AGT002 | METADATA | 매장 검색 에이전트2 | 40 | 지역 기반 매장 검색 및 추천 서비스 제공 | DMN001 | AGTSCORE#40 |
3 | AGT003 | METADATA | 매장 검색 에이전트3 | 50 | 동네 매장 검색 추천 서비스 | DMN001 | AGTSCORE#50 |
툴(Tool)
PK | SK | ToolNM | ToolSpec | Description | GSI1_PK | GSI1_SK | |
1 | AGT001 | TL001 | FIND_LOC | {“description”: “특정 지역 검색”, “parameters”: {“location”: “string”, “radius”: “string””}} | 특정지역주변의 매장을 검색하는 도구 | DMN001 | TL#FIND_LOC |
2 | AGT001 | TL002 | USE_SEARCH_ENGINE_API | {“description”: “검색엔진의 API 호출”, “parameters”: {“location”: “string”, “radius”: “string”, “cuisine”: “string”}} | 위치값,반경등을 받아 검색엔진의 API를 순차적으로 검색 | DMN001 | TL#USE_SEARCH_ENGINE_API |
3 | AGT001 | TL003 | FORMAT_RESULTS | {“description”: “결과를 리스트형태로 전달”, “parameters”: {“results”: “string”}} | 결과값을 포맷팅하여 리스트형태로 전달 | DMN001 | TL#FORMAT_RESULTS |
사용자 프로파일(UserInfo)
PK | SK | UserNM | UserProfile | LastLoginDT | CreationDT | |
1 | USR001 | METADATA | 돌칼 | {“birth”:”19741126″,”HP”:”010-2222-3333″,”ADDR”:”서울시 송파구 잠실동”,”age””51″} | 20250614141002 | 20250601093210 |
사용자 세션(UserSession)
PK | SK | UserSessionNM | SessionSummary | SessionStartDT | DtlFileLoc | GSI1_PK | GSI1_SK | |
1 | USR001 | SESS20250614001 | 잠실종합운동장 맛집 검색 세션 | 잠실종합 운동장 주변 맛집 검색 및 추천 | 20250614140530 | S3://usersess/2025… | USR001 | 잠실종합운동장 맛집 검색 세션 |
사용자 세션 프로세스 (UserSessionProcess)
PK | SK | UserPrompt | ProcNM | ProcDesc | ProcDT | |
1 | USR001#SESS20250614001 | PRC001 | 잠실종합운동장 근처에 있는 N매장 근처의 최근에 생긴 카페를 찾아줘 | 위치확인 | 먼저 잠실종합운동장의 위치를 확인한다. | 20250614140601 |
2 | USR001#SESS20250614001 | PRC002 | 매장검색 | 잠실종합운동장 근처의 카페를 찾기 위해 검색엔진과 SNS의 API를 활용한다. | 20250614140601 | |
3 | USR001#SESS20250614001 | PRC003 | 결과포맷팅 | 검색한결과를 포맷팅해서 최종결과를 응답한다 | 20250614140602 |
사용자 세션 프로세스 툴 매핑이력 (UserSessPrcToolMappHist)
PK | SK | ToolNM | ToolValues | TransactDT | SuccYN | ResultMsg | |
1 | USR001#SESS20250614001 | PRC001#AGT001#TL001 | FIND_LOC | “잠실종합운동장 근처에 있는 N매장 근처의 최근에 생긴 카페를 찾아줘” | 20250614140605 | Y | 사용자의 요청, 잠실종합운동장 위치 |
2 | USR001#SESS20250614001 | PRC002#AGT001#TL002 | USE_SEARCH_ENGINE_API | 위 밸류값, 잠실종합운동장 위치, 네이버, 구글검색API | 20250614140606 | Y | 카페리스트 |
3 | USR001#SESS20250614001 | PRC003#AGT001#TL003 | FORMAT_RESULTS | 맛집명단리스트, 포맷팅 템플릿 스크립트 | 20250614140607 | Y | 포맷된 카페리스트 |
사용자 프로세스 툴 매핑( UserPrcToolMapp )
PK | SK | Proclist | AgentList | Toollist | GSI1_PK | GSI1_SK | |
1 | USR001 | UserPrcToolMapp#매장검색: 카페, 식당 | {“PRC001″:”위치검색”},{“PRC002″:”매장검색”}, {“PRC003″,”결과포맷팅”} | {“AGT001″:”매장검색에이전트”} | {“PRC001#AGT001#TL001″:”위치검색”},{“PRC002#AGT001#TL002″:”매장검색”}, {“PRC003#AGT001#TL003″,”결과포맷팅”} | UserPrcToolMapp | 매장검색: 카페, 식당 |
지금까지 나온 엔터티들을 하나의 테이블로 합쳐보겠습니다. DynamoDB 에서는 하나의 테이블로 여러개의 엔터티를 정의할수 있는 유연한 구조를 가지고 있고, 아래 표는 싱글 테이블 디자인의 예를 보여드립니다. 혹시 Join된 결과가 필요없다면, 각 엔티티를 하나의 DynamoDB 테이블로 생성하시면 됩니다. Primary Key와 Global Secondary Index가 아닌 일반 속성들은 EntityType 외에는 지면관계상 표시하지 않았습니다.
PK | SK | EntityType | GSI1_PK | GSI1_SK | |
1 | DMN001 | METADATA | Domain | Domain | 매장검색서비스 |
2 | AGT001 | METADATA | Agent | DMN001 | AGTSCORE#60 |
3 | AGT001 | TL001 | Tool | DMN001 | TL#USE_SEARCH_ENGINE_API |
4 | USR001 | METADATA | UserInfo | ||
5 | USR001 | SESS20250614001 | UserSession | USR001 | 잠실종합운동장 맛집 검색 세션 |
6 | USR001#SESS20250614001 | PRC001 | UserSessionProcess | USR001#Prompt | 위치검색 |
7 | USR001#SESS20250614001 | PRC001#AGT001#TL001 | UserSessPrcToolMappHist | ||
8 | USR001 | 매장검색: 카페, 식당 | UserPrcToolMapp | UserPrcToolMapp#01 | 매장검색: 카페, 식당 |
이제는 앞서 정리했던 액세스 패턴을 자세하게 정의해 보겠습니다.
사용자의 요청에 따른 적절한 도메인을 찾을수 있습니다.
- 도메인은 가장 상위 개념이므로 데이터가 많지 않습니다. 도메인 데이터를 풀스캔하면 좋겠지만, 도메인 엔터티외에 다른 엔터티까지 모두 하나의 테이블에 들어가 있는 원 테이블 디자인이라면, 풀스캔하기 어려울 것입니다. 그럴 경우 Global Secondary Index를 이용하는 방법이 있습니다. GSI1_PK라는 어트리뷰트를 만들고 여기에 엔터티 타입값인 ‘Domain’을 넣습니다. GSI1_SK에는 도메인명을 넣습니다. 다만, 사용자의 요청과 매칭되는 정보가 안나올 경우는 GSI1_PK값만으로 조회합니다. 그러면 도메인에 해당되는 GSI 인덱스의 일부만 스캔합니다.
- GSI1_PK = ‘Domain’
- (선택적) GSI1_SK begins_with(SK, ‘매장검색서비스’) : 어느정도 앞부분의 Prefix를 알고 있다면 Sort key도 활용합니다.
-
aws dynamodb query \ --table-name AgentTable \ --index-name GSI1 \ --key-condition-expression "GSI1_PK = :pk" \ --expression-attribute-values '{":pk":{"S":"Domain"}}' \ --no-scan-index-forward
하나의 도메인에 속한 여러 에이전트 중에서 스코어가 높은 에이전트를 불러올수 있습니다.
- 위와 같이 에이전트를 불러올때 에이전트가 많다면 그 중에 무엇을 골라야 할지 판단이 필요합니다. 사용자의 요청에 가장 적합하면서 검증이 된 안정적인 에이전트가 필요할 것입니다. 아래와 같이 스코어가 높은 에이전트를 우선적으로 불러와서 Amazon Nova 또는 Claude 같은 생성형 AI서비스가 검토하게 할수 있습니다.
- PK = ‘DMN001’
- SK begins_with(SK, ‘AGTSCORE’)
- 인덱스 스캔방식 : no-scan-index-forward
-
aws dynamodb query \ --table-name AgentTable \ --index-name GSI1 \ --key-condition-expression "GSI1_PK = :pk AND begins_with(GSI1_SK, :sk)" \ --expression-attribute-values '{":pk":{"S":"DMN001"},":sk":{"S":"AGTSCORE"}}' \ --no-scan-index-forward \ --limit 5
하나의 도메인에 속한 여러 에이전트 중에서 사용자의 요청에 가장 적합한 툴을 가진 에이전트를 불러올수 있습니다.
- 에이전트와 툴정보를 모두 읽어서 사용자의 요청에 가장 적합한 에이전트와 툴정보가 있는지 확인할 수 있습니다.
- GSI1_PK = ‘DMN001’
- GSI1_SK begins_with(SK, ‘TL’)
-
aws dynamodb query \ --table-name AgentTable \ --index-name GSI1 \ --key-condition-expression "GSI1_PK = :pk AND begins_with(GSI1_SK, :sk)" \ --expression-attribute-values '{":pk":{"S":"DMN001"},":sk":{"S":"TL"}}'
하나의 에이전트의 툴 정보를 전부 불러옵니다.
- 하나의 에이전트에 어떤 툴정보가 있는지 모두 조회해야 할때 사용합니다. 또는 특정 툴정보만 확인할수도 있습니다.
- PK = ‘AGT001’
- SK begins_with(SK, ‘TL’)
-
aws dynamodb query \ --table-name AgentTable \ --key-condition-expression "PK = :pk AND begins_with(SK, :sk)" \ --expression-attribute-values '{":pk":{"S":"AGT001"},":sk":{"S":"TL"}}'
특정 사용자의 모든 세션을 최신일자별로 특정 건수만큼 조회합니다.
- 사용자의 세션을 최신일자로 가져와서 애플리케이션 메인화면에 보여줍니다.
- PK = ‘USR001’
- SK begins_with(SK, ‘SESS’)
- Sort key에 날짜정보(예: ‘SESS20250614001’ )가 들어가 있으므로 최근 진행한 세션이력을 가져올수 있습니다.
- no-scan-index-forward
-
aws dynamodb query \ --table-name AgentTable \ --key-condition-expression "PK = :pk AND begins_with(SK, :sk)" \ --expression-attribute-values '{":pk":{"S":"USR001"},":sk":{"S":"SESS"}}' \ --no-scan-index-forward \ --limit 10
특정 사용자의 특정 세션의 프로세스 및 툴 실행이력을 조회합니다.
- 사용자가 예전 세션중 하나를 선택하면 세션의 프로세스 이력과 툴이력을 가져옵니다. 유저 세션정보를 파티션 키로 하고 소트키에는 ‘PRC’ Prefix값을 begin_with 연산자로 조회하면 프로세스 이력을 순서대로 가져올수 있습니다. 소트키에 ‘PRC001’,‘PRC002’와 같이 숫자값이 들어가 있기 때문에 프로세스 실행순서 대로 또는 최근 실행순서로도 가져올수 있습니다.
- PK = ‘USR001#SESS20250614001’
- SK begins_with(SK, ‘PRC’)
-
aws dynamodb query \ --table-name AgentTable \ --key-condition-expression "PK = :pk AND begins_with(SK, :sk)" \ --expression-attribute-values file://query-params.json #query-params.json 파일에 먼저 파라미터값을 저장합니다. { ":pk": {"S": "USR001#SESS20250614001"}, ":sk": {"S": "PRC"} }
특정 사용자 또는 다른 사용자의 유사한 요청에 최적화된 툴 매핑정보를 가져옵니다.
- 사용자가 비슷한 요청을 반복적으로 하게 되면, 먼저 Amazon Nova나 Claude 같은 생성형AI 서비스에게 추론할때, 이미 수행된 툴 실행이력을 바탕으로 Redshift에서 분석되어 가공된 툴 실행 매핑정보를 Context로 전달할수 있습니다. 이것을 통해서 사용자 요청에 가장 적합하고 안정화된 에이전트의 툴 조합을 실행하도록 유도합니다.
- 사용자 자신의 비슷하거나 동일한 프로세스를 검색할때
- PK = ‘USR001’
- (optional) SK begins_with(SK, ‘UserPrcToolMapp#매장검색’)
-
aws dynamodb query \ --table-name AgentTable \ --key-condition-expression "PK = :pk AND begins_with(SK, :sk)" \ --expression-attribute-values file://query-params2.json #query-params2.json 파일에 먼저 파라미터 값을 저장합니다. { ":pk": {"S": "USR001"}, ":sk": {"S": "UserPrcToolMapp#매장검색"} }
- 전체 사용자 중에서 비슷하거나 동일한 프로세스를 검색할때
- GSI1_PK = ‘UserPrcToolMapp’
- (optional) GSI1_SK begins_with(SK, ‘매장검색’)
-
aws dynamodb query \ --table-name AgentTable \ --index-name GSI1 \ --key-condition-expression "GSI1_PK = :pk AND begins_with(GSI1_SK, :sk)" \ --expression-attribute-values file://query-params2.json #query-params2.json 파일에 먼저 파라미터 값을 저장합니다. { ":pk": {"S": "UserPrcToolMapp"}, ":sk": {"S": "매장검색"} }
위 내용들을 표로 정리하면 아래와 같습니다. DynamoDB 키디자인은 이러한 형식으로 만들고 액세스 패턴들을 계속해서 리뷰를 반복해야합니다. 가능하면 관련된 모든 사람들과 놓친 액세스 패턴은 없는지, 설계한 파티션키와 소트키에는 문제가 없는지를 충분히 검토해야 합니다. 그래야 서비스가 런칭된 이후 키디자인 변경이나 추가 GSI를 만드는 비용을 최소화할수 있습니다.
액세스패턴 | Table/GSI | Key Condition | |
1 | 사용자의 요청에 따른 적절한 도메인을 찾을수 있습니다. | GSI | GSI1_PK = ‘Domain’ (optional) GSI1_SK begins_with(SK, ‘지역기반’) |
2 | 하나의 도메인에 속한 여러 에이전트 중에서 스코어가 높은 에이전트를 불러올수 있습니다. | GSI | GSI1_PK = ‘DMN001’, GSI1_SK begins_with(SK, ‘AGTSCORE’)인덱스 스캔방식 : no-scan-index-forward |
3 | 하나의 도메인에 속한 여러 에이전트 중에서 사용자의 요청에 가장 적합한 툴을 가진 에이전트를 불러올수 있습니다. | GSI | GSI1_PK = ‘DMN001’, GSI1_SK begins_with(SK, ‘TL’) |
4 | 하나의 에이전트의 툴 정보를 전부 불러옵니다. | TABLE | PK = ‘AGT001’SK begins_with(SK, ‘TL’) |
5 | 사용자의 세션을 최신일자로 가져옵니다 | TABLE | PK = ‘USR001’, SK begins_with(SK, “SESS”) 인덱스 스캔방식 : no-scan-index-forward |
6 | 특정 사용자의 특정 세션의 모든 상세 정보를 조회하면서 가장 최근에 처리한 프로세스 및 툴정보를 가져옵니다. | TABLE | PK = ‘USR001#SESS20250614001’SK begins_with(SK, ‘PRC’)인덱스 스캔방식 : no-scan-index-forward |
7 | 특정 사용자 자신의 유사한 요청에 최적화된 툴 매핑정보를 가져옵니다. | TABLE | PK = ‘USR001’(optional) SK begins_with(SK, ‘UserPrcToolMapp#매장검색’) |
8 | 전체 사용자 중에서 비슷하거나 동일한 요청에 최적화된 툴 매핑정보를 가져옵니다. | GSI | GSI1_PK = ‘UserPrcToolMapp’(optional) GSI1_SK begins_with(SK, ‘매장검색’) |
이와 같은 액세스 패턴을 통해서 특정 유저의 세션 정보 뿐만 아니라, 해당 유저의 세션에서 어떤 요청을 생성형AI에 하였고 그 결과 어떤 논리적인 프로세스를 도출했는지, 그 프로세스는 어떤 에이전트의 툴 정보를 어떤 순서로 어떤 값을 가지고 실행했고, 그 결과가 성공인지 실패인지 추적하여 특정 에이전트의 툴이 제대로 실행되었는지 분석이 가능해집니다.
Redshift 스키마 설계
에이전트와 사용자 세션등 데이터 저장 스키마 설계를 DynamoDB로 했다면, 유저세션 상세의 툴 매핑 이력정보를 Redshift에서 분석합니다. DynamoDB의 데이터는 ZeroETL기능을 연동해서 Redshift로 준 실시간으로 보낼수 있습니다. 툴 매핑이력 엔터티를 통해서 유저의 어떤 요청에 에이전트가 어떤 프로세스와 툴을 몇번 실행했고 몇번 성공했는지 정보등이 필요합니다. 또한 가장 최근 실행한 일시는 언제인지도 확인이 필요합니다. 이것은 앞서 Redshift를 에이전트에 대한 스코어를 매길때 어떤 값들을 통계로 처리하고 스코어에 반영할지 이미 언급했던 내용입니다.
- 스코어 = 총실행건수 × exp{-0.1 ×(오늘일시-최근실행일시) } × (성공건수/실행건수)²
ETL로 가져온 원본 데이터를 1차 가공하여 위 스코어를 부여하기 쉬운 형태인 스테이징 테이블로 만드는 과정이 필요합니다. 위에서 설계한 DynamoDB의 UserSessPrcToolMappHist(유저세션프로세스 툴 매핑이력)는 아래와 같은 형태로 PK, SK 외에는 Value안에 저장됩니다.
SELECT "PK","SK","value"
FROM dbagent.public."AgentTable"
WHERE "value"."EntityType"."S" = 'UserSessPrcToolMappHist'
PK | SK | Value | |
1 | USR001#SESS20250614001 | PRC001#AGT001#TL001 | {“PK”:{“S”:”USR001#SESS20250614001″},”SK”:{“S”:”PRC001#AGT001#TL001″},”TransactDT”:{“S”:”20250614140605″},”ResultMsg”:{“S”:”사용자의 요청, 잠실종합운동장 위치”},”ToolNM”:{“S”:”FIND_LOC”},”ToolValues”:{“S”:”잠실종합운동장 근처에 있는 N매장 근처의 최근에 생긴 카페를 찾아줘”},”EntityType”:{“S”:”UserSessPrcToolMappHist”},”SuccYN”:{“S”:”Y”}} |
2 | USR001#SESS20250614001 | PRC002#AGT001#TL002 | {“PK”:{“S”:”USR001#SESS20250614001″},”SK”:{“S”:”PRC002#AGT001#TL002″},”TransactDT”:{“S”:”20250614140606″},”ResultMsg”:{“S”:”카페리스트”},”ToolNM”:{“S”:”USE_SEARCH_ENGINE_API”},”ToolValues”:{“S”:”위 밸류값, 잠실종합운동장 위치, 네이버, 구글검색API”},”EntityType”:{“S”:”UserSessPrcToolMappHist”},”SuccYN”:{“S”:”Y”}} |
3 | USR001#SESS20250614001 | PRC003#AGT001#TL003 | {“PK”:{“S”:”USR001#SESS20250614001″},”SK”:{“S”:”PRC003#AGT001#TL003″},”TransactDT”:{“S”:”20250614140607″},”ResultMsg”:{“S”:”포맷된 카페리스트”},”ToolNM”:{“S”:”FORMAT_RESULTS”},”ToolValues”:{“S”:”맛집명단리스트, 포맷팅 템플릿 스크립트”},”EntityType”:{“S”:”UserSessPrcToolMappHist”},”SuccYN”:{“S”:”Y”}} |
ZeroETL을 통해 가져온 Value 어트리뷰트에 Super Type 데이터타입으로 들어가 있기때문에, 아래와 같은 쿼리로 컬럼을 분리하는 과정이 필요합니다. 아래와 같이 분석하기 편하도록 새로운 테이블에 각각의 컬럼에 맞게 데이터를 분리해서 입력합니다.
INSERT INTO user_tool_mapping_hist
SELECT SPLIT_PART("value"."PK"."S"::VARCHAR,'#',1) as UserID,
SPLIT_PART("value"."PK"."S"::VARCHAR,'#',2) as SessionID,
SPLIT_PART("value"."SK"."S"::VARCHAR,'#',1) as ProcessID,
SPLIT_PART("value"."SK"."S"::VARCHAR,'#',2) as ToolID,
"value"."ToolNM"."S"::VARCHAR as ToolNM,
"value"."ToolValues"."S"::VARCHAR as ToolValues,
"value"."TransactDT"."S"::VARCHAR as TransactDT,
"value"."SuccYN"."S"::VARCHAR as SuccYN,
"value"."ResultMsg"."S"::VARCHAR as ResultMsg,
GETDATE() as load_timestamp
FROM dbagent.public."AgentTable"
WHERE "value"."EntityType"."S" = 'UserSessPrcToolMappHist';
에이전트 툴 분석 스테이징 (agent_tool_analysis_staging)
userid | sessionid | processid | domainid | agentid | toolid | toolnm | success_flag | tool_values | result_message | |
1 | USR001 | SESS20250614001 | PRC001 | DMN001 | AGT001 | TL001 | FIND_LOC | TRUE | 잠실종합운동장 근처에 있는 N매장 근처의 최근에 생긴 카페를 찾아줘 | 사용자의 요청, 잠실종합운동장 위치 |
2 | USR001 | SESS20250614001 | PRC002 | DMN001 | AGT001 | TL002 | USE_SEARCH_ENGINE_API | TRUE | 위 밸류값, 잠실종합운동장 위치, 네이버, 구글검색API | 카페리스트 |
3 | USR001 | SESS20250614001 | PRC003 | DMN001 | AGT001 | TL003 | FORMAT_RESULTS | TRUE | 맛집명단리스트, 포맷팅 템플릿 스크립트 | 포맷된 카페리스트 |
에이전트 스코어 랭킹 (agent_score_summary)
DomainID | AgentID | AgentNM | total_executions | success_count | failure_count | last_execution_date | calculated_score | |
1 | DMN001 | AGT001 | 매장 검색 에이전트 | 2 | 1 | 1 | 0.5 | 0.02 |
2 | DMN001 | AGT002 | 매장 검색 에이전트2 | 1 | 1 | 0 | 1 | 0.05 |
사용자별 추천 에이전트 패턴 (user_process_patterns)
UserID | ProcPattern | AgentID | ToolSequence | ExecCnt | SuccCnt | Recommended | |
1 | USR002 | PRC001→PRC002→PRC003 | AGT001 | FIND_LOC→USE_SEARCH_ENGINE_API→FORMAT_RESULTS | 1 | 0 | FALSE |
2 | USR001 | PRC001→PRC002→PRC003 | AGT001 | FIND_LOC→USE_SEARCH_ENGINE_API→FORMAT_RESULTS | 1 | 1 | TRUE |
3 | USR003 | PRC001→PRC002→PRC003 | AGT002 | FIND_LOC→USE_SEARCH_ENGINE_API→FORMAT_RESULTS | 1 | 1 | TRUE |
위 테이블을 기반으로 다음과 같이 에이전트의 스코어를 매기거나, 사용자별 추천 에이전트-툴을 제안하는 등의 분석쿼리를 작성할수 있습니다.
에이전트 툴 분석 스테이징 (agent_tool_analysis_staging)
--1. 에이전트 스코어 랭킹
SELECT
'=== 에이전트 스코어 랭킹 ===' as title,
'' as DomainID, '' as AgentID, '' as agent_name,
NULL as total_executions, NULL as success_rate, NULL as calculated_score,
NULL as score_rank
UNION ALL
SELECT
'' as title,
DomainID,
AgentID,
AgentNM,
total_executions,
success_rate,
calculated_score,
RANK() OVER (PARTITION BY DomainID ORDER BY calculated_score DESC) as score_rank
FROM agent_score_summary
ORDER BY title DESC, calculated_score DESC;
--2. 사용자별 추천 에이전트 패턴
SELECT
'=== 사용자별 추천 에이전트 패턴 ===' as title,
'' as UserID, '' as agent_sequence, 0 as pattern_score,
'' as recommendation_status
UNION ALL
SELECT
'' as title,
UserID,
agent_sequence,
pattern_score,
CASE WHEN is_recommended THEN '추천' ELSE '일반' END as recommendation_status
FROM user_process_patterns
ORDER BY title DESC, pattern_score DESC;
--3.에이전트별 툴 에러 집계
WITH agent_totals AS (
SELECT AgentID, COUNT(*) as total_executions
FROM agent_tool_analysis_staging
GROUP BY AgentID
),
agent_errors AS (
SELECT AgentID, COUNT(*) as total_errors,
LISTAGG(DISTINCT ToolNM, ', ')
WITHIN GROUP (ORDER BY ToolNM) as failed_tools
FROM agent_tool_analysis_staging
WHERE success_flag = FALSE
GROUP BY AgentID
)
이러한 분석들을 통해 다음과 같은 인사이트를 얻을 수 있습니다:
- 최고 성능 에이전트가 무엇인지 실제 실행된 수행결과를 바탕으로 추천할수 있습니다.
- 사용자별로 실행된 프로세스를 분석하여, 사용자에게 가장 알맞는 프로세스와 툴 정보를 추천할 수 있습니다.
- 실패 위험이 높은 툴/패턴을 사전에 감지하여 에이전트 실행오류를 최소화 합니다.
이와 같이, 에이전트의 실행결과를 보고 사람이 에이전트에 대한 우선순위를 결정하거나 툴을 수정할 수도 있지만, 생성형AI 서비스가 리뷰할수 있는 컨텍스트로도 제공할 수 있습니다. 이를 통해서 에이전트가 발생할 수 있는 예상치 못한 오류나 잘못된 행동을 통제할 수 있는 안정적인 서비스를 만들 수 있습니다.
Valkey 스키마 설계
Valkey는 메모리 기반 캐시 서비스로서 다음과 같은 역할을 담당 합니다.
- 실시간 세션 상태 관리: 진행 중인 사용자 세션의 임시 데이터를 저장합니다. 이 데이터는 비동기로 DynamoDB에 저장됩니다.
- 핫 데이터 캐싱: 자주 조회되는 에이전트/툴 정보를 캐싱하여 빠르게 조회할수 있도록 합니다.
- 분석 결과 캐싱: Redshift에서 계산된 스코어와 사용자별 맞춤 프로세스를 추천해줍니다.
- 성능 최적화: DynamoDB에 데이터 조회나 쓰기가 집중되지 않도록, 앞단에서 가장 먼저 사용자 요청을 처리합니다.
만약 Valkey가 문제가 생길 경우에는 DynamoDB로 트래픽이 Fallback 됩니다. Valkey에서 사용하는 테이블 디자인은 모두 DynamoDB의 테이블 설계와 동일하거나 테이블 스키마의 일부 중요 키와 어트리뷰트들로도 구성할수 있습니다.
글을 마치며
이번 글에서는 에이전트 서비스에서 사용되는 여러 종류의 데이터들을 활용하는 방법을 살펴 보았습니다. 3가지 핵심 데이터베이스, Amazon DynamoDB, Amazon Redshift, Amazon Elasticache Valkey 서비스를 이용하여, 에이전트에서 사용되는 데이터들을 어떻게 저장하고, 조회하는지 설명 드렸습니다. Key-Value(키-값) 형태의 DynamoDB 및 Valkey는 프런트 서비스에서 고객의 응답에 빠르게 반응하고, 에이전트에게 필요한 컨텍스트를 생성형AI서비스에게 효율적으로 전달할수 있습니다. 또한, Redshift를 이용하여 데이터의 다양한 각도를 분석하고 여러가지 중요한 인사이트를 얻는 한편, 컨텍스트로서 생성형AI서비스에게 분석한 내용을 전달하여 에이전트 서비스가 올바르게 작동하는데 도움을 줄 수 있습니다. 이러한 과정을 통해서, 에이전트 서비스의 성능과 비용을 최적화하고, Human in the Loop를 강화하여 실패를 최소화하는 안정적인 에이전트 서비스를 만들어갈 수 있습니다. 많은 내용을 담기에는 지면이 부족하여, 위 3가지 데이터베이스의 설계와 몇가지 쿼리 예시를 중심으로 설명을 드렸습니다. 테이블 설계에 대한 자세한 스크립트는 깃허브 링크(클릭)를 참조해주세요. 이를 통해서 보다 나은 에이전트 서비스를 고민하는 분들께 조금이나마 도움이 되었으면 합니다. 감사합니다.