Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,4 @@ cython_debug/
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
__marimo__/
2 changes: 1 addition & 1 deletion app/models/fraud_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ class FraudResponse(BaseModel):
estimatedFraudType: str # 분류된 사기 유형
keywords: List[str] = Field(..., min_items=1, max_items=3, description="주요 위험 키워드 (최대 3개)")
explanation: str # 해당 유형으로 판단한 이유
score: float = Field(..., ge=0, le=100, description="위험도(0~100)")
score: float = Field(..., ge=0, le=70, description="위험도(0~70)")
417 changes: 398 additions & 19 deletions app/prompts/data/fraud_examples.py

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions app/prompts/data/score_rules.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
CAT01 기관 사칭형
A:30:검찰청|경찰서|금융감독원|형사기동대
B:10:사건 송치|수사 협조|조사 회신
C:10:문서번호|공문 발송|사건번호
D:10:주민등록번호|주소|성명
E:10:명의 도용|압수|보안 강화|검찰소환
F:10:bit.ly|han.gl|is.gd|vo.la|me2.do|http|https
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

http/https 범용 토큰은 과도한 오탐 위험 — 일반 링크 토큰 제거/가중치 하향 권고

여러 규칙에서 http|https 같은 범용 토큰이 포함되어 있습니다(예: 라인 7, 29, 123). 이는 거의 모든 정상 메시지에도 존재하는 신호라 오탐을 크게 높입니다. 단축 URL/의심 도메인만 유지하고, 일반 프로토콜 토큰은 제거하거나 가중치를 크게 낮추는 것을 권장합니다.

예시로 아래와 같이 최소 수정안을 제안드립니다(패턴 유지, 오탐 감소 목표).

-  F:10:bit.ly|han.gl|is.gd|vo.la|me2.do|http|https
+  F:10:bit.ly|han.gl|is.gd|vo.la|me2.do

-  D:20:han.gl|bit.ly|c11.kr|na.to|.apk|is.gd|vo.la|me2.do|http|https
+  D:20:han.gl|bit.ly|c11.kr|na.to|.apk|is.gd|vo.la|me2.do

-  C:15:단축URL|의심URL|IP링크|http|https|hxxp|hxxps|bit.ly|han.gl|is.gd|vo.la|me2.do
+  C:15:단축URL|의심URL|IP링크|hxxp|hxxps|bit.ly|han.gl|is.gd|vo.la|me2.do

추가로, 일반 링크 토큰을 반드시 유지해야 한다면 가중치를 1~2로 낮춰 다른 강한 신호와의 동시 출현에서만 임계치를 넘기도록 구성하는 것을 추천합니다.

Also applies to: 29-29, 123-123

🤖 Prompt for AI Agents
In app/prompts/data/score_rules.txt around line 7, the rule includes generic
tokens "http|https" which produce high false positives; remove the generic
protocol tokens from this rule (retain only shortener/ suspicious domains like
bit.ly|han.gl|is.gd|vo.la|me2.do) or, if protocol tokens must remain, lower
their scoring weight to 1–2 so they only contribute when combined with stronger
signals; apply the same change to the other rules referenced (lines 29 and 123)
to reduce over-detection.


CAT02 대출 사기형
A:30:은행|농협|신한저축|금융복지정책
B:15:대출진행|대환대출|생계자금|정부지원상품
C:10:무직자 가능|신용등급 무관|무서류|저금리
D:10:신분증|등본|통장사본|재직증명서
E:10:신청서.zip|앱설치|.apk|hxxp
F:5:상담신청|접수완료|한도조회|대표번호

CAT03 카드사 사칭형
A:30:KB국민카드|현대카드|롯데카드|신한카드|카드
B:15:카드 신청완료|카드 발급완료|개통완료
C:15:본인 신청 아니면 신고|본인 아닐 경우 문의|본인 아님 신고
D:10:개인정보 유출|승인 거절|이상 거래 감지
E:5:상담접수|즉시 문의|콜센터|취소 요청
F:5:Web발신|승인불가|개통내용 아닌 경우

CAT04 돌잔치 초대장형
A:35:돌잔치|첫돌|아들 돌잔치|축하해주세요|많이많이 축하
B:20:모바일 초대장|초대장을 보내드렸습니다|참석하여 주시기 바랍니다
C:5:축복해주세요|모두 축하
D:20:oa.to|vo.to|pa.to|pro.ps|.apk|bit.ly|han.gl|is.gd|http|https

CAT05 모바일 청첩장형
A:35:결혼식|재혼|웨딩홀|결혼합니다|사랑의 결실
B:20:모바일청첩장|청첩장 도착|청접장|Wedding Invitation
C:5:축하해주세요|참석 부탁|인연으로 하나됩니다
D:20:han.gl|bit.ly|c11.kr|na.to|.apk|is.gd|vo.la|me2.do|http|https

CAT06 부고문자형
A:40:별세|소천|사망하셨습니다|부고|訃告
B:10:아버님|어머님|부모님|부친|모친
C:10:삼가 고인의 명복|안타깝게도|불행하게도
D:10:장례식장|빈소|오시는 길
E:10:buly.kr|vo.la|iplogger.com|xgo.kr|gg.gg|bit.ly|han.gl|is.gd|http|https

CAT07 범칙금/과태료 부과형
A:30:도로교통법 위반|정지선 위반|어린이 보호구역
B:15:과태료 부과|벌점 통지서|고지서 발송|가압류|장기연체대상자
C:10:민원24|신고 접수|단속되어|민원내용 확인
D:10:쓰레기 무단투기|음식물분리수거 위반|층간소음행위
E:10:관세청|수입세금|관세징수과|통관번호|세금 미납 안내
F:5:paso.mobilecar.pe.kr|goosmsi.com|homes|mobilecar|bit.ly|han.gl|is.gd|http|https

CAT08 건강보험공단 사칭형
A:35:국민건강보험|건강보험공단|건강지키미
B:10:건강검진 통지서|건강검사 통보문|종합건강검진
C:15:환급금 신청마감|보험료 환급금|건강보험료 미납
D:10:확인바랍니다|조회하기|신청요망
E:10:dokdo.in|nhisis.xyz|cloud|nhis|nhi|bit.ly|han.gl|is.gd|http|https

CAT09 경찰 출석 요구형
A:30:검찰청|경찰서|법원|국정원
B:10:출석요구서|소환장|사건조회번호
C:10:형사소송건|민사소송|재산몰수 통지서
D:10:긴급출석요구|발부되었습니다|즉시 확인
E:10:사건번호|고단
F:10:sc-police.co.kr|xuto.tk|is.gd|bit.ly|han.gl|http|https

CAT10 국세청 사칭형
A:35:국세청|홈택스|국민연금공단|납세자연맹
B:10:종합소득세|연말정산|세금 미납|세금 환급금
C:15:환급 대상자|환급금 수령|금액 확인|환급 내역 조회
D:10:신청하세요|수령 확인|조회 및 납부|통지서 확인
E:10:han.gl|goo.gl|me2.do|hometax.go.kr|bit.ly|is.gd|http|https

CAT11 알바/부업 사기형
A:30:재택근무|단기 알바|쉬운 업무|문자 알바
B:15:시급 3만원|고수익 보장|일 10만 이상|당일 정산
C:10:누구나 가능|자격무관|당일 지급
D:15:오픈채팅|카톡 추가|링크 클릭|접속하세요
E:10:open.kakao.com|bit.ly|t.me|reurl.kr|han.gl|is.gd|http|https

Comment on lines +75 to +80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

오퍼 유도형(알바/투자) 규칙의 강한 미끼 단어 가중치 재점검

  • CAT11(B: “시급 3만원/고수익 보장/당일 정산”)과 CAT15(A/D/F/G: “고수익/원금보장/선착순/링크 클릭/지금 가입/마지막 기회/실시간 인증” 등)은 정상 마케팅 문구와도 일부 겹칩니다.
  • 일반 링크 토큰과 결합 시 오탐 증가가 우려됩니다.

권장:

  • 강한 상업/미끼 신호 단어의 가중치를 소폭 하향하고,
  • 단축 URL/오픈채팅/메신저 초대 링크와의 동시 출현 시 추가 가중(AND 조건 가중)을 적용.

현재 데이터 파일만으로는 AND 로직을 표현하기 어렵다면, 프롬프트/파서 레벨에서 “동시 매칭 보너스”를 구현하는 방향을 검토해 주세요.

Also applies to: 103-111

🤖 Prompt for AI Agents
In app/prompts/data/score_rules.txt around lines 75-80 (also apply same change
to lines 103-111), the weight for strong bait/commercial tokens is too high and
is causing false positives when combined with generic marketing text and links;
reduce the numeric weights for those categories slightly (e.g., lower B/C/D/E
group weights by ~20-30% or adjust the specific category labels referenced in
the file) and introduce an AND-condition bonus when short
URLs/openchat/messenger-invite tokens co-occur with bait tokens: implement a
small additional positive score in the scoring logic (not the static token list)
when both a bait category token and a short-URL/openchat token are matched
together; if the flat data file cannot represent AND logic, implement the
simultaneous-match bonus at the prompt/parser layer that consumes this file so
co-occurrence yields extra weight while individual token weights remain reduced.

CAT12 정부지원금 위장형
A:30:재난지원금|방역지원금|민생안정지원금
B:10:국세청|고용노동부|중소벤처기업부
C:10:신청마감|신속지급|신청하기 클릭
D:10:신분증 사본|본인 확인|제출서류
E:10:예산소진|원 도착|예산 초과 시 부지급
F:5:bit.ly|han.gl|is.gd|me2.do|vo.la|http|https
G:5:민생 회복|꼭 확인해주세요|확인 안 될 경우 미지급

CAT13 택배 사기형
A:30:택배|쿠팡|택배기사
B:15:주소 오류|주소 불일치|배송지 오류|주소 확인 요망
C:15:배송 지연|배송 실패|미수령 택배|통관번호 오류
D:10:bit.ly|han.gl|is.gd|vo.la|me2.do|http|https
E:10:앱 다운로드|배송조회 바로가기|주소 입력 페이지

CAT14 가상화폐 사기형
A:30:소각 예정|자산 복구|소멸 예정|휴면계정 소각
B:20:해외IP 로그인|로그인 감지|수상한 기기|기기등록 후 사용|본인이 아닐 경우|로그인 알림
C:15:ETH|BTC|이더리움|비트코인|테더|잔여 ETH
D:15:line.me|bit.ly|me2.kr|카톡|상담 바로가기|t.me|http|https

CAT15 주식투자 사기형
A:35:고수익|수익보장|원금보장|손실보상
B:15:에넥스|덕신하우징|2차전지|바이오|유진테크놀로지
C:5:bit.ly|t.me|open-kakao|open.kakao|han.gl|reurl.kr|is.gd|http|https
D:5:쿠폰 증정|단타 무료방|무료혜택|선착순 모집
E:10:단타|매수|매도|수급|테마|상장특별공급|종목추천
F:5:지금 가입|링크 클릭|바로 입장|채팅방 이동|666
G:5:마지막 기회|안정수익|실시간 인증|장마감공개

CAT16 청약 공모주 사기형
A:30:공모주|청약|상장예정|IPO|무료배정
B:15:미래에셋|신한증권|하나증권|증권사|청약담당자
C:15:할인공급가|수익률|급등|이익률
D:10:문자상담|상담전화|빠른회신|순차연락
E:5:할인혜택|무상입고|무료거부|종목확인|IR안내
F:5:두산로보틱스|이노스페이스|APR|에이피알

CAT17 허위결제 사기형
A:35:결제완료|승인요청|인증번호|해외승인|본인아님
B:15:신용카드|휴대폰결제|이니시스|모빌리언스|위메프|아마존
C:15:단축URL|의심URL|IP링크|http|https|hxxp|hxxps|bit.ly|han.gl|is.gd|vo.la|me2.do
D:15:고객센터|소비자보호|문의전화|신고요청

CAT18 가족/지인 사칭형
A:30:가족|친구|지인
B:15:폰고장|액정깨짐|임시폰|문자접속
C:15:급해|갚을게|부탁|수리중|지금바로
D:10:송금|계좌이체|기프트카드|입금요청
E:10:신분증|주민번호|카드사진|인증번호
5 changes: 2 additions & 3 deletions app/prompts/fraud_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

class FraudExample(BaseModel):
type_name: str
message_content: str
message_content: List[str]
keywords: List[str]
additional_description: str
image_content: str
additional_description: List[str]
78 changes: 45 additions & 33 deletions app/prompts/fraud_prompts.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,58 @@
from typing import List, Dict
from pathlib import Path
from app.prompts.fraud_example import FraudExample
from app.prompts.data.fraud_examples import FRAUD_EXAMPLES

SYSTEM_PROMPT = """
너는 보이스피싱 사기 탐지 전문가다.
다음 규칙표를 사용해 입력 텍스트를 분석하고,
1) 사기 유형(카테고리) 선택
2) 핵심 키워드(최소1~최대3) 추출
3) 점수는 최소 0점에서 최대 70점

[점수 규칙]
- 규칙표는 "카테고리ID 카테고리명" 한 줄 + 그 아래 여러 룰 라인으로 구성된다.
- 룰 라인 형식: "룰ID:배점:키워드1|키워드2|...".
- 텍스트에 해당 룰의 키워드 중 1개라도 의미/표현상 매칭되면 그 룰의 배점을 가산(룰별 최대 1회).
- 유저가 입력한 'keywords'에서 점수 계산을 하고, 'additionalDescription'과 'imageContent'를 참고하여 점수 계산.
- 키워드는 의미가 같으면 띄어쓰기/대소문자/오타 등과 동의어 허용.
- 동일 카테고리에서 여러 룰이 적중할 수 있으며, 합산 후 카테고리 점수는 70을 초과하지 않는다(상한 70점).
- 최종적으로 가장 점수가 높은 카테고리를 estimatedFraudType으로 선택.
- 동점이면 적중 룰 개수가 더 많은 카테고리를 선택. 그래도 동률이면 의미상 더 근접한 쪽.
- 링크/단축URL은 http/https, bit.ly/han.gl/is.gd/vo.la/me2.do 등도 매칭으로 본다(표기 변형 허용).

출력은 반드시 valid JSON 객체로만 응답. 오직 JSON만 출력.
코드블록(```), 주석, 추가 텍스트, 설명 모두 금지.
'estimatedFraudType' 에는 오로지 카테고리명만 넣을 것.
아래는 응답 예시.
{
"estimatedFraudType": "<카테고리명>",
"keywords": ["<키워드1>", "<키워드2>", "<키워드3>"],
"explanation": "<왜 이 유형이고 어떤 이유로 판단하였는지 간결히 설명>",
"score": <0~70의 실수값>
}

""".strip()


def _load_rules() -> str:
rules_path = Path(__file__).parent / "data" / "score_rules.txt"
return rules_path.read_text(encoding="utf-8")

Comment on lines +38 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

규칙 파일 매 요청 디스크 로드 — 캐싱 및 예외 처리 추가 권장

현재 매 호출마다 파일 I/O가 발생합니다. LRU 캐싱과 파일 없을 때의 명확한 예외로 신뢰성을 높이세요.

해당 범위 내 수정(diff) 및 추가 import:

+from functools import lru_cache
@@
-def _load_rules() -> str:
-    rules_path = Path(__file__).parent / "data" / "score_rules.txt"
-    return rules_path.read_text(encoding="utf-8")
+@lru_cache(maxsize=1)
+def _load_rules() -> str:
+    rules_path = Path(__file__).parent / "data" / "score_rules.txt"
+    try:
+        return rules_path.read_text(encoding="utf-8")
+    except FileNotFoundError as e:
+        raise RuntimeError(f"규칙 파일을 찾을 수 없습니다: {rules_path}") from e
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _load_rules() -> str:
rules_path = Path(__file__).parent / "data" / "score_rules.txt"
return rules_path.read_text(encoding="utf-8")
from functools import lru_cache
@lru_cache(maxsize=1)
def _load_rules() -> str:
rules_path = Path(__file__).parent / "data" / "score_rules.txt"
try:
return rules_path.read_text(encoding="utf-8")
except FileNotFoundError as e:
raise RuntimeError(f"규칙 파일을 찾을 수 없습니다: {rules_path}") from e
🤖 Prompt for AI Agents
In app/prompts/fraud_prompts.py around lines 38 to 41, the _load_rules function
currently performs disk I/O on every call and lacks clear error handling; add an
LRU cache to avoid repeated reads (e.g., import functools and decorate with
@lru_cache(maxsize=1)) and wrap the file read in a try/except that catches
FileNotFoundError (or OSError) and raises a clear exception including the
rules_path string so callers get a meaningful error; ensure you keep utf-8
encoding and add the required import(s) at the top of the file.

def get_fraud_detection_prompt(
message_content: str,
additional_description: str,
keywords: List[str],
image_content: str,
examples: List[FraudExample] = FRAUD_EXAMPLES
) -> List[Dict[str, str]]:

system = {
"role": "system",
"content": (
"당신은 사기 탐지 어시스턴트입니다. "
"입력된 텍스트를 반드시 미리 정의된 사기 유형 중 하나로 분류하고, "
"최소 1개에서 최대 3개의 주요 위험 키워드를 추출하며, 그 이유를 설명하고, "
"위험 점수(0–100%)를 제공해야 합니다.\n"
"출력은 반드시 valid JSON 객체로만 응답하세요. 아래는 응답 예시입니다:\n"
"{\n"
" \"estimatedFraudType\": \"복권 사기\",\n"
" \"keywords\": [\"키워드1\", \"키워드2\"],\n"
" \"explanation\": \"...\",\n"
" \"score\": 92.4\n"
"}\n"
)
}
rules_text = _load_rules()
example_text = "".join(build_example_lines(examples))

example_lines = build_example_lines(examples)
assistant_content = "".join(example_lines)
assistant_content += (
"이제 아래 입력을 같은 형식으로 분류하세요:\n"
)

assistant = {
"role": "assistant",
"content": assistant_content
system = {
"role": "system",
"content": f"{SYSTEM_PROMPT}\n\n[규칙표]\n{rules_text}\n\n[예시]\n{example_text}"
}

user = {
Expand All @@ -48,7 +65,8 @@ def get_fraud_detection_prompt(
)
}

return [system, assistant, user]
return [system, user]


def build_example_lines(examples):
example_lines = ["사기 유형 및 예시:\n"]
Expand All @@ -57,11 +75,5 @@ def build_example_lines(examples):
example_lines.append(f" messageContent: '{ex.message_content}'\n")
example_lines.append(f" additionalDescription: '{ex.additional_description}'\n")
example_lines.append(f" keywords: '{ex.keywords}'\n")
example_lines.append(f" imageContent: '{ex.image_content}'\n")
example_lines.append(" 출력 JSON 예시:\n")
example_lines.append(
f" {{\"estimatedFraudType\": \"{ex.type_name}\", "
f"\"keywords\": [...], \"explanation\": \"...\", \"score\": ...}}\n\n"
)

return example_lines
return example_lines
2 changes: 1 addition & 1 deletion app/services/gpt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async def call_gpt(request: FraudRequest):
response = client.responses.create(
model="gpt-4o-mini",
input = messages,
temperature = 0.5, # 생성된 텍스트의 무작위성을 결정
temperature = 0.1, # 생성된 텍스트의 무작위성을 결정
max_output_tokens = 200
)
logger.info(f"GPT API 호출 성공: {response}")
Expand Down