Files
ExperionCrawler/plans/빅피클-잔여작업-코딩계획.md
windpacer 302183c97e 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 업데이트
- 정리: 진단 체크리스트 문서 삭제
2026-05-21 23:36:57 +09:00

20 KiB

빅피클 — 잔여 작업 상세 코딩 계획

기준: 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
_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 도구 항목 추가

분류 규칙 (예시)

_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 응답 형식

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 | "📈 추세 보기" 토글 버튼 |

스파클라인 생성 코드 (예시)

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.messagestool_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)

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)

// 변경 전
const assistantMsg = { role: 'assistant', content: '' };

// 변경 후
const assistantMsg = { role: 'assistant', content: '', toolCalls: [] };

SSE tool_start 핸들러 (line 3838)

// 추가
assistantMsg.toolCalls.push({ id: t.id, name: t.name, args: t.args, ok: null, payload: null });

SSE tool_result 핸들러 (line 3846)

// 추가
const tc = assistantMsg.toolCalls.find(x => x.id === t.id);
if (tc) { tc.ok = t.ok; tc.payload = t.payload; }

llmRenderMessages() - toolCalls 렌더링 (line 3449 이후)

// 각 메시지의 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 현장 재고 데이터 출처 보류

빌드/검증 명령

# .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)