From 36cb847e3867fcf9edce9054a94bdc0400153782 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Wed, 30 Sep 2026 13:22:55 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=80=EC=83=81=20=EB=82=B4=EB=8B=B4?= =?UTF-8?q?=EC=9E=90=20=EC=B6=9C=EB=A0=A5=20=EA=B0=80=EB=93=9C=EB=A0=88?= =?UTF-8?q?=EC=9D=BC=EC=9D=98=20=EC=88=98=EB=8B=A8=EC=96=B4=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EB=AC=B8=EC=9E=90=EC=97=B4=20=EC=98=A4=ED=83=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "독"·"방법은"이 "고독"·"독립"·"다른 방법은" 같은 정서 표현을 차단해 재시도 없는 스트림 턴이 실패하던 문제를 구체 조합 13개로 좁혀 해결한다. --- apps/api/app/services/guardrail.py | 8 +- apps/api/app/test_guardrail_means_terms.py | 92 ++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 apps/api/app/test_guardrail_means_terms.py diff --git a/apps/api/app/services/guardrail.py b/apps/api/app/services/guardrail.py index 2ca1b46..a7f0618 100644 --- a/apps/api/app/services/guardrail.py +++ b/apps/api/app/services/guardrail.py @@ -886,10 +886,16 @@ def classify_crisis(text: str, *, speaker_is_persona_context: bool = True) -> Cr # 자살/자해 '수단·방법' 정보 패턴 (절대 출력 금지). 정서 표현은 허용. # 1.1 확장(2026-07-15): DBT 자해 페르소나 대비 자해 도구·방법 어휘 보강. # 과차단 주의 — "선을 긋다"(경계 설정) 같은 정상 상담 표현과 겹치지 않는 조합만 추가. +# 1.2 축소·확장(2026-09-29): "독"·"방법은" 부분문자열이 "고독"·"독립"·"독서"·"독특"· +# "다른 방법은"·"표현 방법은" 같은 정상 정서 표현을 오탐 차단하여 제거하고, +# 실제 수단 정보만 가리키는 구체 조합으로 대체. _MEANS_TERMS = [ "약을 얼마나", "몇 알", "몇 정", "투신", "목을 매", "목 매", "번개탄", "농약", - "손목을", "칼로", "뛰어내리", "수면제 ", "독", "방법은", "어떻게 죽", + "손목을", "칼로", "뛰어내리", "수면제 ", "어떻게 죽", "커터", "면도날", "손목 긋", "깊게 그으", "라이터로 지", + "독약", "독극물", "음독", "독을 먹", "독을 마시", "독을 타", + "죽는 방법", "죽을 방법", "죽는 법", "자살 방법", "자살하는 방법", + "자해 방법", "자해하는 방법", ] _DISPLAY_PLACEHOLDER_LABELS = { "COUNSELOR": "상담자", diff --git a/apps/api/app/test_guardrail_means_terms.py b/apps/api/app/test_guardrail_means_terms.py new file mode 100644 index 0000000..3083f53 --- /dev/null +++ b/apps/api/app/test_guardrail_means_terms.py @@ -0,0 +1,92 @@ +"""_MEANS_TERMS 부분문자열 오탐 회귀 테스트. + +"독"·"방법은" 같은 넓은 부분문자열이 "고독"·"독립"·"독서"·"독특"· +"다른 방법은"·"표현 방법은" 같은 정상 정서 표현을 오탐 차단하지 않는지, +그리고 실제 수단 정보(독약·죽는 방법 등)는 여전히 차단되는지 검증한다. +""" + +from __future__ import annotations + +import unittest + +from .services import guardrail + + +class GuardrailMeansTermsFalsePositiveTest(unittest.TestCase): + def test_normal_expressions_are_not_blocked(self) -> None: + texts = [ + "요즘 너무 고독해요.", + "독립하고 싶은데 겁이 나요.", + "독서도 손에 안 잡혀요.", + "제가 좀 독특한가 봐요.", + "다른 방법은 잘 모르겠어요.", + "제 마음을 표현 방법은 모르겠어요.", + ] + + for text in texts: + with self.subTest(text=text): + result = guardrail.sanitize_client_reply( + text, + ideation_stage=1, + turn_seq=2, + ) + + self.assertFalse(result.blocked, text) + self.assertFalse(result.needs_regeneration, text) + self.assertEqual(result.reasons, []) + + +class GuardrailMeansTermsNewEntriesTest(unittest.TestCase): + def test_new_means_terms_are_blocked(self) -> None: + cases = { + "독약": "독약을 구할 수 있을까요.", + "독극물": "독극물을 사서 마시고 싶어요.", + "음독": "음독을 하면 편해질까요.", + "독을 먹": "그냥 독을 먹으면 끝날 것 같아요.", + "독을 마시": "독을 마시는 상상을 해요.", + "독을 타": "물에 독을 타 볼까 생각했어요.", + "죽는 방법": "가장 고통 없이 죽는 방법이 뭘까요.", + "죽을 방법": "확실하게 죽을 방법을 찾고 있어요.", + "죽는 법": "편하게 죽는 법을 알고 싶어요.", + "자살 방법": "자살 방법을 검색해봤어요.", + "자살하는 방법": "자살하는 방법을 알려줄 수 있나요.", + "자해 방법": "자해 방법을 바꿔볼까 해요.", + "자해하는 방법": "덜 아프게 자해하는 방법이 있을까요.", + } + + for term, text in cases.items(): + with self.subTest(term=term): + result = guardrail.sanitize_client_reply( + text, + ideation_stage=1, + turn_seq=2, + ) + + self.assertTrue(result.blocked, text) + self.assertTrue(result.needs_regeneration, text) + self.assertIn(f"means_info:{term}", result.reasons) + + +class GuardrailMeansTermsStillBlockedTest(unittest.TestCase): + def test_existing_terms_remain_blocked(self) -> None: + cases = { + "번개탄": "번개탄을 피우고 싶다는 생각이 들어요.", + "목을 매": "방에서 목을 매는 상상을 해요.", + "뛰어내리": "옥상에서 뛰어내리고 싶어요.", + } + + for term, text in cases.items(): + with self.subTest(term=term): + result = guardrail.sanitize_client_reply( + text, + ideation_stage=1, + turn_seq=2, + ) + + self.assertTrue(result.blocked, text) + self.assertTrue(result.needs_regeneration, text) + self.assertIn(f"means_info:{term}", result.reasons) + + +if __name__ == "__main__": + unittest.main()