feat: P&ID 연결 분석, LLM 에이전트 모드, KB 확장, MCP 서버 리팩토링

- P&ID: 연결 분석 API, Prefix 규칙 관리, 카테고리 분류, DXF 그래프 빌드
- LLM: 대화 요약, tool card 영구 보존, 시계열 차트(uPlot), 에이전트 모드
- KB: 청크 미리보기, Field Instrument Inference, 인증/Qdrant 클라이언트
- MCP: 서버 기능 확장, 파이프라인 수정, timeout 개선
- Frontend: P&ID UI, LLM UI, KB UI, OPC UA Write 탭 추가
- 설정: AGENTS.md, plant_context, README, opencode.json 업데이트
- 정리: 진단 체크리스트 문서 삭제
This commit is contained in:
windpacer
2026-05-21 23:36:57 +09:00
parent 960bda4a3c
commit 302183c97e
142 changed files with 2432231 additions and 1082 deletions

View File

@@ -0,0 +1,15 @@
파워포인트 파일 작성, 아래 요구사항에 따라서
1) AX 도입의 필연성 및 효율성 (1 페이지)
2) AX화 를 위해서 전산팀(소프트웨어 개발 부서)외의 현업 부서에서 해야할 사전 준비 항목 서술
- 신규 프로젝트 시 ,
- DXF, PDF GRAPH화를 위한 캐드 도면 작성 규칙
- 문서 작성 규칙
- 기존 프로젝트 RAG화 방안
- 좋은 방법 제안해줘
- 현실적인 OCR 등의 문제(기존 도면 및 문서의 표준 PDF, DXF 화 불가능시)를 감안
- 다른 관점에서도 제안
3) 현재 ExperionCrawler 소개
4) 위의 AX화를 위한 문제점을 ExperionCrawler에서 어떻게 보완하고 있는가 설명
5) 너의 의견 및 제안 환영함!!!

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,591 @@
# P&ID 추출 데이터 이용방안 플랜
작성일: 2026-05-14
배경: `pid_equipment` 테이블에 No-10_Plant_PID.dxf로부터 460건의 구조화 데이터(태그·라인번호·fluid·종류·area) 적재 완료. Experion realtime_table과의 자동 매핑(exact + prefix only, false positive 제거)도 동작 중. 다음 단계로 이 데이터를 무엇에 쓸지 결정.
## 현재 보유 데이터
| 컬럼 | 의미 | 예시 |
|------|------|------|
| `tag_no` | 추출된 원본 태그/라인번호 | `P-10138-600A-F2A-H100`, `PSV-10217`, `FCV-6113` |
| `instrument_type` | prefix 분류 | `FCV`, `PSV`, `P`, `T`, `BT`, `LIA` |
| `line_number` | LineNo 식별번호 (pipe만) | `10138`, `6203` |
| `confidence` | 추출 신뢰도 | 0.95~0.99 |
| `experion_tag_id` | realtime_table FK (매핑 시) | NULL or int |
| `is_active` | 활성 여부 | true/false |
서버 내부 사전(코드)으로 `_PID_FLUID_DICT`(P=PROCESS, CWS=COOLING WATER SUPPLY 등 19종), `_PID_EQUIPMENT_PREFIX`(P=Pump, T=Tank, F=Filter 등 14종), `_PID_INSTRUMENT_FIRST`/`_PID_INSTRUMENT_MODIFIER`(ISA letter 코드)도 함께 보유.
---
## 갈래 1 — 채팅 컨텍스트 보강 (최우선 추천)
**가치**: 운전원이 즉시 체감. 작업량 최소.
**작업량**: 0.5~1일.
### 구현
MCP 도구 2개 추가 (mcp-server/server.py):
```python
@mcp.tool()
async def find_pid_equipment(query: str, kind: str | None = None,
area: str | None = None, limit: int = 20) -> str:
"""P&ID 장비/계기 검색. tag_no ILIKE 매칭 + kind/area 필터.
kind: 'pipe'|'equipment'|'instrument' (없으면 전체)
area: 6-1 → tag_no가 P-61XX/T-61XX 등 6+1로 시작
"""
@mcp.tool()
async def get_pid_equipment_info(tag_no: str) -> str:
"""단일 태그 상세. 매핑된 Experion 태그도 함께 반환.
응답: {tag_no, type(fluid 또는 장비종류), instrument_type, line_number,
experion_tag, latest_value (있으면)}
"""
```
`ToolGuideKo`(OllamaController.cs)에 새 도구 설명 추가 → LLM이 자동 활용.
### 효과 예시
- "PSV-10217이 뭐야?" → "Pressure Safety Valve (mechanical, OPC 신호 없음). 10차 area"
- "10차에 펌프 몇 대?" → `find_pid_equipment(kind='equipment', area='10')` → 카운트 + 리스트
- "P-10138 어떤 fluid?" → "PROCESS FLUID, 600A, F2A spec, H100 단열"
- "FCV-6113 지금 값은?" → 매핑된 experion 태그로 자동 join → 실시간값
---
## 갈래 2 — 알람·이벤트 컨텍스트 자동 주입
**가치**: 알람 보고서가 풍부해짐. 운전원이 태그 코드만 보고 의미 파악 가능.
**작업량**: 0.5일. 기존 `summarize_events`/`generate_status_report`에 join 추가.
### 구현
- `query_events` 결과를 `pid_equipment`와 LEFT JOIN해서 각 이벤트에 PID 컨텍스트 첨부
- 예: ALARM event `tagname=fcv-6113.pv` → 컨텍스트 "FCV-6113, Flow Control Valve, 6-1 area, P-10138 라인의 PROCESS 흐름 제어"
- `generate_status_report` 마크다운 출력에 "## 영향 장비" 섹션 추가 (해당 area의 PID 인벤토리 요약)
### 효과
- 알람 요약 LLM이 "단순 태그 나열" → "공정 맥락 포함된 운전 보고서"로 격상
- KB 문서에서 같은 area의 절차서 자동 인용 (search_kb area 필터 + PID 데이터 연동)
---
## 갈래 3 — P&ID 다이어그램 시각화 (UI)
**가치**: 시각적 자산 파악. 도면 없이도 area별 장비 구조 한눈에 보기.
**작업량**: UI 작업 중심. 그래프 보기까지 가려면 from-to 추출 추가 필요.
### Phase 3a (즉시 가능)
현재 데이터만으로 — area별·종류별 트리뷰/리스트뷰:
```
└─ 10차 area
├─ 펌프 (15) P-10101, P-10116, …
├─ 탱크 (19) T-10101, T-10102, …
├─ 필터 (4) F-10101A, F-10101B, …
├─ 안전밸브 (18) PSV-10101, PSV-10103, …
└─ 라인 (172) P-10138, CWS-10612, …
```
14번 P&ID 탭 또는 신규 탭에서 구현.
### Phase 3b (확장, 작업 큼)
DXF에서 라인 끝점 좌표 + 장비 좌표 추출 후 KD-tree로 from-to 추정 → cytoscape/d3로 노드 그래프. 사용자 이미 결정한 "위치좌표 계산 없이" 방향과 상충 — **추후 필요해질 때만 착수**.
---
## 갈래 4 — 자산 마스터 + KB 통합 (장기 그림)
**가치**: 단순 검색을 넘어 정비·운영 데이터 통합 플랫폼.
**작업량**: 큼. 다른 데이터 소스 통합 필요.
### 구성요소
1. **equipment_registry.yaml** (이전 논의) — Experion htm에서 추출한 좌표·area + PID 추출 데이터 머지
2. **정비/점검 일정 테이블** — 종류별 주기 정의 (PSV 1년, FCV 6개월, …) → 다음 점검일 자동 계산
3. **누락 검출 리포트**`pid_equipment`(P&ID 기준) vs `realtime_table`(Experion 기준) 양방향 차집합
- P&ID에만 있는 것 → 측정 누락 (or mechanical/지표 없는 장비)
- Experion에만 있는 것 → 도면 누락 (or 시스템 신호)
4. **KB 문서 자동 인용**`search_kb`에 PID 태그 정보 자동 추가해서 검색 품질 향상
### 의존성
- equipment_registry yaml 구축이 선결 (이전 결정: htm 파서 추가)
- 정비 일정은 별도 사용자 입력 또는 기존 정비 시스템 연동 필요
---
## 갈래 5 — 기존 `infer_field_instruments`와의 통합 (보완 관계, 죽이지 않음)
**배경**: RAG 관리 → Field Instrument 탭의 기존 기능(`infer_field_instruments` → Excel 초안 다운로드)이 `parse_pid_dxf` 등장 후 의미를 잃었는지 의문 제기됨. **결론: 의미 없어지지 않음, 오히려 통합 가치가 생김**.
### 두 기능의 본질적 차이
| 구분 | `infer_field_instruments` (기존) | `parse_pid_dxf` (신규) |
|------|--------------------------------|-----------------------|
| **입력 소스** | Experion DB의 OPC 태그 (`v_tag_summary`) | DXF 도면 파일 |
| **추론 방향** | "실제 측정점이 있다 → 현장에 계기 있을 것" | "도면에 그려져 있다 → 자산으로 존재" |
| **`ficq-6101` 처리** | base 보고 FT-6101 / FIC-6101 / FCV-6101 **3개로 분해** | 도면에 그려진 그대로 (보통 FCV-6101, FIC-6101 등) 인식 |
| **PSV-10217** (mechanical, 신호 없음) | ❌ 못 만듦 | ✅ 도면에 있으면 추출 |
| **추가된 임시 계기** (OPC 신호만 존재) | ✅ 추론 가능 | ❌ 도면에 없으면 못 잡음 |
| **출력** | Excel 초안 (운전원 수동 검수 후 KB 업로드) | `pid_equipment` 테이블 |
| **동력기기** | `power_equipment` role로 별도 분류 | Pump prefix(`P`) 등으로 분류 |
### 잡는 누락이 정반대 — 단독으로는 불완전
- **OPC 신호 있지만 도면 미반영**: `infer_field_instruments`만 잡음 (도면 업데이트 누락 케이스)
- **도면엔 있지만 OPC 신호 없음**: `parse_pid_dxf`만 잡음 (PSV/수동밸브/게이지/파이프 등 mechanical 자산)
- **둘 다 존재**: `match_pid_tags`가 자동 연결 (exact + prefix)
### 통합으로 새로 가능해지는 작업
1. **`infer_field_instruments` 출력 자동 검증**
- infer가 추론한 FT-6101이 실제 도면(`pid_equipment`)에 있는지 즉시 대조
- 추론 정확도 자동 측정 → 추론 룰 개선 피드백 루프 가능
2. **Excel 초안 자동 라벨링 보강**
- 도면에는 PSV-10217이 있는데 infer는 못 만들었음 → "OPC 신호 없는 mechanical" 라벨로 Excel에 자동 행 추가
- 도면 LineNo 정보(`P-10138-600A-F2A-H100`)로 fluid/size/material 컬럼 자동 채움
- infer가 만든 row와 P&ID에서만 보이는 row를 한 Excel에 합쳐서 운전원이 한 번에 검수
3. **양방향 차집합 리포트** (갈래 4의 핵심 산출물)
| 차집합 | 의미 | 운영 액션 |
|--------|------|-----------|
| `pid_equipment` `realtime_table` (도면 ⊖ 실제) | 도면엔 있는데 측정 없는 자산 | (a) mechanical → 정상, "OPC 신호 없음" 라벨로 마스터에 등록<br>(b) 측정 누락 → 신호 추가 검토 |
| `realtime_table` `pid_equipment` (실제 ⊖ 도면) | 측정은 있는데 도면 없는 신호 | 도면 업데이트 누락 → P&ID 도면 보완 요청 |
| 양쪽 모두 | 정상 매핑 | UI에서 ✅ 표시 (현재 상태) |
### 구현 (작업량 적음)
- `infer_field_instruments`의 dedup 단계 뒤에 `pid_equipment` LEFT JOIN 한 번
- infer 결과 row마다 `in_pid_drawing: bool`, `pid_line_number`, `pid_drawing_no` 컬럼 추가
- infer가 만들지 못한 `pid_equipment` row를 별도 시트(`Pid_Only_Mechanical`)로 Excel에 추가
- `parse_pid_dxf`는 손대지 않음. infer 쪽에만 join 추가
- 운전원이 다운받는 Excel이 그대로 더 풍부해짐 — 추가 UI 변경 불필요
### 의존성
- 갈래 1·2 없어도 가능. 단독 진행 가능
- `infer_field_instruments`의 출력 Excel 포맷 일부 변경 (시트 추가) — 기존 운영 흐름에 영향 적음
---
## 갈래 6 — PID 추출 → 옵시디언 노트 부트스트랩 → 하이브리드 RAG
**배경**: `plans/옵시디언-구조적용-플랜.md`는 마크다운 노트 + `[[wikilinks]]` + frontmatter 기반 그래프 RAG를 제안하지만 **아직 미구현** (`notes/` 디렉토리·DB 테이블 없음, 위키링크가 들어있는 .md는 계획 문서 자체뿐). 옵시디언 노트 시스템을 0에서 시작하면 "초기 비어있음" 문제가 큰데, PID 추출 결과를 활용하면 **첫날부터 460개 anchor 노트**로 부트스트랩 가능.
### 역할 분담
| 영역 | PID 추출 (`pid_equipment`) | 옵시디언 노트 (planned) |
|------|---------------------------|------------------------|
| 채우는 것 | **뼈대** — 모든 태그/라인/장비 자동 인벤토리 | **살** — 절차, 루프 의미, 트러블슈팅, 벤더 정보, 운전 노하우 |
| 입력 | 도면 1번 + 자동 | 운전원/엔지니어 수기 + 채팅 저장 |
| 갱신 | 도면 바뀌면 재추출 | 사람이 알게 될 때마다 |
| 핵심 식별자 | `tag_no` (FCV-6113, P-10101) | 노트 ID `tag/fcv-6113`, `loop/compression-a` |
**공통 anchor가 tag ID** — PID가 만든 모든 태그는 곧 노트 ID가 되고, 사람이 쓴 노트는 `[[tag/fcv-6113]]` 위키링크로 PID 인벤토리에 자동 연결.
### 구현 단계
#### Step 1 — PID stub 노트 자동 생성
`pid_equipment` 460건을 각각 frontmatter-only `.md` 파일로 발행:
```yaml
---
id: tag/fcv-6113
kind: tag
prefix: FCV
type: Flow Control Valve
area: 6-1
line_number: 6113
experion_tag: fcv-6113.pv
source_drawing: No-10_Plant_PID.dxf
source: pid_equipment.id=42
generated_at: 2026-05-14
---
<!-- 본문은 비어둠 — 운전원이 채움 -->
```
- 디렉토리: `notes/tag/`, `notes/area/`, `notes/drawing/`
- 한 파일 = 한 노트 = `pid_equipment` 1행
- 본문 비어있어도 anchor 역할 → 위키링크 타깃으로 즉시 사용 가능
#### Step 2 — 사람 노트는 위에 자라남
운전원/엔지니어가 만드는 손 노트는 별도 kind:
- `notes/loop/compression-a.md` — "이 루프는 [[tag/fcv-6113]] [[tag/pt-6111]] [[tag/ft-6113]]로 구성됨"
- `notes/procedure/start-up-a.md` — "기동 시 [[loop/compression-a]] 먼저 안정화"
- `notes/event-pattern/surge-recovery.md` — "[[tag/fcv-6113]] surge 발생 시 [[procedure/anti-surge]] 적용"
**원칙**: 사람은 신규 마크다운만 쓰고 PID stub은 안 건드림. 재추출 시 stub 안전하게 덮어쓰기 가능.
#### Step 3 — Stub 갱신 정책
PID 재추출 시:
- frontmatter는 항상 최신 `pid_equipment` 값으로 덮어씀
- 본문(`<!-- comment -->` 아래)에 운전원이 추가한 텍스트가 있으면 보존 (merge 로직)
- 새로 등장한 tag → 신규 stub 생성
- 사라진 tag → `archived: true` frontmatter 추가 (파일 삭제 안 함, 위키링크 깨짐 방지)
#### Step 4 — KB 인덱싱 + 그래프 엣지 추출
`notes/` 디렉토리를 KbIngestWorker가 별도 컬렉션(예: `kb_notes`)으로 처리:
- 노트 1개 = 1 청크 (헤딩으로 자르지 않음 — 작은 단위 유지)
- frontmatter → Qdrant payload (area, kind, prefix 필터링 가능)
- 본문에서 `[[note-id]]` 정규식 추출 → 신규 테이블 `kb_note_edges (source_id, target_id, link_type)`에 적재
#### Step 5 — 채팅 RAG에 그래프 1-hop 결합
`search_kb`가 top-k 청크 검색 → 각 청크가 노트면 `kb_note_edges`에서 1-hop 위키링크 타깃 조회 → 그 타깃 노트들의 frontmatter+첫 200자를 컨텍스트에 함께 주입.
```python
# 의사 코드
chunks = search_kb(query, top_k=5)
extra_context = []
for chunk in chunks:
if chunk.note_id:
targets = db.fetch_edges(chunk.note_id, depth=1)
extra_context.extend(targets) # 5개 청크 + N개 1-hop 노트
return chunks + extra_context # LLM에 함께 전달
```
LLM은 청크 5개만이 아니라 "이 청크와 명시적으로 연결된 절차/루프/이벤트 패턴"까지 함께 보고 답변.
### 트레이드오프
- (+) 옵시디언 시스템의 "초기 비어있음" 문제 해결 — 첫날부터 460개 anchor 보유
- (+) 운전원이 채팅에서 받은 답변을 `loop/surge-recovery.md` 한 줄로 영구화 가능 → 지식 자산이 자라남
- (+) 마크다운이라 외부 옵시디언 앱으로 열어 편집해도 호환 (부수효과)
- () stub 갱신 시 본문 보존 머지 로직 필요 (text diff 충돌 검출)
- () 운전원이 위키링크 작성 습관 안 들이면 anchor만 잔뜩에 살이 없는 상태 → **저진입 UX 필수**:
- 채팅에서 "이 답변을 [[loop/compression-a]]에 저장" 같은 한 클릭 액션
- 노트 작성 시 `[[` 입력하면 자동완성 드롭다운으로 PID stub 추천
### 의존성
- 갈래 1 권장 선결 — PID 데이터 조회 인프라가 있어야 stub 생성 스크립트 작성이 쉬움
- `옵시디언-구조적용-플랜.md`의 데이터 모델 결정사항 따름 (note ID 규칙, kind 9종, frontmatter 스키마)
- KbIngestWorker에 notes 디렉토리 전용 처리 분기 추가
### 작업 분할
| 단계 | 산출물 | 작업량 |
|------|--------|--------|
| Step 1 | `mcp-server/notes_sync.py` — pid_equipment → notes/tag/*.md stub 생성기 | 0.5일 |
| Step 3 | 머지 보존 로직 + archived 처리 | 0.5일 |
| Step 4 | KbIngestWorker notes 분기 + `kb_note_edges` 테이블 + 위키링크 파서 | 1일 |
| Step 5 | `search_kb` 그래프 1-hop 보강 + 채팅 통합 | 0.5일 |
| 사람 노트 저진입 UX | "답변 → 노트 저장" 버튼, `[[` 자동완성 | 1일 (UI) |
| **합계** | | **3.5일** |
---
## 갈래 7 — Excel 라운드트립으로 service/role 메타데이터 보강 (마지막 연결고리)
**배경**: DXF 파서는 tag·LineNo·fluid(파이프 한정)·prefix 분류는 자동 추출하지만, **장비의 실질 정보(service, role, contents, from-to)는 도면에 텍스트로 없음**. 예: T-201이 원료탱크인지 폐액탱크인지, 무슨 fluid를 담는지, 어디로 보내는지 — LLM이 답할 컨텍스트가 없음. 운전원이 가장 익숙한 도구(Excel)로 한 번에 채우는 round-trip이 가장 실용적인 해결책.
**갈래 5와의 차이**: 갈래 5는 `infer_field_instruments` Excel(현장 계기 초안, **다운로드 전용**)에 PID 정보를 합치는 것. **갈래 7은 PID 메타데이터를 사람이 채워 다시 시스템에 반영하는 round-trip — 편집 가능한 마스터 시트**.
### 시트 구성 (장비 종류별 분리)
| 시트 | 자동 채워진 컬럼 | 사용자가 채울 컬럼 |
|------|----------------|------------------|
| `Tanks` (T-XXX) | tag_no, area, source_drawing | service, role(feed/buffer/product/waste), contents(PGMEA/HBM/...), capacity, notes |
| `Drums` (D-XXX) | 위와 동일 | service, role, contents |
| `Filters` (F-XXX) | tag_no, area | service, medium, from_tag, to_tag |
| `Heat Exchangers` (E-XXX) | tag_no, area | service, hot_side, cold_side, duty |
| `Columns` (C-XXX) | tag_no, area | service, product, overhead, bottom |
| `Pumps` (P-XXX) | tag_no, area, mapped_experion_tag | service, driver(motor/turbine), from_tag, to_tag, power_kw |
| `Compressors / Fans` | tag_no, area | service, driver, power_kw |
| `Cooling Towers` | tag_no, area | service, capacity |
| `Manual Valves` (BV/GV/XV) | tag_no, area | service, normal_position(open/closed), purpose |
| `Pipes` (LineNo) | service, fluid, size, spec, insul (이미 파싱됨) | from_tag, to_tag (오직 이거만) |
| `Legend` | dropdown 후보 (role 종류, fluid 코드, position 등) | — |
| `Index` | 시트별 완성도 (채워진 비율) | — |
### Round-trip 흐름
```
[다운로드]
pid_equipment → 장비 종류별 그룹핑 → 시트별 분리 → Excel 생성
(자동 채운 컬럼 + 빈 컬럼 + dropdown legend)
[운전원이 채움 (며칠~몇 주, 영역별 분할 가능)]
[업로드]
Excel 파싱 → 검증 (dropdown 값, 필수 필드, 참조 tag_no 존재 여부)
→ pid_equipment에 신규 컬럼들(service, role, contents, from_tag, to_tag, …) UPDATE
→ 변경 diff 리포트 (몇 건 채워졌는지, 무효 값 몇 건)
→ (갈래 6 도입 후) tag stub 노트 frontmatter 자동 갱신
```
### 데이터 모델 확장
`pid_equipment` 테이블에 컬럼 추가:
```sql
ALTER TABLE pid_equipment
ADD COLUMN service TEXT, -- 운전원 입력 fluid/용도 (P=PROCESS와 별개로 자유 텍스트)
ADD COLUMN role TEXT, -- feed_tank, buffer_tank, waste_tank, transfer_pump, ...
ADD COLUMN contents TEXT, -- PGMEA, HBM, CITY_WATER, ...
ADD COLUMN from_tag TEXT, -- 흐름 상류 tag (수동 입력)
ADD COLUMN to_tag TEXT, -- 흐름 하류 tag
ADD COLUMN driver TEXT, -- motor / turbine / electric / steam
ADD COLUMN capacity TEXT, -- 자유 텍스트 (단위 포함)
ADD COLUMN power_kw NUMERIC,
ADD COLUMN user_notes TEXT, -- 운전원 자유 메모
ADD COLUMN metadata_filled_at TIMESTAMPTZ; -- 마지막 사용자 갱신 시각
```
### 갈래 5/6과의 관계 — 같은 그림의 다른 단계
| 단계 | 역할 | 갈래 |
|------|------|------|
| 자동 추출 | 뼈대 (모든 태그 자동 인벤토리) | PID 파서 (완료) |
| 자동 검증 Excel | mechanical/누락 자산 검출 (read-only) | 갈래 5 |
| **수기 보강 round-trip** | **구조화 슬롯 일괄 채우기 (service/role/from-to)** | **갈래 7 (이 항목)** |
| 노트 자동 생성 | tag stub frontmatter에 갈래 7 데이터 자동 반영 | 갈래 6 Step 1 |
| 채팅 RAG 강화 | 위 모두 통합 검색 | 갈래 1+2+6 |
**갈래 7이 진짜 마지막 연결고리**. 이게 없으면 갈래 6 stub의 frontmatter가 빈약하고, 채팅도 "T-201이 뭐 담는지" 답을 못 함.
### 트레이드오프
- (+) Excel은 운전원이 가장 자연스럽게 다루는 도구 — 마크다운 학습 비용 0
- (+) 한 번 다운로드받아 오프라인에서도 채울 수 있음
- (+) 시트별 분리로 "오늘은 펌프만, 내일은 탱크만" 같은 분할 작업 가능
- (+) dropdown legend로 데이터 일관성 자동 확보
- () Excel은 discrete batch — 동시에 여러 사람이 채우면 머지 충돌 위험. 한 번에 한 사람 권장
- () 다운로드 받은 파일이 오래되면 사이에 PID 재추출로 신규 tag 생긴 경우 머지 필요 — 업로드 시 "신규 tag는 이번 Excel에 없음" 경고
- () 한 번에 460건 다 채우라 하면 부담 → 영역별/종류별 분할 가이드 필요 ("이번 주는 6-1 area Pump만")
### 구현 (작업량)
| 단계 | 산출물 | 작업량 |
|------|--------|--------|
| DB 마이그레이션 | `pid_equipment` 신규 컬럼 ALTER + InitializeAsync DDL 갱신 | 0.3일 |
| 다운로드 엔드포인트 | `GET /api/pid/metadata/excel` — 시트별 분리 Excel 생성 (openpyxl) | 0.5일 |
| 업로드 엔드포인트 | `POST /api/pid/metadata/excel` — 파싱+검증+UPDATE+diff 리포트 | 1일 |
| UI (RAG 관리 또는 P&ID 탭) | 다운로드/업로드 버튼 + 진행률 + diff 표시 | 0.5일 |
| Legend dropdown 사전 | role/contents/driver 등 유효값 정의 + 운영 후 확장 가능하게 | 0.2일 |
| **합계** | | **2.5일** |
### 의존성
- 갈래 1·5와 독립적으로 진행 가능
- 갈래 6보다 먼저 끝나야 stub frontmatter가 풍부해짐 → **Phase B 또는 Phase C 초반에 진행 권장**
---
## 갈래 8 — LineNo 파생 정보 컬럼 추가 (작지만 모든 갈래에 가치 전파)
**배경**: 현업 통찰 — 운영 부서는 LineNo(`P-10138`, `16456` 등 시공·단관 제작용 번호)로 파이프를 부르지 않음. 가치 있는 건 거기서 파생되는 **{service, fluid, size, material, flange_rating, insulation}**. LineNo 자체는 시공·정비 참조용으로 보존하되, 파생 정보를 별도 컬럼으로 저장해야 운영 검색·통계·일관성 검증이 가능해짐.
**현재 상태**: `_parse_pid_lineno`가 MCP 응답에 7개 필드를 다 담고 있지만, C# 측이 `line_number` 하나만 DB에 저장하고 나머지는 버리고 있음. 추출은 되는데 활용이 안 됨.
### 구현
```sql
-- pid_equipment 컬럼 추가 (line_number는 그대로 유지)
ALTER TABLE pid_equipment
ADD COLUMN service_code TEXT, -- 'P', 'CWS', 'ST', 'VG', ...
ADD COLUMN fluid_name TEXT, -- 'PROCESS FLUID', 'STEAM' (legend 변환)
ADD COLUMN pipe_size TEXT, -- '25A', '600A'
ADD COLUMN material_spec TEXT, -- 'F', 'S'
ADD COLUMN flange_rating INT, -- 1=150#, 2=300#
ADD COLUMN insul_code TEXT, -- 'A', 'B'
ADD COLUMN insul_thickness TEXT; -- 'H50', 'H100', 'E50', 'n'
```
장비/계기 행은 이 컬럼들이 NULL — 정상. 한 테이블 유지, 정규화 분리 안 함.
### 변경 파일
| 파일 | 변경 |
|------|------|
| `ExperionDbContext.InitializeAsync` | ALTER TABLE 7개 컬럼 추가 (또는 새 설치용 CREATE TABLE 갱신) |
| `PidEquipment` 엔티티 | 7개 속성 + `[Column("snake_case")]` 매핑 |
| `ExtractedItem` DTO | 7개 필드 추가 (PipeService/FluidName/PipeSize/...) |
| `PidExtractorService.ParseJson` | MCP 응답의 pipe 필드를 ExtractedItem에 매핑 |
| `PidExtractorService.ExtractFromStreamAsync` | save 시 7개 컬럼 채움 (pipe 행만, instrument/equipment는 null) |
| MCP `extract_pid_tags` / `parse_pid_dxf` | 이미 7개 필드 응답 중 — 변경 불필요 |
### 활용은 자동으로 따라옴
컬럼만 채워지면 기존 갈래들이 즉시 더 풍부해짐:
- **갈래 1 채팅 검색**: "CWS 25A 이상 라인", "스팀 H100 단열 라인" 같은 속성 조합 검색 즉시 가능
- **갈래 6 stub frontmatter**: pipe 노트에 `service: P`, `fluid: PROCESS FLUID`, `pipe_size: 25A`, `insul_thickness: H50` 자동 반영 → 노트가 빈약하지 않음
- **갈래 7 Pipes 시트**: 자동 채워진 컬럼이 7개로 늘어남, 사용자는 from_tag/to_tag/서비스별칭만 채우면 됨
- **(부수효과)** 사양 분포 리포트, 일관성 검증 같은 분석이 가능해짐 — 필요할 때 별도 도구로 노출하면 됨 (지금은 컬럼만 추가)
### 작업량
| 단계 | 산출물 | 작업량 |
|------|--------|--------|
| DB 컬럼 추가 | ALTER TABLE + InitializeAsync DDL 갱신 | 0.2일 |
| `PidEquipment` 엔티티 + `[Column]` 매핑 | 7개 속성 추가 | 0.1일 |
| `ExtractedItem` 7개 필드 + ParseJson 매핑 | DTO 확장 | 0.2일 |
| `PidExtractorService` save 경로 | 7개 컬럼 INSERT | 0.2일 |
| 기존 460건 재추출 (또는 in-place UPDATE 스크립트) | truncate 후 재추출이 간단 | 0.1일 |
| **합계** | | **0.8일** |
### 의존성
- 없음. 단독 진행 가능
- **갈래 1·6·7의 가치를 증폭시키므로 가장 먼저 배치 권장**
---
## 갈래 9 — 추출 결과 저장·폐기 선택 UX (실수 방지)
**배경**: 현재 P&ID 추출 UI는 "추출 시작" 클릭 시 MCP 추출 → `pid_equipment` 즉시 INSERT가 자동으로 이뤄짐. 운전원이 실수로 다른 파일(다른 플랜트, 구버전 도면 등)을 추출해도 되돌릴 방법이 없음. TRUNCATE는 정상 데이터까지 날아가서 사용 불가. 추출 전/후에 사용자가 검토·취사선택할 수 있어야 함.
**결정 보류**: 아래 3개 옵션 중 어느 방향으로 갈지 사용자 결정 필요.
### 옵션 비교
#### 옵션 A — Dry-run + 저장 버튼 (사전 검토)
```
[추출 시작] → MCP 추출만 (DB 저장 X) → "미리보기 460건" 표시
├── [✅ DB에 저장] → INSERT 수행
└── [🗑️ 폐기] → 결과 버림
```
| 항목 | 내용 |
|------|------|
| 장점 | 가장 안전 — DB 들어가기 전 검토 가능. 운전원 의도 명확 ("이걸 저장할까?") |
| 단점 | UI 흐름 2단계로 늘어남. 결과를 서버 임시 캐시 또는 클라이언트 메모리에 보관 필요. 사용자가 검토 중 페이지 떠나면 데이터 잃음 |
| 작업량 | 약 1일 |
| 구현 | `PidExtractorService.ExtractWithoutSaveAsync` 추가, `IMemoryCache` 결과 보관, UI에 미리보기 + 저장/폐기 버튼 |
#### 옵션 B — 자동 저장 + 즉시 취소 버튼 (사후 롤백)
```
[추출 시작] → MCP 추출 → DB 저장 (extraction_batch_id 부여)
→ "✅ 추출 완료: 460건" + [🗑️ 이번 추출 취소] 버튼
batch_id WHERE 절로 일괄 삭제
```
| 항목 | 내용 |
|------|------|
| 장점 | 기존 UX 거의 그대로. 추출 이력 자체가 자동으로 남아 추후 분석 가능 |
| 단점 | 저장 후 후속 작업(confidence 수정 등)이 일어났으면 일괄 삭제가 그 작업까지 날림 → "최근 N분만 취소 가능" 같은 가드 필요. 다른 사용자가 잠깐 잘못된 데이터 볼 수 있음 |
| 작업량 | 약 0.5일 |
| 구현 | `pid_equipment.extraction_batch_id` (UUID) 컬럼 추가, 추출 시 부여, UI에 "이번 추출 취소" 버튼 + 시간 가드 |
#### 옵션 C — A + 영구 batch_id 보존 (절충)
```
[추출 시작] → MCP 추출 (DB 저장 X, staging 보관)
├── [저장] → batch_id와 함께 INSERT
│ → 이후에도 "추출 이력" 페이지에서 batch_id로 일괄 삭제 가능
└── [폐기] → staging만 버림
```
| 항목 | 내용 |
|------|------|
| 장점 | A의 안전성 + B의 사후 롤백 모두 확보. "추출 이력" 페이지에서 과거 batch 단위 관리 가능 |
| 단점 | 작업량 가장 큼. UI도 가장 복잡 (미리보기 + 저장 + 이력 관리) |
| 작업량 | 약 1.5~2일 |
| 구현 | 옵션 A + 옵션 B 합친 형태. staging 캐시 + batch_id 컬럼 + 이력 페이지 |
### 추천 (결정 보류 상태)
| 우선 고려 | 추천 옵션 |
|----------|----------|
| 안전성 우선 | **옵션 A** |
| 작업량 최소 | 옵션 B |
| 장기 운영 (감사·이력) | 옵션 C |
운전원이 자주 추출하는 운영 환경이면 옵션 A의 사전 검토가 정신적 부담 적고 안전 — **기본 추천은 옵션 A**. 단, "추출 이력 관리" 같은 요구가 미래에 나올 가능성이 크면 옵션 C가 미리 대응됨.
### 의존성
- 옵션 A: `IMemoryCache` 또는 클라이언트 세션 — 표준 ASP.NET Core 기능
- 옵션 B/C: `pid_equipment.extraction_batch_id` 컬럼 (UUID) — 갈래 8 컬럼 추가와 함께 ALTER 묶어 처리하면 효율적
- 어느 옵션이든 갈래 8과 같은 시점에 진행하면 마이그레이션 1번으로 끝낼 수 있음
### 결정 시점
- 갈래 1~5 진행 중 사용 패턴 보면서 결정 가능 (당장 결정 안 해도 됨)
- 단, 옵션 B/C를 선택한다면 갈래 8 마이그레이션 시 `extraction_batch_id` 컬럼을 함께 추가하는 게 효율적 → **갈래 8 착수 전에 한 번 가볍게 결정 권장**
---
## 추천 진행 순서
| 우선순위 | 항목 | 작업량 | 가치 | 의존성 |
|----------|------|--------|------|--------|
| **0** | **갈래 8 (LineNo 파생 정보 컬럼 추가)** | 0.8일 | **모든 갈래에 가치 전파** | 없음 |
| **0+** | **갈래 9 (추출 결과 저장/폐기 UX)** | 0.5~2일 (옵션별) | 높음 (실수 방지) | 갈래 8과 함께 마이그레이션 묶기 권장 |
| **1** | 갈래 1 (MCP 도구 2개) | 0.5~1일 | 높음 | 갈래 8 권장 |
| **2** | 갈래 5 (infer ↔ pid_equipment 통합 Excel) | 0.5일 | 높음 | 없음 |
| **3** | 갈래 2 (이벤트 컨텍스트) | 0.5일 | 높음 | 갈래 1 |
| **4** | 갈래 7 (Excel round-trip으로 service/role 보강) | 2.5일 | 매우 높음 | 갈래 8 권장 |
| **5** | 갈래 6 (옵시디언 노트 부트스트랩 + 하이브리드 RAG) | 3.5일 | 매우 높음 (장기) | 갈래 1·7·8 권장 |
| 6 | 갈래 3a (트리뷰 UI) | 1~2일 | 중 | 없음 |
| 7 | 갈래 4 일부 (누락 검출 리포트 단독) | 0.5일 | 중 | 갈래 5와 중복 — 갈래 5에 통합 권장 |
| 보류 | 갈래 3b (그래프 시각화) | 큼 | 사용 패턴 보고 결정 | from-to 추출 |
| 보류 | 갈래 4 전체 (정비 일정·KB 통합) | 큼 | 큼 | equipment_registry, 정비 시스템 |
### 추천 흐름
**Phase 0 (반나절, 0.8일)** — 모든 갈래의 가치 증폭
0. **갈래 8**: `pid_equipment`에 service/fluid/size/material/flange/insul 7개 컬럼 추가 + 460건 재추출
- 이후 모든 갈래가 더 풍부한 데이터로 동작
**Phase A (이번 주, 1~2일)** — 즉시 효과
1. **갈래 1**: `find_pid_equipment` / `get_pid_equipment_info` 추가 → 갈래 8로 채워진 7개 필드까지 검색·표시
2. **갈래 5** 병행: `infer_field_instruments``pid_equipment` join 추가 → Excel 초안에 mechanical/누락 자산 자동 포함
**Phase B (Phase A 사용 패턴 본 뒤, 0.5일)** — 정착 단계
3. **갈래 2**: 알람·이벤트 컨텍스트에 PID 정보 자동 첨부 → 보고서 품질 향상
**Phase C (2.5일 + 운전원 채움 기간)** — 마지막 연결고리
4. **갈래 7**: Excel round-trip 인프라 구축 → 운전원이 service/role/contents/from-to 채움
- 자동 컬럼이 갈래 8 덕분에 7개 더 풍부 → 사용자 부담 줄어듦
- 구축 자체는 2.5일이지만 **운전원이 채우는 데 며칠~몇 주** 걸림 (영역별 분할)
- 일부라도 채워지면 즉시 갈래 1·2의 답변 품질이 올라감 (T-201 → "PGMEA 원료 탱크" 답변 가능)
**Phase D (Phase C 진행 중 병행, 3.5일)** — 지식 자산화로 진화
5. **갈래 6**: PID stub 자동 생성 → 옵시디언 노트 시스템 부트스트랩 → 하이브리드 RAG
- 갈래 8·7의 결과가 stub frontmatter로 자동 반영되어 빈약하지 않음
- 운전원이 채팅에서 노트 작성 습관 들이면 시간이 갈수록 그래프가 자라남
- **장기적으로는 이 갈래가 시스템의 핵심 자산** — PID/Experion은 자동 입력 소스, 노트는 사람의 운영 지식 저장소
**Phase E (필요해질 때)** — 확장
6. **갈래 3a**: 시각적 인벤토리 필요해지면 트리뷰 UI 작업
7. **갈래 4 전체** 또는 **갈래 3b**: 정비 시스템 연동/그래프 시각화 등은 실제 운영 요구가 명확해진 시점에 착수
### 우선순위 배치 근거
- **갈래 8 최우선**: 0.8일 작업으로 갈래 1·6·7 모두의 데이터 가치를 증폭. ROI 극단적으로 높음. 다른 모든 작업 전에 끝내는 게 합리적
- **갈래 5 > 갈래 4**: 갈래 4의 핵심 산출물(양방향 차집합 리포트)이 갈래 5의 Excel 시트로 자연스럽게 포함됨. 운전원이 이미 익숙한 UX 위에 얹는 거라 학습 비용 0
- **갈래 7 > 갈래 6**: 갈래 6 stub의 frontmatter는 갈래 7의 결과(service/role/contents)로 채워질 때 비로소 가치가 있음. 갈래 7 없이 갈래 6만 가면 빈 anchor만 잔뜩
- **갈래 7 > 갈래 3a**: service/role이 채워지지 않은 상태에서 시각화는 "이름만 있는 그림" — 우선 의미를 채워야 함
- **갈래 6은 갈래 1·2·7·8 이후가 적기**: 채팅에서 PID 데이터 활용 패턴이 나오고 메타데이터가 채워진 뒤라야 노트 시스템이 의미 있음
---
## 결정 필요 항목
- [ ] **갈래 8** 즉시 착수 여부 — `pid_equipment` 7개 컬럼 추가 + 460건 재추출 (0.8일, ROI 최고)
- [ ] **갈래 9** 옵션 선택 — A(dry-run, 1일) / B(자동저장+취소, 0.5일) / C(절충, 1.5~2일) 중 하나 선택. **갈래 8 착수 전 결정 권장** (B/C면 `extraction_batch_id` 컬럼을 갈래 8 마이그레이션에 함께 묶음)
- [ ] **갈래 1** 즉시 착수 여부 — `find_pid_equipment` / `get_pid_equipment_info` MCP 도구 추가
- [ ] **갈래 5** 즉시 착수 여부 — `infer_field_instruments``pid_equipment` LEFT JOIN + 별도 시트 추가
- [ ] 갈래 2 진행 여부 — Phase A 정착 후
- [ ] **갈래 7** 데이터 모델 확정 — `pid_equipment` 컬럼 추가 항목(service/role/contents/from_tag/to_tag/driver/capacity/power_kw/user_notes) 확인
- [ ] **갈래 7** legend dropdown 사전 시드값 — role/contents/driver 유효값 누가 정의
- [ ] **갈래 7** 운전원 채우기 가이드 — 영역별 분할 방식(어떤 area부터?)
- [ ] **갈래 6 Step 1·2** 일정 — PID stub 생성기 + 머지 보존 로직 (갈래 8·7 컬럼이 stub frontmatter로 흐르도록 같이 설계)
- [ ] 갈래 6 Step 5 (그래프 1-hop RAG) — `search_kb` 수정 시점 확정 필요
- [ ] 갈래 6 저진입 UX (채팅 답변 → 노트 저장 버튼) — UI 작업 일정
- [ ] 갈래 3a 트리뷰 UI 작업 일정 — 갈래 7 보강 데이터가 채워진 뒤가 효과적
- [ ] 갈래 4 단독 누락 검출 리포트 — 갈래 5에 흡수할지, 별도 페이지로 만들지
## 잔여 데이터 정합성 이슈
- **중복 추출**: 현재 460건 중 동일 tag_no가 2번씩 들어간 행 다수 발견 (예: PSV-10101이 2회). DXF에서 같은 텍스트가 여러 위치에 그려져서 발생. unique index 또는 (tag_no, pid_drawing_no) 복합키로 해결 필요.
- **pid_drawing_no NULL**: 현재 모든 행 NULL. 도면 파일명을 자동 채우거나 사용자 입력 받도록 보강 필요 — 갈래 4 누락 검출에 도면 단위 추적 필요할 때.
- **prefix 오분류**: PFD, SP, SC, TR 등 도면 라벨/약어가 instrument로 잘못 분류되는 케이스 — `_PID_TAG_RE` 또는 `_classify_pid_tag`에 예외 사전 추가 필요.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,407 @@
# RAG 지식 증강 보완 플랜 — Field-Instrument 자동 유추 + 관계 그래프
> 옵시디언 vault 풀구현(`plans/옵시디언-구조적용-플랜.md`) 이전에, **신규 인프라 0** + **휴먼-인-더-루프**로 RAG 품질을 빠르게 끌어올리는 단계.
>
> DCS 로직 태그(`ficq-6101.pv` 등) → 글자/loop 룰로 현장 계기 유추 → role/from/to까지 채운 Excel 초안 → 운영자가 검토·수정 → 기존 KB 업로드 흐름으로 인덱싱.
작성: 2026-05-14 | 위치: 14번 탭(RAG 관리) 확장 | 의존: 기존 `xlsx_parser`, `KbIngestWorker`, Qdrant `system_instrument` 컬렉션
---
## 1. 목적과 이유
### 1.1 해결하려는 문제
- `node_map_master`/`v_tag_summary`에는 **DCS 로직 블록 태그**만 있다 (`ficq-6101`, `xv-6124` ...).
- 운전원 현장 언어는 **계기 단위**다 ("FT-6101 막혔어", "FCV-6101 포지셔너 점검").
- 사이의 **번역 갭**을 RAG가 메우려면 명시적 매핑이 필요한데, 현재 KB에는 그 매핑이 없다.
- 게다가 도면/PDF만으로는 "이 송신기 → 이 컨트롤러 → 저 밸브" 같은 **신호 흐름**이 LLM 컨텍스트로 들어오지 않는다.
### 1.2 왜 지금 이 단계인가
- 옵시디언 vault 풀구현은 데이터 모델 5개 테이블 + 워커 + UI 확장 + 그래프 검색까지 1~2주 작업.
- 이 단계는 **0.5~1일** 작업으로 RAG 품질이 즉시 좋아지고, 만들어진 Excel은 vault 마이그레이션 시 **그대로 시드**가 된다 → 손해보는 작업이 아니다.
- LLM은 지루한 초안만, 사람은 도메인 검증만 → 가장 비용 효율적인 분업.
### 1.3 비채택 옵션
- LLM 100% 추론 (옵션 C) — 2000행에 27B 호출 비용 큼, 환각 위험. 채택 안 함.
- 자동 인덱싱 (운영자 검토 없이 바로 KB) — 부정확한 매핑이 RAG 답변에 그대로 섞이면 신뢰도 손상. 채택 안 함.
---
## 2. 채택: 규칙 + 부분 LLM (옵션 B)
| 부분 | 방법 | 처리량 |
|------|------|--------|
| 계기 구조 추론 (FT/FCV/Totalizer) | **결정론적 룰** | 2000행 < 5초 |
| role/from/to 기본값 | **결정론적 룰** | 위와 함께 |
| description 한국어 초안 (없을 때) | **LLM 배치** (옵션 토글) | 2000행 ≈ 수십 분 |
| confidence / needs_review | 룰 적중도 계산 | 즉시 |
`use_llm=false`로도 운영자가 즉시 작업 시작 가능. LLM은 옵션.
---
## 3. Excel 스키마 — Long Format (1행 = 1계기)
> 이전 안의 "1행 = 1 base_tag, 컬럼에 FT/FCV/..." 와이드 포맷을 버리고 **롱 포맷** 채택. role/from/to를 1열에 자연스럽게 표현할 수 있고, 향후 vault note 1개 ↔ 행 1개 대응이 깔끔.
### 3.1 시트 1: `instruments`
| 열 | 타입 | 설명 | 예 |
|----|------|------|----|
| `instrument_id` | text (PK) | 정규화 ID, 소문자 kebab | `ft-6101` |
| `display_name` | text | 운전원이 부르는 이름 | `FT-6101` |
| `parent_base_tag` | text | 이 계기가 속한 DCS 태그 | `ficq-6101` |
| `role` | enum | §4의 역할 표 | `transmitter` |
| `loop` | text | loop 번호 | `6101` |
| `area` | text | unit/area | `A` |
| `measures` | enum/text | 측정량 (transmitter류) | `flow` |
| `data_points` | text(csv) | DCS 데이터포인트 | `.pv` 또는 `.qv,.qv.value` |
| `from` | text | 신호/물질 출처 | `process/compressor-a-suction` |
| `to` | text | 신호/물질 목적지 | `tag/ficq-6101` |
| `description` | text | 한국어 설명 | `압축기 A열 입구 유량 송신기` |
| `confidence` | enum | `high` / `medium` / `low` | `high` |
| `needs_review` | bool | TRUE면 운영자 검토 필수 | `FALSE` |
| `inference_basis` | text | 어떤 룰로 유추됐는지 | `F+T@modifier rule` |
| `operator_notes` | text | 운영자 자유 입력 | `2024년 교체. P&ID 105` |
| `delete` | bool | TRUE면 재업로드 시 제외 | `FALSE` |
### 3.2 시트 2: `naming_convention` (참조용, read-only)
운영자가 시트 1을 수정할 때 참조하는 룰 표. §6 YAML을 그대로 시트로 풀어둠.
### 3.3 시트 3: `unmatched_tags` (운영자 보충용)
룰이 전혀 매칭 안 된 base_tag 목록 (예: 비표준 prefix, 한글 prefix). 운영자가 직접 시트 1에 행 추가해야 함.
### 3.4 예시 5행
| instrument_id | display_name | parent_base_tag | role | loop | area | measures | data_points | from | to | description | confidence | needs_review | inference_basis |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ft-6101 | FT-6101 | ficq-6101 | transmitter | 6101 | A | flow | .pv | process/inlet-a | tag/ficq-6101 | 입구 유량 송신기 | high | FALSE | F+(implied T) |
| fic-6101 | FIC-6101 | ficq-6101 | controller | 6101 | A | flow | .sp,.op | tag/ft-6101 | tag/fcv-6101 | 입구 유량 제어기 | high | FALSE | F+I+C |
| fcv-6101 | FCV-6101 | ficq-6101 | control-valve | 6101 | A | — | .op | tag/fic-6101 | process/compressor-a-suction | 입구 제어밸브 | high | FALSE | C → CV |
| fq-6101 | Totalizer-6101 | ficq-6101 | totalizer | 6101 | A | flow | .qv,.qv.value | tag/ficq-6101 | (none) | 입구 유량 적산기 | medium | FALSE | Q → totalizer |
| xv-6124 | XV-6124 | xv-6124 | shutdown-valve | 6124 | A | — | .instate0..7 | safety/esd-loop-1 | process/header-a | ESD 차단밸브 | low | **TRUE** | digital-only |
---
## 4. Role 정의 (시드 표)
> 옵시디언 vault의 `kind: instrument` 노트 frontmatter `role` 필드와 1:1 매칭되도록 설계.
| role | 의미 | 룰 글자 | 일반 data_point |
|------|------|---------|-----------------|
| `transmitter` | 측정 송신기 (FT/PT/TT/LT/AT) | 수식어 T 또는 측정 letter 단독 | `.pv` |
| `indicator` | 표시기 (계기 없이 DCS 화면) | I | (가상) |
| `controller` | 제어기 (FIC/PIC/TIC/...) | C | `.sp`, `.op`, `.mode` |
| `recorder` | 기록계 | R | (가상) |
| `totalizer` | 적산기 | Q | `.qv`, `.qv.value` |
| `switch` | 스위치 (FS/PS/LS/...) | S | `.instate0..n` |
| `alarm` | 알람 단독 | A | (이벤트 채널) |
| `control-valve` | 제어밸브 (FCV/PCV/TCV) | 제어기 C 동반 시 자동 추론 | `.op` 참조 |
| `shutdown-valve` | 차단/ESD 밸브 (XV/SDV) | X/SDV prefix | `.instate*` |
| `check-valve` | 체크밸브 | (운영자 수동) | — |
| `positioner` | 포지셔너 (FZ) | Z | — |
| `interlock-relay` | 인터록 릴레이 (FY) | Y | — |
| `motor` | 모터 (M, KM) | 별도 prefix | `.run`, `.fault` |
| `pump` | 펌프 (P) | 별도 prefix | `.run`, `.flow` |
| `compressor` | 압축기 (K, C) | 별도 prefix | (다중) |
| `analyzer` | 분석기 (AT, AIT) | A 단독+T | `.pv` |
| `damper` | 댐퍼 | (운영자 수동) | — |
| `equipment` | 그 외 정적 장치 | (운영자 수동) | — |
→ 운영자가 신규 role을 만들어도 그대로 보존. role 자체가 frontmatter라 시스템 enum 위반은 경고만(차단 X).
---
## 5. From / To 의미론
> "**신호 또는 물질의 흐름**"을 표현. 운영자 직관과 맞아야 함.
### 5.1 기본 규칙 (role별 자동 채움)
| role | from 기본값 | to 기본값 |
|------|------------|----------|
| `transmitter` | `process/<area>-<loop-측정점>` (placeholder) | `tag/<parent>` |
| `controller` | `tag/<measurement-transmitter>` | `tag/<paired-valve>` |
| `control-valve` | `tag/<paired-controller>` | `process/<area>-<loop-downstream>` (placeholder) |
| `totalizer` | `tag/<parent>` | `(none)` |
| `switch` | `process/<area>-<loop-측정점>` | `safety/<interlock-id>` (운영자 보충) |
| `shutdown-valve` | `safety/<esd-trigger>` (운영자 보충) | `process/<area>-<downstream>` |
| 그 외 | `(none)` | `(none)` |
`process/...`, `safety/...` 경로는 **placeholder**. 운영자가 실제 ID(있다면)로 바꾸거나 그대로 둬도 됨. vault 마이그레이션 시 자동으로 `process` kind 노트가 빨간 링크로 잡힘 (= 채워야 할 노트 목록 = §10 옵시디언 플랜의 `vault_unresolved` 기능).
### 5.2 작성 형식
운영자는 4가지 중 하나로 적을 수 있음:
1. **vault note id**: `tag/ficq-6101`, `instrument/ft-6101`, `process/header-a` — 향후 vault 그래프로 직변환
2. **base_tag 그대로**: `ficq-6101` — 자동 정규화로 `tag/ficq-6101`
3. **자유 텍스트**: `압축기 1열 입구 헤더` — 보존만, 그래프 엣지로는 안 잡힘
4. **`(none)`**: 명시적 없음
룰 엔진은 4번을 디폴트로 두지 않고, 가능한 한 1번 형식의 placeholder를 채움.
### 5.3 검증
업로드 시:
- `from` / `to`가 형식 1·2면 자동 엣지로 인식
- 형식 3은 그대로 보존하되 "그래프 엣지 미생성" 정보 행 카운트 표시
- 형식 4는 명시적 제외
---
## 6. 룰 테이블 (`prompts/instrument_inference.yaml`)
> 코드 외부에 두는 이유: 운영자/엔지니어가 plant_context.md처럼 직접 편집 가능.
```yaml
# 첫 글자 = 측정량
measurement:
F: flow
P: pressure
T: temperature
L: level
A: analysis
S: speed
W: weight
D: density
J: power
M: moisture
# 두 번째 이후 = 기능 수식어 (순서대로 별도 계기 생성)
modifiers:
I: { role: indicator, virtual: true }
R: { role: recorder, virtual: true }
T: { role: transmitter, data_points: [.pv] }
C: { role: controller, data_points: [.sp, .op] }
Q: { role: totalizer, data_points: [.qv, "qv.value"] }
S: { role: switch, data_points: [.instate0, .instate1] }
A: { role: alarm }
Y: { role: interlock-relay }
Z: { role: positioner }
# 컨트롤러가 있으면 자동으로 제어밸브 1개 생성
auto_pair:
- if_role: controller
create:
role: control-valve
id_pattern: "{meas}cv-{loop}"
display_pattern: "{MEAS}CV-{loop}"
# 특수 prefix (글자 분해 룰 미적용)
special_prefixes:
xv: { role: shutdown-valve, measures: null }
sdv: { role: shutdown-valve, measures: null }
fy: { role: interlock-relay, measures: flow }
fz: { role: positioner, measures: flow }
km: { role: motor }
p: { role: pump }
k: { role: compressor }
# data_point 패턴별 보정 (룰 추론 결과 검증)
data_point_validation:
has_qv_when: role == "totalizer"
has_sp_op_when: role == "controller"
has_instate_when: role in ["switch", "shutdown-valve"]
# 신뢰도 계산
confidence:
high_when:
- prefix matches measurement OR special_prefixes
- all modifiers resolved
- data_points present and consistent
medium_when:
- prefix matches but some data_points missing
- or modifiers contain unknown letter
low_when:
- special_prefixes only AND data_points unusual
- or first letter not in measurement table
```
---
## 7. 유추 알고리즘 (Python 의사코드)
```python
def infer_instruments_for_base_tag(base_tag: str, data_points: list[str], area: str) -> list[dict]:
# 1. 분해
head, loop = split_letters_and_number(base_tag) # "ficq", "6101"
# 2. 특수 prefix 우선
if head in rules["special_prefixes"]:
return [build_special_instrument(head, loop, data_points, area)]
# 3. 첫 글자 = 측정량
if head[0] not in rules["measurement"]:
return [build_unmatched(base_tag, area)] # confidence=low, needs_review=TRUE
meas = rules["measurement"][head[0]]
instruments = []
# 4. 수식어 글자별로 계기 생성
has_transmitter = False
for letter in head[1:]:
mod = rules["modifiers"].get(letter)
if not mod or mod.get("virtual"):
continue # I, R은 가상
inst = build_instrument(meas, mod["role"], loop, area, data_points)
instruments.append(inst)
if mod["role"] == "transmitter":
has_transmitter = True
# 5. T 글자가 명시 안 됐어도 측정 letter만으로 송신기 암시 (FI-6101 → FT-6101 묵시)
if not has_transmitter and any(m["role"] == "controller" for m in instruments):
instruments.insert(0, build_implicit_transmitter(meas, loop, area))
# 6. 컨트롤러 → 제어밸브 자동 생성 (auto_pair)
for inst in list(instruments):
if inst["role"] == "controller":
instruments.append(build_paired_valve(meas, loop, area))
# 7. from/to 채우기 (§5.1)
link_signal_flow(instruments)
# 8. data_point 검증 → confidence 계산
for inst in instruments:
inst["confidence"] = score_confidence(inst, data_points)
inst["needs_review"] = inst["confidence"] == "low"
return instruments
```
---
## 8. LLM 보강 (옵션, `use_llm=true`)
- **대상**: `description` 열이 비어 있거나 confidence=low인 행만.
- **호출**: 한 번에 최대 50행 묶어서 vLLM에 한국어 요약 요청. 시스템 프롬프트는 `prompts/plant_context.md` + role 표.
- **출력**: description 한 줄(≤80자). 환각 방지 위해 "확실치 않으면 비워두라" 명시.
- **캐시**: 같은 (parent_base_tag, role) 조합 결과는 재실행 시 재사용.
- **토큰 예산**: 50행 묶음 × ~40 묶음 = 호출 40회, 각 ≤ 2k 토큰.
---
## 9. 워크플로
```
[14번 탭 > Field Instrument 모드]
1. ▼ 초안 생성 (use_llm 토글)
POST /api/kb/instruments/infer → 백그라운드 잡 → 진행률 폴링
2. ▼ Excel 다운로드 (instruments_draft_YYYYMMDD.xlsx)
3. 운영자가 로컬에서 Excel 편집
- confidence=low / needs_review=TRUE 행 우선
- from/to 보충 (process/, safety/ placeholder 채우기)
- delete=TRUE로 잘못된 추론 제외
- 시트 3(unmatched_tags)에서 행 추가
4. ▼ 같은 탭에 업로드 (collection = system_instrument)
5. 기존 KbIngestWorker → 시트별/행별 청킹 → Qdrant 인덱싱
6. 채팅에서 즉시 search_kb로 활용
- "FT-6101 점검 이력" → instrument_id, parent_base_tag, description 청크 hit
- "ficq-6101 관련 계기" → parent_base_tag 매칭으로 4행(FT/FIC/FCV/Q) 회수
```
재실행 시: 운영자가 수정한 Excel을 시드로 받아 (`POST /api/kb/instruments/infer?seed_doc_id=...`) **사용자 수정사항 보존 + 신규 base_tag만 추가** 모드.
---
## 10. 구현 단계
### Phase A — 룰 엔진 + Excel 생성 (0.5일)
- `prompts/instrument_inference.yaml` 작성 (§6)
- `mcp-server/instrument_inference/` 신규 모듈
- `rules.py` — YAML 로더
- `infer.py``infer_instruments_for_base_tag()` (§7)
- `excel.py` — openpyxl 기반 3시트 생성기
- `@mcp.tool() infer_field_instruments(use_llm=False, seed_doc_id=None)` 신규 도구
- 단위 테스트 10건 (ficq, xv, pic, tic, ts, lt, fy, km, 비표준 prefix, area 누락)
### Phase B — API + UI (0.3일)
- `GET /api/kb/instruments/infer/start` (admin) — 백그라운드 잡 ID 반환
- `GET /api/kb/instruments/infer/status/{jobId}` — 진행률
- `GET /api/kb/instruments/infer/download/{jobId}` — xlsx 스트림
- 14번 탭: "Field Instrument 초안" 버튼 + 진행률 카드 + 다운로드 링크
- 업로드는 **기존 14번 탭 업로드** 그대로 (system_instrument 컬렉션 선택)
### Phase C — LLM 보강 (0.2일, 옵션)
- `infer.py``enrich_with_llm(rows, batch=50)` 추가
- vLLM 호출, 캐시 dict
- 토글로 on/off
### Phase D — 검증/소비 강화 (0.2일)
- `search_kb``system_instrument` 컬렉션 우선 가중 (이미 가능, 가중치만)
- `ToolGuideKo`에 "FT/FCV 등 현장 계기명 질의는 system_instrument 컬렉션 우선" 한 줄 추가
- 채팅 답변에 instrument hit 시 `parent_base_tag` 동시 노출
### Phase E (선택) — diff 도구
- 운영자가 수정한 Excel ↔ 룰 출력 비교 → 어떤 룰을 추가/수정하면 되는지 리포트
- 룰 진화 피드백 루프
총 1.2일 (옵션 제외 0.8일).
---
## 11. vault 마이그레이션 시 변환 (미래)
> 옵시디언 vault 구현이 시작되면 이 Excel이 **그대로 시드**.
| Excel 열 | vault 변환 |
|---------|----------|
| `instrument_id` | `instrument/{id}` 노트 ID |
| `display_name` | `title` |
| `role` | frontmatter `role` |
| `parent_base_tag` | 본문에 `[[tag/{parent}]]` 위키링크 |
| `loop`, `area` | frontmatter |
| `measures`, `data_points` | frontmatter |
| `from` (vault id 형식) | 본문 `## 입력` 섹션에 `[[from]]` |
| `to` (vault id 형식) | 본문 `## 출력` 섹션에 `[[to]]` |
| `description` | 본문 첫 단락 |
| `operator_notes` | 본문 `## 운영자 메모` 섹션 |
→ 변환기 1개로 노트 N개 + `vault_links` M개 자동 생성. Excel 수정 이력이 그대로 그래프가 됨.
---
## 12. 산출물 미리보기 (작업 후 KB가 답할 수 있는 질문)
| 질의 | 응답 가능성 (전) | 응답 가능성 (후) |
|------|----------------|----------------|
| "FT-6101 어디 붙어있어?" | ❌ (계기 매핑 없음) | ✅ parent_base_tag + area + from |
| "FCV-6101은 뭐가 제어해?" | ❌ | ✅ `to: tag/ficq-6101` 백트래킹 |
| "ficq-6101 관련 현장 계기 다 보여줘" | △ description ILIKE만 | ✅ parent_base_tag로 4행 일괄 |
| "ESD 차단밸브 목록" | ❌ | ✅ role=shutdown-valve 필터 |
| "FT-6101 점검 이력" | △ (운영자 노트가 KB 어딘가에 있다면) | ✅ 동일 + operator_notes 청크 매칭 |
---
## 13. 잔여 결정 (사용자 확인 필요)
| 항목 | 옵션 | 추천 |
|------|------|------|
| 첫 실행 LLM 사용 | (A) off (B) description만 on | **A** — 빠른 1차 산출, 운영자 작업 시작 |
| Excel 컬렉션 | `system_instrument` | 기존 시드 그대로 |
| ID 정규화 정책 | 무조건 소문자 kebab | 운영자가 대문자 입력해도 자동 변환 |
| 운영자 신규 role 허용 | 허용(경고만) / 차단 | **허용** — frontmatter 자유도 유지 |
| `from`/`to` 미입력 시 | placeholder 자동 / 공란 | **placeholder** — vault 시 빨간 링크로 가시화 |
| 재실행 정책 | 매번 새 Excel / seed_doc_id 기반 머지 | **머지** — 운영자 수정 보존 |
| unmatched_tags 시트 알림 | UI에 카운트 | 운영자 행동 유도 |
---
## 14. 핵심 메시지
- **신규 인프라 0** — 기존 xlsx 인덱싱·KB 업로드·Qdrant 컬렉션 그대로 활용.
- **롱 포맷 + role/from/to** — 단순 매핑 표가 아니라 **계기 그래프의 노드+엣지**. vault로 그대로 이전.
- **휴먼-인-더-루프** — LLM은 초안만, 운영자는 검증만. 백지에서 작성하는 것 대비 100배 빠름.
- **재실행 안전** — 룰 개선 시 운영자 수정 보존하고 신규만 추가.
- **즉시 효과** — 업로드 직후 채팅이 현장 계기명으로 답할 수 있음. 옵시디언 vault 풀구현을 기다리지 않아도 됨.

View File

@@ -0,0 +1,14 @@
# 현재 운엉상의 문제점
개선안 1) RAG관리 -> 초안생성시 : 계기 유추 전혀 안됨, FICQ-6101 로 부터 FT-6101, FCV-6101를 유추해서 초안에 만들어 줘야 함
예) 1. FICQ-XXXXX, FIC-XXXXX -> FT-6101, FCV-6101 유추, PICX-> PT & PCV, LICX->LT & LCV 등등 ( 태그명 XXCXX- 'C'가 있으면 CONTROL VALVE가 있다고 생각)
2. FI-XXXXX, FIA-XXXXX -> FT-XXXXX
3. TI-XXXXX, TIA-XXXXX, TIS-XXXXX -> TE-XXXXX (Temperature Element)
4. LI-XXXXX, LIA-XXXXX, LIS-XXXXX -> LT-XXXXX
5. PI-XXXXX, PIA-XXXXX, PIS-XXXXX -> PT-XXXXX
수정완료[x]
개선안 2) :
1. display_name : FT-10114a --> 모두 대문자로
2. 규칙에서 어긋난 태그명 : unmatched_tags로 분류할 것, fica-3102_op -> ft-3102_op, fcv-3102_op
3. lt-9113-lo-esd, lcv-9113-lo-esd 등등 %-esd% 인 것들 --> 시스템 포인트 임 계기 아님

View File

@@ -0,0 +1,680 @@
# No-10 플랜트 P&ID 추출 데이터
**도면**: `No-10_Plant_PID.dxf` (DXF)
**추출 일자**: 2026-05-14
**데이터 소스**: PostgreSQL `pid_equipment` 테이블 (460건, is_active=true)
**용도**: RAG 검색 보조용 — 운전원 질의 시 P&ID 컨텍스트 자동 인용
---
## 통계
| 구분 | 수량 |
|---|---|
| **파이프 (LineNo)** | 247 |
| **장비** | 78 |
| **계기** | 110 |
| **미분류** | 25 |
| **Experion 매핑 완료** | 25 |
### 영역(area)별 분포
| area 추정 | 항목 수 | 비고 |
|---|---|---|
| `10` | 318 | 태그 prefix 숫자 기반 추정 |
| `91` | 19 | 태그 prefix 숫자 기반 추정 |
| `61` | 17 | 태그 prefix 숫자 기반 추정 |
| `94` | 16 | 태그 prefix 숫자 기반 추정 |
| `1` | 13 | 태그 prefix 숫자 기반 추정 |
| `21` | 13 | 태그 prefix 숫자 기반 추정 |
| `62` | 12 | 태그 prefix 숫자 기반 추정 |
| `92` | 9 | 태그 prefix 숫자 기반 추정 |
| `51` | 8 | 태그 prefix 숫자 기반 추정 |
| `9` | 7 | 태그 prefix 숫자 기반 추정 |
| `29` | 5 | 태그 prefix 숫자 기반 추정 |
| `2` | 4 | 태그 prefix 숫자 기반 추정 |
| `32` | 3 | 태그 prefix 숫자 기반 추정 |
| `34` | 3 | 태그 prefix 숫자 기반 추정 |
| `0` | 2 | 태그 prefix 숫자 기반 추정 |
| `27` | 2 | 태그 prefix 숫자 기반 추정 |
| `31` | 2 | 태그 prefix 숫자 기반 추정 |
| `81` | 2 | 태그 prefix 숫자 기반 추정 |
| `53` | 1 | 태그 prefix 숫자 기반 추정 |
| `66` | 1 | 태그 prefix 숫자 기반 추정 |
| `7` | 1 | 태그 prefix 숫자 기반 추정 |
| `90` | 1 | 태그 prefix 숫자 기반 추정 |
| `99` | 1 | 태그 prefix 숫자 기반 추정 |
### 파이프 서비스(fluid) 분포
| service 코드 | 의미 | 수량 |
|---|---|---|
| `P` | PROCESS FLUID | 90 |
| `VG` | VENT GAS | 46 |
| `CWS` | COOLING WATER SUPPLY | 24 |
| `CWR` | COOLING WATER RETURN | 22 |
| `IA` | INSTRUMENT AIR | 20 |
| `SW` | SOFT WATER | 12 |
| `SAM` | SAMPLE LINE | 9 |
| `CD` | STEAM CONDENSATE | 6 |
| `NBD` | NITROGEN BLOW DOWN | 5 |
| `ST` | STEAM | 3 |
| `WW` | WASTE WATER | 3 |
| `CHR` | CHILLED WATER RETURN | 2 |
| `CHS` | CHILLED WATER SUPPLY | 2 |
| `SC` | VENT GAS | 2 |
| `PW` | PW | 1 |
### 장비 종류 분포
| prefix | 의미 | 수량 |
|---|---|---|
| `T` | Tank | 20 |
| `E` | Heat Exchanger | 18 |
| `P` | Pump | 15 |
| `D` | Drum | 5 |
| `C` | Column | 4 |
| `F` | Filter | 4 |
| `K` | Compressor | 4 |
| `DP` | Drainage Point | 3 |
| `VP` | Vacuum Pump | 2 |
| `BT` | Buffer Tank | 1 |
| `CH` | Chiller | 1 |
| `CT` | Cooling Tower | 1 |
### 계기 종류 분포
| prefix | 의미 | 수량 |
|---|---|---|
| `FCV` | Flow Control Valve | 46 |
| `PSV` | Pressure Switch Valve | 27 |
| `PCV` | Pressure Control Valve | 11 |
| `LCV` | Level Control Valve | 10 |
| `TCV` | Temperature Control Valve | 9 |
| `TR` | Temperature Recorder | 3 |
| `PFD` | Pressure | 2 |
| `SP` | Speed | 2 |
---
## 장비 목록
### BT — Buffer Tank (1대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `BT-6200` | 62 | `—` | 0.950 |
### C — Column (4대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `C-10111` | 10 | `—` | 0.950 |
| `C-10211` | 10 | `—` | 0.950 |
| `C-9111` | 91 | `—` | 0.950 |
| `C-9128` | 91 | `—` | 0.950 |
### CH — Chiller (1대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `CH-6601` | 66 | `—` | 0.950 |
### CT — Cooling Tower (1대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `CT-10601` | 10 | `—` | 0.950 |
### D — Drum (5대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `D-10113` | 10 | `—` | 0.950 |
| `D-10213` | 10 | `—` | 0.950 |
| `D-10901` | 10 | `—` | 0.950 |
| `D-2901` | 29 | `—` | 0.950 |
| `D-901` | 9 | `—` | 0.950 |
### DP — Drainage Point (3대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `DP-10101` | 10 | `—` | 0.950 |
| `DP-10201` | 10 | `—` | 0.950 |
| `DP-3210` | 32 | `—` | 0.950 |
### E — Heat Exchanger (18대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `E-10103` | 10 | `—` | 0.950 |
| `E-10112` | 10 | `—` | 0.950 |
| `E-10115` | 10 | `—` | 0.950 |
| `E-10115A` | 10 | `—` | 0.950 |
| `E-10115B` | 10 | `—` | 0.950 |
| `E-10117` | 10 | `—` | 0.950 |
| `E-10119` | 10 | `—` | 0.950 |
| `E-10203` | 10 | `—` | 0.950 |
| `E-10212` | 10 | `—` | 0.950 |
| `E-10215` | 10 | `—` | 0.950 |
| `E-10217` | 10 | `—` | 0.950 |
| `E-10219` | 10 | `—` | 0.950 |
| `E-8115` | 81 | `—` | 0.950 |
| `E-9103` | 91 | `—` | 0.950 |
| `E-9112` | 91 | `—` | 0.950 |
| `E-9115` | 91 | `—` | 0.950 |
| `E-9203` | 92 | `—` | 0.950 |
| `E-9215` | 92 | `—` | 0.950 |
### F — Filter (4대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `F-10900` | 10 | `—` | 0.950 |
| `F-10952` | 10 | `—` | 0.950 |
| `F-2952` | 29 | `—` | 0.950 |
| `F-952` | 9 | `—` | 0.950 |
### K — Compressor (4대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `K-10901` | 10 | `—` | 0.500 |
| `K-2901` | 29 | `—` | 0.500 |
| `K-901A` | 9 | `—` | 0.500 |
| `K-901B` | 9 | `—` | 0.500 |
### P — Pump (15대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `P-10101` | 10 | `p-10101.pv` | 0.950 |
| `P-10114` | 10 | `p-10114.pv` | 0.950 |
| `P-10116` | 10 | `p-10116.pv` | 0.950 |
| `P-10118` | 10 | `p-10118.pv` | 0.950 |
| `P-10201` | 10 | `p-10201.pv` | 0.950 |
| `P-10214` | 10 | `p-10214.pv` | 0.950 |
| `P-10216` | 10 | `p-10216.pv` | 0.950 |
| `P-10218` | 10 | `p-10218.pv` | 0.950 |
| `P-10221` | 10 | `p-10221.pv` | 0.950 |
| `P-201` | 2 | `p-201.pv` | 0.950 |
| `P-202` | 2 | `p-202.pv` | 0.950 |
| `P-3101` | 31 | `—` | 0.950 |
| `P-5101` | 51 | `—` | 0.950 |
| `P-6101` | 61 | `p-6101.pv` | 0.950 |
| `P-9102` | 91 | `p-9102.pv` | 0.950 |
### T — Tank (20대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `T-10100` | 10 | `—` | 0.950 |
| `T-10101` | 10 | `—` | 0.950 |
| `T-10200` | 10 | `—` | 0.950 |
| `T-10201` | 10 | `—` | 0.950 |
| `T-10221` | 10 | `—` | 0.950 |
| `T-10800` | 10 | `—` | 0.950 |
| `T-201` | 2 | `—` | 0.950 |
| `T-202` | 2 | `—` | 0.950 |
| `T-2704` | 27 | `—` | 0.950 |
| `T-3101` | 31 | `—` | 0.950 |
| `T-3210` | 32 | `—` | 0.950 |
| `T-6121` | 61 | `—` | 0.950 |
| `T-6122` | 61 | `—` | 0.950 |
| `T-6125` | 61 | `—` | 0.950 |
| `T-6126` | 61 | `—` | 0.950 |
| `T-6222` | 62 | `—` | 0.950 |
| `T-8121` | 81 | `—` | 0.950 |
| `T-9123` | 91 | `—` | 0.950 |
| `T-9124` | 91 | `—` | 0.950 |
| `T-9125` | 91 | `—` | 0.950 |
### VP — Vacuum Pump (2대)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `VP-10117` | 10 | `vp-10117.pv` | 0.950 |
| `VP-10217` | 10 | `vp-10217.pv` | 0.950 |
---
## 계기 목록
### FCV — Flow Control Valve (46개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `FCV-10101` | 10 | `—` | 0.950 |
| `FCV-10113` | 10 | `—` | 0.950 |
| `FCV-10114A` | 10 | `—` | 0.950 |
| `FCV-10116` | 10 | `—` | 0.950 |
| `FCV-10118` | 10 | `—` | 0.950 |
| `FCV-10201` | 10 | `—` | 0.950 |
| `FCV-10213` | 10 | `—` | 0.950 |
| `FCV-10214` | 10 | `—` | 0.950 |
| `FCV-10216` | 10 | `—` | 0.950 |
| `FCV-10218` | 10 | `—` | 0.950 |
| `FCV-111` | 1 | `—` | 0.950 |
| `FCV-113` | 1 | `—` | 0.950 |
| `FCV-122` | 1 | `—` | 0.950 |
| `FCV-123` | 1 | `—` | 0.950 |
| `FCV-124` | 1 | `—` | 0.950 |
| `FCV-131` | 1 | `—` | 0.950 |
| `FCV-2111` | 21 | `—` | 0.950 |
| `FCV-2122` | 21 | `—` | 0.950 |
| `FCV-2123` | 21 | `—` | 0.950 |
| `FCV-2124` | 21 | `—` | 0.950 |
| `FCV-2131` | 21 | `—` | 0.950 |
| `FCV-5101` | 51 | `—` | 0.950 |
| `FCV-5113` | 51 | `—` | 0.950 |
| `FCV-5114` | 51 | `—` | 0.950 |
| `FCV-5116` | 51 | `—` | 0.950 |
| `FCV-5118` | 51 | `—` | 0.950 |
| `FCV-6101` | 61 | `—` | 0.950 |
| `FCV-6113` | 61 | `—` | 0.950 |
| `FCV-6114` | 61 | `—` | 0.950 |
| `FCV-6116` | 61 | `—` | 0.950 |
| `FCV-6118` | 61 | `—` | 0.950 |
| `FCV-6201` | 62 | `—` | 0.950 |
| `FCV-6213` | 62 | `—` | 0.950 |
| `FCV-6214` | 62 | `—` | 0.950 |
| `FCV-6216` | 62 | `—` | 0.950 |
| `FCV-6218` | 62 | `—` | 0.950 |
| `FCV-9101` | 91 | `—` | 0.950 |
| `FCV-9113` | 91 | `—` | 0.950 |
| `FCV-9114` | 91 | `—` | 0.950 |
| `FCV-9116` | 91 | `—` | 0.950 |
| `FCV-9118` | 91 | `—` | 0.950 |
| `FCV-9201` | 92 | `—` | 0.950 |
| `FCV-9213` | 92 | `—` | 0.950 |
| `FCV-9214` | 92 | `—` | 0.950 |
| `FCV-9216` | 92 | `—` | 0.950 |
| `FCV-9218` | 92 | `—` | 0.950 |
### LCV — Level Control Valve (10개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `LCV-111` | 1 | `—` | 0.950 |
| `LCV-113` | 1 | `—` | 0.950 |
| `LCV-121` | 1 | `—` | 0.950 |
| `LCV-131` | 1 | `—` | 0.950 |
| `LCV-2111` | 21 | `—` | 0.950 |
| `LCV-2113` | 21 | `—` | 0.950 |
| `LCV-2121` | 21 | `—` | 0.950 |
| `LCV-2131` | 21 | `—` | 0.950 |
| `LCV-2705` | 27 | `—` | 0.950 |
| `LCV-3402` | 34 | `—` | 0.950 |
### PCV — Pressure Control Valve (11개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `PCV-10111` | 10 | `—` | 0.950 |
| `PCV-10211` | 10 | `—` | 0.950 |
| `PCV-111` | 1 | `—` | 0.950 |
| `PCV-121` | 1 | `—` | 0.950 |
| `PCV-2111` | 21 | `—` | 0.950 |
| `PCV-2121` | 21 | `—` | 0.950 |
| `PCV-5111` | 51 | `—` | 0.950 |
| `PCV-6111` | 61 | `—` | 0.950 |
| `PCV-6211` | 62 | `—` | 0.950 |
| `PCV-9111` | 91 | `—` | 0.950 |
| `PCV-9211` | 92 | `—` | 0.950 |
### PFD — Pressure (2개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `PFD-001` | 0 | `—` | 0.950 |
| `PFD-002` | 0 | `—` | 0.950 |
### PSV — Pressure Switch Valve (27개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `PSV-10101` | 10 | `—` | 0.950 |
| `PSV-10103` | 10 | `—` | 0.950 |
| `PSV-10111` | 10 | `—` | 0.950 |
| `PSV-10112` | 10 | `—` | 0.950 |
| `PSV-10115` | 10 | `—` | 0.950 |
| `PSV-10117` | 10 | `—` | 0.950 |
| `PSV-10119A` | 10 | `—` | 0.950 |
| `PSV-10119B` | 10 | `—` | 0.950 |
| `PSV-10201` | 10 | `—` | 0.950 |
| `PSV-10203` | 10 | `—` | 0.950 |
| `PSV-10211` | 10 | `—` | 0.950 |
| `PSV-10212` | 10 | `—` | 0.950 |
| `PSV-10215` | 10 | `—` | 0.950 |
| `PSV-10217` | 10 | `—` | 0.950 |
| `PSV-10219A` | 10 | `—` | 0.950 |
| `PSV-10219B` | 10 | `—` | 0.950 |
| `PSV-10900` | 10 | `—` | 0.950 |
| `PSV-10900A` | 10 | `—` | 0.950 |
| `PSV-10900B` | 10 | `—` | 0.950 |
| `PSV-10901` | 10 | `—` | 0.950 |
| `PSV-10902` | 10 | `—` | 0.950 |
| `PSV-2900` | 29 | `—` | 0.950 |
| `PSV-2901` | 29 | `—` | 0.950 |
| `PSV-6203` | 62 | `—` | 0.950 |
| `PSV-900A` | 9 | `—` | 0.950 |
| `PSV-900B` | 9 | `—` | 0.950 |
| `PSV-901` | 9 | `—` | 0.950 |
### SP — Speed (2개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `SP-10601` | 10 | `—` | 0.950 |
| `SP-10602` | 10 | `—` | 0.950 |
### TCV — Temperature Control Valve (9개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `TCV-10111` | 10 | `—` | 0.950 |
| `TCV-10211` | 10 | `—` | 0.950 |
| `TCV-111` | 1 | `—` | 0.950 |
| `TCV-2111` | 21 | `—` | 0.950 |
| `TCV-5111` | 51 | `—` | 0.950 |
| `TCV-6111` | 61 | `—` | 0.950 |
| `TCV-6211` | 62 | `—` | 0.950 |
| `TCV-9111` | 91 | `—` | 0.950 |
| `TCV-9211` | 92 | `—` | 0.950 |
### TR — Temperature Recorder (3개)
| tag_no | area 추정 | Experion 매핑 | confidence |
|---|---|---|---|
| `TR-10115A` | 10 | `—` | 0.950 |
| `TR-10115B` | 10 | `—` | 0.950 |
| `TR-10215` | 10 | `—` | 0.950 |
---
## 파이프(LineNo) 목록
총 247개 파이프 라인. 각 라인은 `service-line_no-size-spec-insul` 형식으로 명명됨.
| tag_no | service (fluid) | line_no | size | material | flange | insul |
|---|---|---|---|---|---|---|
| `CD-10513-40A-S1A-H50` | CD (STEAM CONDENSATE) | 10513 | 40A | S | 1 | AH50 |
| `CD-10514-40A-S1A-H50` | CD (STEAM CONDENSATE) | 10514 | 40A | S | 1 | AH50 |
| `CD-10515-65A-S1A-H50` | CD (STEAM CONDENSATE) | 10515 | 65A | S | 1 | AH50 |
| `CD-10516-65A-S1A-H50` | CD (STEAM CONDENSATE) | 10516 | 65A | S | 1 | AH50 |
| `CD-10522-40A-S1A-H50` | CD (STEAM CONDENSATE) | 10522 | 40A | S | 1 | AH50 |
| `CD-10523-40A-S1A-H50` | CD (STEAM CONDENSATE) | 10523 | 40A | S | 1 | AH50 |
| `CHR-10641-65A-F1A-C50` | CHR (CHILLED WATER RETURN) | 10641 | 65A | F | 1 | AC50 |
| `CHR-10642-50A-F1A-C50` | CHR (CHILLED WATER RETURN) | 10642 | 50A | F | 1 | AC50 |
| `CHS-10631-65A-F1A-C50` | CHS (CHILLED WATER SUPPLY) | 10631 | 65A | F | 1 | AC50 |
| `CHS-10632-50A-F1A-C50` | CHS (CHILLED WATER SUPPLY) | 10632 | 50A | F | 1 | AC50 |
| `CWR-10620-300A-S2A-N` | CWR (COOLING WATER RETURN) | 10620 | 300A | S | 2 | AN |
| `CWR-10621-80A-S2A-N` | CWR (COOLING WATER RETURN) | 10621 | 80A | S | 2 | AN |
| `CWR-10621-80A-S2A-n` | CWR (COOLING WATER RETURN) | 10621 | 80A | S | 2 | An |
| `CWR-10622-200A-S2A-N` | CWR (COOLING WATER RETURN) | 10622 | 200A | S | 2 | AN |
| `CWR-10622-200A-S2A-n` | CWR (COOLING WATER RETURN) | 10622 | 200A | S | 2 | An |
| `CWR-10623-50A-S2A-N` | CWR (COOLING WATER RETURN) | 10623 | 50A | S | 2 | AN |
| `CWR-10623-50A-S2A-n` | CWR (COOLING WATER RETURN) | 10623 | 50A | S | 2 | An |
| `CWR-10624-150A-S2A-N` | CWR (COOLING WATER RETURN) | 10624 | 150A | S | 2 | AN |
| `CWR-10624-150A-S2A-n` | CWR (COOLING WATER RETURN) | 10624 | 150A | S | 2 | An |
| `CWR-10625-50A-F1A-N` | CWR (COOLING WATER RETURN) | 10625 | 50A | F | 1 | AN |
| `CWR-10625-50A-S2A-N` | CWR (COOLING WATER RETURN) | 10625 | 50A | S | 2 | AN |
| `CWR-10625-50A-S2A-n` | CWR (COOLING WATER RETURN) | 10625 | 50A | S | 2 | An |
| `CWR-10626-25A-F1A-N` | CWR (COOLING WATER RETURN) | 10626 | 25A | F | 1 | AN |
| `CWR-10626-25A-F1A-n` | CWR (COOLING WATER RETURN) | 10626 | 25A | F | 1 | An |
| `CWR-10626-25A-S2A-N` | CWR (COOLING WATER RETURN) | 10626 | 25A | S | 2 | AN |
| `CWR-10627-25A-F1A-N` | CWR (COOLING WATER RETURN) | 10627 | 25A | F | 1 | AN |
| `CWR-10627-25A-F1A-n` | CWR (COOLING WATER RETURN) | 10627 | 25A | F | 1 | An |
| `CWR-10627-25A-S2A-N` | CWR (COOLING WATER RETURN) | 10627 | 25A | S | 2 | AN |
| `CWR-10628-15A-F1A-N` | CWR (COOLING WATER RETURN) | 10628 | 15A | F | 1 | AN |
| `CWR-10628-25A-S2A-N` | CWR (COOLING WATER RETURN) | 10628 | 25A | S | 2 | AN |
| `CWR-10629-15A-F1A-N` | CWR (COOLING WATER RETURN) | 10629 | 15A | F | 1 | AN |
| `CWR-10629-25A-S2A-N` | CWR (COOLING WATER RETURN) | 10629 | 25A | S | 2 | AN |
| `CWS-10600-300A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10600 | 300A | S | 2 | AN |
| `CWS-10601-300A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10601 | 300A | S | 2 | AN |
| `CWS-10611-80A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10611 | 80A | S | 2 | AN |
| `CWS-10611-80A-S2A-n` | CWS (COOLING WATER SUPPLY) | 10611 | 80A | S | 2 | An |
| `CWS-10612-200A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10612 | 200A | S | 2 | AN |
| `CWS-10612-200A-S2A-n` | CWS (COOLING WATER SUPPLY) | 10612 | 200A | S | 2 | An |
| `CWS-10613-50A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10613 | 50A | S | 2 | AN |
| `CWS-10613-50A-S2A-n` | CWS (COOLING WATER SUPPLY) | 10613 | 50A | S | 2 | An |
| `CWS-10614-150A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10614 | 150A | S | 2 | AN |
| `CWS-10614-150A-S2A-n` | CWS (COOLING WATER SUPPLY) | 10614 | 150A | S | 2 | An |
| `CWS-10615-50A-F1A-N` | CWS (COOLING WATER SUPPLY) | 10615 | 50A | F | 1 | AN |
| `CWS-10615-50A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10615 | 50A | S | 2 | AN |
| `CWS-10615-50A-S2A-n` | CWS (COOLING WATER SUPPLY) | 10615 | 50A | S | 2 | An |
| `CWS-10616-25A-F1A-N` | CWS (COOLING WATER SUPPLY) | 10616 | 25A | F | 1 | AN |
| `CWS-10616-25A-F1A-n` | CWS (COOLING WATER SUPPLY) | 10616 | 25A | F | 1 | An |
| `CWS-10616-25A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10616 | 25A | S | 2 | AN |
| `CWS-10617-25A-F1A-N` | CWS (COOLING WATER SUPPLY) | 10617 | 25A | F | 1 | AN |
| `CWS-10617-25A-F1A-n` | CWS (COOLING WATER SUPPLY) | 10617 | 25A | F | 1 | An |
| `CWS-10617-25A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10617 | 25A | S | 2 | AN |
| `CWS-10618-15A-F1A-N` | CWS (COOLING WATER SUPPLY) | 10618 | 15A | F | 1 | AN |
| `CWS-10618-25A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10618 | 25A | S | 2 | AN |
| `CWS-10619-15A-F1A-N` | CWS (COOLING WATER SUPPLY) | 10619 | 15A | F | 1 | AN |
| `CWS-10619-25A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10619 | 25A | S | 2 | AN |
| `CWS-10620-300A-S2A-N` | CWS (COOLING WATER SUPPLY) | 10620 | 300A | S | 2 | AN |
| `IA-10900-25A-F1A-n` | IA (INSTRUMENT AIR) | 10900 | 25A | F | 1 | An |
| `IA-10901-15A-F1A-n` | IA (INSTRUMENT AIR) | 10901 | 15A | F | 1 | An |
| `IA-10902-15A-F1A-n` | IA (INSTRUMENT AIR) | 10902 | 15A | F | 1 | An |
| `IA-10903-15A-F1A-n` | IA (INSTRUMENT AIR) | 10903 | 15A | F | 1 | An |
| `IA-10904-15A-F1A-n` | IA (INSTRUMENT AIR) | 10904 | 15A | F | 1 | An |
| `IA-10905-15A-F1A-n` | IA (INSTRUMENT AIR) | 10905 | 15A | F | 1 | An |
| `IA-10906-15A-F1A-n` | IA (INSTRUMENT AIR) | 10906 | 15A | F | 1 | An |
| `IA-10907-15A-F1A-n` | IA (INSTRUMENT AIR) | 10907 | 15A | F | 1 | An |
| `IA-10908-15A-F1A-n` | IA (INSTRUMENT AIR) | 10908 | 15A | F | 1 | An |
| `IA-10909-15A-F1A-n` | IA (INSTRUMENT AIR) | 10909 | 15A | F | 1 | An |
| `IA-10910-15A-F1A-n` | IA (INSTRUMENT AIR) | 10910 | 15A | F | 1 | An |
| `IA-10912-15A-F1A-n` | IA (INSTRUMENT AIR) | 10912 | 15A | F | 1 | An |
| `IA-10913-25A-F1A-n` | IA (INSTRUMENT AIR) | 10913 | 25A | F | 1 | An |
| `IA-10914-25A-F1A-n` | IA (INSTRUMENT AIR) | 10914 | 25A | F | 1 | An |
| `IA-10915-15A-F1A-n` | IA (INSTRUMENT AIR) | 10915 | 15A | F | 1 | An |
| `IA-10918-15A-F1A-n` | IA (INSTRUMENT AIR) | 10918 | 15A | F | 1 | An |
| `IA-10919-15A-F1A-n` | IA (INSTRUMENT AIR) | 10919 | 15A | F | 1 | An |
| `IA-10920-15A-F1A-n` | IA (INSTRUMENT AIR) | 10920 | 15A | F | 1 | An |
| `IA-10921-15A-F1A-n` | IA (INSTRUMENT AIR) | 10921 | 15A | F | 1 | An |
| `IA-10922-25A-F1A-n` | IA (INSTRUMENT AIR) | 10922 | 25A | F | 1 | An |
| `NBD-10100` | NBD (NITROGEN BLOW DOWN) | 10100 | — | — | — | — |
| `NBD-10101` | NBD (NITROGEN BLOW DOWN) | 10101 | — | — | — | — |
| `NBD-10200` | NBD (NITROGEN BLOW DOWN) | 10200 | — | — | — | — |
| `NBD-10201` | NBD (NITROGEN BLOW DOWN) | 10201 | — | — | — | — |
| `NBD-10221` | NBD (NITROGEN BLOW DOWN) | 10221 | — | — | — | — |
| `P-10101-25A-F1A-n` | P (PROCESS FLUID) | 10101 | 25A | F | 1 | An |
| `P-10102-25A-F1A-n` | P (PROCESS FLUID) | 10102 | 25A | F | 1 | An |
| `P-10103-25A-F1A-n` | P (PROCESS FLUID) | 10103 | 25A | F | 1 | An |
| `P-10104-25A-F1A-n` | P (PROCESS FLUID) | 10104 | 25A | F | 1 | An |
| `P-10106-25A-F1A-n` | P (PROCESS FLUID) | 10106 | 25A | F | 1 | An |
| `P-10107-40A-F1A-n` | P (PROCESS FLUID) | 10107 | 40A | F | 1 | An |
| `P-10109-40A-F1A-n` | P (PROCESS FLUID) | 10109 | 40A | F | 1 | An |
| `P-10110-40A-F1A-n` | P (PROCESS FLUID) | 10110 | 40A | F | 1 | An |
| `P-10111-25A-F1A-n` | P (PROCESS FLUID) | 10111 | 25A | F | 1 | An |
| `P-10112-25A-F1A-n` | P (PROCESS FLUID) | 10112 | 25A | F | 1 | An |
| `P-10113-25A-F1A-H50` | P (PROCESS FLUID) | 10113 | 25A | F | 1 | AH50 |
| `P-10114-250A-F2A-H50` | P (PROCESS FLUID) | 10114 | 250A | F | 2 | AH50 |
| `P-10115-200A-F2A-H50` | P (PROCESS FLUID) | 10115 | 200A | F | 2 | AH50 |
| `P-10116-200A-F2A-H50` | P (PROCESS FLUID) | 10116 | 200A | F | 2 | AH50 |
| `P-10117-300A-F2A-H50` | P (PROCESS FLUID) | 10117 | 300A | F | 2 | AH50 |
| `P-10117-500A-F2A-H50` | P (PROCESS FLUID) | 10117 | 500A | F | 2 | AH50 |
| `P-10118-300A-F2A-H50` | P (PROCESS FLUID) | 10118 | 300A | F | 2 | AH50 |
| `P-10119-32A-F1A-H50` | P (PROCESS FLUID) | 10119 | 32A | F | 1 | AH50 |
| `P-10120-25A-F1A-H50` | P (PROCESS FLUID) | 10120 | 25A | F | 1 | AH50 |
| `P-10121-25A-F1A-H50` | P (PROCESS FLUID) | 10121 | 25A | F | 1 | AH50 |
| `P-10122-20A-F1A-n` | P (PROCESS FLUID) | 10122 | 20A | F | 1 | An |
| `P-10122-25A-F1A-N` | P (PROCESS FLUID) | 10122 | 25A | F | 1 | AN |
| `P-10123-20A-F1A-n` | P (PROCESS FLUID) | 10123 | 20A | F | 1 | An |
| `P-10124-20A-F1A-n` | P (PROCESS FLUID) | 10124 | 20A | F | 1 | An |
| `P-10125-20A-F1A-n` | P (PROCESS FLUID) | 10125 | 20A | F | 1 | An |
| `P-10126-20A-F1A-n` | P (PROCESS FLUID) | 10126 | 20A | F | 1 | An |
| `P-10127-65A-F2A-n` | P (PROCESS FLUID) | 10127 | 65A | F | 2 | An |
| `P-10128-50A-F2A-n` | P (PROCESS FLUID) | 10128 | 50A | F | 2 | An |
| `P-10129-25A-F2A-n` | P (PROCESS FLUID) | 10129 | 25A | F | 2 | An |
| `P-10130-25A-F2A-n` | P (PROCESS FLUID) | 10130 | 25A | F | 2 | An |
| `P-10132-25A-F2A-n` | P (PROCESS FLUID) | 10132 | 25A | F | 2 | An |
| `P-10133-25A-F2A-n` | P (PROCESS FLUID) | 10133 | 25A | F | 2 | An |
| `P-10134-25A-F2A-n` | P (PROCESS FLUID) | 10134 | 25A | F | 2 | An |
| `P-10135-25A-F2A-n` | P (PROCESS FLUID) | 10135 | 25A | F | 2 | An |
| `P-10136-25A-F2A-n` | P (PROCESS FLUID) | 10136 | 25A | F | 2 | An |
| `P-10137-25A-F1A-n` | P (PROCESS FLUID) | 10137 | 25A | F | 1 | An |
| `P-10138-600A-F2A-H100` | P (PROCESS FLUID) | 10138 | 600A | F | 2 | AH100 |
| `P-10139-600A-F2A-n` | P (PROCESS FLUID) | 10139 | 600A | F | 2 | An |
| `P-10140-40A-F2A-n` | P (PROCESS FLUID) | 10140 | 40A | F | 2 | An |
| `P-10141-32A-F2A-n` | P (PROCESS FLUID) | 10141 | 32A | F | 2 | An |
| `P-10142-25A-F2A-n` | P (PROCESS FLUID) | 10142 | 25A | F | 2 | An |
| `P-10143-32A-F2A-n` | P (PROCESS FLUID) | 10143 | 32A | F | 2 | An |
| `P-10144-25A-F1A-n` | P (PROCESS FLUID) | 10144 | 25A | F | 1 | An |
| `P-10146-20A-F1A-n` | P (PROCESS FLUID) | 10146 | 20A | F | 1 | An |
| `P-10147-25A-F1A-n` | P (PROCESS FLUID) | 10147 | 25A | F | 1 | An |
| `P-10148-25A-F1A-n` | P (PROCESS FLUID) | 10148 | 25A | F | 1 | An |
| `P-10149-40A-F1A-n` | P (PROCESS FLUID) | 10149 | 40A | F | 1 | An |
| `P-10150-25A-F1A-n` | P (PROCESS FLUID) | 10150 | 25A | F | 1 | An |
| `P-10151-15A-F1A-n` | P (PROCESS FLUID) | 10151 | 15A | F | 1 | An |
| `P-10201-25A-F1A-n` | P (PROCESS FLUID) | 10201 | 25A | F | 1 | An |
| `P-10202-40A-F1A-n` | P (PROCESS FLUID) | 10202 | 40A | F | 1 | An |
| `P-10203-40A-F1A-n` | P (PROCESS FLUID) | 10203 | 40A | F | 1 | An |
| `P-10204-25A-F1A-n` | P (PROCESS FLUID) | 10204 | 25A | F | 1 | An |
| `P-10205-25A-F1A-n` | P (PROCESS FLUID) | 10205 | 25A | F | 1 | An |
| `P-10207-25A-F1A-H50` | P (PROCESS FLUID) | 10207 | 25A | F | 1 | AH50 |
| `P-10208-400A-F2A-H50` | P (PROCESS FLUID) | 10208 | 400A | F | 2 | AH50 |
| `P-10209-200A-F2A-H50` | P (PROCESS FLUID) | 10209 | 200A | F | 2 | AH50 |
| `P-10210-25A-F1A-H50` | P (PROCESS FLUID) | 10210 | 25A | F | 1 | AH50 |
| `P-10211-20A-F1A-H50` | P (PROCESS FLUID) | 10211 | 20A | F | 1 | AH50 |
| `P-10212-20A-F1A-H50` | P (PROCESS FLUID) | 10212 | 20A | F | 1 | AH50 |
| `P-10213-20A-F1A-n` | P (PROCESS FLUID) | 10213 | 20A | F | 1 | An |
| `P-10214-20A-F1A-n` | P (PROCESS FLUID) | 10214 | 20A | F | 1 | An |
| `P-10215-20A-F1A-n` | P (PROCESS FLUID) | 10215 | 20A | F | 1 | An |
| `P-10216-20A-F1A-n` | P (PROCESS FLUID) | 10216 | 20A | F | 1 | An |
| `P-10217-65A-F2A-n` | P (PROCESS FLUID) | 10217 | 65A | F | 2 | An |
| `P-10218-40A-F2A-n` | P (PROCESS FLUID) | 10218 | 40A | F | 2 | An |
| `P-10219-25A-F2A-n` | P (PROCESS FLUID) | 10219 | 25A | F | 2 | An |
| `P-10220-25A-F1A-n` | P (PROCESS FLUID) | 10220 | 25A | F | 1 | An |
| `P-10221-25A-F2A-n` | P (PROCESS FLUID) | 10221 | 25A | F | 2 | An |
| `P-10222-25A-F2A-n` | P (PROCESS FLUID) | 10222 | 25A | F | 2 | An |
| `P-10223-25A-F2A-n` | P (PROCESS FLUID) | 10223 | 25A | F | 2 | An |
| `P-10224-40A-F2A-n` | P (PROCESS FLUID) | 10224 | 40A | F | 2 | An |
| `P-10225-25A-F2A-n` | P (PROCESS FLUID) | 10225 | 25A | F | 2 | An |
| `P-10226-25A-F2A-n` | P (PROCESS FLUID) | 10226 | 25A | F | 2 | An |
| `P-10227-25A-F2A-n` | P (PROCESS FLUID) | 10227 | 25A | F | 2 | An |
| `P-10228-500A-F2A-H100` | P (PROCESS FLUID) | 10228 | 500A | F | 2 | AH100 |
| `P-10228-500A-F2A-n` | P (PROCESS FLUID) | 10228 | 500A | F | 2 | An |
| `P-10229-40A-F2A-n` | P (PROCESS FLUID) | 10229 | 40A | F | 2 | An |
| `P-10230-25A-F2A-n` | P (PROCESS FLUID) | 10230 | 25A | F | 2 | An |
| `P-10231-15A-F2A-n` | P (PROCESS FLUID) | 10231 | 15A | F | 2 | An |
| `P-10232-25A-F1A-n` | P (PROCESS FLUID) | 10232 | 25A | F | 1 | An |
| `P-10233-25A-F2A-n` | P (PROCESS FLUID) | 10233 | 25A | F | 2 | An |
| `P-10234-15A-F1A-n` | P (PROCESS FLUID) | 10234 | 15A | F | 1 | An |
| `P-10235-15A-F1A-n` | P (PROCESS FLUID) | 10235 | 15A | F | 1 | An |
| `P-10236-40A-F1A-n` | P (PROCESS FLUID) | 10236 | 40A | F | 1 | An |
| `P-10237-40A-F1A-n` | P (PROCESS FLUID) | 10237 | 40A | F | 1 | An |
| `P-10311-125A-F1A-n` | P (PROCESS FLUID) | 10311 | 125A | F | 1 | An |
| `P-10312-100A-F1A-n` | P (PROCESS FLUID) | 10312 | 100A | F | 1 | An |
| `P-10313-100A-F1A-n` | P (PROCESS FLUID) | 10313 | 100A | F | 1 | An |
| `P-10314-80A-F1A-n` | P (PROCESS FLUID) | 10314 | 80A | F | 1 | An |
| `PW-10903-25A-F1A-E40` | PW (PW) | 10903 | 25A | F | 1 | AE40 |
| `SAM-10951-10A-F2A-n` | SAM (SAMPLE LINE) | 10951 | 10A | F | 2 | An |
| `SAM-10952-10A-F2A-n` | SAM (SAMPLE LINE) | 10952 | 10A | F | 2 | An |
| `SAM-10953-10A-F2A-n` | SAM (SAMPLE LINE) | 10953 | 10A | F | 2 | An |
| `SAM-10954-10A-F2A-n` | SAM (SAMPLE LINE) | 10954 | 10A | F | 2 | An |
| `SAM-10955-10A-F2A-n` | SAM (SAMPLE LINE) | 10955 | 10A | F | 2 | An |
| `SAM-10956-10A-F2A-n` | SAM (SAMPLE LINE) | 10956 | 10A | F | 2 | An |
| `SAM-10957-10A-F2A-n` | SAM (SAMPLE LINE) | 10957 | 10A | F | 2 | An |
| `SAM-10958-10A-F2A-n` | SAM (SAMPLE LINE) | 10958 | 10A | F | 2 | An |
| `SAM-9954-10A-F2A-n` | SAM (SAMPLE LINE) | 9954 | 10A | F | 2 | An |
| `SC-10128` | SC (VENT GAS) | 10128 | — | — | — | — |
| `SC-9128` | SC (VENT GAS) | 9128 | — | — | — | — |
| `ST-10511-100A-S1A-H50` | ST (STEAM) | 10511 | 100A | S | 1 | AH50 |
| `ST-10512-100A-S1A-H50` | ST (STEAM) | 10512 | 100A | S | 1 | AH50 |
| `ST-10521-65A-S1A-H50` | ST (STEAM) | 10521 | 65A | S | 1 | AH50 |
| `SW-10801-50A-F1A-E50` | SW (SOFT WATER) | 10801 | 50A | F | 1 | AE50 |
| `SW-10802-25A-F1A-E50` | SW (SOFT WATER) | 10802 | 25A | F | 1 | AE50 |
| `SW-10803-20A-F1A-E50` | SW (SOFT WATER) | 10803 | 20A | F | 1 | AE50 |
| `SW-10804-25A-F1A-E50` | SW (SOFT WATER) | 10804 | 25A | F | 1 | AE50 |
| `SW-10805-25A-F1A-E50` | SW (SOFT WATER) | 10805 | 25A | F | 1 | AE50 |
| `SW-10806-25A-F1A-E50` | SW (SOFT WATER) | 10806 | 25A | F | 1 | AE50 |
| `SW-10807-25A-F1A-E50` | SW (SOFT WATER) | 10807 | 25A | F | 1 | AE50 |
| `SW-10808-25A-F1A-E50` | SW (SOFT WATER) | 10808 | 25A | F | 1 | AE50 |
| `SW-10809-15A-F1A-E50` | SW (SOFT WATER) | 10809 | 15A | F | 1 | AE50 |
| `SW-10810-25A-F1A-E50` | SW (SOFT WATER) | 10810 | 25A | F | 1 | AE50 |
| `SW-10821-50A-F1A-E50` | SW (SOFT WATER) | 10821 | 50A | F | 1 | AE50 |
| `SW-10822-32A-F1A-E50` | SW (SOFT WATER) | 10822 | 32A | F | 1 | AE50 |
| `VG-10401-150A-F1A-N` | VG (VENT GAS) | 10401 | 150A | F | 1 | AN |
| `VG-10411-65A-F1A-N` | VG (VENT GAS) | 10411 | 65A | F | 1 | AN |
| `VG-10411-65A-F1A-n` | VG (VENT GAS) | 10411 | 65A | F | 1 | An |
| `VG-10412-50A-F1A-N` | VG (VENT GAS) | 10412 | 50A | F | 1 | AN |
| `VG-10412-50A-F1A-n` | VG (VENT GAS) | 10412 | 50A | F | 1 | An |
| `VG-10421-50A-F1A-N` | VG (VENT GAS) | 10421 | 50A | F | 1 | AN |
| `VG-10421-50A-F1A-n` | VG (VENT GAS) | 10421 | 50A | F | 1 | An |
| `VG-10422-50A-F1A-N` | VG (VENT GAS) | 10422 | 50A | F | 1 | AN |
| `VG-10422-50A-F1A-n` | VG (VENT GAS) | 10422 | 50A | F | 1 | An |
| `VG-10423-50A-F1A-N` | VG (VENT GAS) | 10423 | 50A | F | 1 | AN |
| `VG-10424-50A-F1A-N` | VG (VENT GAS) | 10424 | 50A | F | 1 | AN |
| `VG-10425-50A-F1A-N` | VG (VENT GAS) | 10425 | 50A | F | 1 | AN |
| `VG-10426-50A-F1A-N` | VG (VENT GAS) | 10426 | 50A | F | 1 | AN |
| `VG-10431-50A-F1A-N` | VG (VENT GAS) | 10431 | 50A | F | 1 | AN |
| `VG-10431-50A-F1A-n` | VG (VENT GAS) | 10431 | 50A | F | 1 | An |
| `VG-10432-50A-F1A-N` | VG (VENT GAS) | 10432 | 50A | F | 1 | AN |
| `VG-10432-50A-F1A-n` | VG (VENT GAS) | 10432 | 50A | F | 1 | An |
| `VG-10433-50A-F1A-N` | VG (VENT GAS) | 10433 | 50A | F | 1 | AN |
| `VG-10433-50A-F1A-n` | VG (VENT GAS) | 10433 | 50A | F | 1 | An |
| `VG-10440-300A-F1A-N` | VG (VENT GAS) | 10440 | 300A | F | 1 | AN |
| `VG-10441-125A-F1A-N` | VG (VENT GAS) | 10441 | 125A | F | 1 | AN |
| `VG-10441-200A-F1A-N` | VG (VENT GAS) | 10441 | 200A | F | 1 | AN |
| `VG-10441-200A-F1A-n` | VG (VENT GAS) | 10441 | 200A | F | 1 | An |
| `VG-10442-100A-F1A-N` | VG (VENT GAS) | 10442 | 100A | F | 1 | AN |
| `VG-10442-150A-F1A-N` | VG (VENT GAS) | 10442 | 150A | F | 1 | AN |
| `VG-10443-25A-F1A-N` | VG (VENT GAS) | 10443 | 25A | F | 1 | AN |
| `VG-10443-25A-F1A-n` | VG (VENT GAS) | 10443 | 25A | F | 1 | An |
| `VG-10444-25A-F1A-N` | VG (VENT GAS) | 10444 | 25A | F | 1 | AN |
| `VG-10444-25A-F1A-n` | VG (VENT GAS) | 10444 | 25A | F | 1 | An |
| `VG-6203-15A-F1A-n` | VG (VENT GAS) | 6203 | 15A | F | 1 | An |
| `VG-9400-150A-F1A-N` | VG (VENT GAS) | 9400 | 150A | F | 1 | AN |
| `VG-9411-50A-F1A-N` | VG (VENT GAS) | 9411 | 50A | F | 1 | AN |
| `VG-9412-50A-F1A-N` | VG (VENT GAS) | 9412 | 50A | F | 1 | AN |
| `VG-9421-50A-F1A-N` | VG (VENT GAS) | 9421 | 50A | F | 1 | AN |
| `VG-9423-50A-F1A-N` | VG (VENT GAS) | 9423 | 50A | F | 1 | AN |
| `VG-9424-50A-F1A-N` | VG (VENT GAS) | 9424 | 50A | F | 1 | AN |
| `VG-9425-50A-F1A-N` | VG (VENT GAS) | 9425 | 50A | F | 1 | AN |
| `VG-9426-50A-F1A-N` | VG (VENT GAS) | 9426 | 50A | F | 1 | AN |
| `VG-9431-50A-F1A-N` | VG (VENT GAS) | 9431 | 50A | F | 1 | AN |
| `VG-9432-50A-F1A-N` | VG (VENT GAS) | 9432 | 50A | F | 1 | AN |
| `VG-9433-50A-F1A-N` | VG (VENT GAS) | 9433 | 50A | F | 1 | AN |
| `VG-9434-50A-F1A-N` | VG (VENT GAS) | 9434 | 50A | F | 1 | AN |
| `VG-9440-300A-F1A-N` | VG (VENT GAS) | 9440 | 300A | F | 1 | AN |
| `VG-9441-125A-F1A-N` | VG (VENT GAS) | 9441 | 125A | F | 1 | AN |
| `VG-9441-150A-F1A-n` | VG (VENT GAS) | 9441 | 150A | F | 1 | An |
| `VG-9442-65A-F1A-N` | VG (VENT GAS) | 9442 | 65A | F | 1 | AN |
| `WW-10191-25A-F1A-E50` | WW (WASTE WATER) | 10191 | 25A | F | 1 | AE50 |
| `WW-10193-25A-F1A-N` | WW (WASTE WATER) | 10193 | 25A | F | 1 | AN |
| `WW-9193-25A-F1A-N` | WW (WASTE WATER) | 9193 | 25A | F | 1 | AN |
---
## 미분류 항목 (25건)
| tag_no | instrument_type | line_number | confidence |
|---|---|---|---|
| `BV-10100` | BV | — | 0.500 |
| `BV-10101` | BV | — | 0.500 |
| `BV-10200` | BV | — | 0.500 |
| `BV-10201` | BV | — | 0.500 |
| `BV-10221` | BV | — | 0.500 |
| `KA-10901` | KA | — | 0.500 |
| `KD-10901` | KD | — | 0.500 |
| `KF-10901A` | KF | — | 0.500 |
| `KF-10901B` | KF | — | 0.500 |
| `UFD-9005` | UFD | — | 0.500 |
| `XV-10111` | XV | — | 0.500 |
| `XV-10211` | XV | — | 0.500 |
| `XV-2136` | XV | — | 0.500 |
| `XV-3208` | XV | — | 0.500 |
| `XV-3402` | XV | — | 0.500 |
| `XV-3470` | XV | — | 0.500 |
| `XV-5320` | XV | — | 0.500 |
| `XV-6120` | XV | — | 0.500 |
| `XV-6121` | XV | — | 0.500 |
| `XV-6122` | XV | — | 0.500 |
| `XV-6125` | XV | — | 0.500 |
| `XV-6126` | XV | — | 0.500 |
| `XV-6220` | XV | — | 0.500 |
| `XV-705` | XV | — | 0.500 |
| `XV-9125` | XV | — | 0.500 |

View File

@@ -0,0 +1,145 @@
# 결정 보류 2건 — 사용자 액션 가이드
> 작성일: 2026-05-14
> 대상: `plans/빅피클-잔여작업-코딩계획.md` 7번(현장 재고), 8번(BGE-M3)
> 상태: 코드 작업 불필요. **사용자 의사 결정과 운영 액션이 필요**.
---
## 1. 현장 재고 데이터 출처 결정
### 현황
Phase 0 설계서(G3)에서 "현장 재고 데이터 자체가 시스템에 없음"으로 식별됨.
운전원이 "P-6201 펌프 예비품 재고", "교체용 PT100 센서 수량" 같은 질문을 했을 때
참조할 데이터 소스가 없어 답변 불가.
### 결정해야 할 것
| 옵션 | 설명 | 사용자 액션 |
|------|------|------------|
| **A. KB 업로드 (권장)** | 엑셀/CSV/PDF 재고 대장을 `plant_operation` 또는 `report` 컬렉션에 업로드 → 즉시 RAG 검색 가능 | 1) 재고 대장 파일 확보 (관리부/구매팀 협조)<br>2) 컬럼 구조 정리 (품목·규격·수량·위치·재발주점)<br>3) 14번 탭 "RAG 관리" → 업로드 |
| **B. ERP/MES API 연동** | 외부 시스템에서 실시간 재고 조회 | 1) ERP 운영팀과 API 협의<br>2) 인증/네트워크 방화벽 정책<br>3) 별도 개발 필요 (Phase 8) |
| **C. 수동 입력 테이블** | PostgreSQL에 `inventory_table` 신설, 관리자 UI에서 직접 입력 | 1) 누가 어떻게 최신화할지 거버넌스 결정<br>2) 별도 개발 필요 |
| **D. 보류 유지** | 운전원 질문 시 "재고 정보는 별도 시스템 참조" 안내 | 운전원 교육 |
### 권장: 옵션 A
**근거**:
- 현재 KB 시스템(Qdrant 5종 컬렉션)이 이미 운영 중
- xlsx/pdf 자동 파싱 + 임베딩 + 한국어 검색 모두 동작
- 코드 추가 0줄, 즉시 시도 가능
- 부족하다면 그때 B 또는 C로 확장
### 사용자가 해야 할 일 (옵션 A 채택 시)
1. **재고 대장 파일 확보**
- [ ] 관리부/구매팀에서 최신 재고 엑셀 입수
- [ ] 민감정보(가격·공급사 단가 등) 제외 또는 가공 결정
2. **업로드**
- [ ] 앱 접속 → 14번 "RAG 관리" 탭
- [ ] 관리자 비밀번호 로그인
- [ ] 컬렉션 = `plant_operation` (또는 신규 `inventory` 컬렉션을 원하면 DB 시드 확장 요청)
- [ ] 태그 = `inventory`, `예비품` 등 일관된 태그 부착
3. **검증**
- [ ] 채팅(#13)에서 "PT100 센서 재고 알려줘" 같은 질문으로 검색 결과 확인
- [ ] 결과가 부정확하면 청크 미리보기(🔍 버튼)로 인덱싱 품질 확인
4. **운영 룰**
- [ ] 월 1회 or 분기 1회 최신 파일 재업로드 (이전 버전은 "🚫 비활성화" 후 "🗑 정리")
- [ ] 갱신 주체 1명 지정
### 사용자가 해야 할 일 (옵션 B/C로 갈 경우)
**이 문서 범위 밖**. 별도 요건 정의서가 필요. 본 항목은 옵션 결정만 요청.
---
## 2. 임베딩 모델 BGE-M3 마이그레이션 검토
### 현황
- 현재 사용 중: **`nomic-embed-text`** (768-dim, Ollama 호스트)
- 후보: **`bge-m3`** (1024-dim, 다국어 SOTA, 특히 한국어 성능 우수)
- 5개 Qdrant 컬렉션이 768-dim으로 생성된 상태
### 결정해야 할 것
전환 시 **이득**과 **비용**을 비교 후 GO/NO-GO/POSTPONE 결정.
| 이득 | 비용/위험 |
|------|----------|
| 한국어 검색 품질 향상 (대시기관 평가 기준 +5~15%) | Qdrant 컬렉션 5개 전부 재생성 → 기존 인덱스 일시 소실 |
| 멀티링구얼(영문 매뉴얼+한국어 SOP 혼합)에 강함 | 1024-dim → Qdrant 디스크 사용량 +33% |
| Dense + Sparse + ColBERT 통합 모델 | 임베딩 속도 ~30% 느려짐 (GPU 점유율 ↑) |
| 청크 크기 8K 토큰까지 지원 (nomic은 2K) | Ollama가 bge-m3 미지원 시 다른 호스트 필요 (HF transformers 등) |
### 마이그레이션 절차 (실행 시)
1. **사전 평가** ⚠️ (사용자 액션)
- [ ] BGE-M3가 Ollama Library에 있는지 확인 (`ollama pull bge-m3`)
- [ ] 없으면 대안 결정: HF transformers, sentence-transformers 또는 별도 임베딩 서버
- [ ] 현재 KB 검색 품질에 실질 문제가 있는지 — 운전원 피드백 수집 (1~2주)
2. **샘플 비교 (선택)** (사용자 액션)
- [ ] 동일 한국어 쿼리 10건을 두 모델로 비교
- [ ] 한국어 정밀도에서 명백한 개선이 보일 때만 GO
3. **백업** (사용자 액션)
- [ ] `storage/kb/` 전체 백업
- [ ] `pg_dump``kb_*` 테이블 백업
4. **코드 작업** (개발자 액션 — 본 문서 범위 아님)
- `appsettings.json``Kb:EmbeddingModel`, `Kb:VectorSize` 설정 추가
- `KbEmbeddingClient.cs` 에서 모델명/차원 환경변수 참조
- `KbStartupService.cs` 에서 컬렉션 차원 mismatch 감지 시 재생성
5. **컷오버** (사용자 액션)
- [ ] 운영 외 시간대 선택 (KB 검색 일시 중단)
- [ ] Qdrant 컬렉션 5개 삭제 → 새 차원으로 재생성
- [ ] 모든 KB 문서 일괄 `재인덱싱(↻)` 트리거
- [ ] 완료까지 모니터링 (문서 수 × 청크 수 × 임베딩 시간)
### 권장: POSTPONE (당분간 보류)
**근거**:
- 현재 nomic-embed-text 검색 품질이 운영에 임계 미달이라는 정량 증거가 없음
- BGE-M3 전환은 비가역적 비용(재인덱싱 시간, 디스크 +33%)이 큰 결정
- Phase 7 신규 기능(채팅 통합·청크 미리보기·요약 등)이 운영에서 안정화된 후
사용자 피드백으로 검색 품질 이슈가 누적되면 그때 검토
### 사용자가 해야 할 일
- [ ] 1~2개월간 운영하면서 **KB 검색 결과가 부정확했던 사례를 메모**
- 질문 / 기대 답변 / 실제 검색된 문서 / 점수
- 메모 위치: 별도 텍스트 파일 또는 본 문서에 추가
- [ ] 사례가 **5건 이상 누적**되고 한국어 매칭 실패가 패턴화되면 마이그레이션 재검토 회의
- [ ] 회의 시점에 다시 본 문서를 갱신 (GO/NO-GO 결정 기록)
### 즉시 가능한 대안 (개발자 액션 불필요)
검색 품질이 부족할 때 BGE-M3 전환 없이 시도해 볼 수 있는 것:
| 시도 | 방법 |
|------|------|
| 더 좋은 청킹 | 업로드 시 `chunking_policy` 옵션 조정 (현재는 자동) |
| 태그 보강 | KB 업로드 시 태그를 풍부하게 부착 → `search_kb`의 tags 필터로 정확도 ↑ |
| 컬렉션 분리 | doc_type별 5개 컬렉션을 더 세분화 |
| `top_k` 조정 | 채팅에서 검색 결과 수를 늘려 LLM이 더 많은 단서로 답하게 |
---
## 체크리스트 요약 (사용자가 해야 할 일)
### 단기 (이번 주 ~ 2주 내)
- [ ] **재고 데이터**: 옵션 A(KB 업로드) 채택 여부 결정 → 채택 시 관리부에 파일 요청
- [ ] **BGE-M3**: 마이그레이션은 보류, 검색 품질 모니터링 시작
### 중기 (1~3개월)
- [ ] 재고 KB 파일 1차 업로드 + 검증
- [ ] KB 검색 부정확 사례 누적 (5건 임계)
### 장기 (3개월+)
- [ ] BGE-M3 마이그레이션 재검토 회의 (사례 누적 시)
- [ ] 재고 데이터를 ERP 연동으로 격상할지 검토 (옵션 A로 한계 도달 시)
---
## 본 문서 갱신 규칙
이 문서는 결정이 진행됨에 따라 사용자가 직접 갱신합니다.
- 결정 완료 시: 해당 섹션에 "결정 결과", "결정자", "결정일" 추가
- 보류 유지: "재검토 예정일" 추가
- 옵션 변경: 새 옵션 행 추가 + 이전 결정 사유 보존

14
plans/막쓰는노트.md Normal file
View File

@@ -0,0 +1,14 @@
qwen 역할
1) todo.md 읽고,
2) diagnosis-checklist.md 규칙에 따라서 스텝별 실행계획 만들기
3) 스텝 미션을 gemma4 에게 전달
5) 끝났다고 보고 받으면, 검증 시작
6) 검증완료면 다음 스텝 미션 전달
gemma4는
1) 스텝 미션 받아서,
2) 코딩하고,
3) diagnosis-checklist.md에 따라서 자기 검증
4) 문제있으면 loop 돌림
5) 끝나면 끝났다고 보고

View File

@@ -0,0 +1,301 @@
# 배포 테스트 — Qwen2.5-7B-Instruct-FP8 모델 전환 검토
**날짜**: 2026-05-15
**환경**: NVIDIA GB10 (DGX Spark) / 통합 메모리 121 GiB / vLLM in Docker `vllm_node`
**결론**: **7B 운영 부적합. 27B 유지 결정.**
---
## TL;DR
- 동기: 27B Qwen3.6-FP8이 48 tok/s로 느리게 느껴져, 더 가벼운 7B FP8로 교체 시 속도/메모리 이득을 볼 수 있는지 검토.
- 실측: 7B FP8(`RedHatAI/Qwen2.5-7B-Instruct-FP8-dynamic`)이 29.4 tok/s로 오히려 **느림** (27B는 `qwen3_next_mtp` speculative decoding 효과).
- 결정타: 7B는 **도구 호출을 회피하고 가짜 데이터를 만들어냄** — 운전원 안전 위험. 도구 토글 ON·에이전트 모드 ON에도 동작 안 함.
- 조치: 27B로 즉시 복귀. 검증 중 만든 시스템 프롬프트 강화는 그대로 유지(27B에도 도움).
---
## 1. 환경 정보
### 하드웨어
| 항목 | 값 |
|---|---|
| GPU/CPU | NVIDIA GB10 (Grace Blackwell, unified memory) |
| 통합 메모리 | 121 GiB (시스템·GPU 공유) |
| Swap | 15 GiB |
### 27B 운영 구성 (baseline)
| 항목 | 값 |
|---|---|
| 모델 | `Qwen/Qwen3.6-27B-FP8` |
| 메모리 점유 | ~97 GiB |
| `max-model-len` | 262,144 (256K) |
| `gpu-memory-utilization` | 0.80 |
| `speculative-config` | `qwen3_next_mtp`, 2 tokens |
| `tool-call-parser` | `qwen3_coder` |
| 측정 속도 | **48 tok/s** |
| GB10 튜닝 mod | `mods/vllm-tune-qwen--qwen3.6-27b-fp8-tp1` 적용 |
---
## 2. 7B FP8 띄우기 레시피 (재현용)
### 모델 선택
HF에서 `neuralmagic/Qwen2.5-7B-Instruct-FP8`**존재하지 않음** (404). neuralmagic이 RedHatAI로 흡수됨.
올바른 repo:
-`RedHatAI/Qwen2.5-7B-Instruct-FP8-dynamic` (다운로드 2378회, 공식 후속)
- 그 외: `CalamitousFelicitousness/...`, `llmcompressor-quants/...` 등 community 변형
### 27B 종료 + 7B 기동 절차
```bash
# 1. 현재 27B vllm 프로세스만 종료 (컨테이너는 sleep infinity로 유지)
docker exec vllm_node pkill -f "vllm serve"
sleep 5
# 2. 7B FP8 기동 (launch-cluster.sh exec 패턴)
cd /home/windpacer/ai-models/spark-vllm-docker
./launch-cluster.sh -t vllm-node-tf5 --solo -d \
exec vllm serve RedHatAI/Qwen2.5-7B-Instruct-FP8-dynamic \
--served-model-name Qwen2.5-7B-Instruct-FP8 \
--max-model-len 32768 \
--max-num-seqs 4 \
--enable-prefix-caching \
--gpu-memory-utilization 0.30 \
--port 8000 --host 0.0.0.0 \
--enable-chunked-prefill \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--trust-remote-code \
-tp 1
```
### 27B 명령에서 제거한 인자 (Qwen3 전용)
| 인자 | 이유 |
|---|---|
| `--apply-mod mods/vllm-tune-qwen--qwen3.6-27b-fp8-tp1` | Qwen3.6-27B 전용 GEMM kernel config |
| `--load-format instanttensor` | Qwen3 instant tensor 최적화 |
| `--reasoning-parser qwen3` | Qwen3 reasoning trace (Qwen2.5 미지원) |
| `--default-chat-template-kwargs '{"preserve_thinking":true}'` | Qwen3 thinking block |
| `--speculative-config '{"method":"qwen3_next_mtp",...}'` | Qwen3 MTP — Qwen2.5에 적용 불가 |
| `--tool-call-parser qwen3_coder` | → `hermes`로 교체 (Qwen2.5는 hermes 포맷) |
| `--max-model-len 262144` | → 32768로 축소 (KV 캐시 절약) |
| `--gpu-memory-utilization 0.80` | → 0.30 (7B는 메모리 적게 필요) |
| `--max-num-batched-tokens 32768` | 32K로 충분 |
| `--override-generation-config` | 7B 기본값 사용 |
### 동기화 작업
```bash
# vllm_model 표시명을 UI에 맞게 변경
echo '{"vllm_model":"Qwen2.5-7B-Instruct-FP8"}' > mcp-server/llm-model.json
```
`appsettings.json`의 vLLM endpoint, `OllamaController.cs`, `KbEmbeddingClient.cs` 등은 **변경 불필요** — 포트 8000 동일.
### 기동 시간
- 가중치 다운로드: 152초 (~8 GiB, 첫 회만)
- 가중치 로드: 45초
- torch.compile 캐시: ~30초
- **합계 약 4분** (캐시 후 재기동은 ~75초)
---
## 3. 검증 결과
### 3.1 메모리 점유
| 모델 | 점유 | 가용 |
|---|---|---|
| 27B FP8 (max-len 256K, util 0.80) | 97 GiB | 11 GiB |
| 7B FP8 (max-len 32K, util 0.30) | ~25 GiB | ~85 GiB |
### 3.2 순수 채팅 속도 (3회 평균)
```
27B FP8 + qwen3_next_mtp : 48 tok/s
7B FP8 (vanilla) : 29.4 tok/s
```
**역설**: 7B가 더 느림. 이유:
1. 27B는 MTP(Multi-Token Prediction) speculative decoding으로 한 step에 2~3토큰 생성
2. 27B는 GB10 전용 GEMM kernel mod 적용
3. 7B는 default kernel + vanilla autoregressive
### 3.3 도구 호출 능력 — 결정적 검증
#### 테스트 1: 일반 KB 질의
> "5월12일 6차플랜트 이상 상황 보고해줘"
- 7B 응답: `summarize_events(since=2026-05-12T00:00:00Z, area=P6)` ⭕ 도구 선택·area 매핑 정확
- 단 KST→UTC 변환 일부 생략 (27B는 `2026-05-11T15:00:00Z`로 정확)
#### 테스트 2: 시간 인자 정확도
> "지난 30분 동안 ficq-6101.pv의 값을 표시해줘"
- 7B 응답: `time_from: 2026-05-15T14:00:00, time_to: 2026-05-15T14:30:00` (**현재로부터 +16시간 미래!**)
- 결과 `count: 0` → "데이터 없습니다" 답변 (사실은 84건 존재)
- 원인: 7B가 "지난 30분"의 절대 시각 계산 못 함 → 학습 데이터에서 흔한 KST 14:00 차용 후 KST→UTC 변환 생략
**시스템 프롬프트 강화 후 재시도** (`BuildDateContextKo`에 시:분 + 상대시간 표 + 미래 금지):
- 결과: 시간은 그럴듯하게 표기되었으나 **도구 호출 자체를 skip**하고 가짜 값(50.2 → 51.1 단조 증가) 생성
#### 테스트 3: 결정적 검증 프롬프트
요청:
```
다음 3가지를 정확히 알려줘. 도구 결과만 인용, 추측 금지.
1) ficq-6101.pv 가장 최근 1건 (값 + UTC 시각)
2) ficq-6101.sp 가장 최근 1건 (값 + UTC 시각)
3) event_history_table 전체 이벤트 수
```
비교:
| 항목 | 7B 답변 | 실제 DB | 판정 |
|---|---|---|---|
| 1) ficq-6101.pv | `50.2` @ `22:19:00Z` | `47.4666...` @ `22:23:03.055Z` | ❌ |
| 2) ficq-6101.sp | `50.5` @ `22:18:00Z` | `34.4000...` @ `22:23:03.055Z` | ❌ |
| 3) event_history_table count | **`12345`** | **`53`** | ❌ |
**3가지 모두 fabrication**. 특히 `12345`는 챗봇 demo placeholder. "도구 결과 그대로 인용"이라고 7B가 표기까지 했음에도 실제 도구 호출 없음.
#### UI 측 도구 호출 카드
- 도구 토글: ON ✅
- 에이전트 모드(ReAct 강제): ON ✅
- **실제 도구 카드 표시: 없음** ❌
7B가 도구 호출 자체를 회피. ReAct 프롬프트도 무시.
### 3.4 일반 채팅 품질
- 한국어 자연스러움: ⭕ 양호 (시스템 프롬프트 한자 금지 강화 후)
- 프로젝트 설명 등 paraphrase: ⭕ 양호
- 시스템 프롬프트 내용 활용: ⭕ 양호
---
## 4. 결론
| 사용 유형 | 7B FP8 평가 |
|---|---|
| 일반 한국어 Q&A | ⭕ 사용 가능 |
| 시스템/문서 내용 paraphrase | ⭕ 사용 가능 |
| **실시간 도구 호출** | ❌ **결정적 실패** |
| **데이터 값 정확 인용** | ❌ **fabrication** |
| 속도 | ❌ 27B보다 느림 |
| 메모리 절약 | ⭕ 유일한 이점 (~70 GiB 절약) |
**판단 기준**: 운전원이 "최근 PV값"을 물었을 때 가짜 수치(50.2 vs 실제 47.47)를 사실로 받는 것은 산업 안전 위험. 12,345 같은 명백한 placeholder를 53건으로 보고하면 운영 판단 완전 왜곡.
**7B 운영 부적합 확정**.
---
## 5. 27B 롤백 절차
검증 종료 시 즉시 복귀.
### 롤백 스크립트 (`/tmp/restore-27b.sh`)
```bash
#!/bin/bash
# 27B vLLM 복귀
set -e
cd /home/windpacer/ai-models/spark-vllm-docker
# 현재 vllm 프로세스 종료
docker exec vllm_node pkill -f "vllm serve" 2>/dev/null || true
sleep 3
./launch-cluster.sh -t vllm-node-tf5 --solo -d \
--apply-mod mods/vllm-tune-qwen--qwen3.6-27b-fp8-tp1 \
exec vllm serve Qwen/Qwen3.6-27B-FP8 \
--served-model-name Qwen3.6-27B-FP8 \
--max-model-len 262144 \
--max-num-seqs 4 \
--enable-prefix-caching \
--gpu-memory-utilization 0.80 \
--port 8000 --host 0.0.0.0 \
--load-format instanttensor \
--enable-chunked-prefill \
--enable-auto-tool-choice \
--tool-call-parser qwen3_coder \
--reasoning-parser qwen3 \
--max-num-batched-tokens 32768 \
--trust-remote-code \
--default-chat-template-kwargs '{"preserve_thinking": true}' \
--speculative-config '{"method": "qwen3_next_mtp", "num_speculative_tokens": 2}' \
--generation-config auto \
--override-generation-config '{"temperature": 0.6, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0}' \
-tp 1
```
### 함께 복원
```bash
echo '{"vllm_model":"Qwen3.6-27B-FP8"}' > mcp-server/llm-model.json
```
C# 앱은 사용자 재시작 (`dotnet run --project src/Web/ExperionCrawler.csproj`).
### 로딩 시간 (캐시 hit)
- 약 75초 (가중치는 이미 `~/.cache/huggingface/`에 있음)
---
## 6. 향후 재시도 시 점검 항목
다른 모델/구성으로 재검토할 경우 반드시 통과해야 할 체크리스트:
### 필수 (이 중 하나라도 실패하면 운영 불가)
- [ ] **결정 프롬프트** (Section 3.3 테스트 3) 3개 항목 모두 도구 결과 정확 인용
- [ ] UI에서 `query_pv_history` / `run_sql` 도구 카드가 시각적으로 표시됨
- [ ] `event_history_table` 전체 카운트가 실제값과 일치 (추측 불가 숫자)
- [ ] 시각 인자에 미래 시각(NOW 이후) 절대 안 나옴
- [ ] 데이터 값이 단조 증가 demo 패턴(`50.2 → 51.1` 등)이 아님
### 우대 (있으면 좋음)
- [ ] 한자(중국어) 글자 응답에 섞이지 않음
- [ ] KST/UTC 라벨 정확
- [ ] 27B 대비 동등 또는 더 빠른 tok/s
- [ ] `plant_context.md` 의 area 매핑(`P1~P10`) 따라 인자 생성
### 검토 후보 (참고)
| 모델 | 비고 |
|---|---|
| **`gemma4-quantized` (사용자 우선순위)** | **다음 검토 1순위.** 70~80 GiB 점유 예상 (27B와 유사 위치). 배포 환경에서도 이걸 쓰기로 계획됨. 검증 시 GB10 호환 양자화 종류(AWQ/GPTQ/FP8/MXFP4 등) 확인 필요. tool-call-parser는 Gemma 전용 파서(`gemma`/`hermes` 호환 여부) 점검 |
| `RedHatAI/Qwen2.5-72B-Instruct-FP8` | 72B FP8 ~72 GiB. 도구 호출 신뢰성 27B에 근접 예상 |
| `exaone3.5:7.8b` (LG) | 한국어 특화 7.8B. 함수 호출 미지원일 수 있어 사전 확인 필요 |
| `Qwen3-30B-A3B-Instruct-2507` | Qwen3 차세대 MoE 30B (3B active). 출시되면 검토 가치 |
### Gemma4 검토 시 사전 준비 (TODO)
- [ ] HF에서 GB10·FP8 호환 정확한 repo 식별 (예: `google/gemma-4-27b-it`의 FP8/AWQ 변형, RedHatAI/neuralmagic 양자화 후속)
- [ ] vLLM의 Gemma 지원 버전 확인 — `--tool-call-parser` 옵션 (`gemma`, `pythonic`, `hermes` 중 어느 것)
- [ ] 기존 mod 디렉토리에 Gemma 관련 자료 유무 점검 (`/home/windpacer/ai-models/spark-vllm-docker/mods/fix-gemma4-tool-parser`가 이미 있음 — Gemma4 도구 파서 패치 시사)
- [ ] speculative decoding 가능 여부 (Gemma3는 미지원, Gemma4는 별도 확인)
- [ ] 27B FP8 (97 GiB)을 종료한 뒤에만 충분한 여유. 동시 운영은 메모리 부족 가능성 (배포 환경 기준 80GB로 가능)
---
## 7. 부산물 — 이 검증 과정에서 추가된 코드 개선
7B 검증 실패와 무관하게 시스템 전반에 도움이 되는 변경:
| 파일 | 개선 내용 |
|---|---|
| `src/Web/Controllers/OllamaController.cs` | `BuildDateContextKo`에 KST/UTC 현재 시:분 + 상대시간 변환 표(지난 30분/1시간/24시간/오늘/어제) + 미래 시각 금지 명시 |
| `src/Web/Controllers/OllamaController.cs` | `BaseSystemPromptKo`에 한자(중국어) 금지 강화 — 자주 출현하는 9글자(`请·您·是·了·我·会·什么·可以·需要`) 명시 |
| `src/Web/Controllers/OllamaController.cs` | `GetModels`에 Ollama `/api/show` 호출 → `capabilities=['embedding']` 모델(bge-m3, nomic-embed-text) 채팅 셀렉터에서 자동 제외 |
| `prompts/plant_context.md` | P1~P10/UTIL/PACKING area 매핑 + `event_history_table` vs `v_tag_summary` area 형식 차이 안내 + 의도별 권장 도구 표 |
| `mcp-server/server.py` | `_CLASSIFY_RULES``이상.*상황\|상황.*보고\|이상.*보고\|비정상.*상황\|abnormal``summarize_events` 추가 |
27B에도 도움 — 그대로 유지.
---
## 8. 메모
- `/tmp/restore-27b.sh`는 임시 파일. 재기동 후 사라짐. 영구 보관하려면 `scripts/restore-27b.sh`로 옮기는 것 권장.
- 7B 가중치는 `~/.cache/huggingface/hub/models--RedHatAI--Qwen2.5-7B-Instruct-FP8-dynamic/`에 캐시됨 (~8 GiB). 디스크 정리 시 삭제 가능.
- 다음에 7B를 다시 시도할 경우 **결정 프롬프트(Section 3.3 테스트 3)부터** 30분 안에 통과 여부 판정 가능. 시간 낭비 방지.

View File

@@ -0,0 +1,517 @@
# 빅피클 — 잔여 작업 상세 코딩 계획
> 기준: `CLAUDE.md` 잔여 작업 항목 (2026-05-14)
> 상태: Phase 0~6 완료, Phase 7 + Phase 5 후순위 + 결정 보류 항목이 대상
---
## 현황 요약
| 영역 | 상태 | 비고 |
|------|------|------|
| Phase 0~5 (RAG + 채팅) | ✅ 완료 | Ollama/vLLM 이중 백엔드, SSE 스트리밍, MCP 툴콜 루프(10라운드), KB 업로드/인덱싱 5종 컬렉션 |
| Phase 5 핫픽스 | ✅ 완료 | nl2sql 버그, KB DDL `String.Format` 이슈, 비번 로그 마스킹, `IHttpClientFactory`, plant_context 캐시 |
| Phase 6 (run_sql 가드) | ✅ 완료 | `_validate_sql`, `_apply_sql_guards`, keyword 차단, auto-LIMIT, statement_timeout |
| Phase 6 (보강 도구 5종) | ✅ 완료 | `find_tags`, `query_events`, `active_alarms`, `summarize_events`, `generate_status_report` |
| **Phase 7 (옵션)** | **⏳ 잔여** | NL2SQL 의도 라우터, 대화 요약, 에이전트 모드, KB 청크 미리보기 |
| **Phase 5 후순위** | **⏳ 잔여** | 시계열 미니 스파클라인, 툴 카드 메시지 영구 보존 |
| **결정 보류** | **⏳ 분석** | 현장 재고 데이터 출처, BGE-M3 마이그레이션 |
---
## 1. NL2SQL 의도 라우터 (Phase 7.1)
### 배경
현재 `query_with_nl`은 모든 자연어 질문을 무조건 LLM에 보내 SQL 생성을 시도한다.
"활성 알람 보여줘", "이벤트 요약" 같은 질문도 SQL 경로로 가므로 불필요한 LLM 호출 + SQL
실패 위험이 있다. 의도 라우터가 질문을 분류하여 적절한 MCP 도구로 직접 라우팅한다.
### 설계
```
사용자 질문
[_classify_intent()] ← 키워드/Trie 기반 (ML 불필요)
├── "알람/트립" → active_alarms 도구
├── "이벤트/요약/보고서" → summarize_events 또는 generate_status_report
├── "태그 찾기/검색" → find_tags 도구
├── "SQL/조회/데이터" → query_with_nl (기존)
└── "기타/모름" → query_with_nl (fallback)
```
### 수정 파일
**mcp-server/server.py**
| 위치 | 변경 |
|------|------|
| `_CLASSIFY_RULES` 상수 (신규) | 키워드→도구 매핑 규칙 맵 (예: `{"alarm|트립|경보|trip": "active_alarms", ...}`) |
| `_classify_intent(query) → str` (신규) | 정규식 + 키워드 Trie로 의도 분류, fallback은 `"query_with_nl"` |
| `query_with_nl` 수정 | `query_with_nl` 내부 첫 줄에서 `_classify_intent` 호출, 매칭되면 해당 도구로 위임 |
| `@mcp.tool() classify_intent(question)` 검토 | MCP 도구로 별도 노출할지 결정 (OllamaController의 JSON 폴백 경로에 활용 가능) |
**src/Web/Controllers/OllamaController.cs**
| 위치 | 변경 |
|------|------|
| `ToolGuideKo` | (필요시) `classify_intent` 도구 항목 추가 |
### 분류 규칙 (예시)
```python
_CLASSIFY_RULES = [
(r'alarm|트립|경보|trip|경보|비상', 'active_alarms'),
(r'요약|보고서|리포트|summary|report', 'generate_status_report'),
(r'태그.*찾|검색|찾아줘|찾기|find.*tag', 'find_tags'),
(r'이벤트|event|로그|기록', 'query_events'),
(r'SQL|조회|데이터|select|값.*보여줘|수치', 'query_with_nl'),
]
```
### 동작 흐름 (query_with_nl 변경 후)
```
query_with_nl("지금 알람 상황 알려줘")
→ _classify_intent → "active_alarms"
→ active_alarms(area=None) 호출 → 결과 반환
→ (SQL 생성 생략)
```
### 검증
- `python3 -c "import server; print(server._classify_intent('지금 알람 알려줘'))"``"active_alarms"`
- `python3 -c "import server; print(server._classify_intent('FIC-6113.PV 값 보여줘'))"``"query_with_nl"`
- `python3 -c "import server; print(server._classify_intent('안녕'))"``"query_with_nl"` (fallback)
---
## 2. 대화 요약 (Phase 7.2)
### 배경
현재 매 턴 전체 messages 배열을 LLM에 전송한다. 대화가 길어질수록 컨텍스트 윈도우를
초과하거나 토큰 비용이 증가한다. N messages 이상이면 이전 메시지를 요약하여
system prompt에 "지금까지의 대화 요약: ..." 형태로 압축한다.
### 설계
```
messages 길이 > MAX_HISTORY (기본 20)
llmSend() 전에 PreprocessMessages()
├── 오래된 메시지를 LLM에 요약 요청
├── 요약 텍스트를 system prompt에 "대화 요약: ..." 형태로 삽입
└── 요약된 메시지 제거
```
### 수정 파일
**src/Web/wwwroot/js/app.js**
| 위치 | 변경 |
|------|------|
| `LLM_MAX_HISTORY = 20` (신규 상수) | 요약 트리거 기준 messages 수 |
| `llmPreprocessMessages(messages)` (신규) | messages 길이 체크 → 초과 시 요약 API 호출 |
| `llmSend()` (line 3725) | `llmPreprocessMessages` 호출 추가 (line 3748 앞) |
| session 저장 구조 확장 | `sess.summary` 필드 추가 (요약 텍스트 보관) |
| `llmRenderMessages()` | summary 표시 (접힌 카드) |
**src/Web/Controllers/OllamaController.cs**
| 위치 | 변경 |
|------|------|
| `POST /api/ollama/summarize` (신규) | messages 배열 받아 LLM으로 요약 + 반환 |
| `VllmChatStreamWithTools` | `maxToolRounds` 카운트 유지, 필요시 요약 |
**src/Web/wwwroot/css/style.css**
| `.llm-summary-card` | 요약 표시용 스타일 (접힘/펼침) |
### UI 동작
```
def llmPreprocessMessages(messages):
if len(messages) <= LLM_MAX_HISTORY: return messages
# 최근 절반 유지, 오래된 절반 요약 요청
old_msgs = messages[:-LLM_MAX_HISTORY//2]
new_msgs = messages[-LLM_MAX_HISTORY//2:]
summary = await api('POST', '/api/ollama/summarize', {messages: old_msgs})
sess.summary = summary
# system prompt에 요약 주입
systemPrompt = f"[대화 요약]\n{summary}\n\n[최근 대화]"
return new_msgs
```
### 검증
- 25개 메시지 → 20개 초과 → 요약 트리거
- 요약 이후 처음 메시지에 system prompt에 `[대화 요약]` 포함 확인
- 요약 카드가 UI에 정상 렌더링
---
## 3. 에이전트 모드 (Phase 7.3)
### 배경
현재 툴콜 루프(10라운드)는 LLM이 도구를 호출하면 실행하고 결과를 다시 LLM에 주입한다.
하지만 "이 공정의 문제점을 분석해줘" 같은 복합 태스크는: (1) 활성 알람 조회 →
(2) 관련 태그 이력 조회 → (3) 보고서 생성 순서로 여러 도구를 자율적으로 호출해야 한다.
### 설계
"에이전트 모드" 토글을 채팅 UI에 추가:
- OFF (현행): 단순 툴콜 루프 (LLM이 요청한 도구만 실행)
- ON: 계획→실행→관찰→반복 사이클 (ReAct 패턴)
### 수정 파일
**src/Web/wwwroot/index.html**
| 위치 | 변경 |
|------|------|
| `#llm-agent-mode` 체크박스 (line 1269 옆) | "에이전트 모드" 토글 추가 (MCP 도구 체크박스 옆) |
| 설명 툴팁 | "복합 태스크를 단계별로 계획하고 실행합니다" |
**src/Web/wwwroot/js/app.js**
| 위치 | 변경 |
|------|------|
| `llmAgentMode` 변수 (line 3297 옆) | localStorage persist |
| `llmToggleAgentMode()` (신규) | 토글 변경 시 상태 저장 |
| `llmSend()` | agentMode ON이면 system prompt에 ReAct 프롬프트 주입 |
| `llmRenderMessages()` | agent planning 단계 시각화 |
**src/Web/Controllers/OllamaController.cs**
| 위치 | 변경 |
|------|------|
| `ComposeSystemPrompt()` | agentMode 인자 추가 → ReAct 가이드 포함 |
| `ToolGuideKo` | agent 모드용 tool 사용 설명 추가 |
**ReAct 시스템 프롬프트 (ToolGuideKo에 추가)**
```
[에이전트 모드]
복잡한 질문은 다음 단계로 분해하여 도구를 호출하세요:
1. Thought: 현재 상황과 필요한 정보 파악
2. Action: 적절한 도구 호출 (active_alarms, query_events, query_pv_history 등)
3. Observation: 도구 결과 분석
4. Thought: 다음 단계 결정
5. ...반복...
6. Final Answer: 모든 정보를 종합하여 답변
```
### 동작 흐름
```
사용자: "지금 공장 상황을 분석해줘"
Round 1: Thought → active_alarms() → 결과 분석
Round 2: Thought → find_tags("pump") → 결과 분석
Round 3: Thought → query_events(area="A") → 결과 분석
Round 4: Thought → generate_status_report() → 최종 보고서 생성
최종: 모든 정보를 종합한 한국어 보고서 출력
```
### 검증
- 에이전트 ON/OFF 토글 localStorage 저장 확인
- ON 상태에서 복합 질문 시 2라운드 이상 도구 호출
- 최종 응답에 모든 단계의 정보가 통합됨
---
## 4. KB 청크 미리보기 UI (Phase 7.4)
### 배경
현재 KB 관리 탭에서 문서 목록은 보이지만, 각 문서의 청크 내용을 볼 수 없다.
인덱싱 결과를 눈으로 확인할 수 없어 디버깅과 품질 검증이 어렵다.
### 설계
Qdrant에서 `doc_id`로 청크를 조회하는 API → 프론트엔드에서 접이식 카드로 표시.
### 수정 파일
**src/Infrastructure/Kb/KbQdrantClient.cs**
| 위치 | 변경 |
|------|------|
| `GetChunksByDocIdAsync(docId, collection)` (신규) | Qdrant Scroll API로 doc_id 필터, payload(text, chunk_kind, locator) 반환 |
**src/Web/Controllers/KbController.cs**
| 위치 | 변경 |
|------|------|
| `GET /api/kb/documents/{id}/chunks` (신규) | admin 인증 필요, `KbQdrantClient.GetChunksByDocIdAsync` 호출 |
**src/Web/wwwroot/js/app.js**
| 위치 | 변경 |
|------|------|
| `kbShowChunks(docId)` (신규) | `/api/kb/documents/{id}/chunks` 호출 → 모달에 렌더 |
| `kbRenderChunks(chunks)` (신규) | 청크 목록을 접이식 카드로 표시 (chunk_kind 배지, text 미리보기 200자, locator 표시) |
| `kbRenderDocs` | "청크 미리보기" 버튼 추가 (청크 수 > 0인 경우) |
**src/Web/wwwroot/index.html**
| 위치 | 변경 |
|------|------|
| 청크 미리보기 모달 (kb-upload-modal 다음) | #kb-chunk-modal: 모달 내 청크 리스트 + 닫기 버튼 |
**src/Web/wwwroot/css/style.css**
| `.kb-chunk-card` | 청크 카드 (테두리, 접힘/펼침) |
| `.kb-chunk-badge` | chunk_kind 배지 (table/page/text 등) |
| `.kb-chunk-locator` | locator 표시 (파일 내 위치) |
### API 응답 형식
```json
GET /api/kb/documents/{id}/chunks
{
"success": true,
"docId": "uuid",
"collection": "plant_operation",
"count": 12,
"chunks": [
{
"text": "청크 내용...",
"chunk_kind": "table",
"locator": "Sheet1, Row 5-10",
"score": null
}
]
}
```
### UI 동작
```
[d832a1f2] 온도센서 교체 매뉴얼.pdf | plant_operation | indexed | 12청크 | [청크보기]
↓ 클릭
┌─ 청크 미리보기 ─────────────────────────────────┐
│ [table] Sheet1, Row 5-10 │
│ ┌───────┬──────┬──────┐ │
│ │ 온도 │ 범위 │ 오차 │ │
│ ├───────┼──────┼──────┤ │
│ │ PT100 │ -200…│ ±0.1 │ │
│ └───────┴──────┴──────┘ │
│ │
│ [text] 페이지 2, 3번째 문단 │
│ RTD 센서는 저항 온도 센서로... [펼치기] │
└──────────────────────────────────────────────────┘
```
### 검증
- 청크 0건 문서 → 버튼 미표시
- 청크 12건 문서 → 버튼 표시 → 클릭 시 모달 오픈
- 모달 내 청크 카드 접기/펼치기 동작
---
## 5. 시계열 미니 스파클라인 (Phase 5 후순위)
### 배경
Phase 5 설계서(C5)에서 "표 자동 렌더링"은 완료했으나 "시계열 스파클라인"은 보류됨.
uPlot은 이미 fastRecord 탭에서 사용 중 (`index.html:1535` 로드 완료).
### 설계
`query_pv_history` 또는 `run_sql` 결과에서 timestamp + numeric value 컬럼 감지 시
uPlot 미니 차트(스파클라인)를 자동 렌더링.
### 수정 파일
**src/Web/wwwroot/js/app.js**
| 위치 | 변경 |
|------|------|
| `llmRenderSparkline(containerId, data)` (신규) | uPlot 미니차트 생성 (height: 80px, grid 없음, tooltip만) |
| `llmRenderToolPayload()` (line 3587) | JSON 응답에 timestamp+value 패턴 감지 시 스파클라인 렌더링 옵션 제공 |
| `llmDetectTimeSeries(data)` (신규) | 데이터가 `[{timestamp, value}]` 또는 `[{recorded_at, pv}]` 형태인지 감지 |
**src/Web/wwwroot/css/style.css**
| `.llm-sparkline-box` | 스파클라인 컨테이너 (padding, border, max-width) |
| `.llm-sparkline-toggle` | "📈 추세 보기" 토글 버튼 |
### 스파클라인 생성 코드 (예시)
```javascript
function llmRenderSparkline(containerId, data, valueKey, timeKey) {
const times = data.map(r => new Date(r[timeKey || 'timestamp']).getTime() / 1000);
const vals = data.map(r => parseFloat(r[valueKey || 'value']));
const opts = {
width: 280, height: 64,
cursor: { show: true },
select: { show: false },
axes: [{ show: false }, { show: false }],
series: [
{ label: '' },
{ label: '', stroke: 'var(--accent)', width: 1, points: { show: false } }
]
};
new uPlot(opts, [times, vals], document.getElementById(containerId));
}
```
### 동작 흐름
```
query_pv_history 결과
→ llmRenderToolPayload에서 {success, data:[{tag_name, timestamp, value}]} 감지
→ "📈 추세 보기" 버튼 표시
→ 클릭 시 llmRenderSparkline() 실행
→ uPlot 미니차트가 툴 카드 내에 렌더링
```
### 검증
- 시계열 데이터 2건 미만 → 차트 미표시
- 3건 이상 → "📈 추세 보기" 버튼 → 클릭 시 uPlot 차트 렌더링
- 툴 카드 접기/펼치기와 호환
---
## 6. 툴 카드 메시지 영구 보존 (Phase 5 후순위)
### 배경
현재 툴 카드(`llm-tool-card`)는 SSE `tool_start`/`tool_result` 이벤트로 DOM에 직접
삽입되지만, `llmRenderMessages()``sess.messages`만으로 전체 메시지 영역을
innerHTML=...로 재생성하므로 툴 카드가 사라진다.
즉, 페이지 새로고침이나 탭 전환 후 이전 대화를 열면 툴 카드가 모두消失.
### 설계
`sess.messages``tool_call` 타입 메시지를 저장하고, `llmRenderMessages()`에서
툴 카드를 재생성할 수 있도록 구조화.
### 수정 파일
**src/Web/wwwroot/js/app.js**
| 위치 | 변경 |
|------|------|
| **데이터 모델** | `sess.messages` item에 type: `"text"` \| `"tool_call"` 속성 추가 |
| `llmSend()` (line 3758) | assistant 메시지에 `toolCalls: []` 배열 추가 (초기 빈 배열) |
| SSE `tool_start` 처리 (line 3838) | `assistantMsg.toolCalls.push({id, name, args, ok:null, payload:null})` |
| SSE `tool_result` 처리 (line 3846) | 해당 toolCalls 항목 업데이트 (`ok`, `payload`, `preview`, `length`) |
| `llmRenderMessages()` (line 3418) | 메시지에 `toolCalls` 배열 있으면 툴 카드 렌더링 |
| `llmRenderToolCards(toolCalls)` (신규) | toolCalls 배열 → 툴 카드 HTML 생성 |
| `llmSaveSessions()` | 변경 없음 (messages 배열에 포함되어 자동 저장) |
### 데이터 구조 (localStorage)
```javascript
sess.messages = [
{ role: 'user', content: '지금 알람 보여줘' },
{
role: 'assistant',
content: '현재 3개의 활성 알람이 있습니다...',
toolCalls: [
{ id: 'tc_1_xxx', name: 'active_alarms', args: '{}', ok: true, payload: '{...}' }
]
}
]
```
### 수정 상세
**llmSend() - toolCalls 배열 초기화 (line 3758)**
```javascript
// 변경 전
const assistantMsg = { role: 'assistant', content: '' };
// 변경 후
const assistantMsg = { role: 'assistant', content: '', toolCalls: [] };
```
**SSE `tool_start` 핸들러 (line 3838)**
```javascript
// 추가
assistantMsg.toolCalls.push({ id: t.id, name: t.name, args: t.args, ok: null, payload: null });
```
**SSE `tool_result` 핸들러 (line 3846)**
```javascript
// 추가
const tc = assistantMsg.toolCalls.find(x => x.id === t.id);
if (tc) { tc.ok = t.ok; tc.payload = t.payload; }
```
**llmRenderMessages() - toolCalls 렌더링 (line 3449 이후)**
```javascript
// 각 메시지의 content 다음에 toolCalls 렌더링
if (m.toolCalls && m.toolCalls.length > 0) {
const cardsHtml = m.toolCalls.map(tc => {
const statusClass = tc.ok === null ? 'running' : (tc.ok ? 'ok' : 'err');
const statusText = tc.ok === null ? '실행 중…' : (tc.ok ? '완료' : '실패');
// ... 툴 카드 HTML 생성
}).join('');
html += `<div class="llm-tool-cards">${cardsHtml}</div>`;
}
```
### 마이그레이션 (기존 세션 호환)
기존 localStorage `llmSessions`에는 `toolCalls` 필드가 없음. `llmRenderMessages()`에서
`toolCalls`가 undefined/null이면 툴 카드 렌더링 생략 (기존처럼 동작).
### 검증
1. 새 채팅에서 도구 호출 → SSE 완료 후 localStorage 확인 → `toolCalls` 배열 저장됨
2. F5 새로고침 → 이전 대화 열기 → 툴 카드가 정상 렌더링
3. 기존 세션(툴 카드 없는 메시지) → 오류 없음
4. 저장/로드 시 JSON 용량 증가 확인 (toolCalls 1개당 약 200바이트)
---
## 7. 현장 재고 데이터 출처 (결정 보류 — 분석만)
### 현황
Phase 0 설계서(G3)에서 "현장 재고 데이터 자체 없음"으로 식별됨. 코드 작업 없음.
### 분석 방향
| 측면 | 검토 사항 |
|------|----------|
| 데이터 성격 | 예비품 재고, 소모품, 교체 이력, 위치 정보 |
| 가능한 출처 | 별도 엑셀 관리 → KB 업로드, ERP 연동 API, 수동 입력 |
| KB 활용 | plant_operation 또는 report 컬렉션에 엑셀 업로드로 즉시 해결 가능 |
| 우선순위 | 낮음 (Phase 0~6 안정화 후 검토) |
### 권장
현장 재고 데이터는 기존 KB 시스템에 엑셀/CSV를 `plant_operation` 컬렉션으로
업로드하여 즉시 RAG 검색 가능. 별도 개발 불필요.
---
## 8. 임베딩 모델 BGE-M3 마이그레이션 (결정 보류 — 계획만)
### 현황
현재 `nomic-embed-text` (768차원, Ollama) 사용 중. BGE-M3 (1024차원)는
다국어(한국어) 성능이 더 우수하나 Qdrant 컬렉션 재생성이 필요.
### 마이그레이션 계획 (향후 실행 시)
| 단계 | 작업 | 영향 |
|------|------|------|
| 1 | BGE-M3 Ollama에 pull (`ollama pull bge-m3`) | 서버 리소스 추가 사용 |
| 2 | `KbEmbeddingClient.cs` 모델명 변경 (settings.json) | 임베딩 차원 768→1024 |
| 3 | Qdrant 컬렉션 5개 재생성 (기존 삭제 + vector_size=1024로 recreate) | 기존 인덱스 전부 소멸 |
| 4 | 전체 문서 재인덱싱 | 시간 소요 (문서 수에 비례) |
| 5 | `_embed()` consistency check | 1024차원 정상 출력 확인 |
### 위험
- 기존 Qdrant 컬렉션 삭제 시 모든 KB 검색 불가 (재인덱싱 완료까지)
- 1024차원으로 변경 시 메모리 사용량 증가 (약 33%)
- BGE-M3의 한국어 성능 향상이 임계값 이상인지 사전 평가 필요
### 권장
긴급하지 않음. nomic-embed-text로 Phase 7 운영 후, BGE-M3 안정성 확인 후
점진적 마이그레이션. `KbEmbeddingClient.cs`에 dimension을 환경변수/설정에서
읽도록 개선하는 선행 작업 권장.
---
## 실행 우선순위
| 순위 | 작업 | 예상 시간 | 영향도 |
|------|------|-----------|--------|
| 1 | **툴 카드 영구 보존** | 2~3h | HIGH — UX 품질, 데이터 손실 방지 |
| 2 | **KB 청크 미리보기 UI** | 2~3h | MED — 관리자 디버깅 편의 |
| 3 | **시계열 스파클라인** | 1~2h | MED — 데이터 가시성 향상 |
| 4 | **NL2SQL 의도 라우터** | 1~2h | MED — 불필요한 SQL 호출 감소 |
| 5 | **대화 요약** | 1~2h | LOW — 장기 대화 안정성 |
| 6 | **에이전트 모드** | 2~3h | LOW — 고급 기능, Phase 7 후순위 |
| 7 | **BGE-M3 분석/계획 수립** | — | 보류 |
| 8 | **현장 재고 데이터 출처** | — | 보류 |
---
## 빌드/검증 명령
```bash
# .NET 빌드
dotnet build src/Web/ExperionCrawler.csproj
# Python syntax check
python3 -m py_compile mcp-server/server.py mcp-server/worker/nl2sql_worker.py
# Python import check
cd mcp-server && python3 -c "import server"
```
## 런타임 셋업 (코드 외)
- mcp-server 재시작 (의도 라우터 추가 시)
- 브라우저 캐시 무효화 (Ctrl+F5)

View File

@@ -0,0 +1,453 @@
# 옵시디언-식 그래프 구조 적용 플랜
> 옵시디언(Obsidian) 앱을 도입하는 것이 아니라, 옵시디언이 사용하는 **데이터 패턴**(마크다운 노트 + YAML frontmatter + `[[wikilinks]]` + `#tags` + 그래프 탐색)을 현재 RAG 파이프라인 안에 흡수해 P&ID/DXF/PDF만으로 부족한 지식 표현을 보강하는 것이 목적이다.
작성: 2026-05-14 | 대상 스택: vLLM Qwen3.6-27B-FP8 + Ollama nomic-embed-text(768) + Qdrant + PostgreSQL + KbIngestWorker(C#) + MCP(Python)
---
## 1. 배경 — 왜 지금 그래프 구조가 필요한가
### 1.1 현재 한계
| 영역 | 현재 동작 | 한계 |
|------|----------|------|
| P&ID / DXF | `parse_pid_dxf`, `parse_pid_pdf`로 태그/심볼/선 추출 → PostgreSQL `pid_*` 테이블 적재 | 추출 정확도가 도면 품질에 종속. LLM이 그림을 "읽지" 못해 27B로도 흐름/인과 추론이 약함. 누락된 관계는 영원히 누락 |
| PDF/Excel KB | `parse_document`로 텍스트 청크 → 벡터 임베딩 → Qdrant 5개 컬렉션 | 청크는 의미 단위가 아닌 페이지/시트/표 단위. 청크 간 관계(이 절차는 저 알람과 연결됨)는 모델이 매번 "발견"해야 함 |
| 채팅 RAG | `search_kb`/`rag_query`로 top-k 청크 retrieval | 의미적으로 가까운 청크는 잘 찾지만 **명시적 연결**이 없는 항목(같은 루프의 다른 태그, 같은 area의 작년 이벤트)은 안 따라옴 |
| 현장 지식 | 운전원 머릿속 / Excel / Word | 검색·연결·LLM 주입이 불가 |
### 1.2 27B 모델의 현실
`Qwen3.6-27B-FP8` 단일 GPU 서빙. 한 답변에 토큰 46k 정도가 안정 구간. 따라서:
- **검색기가 똑똑할수록 LLM 부담이 줄어든다** — 청크 100개 던지고 LLM에게 정리시키면 토큰·지연 폭증. 청크 510개 + **관계 그래프 1-hop**을 던지는 게 훨씬 싸다.
- **그래프 탐색은 LLM 추론이 아니라 인덱스 조회로 해결**해야 한다. GraphRAG처럼 LLM으로 엔티티/관계를 추출하는 무거운 방식은 우리 GPU에서 비용이 크다.
### 1.3 옵시디언 패턴이 잘 맞는 이유
- **마크다운 = LLM 친화** — 토큰화 효율, 헤딩 기반 청킹 자연스러움
- **`[[wikilinks]]` = 사람이 손으로 만든 명시적 엣지** — LLM이 추출할 필요 없음. 운전원/엔지니어가 노트 쓰면서 자동으로 그래프가 자란다
- **YAML frontmatter = 구조화 메타데이터** — `area`, `loop`, `vendor`, `range` 같은 필드가 노트 수준에서 일관됨
- **`#tags` = 가로 분류** — 폴더 계층과 무관한 횡적 묶음
- 모두 **로컬 텍스트 파일**로 표현 가능 → 백업/git/diff 자유롭고 KB 파이프라인에 그대로 흘려 넣을 수 있음
---
## 2. 채택할 것 / 채택하지 않을 것 (스코프 선)
### 채택
- 노트 = 마크다운 1개 파일 + YAML frontmatter
- `[[note-id]]` 위키링크 문법 — id는 안정적이고 URL-safe
- `#tag` 인라인 태그 + frontmatter `tags:` 둘 다 인정
- 노트 간 **유향 그래프** (`source_note``target_note`, `link_type`)
- 그래프 13 hop 탐색 도구 + 벡터 검색과의 하이브리드
### 채택하지 않음
- ❌ Obsidian 앱 실행 / `.obsidian/` 설정 / plugin 호환
- ❌ Obsidian Sync · Publish
- ❌ Canvas, Excalidraw 등 GUI 부속
- ❌ "Vault 폴더 → 자동 동기화" 패턴 (파일 워처) — 현재 KB는 업로드형 큐 모델이고 일관성 유지에 유리. 동기화는 단방향 임포트 도구로 충분
- ❌ LLM으로 자동 엔티티/관계 추출 (GraphRAG 식) — 27B 처리량 부담. 대신 **결정론적 추출**(정규식·frontmatter·기존 P&ID 파서 결과) + 사용자 작성을 1차로 한다
→ 옵시디언으로 노트 편집하고 싶은 사용자는 `.md` 파일을 외부에서 옵시디언으로 열어도 호환된다. 그것은 **부수효과**이지 의존 관계가 아니다.
---
## 3. 데이터 모델
### 3.1 노트 ID 규칙
`{kind}/{slug}` 형식. 예:
```
tag/fic-6113
loop/compression-a
area/unit-a
drawing/pid-105
procedure/start-up-a
vendor/honeywell-c300
event-pattern/surge-recovery
```
- 모두 소문자, kebab-case
- `tag/`, `loop/`, `area/`, `drawing/`, `procedure/`, `vendor/`, `event-pattern/`, `term/`, `kpi/` 9종 (시드, 확장 가능)
- 위키링크 본문에선 `[[tag/fic-6113]]` 또는 별칭 `[[tag/fic-6113|FIC-6113]]`
### 3.2 Frontmatter 스키마 (공통 + kind별)
공통:
```yaml
---
id: tag/fic-6113 # 필수, 위 ID 규칙
title: FIC-6113 (압축단 1열 입구 유량)
kind: tag # 필수, 9종 중 하나
tags: [flow, area-a, critical]
aliases: [FIC6113, fic-6113.pv]
created: 2026-05-14
updated: 2026-05-14
sources: # 선택 — 이 노트의 근거 문서
- kb_doc_id: 1f3a... # 기존 kb_documents UUID
locator: "p.12 §3.2"
---
```
kind별 추가 필드 (선택, frontmatter라 유연):
| kind | 권장 필드 |
|------|----------|
| `tag` | `pv_tag`, `sp_tag`, `op_tag`, `unit`, `range_min`, `range_max`, `eu`, `area`, `loop` |
| `loop` | `area`, `controllers`, `actuators`, `transmitters`, `setpoint_strategy` |
| `area` | `unit`, `manager`, `process_description` |
| `drawing` | `drawing_number`, `revision`, `pid_doc_id` (기존 pid_drawings.id 연결) |
| `procedure` | `applicable_loops`, `last_review`, `owner` |
| `vendor` | `manufacturer`, `model`, `firmware` |
| `event-pattern` | `event_types`, `area`, `mitigation` |
### 3.3 본문 컨벤션
```markdown
# FIC-6113
압축기 A열 입구 유량 제어. [[loop/compression-a]] 의 1차 제어 변수.
## 정상 운전
- SP: 120 t/h (보통 115125 범위)
- 알람 상한: 135 t/h → 트립: 145 t/h
- 상위 cascade: [[tag/pic-6101]]
## 관련 이벤트 패턴
- [[event-pattern/surge-recovery]] — 빠르게 떨어지면 ESV 확인
- 정기점검 후 첫 기동 시 잦은 흔들림 → [[procedure/start-up-a]] 단계 7 참조
## 참고
- 도면: [[drawing/pid-105]]
- 벤더: [[vendor/honeywell-c300]]
```
- 헤딩 ≤ H3 권장 → 청킹 단위
- 위키링크 `[[id]]` 또는 `[[id|alias]]`
- 인라인 태그 `#flow #critical` 도 허용 (frontmatter `tags:`에 자동 병합)
### 3.4 PostgreSQL 스키마 (신규 5개 테이블)
기존 KB 파이프라인과 **분리된 네임스페이스**(`vault_*` prefix). `kb_documents`는 원본 파일(PDF/Excel)용 그대로 유지, vault는 별도 트랙.
```sql
-- 노트 본체
CREATE TABLE vault_notes (
id TEXT PRIMARY KEY, -- "tag/fic-6113"
kind TEXT NOT NULL, -- 9종 중 하나
title TEXT NOT NULL,
body_md TEXT NOT NULL, -- 원본 마크다운
frontmatter JSONB NOT NULL DEFAULT '{}',
content_hash TEXT NOT NULL, -- SHA256(body_md+frontmatter)
status TEXT NOT NULL DEFAULT 'pending',-- pending/embedding/indexed/failed
error_message TEXT,
source_doc_id UUID REFERENCES kb_documents(id),-- 임포트된 경우
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON vault_notes (kind);
CREATE INDEX ON vault_notes USING gin (frontmatter);
-- 별칭 (id ↔ alias 양방향)
CREATE TABLE vault_aliases (
alias TEXT PRIMARY KEY, -- 소문자 정규화
note_id TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE
);
-- 태그 (frontmatter + 인라인 #tag 병합 결과)
CREATE TABLE vault_tags (
note_id TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (note_id, tag)
);
CREATE INDEX ON vault_tags (tag);
-- 위키링크 (유향 엣지)
CREATE TABLE vault_links (
src_note_id TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
dst_note_id TEXT NOT NULL, -- 미존재 노트도 허용(빨간 링크)
dst_resolved BOOLEAN NOT NULL DEFAULT FALSE, -- 타겟 노트가 실제 존재?
link_type TEXT NOT NULL DEFAULT 'wikilink', -- 향후 'cascade','vendor-of' 등 확장
occurrences INT NOT NULL DEFAULT 1, -- 같은 노트에서 N회 등장
PRIMARY KEY (src_note_id, dst_note_id, link_type)
);
CREATE INDEX ON vault_links (dst_note_id);
-- 청크 (Qdrant point_id ↔ 노트/헤딩 매핑)
CREATE TABLE vault_chunks (
point_id UUID PRIMARY KEY, -- Qdrant point_id
note_id TEXT NOT NULL REFERENCES vault_notes(id) ON DELETE CASCADE,
heading_path TEXT, -- "## 정상 운전 > ### 알람"
chunk_text TEXT NOT NULL,
token_count INT
);
CREATE INDEX ON vault_chunks (note_id);
```
`dst_resolved=false`인 링크는 "아직 노트가 안 만들어진 빨간 링크" — 자라야 할 그래프의 빈자리를 가시화한다.
### 3.5 Qdrant 컬렉션
신규 1개 — `vault` (768-dim cosine, payload만 인덱스: `note_id`, `kind`, `tags`, `area`, `loop`).
기존 `kb_*` 5개와 **공존**. `search_kb`는 그대로, `search_vault`는 신규(아래 §6).
---
## 4. 인덱싱 파이프라인
### 4.1 입력 경로 3가지
| 경로 | 동작 |
|------|------|
| **A. UI에서 마크다운 직접 작성/수정** | 14번 탭 확장. 저장 시 `vault_notes` UPSERT + 인덱싱 큐 적재 |
| **B. zip/폴더 임포트** | `.md` 파일들을 zip으로 업로드 → 일괄 파싱·검증·노트 생성 |
| **C. 기존 데이터 자동 시드** | `node_map_master`/`pid_tags`/`pid_drawings`/`kb_collections` 등에서 자동으로 노트 스켈레톤 생성 (§7.2 별도 절) |
세 경로 모두 동일한 **MarkdownNoteWorker**(C# `BackgroundService`, 기존 `KbIngestWorker` 패턴 차용)로 수렴.
### 4.2 워커 단계 (단일 패스)
```
[큐: vault_notes WHERE status='pending']
1. 파싱 (server.py 또는 C# Markdig)
- frontmatter 추출 → JSON 검증
- 본문에서 [[id]] / [[id|alias]] / #tag 추출
- 헤딩 트리 추출 → 청크 분할 (≤512 토큰, 헤딩 경계 우선)
2. 정규화
- aliases 소문자화, 중복 제거
- 위키링크 id의 alias 해석 (vault_aliases 조회)
- 자기 자신 링크 제거
3. 그래프 갱신 (트랜잭션)
- vault_links 기존 행 삭제 후 재삽입
- dst_resolved 계산: dst가 vault_notes에 존재 OR alias에 존재
- 신규 노트가 들어오면 기존의 'unresolved' 링크들을 다시 resolve 시도
- vault_tags 동기화
4. 임베딩 (Ollama /api/embeddings, 청크별)
- 실패 청크는 skip + error_message에 누적, attempts<3
5. Qdrant upsert
- 기존 doc의 point들 먼저 delete(filter: note_id)
- 신규 청크 일괄 upsert
6. status='indexed' or 'failed'
```
부분 실패 정책은 기존 KbIngestWorker와 동일(전부 실패 시 failed, 일부 실패 시 indexed + error_message).
### 4.3 파싱 위치 선택
- **C# 측 Markdig + 자체 정규식** — 의존성 단순. frontmatter는 `YamlDotNet`.
- 또는 **MCP server.py에 `parse_markdown_note` 도구 추가** — Python `markdown-it-py` + `python-frontmatter` 사용, 기존 parsers/ 구조와 일관.
권장: **server.py에 추가**. 이유: 기존 `parse_document`/`parse_pid_*`와 같은 파이프라인이고, 위키링크·태그 추출 같은 텍스트 처리는 Python이 더 쉬움. C# Worker는 큐 관리·Qdrant 호출만 담당.
---
## 5. 하이브리드 검색 (핵심 차별점)
### 5.1 단계
```
질의 q
A. 벡터 검색 (Qdrant vault collection, top_k=10)
→ 시드 노트 집합 S = { note_id of top hits }
B. 그래프 확장 (PostgreSQL, 1-hop, 옵션 2-hop)
→ for each s in S:
out_links = vault_links WHERE src=s AND dst_resolved
in_links = vault_links WHERE dst=s
→ 확장 집합 E = S neighbors
C. 재순위 (rule-based, LLM 호출 없음)
- 벡터 점수 + boost(같은 area), boost(같은 loop)
- kind 우선순위 가중 (질의에 "절차" 포함 시 procedure 가중)
D. 청크 선택 + 컨텍스트 패킹
- 시드 노트는 top-2 청크
- 이웃 노트는 frontmatter + 첫 문단(요약)만
- 총 토큰 ≤ 3000 budget
LLM에 주입
```
핵심: **그래프 확장은 PostgreSQL JOIN 한두 번**. LLM 호출 0회. 27B GPU 부담 없음.
### 5.2 옵시디언의 "백링크"가 주는 가치
특정 태그 노트(`tag/fic-6113`)에 대한 백링크 목록 = 그 태그를 언급한 모든 절차/이벤트패턴/도면. 운전원이 "FIC-6113 이상해" 라고 물으면:
1. 벡터 검색이 `tag/fic-6113` 노트를 시드로 잡고
2. 백링크로 `[[procedure/start-up-a]]`, `[[event-pattern/surge-recovery]]` 가 따라붙고
3. cascade frontmatter로 `[[tag/pic-6101]]`도 자동 추가
→ 27B에게 던지는 컨텍스트가 **의미 검색만으로는 절대 못 모을 조합**으로 채워진다.
---
## 6. MCP 도구 (신규)
`server.py`에 추가. 모두 PostgreSQL/Qdrant 조회 — LLM 호출 없음.
| 도구 | 시그니처 | 동작 |
|------|---------|------|
| `vault_search` | `(query, kinds?, tags?, top_k=8, expand_hops=1)` | §5의 하이브리드 검색. 시드 + 확장 노트 목록 + 청크 반환 |
| `vault_get_note` | `(note_id, include_body=True)` | 단일 노트 전체. 헤딩 트리·frontmatter·in/out 링크 카운트 |
| `vault_backlinks` | `(note_id, limit=50)` | 이 노트를 가리키는 모든 src 노트 (`vault_links` WHERE dst) |
| `vault_neighbors` | `(note_id, hops=1, kinds?)` | 13 hop 이웃. 시각화·운전원 질문 "이 루프에 뭐 있어" |
| `vault_by_tag` | `(tag, kind?, limit=50)` | 태그 매칭 노트 목록 |
| `vault_unresolved` | `(limit=50)` | dst_resolved=false 링크 리스트 — "비어있는 노트" 진단 |
| `vault_update_note` | `(note_id, body_md, frontmatter)` | (admin) 노트 upsert. 큐 적재. 운전원이 채팅 중 "이거 노트로 저장" 흐름 위해 |
채팅 의도 라우터(`_classify_intent`)에도 규칙 추가:
```
"노트|이 태그.*정보|관련.*뭐|연결" → vault_search
"백링크|어디서.*언급" → vault_backlinks
```
`ToolGuideKo`에 위 도구들 추가, `rag_query` 확장 옵션 `use_vault=True` (벡터-only 모드와 토글 가능).
---
## 7. UI (14번 탭 확장 + 신규 카드 / 신규 탭은 보류)
> 신규 탭(15번)을 또 만들기보다 **14번 "RAG 관리" 탭 안에 두 번째 모드 토글**을 두는 게 코드/사용자 인지부하 면에서 깔끔하다.
### 7.1 14번 탭 — 모드 전환
```
[원본 문서] | [Vault 노트] ← 토글
```
### 7.2 Vault 노트 모드 화면
- 좌측: kind 필터(tag/loop/area/...) + 태그 facet + 빨간 링크 카운트
- 중앙: 노트 목록 (검색·정렬·일괄 선택)
- 우측: 단일 노트 뷰
- 상단: frontmatter 폼 (수정 가능, 저장 시 큐 적재)
- 가운데: 마크다운 편집기 (textarea + 프리뷰 토글, [[ 자동완성)
- 하단: "백링크 (N)" / "이웃 그래프 미니뷰" (SVG 간단한 force-layout — 100 노드 미만, 그 이상은 리스트로 fallback)
### 7.3 채팅 통합
- `vault_search` 결과 카드: 노트 제목 클릭 시 14번 탭의 해당 노트로 점프
- `[[id]]` 토큰이 답변에 등장하면 자동으로 다운로드/노트뷰 링크 치환 (현재 `llmLinkKbCitations`와 동일 패턴 확장)
신규 라이브러리는 도입하지 않는다 — 마크다운 렌더는 기존이 있으면 재사용, 없으면 textarea + 단순 `marked` CDN 한 줄.
---
## 8. P&ID/DXF 한계 해결 — 자동 시드 + 운전원 보강
### 8.1 자동 시드 (1회성 마이그레이션 잡)
기존 PostgreSQL 데이터에서 결정론적으로 노트를 만든다 (LLM 0회):
| 소스 | 생성될 노트 | frontmatter 자동 채움 |
|------|------------|-----------------------|
| `node_map_master` | `tag/{base-tag}` 각 1개 | `pv_tag`, `sp_tag`, `op_tag`, `unit`, `range_*`, `eu` |
| `pid_drawings` | `drawing/{drawing_number}` | `revision`, `pid_doc_id` |
| `pid_tags` (drawing 안의 태그) | drawing 노트 본문에 `[[tag/...]]` 위키링크로 자동 삽입 | — |
| `kb_collections` 시드 5종 | `term/system-instrument` 등 | — |
| `event_history_table` 빈도 상위 패턴 | (보류 — LLM 없이 군집화 어려움) | — |
→ 첫 임포트로 **수천 개 노트가 빨간 링크 없는 상태**로 깔린다. P&ID에서 못 잡은 관계는 운전원이 노트 본문에 `[[tag/...]]` 한 줄 추가로 채운다. 채울수록 그래프가 풍부해진다.
### 8.2 DXF/PDF는 그대로 두되 결과를 노트로 발행
`parse_pid_dxf` 결과는 지금처럼 `pid_*` 테이블에 그대로. 거기에 더해:
- drawing 1개 = 노트 1개 (자동 생성, 본문은 태그 리스트 + 도면 파일 임베드 링크)
- DXF 파싱이 인식한 태그 = 해당 drawing 노트 본문에 위키링크
- 인식 못 한 영역은 노트 본문 하단에 `## TODO` 섹션 자동 생성 → 운전원이 채움
**LLM이 도면을 "읽을" 필요가 없어진다.** 운전원이 노트로 옮긴 정보만 LLM이 본다.
---
## 9. 하드웨어/성능 예측
가정: 자동 시드 후 노트 ~3,000개, 평균 본문 1.5KB, 청크 평균 3개/노트 → 청크 ~9,000개.
| 자원 | 추정 | 비고 |
|------|------|------|
| PostgreSQL 디스크 | < 50 MB | vault_notes(본문) + 인덱스 |
| Qdrant 메모리 | 9,000 × 768 × 4byte ≈ 28 MB + HNSW 오버헤드 ≈ **< 100 MB** | 기존 ws-/experion-opc-docs와 별도 |
| 임베딩 초기 비용 | 9,000 청크 × nomic-embed-text | CPU/GPU에 따라 다르지만 단발성. 이후 변경된 노트만 |
| 검색 (벡터+1hop) | Qdrant 10ms + PG 5ms ≈ **< 50ms** | LLM 호출 없음 |
| 검색 (벡터+2hop) | PG가 노드 수 폭발 가능 → **2-hop은 옵션 + 노드 cap=200** | |
| LLM 컨텍스트 증가 | 시드 청크 5 + 이웃 메타 10개 ≈ **+1.5k 토큰** | 27B 컨텍스트 32k 한계 내 안전 |
| 동시성 | 워커 1개 단일 패스 (기존 정책 유지) | 임베딩 큐가 LLM과 GPU 충돌 시: **임베딩은 Ollama(별도 프로세스/CPU 가능)** 이므로 vLLM과 분리 |
**하드웨어 추가 부담 거의 없음**. 가장 큰 비용은 초기 임베딩 1회.
---
## 10. Phase 분할 (현실적 단위)
### Phase A — 데이터 모델 + 워커 골격 (변경 최소, 채팅 영향 없음)
- `vault_*` 테이블 5개 DDL (ExperionDbContext.InitializeAsync에 추가, 기존 KB DDL 패턴 답습 — `NpgsqlConnection.ExecuteNonQueryAsync` 사용해 중괄호 이슈 회피)
- `VaultNote` / `VaultLink` / `VaultTag` / `VaultChunk` 엔티티
- `IVaultService` 인터페이스 + 구현 (CRUD + 큐 적재)
- `MarkdownNoteWorker` BackgroundService (parse → embed → index)
- `server.py``parse_markdown_note` MCP 도구
- 검증: 손으로 `.md` 1개 업로드 → `indexed` 도달 + Qdrant 점 생성 확인
### Phase B — MCP 도구 + 채팅 연결
- `vault_search` / `vault_get_note` / `vault_backlinks` / `vault_neighbors` / `vault_by_tag` / `vault_unresolved`
- `ToolGuideKo` 갱신
- 의도 라우터 규칙 추가
- 채팅 결과 카드에 노트 인용 렌더링 (`llmLinkVaultCitations`)
- 검증: 채팅에서 "FIC-6113 관련 노트 보여줘" → 도구 호출 + 카드 표시
### Phase C — UI (14번 탭 확장)
- 모드 토글, 노트 목록, 노트 뷰/편집, frontmatter 폼
- 빨간 링크 진단 패널
- 이웃 그래프 미니뷰 (간단 SVG)
- zip 임포트 엔드포인트
### Phase D — 자동 시드 마이그레이션
- `node_map_master` → tag 노트 생성 잡
- `pid_drawings` + `pid_tags` → drawing 노트 + 위키링크
- 1회성 admin-only 엔드포인트 `POST /api/vault/seed` (dry-run 옵션 포함)
### Phase E (선택) — 점진적 자동화
- 채팅에서 LLM이 답변하면서 새 fact를 발견하면 "이걸 [[tag/...]] 노트에 추가할까요?" 제안 → 운전원 확인 시 `vault_update_note` 호출
- 운영자가 일정 누르면 "지난주 자주 등장한 unresolved 링크 N개 — 노트 만들기" 작업 큐
Phase AB는 12일, C는 1일, D는 0.5일, E는 보류 가능.
---
## 11. 잔여 결정사항 (사용자 확인 필요)
| 항목 | 옵션 | 코멘트 |
|------|------|--------|
| 노트 ID 충돌 시 | (1) 거부 (2) 버전 suffix | 기본 (1) 권장 |
| `.md` 외부 편집 허용 여부 | (1) UI 전용 (2) 파일 export/import 양방향 | 양방향은 동기화 충돌 비용 큼. (1) 권장. 필요 시 read-only export만 |
| 옵시디언 앱과 호환 (부수효과) | (1) frontmatter 키 호환 (2) Obsidian 전용 키(`cssclasses` 등) 무시 | (1) — 우리가 정의한 키 외엔 그냥 frontmatter에 보존만 하면 됨 |
| 이웃 그래프 시각화 라이브러리 | (1) 직접 SVG (2) Cytoscape.js (3) vis-network | (1) 노드 ≤ 100 가정. 그 이상은 리스트 fallback |
| 자동 시드 후 첫 화면 | (1) 모든 태그 노트가 "빈 본문" (2) 자동으로 한 줄 요약 LLM 생성 | (1) 권장 — LLM 비용 회피 + 운전원이 손으로 채우면서 도메인 정합 |
| 백업/git | `storage/vault-export/` 매일 1회 `.md` 덤프 | DB 백업과 별개로 사람이 읽을 수 있는 형태 보존 |
---
## 12. 핵심 메시지
- **옵시디언 도입이 아니다.** 옵시디언이 검증한 4개 패턴(마크다운 + frontmatter + 위키링크 + 태그)만 빌려와 기존 KB 위에 **그래프 레이어**를 얹는다.
- **27B로 그래프를 "추론"시키지 않는다.** 그래프는 사람·규칙·기존 DB로 만든다. LLM은 만들어진 그래프를 **탐색**한 결과를 받아쓴다.
- **P&ID 한계는 노트로 우회한다.** 도면이 표현 못 한 관계를 운전원이 위키링크 한 줄로 채운다. 채울수록 RAG 품질이 자란다.
- **하드웨어 부담은 임베딩 초기 1회 + Qdrant < 100 MB.** 검색 경로에 LLM 추가 호출이 없다.
- **기존 KB와 공존**한다. PDF/Excel은 그대로, vault는 별도 트랙. 채팅은 두 채널 결과를 모두 본다.