매뉴얼 등록 분리
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
import chromadb
|
import chromadb
|
||||||
from chromadb.utils import embedding_functions
|
from chromadb.utils import embedding_functions
|
||||||
from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
|
from transformers import AutoTokenizer, AutoModelForCausalLM
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
# # === 경로 설정 (모두 로컬) ===
|
# # === 경로 설정 (모두 로컬) ===
|
||||||
@@ -22,105 +21,126 @@ collection = chroma_client.get_or_create_collection(
|
|||||||
_model = None
|
_model = None
|
||||||
_tokenizer = None
|
_tokenizer = None
|
||||||
|
|
||||||
def get_qwen_model() :
|
def get_qwen_model():
|
||||||
global _model, _tokenizer
|
global _model, _tokenizer
|
||||||
if _model is None:
|
if _model is not None:
|
||||||
model_name = "Qwen/Qwen3-0.6B"
|
return _model, _tokenizer
|
||||||
_tokenizer = AutoTokenizer.from_pretrained(
|
_tokenizer = AutoTokenizer.from_pretrained(
|
||||||
QWEN_MODEL_PATH,
|
QWEN_MODEL_PATH,
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
local_files_only=True # 🔒 오프라인 강제
|
local_files_only=True
|
||||||
)
|
)
|
||||||
_model = AutoModelForCausalLM.from_pretrained(
|
_model = AutoModelForCausalLM.from_pretrained(
|
||||||
QWEN_MODEL_PATH,
|
QWEN_MODEL_PATH,
|
||||||
torch_dtype=torch.float32, # CPU 안정성
|
dtype=torch.bfloat16,
|
||||||
device_map="auto",
|
device_map="auto",
|
||||||
trust_remote_code=True,
|
trust_remote_code=True,
|
||||||
local_files_only=True # 🔒 오프라인 강제
|
local_files_only=True
|
||||||
)
|
)
|
||||||
|
# ✅ torch.compile() 적용 (PyTorch 2.0+)
|
||||||
|
if hasattr(torch, 'compile'):
|
||||||
|
try:
|
||||||
|
print("🚀 torch.compile() 적용 중...")
|
||||||
|
# mode="reduce-overhead": 추론 시 추천
|
||||||
|
# dynamic=False: 고정 입력 크기 시 더 빠름 (보통 RAG는 입력 길이가 유동적이므로 dynamic=True 권장)
|
||||||
|
_model = torch.compile(
|
||||||
|
_model,
|
||||||
|
mode="reduce-overhead", # 또는 "max-autotune" (초기 컴파일 시간 ↑, 이후 속도 ↑↑)
|
||||||
|
dynamic=True # 입력 길이가 매번 다르므로 동적 크기 허용
|
||||||
|
)
|
||||||
|
print("✅ torch.compile() 성공!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ torch.compile() 실패, 원본 모델 사용: {e}")
|
||||||
|
pass # 실패하면 그냥 원본 사용
|
||||||
return _model, _tokenizer
|
return _model, _tokenizer
|
||||||
|
|
||||||
# 3. 질의 처리
|
def query_and_summarize(job: str, query: str, top_k: int = 3, min_similarity: float = 0.2):
|
||||||
def query_and_summarize(job: str, query: str, top_k: int = 3):
|
from datetime import datetime
|
||||||
|
print(f'1 {datetime.now()}')
|
||||||
# 관련 문서 검색
|
# 관련 문서 검색 (top_k보다 여유 있게 가져옴)
|
||||||
results = collection.query(
|
results = collection.query(
|
||||||
query_texts=[query],
|
query_texts=[query],
|
||||||
n_results=5,
|
n_results=top_k + 2, # 여유분 확보
|
||||||
where={"dept": job}
|
where={"dept": job}
|
||||||
)
|
)
|
||||||
# results = collection.query(query_texts=[query], n_results=top_k)
|
print(f'{datetime.now()}')
|
||||||
cosine_similarities = [1 - d for d in results['distances'][0]]
|
if not results['documents'][0]:
|
||||||
print("유사도:", cosine_similarities)
|
return "관련 문서를 찾을 수 없습니다."
|
||||||
# 출력 예: [0.610, 0.473, 0.154, 0.142]
|
|
||||||
|
|
||||||
context_with_score = ""
|
print(f'2 {datetime.now()}')
|
||||||
for i, (doc, dist) in enumerate(zip(results['documents'][0], results['distances'][0])):
|
# 유사도 계산 및 필터링
|
||||||
sim = 1 - dist
|
filtered_docs = []
|
||||||
context_with_score += f"[문서 {i+1} | 유사도: {sim:.3f}]\n{doc}\n\n"
|
for doc, dist in zip(results['documents'][0], results['distances'][0]):
|
||||||
|
similarity = 1 - dist
|
||||||
|
if similarity >= min_similarity:
|
||||||
|
filtered_docs.append((doc, similarity))
|
||||||
|
if len(filtered_docs) >= top_k:
|
||||||
|
break
|
||||||
|
print(f'3 {datetime.now()}')
|
||||||
|
if not filtered_docs:
|
||||||
|
return "유사도 기준을 만족하는 문서가 없습니다."
|
||||||
|
|
||||||
print(context_with_score)
|
# 컨텍스트 생성 (유사도 내림차순 정렬은 이미 Chroma가 보장)
|
||||||
print("\n\n\n\n\n")
|
context_parts = []
|
||||||
top_doc = results['documents'][0][0]
|
for i, (doc, sim) in enumerate(filtered_docs):
|
||||||
|
context_parts.append(f"[청크 {i+1} | 유사도: {sim:.3f}]\n{doc}")
|
||||||
# ✅ 명시적으로 모델과 토크나이저 로드
|
context = "\n\n".join(context_parts)
|
||||||
|
print(f'4 {datetime.now()}')
|
||||||
|
# 모델 로드
|
||||||
model, tokenizer = get_qwen_model()
|
model, tokenizer = get_qwen_model()
|
||||||
|
print(f'5 {datetime.now()}')
|
||||||
|
# 프롬프트 구성
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": "당신은 회사 재무/회계 업무 전문 어시스턴트입니다. 문서 내용은 그대로 사용자에게 보여 줘야 하며 이를 기반으로 부가설명을 정확하고 상세하게 답변하세요."},
|
{
|
||||||
{"role": "user", "content": f"다음 문서를 참고하세요:\n{top_doc}\n\n질문: {query}"}
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"당신은 회사 재무/회계 업무 전문 어시스턴트입니다. "
|
||||||
|
"사용자에게 제공된 여러 청크를 종합하여, 정확하고 상세하게 답변하세요. "
|
||||||
|
"필요시 문서 내용을 직접 인용하거나 요약해도 됩니다. "
|
||||||
|
"추측하지 말고, 문서에 근거한 정보만 사용하세요."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": f"다음 문서들을 참고하세요:\n\n{context}\n\n질문: {query}"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
print(f'6 {datetime.now()}')
|
||||||
|
# 토큰화 및 생성
|
||||||
text = tokenizer.apply_chat_template(
|
text = tokenizer.apply_chat_template(
|
||||||
messages,
|
messages,
|
||||||
tokenize=False,
|
tokenize=False,
|
||||||
add_generation_prompt=True,
|
add_generation_prompt=True,
|
||||||
enable_thinking=True # Switches between thinking and non-thinking modes. Default is True.
|
enable_thinking=False # 작은 모델에선 비활성화 권장
|
||||||
)
|
)
|
||||||
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
||||||
|
print(f'7 {datetime.now()}')
|
||||||
# conduct text completion
|
|
||||||
generated_ids = model.generate(
|
generated_ids = model.generate(
|
||||||
**model_inputs,
|
**model_inputs,
|
||||||
max_new_tokens=500
|
max_new_tokens=150,
|
||||||
|
do_sample=True, # ✅ 샘플링 활성화
|
||||||
|
temperature=0.3,
|
||||||
|
top_p=0.9,
|
||||||
|
pad_token_id=tokenizer.eos_token_id
|
||||||
)
|
)
|
||||||
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
|
print(f'8 {datetime.now()}')
|
||||||
|
input_len = model_inputs.input_ids.shape[1]
|
||||||
# parsing thinking content
|
output_ids = generated_ids[0][input_len:]
|
||||||
try:
|
response = tokenizer.decode(output_ids, skip_special_tokens=True).strip()
|
||||||
# rindex finding 151668 (</think>)
|
print(f'9 {datetime.now()}')
|
||||||
index = len(output_ids) - output_ids[::-1].index(151668)
|
return response
|
||||||
except ValueError:
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
|
|
||||||
end_think_id = tokenizer.convert_tokens_to_ids("</think>")
|
|
||||||
if end_think_id in output_ids:
|
|
||||||
idx = len(output_ids) - output_ids[::-1].index(end_think_id)
|
|
||||||
else:
|
|
||||||
idx = 0
|
|
||||||
content = tokenizer.decode(output_ids[idx:], skip_special_tokens=True).strip()
|
|
||||||
|
|
||||||
print(top_doc)
|
|
||||||
print("\n\n\n\n\n")
|
|
||||||
print("thinking content:", thinking_content)
|
|
||||||
print("\n\n\n\n\n")
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
# FastAPI 앱
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
def question(query: str) :
|
def question(query: str):
|
||||||
print(1)
|
|
||||||
answer = query_and_summarize(job="FI", query=query)
|
answer = query_and_summarize(job="FI", query=query)
|
||||||
return {"answer": answer}
|
return {"answer": answer}
|
||||||
|
|
||||||
# 예시 사용
|
# 개발용 실행 (직접 실행 시)
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# init(job="FI")
|
import uvicorn
|
||||||
# FI : 재무 HR : 인사
|
print("서버 시작: uvicorn manual:app --reload")
|
||||||
print(1)
|
# 예시 질의 (주석 해제 시 직접 테스트 가능)
|
||||||
# user_query = "외화 송금 방법?"
|
# print(query_and_summarize("FI", "외화 송금 절차는 어떻게 되나요?"))
|
||||||
# answer = query_and_summarize(job="FI", query=user_query)
|
|
||||||
# print(answer)
|
|
||||||
# 실행방법 uvicorn manual:app --reload
|
|
||||||
145
manual_offline_think.py
Normal file
145
manual_offline_think.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import os
|
||||||
|
import torch
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
import chromadb
|
||||||
|
from chromadb.utils import embedding_functions
|
||||||
|
from transformers import AutoTokenizer, AutoModelForCausalLM
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
# # === 경로 설정 (모두 로컬) ===
|
||||||
|
QWEN_MODEL_PATH = "./models/Qwen3-0.6B"
|
||||||
|
EMBEDDING_MODEL_PATH = "./models/all-MiniLM-L6-v2"
|
||||||
|
|
||||||
|
# 2. 벡터 DB 설정
|
||||||
|
persist_directory = "./chroma_db"
|
||||||
|
chroma_client = chromadb.PersistentClient(path=persist_directory)
|
||||||
|
|
||||||
|
collection = chroma_client.get_or_create_collection(
|
||||||
|
name="manuals",
|
||||||
|
)
|
||||||
|
|
||||||
|
_model = None
|
||||||
|
_tokenizer = None
|
||||||
|
|
||||||
|
def get_qwen_model() :
|
||||||
|
global _model, _tokenizer
|
||||||
|
if _model is not None:
|
||||||
|
return _model, _tokenizer
|
||||||
|
|
||||||
|
# 토크나이저 로드
|
||||||
|
_tokenizer = AutoTokenizer.from_pretrained(
|
||||||
|
QWEN_MODEL_PATH,
|
||||||
|
trust_remote_code=True,
|
||||||
|
local_files_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 모델 로드
|
||||||
|
_model = AutoModelForCausalLM.from_pretrained(
|
||||||
|
QWEN_MODEL_PATH,
|
||||||
|
dtype=torch.bfloat16, # 또는 torch.bfloat16 (CPU), torch.float16 (GPU)
|
||||||
|
device_map="auto",
|
||||||
|
trust_remote_code=True,
|
||||||
|
local_files_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ torch.compile() 적용 (PyTorch 2.0+)
|
||||||
|
if hasattr(torch, 'compile'):
|
||||||
|
try:
|
||||||
|
print("🚀 torch.compile() 적용 중...")
|
||||||
|
# mode="reduce-overhead": 추론 시 추천
|
||||||
|
# dynamic=False: 고정 입력 크기 시 더 빠름 (보통 RAG는 입력 길이가 유동적이므로 dynamic=True 권장)
|
||||||
|
_model = torch.compile(
|
||||||
|
_model,
|
||||||
|
mode="reduce-overhead", # 또는 "max-autotune" (초기 컴파일 시간 ↑, 이후 속도 ↑↑)
|
||||||
|
dynamic=True # 입력 길이가 매번 다르므로 동적 크기 허용
|
||||||
|
)
|
||||||
|
print("✅ torch.compile() 성공!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ torch.compile() 실패, 원본 모델 사용: {e}")
|
||||||
|
pass # 실패하면 그냥 원본 사용
|
||||||
|
return _model, _tokenizer
|
||||||
|
|
||||||
|
# 3. 질의 처리
|
||||||
|
def query_and_summarize(job: str, query: str, top_k: int = 3):
|
||||||
|
|
||||||
|
# 관련 문서 검색
|
||||||
|
results = collection.query(
|
||||||
|
query_texts=[query],
|
||||||
|
n_results=5,
|
||||||
|
where={"dept": job}
|
||||||
|
)
|
||||||
|
# results = collection.query(query_texts=[query], n_results=top_k)
|
||||||
|
cosine_similarities = [1 - d for d in results['distances'][0]]
|
||||||
|
print("유사도:", cosine_similarities)
|
||||||
|
# 출력 예: [0.610, 0.473, 0.154, 0.142]
|
||||||
|
|
||||||
|
context_with_score = ""
|
||||||
|
for i, (doc, dist) in enumerate(zip(results['documents'][0], results['distances'][0])):
|
||||||
|
sim = 1 - dist
|
||||||
|
context_with_score += f"[문서 {i+1} | 유사도: {sim:.3f}]\n{doc}\n\n"
|
||||||
|
|
||||||
|
print(context_with_score)
|
||||||
|
print("\n\n\n\n\n")
|
||||||
|
top_doc = results['documents'][0][0]
|
||||||
|
|
||||||
|
# ✅ 명시적으로 모델과 토크나이저 로드
|
||||||
|
model, tokenizer = get_qwen_model()
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "당신은 회사 재무/회계 업무 전문 어시스턴트입니다. 문서 내용은 그대로 사용자에게 보여 줘야 하며 이를 기반으로 부가설명을 정확하고 상세하게 답변하세요."},
|
||||||
|
{"role": "user", "content": f"다음 문서를 참고하세요:\n{top_doc}\n\n질문: {query}"}
|
||||||
|
]
|
||||||
|
|
||||||
|
text = tokenizer.apply_chat_template(
|
||||||
|
messages,
|
||||||
|
tokenize=False,
|
||||||
|
add_generation_prompt=True,
|
||||||
|
enable_thinking=True # Switches between thinking and non-thinking modes. Default is True.
|
||||||
|
)
|
||||||
|
model_inputs = tokenizer([text], return_tensors="pt").to(model.device)
|
||||||
|
|
||||||
|
# conduct text completion
|
||||||
|
generated_ids = model.generate(
|
||||||
|
**model_inputs,
|
||||||
|
max_new_tokens=300,
|
||||||
|
do_sample=True, # ✅ 샘플링 활성화
|
||||||
|
temperature=0.3,
|
||||||
|
top_p=0.9,
|
||||||
|
pad_token_id=tokenizer.eos_token_id
|
||||||
|
)
|
||||||
|
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):].tolist()
|
||||||
|
|
||||||
|
# parsing thinking content
|
||||||
|
try:
|
||||||
|
# rindex finding 151668 (</think>)
|
||||||
|
index = len(output_ids) - output_ids[::-1].index(151668)
|
||||||
|
except ValueError:
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
thinking_content = tokenizer.decode(output_ids[:index], skip_special_tokens=True).strip("\n")
|
||||||
|
end_think_id = tokenizer.convert_tokens_to_ids("</think>")
|
||||||
|
if end_think_id in output_ids:
|
||||||
|
idx = len(output_ids) - output_ids[::-1].index(end_think_id)
|
||||||
|
else:
|
||||||
|
idx = 0
|
||||||
|
content = tokenizer.decode(output_ids[idx:], skip_special_tokens=True).strip()
|
||||||
|
|
||||||
|
print(top_doc)
|
||||||
|
print("\n\n\n\n\n")
|
||||||
|
print("thinking content:", thinking_content)
|
||||||
|
print("\n\n\n\n\n")
|
||||||
|
return content
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def question(query: str):
|
||||||
|
answer = query_and_summarize(job="FI", query=query)
|
||||||
|
return {"answer": answer}
|
||||||
|
|
||||||
|
# 개발용 실행 (직접 실행 시)
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
print("서버 시작: uvicorn manual:app --reload")
|
||||||
|
# 예시 질의 (주석 해제 시 직접 테스트 가능)
|
||||||
|
# print(query_and_summarize("FI", "외화 송금 절차는 어떻게 되나요?"))
|
||||||
Reference in New Issue
Block a user