#!/usr/bin/env python3 """ Qwen3-Coder-Next-FP8 RAG 연동 벤치마크 - Qdrant 코드베이스 + OPC UA 문서에서 컨텍스트 수집 - 수집된 실제 코드/문서 기반으로 복잡한 신규 기능 구현 요청 - 스트리밍으로 토큰/초 측정 """ import time import sys import httpx from openai import OpenAI VLLM_BASE_URL = "http://localhost:8000/v1" VLLM_MODEL = "glm-4.7-flash" OLLAMA_URL = "http://localhost:11434" EMBED_MODEL = "nomic-embed-text" QDRANT_URL = "http://localhost:6333" COL_CODEBASE = "ws-65f457145aee80b2" COL_OPC_DOCS = "experion-opc-docs" def embed(text: str) -> list[float]: with httpx.Client(timeout=30) as c: r = c.post(f"{OLLAMA_URL}/api/embeddings", json={"model": EMBED_MODEL, "prompt": text}) r.raise_for_status() return r.json()["embedding"] def search(collection: str, query: str, top_k: int = 5) -> list[dict]: vec = embed(query) with httpx.Client(timeout=20) as c: r = c.post( f"{QDRANT_URL}/collections/{collection}/points/search", json={"vector": vec, "limit": top_k, "with_payload": True}, ) r.raise_for_status() return r.json()["result"] def fmt_hits(hits: list[dict], label: str) -> str: chunks = [] for i, h in enumerate(hits, 1): p = h["payload"] src = p.get("file_path") or p.get("source") or p.get("filename") or "unknown" text = p.get("text") or p.get("content") or p.get("chunk") or str(p) score = h.get("score", 0) chunks.append(f"[{label} #{i} | {src} | score={score:.3f}]\n{text}") return "\n\n".join(chunks) def run_benchmark(): client = OpenAI(base_url=VLLM_BASE_URL, api_key="dummy") # ── RAG 컨텍스트 수집 ────────────────────────────────────────────────────── print("RAG 검색 중...") t0 = time.perf_counter() # 코드베이스: 실시간 서비스 구조 + DB 저장 패턴 hits_realtime = search(COL_CODEBASE, "ExperionRealtimeService FlushLoop subscription MonitoredItem", top_k=4) hits_db = search(COL_CODEBASE, "ExperionDbContext history snapshot PostgreSQL EF Core", top_k=3) # OPC UA 문서: 알람/이벤트 관련 hits_alarm = search(COL_OPC_DOCS, "alarm event notification EventNotifier condition OPC UA", top_k=4) rag_time = time.perf_counter() - t0 total_hits = len(hits_realtime) + len(hits_db) + len(hits_alarm) print(f"검색 완료: {total_hits}개 청크 ({rag_time:.2f}s)") print() ctx_realtime = fmt_hits(hits_realtime, "코드베이스/Realtime") ctx_db = fmt_hits(hits_db, "코드베이스/DB") ctx_alarm = fmt_hits(hits_alarm, "OPC UA 문서/Alarm") # ── 프롬프트 구성 ────────────────────────────────────────────────────────── prompt = f"""\ 아래는 ExperionCrawler 프로젝트의 실제 코드와 OPC UA 공식 문서 발췌입니다. 이 컨텍스트를 기반으로 새로운 기능을 구현해줘. ━━━ 코드베이스 컨텍스트 ━━━ {ctx_realtime} {ctx_db} ━━━ OPC UA 문서 컨텍스트 ━━━ {ctx_alarm} ━━━ 구현 요청 ━━━ 위 컨텍스트를 바탕으로 ExperionAlarmService를 C#으로 구현해줘. 요구사항: 1. `IHostedService` + `IExperionAlarmService` 패턴 (기존 ExperionRealtimeService와 동일한 구조). 2. OPC UA `EventNotifier` 방식으로 알람/이벤트를 구독한다. 구독 대상 EventType: ConditionType, AlarmConditionType (OPC UA 표준). 3. 이벤트 수신 시 다음 정보를 `alarm_history` PostgreSQL 테이블에 저장한다: - `id` (bigserial), `tagname`, `event_type`, `severity` (int), `message`, `active` (bool), `occurred_at` (timestamptz) 4. 기존 `ExperionDbContext` / EF Core 패턴을 따른다 (새 DbSet 추가). 5. 컨트롤러 `ExperionAlarmController` — start/stop/status + 최근 알람 조회 (GET /api/alarm/recent?limit=50). 6. `appsettings.json`에 `AlarmServer` 섹션 추가 (NodeId 목록, MaxSeverityFilter). 7. 각 클래스/메서드에 한 줄 XML 문서 주석 포함. 코드는 완성된 형태로 작성하고, 파일별로 명확히 구분해줘. """ prompt_chars = len(prompt) print(f"프롬프트 길이: {prompt_chars:,} chars (RAG 컨텍스트 포함)") print(f"모델: {VLLM_MODEL}") print("=" * 60) print() # ── 스트리밍 LLM 요청 ────────────────────────────────────────────────────── stream = client.chat.completions.create( model=VLLM_MODEL, messages=[ { "role": "system", "content": ( "당신은 C#/.NET 백엔드와 OPC UA 프로토콜 전문가입니다. " "ExperionCrawler 프로젝트의 기존 코드 스타일과 패턴을 그대로 따르며 " "완성도 높은 코드를 작성합니다." ), }, {"role": "user", "content": prompt}, ], max_tokens=4096, temperature=0.1, stream=True, stream_options={"include_usage": True}, ) # ── 스트리밍 수신 + 측정 ──────────────────────────────────────────────────── first_token_time = None start_time = time.perf_counter() completion_tokens = 0 for chunk in stream: if chunk.usage: completion_tokens = chunk.usage.completion_tokens if not chunk.choices: continue delta = chunk.choices[0].delta if delta.content: if first_token_time is None: first_token_time = time.perf_counter() ttft = first_token_time - start_time print(f"[TTFT: {ttft:.3f}s] ", end="", flush=True) sys.stdout.write(delta.content) sys.stdout.flush() end_time = time.perf_counter() # ── 결과 출력 ────────────────────────────────────────────────────────────── total_time = end_time - start_time gen_time = end_time - (first_token_time or start_time) tps_gen = completion_tokens / gen_time if gen_time > 0 else 0 tps_wall = completion_tokens / total_time if total_time > 0 else 0 print() print() print("=" * 60) print(f"RAG 검색 시간 : {rag_time:.2f}s ({total_hits}개 청크)") print(f"총 출력 토큰 : {completion_tokens:,}") print(f"총 소요 시간 : {total_time:.2f}s") print(f"생성 시간 : {gen_time:.2f}s (첫 토큰 이후)") print(f"TTFT : {(first_token_time or start_time) - start_time:.3f}s") print(f"토큰 속도 : {tps_gen:.1f} tok/s (생성 구간)") print(f"토큰 속도 : {tps_wall:.1f} tok/s (전체 구간)") print("=" * 60) if __name__ == "__main__": run_benchmark()