# 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 풀구현을 기다리지 않아도 됨.
