문장 하나가 ① 규칙 검사(정규식) → ② LLM 판별(구조화 출력) → 모델 → ③ 출력 가드 중 어느 층에서 걸리고 어느 층을 통과하는지 한 단계씩 따라갑니다. 공격 문장, 정상 문장, 규칙을 빠져나가는 공격, 개인정보가 담긴 응답, 공격·정상 세트 채점을 탭으로 나눴습니다. 규칙 검사와 마스킹 결과는 함수 본문을 떼어 실제로 돌린 값이고, LLM 판별과 모델 응답은 (예시) 입니다.
| 층 | 자리 | 보는 것 | 비용 · 성질 | 걸리면 |
|---|---|---|---|---|
| ① 규칙 | rule_check (guards_input.py) | 정규식 8개, 금지 주제 문자열 4개. 처음 걸린 것에서 멈춤 | 공짜, 빠름, 같은 입력엔 항상 같은 결과. 적어 둔 표현만 잡음 | (True, "[규칙] …"). LLM 판별을 부르지 않음 |
| ② LLM | llm_check (guards_input.py) | 표현이 아니라 의도. InjectionCheck(is_injection, confidence, reason) | 요청마다 모델 호출 1번. 판별 모델도 틀리거나 속을 수 있음 | is_injection 이고 confidence > 0.7 일 때만 (True, "[LLM] …"). 예외면 통과(fail-open) |
| 거절 | refusal_message (guards_refusal.py) | 사유에 "금지 주제" 가 있는지 | 모델이 쓰지 않는 고정 문자열이라 주입될 수 없음 | 종류별 고정 문구 AIMessage. 사유는 guard_audit.log 에만 |
| ③ 출력 | OutputGuardrailMiddleware.after_model | tool_calls 없는 최종 응답만. PII → 금지 주제 → 근거(LLM) 순서, 걸리면 즉시 return | 3단계만 모델 호출. 판별 모델은 응답 본문만 받음 | 부분 마스킹 / 통째로 고정 문구 / 안내 문구 덧붙임. 모두 같은 id 로 교체 |
입력 가드는 before_model 이 아니라 wrap_model_call 입니다. before_model 은 State 를 고칠 수는 있어도 모델 호출 자체를 대신할 수 없지만, wrap 은 실제 호출을 하는 handler 를 쥐고 있어 부르지 않기로 할 수 있습니다. 그래서 차단된 요청에는 본 모델 비용이 들지 않고 공격 문장이 본 모델에 닿지도 않습니다.
rule_check 와 패턴 목록만 그대로 복사해 실행한 결과입니다. LLM 을 부르지 않으므로 항상 같습니다. 정상 세트 10문항은 모두 규칙을 통과했습니다(오탐 0). 따라서 정상 세트에서 오탐이 나온다면 LLM 판별에서 나온 것입니다.
[guard] LLM 검사 실패, 규칙 결과만 사용 을 찍고 통과입니다. 가용성을 택한 정책이고, 보안이 우선이면 차단(fail-closed)으로 바꿔야 합니다.wrap_model_call 이라 도구를 쓰는 질문이면 같은 사용자 메시지를 매번 다시 검사하고, 규칙을 통과한 입력은 매번 LLM 판별까지 갑니다.check_grounding 까지 가서 판정용 호출이 한 번 더 일어날 수 있습니다.AIMessage(content=…) 로 바꾸면 tool_calls 가 사라져 도구가 실행되지 않고 HITL 승인도 걸리지 않습니다. 75~76줄이 그것을 막습니다. 또 출력 가드는 모델 응답만 보므로 ToolMessage 에 든 개인정보는 State 에 그대로 남습니다.PII_PATTERNS, mask_pii, FORBIDDEN_TOPICS 가 마스킹·입력·출력 파일에 각각 있습니다. 또 refusal_message 는 사유 문자열의 "금지 주제" 라는 표현으로 종류를 가르므로 rule_check 의 문구를 바꾸면 분류도 바뀝니다.guards_input.py1차 정규식 규칙 rule_check → 2차 LLM 구조화 판별 llm_check 을 묶은 input_guard (미들웨어가 아닌 순수 함수)guards_refusal.py종류별 고정 거절 문구와 guard_audit.log 내부 기록guards_output.py응답의 PII 마스킹, 범위 이탈 차단, 근거 없는 단정 안내를 하는 OutputGuardrailMiddlewareguards.py앞 부품들을 다시 내보내고, input_guard 를 wrap_model_call 로 감싼 InputGuardrailMiddlewaregrade_guard.py + attack_set.json / normal_set.json공격 차단 수와 정상 오탐 수를 세는 채점 스크립트와 문항 세트