- 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 업데이트 - 정리: 진단 체크리스트 문서 삭제
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.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)
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이면 툴 카드 렌더링 생략 (기존처럼 동작).
검증
- 새 채팅에서 도구 호출 → SSE 완료 후 localStorage 확인 →
toolCalls배열 저장됨 - F5 새로고침 → 이전 대화 열기 → 툴 카드가 정상 렌더링
- 기존 세션(툴 카드 없는 메시지) → 오류 없음
- 저장/로드 시 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)