- 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 업데이트 - 정리: 진단 체크리스트 문서 삭제
266 lines
9.3 KiB
Markdown
266 lines
9.3 KiB
Markdown
# DXF P&ID 추출 개선 2차 — 배관번호·펌프 태그 누락 버그 수정
|
|
|
|
작성일: 2026-05-17
|
|
작성: Claude Sonnet 4.6
|
|
|
|
---
|
|
|
|
## 문제 개요
|
|
|
|
P&ID DXF 파일에서 아래 태그들이 추출되지 않는 버그 보고:
|
|
|
|
- `P-10101` (10차 펌프 장비 태그)
|
|
- `P-10101-25A-F1A-n` (10차 프로세스 배관번호)
|
|
- `P-9101`, `P-9102` (9차 펌프 장비 태그)
|
|
- `P-9107-25A-F-n` 등 (9차 배관번호)
|
|
|
|
사용자가 "중복방지 로직에 차단된 것 아닌가" 질문함 → 실제로는 **regex 불일치** 와 **레이어 처리 누락** 이 원인.
|
|
|
|
---
|
|
|
|
## 실제 DXF 구조 조사 결과
|
|
|
|
### 파일별 배관번호 형식 차이
|
|
|
|
| DXF 파일 | 배관번호 레이어 | 형식 예시 | 필드 수 |
|
|
|----------|--------------|---------|--------|
|
|
| `p9-p&id-20.03.19.dxf` (9차) | `LINENO` | `P-9107-25A-F-n` | **5필드** |
|
|
| `p9-p&id-20.03.19.dxf` (9차) | `14-D-PIPELINE-LINE` | `CHR-9641-50A-F-C50` | **6필드** |
|
|
| `plant-10100-only.dxf` (10차) | `LINENO` | `P-10101-25A-F1A-n` | **7필드** |
|
|
| `10차플랜트-P&ID.dxf` | `LINENO` | `P-10138-600A-F2A-H100` | **7필드** |
|
|
|
|
### 펌프 태그 레이어
|
|
|
|
| DXF 파일 | 레이어 | 예시 |
|
|
|----------|-------|------|
|
|
| 9차 플랜트 | `0` | `P-9101`, `P-9116`, `P-9201` |
|
|
| 10차 플랜트 | `0`, `1` | `P-10101`, `VP-10117`, `DP-10101` |
|
|
|
|
펌프 태그는 모두 일반 TEXT 엔티티 → 추출 자체는 가능했으나 배관번호로 오인되는 문제 있었음.
|
|
|
|
---
|
|
|
|
## 버그 분석
|
|
|
|
### Bug 1: `_PID_LINENO_FULL_RE` regex가 9차 배관번호 형식 불일치
|
|
|
|
**파일**: `mcp-server/server.py`
|
|
|
|
**기존 regex (7필드 고정)**:
|
|
```
|
|
^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Z])(\d)([A-Z])-([A-Za-z0-9]+)$
|
|
```
|
|
|
|
그룹 구조: `SERVICE - LINENUM - SIZE - MATERIAL - FLANGE_DIGIT - INSUL_CODE - INSUL_THICK`
|
|
|
|
| 입력 | 결과 | 이유 |
|
|
|------|------|------|
|
|
| `P-10101-25A-F1A-n` | ✓ MATCH | F→1→A→n 순서 맞음 |
|
|
| `P-9107-25A-F-n` | ✗ FAIL | F 다음에 `\d` 기대하나 `-n` 등장 |
|
|
| `CHR-9641-50A-F-C50` | ✗ FAIL | 동일 이유 |
|
|
|
|
9차 플랜트는 플랜지등급 숫자와 단열코드가 분리되지 않고 `F-n`, `F-H50` 형식으로 통합되어 있어 7필드 regex에 걸리지 않음.
|
|
|
|
---
|
|
|
|
### Bug 2: `_extract_pid_dxf_fast`가 레이어별로만 배관번호 판단
|
|
|
|
**파일**: `mcp-server/server.py`
|
|
|
|
**기존 로직**:
|
|
```python
|
|
if layer == 'LINENO': # LINENO 레이어만 배관번호 처리
|
|
parsed = _parse_pid_lineno(txt)
|
|
...
|
|
continue
|
|
if _PID_TAG_RE.match(txt): # 그 외 레이어는 TAG_RE만 체크
|
|
...
|
|
```
|
|
|
|
결과:
|
|
- `14-D-PIPELINE-LINE` 레이어의 `CHR-9641-50A-F-C50` → TAG_RE 불일치 → **완전 누락**
|
|
- 다른 도면에서 배관번호 레이어 이름이 다르면 → **모두 누락**
|
|
|
|
레이어 이름 하드코딩은 도면 간 이식성이 없음. 레이어 이름이 아닌 **regex 패턴으로 판단**해야 함.
|
|
|
|
---
|
|
|
|
### Bug 3: `build_pid_graph_parallel` pump extractor가 배관번호를 펌프로 오인
|
|
|
|
**파일**: `mcp-server/worker/pid_extract_prompts.py`
|
|
|
|
LLM pump extractor 프롬프트에 5자리 예시만 있고 배관번호 제외 지시 없음:
|
|
```
|
|
Examples: P-10101, VP-10117, DP-10101, C-10201, CP-10301, BP-10401
|
|
```
|
|
|
|
DXF 전체 텍스트에 `P-10101-25A-F1A-n`이 포함되어 있을 때 LLM이 이를 보고 `P-10101`로 잘못 추출.
|
|
→ Phase 4 `seen_tagnos` 중복 제거에서 실제 펌프 P-10101과 충돌
|
|
→ 배관번호 P-10101-25A-F1A-n은 graph에서 완전 누락
|
|
|
|
(사용자가 의심한 "중복방지 로직 차단"은 이 케이스에 해당 — 다만 원인은 LLM의 잘못된 추출임)
|
|
|
|
---
|
|
|
|
## 수정 내용
|
|
|
|
### Fix 1: `_PID_LINENO_FULL_RE` — 5~7필드 통합 regex
|
|
|
|
**`mcp-server/server.py`**
|
|
|
|
```python
|
|
# 기존 (7필드 고정)
|
|
_PID_LINENO_FULL_RE = re.compile(
|
|
r'^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Z])(\d)([A-Z])-([A-Za-z0-9]+)$'
|
|
)
|
|
|
|
# 수정 (5~7필드 통합: pipe_spec이 F, F1A, F2A 등 가변)
|
|
_PID_LINENO_FULL_RE = re.compile(
|
|
r'^([A-Z][A-Z0-9]{0,3})-(\d{3,6})-(\d{1,4}[A-Z]?)-([A-Za-z][A-Za-z0-9]*)-([A-Za-z0-9]+)$'
|
|
)
|
|
```
|
|
|
|
새 그룹: `(service, line_no, size, pipe_spec, insul)`
|
|
|
|
| 입력 | 매칭 | pipe_spec | insul |
|
|
|------|------|-----------|-------|
|
|
| `P-9107-25A-F-n` | ✓ | F | n |
|
|
| `P-9113-20A-F-H50` | ✓ | F | H50 |
|
|
| `CHR-9641-50A-F-C50` | ✓ | F | C50 |
|
|
| `P-10101-25A-F1A-n` | ✓ | F1A | n |
|
|
| `P-10138-600A-F2A-H100` | ✓ | F2A | H100 |
|
|
| `VG-6203-15A-F1A-n` | ✓ | F1A | n |
|
|
|
|
`_parse_pid_lineno` 반환값도 그룹 수에 맞게 단순화:
|
|
```python
|
|
# 기존: material_spec, flange_rating, insul_code, insul_thickness (4개 필드)
|
|
# 수정: pipe_spec, insul (2개 필드로 통합)
|
|
return {
|
|
"raw": token, "service": service, "fluid": ...,
|
|
"line_no": line_no, "size": size,
|
|
"pipe_spec": pipe_spec, # F, F1A, F2A 등
|
|
"insul": insul, # n, H50, H100, C50 등
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Fix 2: `_extract_pid_dxf_fast` — regex 우선, 레이어는 보조 힌트로만
|
|
|
|
**`mcp-server/server.py`**
|
|
|
|
```python
|
|
# 기존: 레이어 이름 == 'LINENO' 이면 배관번호
|
|
if layer == 'LINENO':
|
|
parsed = _parse_pid_lineno(txt)
|
|
...
|
|
continue
|
|
if _PID_TAG_RE.match(txt):
|
|
...
|
|
|
|
# 수정: FULL_RE 매칭 → 레이어 무관 배관번호, 짧은 형식만 레이어 힌트 사용
|
|
if _PID_LINENO_FULL_RE.match(txt): # 완전한 배관번호 → 레이어 무관
|
|
parsed = _parse_pid_lineno(txt)
|
|
if parsed is not None:
|
|
linenos.append(parsed)
|
|
continue
|
|
|
|
if 'LINENO' in layer.upper(): # 레이어 이름에 LINENO 포함 → 짧은 형식도 배관번호
|
|
parsed = _parse_pid_lineno(txt) # (P-10101 같은 단순형은 펌프와 구분 불가능,
|
|
if parsed is not None: # 레이어 힌트 불가피)
|
|
linenos.append(parsed)
|
|
continue
|
|
|
|
if _PID_TAG_RE.match(txt): # 일반 장비/계기 태그
|
|
...
|
|
```
|
|
|
|
핵심 원칙: **완전한 배관번호는 regex로 식별, 레이어 이름에 의존하지 않음**
|
|
|
|
---
|
|
|
|
### Fix 3: pump extractor 프롬프트 개선
|
|
|
|
**`mcp-server/worker/pid_extract_prompts.py`**
|
|
|
|
```python
|
|
_PUMP_PROMPT = _PROMPT_HEADER + """
|
|
Extract ONLY pumps and compressors (simple equipment tags, NO pipe size suffix).
|
|
|
|
Target equipment types: P (pump), VP (vertical pump), DP (dual pump),
|
|
C (compressor), CP (centrifugal pump), BP (booster pump), SP (sump pump),
|
|
and their variants.
|
|
|
|
Examples (4~5 digit loop numbers): P-10101, VP-10117, DP-10101, C-10201, P-9101, P-9116, VP-9201
|
|
|
|
IMPORTANT: Do NOT extract pipeline/line numbers that have a pipe size suffix (e.g. 25A, 50A, 100A).
|
|
SKIP (pipeline, not a pump): P-10101-25A-F1A-n, P-9107-25A-F-n, CHR-9641-50A-F-C50
|
|
INCLUDE (pump tag): P-10101, VP-10117, P-9101
|
|
"""
|
|
```
|
|
|
|
변경점:
|
|
- 4자리 번호 예시 추가 (`P-9101`, `P-9116`, `VP-9201`)
|
|
- 배관번호 제외 지시 명시 (파이프 사이즈 suffix 있으면 제외)
|
|
- SKIP / INCLUDE 예시로 명확하게 구분
|
|
|
|
---
|
|
|
|
## 검증 결과
|
|
|
|
### regex 단위 테스트 (14/14 통과)
|
|
|
|
```
|
|
✓ P-9107-25A-F-n → pipe (9차 5필드)
|
|
✓ P-9113-20A-F-H50 → pipe (9차 단열)
|
|
✓ P-9127-500A-F-H100 → pipe (9차 대구경)
|
|
✓ P-10101-25A-F1A-n → pipe (10차 7필드)
|
|
✓ P-10138-600A-F2A-H100→ pipe (10차 대구경)
|
|
✓ CHR-9641-50A-F-C50 → pipe (냉각수 6필드)
|
|
✓ VG-6203-15A-F1A-n → pipe (벤트가스)
|
|
✓ SW-10810-25A-F1A-E50 → pipe (소프트워터)
|
|
✓ P-10101 → tag (10차 펌프)
|
|
✓ P-9101 → tag (9차 펌프)
|
|
✓ VP-10117 → tag (진공펌프)
|
|
✓ FIT-10101 → tag (유량계)
|
|
✓ FCV-6113 → tag (유량제어밸브)
|
|
✓ PT-9101 → tag (압력계)
|
|
```
|
|
|
|
### 실제 DXF 엔드투엔드 검증
|
|
|
|
```
|
|
=== p9-p&id-20.03.19.dxf (9차) ===
|
|
배관번호 총 242개 (P-: 83개) ← 수정 전: 0개
|
|
P 배관번호 예시: P-9107-25A, P-9114-20A, P-9113-20A, ...
|
|
펌프 태그: P-9101, P-6101, P-201, P-9201, P-9116
|
|
|
|
=== plant-10100-only.dxf (10차) ===
|
|
배관번호 총 96개 (P-: 57개)
|
|
P 배관번호 예시: P-10138-600A, P-10143-32A, P-10127-65A, ...
|
|
펌프 태그: P-10101, P-10114, P-10116, P-10118
|
|
```
|
|
|
|
---
|
|
|
|
## 수정 파일 목록
|
|
|
|
| 파일 | 변경 라인 | 내용 |
|
|
|------|---------|------|
|
|
| `mcp-server/server.py` | ~221 | `_PID_LINENO_FULL_RE` regex 교체 |
|
|
| `mcp-server/server.py` | ~244 | `_parse_pid_lineno` 반환값 `pipe_spec`/`insul`로 단순화 |
|
|
| `mcp-server/server.py` | ~305 | `_extract_pid_dxf_fast` 레이어/배관번호 처리 로직 수정 |
|
|
| `mcp-server/server.py` | ~359 | `_extract_pid_tags_from_text` step 1 출력에 `pipeSpec`/`insul` 추가 |
|
|
| `mcp-server/worker/pid_extract_prompts.py` | ~61 | `_PUMP_PROMPT` 개선 |
|
|
|
|
---
|
|
|
|
## 설계 결정 사항
|
|
|
|
| 항목 | 결정 | 이유 |
|
|
|------|------|------|
|
|
| regex 필드 통합 방식 | 5필드 통합 (`pipe_spec`이 F, F1A, F2A 통합) | 플랜트마다 배관 사양 코드 체계가 달라 고정 필드 분해는 취약 |
|
|
| 레이어 이름 역할 | FULL_RE 불일치 시 보조 힌트로만 사용 | 레이어 이름은 회사·도면마다 다름. regex가 primary. |
|
|
| 짧은 배관번호(`P-10101`) 처리 | LINENO 계열 레이어에서만 배관번호로 인식 | `P-10101`은 펌프 태그와 텍스트가 동일 → 레이어 힌트 불가피 |
|
|
| C# PidExtractorService | 미수정 | 펌프 태그(TEXT 엔티티)는 기존 코드에서 정상 추출됨. ATTRIB 읽기 추가는 별도 검토 필요 |
|