From 0a91f524adf5c6db91a754ca15dcb34a5440b1b8 Mon Sep 17 00:00:00 2001 From: chan000518 Date: Mon, 17 Nov 2025 18:44:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9E=90=EA=B0=80=20=EB=8B=AC=EB=9D=BC?= =?UTF-8?q?=EB=8F=84=20=EC=BA=90=EC=8B=9C=EB=90=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/history-tasks/ASK_DUPLICATE_CACHE.md | 2 +- src/repositories/ask-question-cache.repository.ts | 6 ++---- src/services/qa.service.ts | 1 - src/services/qa.v2.service.ts | 1 - src/services/session-history.service.ts | 3 --- 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/history-tasks/ASK_DUPLICATE_CACHE.md b/docs/history-tasks/ASK_DUPLICATE_CACHE.md index e6ac36f..f81f63d 100644 --- a/docs/history-tasks/ASK_DUPLICATE_CACHE.md +++ b/docs/history-tasks/ASK_DUPLICATE_CACHE.md @@ -21,7 +21,7 @@ - `persistConversation`(`session-history.service.ts:68`)이 answer tone과 중복 질문 벡터를 함께 `ask_question_cache`에 upsert한다. ### 3. tone-aware 캐시 조회 -- `findCachedAnswer`(`session-history.service.ts:146`)가 owner, requester, post, category 조건에 맞는 후보를 tone ID와 함께 돌려준다. +- `findCachedAnswer`(`session-history.service.ts:146`)가 owner, post, category 조건에 맞는 후보를 tone ID와 함께 돌려준다. `requester_user_id` 필터를 없애 동일 글의 관리자는 여러 상담 채널/요청자 간에도 캐시를 재활용할 수 있다. - `selectToneAwareCacheCandidate`(`session-history.service.ts:36`)가 요청 tone과 동일한 후보를 고르고, 없으면 top-1 후보를 rewrite 대상으로 지정한다. - `qa.service.ts:192`, `qa.v2.service.ts:175`에서 tone이 맞는 캐시는 그대로 재생하고, tone이 다르면 rewrite 경로로 분기한다. diff --git a/src/repositories/ask-question-cache.repository.ts b/src/repositories/ask-question-cache.repository.ts index 310c4b5..fb93b8c 100644 --- a/src/repositories/ask-question-cache.repository.ts +++ b/src/repositories/ask-question-cache.repository.ts @@ -112,7 +112,6 @@ export interface SimilarMessage { export interface SimilarSearchParams { ownerUserId: string; - requesterUserId: string; embedding: number[]; postId?: number | null; categoryId?: number | null; @@ -121,14 +120,13 @@ export interface SimilarSearchParams { export const findSimilarEmbeddings = async ({ ownerUserId, - requesterUserId, embedding, postId, categoryId, limit = 3, }: SimilarSearchParams): Promise => { - const filters = ['owner_user_id = $2', 'requester_user_id = $3']; - const values: unknown[] = [pgvector.toSql(embedding), ownerUserId, requesterUserId]; + const filters = ['owner_user_id = $2']; + const values: unknown[] = [pgvector.toSql(embedding), ownerUserId]; if (postId != null) { values.push(postId); diff --git a/src/services/qa.service.ts b/src/services/qa.service.ts index 52ab7e9..1e2eda3 100644 --- a/src/services/qa.service.ts +++ b/src/services/qa.service.ts @@ -182,7 +182,6 @@ export const answerStream = async ({ const cachedAnswerList = duplicateQuestionEmbedding ? await sessionHistoryService.findCachedAnswer({ ownerUserId, - requesterUserId, embedding: duplicateQuestionEmbedding, postId: postId ?? undefined, categoryId: categoryId ?? undefined, diff --git a/src/services/qa.v2.service.ts b/src/services/qa.v2.service.ts index dd1c8f2..3949d27 100644 --- a/src/services/qa.v2.service.ts +++ b/src/services/qa.v2.service.ts @@ -165,7 +165,6 @@ export const answerStreamV2 = async ({ const cachedAnswerList = duplicateQuestionEmbedding ? await sessionHistoryService.findCachedAnswer({ ownerUserId, - requesterUserId, embedding: duplicateQuestionEmbedding, postId: postId ?? undefined, categoryId: categoryId ?? undefined, diff --git a/src/services/session-history.service.ts b/src/services/session-history.service.ts index 7621a96..acd1124 100644 --- a/src/services/session-history.service.ts +++ b/src/services/session-history.service.ts @@ -136,7 +136,6 @@ export interface CachedAnswerResult { export interface FindCachedAnswerParams { ownerUserId: string; - requesterUserId: string; embedding: number[]; postId?: number; categoryId?: number; @@ -145,7 +144,6 @@ export interface FindCachedAnswerParams { export const findCachedAnswer = async ({ ownerUserId, - requesterUserId, embedding, postId, categoryId, @@ -153,7 +151,6 @@ export const findCachedAnswer = async ({ }: FindCachedAnswerParams): Promise => { const candidates = await questionCacheRepository.findSimilarEmbeddings({ ownerUserId, - requesterUserId, embedding, postId: postId ?? null, categoryId: categoryId ?? null, From eb989cd6bf8b2f87569f144155f85cf743070a44 Mon Sep 17 00:00:00 2001 From: chan000518 Date: Mon, 17 Nov 2025 19:29:08 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/post.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/repositories/post.repository.ts b/src/repositories/post.repository.ts index 7209a4c..d7ba102 100644 --- a/src/repositories/post.repository.ts +++ b/src/repositories/post.repository.ts @@ -419,8 +419,8 @@ export const findSimilarChunksGlobalANN = async (params: { values.push(topK); let orderBy = 'similarity_score DESC'; - if (params.sort === 'created_at_desc') orderBy = 'similarity_score DESC, bp.created_at DESC'; - if (params.sort === 'created_at_asc') orderBy = 'similarity_score DESC, bp.created_at ASC'; + if (params.sort === 'created_at_desc') orderBy = 'similarity_score DESC, created_at DESC'; + if (params.sort === 'created_at_asc') orderBy = 'similarity_score DESC, created_at ASC'; const sql = ` WITH nn AS ( From 02875d38812f73a38139f0a2487bb8ba6717fa3a Mon Sep 17 00:00:00 2001 From: chan000518 Date: Mon, 17 Nov 2025 19:44:52 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=ED=94=84=EB=A1=AC?= =?UTF-8?q?=ED=94=84=ED=8A=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/prompts/qa.prompts.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/prompts/qa.prompts.ts b/src/prompts/qa.prompts.ts index dfea2cd..16e4265 100644 --- a/src/prompts/qa.prompts.ts +++ b/src/prompts/qa.prompts.ts @@ -80,7 +80,11 @@ ${speechTonePrompt} [응답 규칙] 1. 규칙과 말투를 지켜 답하고, 질문과 관련되어 블로그 이용자가 더 살펴볼 만한 포스트·카테고리를 함께 추천한다. 2. 욕설·비난·무관·부적절한 질문은 사과 후 블로그 관련 질문만 응답 가능함을 알리고, 친절하게 탐색 가능한 영역을 안내한다. -3. 블로그 주제에는 맞지만 관련 글이 없거나 대답하기 부족하면 부족함을 알린다.`; +3. 블로그 주제에는 맞지만 해당 글이 없거나 대답하기 부족하면 부족함을 알리고 4번 규칙을 무시하며 대화를 마무리 한다. +3-1. 질문에 완벽히 부합하지 않으면 관련된 부분을 근거로 답변하되, 부족함을 알린다. +4. 질문의 주제 및 대상이 \’일반적인 개념\’과 \’블로그 포스트 개념\’으로 두가지 이상의 의미를 갖고 있을 경우, \’블로그 포스트 관련 개념\’ 위주로 가장 먼저 설명하고, 마지막으로 ’일반적인 개념\’을 간단하게 소개한다. 이때, 두 개념이 분리되어 정리되게 나타나도록 가독성을 높인다. +5. \’일반적인\ 개념’이 존재하지 않고 관련 주제, 카테고리, \’블로그 포스트 개념\’이 존재하는 경우, 그 개념을 기반으로 대답을 하고 마지막으로 해당 게시글이 어떤 글인지 알리며 질문을 유도하고 대화를 마무리한다.`; + const userMessage = `[context] ${topicHint} @@ -191,8 +195,10 @@ ${speechTonePrompt} [응답 규칙] 1. 규칙과 말투를 지켜 답하고, 질문과 관련되어 블로그 이용자가 더 살펴볼 만한 포스트·카테고리를 함께 추천한다. 2. 욕설·비난·무관·부적절한 질문은 사과 후 블로그 관련 질문만 응답 가능함을 알리고, 친절하게 탐색 가능한 영역을 안내한다. -3. 블로그 주제에는 맞지만 관련 글이 없거나 대답하기 부족하면 부족함을 알린다. -4. 관련 주제나 카테고리가 있을 경우 안내하고 질문을 유도한다`; +3. 블로그 주제에는 맞지만 관련 글이 없거나 대답하기 부족하면 부족함을 알리고, 이후 규칙을 무시하며 대화를 마무리 한다. +3-1. 질문에 완벽히 부합하지 않으면 관련된 부분을 근거로 답변하되, 부족함을 알린다. +4. 질문의 주제 및 대상이 \’일반적인 개념\’과 \’블로그 포스트 개념\’으로 두가지 이상의 의미를 갖고 있을 경우, \’블로그 포스트 관련 개념\’ 위주로 가장 먼저 설명하고, 마지막으로 ’일반적인 개념\’을 간단하게 소개한다. 이때, 두 개념이 분리되어 정리되게 나타나도록 가독성을 높인다. +5. \’일반적인\ 개념’이 존재하지 않고 관련 주제, 카테고리, \’블로그 포스트 개념\’이 존재하는 경우, 그 개념을 기반으로 대답을 하고 마지막으로 해당 게시글이 어떤 글인지 알리며 질문을 유도하고 대화를 마무리한다.`; const retrievalSummary = buildRetrievalSummary(options?.retrievalMeta, similarChunks.length);