From b2a8634fde61279c6616bcdd47fe0104838c198c Mon Sep 17 00:00:00 2001 From: bangae1 Date: Wed, 28 Jan 2026 21:24:22 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 11 ++--- config/ai/org_transformer.py | 89 +++++++++++++++++++++++------------- test.py | 2 +- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/app.py b/app.py index 59c73f6..3a01fec 100644 --- a/app.py +++ b/app.py @@ -105,11 +105,8 @@ def query_select_summarize_stream(results, query, ai, min_similarity: float = 0. else : print('일반', results.get('documents')) context_parts = [f'검색된 사용자 수는 {len(results.get('ids'))}'] - docs = [f"{d[d.find('[이름]'): d.find('[', d.find('[이름]')+1)]} {d[d.find('[부서]'): d.find('[', d.find('[부서]')+1)]}" for d in results.get('documents')] - - - - context = "\n".join(context_parts + docs) + docs = [f"[{d[d.find('[이름]'): d.find('[', d.find('[이름]')+1)]} {d[d.find('[부서]'): d.find('[', d.find('[부서]')+1)]}]" for d in results.get('documents')] + context = ",".join(context_parts + docs) print(context) # 모델 로드 @@ -148,7 +145,7 @@ def query_select_summarize_stream(results, query, ai, min_similarity: float = 0. generation_kwargs = dict( **model_inputs, streamer=streamer, - max_new_tokens=3000, + max_new_tokens=600, do_sample=True, temperature=0.3, top_p=0.9, @@ -213,7 +210,7 @@ def query_summarize_simple(query: str) : # conduct text completion generated_ids = model.generate( **model_inputs, - max_new_tokens=300, + max_new_tokens=600, do_sample=True, # ✅ 샘플링 활성화 temperature=0.3, top_p=0.9, diff --git a/config/ai/org_transformer.py b/config/ai/org_transformer.py index 9aba77f..5d54d13 100644 --- a/config/ai/org_transformer.py +++ b/config/ai/org_transformer.py @@ -2,26 +2,28 @@ import json from typing import List, Union import chromadb -from chromadb.utils import embedding_functions +from sentence_transformers import SentenceTransformer # ✅ 임베딩 전용 라이브러리 # === 경로 설정 (모두 로컬) === EMBEDDING_MODEL_PATH = "./models/ko-sroberta-multitask" +# 1. 임베딩 모델 로드 (앱 시작 시 1회만 실행 권장) +# NOTE: 실제 프로덕션에서는 전역 변수나 싱글톤으로 관리 +embedding_model = SentenceTransformer( + EMBEDDING_MODEL_PATH, + device="cpu" +) +# 속도 최적화: 조직도 데이터는 짧으므로 max_seq_length 줄임 +embedding_model.max_seq_length = 128 + # 2. 벡터 DB 설정 persist_directory = "./chroma_db" chroma_client = chromadb.PersistentClient(path=persist_directory) -# 임베딩 함수 설정 -embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction( - model_name=EMBEDDING_MODEL_PATH, # 로컬 폴더 경로 가능 - device="cpu", - normalize_embeddings=True -) - -# 컬렉션 생성 또는 가져오기 +# 컬렉션 생성 (임베딩 함수는 None으로 설정 - 수동 전달할 예정) collection = chroma_client.get_or_create_collection( name="orgchart", - embedding_function=embedding_fn, + embedding_function=None, # ✅ 수동 임베딩 전달 시 None metadata={"hnsw:space": "cosine"} ) @@ -36,21 +38,24 @@ def init(sessionId: str, data: List[Union[str, dict]]): """ print(f'{sessionId} init start') + if not data: + print(f'{sessionId} init skipped: no data') + return + # 문서 ID 생성 doc_ids = [f"{sessionId}_{i}" for i in range(len(data))] - # 데이터 처리: 문자열이면 그대로, 객체면 JSON 문자열로 변환 - documents = [] - + # 데이터 처리: 간결하고 검색 친화적인 텍스트 생성 doc_list = [] + metadatas = [] + for q in data: - # 각 필드를 안전하게 추출 (None 방어) + # 필드 안전 추출 name = q.get('name') or "" dept = q.get('deptNm') or "" grade = q.get('gradeNm') or "" position = q.get('ptsnNm') or "" office_phone = q.get('ofcePhn') or "" - mobile_phone = q.get('mblPhn') or "" chief_name = q.get('chiefNm') or "" state_code = q.get('state') or "" @@ -58,30 +63,48 @@ def init(sessionId: str, data: List[Union[str, dict]]): state_map = {'C': '재직', 'T': '퇴사', 'H': '휴직'} status = state_map.get(state_code, "정보없음") - # [핵심] 검색 엔진이 좋아할만한 서술형 문장 생성 - # 부서명과 이름을 앞부분에 배치하여 가중치 유도 - if name == '': + # ✅ 개선: 짧고 핵심적인 문장 (토큰 수 ↓ → 속도 ↑, 정확도 ↑) + if name: + # 이름 있는 경우: "이름 직급 직위, 부서 소속, 상태" + doc = f"[이름]:{name} [직급]:{grade} [직위]:{position} [부서]:{dept} [상태]: {status}" + if office_phone: + doc += f", 사내번호:{office_phone}" + else : + # 부서장 정보만 있는 경우 doc = ( - f"부서: {dept}. " - f"해당 {dept}의 부서장은 {chief_name}입니다." - ) - else: - doc = ( - f" [이름]:{name}" - f" [부서]:{dept}" - f" [소속]:{dept}" - f" [직급]:{grade}" - f" [직위]:{position}" - f" 현재 {status} 중입니다. " - f" 사내 전화번호(사선)는 {office_phone}입니다." + f"부서: {dept}. 해당 {dept}의 [부서장][팀장]은 {chief_name}입니다." ) + doc_list.append(doc) - # 벡터 DB에 추가 + # 메타데이터는 원본 값 유지 (검색 필터링용) + metadatas.append({ + "sessionId": sessionId, + "deptCd": q.get('deptCd') or "", + "gradeCd": q.get('gradeCd') or "", + "name": name, + "deptNm": dept, + "state": state_code + }) + + # ✅ 임베딩 직접 계산 (Chroma 내부 중복 계산 방지) + try: + embeddings = embedding_model.encode( + doc_list, + normalize_embeddings=True, # cosine 유사도 사용 시 필수 + show_progress_bar=False, + batch_size=32 # 메모리 절약 + ).tolist() + except Exception as e: + print(f"임베딩 생성 실패: {e}") + raise + + # ✅ 벡터 DB에 추가 (임베딩 직접 전달) collection.add( documents=doc_list, ids=doc_ids, - metadatas=[{"sessionId": sessionId, 'deptCd': d.get('deptCd') or "", 'gradeCd': d.get('gradeCd') or ""} for d in data] + embeddings=embeddings, # ← 핵심: 수동 전달 + metadatas=metadatas ) - print(f'{sessionId} init end') + print(f'{sessionId} init end (총 {len(data)}건)') \ No newline at end of file diff --git a/test.py b/test.py index 9195d05..e3a854c 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,5 @@ # 테스트용 스크립트 -d='[이름]:이준원[부서]:토건부[소속]:토건부[직급]:2직급[직위]:현재 재직 중입니다.' +d='[이름]:박현우 [직급]:3직급 [직위]: [소속]:저탄장옥내화부 상태: 재직, 사내번호:1589' print(f'{d[d.find('[이름]'): d.find('[', d.find('[이름]')+1)]}')