# AIBOT2.0 분석
- 최초 작성일: 2021년 07월 23일(금)
## 목차
[TOC]
<!-- ## 목표
- AIBOT2.0 요구사항, 고도화 개발내용을 분석해보고, 100번 기반의 K-SOE 구조/기능에대한 인사이트 도출한다. -->
## 결론
- AIBOT2.0은 100번 대화엔진이 해주는 대화흐름제어를 '봇엔진' 내에서 대화흐름을 제어한다. 그리고 의도맵에따라 시나리오를 동작시킨다. K-SOE에서 Dialogue Manager 내에서 대화흐름 및 시나리오를 제어하는 것과 유사한 형상인 거 같다. (코드 분석 필요)
- [AIBOT2.0 향후 고도화 과제](##AIBOT2.0-향후-고도화-과제) 내용은 K-SOE에도 반영이 되어야할 거 같다.
- AIBOT2.0은 의도탐침은 의도 threshold에따라 미답변(fallback), 되묻기를 통해 의도 추천을 진행한다.
- K-SOE NE 추론에서도 주소/인명/복합 NE 추론에 대한 검토 및 설계가 필요해 보인다.
- AIBOT2.0은 confirm 의도를 solr를 활용하여 기존 시나리오 의도 추론과 같은 방식으로 confirm 의도를 추론한다.
- AIBOT2.0 대화흐름제어에서 context 생성, 참조, 삭제 정책을 K-SOE에 녹이면 좋을 거 같다.
- K-SOE에도 답변노드/슬롯필링노드 내 TTS 재생 기능이 들어가면 좋을 거 같다.
- AIBOT 고도화 내용에도 3rd party NLU 연동을 위해 K-SOE AI adaptor 같은 서비스를 고려 중인 거 같다.
- K-SOE에서도 B2B 고객에게 premade / preset template 제공 방안 검토가 필요해 보인다.
- K-SOE의 Channel Manager Adaptor에서 AIBOT2.0 converter와 같은 기능을 제공하면 좋을 거 같다.
## 코드 분석
- AIBOT2.0의 가상상담 프로세스를 이해하기 위해 코드를 보며 flow를 따라가본다.
### 봇엔진 (모듈명: engine)
- 시스템 구조

- 학습/배포
- cms에서 redis의 engine이 참조하는 지식인 BotCache, RiveCache를 생성
- engine의 /learnig api를 호출하면 BotCache와 RiveCache를 참조하여, 자신의 메모리에 BotAgent 생성
- engine의 /deploy api를 호출하면, Active EngineBot과 StandBy EngineBot의 지식을 switching
- 서비스 runtime
- 상담이 시작되면 channel gateway가 engine의 /start api를 호출
- 봇 발화를 반환하기 위해 /start 호출 이후 /talk api를 호출
- 상담이 종료가 되면 /close api가 호출된다.
- 대화 처리 흐름

- Solr 구조
- engine은 3가지 종류의 profile을 필요로함.
- 3종 perofile: ACTIVE, STANDBY, TEST
- 학습(/learn)하면 STANDBY에 올라가고, 배포(/deploy)하면 ACTIVE <-> STANDBY 스위칭, 실제 가상상담 서비스는 ACTIVE profile에따라 서비스가 수행됨.
- TEST는 학습 검증을 위한 profile
- 시뮬레이터에서 테스트할 bot profile을 선택해서, 특정 profile 기준으로 서비스 테스트
```java
@PostMapping("/simulator/{profile}/start")
public Mono<EngineResult> simulatorStart(@PathVariable("profile") final BotProfile profile, @RequestBody StartTalkRequest talkRequest) {
...생략...
}
@PostMapping("/simulator/{profile}/talk")
public Mono<EngineResult> simulatorTalk(@PathVariable("profile") final BotProfile profile, @RequestBody EngineTalkRequest talkRequest) {
...생략...
}
```
- 봇 유형
```java
public enum ChatBotType implements EnumModel {
GENERAL("챗봇"),
VOC("보이스봇"),
}
```
- 봇 유형(챗봇, 보이스봇)에따라 분기를 태워 각 봇의 고유한 처리를 진행
- 노드 유형
- 노드 유형을 보고 switch-case로 분기를 쳐, 해당 노드 유형에 맞는 task 수행
```java
public enum ChatNodeType implements EnumModel {
BASIC("답변노드"),
SYNC_BASIC("Sync 답변노드"),
FUNCTION("동적노드"),
SLIDE("슬라이드 노드"),
JUMP("JUMP 노드"),
SLOT("Slot 노드"),
SYNC_SLOT("Sync Slot 노드"),
RETURN("Return 노드"),
SPLIT("조건분기 노드"),
API("API 노드"),
CALLBACK("END 노드"),
GOTO("GOTO 노드"),
SMS("SMS 전송 노드"),
COUNSEL("호전환 노드"),
CALLEND("콜종료 노드")
;
}
```
- 봇 구조
- Map<String, BotAgent> agents => botCode로 특정 BotAgent 선택 => BotAgent안에 Map<BotProfile, EngineBot>가 있음 => profile로 특정 EngineBot 선택 => EngineBot 안에 Map<String, String> botParams, Map<String, String> userParams, Map<String, String> scenarioParams 등이 있음
- 봇의 답변은 EngineBot에서 생성
- 주요 API 동작 프로세스
- cms에서 채널을 만들면, chatChannel 정보가 db에 저장되고, redis에 channelCache가 생성된다.
- cms에서 bot을 만들면, 해당 bot 정보가 db에 저장되고, redis에 botCache가 생성된다.
- cms에서 /learn을 하면 redis에 riveCache, channelCache 등이 만들어지고, /depoly를 하게 되면 STANDBY, ACTIVE rivecache 내용을 서로 바꾼다.
- /learning API

1. botCache를 redis에서 가져옴.
2. riveCache를 redis에서 가져옴.
3. botCache, riveCache 정보를 기반으로 STANDBY profile을 가지는 EngineBot 인스턴스 생성하여 메모리에 올려놓음.
- /deploy API

- STANDBY profile의 EngineBot과 ACTIVE profile의 EngineBot 인스턴스를 스위칭
- /start API

- 채널GW에서 engine이 참조하는 userSession이 만들어짐.
1. userSession을 EngineStartRequest의 sessionKey를 기준으로 redis에서 가져옴.
- userSession이 없으면 exception 발생
2. redi에서 가져온 userSession을 InheritableThreadLocal인 UserSessionContextHolder에 저장
- thread 내에서 전역변수처럼 사용할 수 있어, redis를 반복 참조하지 않게함.
3. botCode로 botAgent를 선택, ACTIVE profile로 engineBot 인스턴스를 선택, 해당 engineBot에서 INTRO에 해당하는 engineReply를 가져옴.
```java
// EngineBot.java
EngineReply reply = null;
switch (mode) {
...생략...
case INTRO:
reply = Optional.ofNullable(dialog.getIntroMessages())
.map(map -> map.get(session.getChannelCode()))
.orElse(null);
}
...생략...
return reply;
```
4. 가져온 engineReply의 노드 타입에따라 추가 작업 수행
- INTRO는 BASIC(답변노드)이므로, 추가 작업 수행하지 않음
```java
// EngineChatFacadeImpl.java
...생략...
switch (engineReply.getNodeType()) {
case BASIC:
case SYNC_BASIC:
case SLIDE:
case CALLEND:
return engineReply;
}
...생략...
```
5. engineReply를 가지고 TalkResponse 생성
6. UserSessionContextHolder 초기화
- /talk API
- /talk API 요청 유형
```java
public enum TalkRequestType implements EnumModel {
TALK("대화질의"),
ACTION("사용자 Action"),
FORM_DATA("Data 전송"),
TYPING_ON("입력중"),
TYPING_OFF("입력 종료"),
MUTE("묵음(Voice)"),
NOT_SYNC("동기화 확인되지 않음"),
SYNC_START("동기화 완료/대화 시작"),
;
}
```
1. 요청이 form data인지 판단한 후, form data라면, form data를 세션에 저장하고, 세션 데이터를 이용하여 노드를 수행하여 응답 값 생성
- 끼어들기 유형
```java
public enum InterruptMode implements EnumModel {
STANDBY("대기"),
FREEZE_CONFIRM("끼어들기 확인 상태"),
FREEZE("끼어들기 상태"),
CONFIRM("돌아가기 확인 상태"),
;
}
```

2. form data가 아닌 경우, 끼어들기 여부 확인 / 끼어들기 복귀인지 끼어들 차례인지 확인하여 끼어들기 세션 feeze/thaw 진행

3. 봇 유형이 보이스봇이면 slot-filling이 아닌 경우일 때 묵음이면, fall back or retry 의도 발화 생성

4. 묵음이 아닌 경우, slot-filling 처리 / slot-filling은 단일 slot이 있고 멀티 slot이 있다.
- slot은 solr의 NER을 사용하든지 정규표현식을 통해 slot이 채워졌는지를 판단한다.
- 멀티 slot-filling은 순회하면서 slot-filling을 진행 / 전체 slot-filling이 끝나면 confirm을 통해 고객 확인을 받을 수 있음.

5. 묵음이 아니고 slot-filling이 아니라면, 선택지 처리를 진행한다. 선택지는 postback에서 정의한 데이터와 고객 반환 데이터가 같은지를 판단하고, 같으면 선택지 내의 값을 통해 해당 노드를 실행한다.

6. 묵음, slot-filling, 선택지가 아니면, 의도/NE을 추론하여 해당 의도에 매핑된 시나리오를 수행한다.
- 의도/NE 추론은 solr를 이용한다.
- 다음 노드가 있는 경우, 봇이 반환할 값이 생성될 때까지 재귀를 돌면서 연관된 노드를 전부 실행한다.
- threshold보다 높은 의도가 추출됐을 때 의도 스케줄을 확인하고, 의도 스케줄 조건에 만족하지 못하면, 해당 의도는 실행되지 않는다.
- 여러의도가 추론됐을 때는 멀티의도에 대한 선택지를 통해 처리한다.



- /close API

- engineBot에서 close 시 reply 값을 가져온 후, reply를 처리
- 시스템 NE 추출 (SystemEntityUtils.java 참조)
- K-SOE의 시스템 NE 설계/개발 시에 활용하면 좋을 거 같다.
```java
public static void convertSystemEntity(List<NerEntity> nerEntities) {
LocalDate entityDate = null;
LocalTime entityTime = null;
for (NerEntity nerEntity : nerEntities) {
Matcher matcher = ChatConstants.ChatPatterns.ENTITY.matcher(nerEntity.getType());
String entityKey = nerEntity.getType();
if (matcher.find()) {
entityKey = matcher.group(1);
}
if (NeType.DATE.name().equals(entityKey)) {
entityDate = DateUtils.convertDate(nerEntity.getName(), DateUtils.DATE_FORMAT_SLUSH);
nerEntity.setType(String.format("@{%s}", SystemEntityType.LOCAL_DATE.getCode()));
nerEntity.setName(DateUtils.convertDate(entityDate));
} else if (NeType.TIME.name().equals(entityKey)) {
try {
entityTime = LocalTime.parse(nerEntity.getName());
nerEntity.setType(String.format("@{%s}", SystemEntityType.TIME.getCode()));
nerEntity.setName(DateUtils.convertTime(entityTime));
} catch (CustomException e) {
log.error("Exception, {}", e.getMessage());
} catch (Exception e) {
log.error("Exception, {}", e.getMessage());
}
} else if (NeType.NUMBER.name().equals(entityKey) && NumberUtils.isDigits(nerEntity.getName())) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.NUMBER.getCode()));
} else if (NeType.CONFIRM.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.CONFIRM.getCode()));
} else if (NeType.PHONE.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.PHONE.getCode()));
} else if (NeType.ADULT_NO.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.NUMBER_ADULT.getCode()));
} else if (NeType.CHILD_NO.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.NUMBER_KIDS.getCode()));
} else if (NeType.CAR.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.CAR_NO.getCode()));
} else if (NeType.CARD.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.CARD.getCode()));
} else if (NeType.BIZ.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.BIZ_NO.getCode()));
} else if (NeType.AM_PM.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.AM_PM.getCode()));
} else if (NeType.STOCK.name().equals(entityKey)) {
nerEntity.setType(String.format("@{%s}", SystemEntityType.STOCK.getCode()));
}
}
}
````
- 최초 기동 시 in-memory에 인스턴스 생성 (ChatServiceImpl.java) 참조
- K-SOE에서도 redis를 rdb cache로 사용을 하고, 실제로 run time 시에 참조하는 데이터는 in-memory의 객체 인스턴스로 사용하면 좋을 거 같다.
```java
private void initialize(BotCache botCache) {
log.info("Create RSBotInstance, botCode={}, name={}", botCache.getId(), botCache.getName());
if (agents.containsKey(botCache.getId())) {
log.info("Already exist rsBotInstance, code={}, name={}", botCache.getId(), botCache.getName());
return;
}
// 최초 기동 시 전체 봇에 대하여 Memory에 학습한다.
synchronized (this) {
try {
BotAgent botAgent = new BotAgent(riveCacheService);
agents.put(botCache.getId(), botAgent);
// 1. Active
this.learn(botCache.getId(), BotProfile.ACTIVE);
// 2. Standby
this.learn(botCache.getId(), BotProfile.STANDBY);
// 3. TEST
this.learn(botCache.getId(), BotProfile.TEST);
} catch (CustomException e) {
log.error("CustomException, {}", e.getMessage());
} catch (Exception e) {
log.error("Exception, {}", e.getMessage());
}
}
}
```
- EngineReply(노드) 구성

- BotAgent 구성
- 파라미터 구조

- reply API
reply API는 EngineBot의 ChatDialog가 저장하고 있는 의도와 상황에 맞는 응답 문구를 전달하는 API로 /talk API에서 사용자 발화에 따른 응답 문구를 출력하기 위한 API이다.
- INTENT 모드

- DIALOG 모드

- 그 외 모드

로직을 확인해보면 DIALOG 모드에서 발화에 대응하는 응답 메세지를 전달하고 나머지 모드는 봇이 설정으로 가지는 메시지를 전달하게 된다.
- processReply 메소드
processReply 메소드는 현재 상태에서 들어온 발화에 따라 처리해야 하는 로직을 구현화한 메소드로
/talk API에서 호출하여 현재 노드 상태를 처리하고 EngineBot의 reply 메소드를 통해 봇발화를 전달한다.
호출은 내부에서 노드가 reply return 노드가 될 때까지 다음 노드를 재귀 호출하는 방식으로 진행한다.
- 일반 노드

- FUNCION, API, SPLIT 노드

- RETURN, COUNSEL, JUMP 노드

- SLOT 노드



1. slot 노드가 multislot 인 경우 slot entity 마다 확인하여 상태가 호전환, 종료, 재질의 상태에 따라 처리
에러가 없으면 multislot이 다 채워져 있으면 다음 노드로 진행
2. multislot이 아닌 경우 빈 슬롯이 있는지 확인 후 빈 슬롯이 있으면 재질문, 다 채워져 있으면 다음 노드로 진행
- CALLBACK, GOTO 노드

## AIBOT2.0 구조
### 메인 서비스
- 채널 GW (모듈명: gateway)
- 다양한 채널 수용
- 봇엔진 (모듈명: engine)
- 의도엔진
- solr 기반으로 의도추론, 개체식별, 자동완성
- 대화흐름제어
- 의도 흐름 제어
- 슬롯 필링
- 시나리오 흐름 제어
- 대화 context 관리
- 시나리오엔진
- 시나리오 수행
- CMS (모듈명: cms)
- 봇관리
- 지식관리
- 사전관리
- 로그 관리
- 통계 관리
- NLU 학습
- NLU 검증
- 시뮬레이터
- 대화로그 관리 및 분석 (모듈명: scheduler)
- 대화로그, 대화통계 등
### 대화흐름제어

- /start 때 context 생성
- /start에서 intro 의도를 통해 인사말 제공
- 매 /talk 요청마다 NLU 분석
- 고객의 발화의 의도 점수 threshold에 따른 되묻기/미답변/묵음 발화 처리
- /talk 중 context의 의도, NE, 대화상태 등 정보 갱신
- 시나리오 파라미터로 이동 후, context에 있는 객체는 초기화
- 의도맵 내 의도 스케줄에따라 의도 활성화 여부 체크, 활성화된 의도라면 의도에 매핑된 시나리오 수행
- 슬록 필링 단계에 끼어들기가 세팅되어 있다면, 새로운 의도가 식별되었을 때 새로운 시나리오로 이동
- 답변이 reconfirm이면 suggestion 정보를 context에 저장하고, reconfirm 진행 (정확히는 모르겠음. 코드 분석 필요)
### 대화엔진 구조

### 봇 구축 프로세스
- premade용 봇 생성
- 고객사 정보 등록
- 고객사용 봇 생성
- premade된 봇을 복사해 사용하거나 blank부터 생성해 사용
- 서비스할 채널 정보 등록
- 시나리오 저작
- 봇 학습 및 배포
- 서비스 개시
## AIBOT2.0 추가된 기능
- 멀티 슬롯 필링
- Auto 슬롯 필링 지원, 한번에 여러개 슬롯 필링 지원
- Confirm 의도 기능
- 긍정, 부정, 중립에 대한 예시 문장을 등록하고 학습하는 형식으로, confirm 의도를 추론
- TTS 재생 기능
- 답변, 슬롯필링 노드 내 발화 문구 TTS 재생
- SMS, 콜종료, 호전환, GOTO 노드 추가
- 되묻기 및 끼어들기 기능
- 대화 진행 중 새로운 의도가 파악되면 새로운 시나리오 수행
- 원래 의도로 다시 돌아올 것인지, 아닌지 선택 가능
- 신규 의도 완료 후, 원래 의도로 다시 돌아올 것인지 되묻기 수행
- 시스템에서 사전에 정의한 개체
- 날짜, 시간, 인원, 숫자 등 17종
- Premade / Preset 템플릿 기능
- 특정 도메인에 대한 시나리오 샘플
- premade: 미리 특정 도메인에 대한 bot template을 복사하는 기능
- preset: 미리 정의된 시나리오 template을 복하는 기능
- SMS 노드에서 사용할 SMS 템플릿 제공
- 일정시간 동안 고객 묵음이 들어오면 호전환/콜종료 처리 기능
- 자유발화 기능
- 슬롯필링 노드에서 '길게 말하기'를 체크하면, 고객 답변을 기다리는 시간이 길어짐.
- 일괄 대화 검증
- 질문 목록 엑셀을 업로드하면, 의도 및 score 값을 엑셀로 다시 내려 받을 수 있으
- 외부 api 연동
- CMS를 통해 open api meta 정보를 DB에 등록
- 신규 개발한 open api adapter를 통해 외부 open api 호출
- 의도 schedule에 따른 의도 활성화
- DTMF 추가
- TTS digit, pause 태그 추가
- CMS 봇 목록 노출 UI 개선
- Solr 품질 개선
- 구축형을 위한 패키징
- OS/MW 설치, 배포, config 설정 방법 가이드 정리
## AIBOT2.0 향후 고도화 과제
- 의도 추론 고도화
- 끊어쳐진 의도 추론
- DL 기반 의도 추론
- 멀티 의도 처리
- 여러개의 의도가 파악되면, config에따라 1개의 의도 수행 후 다음 의도 이어서 수행
- NE 추론 고도화
- 복합/Set/Collection NE
- 주소 개체명 추출
- 인명 개체 추출 정확도 향상
- 3rd party NLU 연동
- 의도 추론
- 개체인식
- 외부 api 테스트, 통계 기능/화면 개발
- API 실시간 테스트
- API 연동 이력 및 통계 화면
- 서비스 container화
- sync view (미정)
- 보이스봇과 화면을 동시 제공
- 시나리오 저작도구에서 화면 설계 지원
## AIBOT2.0 질문사항 및 2차 세미나 주제
### 1차 세미나 후 작성
- START API에서 최초 봇 인사말 생성하는지?
- Talk API호출 시, 발화와 의도맵 매핑 방법
- 대화흐름제어(탐침?의도컨펌? 시나리오점프??)와 시나리어 흐름제어 정의/방법/ 판단 조건 설정?
- 시나리오 진행중일 때 대화흐름제어의 역할
- Redis에 저장되는 Context Data 구조 - 해당 Context를 이용하여 할수 있는 일?
- NE변경하여 이전 시나리오 재진입이 되는가?
- Context기반 요약 정보 생성 가능한가?
- 재발화요청 수행?
- 의도 파악 전에 발화/파악된 NE를 보관하고 시나리오 진입 시 jump하거나 컨펌할 수 있는지?
- 대화흐름제어 - 의도추론
- 묵음시 되묻기 문장이, CMS에 설정된 재질문 처리문장이 나가지 않고, 진행중이던 슬롯필링 문장이 한번 더 나가는 식으로 구성 가능?
- 식별한 개체가 시나리오답변과 다를 때의 여러 케이스(1. 다른의도로 jump, 2.개체를사용자가 잘못 말함 3. 상담사연결요청 ..) 가 보여준 flow내에서 모두 처리 가능한지?
- 되묻기 탐침이 실행될 때, 추천의도를 자세히 발화할 순 없는지(낮은 컨피던스 이나 발화에서 추출된 경우. 발화한 lob를 연계한 의도)
- Multi 의도/ 끊어치기 대화의도 추론 ==> Flow 고도화 필요함
- 대화흐름 제어 내에서 스코어링과 우선순위 설정 --> 로직 처리 필요
- 시나리오 진행시 ia 응답 delay 시 대화 처리 로직
- 제공하는 시스템 객체의종류는 무엇이 있는가 ? Pattern이 아닌 DL/ML기반으로 제공,확장 가능한가?다른 Global 에서는?
- --> 날짜(일시/연도/월/일/요일/시간/날짜/시간/분), 고객명, 컨펌, 차량번호, 숫자, 전화번호, 카드번호, 사업자 등록번호, 피플카운트, 오전오후
- 긍정/부정/ 중립을 판단을 통해 Confirm 할때 ML 학습 데이터는??
- 모든 발화에 대해서 식별과정은 거침
- entity 구조는 hierarchy가 있는가?
- vux 제어 흐름 - 픽스된 형태
### 추가 작성
- AIBOT 2.0 B2B 챗봇 딜러버리 lessons learned
- SaaS, 구축형, gcloud 등
- 레가시 타임아웃 시 처리 방식
- end에서 수행하는 후처리는 어떤 게 있는지?
- 감성분석, 목소리인증 등의 event는 어떻게 처리되는 것인지?
- 3rd party NLU 연동 시 자동완성 기능은 어떻게 처리되는 것인지?
- confirm은 의도처럼 처리되는데, confirm NE는 어떤 패턴으로 추출되는 것인지?
- context 정보를 이용하는 예시