- 기존 ask_message_embedding은 단지 이본 질문의 벡터 값만 저장하므로 현재 프로젝트에서 작동중인 맥락 주입 부분에서 불일치가 가능함
- 최근 2턴(사용자 질문)을 다 불러와 이번 질문과 합쳐 하나의 텍스트 블록으로 만든다. 예:
[Q-2]\n[Q-1]\n[Q-now]. - 없을 시 현재 질문만 저장
- 프롬프트에 주입하는 히스토리도 2턴이므로 캐시 비교 기준과 완전히 일치해 follow-up 질문 반복 시 캐시 정확도가 올라간다.
- 임베딩은 질문당 1회만 추가로 수행되므로 비용 증가는 미미하며, 길이가 길 경우 앞 턴을 줄이는 로직을 헬퍼에서 처리한다.
- DB 스키마 변경: 중복 질문 판별 전용임을 명시하기 위해
ask_message_embedding을ask_question_cache(또는ask_duplicate_embedding)으로 리네임한다. 리네임 후speech_tone_id integer NOT NULL DEFAULT -1컬럼을 추가하고, 기존 레코드는 tone 정보가 없어 재사용 가치가 낮으므로 컬럼 추가 직후TRUNCATE또는DELETE로 전량 삭제한다. - 엔티티/레포지토리 업데이트:
ask-message-embedding.repository.ts에서MessageEmbedding타입과upsertEmbedding/findSimilarEmbeddings결과에speechToneId필드를 노출한다. - persistConversation 수정:
session-history.service.ts에서persistConversation호출 시speechTone파라미터를 새로 받아, 메시지 레포지토리에는 아무 변화 없이embeddingRepository.upsertEmbedding에만 전달한다. - 캐시 비교 및 재작성 로직:
findCachedAnswer가speechToneId를 반환하도록 수정하고,qa.service.ts/qa.v2.service.ts에서 tone ID 비교 결과에 따라 캐시 재생 또는 tone 재작성 분기를 처리한다. - 백필 전략(옵션): 추가 데이터를 이관하고 싶다면 별도 배치를 설계해
speech_tone_id를 채울 수 있지만, 초기에는 기본값-1을 tone 불명 값으로 삼고 rewrite 플로우를 따른다. - 명명 개선 검토: 테이블 리네임과 컬럼 추가는 같은 마이그레이션에서 처리하고, 관련 코드/SQL 명칭도 일괄 업데이트한다.
- 목표: 캐시에서 꺼낸 답변의 말투가 API 요청 값과 일치하면 즉시 재사용하고, 불일치하면 동일 답변을 tone 전용 LLM으로 재작성한 뒤 전달한다.
- tone 검증 순서
- (a)
findCachedAnswer가 반환한 후보 배열(유사도 기준 정렬)을 순회하며speechToneId === 요청 값인 항목을 찾는다. - (b) 같은 ID가 있는 경우 해당 후보를 즉시 재생하고, tone 재작성은 생략한다.
- (c) 같은 ID가 하나도 없으면 유사도 1순위 후보를 선택해
replace-tone.service.ts에 전달하고, tone만 바꾼 결과를 사용자에게 전송한다. (threshold 미달이면 기존처럼 새 LLM 답변 생성)
- (a)
- replace-tone.service.ts
- 시그니처:
rewriteTone(answer: string, opts: { speechToneId: number; speechTonePrompt: string; llm?: LlmOverride }). - 프롬프트 구성
- System: "너는 편집자다. 아래 콘텐츠의 의미, 사실, 구조를 훼손하지 말고, 요청된 말투 지시만 반영해 다시 작성해."
- User:
tone 지시: ${speechTonePrompt} 원문: ${answer}
- 모델/프로바이더는 운영 편의를 위해 기존 QA 파이프라인과 동일한
generate래퍼를 그대로 사용한다(즉, Ask 요청에서 선택된 LLM 설정을 재사용). temperature는 0~0.2, max_tokens는 원문 길이와 비슷하게 맞춘다. - tone 재작성 결과가 비어 있거나 원문과 지나치게 다르면 실패로 간주하고 캐시를 포기한 뒤 RAG/LLM 경로로 폴백한다.
- 시그니처:
- 서비스 연동 (
qa.service.ts,qa.v2.service.ts)findCachedAnswer가 tone ID와 함께 후보 배열을 돌려줄 수 있도록 확장하거나, tone별 우선순위를 반환한다.- tone 동일 후보가 있으면 기존
replayCachedAnswer를 실행한다. - tone 불일치만 있는 경우엔
rewriteTone호출 후, SSEanswer이벤트와persistConversation에 재작성된 텍스트를 사용하고speech_tone_id를 목표 값으로 저장한다.
- 운영 고려사항: tone ID의 기본값을
-1(unknown)으로 두고, 이 값은 tone 동일 후보 검색에서 매칭되지 않도록 처리한다. 즉, 모든 후보가-1이면 top-1 rewrite 대상으로만 사용된다.
- 중복 질문 판별 개선: 히스토리 2턴을 합친 텍스트 블록 기반으로 임베딩을 저장하고 캐시 비교에 활용한다. (상단 계획을 먼저 적용)
- 말투 ID 컬럼 추가: 위 마이그레이션 계획대로 DB 및 레포지토리를 확장해 tone 정보가 영속되도록 한다.
- 말투 조정 기능 도입:
replace-tone.service.ts구현과replayCachedAnswer통합으로 캐시 히트 시 tone 검증/재작성 플로우를 완성한다. - 후속 최적화: tone 컬럼이 채워진 이후에는 tone 일치 여부를 먼저 확인해 tone 분석/재작성 호출을 최소화한다.
- 마이그레이션
- 새 SQL 파일(예:
docs/migrations/2025-XX-ask-question-cache-tone.sql)을 작성하여 테이블 리네임(ALTER TABLE ask_message_embedding RENAME TO ask_question_cache;) → 컬럼 추가(ADD COLUMN speech_tone_id integer NOT NULL DEFAULT -1;) → 기존 데이터 삭제(TRUNCATE ask_question_cache;)를 순차 진행한다. - 필요한 경우
speech_tone_id에 인덱스(CREATE INDEX ... ON ask_question_cache(owner_user_id, requester_user_id, speech_tone_id))를 추가해 tone 별 검색을 빠르게 한다.
- 새 SQL 파일(예:
- 레포지토리 계층
ask-message-embedding.repository.ts(리네임 후ask-question-cache.repository.ts고려)MessageEmbedding/SimilarMessage인터페이스에speechToneId: number추가. 기본값-1은 별도 상수로 관리한다.upsertEmbeddingINSERT/UPDATE 문에speech_tone_id컬럼을 포함하고, 매개변수로 tone ID를 받는다.findSimilarEmbeddingsSELECT에speech_tone_id AS "speechToneId"를 추가하고, 반환 타입에 포함.
session-history.service.tspersistConversation파라미터에speechTone?: number를 추가하여 assistant 톤을 전달.embeddingRepository.upsertEmbedding호출 시 새 tone 값을 전달.findCachedAnswer가speechToneId를 함께 포함한 후보 배열을 리턴하도록 수정 (ex:{ answer, searchPlan, retrievalMeta, similarity, speechToneId }).
- 서비스 계층 (QA)
qa.service.ts/qa.v2.service.ts- 캐시 조회 결과를 tone별로 분류:
const matched = candidates.find(c => c.speechToneId === speechTone). matched가 있으면 기존replayCachedAnswer(matched)실행.- 없고 후보 배열이 존재하면
const primary = candidates[0];로 선정 후replaceTone.rewriteTone(primary.answer, speechTonePrompt)호출. - 재작성 결과 텍스트를 SSE
answer이벤트로 흘려보내고,persistConversation에speechTone값을 명시해 저장. - 재작성 여부를
DebugLogger에 남기고, 실패 시 기존 RAG/LLM 경로로 폴백한다.
- 캐시 조회 결과를 tone별로 분류:
- replace-tone.service.ts
- 최종 시그니처:
export const rewriteTone = async ( answer: string, opts: { speechToneId: number; speechTonePrompt: string; llm?: LlmOverride } ): Promise<string>. - 내부에서
generate를 호출하며, 시스템 프롬프트에 "다음 답변의 내용은 유지하고 tone만 아래 지시에 맞춰라" 구조를 사용한다. - 응답이 비거나 너무 짧으면 실패로 간주하고 오류를 throw.
- 최종 시그니처:
- SSE / 이벤트
- tone 재작성 시에도
search_plan,context이벤트는 캐시된 값 그대로 재생하고answer이벤트에만 수정된 텍스트를 전송. session_saved이벤트에cached: true와tone_rewritten: true(추가 속성) 등을 포함해 프론트에서 구분할 수 있도록 한다.
- tone 재작성 시에도
-
마이그레이션 + DB 명명 정리
docs/migrations/2025-XX-ask-question-cache-tone.sql추가: 테이블 리네임 → 컬럼 추가 → TRUNCATE → 인덱스 생성.README.md등 마이그레이션 가이드에 새 스크립트 실행 방법 추가.
-
레포지토리 계층 업데이트
- (선택)
ask-message-embedding.repository.ts파일명을ask-question-cache.repository.ts로 변경하고 import 경로 수정. - 인터페이스/쿼리에
speechToneId반영, upsert 파라미터에 tone ID 추가.
- (선택)
-
세션 히스토리 서비스 수정
persistConversation시그니처에speechTone?: number추가.embeddingRepository.upsertEmbedding호출부에 tone 전달.findCachedAnswer반환 타입을 tone 정보 포함 배열로 변경.
-
QA 서비스 캐시 로직 개편
qa.service.ts/qa.v2.service.ts에서 tone 일치 후보 우선 사용, 불일치 시replaceTone경로로 분기.- SSE 이벤트/
persistConversation에 재작성 결과 및 tone ID 반영. - DebugLogger 로깅 추가.
-
replace-tone.service.ts 신규 추가
rewriteTone함수 구현, 프롬프트 템플릿/에러 처리를 포함.- 필요 시
qa.prompts.ts에 tone 전용 프롬프트 자산 추가.