hee_!J commited on
Commit ·
c4e7eab
1
Parent(s): f630bf1
feat: Tier 2 원인 분석 LLM+RAG 구현
Browse files- agents/cause.py +75 -5
- agents/llm.py +21 -0
- agents/rag/knowledge/FMEA-PH-007.md +30 -0
- agents/rag/knowledge/INC-2024-0289.md +28 -0
- agents/rag/knowledge/INC-2024-0312.md +29 -0
- agents/rag/knowledge/SOP-PH-LENS-002.md +32 -0
- agents/rag/store.py +25 -6
agents/cause.py
CHANGED
|
@@ -1,11 +1,81 @@
|
|
| 1 |
-
"""Tier 2 원인 분석 에이전트
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
모델: GPT-5 mini
|
| 6 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from core.schema import Tier1, Tier2
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def run_cause(alarm: dict, tier1: Tier1) -> Tier2:
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tier 2 원인 분석 에이전트
|
| 2 |
|
| 3 |
+
알람 컨텍스트 + Tier 1 결과 + RAG 지식 문서를 근거로
|
| 4 |
+
추정 원인을 기여도(%)와 근거, citation과 함께 산출
|
| 5 |
+
모델: GPT-5 mini (agents.llm.SUBAGENT_MODEL)
|
| 6 |
"""
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
from agents.llm import SUBAGENT_MODEL, client
|
| 10 |
+
from agents.rag.store import load_document, search
|
| 11 |
from core.schema import Tier1, Tier2
|
| 12 |
|
| 13 |
+
TOP_K_DOCS = 3
|
| 14 |
+
|
| 15 |
+
# structured output용 Tier2 JSON 스키마
|
| 16 |
+
TIER2_SCHEMA = {
|
| 17 |
+
"type": "object",
|
| 18 |
+
"properties": {
|
| 19 |
+
"causes": {
|
| 20 |
+
"type": "array",
|
| 21 |
+
"items": {
|
| 22 |
+
"type": "object",
|
| 23 |
+
"properties": {
|
| 24 |
+
"name": {"type": "string"},
|
| 25 |
+
"pct": {"type": "integer"},
|
| 26 |
+
"evidence": {"type": "string"},
|
| 27 |
+
"citations": {"type": "array", "items": {"type": "string"}},
|
| 28 |
+
},
|
| 29 |
+
"required": ["name", "pct", "evidence", "citations"],
|
| 30 |
+
"additionalProperties": False,
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"required": ["causes"],
|
| 35 |
+
"additionalProperties": False,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
SYSTEM_PROMPT = """당신은 반도체 Photo 공정의 원인 분석 전문가입니다.
|
| 39 |
+
주어진 이상 알람과 탐지 결과, 사내 지식 문서를 근거로 가장 가능성 높은 원인을
|
| 40 |
+
2~3개 추정합니다. 각 원인은 기여도(pct, %)를 가지며 합이 100에 가깝도록 합니다.
|
| 41 |
+
근거(evidence)는 제공된 문서 내용에 기반해 구체적으로 작성하고, citations에는
|
| 42 |
+
근거가 된 문서 ID만 정확히 기입합니다. 제공되지 않은 문서는 인용하지 않습니다.
|
| 43 |
+
기여도가 높은 원인부터 순서대로 제시합니다."""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _build_query(alarm: dict, tier1: Tier1) -> str:
|
| 47 |
+
feature = alarm.get("feature") or ""
|
| 48 |
+
return f"{alarm['title']} {feature} 원인 CD 산포 렌즈 노광 진동 표면 결함"
|
| 49 |
+
|
| 50 |
|
| 51 |
def run_cause(alarm: dict, tier1: Tier1) -> Tier2:
|
| 52 |
+
doc_ids = search(_build_query(alarm, tier1), top_k=TOP_K_DOCS)
|
| 53 |
+
knowledge = "\n\n".join(f"[{d}]\n{load_document(d)}" for d in doc_ids)
|
| 54 |
+
|
| 55 |
+
sensors = ", ".join(f["name"] for f in tier1["features"])
|
| 56 |
+
user_prompt = f"""## 이상 알람
|
| 57 |
+
- 공정: {alarm['title']}
|
| 58 |
+
- lot: {alarm['lot_id']}
|
| 59 |
+
- 이상 피처: {alarm.get('feature')} {alarm.get('feature_arrow') or ''}
|
| 60 |
+
|
| 61 |
+
## Tier 1 이상 탐지 결과
|
| 62 |
+
- 이상 점수: {tier1['score']}
|
| 63 |
+
- 기여 센서(Top): {sensors}
|
| 64 |
+
|
| 65 |
+
## 사내 지식 문서
|
| 66 |
+
{knowledge}
|
| 67 |
+
|
| 68 |
+
위 정보를 근거로 원인을 분석해 주세요."""
|
| 69 |
+
|
| 70 |
+
resp = client().chat.completions.create(
|
| 71 |
+
model=SUBAGENT_MODEL,
|
| 72 |
+
messages=[
|
| 73 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 74 |
+
{"role": "user", "content": user_prompt},
|
| 75 |
+
],
|
| 76 |
+
response_format={
|
| 77 |
+
"type": "json_schema",
|
| 78 |
+
"json_schema": {"name": "tier2", "schema": TIER2_SCHEMA, "strict": True},
|
| 79 |
+
},
|
| 80 |
+
)
|
| 81 |
+
return json.loads(resp.choices[0].message.content)
|
agents/llm.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenAI 클라이언트와 모델 설정
|
| 2 |
+
|
| 3 |
+
서브에이전트(cause/impact/response) = SUBAGENT_MODEL
|
| 4 |
+
오케스트레이터 = ORCHESTRATOR_MODEL
|
| 5 |
+
모델명을 여기 한 곳에서 관리해 교체를 쉽게 함
|
| 6 |
+
"""
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
from openai import OpenAI
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
SUBAGENT_MODEL = "gpt-5-mini"
|
| 15 |
+
ORCHESTRATOR_MODEL = "gpt-5"
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@lru_cache(maxsize=1)
|
| 19 |
+
def client() -> OpenAI:
|
| 20 |
+
"""OpenAI 클라이언트, 첫 호출 시 1회만 생성"""
|
| 21 |
+
return OpenAI()
|
agents/rag/knowledge/FMEA-PH-007.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FMEA-PH-007 — Photo 공정 실패 모드 분석
|
| 2 |
+
|
| 3 |
+
## 대상
|
| 4 |
+
|
| 5 |
+
Photo / Lithography 공정의 주요 실패 모드와 원인, 영향, 검출 방법을 정리합니다.
|
| 6 |
+
|
| 7 |
+
## 실패 모드
|
| 8 |
+
|
| 9 |
+
### 1. CD 산포 증가
|
| 10 |
+
|
| 11 |
+
- 잠재 원인: 투영 렌즈 오염, 노광 에너지 드리프트, 스테이지 진동
|
| 12 |
+
- 영향: 후공정 Etch 단계의 트렌치 깊이 편차 확대, 수율 손실
|
| 13 |
+
- 검출: SPC 기반 CD-X 산포 모니터링, 헤이즈 센서
|
| 14 |
+
|
| 15 |
+
### 2. 오버레이 오차
|
| 16 |
+
|
| 17 |
+
- 잠재 원인: 스테이지 진동, 정렬 마크 손상
|
| 18 |
+
- 영향: 패턴 정합 불량
|
| 19 |
+
- 검출: 오버레이 계측, 진동 센서
|
| 20 |
+
|
| 21 |
+
### 3. 포커스 편차
|
| 22 |
+
|
| 23 |
+
- 잠재 원인: 웨이퍼 평탄도 불량, 척 오염
|
| 24 |
+
- 영향: 패턴 해상도 저하
|
| 25 |
+
- 검출: 포커스 계측
|
| 26 |
+
|
| 27 |
+
## 우선순위
|
| 28 |
+
|
| 29 |
+
CD 산포 증가는 발생 빈도와 후공정 영향이 모두 커 최우선 관리 대상입니다. 원인
|
| 30 |
+
중 렌즈 오염은 PM 주기와 강한 상관이 있어 정비 이력 확인이 우선입니다.
|
agents/rag/knowledge/INC-2024-0289.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# INC-2024-0289 — 스테이지 진동에 의한 노광 위치 오차
|
| 2 |
+
|
| 3 |
+
## 분류
|
| 4 |
+
|
| 5 |
+
- 유형: 공정 인시던트
|
| 6 |
+
- 공정: Photo / Lithography
|
| 7 |
+
- 사업부: 메모리 2동
|
| 8 |
+
- 발생일: 2024-02-28
|
| 9 |
+
|
| 10 |
+
## 증상
|
| 11 |
+
|
| 12 |
+
특정 시간대에 처리된 로트에서 오버레이와 CD 산포가 동시에 악화되었습니다.
|
| 13 |
+
이상이 간헐적으로 발생하여 초기 원인 규명에 어려움이 있었습니다.
|
| 14 |
+
|
| 15 |
+
## 원인
|
| 16 |
+
|
| 17 |
+
웨이퍼 스테이지의 진동이 원인으로 확인되었습니다. 인접 설비의 정비 작업 중
|
| 18 |
+
발생한 진동이 전달된 것으로, 진동 센서에서 동일 시간대 이상치가 검출되었습니다.
|
| 19 |
+
|
| 20 |
+
## 조치
|
| 21 |
+
|
| 22 |
+
- 인접 설비 정비 일정 조정
|
| 23 |
+
- 진동 센서 모니터링 구간 확대
|
| 24 |
+
|
| 25 |
+
## 참고
|
| 26 |
+
|
| 27 |
+
렌즈 오염과 달리 진동 원인은 발생 시간대와의 상관관계가 뚜렷합니다. 원인 구분
|
| 28 |
+
시 시간대 패턴을 함께 확인하는 것이 유효합니다.
|
agents/rag/knowledge/INC-2024-0312.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# INC-2024-0312 — Photo 렌즈 오염으로 인한 CD 산포 증가
|
| 2 |
+
|
| 3 |
+
## 분류
|
| 4 |
+
|
| 5 |
+
- 유형: 공정 인시던트
|
| 6 |
+
- 공정: Photo / Lithography
|
| 7 |
+
- 사업부: 메모리 1동
|
| 8 |
+
- 발생일: 2024-03-12
|
| 9 |
+
|
| 10 |
+
## 증상
|
| 11 |
+
|
| 12 |
+
노광 공정에서 CD-X 산포가 관리 한계를 초과하였습니다. 동일 스캐너에서 처리된
|
| 13 |
+
연속 로트에서 CD 균일도가 점진적으로 악화되는 양상이 관측되었습니다.
|
| 14 |
+
|
| 15 |
+
## 원인
|
| 16 |
+
|
| 17 |
+
투영 렌즈 표면 오염으로 확인되었습니다. 직전 PM(예방 정비) 이후 14일이 경과한
|
| 18 |
+
시점이었으며, 헤이즈 센서 측정값이 평소 대비 18% 상승해 있었습니다.
|
| 19 |
+
|
| 20 |
+
## 조치
|
| 21 |
+
|
| 22 |
+
- 렌즈 긴급 PM 투입 (약 2시간 소요)
|
| 23 |
+
- 영향 로트 후공정 진입 보류
|
| 24 |
+
- PM 주기를 30일에서 21일로 단축 권고
|
| 25 |
+
|
| 26 |
+
## 재발 방지
|
| 27 |
+
|
| 28 |
+
헤이즈 센서 임계값을 신설하고, 임계 초과 시 자동 알람이 발생하도록 모니터링을
|
| 29 |
+
강화하였습니다.
|
agents/rag/knowledge/SOP-PH-LENS-002.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SOP-PH-LENS-002 — 렌즈 PM 표준 절차
|
| 2 |
+
|
| 3 |
+
## 목적
|
| 4 |
+
|
| 5 |
+
Photo 스캐너 투영 렌즈의 예방 정비(PM) 절차를 표준화합니다.
|
| 6 |
+
|
| 7 |
+
## 적용 범위
|
| 8 |
+
|
| 9 |
+
전 사업부 Photo / Lithography 스캐너 설비.
|
| 10 |
+
|
| 11 |
+
## PM 주기
|
| 12 |
+
|
| 13 |
+
- 표준 주기: 30일
|
| 14 |
+
- CD 산포 악화 또는 헤이즈 센서 상승 시: 21일로 단축
|
| 15 |
+
|
| 16 |
+
## 절차
|
| 17 |
+
|
| 18 |
+
1. 설비 정지 및 로트 진입 차단
|
| 19 |
+
2. 렌즈 표면 오염도 측정 (헤이즈 센서)
|
| 20 |
+
3. 세정 작업 수행
|
| 21 |
+
4. 세정 후 오염도 재측정 및 기준 충족 확인
|
| 22 |
+
5. 시험 노광으로 CD 산포 정상 범위 확인
|
| 23 |
+
6. 설비 재가동 및 로트 진입 재개
|
| 24 |
+
|
| 25 |
+
## 소요 시간
|
| 26 |
+
|
| 27 |
+
표준 약 2시간. 오염도가 높을 경우 추가 세정으로 연장될 수 있습니다.
|
| 28 |
+
|
| 29 |
+
## 기록
|
| 30 |
+
|
| 31 |
+
PM 수행 이력은 인시던트 DB에 기록하며, 직전 PM 경과일은 원인 분석 시 주요 참고
|
| 32 |
+
지표로 활용합니다.
|
agents/rag/store.py
CHANGED
|
@@ -1,22 +1,41 @@
|
|
| 1 |
-
"""RAG 도메인 지식 검색
|
| 2 |
|
| 3 |
knowledge/ 의 마크다운 문서(INC-*, FMEA-*, SOP-* 등)를 검색해
|
| 4 |
원인 분석(Tier 2)·대응 권고(Tier 4) 에이전트에 근거를 제공
|
| 5 |
|
| 6 |
-
문서가
|
| 7 |
-
citation ID
|
| 8 |
"""
|
|
|
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
KNOWLEDGE_DIR = Path(__file__).parent / "knowledge"
|
| 12 |
|
| 13 |
|
| 14 |
def load_document(doc_id: str) -> str:
|
| 15 |
-
"""citation ID
|
| 16 |
path = KNOWLEDGE_DIR / f"{doc_id}.md"
|
| 17 |
return path.read_text(encoding="utf-8") if path.exists() else ""
|
| 18 |
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
def search(query: str, top_k: int = 3) -> list[str]:
|
| 21 |
-
"""쿼리와 관련된 문서 ID를
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""RAG 도메인 지식 검색
|
| 2 |
|
| 3 |
knowledge/ 의 마크다운 문서(INC-*, FMEA-*, SOP-* 등)를 검색해
|
| 4 |
원인 분석(Tier 2)·대응 권고(Tier 4) 에이전트에 근거를 제공
|
| 5 |
|
| 6 |
+
문서가 소수라 벡터DB는 오버스펙, 키워드 매칭으로 충분
|
| 7 |
+
파일명이 곧 citation ID (예: INC-2024-0312.md -> "INC-2024-0312")
|
| 8 |
"""
|
| 9 |
+
import re
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
KNOWLEDGE_DIR = Path(__file__).parent / "knowledge"
|
| 13 |
|
| 14 |
|
| 15 |
def load_document(doc_id: str) -> str:
|
| 16 |
+
"""citation ID로 knowledge 문서 본문을 로드, 없으면 빈 문자열"""
|
| 17 |
path = KNOWLEDGE_DIR / f"{doc_id}.md"
|
| 18 |
return path.read_text(encoding="utf-8") if path.exists() else ""
|
| 19 |
|
| 20 |
|
| 21 |
+
def _knowledge_docs() -> dict[str, str]:
|
| 22 |
+
"""knowledge/ 의 모든 문서를 {doc_id: 본문}으로 로드, README는 제외"""
|
| 23 |
+
docs = {}
|
| 24 |
+
for path in KNOWLEDGE_DIR.glob("*.md"):
|
| 25 |
+
if path.stem == "README":
|
| 26 |
+
continue
|
| 27 |
+
docs[path.stem] = path.read_text(encoding="utf-8")
|
| 28 |
+
return docs
|
| 29 |
+
|
| 30 |
+
|
| 31 |
def search(query: str, top_k: int = 3) -> list[str]:
|
| 32 |
+
"""쿼리와 관련된 문서 ID를 키워드 매칭으로 반환, 관련도 내림차순"""
|
| 33 |
+
keywords = [w for w in re.split(r"\W+", query.lower()) if len(w) >= 2]
|
| 34 |
+
scored = []
|
| 35 |
+
for doc_id, text in _knowledge_docs().items():
|
| 36 |
+
low = text.lower()
|
| 37 |
+
hits = sum(low.count(kw) for kw in keywords)
|
| 38 |
+
if hits > 0:
|
| 39 |
+
scored.append((doc_id, hits))
|
| 40 |
+
scored.sort(key=lambda x: -x[1])
|
| 41 |
+
return [doc_id for doc_id, _ in scored[:top_k]]
|