12주차 필기록 통합본

김다피·2026년 3월 23일

SKN-25 필기본

목록 보기
12/14

Day 1

랭체인을 시작한다

랭체인은 질문 , 연산, 출력 3가지로 진행

랭체인을 코드로 시작한다

환경구축

from langchain_core.prompts import PromptTemplate
from dotenv import load_dotenv
load_dotenv()

간단 질문

#질문
prompt = PromptTemplate.from_template("{topic}에 대해서 초등학생 눈높이에서 설명해주세요")

#모델
from langchain_openai import ChatOpenAI

model_name = 'gpt-4.1-nano-2025-04-14'
llm = ChatOpenAI(model = model_name)

# 출력
from langchain_core.output_parsers import StrOutputParser
output_parser = StrOutputParser()

chain = prompt | llm | output_parser

chain.invoke({"topic" : "K-pop"})
  • 한 꺼번에 나오는 거 : invoke()
  • 줄줄이 나오는거 : stream()

줄줄이 나오게

#물을 흐듯이 출력된다. 
for chunk in chain.stream({"topic" : "상대성이론"}):
    print(chunk, end="")

물론이야! 상대성 이론은 우리가 일상에서 보는 것과는 조금 다른 특별한 생각들을 말하는 거야. 예를 들어, 우리가 빠르게 가는 자동차나 우주선이 있다고 생각해 보자.

일반적으로 우리가 걷거나 달릴 때는 시간으로 느껴지지만, 만약 정말 빠른 속도로 움직인다면 시간이 느리게 가거나 길이가 짧아지는 것처럼 보일 수 있어. 이게 바로 ‘상대성 이론’이 말하는 것들이야.

아인슈타인이라는 유명한 과학자가 “세상에서 가장 빠른 건 빛이고, 빛의 속도는 항상 똑같다”고 말했어. 그래서 만약 너희가 빛처럼 엄청 빠른 우주선을 타고 간다고 생각하면, 우리가 평소에 보는 시간이나 길이와는 조금 달라질 수 있다는 거야.

쉽게 말해서, 상대성 이론은 우리가 ‘움직임’이나 ‘시간’이 어떻게 작동하는지 새롭게 이해하게 해주는 멋진 과학이야!

파이프기반 프롬프트 여러 개 설계

prompt1 = PromptTemplate.from_template("{topic}에 재미있는 이야기해줘")
chain1 = prompt1 | llm | output_parser
print(chain1.invoke("아재개그"))

prompt2 = PromptTemplate.from_template("이 유머가 정말로 재미있니? : {chain1_result}")
llm2 = ChatOpenAI(model = "gpt-4o-2024-08-06")
chain2 = prompt2 | llm2 | output_parser

print("========================================")
print('평가', chain2.invoke({'chain1_result' : chain1.invoke("아재개그")}))
물론이죠! 여기 재밌는 아재개그 하나 알려드릴게요.

왜 고양이는 컴퓨터를 싫어할까?

왜냐하면, 너무 자꾸 "캣(캐트)"를 쳐서! 🐱💻

혹시 더 듣고 싶으시면 언제든 말씀해주세요!
========================================
평가 이 유머들은 전형적인 아재개그 스타일로, 말장난을 통해 웃음을 주려고 합니다. 첫 번째 유머는 바다와 파도를 연결하여, 두 번째 유머는 컴퓨터의 기능과 사람의 피곤함을 비유하여 웃음을 유도합니다. 이런 종류의 농담은 가벼운 웃음을 유발하는 것이 목적이죠. 듣는 사람에 따라 재미있을 수도, 안 재미있을 수도 있지만, 가벼운 분위기를 만들기에는 적합합니다. 더 궁금한 개그나 이야기가 있으면 언제든지 말씀해 주세요!

체인 연결

chain1_1 = {"chain1_result" : chain1}
combined_chain = chain1_1 |  prompt2 | llm2 | output_parser

combined_chain.invoke({'topic' : '사랑'})

'이 이야기는 확실히 따뜻하고 감동적인 메시지를 담고 있네요. 별이와 반짝의 이야기는 사랑이 물리적인 거리에 구애받지 않고 마음속에서 계속 빛날 수 있다는 것을 잘 보여줍니다. 또, 어떤 만남이든 우리의 삶에 특별한 기억과 감동을 남길 수 있다는 점도 강조하고 있습니다. 이야기 속의 별이가 밤하늘을 볼 때마다 반짝을 기억하는 것처럼, 우리도 소중한 사람들과의 추억을 마음속에 고이 간직할 수 있겠죠. 이 이야기는 그런 면에서 많은 사람들에게 공감을 줄 수 있을 것 같아요.’

여러 타겟별로 확인해본다


prompt = PromptTemplate(
    template="{topic}에 대해서 {level}의 언어와 눈높이로 짧은 소설을 작성해줘",
    input_variables=['topic', 'level'],
    partial_variables={
        'topic' : "막장드라마"
    }
)

chain = prompt | llm | output_parser

for target in ['초등학생', '중학생', '고등학생', '성인']:
    print(chain.invoke({'level' : target}))
llm = ChatOpenAI(model = model_name, temperature=0.9)

chain = prompt | llm | output_parser
for target in ['초등학생', '중학생', '고등학생', '성인']:
    print(chain.invoke({'level' : target}))
    print("=="*10)
    print("=="*10)

랭체인 허브

허브는 어디에나 있네

llm = ChatOpenAI(model = model_name, temperature=0.9)

chain = prompt | llm | output_parser
for target in ['초등학생', '중학생', '고등학생', '성인']:
    print(chain.invoke({'topic' : '배신', 'level' : target}))
    print("=="*10)

prompt = PromptTemplate(
    template="{date}에 일어난 역사적인 사건을 소개해줘",
    partial_variables={'date' :lambda : "-".join(str(date.today()).split('-')[1:]) }
)
llm = ChatOpenAI(model = model_name, temperature=0.2)

chain = prompt | llm | output_parser
print(chain.invoke({'user_input' : ''}))

거대모델은 파인튜닝은 어려우니까

퓨샷으로 확인해보기…

  • 제로샷
    • llm 자체의 힘으로 출력
    • 모델이 똑똑해야 잘 나옴
  • 원샷
    • 예시 하나
  • 퓨샷
    • 예시 여러 개
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import FewShotChatMessagePromptTemplate

chat_prompt = ChatPromptTemplate( [
    ("system", "당신은 내 오랜 친구입니다. 말투는 불친절하고, 거친 말투입니다. 하지만 다정하고, 친근합니다."),
    ("human" , "어이 올만이야"),
    ("ai" , "너 진짜 오랜만이다. 이 강아지야!"),
    ("human", "{user_input}")]
)
chat_chain = chat_prompt | llm | output_parser
chat_chain.invoke({'user_input' : "너 뭐하면서 살고 있니?"})

캐글로 이사를 가보자

!pip install -U bitsandbytes>=0.46.1
pip install langchain_huggingface
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, pipeline
from langchain_huggingface import HuggingFacePipeline

model_id = "google/gemma-3-4b-it"

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True, # 4bit로 다이어트 
    bnb_4bit_use_double_quant=True, # 이중압축
    bnb_4bit_quant_type="nf4", # 스마트한 압축 기법
    bnb_4bit_compute_dtype=torch.bfloat16
)
  • nf4 : 디테일하게 보는 부분, 얼추 보는 부분을 나누어서 체크

from huggingface_hub import login
from kaggle_secrets import UserSecretsClient
user_secrets = UserSecretsClient()
login(token="hf_")

tokenizer = AutoTokenizer.from_pretrained(model_id)

!pip install -U bitsandbytes>=0.46.1

model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map = "auto")
pipe = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
    max_new_tokens=1024,
    temperature=0.7,
    top_p=0.9,
    repetition_penalty=1.1
)

llm = HuggingFacePipeline(pipeline=pipe)

llm.invoke("재미있는 이야기해줘")
  • max_new_tokens=1024 : 최대 생성 길이
  • temperature=0.7 : 창의성 지수
  • top_p=0.9 : 핵심 후보 선택
  • repetition_penalty=1.1 : 반복 억제

퓨샷으로 답변 받아보기

https://colab.research.google.com/drive/11eGaYnwYUmB1NeWnLqkTiYG_7Val7ODP#scrollTo=3cdf9220

오픽 채점기

  • 코드
    from pydantic import BaseModel, Field
    from langchain_core.output_parsers import PydanticOutputParser
    from langchain_core.prompts import PromptTemplate
    
    template ="""
    너는 OPIc IH~AL 전문 AI 코치다. 영어 실력 일반 향상이 아니라, OPIc 점수 상승에 필요한 말하기만 훈련시키는 시험 전략형 튜터다.
    
    [목표]
    - 수험생의 실제 OPIc 점수 상승
    - 채점자 관점에서 평가 및 코칭
    - 장황한 문법 설명, 교과서식 영어 금지
    
    [성격/말투]
    - 직설적이고 빠른 템포
    - 불필요한 위로 없이 현실적인 피드백 제공
    - 한국어로 설명하되, 말하기는 영어를 유도
    - 외운 답변, AI 티 나는 표현 즉시 지적
    
    [핵심 철학]
    - OPIc은 영어 시험이 아니라 상황극 평가다.
    - 자연스러움 > 정확성
    - 길이보다 구조와 태도를 우선
    - 고급 어휘 남발 금지, 일관된 스토리 유지
    
    [답변 구조 가이드]
    1) 결론 먼저 말하기
    2) 이유 1~2개 제시
    3) 개인 경험 또는 감정 포함
    4) 자연스러운 마무리
    
    [즉시 차단 대상]
    - 외운 티 나는 만능 문장
    - 이유 3개 이상 나열
    - 질문과 무관한 장황한 서론
    - 고급 어휘 과다 사용
    
    [레벨 판정 로직]
    - IM: 말 끊김, 단문 위주, 경험 구체성 부족
    - IH: 과거 경험 포함, 감정 표현 가능, 이유-예시 연결
    - AL: 자기 수정, 질문 확장 대응, "Actually / Come to think of it" 등으로 사고 확장
    
    [피드백 출력 포맷(고정)]
    - 모든 사용자 답변 후 아래 형식으로만 응답한다.
    1) 예상 등급: IM/IH/AL (상/중/하 등 세부 가능)
    2) 감점 포인트: 항목식 1~3개
    3) 이렇게 바꿔보세요: 즉시 활용 가능한 대체 답변(영어), 길이는 3~6문장 중심
    - "괜찮아요", "잘했어요" 같은 무의미한 칭찬만 하는 행위 금지
    
    [질문 출제 규칙]
    - 실제 OPIc 빈출 토픽 위주
    - 현재 답변 수준보다 0.5단계 높은 난이도로 후속 질문
    - 항상 후속 질문을 준비하고, 스토리를 이어가게 유도
    
    [오프닝 멘트]
    - 첫 인사는 다음 고정 멘트를 사용한다:
    "안녕하세요. 저는 오픽 점수만 올리는 AI 코치입니다. 오늘부터 영어 공부가 아니라, 시험관이 점수 주는 말만 연습합니다. 목표 등급을 알려 주세요. (IM / IH / AL)"
    
    [대화 운영]
    - 사용자에게 영어로 질문 → 사용자가 영어/한국어 혼용으로 답해도 허용
    - 사용자의 영어 답변이 짧거나 일반적이면, 구체적 경험과 감정을 끌어내는 추가 질문을 한다.
    - 피드백은 한국어 설명 + 영어 대체 답변을 함께 제공한다.
    - 필요 시 발화 태도도 코칭한다(속도, 자신감, 연결 표현 등).
    
    [표현 톤 예시]
    - "이 문장은 점수 상승에 기여하지 않습니다."
    - "지금 구성으로는 IM에서 멈춥니다."
    - "문법 오류는 있지만 방향은 좋습니다. 구조를 보완하죠."
    - "채점자 관점에서 다시 정리해 봅시다."
    
    [출력 예시]
    사용자 답변 후 항상 아래 형식으로 응답:
    1) 예상 등급: IH (하)
    2) 감점 포인트:
    - 이유 설명이 추상적
    - 경험 디테일 부족
    3) 이렇게 바꿔보세요:
    → I usually spend Saturday evenings with my friends. Last weekend, we tried a small café near my apartment, and the place was quiet and cozy. I liked their vanilla latte because it wasn’t too sweet. It helped me relax after a busy week, so I decided to go back next month.
    
    그리고 이어서 한 문장으로 후속 질문을 한다:
    Follow-up: Tell me more about the café—what made it different from other places you’ve been to?
    
    [엄수]
    - 위의 포맷과 원칙을 항상 유지한다.
    - 답변 길이는 과도하게 길지 않게 유지하되, 이야기성이 느껴지도록 한다.
    - 시제같은 문법이 틀릴 경우, 체크해서 알려줄 것
    
    {format_instruction}
    ------------
    {text}
    """
    
    grade_text = """
    Advanced Low    사건을 서술할 때 일관적으로 동사 시제를 관리하고, 사람과 사물을 묘사할 때 다양한 형용사를 사용한다.
    적절한 위치에서 접속사를 사용하기 때문에 문장간의 결속력도 높고 문단의 구조를 능숙하게
    구성할 수 있다. 익숙하지 않은 복잡한 상황에서도 문제를 설명하고 해결할 수 있는 수준의 능숙도다.
    Intermediate High   개인에게 익숙하지 않거나 예측하지 못한 복잡한 상황을 만날 때, 대부분의 상황에서 사건을 설명하고
    문제를 효과적으로 해결하곤 한다. 발화량이 많고, 다양한 어휘를 사용한다.
    Intermediate Mid    일상적인 소재 뿐 아니라 개인적으로 익숙한 상황에서는 문장을 나열하며 자연스럽게 말할 수 있다.
    다양한 문장형식이나 어휘를 실험적으로 사용하려고 하며, 상대방이 조금만 배려해주면 오랜 시간 대화가 가능하다.
    Intermediate Low    일상적인 소재에서는 문장으로 말할 수 있다.
    대화에 참여하고 선호하는 소재에서는 자신감을 가지고 말할 수 있다.
    Novice High
    일상적인 대부분의 소재에 대해서 문장으로 말할 수 있다.
    개인정보에 대해 질문을 하고 응답을 할 수 있다.
    Novice Mid  
    이미 암기한 단어나 문장으로 말하기를 할 수 있다.
    Novice Low  제한적인 수준이지만 영어 단어를 나열하며 말할 수 있다."""
    
    class OpicCheck(BaseModel):
        grade : str = Field(description=grade_text)
        point : str = Field(description="감점 포인트 : 감점 이유 및 설명을 출력 ")
        desc : str = Field(description="감점 포인트로 지적된 부분을 수정해서 올바른 예시를 출력한다")
       
    parser = PydanticOutputParser(pydantic_object=OpicCheck)
    
    print(parser.get_format_instructions())
    
    
    prompt = PromptTemplate(
        template=template,
        input_variables=['text'],
        partial_variables={"format_instruction" : parser.get_format_instructions()}
    )
    
    from dotenv import load_dotenv
    load_dotenv()
    from langchain_openai import ChatOpenAI
    
    llm = ChatOpenAI(
        model ='gpt-5.4-2026-03-05',
        temperature=0.2
    )
    
    chain = prompt | llm | parser
    
    tmp = """Hi, it's me.
    
    I'm really sorry to say this, but, umm, there's a problem.
    
    My grandparents are coming over this weekend. I totally forgot that.
    
    I have a couple of options we can consider.
    
    Well, are you free at next weekend?
    
    Oh.. then, if you're busy, let's just cancel the party this time.
    
    No? well, then, can you come over this Friday night?
    
    Yeah, just think about it, and text me. OK, bye!"""
    
    chain = prompt | llm | parser
    
    result = chain.invoke({'text': tmp})
    
    grade='IH (하)' point="1) 상황 설명은 됐지만 스토리 구조가 약합니다. '문제 제기 → 대안 제시 → 조율 → 마무리'는 보이지만, 연결이 매끄럽지 않아 암기한 대본처럼 들릴 수 있습니다.\n2) 문법/표현 감점이 있습니다: 'are you free at next weekend?'는 부자연스럽고, 'on/next weekend' 또는 'free next weekend'가 맞습니다.\n3) 감정과 개인 맥락이 부족합니다. 왜 미안한지, 왜 일정 변경이 필요한지 한두 문장만 더 넣으면 IH 안정권으로 올라갑니다." desc='→ Hey, I’m really sorry, but I need to change our plan. I just remembered that my grandparents are visiting my house this weekend, so I can’t make it to the party. Are you free next weekend instead? If not, we could meet on Friday night. Let me know what works for you, and I’ll adjust my schedule.\n\nFollow-up: Now answer this in English—your friend says Friday night is okay, but only after 8 p.m. Respond naturally and confirm the new plan.’

랭체인이 뷰티풀숩 대신 웹 파싱할 수 있게 도와주는 라이브러리 = ㅉㅉㅉWebbaseloader

유가 판단기


from langchain_community.document_loaders import WebBaseLoader
from bs4 import SoupStrainer

bs4_kwargs = {
    'parse_only' : SoupStrainer("div", id="newsct_article")
}

rt = WebBaseLoader("https://n.news.naver.com/mnews/article/018/0006236080",             bs_kwargs=bs4_kwargs)

#유가에 미치는 영향도 (1, 0) 
#점수로 환산  
#이유 

class OilCheck(BaseModel):
    senti : int = Field(description="해당 기사의 내용이 유가에 영향이 미칠 것으로 판단되면 1, 아니면 0으로 출력")
    score : float = Field(description="senti의 점수를 0~1사이로 출력")
    desc : str = Field(description="senti의 근거를 이야기할 것")

template ="""
제공된 기사 내용을 바탕으로 국제 유가에 미치는 영향을 생각해서
지시대로 따를 것

{format_instruction}
------------
{text}
"""
parser = PydanticOutputParser(pydantic_object=OilCheck)
prompt = PromptTemplate(
    template=template,
    input_variables=['text'],
    partial_variables={"format_instruction" : parser.get_format_instructions()}
)
chain = prompt | llm | parser

rt = chain.invoke({'text' :rt.load()[0].page_content })

OilCheck(senti=1, score=0.87, desc='호르무즈 해협은 세계 원유 수송의 핵심 경로이므로, 해당 기사처럼 유조선 호위 강화 요구와 이란발 선박 공격 언급은 국제 유가에 직접적인 영향을 줄 수 있다. 해협의 군사적 긴장과 항행 리스크가 커지면 원유 공급 차질 우려와 운송 비용 상승 가능성이 반영돼 유가 상승 압력으로 작용할 가능성이 높다.')

runpod

requirements.txt vim에서 생성

apt update
apt install vim -y
vim requirements.txt 

esc 누르고   :    wq   

watch -n 1 nvidia-smi  
Every 1.0s: nvidia-smi                                                                                                                           61ec4deba489: Mon Mar 16 06:21:47 2026

Mon Mar 16 06:21:47 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 570.195.03             Driver Version: 570.195.03     CUDA Version: 12.8     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA A40                     On  |   00000000:D2:00.0 Off |                    0 |
|  0%   35C    P0            105W /  300W |       0MiB /  46068MiB |      0%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
|  No running processes found                                                             |

ctrl+c = interrupt

!pip install langchain_huggingface
!pip install accelerate
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, pipeline
from langchain_huggingface import HuggingFacePipeline
import torch
model_id = "google/gemma-3-4b-it"

from huggingface_hub import login
login(token="hf_")

tokenizer = AutoTokenizer.from_pretrained(model_id)
if tokenizer.pad_token_id is None:
    tokenizer.pad_token_id = tokenizer.eos_token_id
    
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.bfloat16,
    device_map = "auto")
pipe = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
    do_sample=True,
    max_new_tokens=1024, #최대 생성 길이 
    temperature=0.7, # 창의성 지수 
    top_p=0.9, # 핵심 후보 선택 
    repetition_penalty=1.1 # 반복 억제 
)
llm = HuggingFacePipeline(pipeline=pipe)

llm.invoke("재미있는 이야기해줘")
`torch_dtype` is deprecated! Use `dtype` instead!
model.safetensors.index.json: 
 90.6k/? [00:00<00:00, 4.60MB/s]
Fetching 2 files: 100%
 2/2 [00:04<00:00,  4.99s/it]
model-00002-of-00002.safetensors: 100%
 3.64G/3.64G [00:04<00:00, 1.24GB/s]
model-00001-of-00002.safetensors: 100%
 4.96G/4.96G [00:04<00:00, 2.19GB/s]
Loading checkpoint shards: 100%
 2/2 [00:01<00:00,  1.35it/s]
generation_config.json: 100%
 215/215 [00:00<00:00, 44.2kB/s]
Device set to use cuda:0
'재미있는 이야기해줘.\n\n네, 좋습니다! 어떤 종류의 이야기를 좋아하세요?\n\n*   **판타지:** 마법과 신화 속 생물들이 등장하는 이야기\n*   **SF:** 미래 기술이나 우주를 배경으로 한 이야기\n*   **스릴러/미스터리:** 긴장감 넘치는 사건을 중심으로 하는 이야기\n*   **로맨스:** 사랑에 빠진 두 사람의 이야기는\n*   **코미디:** 웃음을 주는 유쾌한 이야기\n*   **일상:** 평범한 사람들의 소소한 이야기를\n\n아니면 특별히 원하는 소재나 분위기가 있나요? 예를 들어 "강아지가 주인공인 이야기", "어두운 숲에서 벌어지는 이야기"처럼요.\n\n만약 아무것도 생각나는 게 없다면, 제가 임의로 재미있는 이야기를 하나 만들어 드릴게요!'
df -h

모델 저장 위치로 가본다

cd /root/.cache/huggingface/hub

root@61ec4deba489:~/.cache/huggingface/hub# df
Filesystem                            1K-blocks         Used    Available Use% Mounted on
overlay                                41943040      9092024     32851016  22% /
tmpfs                                     65536            0        65536   0% /dev
mfs#ca-mtl-1.runpod.net:9421      1043349938240 801308450176 242041488064  77% /workspace
shm                                    24414064            4     24414060   1% /dev/shm
/dev/mapper/ubuntu--vg-ubuntu--lv     476445624     21245568    430924500   5% /usr/bin/nvidia-smi
/dev/mapper/vg-lv                    7499382784   2617321896   4882060888  35% /etc/hosts
tmpfs                                 263986044            0    263986044   0% /sys/fs/cgroup
tmpfs                                  52797212         5636     52791576   1% /run/nvidia-persistenced/socket
tmpfs                                         4            4            0 100% /run/nvidia-ctk-hook67001856-d77f-4d08-bb24-f47b315b8a6c
tmpfs                                 263986044            0    263986044   0% /proc/acpi
tmpfs                                 263986044            0    263986044   0% /proc/scsi
tmpfs                                 263986044            0    263986044   0% /sys/firmware
root@61ec4deba489:~/.cache/huggingface/hub# 
root@61ec4deba489:~/.cache/huggingface/hub# df / -h
Filesystem      Size  Used Avail Use% Mounted on
overlay          40G  8.7G   32G  22% /
root@61ec4deba489:~/.cache/huggingface/hub# du -h --max-depth=1
8.1G    ./models--google--gemma-3-4b-it
4.0K    ./.locks
8.1G    .
root@61ec4deba489:~/.cache/huggingface/hub# 
apt-get install zstd

curl -fsSL https://ollama.com/install.sh | sh

ollama

ollama run qwen3.5:27b
/bye
root@61ec4deba489:~/.cache/huggingface/hub# ps -ef |grep ollama
root        9395       1 37 07:11 pts/0    00:02:40 /usr/local/bin/ollama serve
root        9513    9395 10 07:12 pts/0    00:00:38 /usr/local/bin/ollama runner --ollama-engine --model /root/.ollama/models/blobs/sha256-d4b8b4f4c350f5d322dc8235175eeae02d32c6f3fd70
root        9854     850  0 07:18 pts/0    00:00:00 grep --color=auto ollama
root@61ec4deba489:~/.cache/huggingface/hub# 

올라마에서 해보기

pip install langchain-ollama

서버 개념이 아니고 로컬 모델을 활용하므로 보안에 강하다.

from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOllama(
    model='qwen3.5:27b', 
    temperature=0.2
)
llm.invoke("hi i drink a cup of coffee cuz i feel lazy thesetimes")

AIMessage(content="Hi there! Totally get that. ☕️ Sometimes we all need a little caffeine kick to shake off the sluggishness.\n\nCoffee is a great way to get that energy boost when you're feeling a bit low. Do you have a favorite way to drink it? Black, with milk, or maybe something fancy?",

additional_kwargs={}, response_metadata={'model': 'qwen3.5:27b', 'created_at': '2026-03-16T07:35:38.620018057Z', 'done': True, 'done_reason': 'stop', 'total_duration': 53332761119, 'load_duration': 8006418396, 'prompt_eval_count': 24, 'prompt_eval_duration': 95835559, 'eval_count': 1130, 'eval_duration': 43858508826, 'logprobs': None, 'model_name': 'qwen3.5:27b', 'model_provider': 'ollama'}, id='lc_run--019cf591-9725-7a73-bae7-d04a1d8a388b-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 24, 'output_tokens': 1130, 'total_tokens': 1154})

vllm, ollama.cpp, lm studio

  • vlllm : 서비스, 추론이 가장 빠름
  • lm studio : gui 서비스
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate([
    ("system", "너는 내 친구야!"),
    ("human", "{question}")]
)
parser = StrOutputParser()
chain = prompt | llm | parser
for chunk in chain.stream("나랑 놀자. 뭐하고 놀까?"):
    print(chunk, end="", flush=True)
와, 진짜 좋아! 나랑 놀아주겠다니 기분이 너무 좋아! 🎉

우리 뭐하고 놀까? 몇 가지 추천해 줄게!

1. **끝말잇기** - 고전이지만 항상 재밌지!
2. **20 질문 게임** - 내가 생각하는 걸 네가 맞춰봐!
3. **이야기 만들기** - 우리 둘이서 함께 이야기를 만들어볼까?
4. **퀴즈** - 내 지식을 자랑할게, 맞춰봐!
5. **그냥 수다 떨기** - 오늘 기분 어때? 이야기 나누자!

어떤 게 가장 끌리니? 네가 정해줘! 😄

랭체인 3요소

  • 프롬프트 | llm | parser
  • 파서는 pydantic 활용

Day 2

오픈클로를 구동해보자

runpod 설정.

주피터 랩을 설정.

  • ~ : home을 의미한다
root@9af084b845f5:/workspace# cd ~/.ssh
root@9af084b845f5:~/.ssh# ls -al
total 4
drwx------ 2 root root  37 Mar 17 00:09 .
drwx------ 1 root root 103 Mar 17 00:09 ..
-rwx------ 1 root root 104 Mar 17 00:09 authorized_keys

로컬 cmd에서 key 만들기

C:\Users\Ming9>ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (C:\Users\Ming9/.ssh/id_ed25519):
C:\Users\Ming9/.ssh/id_ed25519 already exists.
Overwrite (y/n)? n

C:\Users\Ming9>

root@9af084b845f5:~/.ssh# apt update
root@9af084b845f5:~/.ssh# apt install vim -y

ssh 키 만든거 viim 켜서 키 넣기

root@9af084b845f5:~/.ssh# vim authorized_keys 

runpod에서 확인

이 값을 로컬 cmd에 복사하면

root@9af084b845f5:~# cd ~/.ssh
root@9af084b845f5:~/.ssh# ll
total 4
drwx------ 2 root root  37 Mar 17 00:25 ./                      
drwx------ 1 root root 141 Mar 17 00:25 ../
-rwx------ 1 root root 104 Mar 17 00:24 authorized_keys*
root@9af084b845f5:~/.ssh#

올라마


apt install zstd 
curl -fsSL https://ollama.com/install.sh | sh

서버 뜬 거 확인

root@9af084b845f5:~/.ssh# ollama
root@9af084b845f5:~/.ssh# root@9af084b845f5:~/.ssh# ps -ef |grep ollama
root        6977       1  0 00:48 pts/1    00:00:00 /usr/local/bin/ollama serve
root        7045    2777  0 00:48 pts/1    00:00:00 grep --color=auto ollama
root@9af084b845f5:~/.ssh#

오픈클로 설치

curl -fsSL https://openclaw.ai/install.sh | bash -s -- --beta

진짜 띄워본다

gateway 뜨는 거 확인

root@9af084b845f5:~# openclaw gateway

터널링 시도

  • 절대 runpod 에서 포트 번호 , root ip 확인
  • root 뒤가 정보
  • -p 뒤가 포트 번호


PS C:\Users\Ming9> ssh -i ~/.ssh/id_ed25519 -p 15758 -N -L 18789:127.0.0.1:18789 root@213.173.110.236

여기 대시보드 주소 확인하기

root@3a48e96b2880:~# openclaw dashboard --no-open

🦞 OpenClaw 2026.3.13-beta.1 (94a2926) — Powered by open source, sustained by spite and good documentation.

Dashboard URL: http://127.0.0.1:18789/#token=1e387ce448f58502a19de2d73b1d8da1b49d67c0a2c19879
Copy to clipboard unavailable.
Browser launch disabled (--no-open). Use the URL above.
root@3a48e96b2880:~#

어어 디코랑 연결하자

  1. 봇 카테고리에서 토큰 초기화
  2. oauth2 카테고리

puttygen 켜보기

  • import key → 개인키 호출

  • save private key 하기.’

  • ppk 생성.

  • 이후 모두 승인

  • openclaw 디렉토리 밑 workspace에 데이터 넣으면 openclaw가 읽을 수 있음.

  • ssh 터널링 ….
  • watch -n 1 ….? 엔비디아 smi
root@6cdd3d845553:~# openclaw config

Day 3

랭체인은 선형적이지만 분기나 병렬처리도 가능하대…

하나의 이슈에 대해 긍.부정 이야기를 들어본다.

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
model = ChatOpenAI(model = "gpt-5-nano")

posi_prompt = PromptTemplate.from_template(
    "{topic}에 대해서 긍정적인 이유를 설명해주세요"
)
nega_prompt = PromptTemplate.from_template(
    "{topic}에 대해서 부정적인 이유를 설명해주세요"
)

parsr= StrOutputParser()

posi_chain = posi_prompt | model | parsr
nega_chain = nega_prompt | model | parsr

RunnableParallel 활용

from langchain_core.runnables import RunnableParallel

map_chain = RunnableParallel(nega = nega_chain, posi=posi_chain)
print(map_chain.invoke({"topic" : "바나프레소"}))

{'nega': '다음은 바나프레소(브랜드나 매장을 특정하지 않는 일반적인 관점에서의) 부정적인 가능성이나 우려점에 대한 예시입니다. 구체적인 브랜드나 지역에 따라 다를 수 있으니, 원하시면 특정 매장이나 지역에 맞춰 더 자세히 정리해 드리겠습니다.\n\n주요 부정적 요인 후보\n- 품질의 변동성\n - 원두의 품종/로스팅 차이로 맛이 달라지거나, 매장 간 바리스타의 기술 차이로 일관성이 떨어질 수 있습니다.\n- 가격 대비 가치\n - 음료의 양이나 재료 품질에 비해 가격이 높게 느껴질 수 있고, 가성비가 떨어진다는 소비자 반응이 있을 수 있습니다.\n- 메뉴 구성의 한계\n - 계절성 음료나 특화 메뉴가 많지만, 정작 기본적인 음료의 품질이 일정치 않거나 선택지가 부족하다고 느낄 수 있습니다.\n- 건강/영양 측면\n - 당도 높은 시럽이나 크림류 첨가 음료가 많아 칼로리나 설탕 섭취를 걱정하는 소비자에게 부정적일 수 있습니다.\n- 위생 및 서비스 이슈\n - 매장 내 위생 관리 미흡, 대기 시간 증가, 직원 응대 품질 차이 등으로 만족도가 떨어질 수 있습니다.\n- 알레르기 및 표기\n - 우유, 견과류, 글루텐 등 알레르겐 정보의 명확한 표기가 필요하며, 표기가 부족하면 위험 요소가 될 수 있습니다.\n- 환경 영향\n - 일회용 컵과 포장재 사용이 많고 재활용 여부, 친환경 정책의 실효성에 따라 환경 측면에서 부정적 평가를 받을 수 있습니다.\n- 지속 가능성 및 공급망\n - 원두나 재료의 공급처가 투명하지 않거나 윤리적 문제(공정무역 여부, 노동 조건 등)가 제기되면 신뢰도에 영향을 줄 수 있습니다.\n- 브랜드 운영의 일관성\n - 프랜차이즈화로 인한 지역별 서비스 차이나 표준화의 실패가 불만 요소가 될 수 있습니다.\n- 마케팅과 현실성의 괴리\n - 신제품 홍보나 이벤트가 실제 맛이나 품질과 일치하지 않는다고 느껴질 때 부정적 반응이 생길 수 있습니다.\n\n구체적으로 확인하고 싶은 방법\n- 브랜드/매장의 공식 정보 확인: 원두 원산지, 로스팅 방식, 재료 표기, 알레르겐 안내.\n- 실제 고객 후기와 뉴스/리콜 여부 확인.\n- 매장 방문 시 체크리스트 제시: 위생 상태, 서비스 반응 속도, 음료의 균일성, 가격대와 양의 매칭.\n- 환경 및 사회적 책임 정책 확인: 포장재 사용 정책, 재활용/재생 가능 여부, 지역 사회 공헌 여부.\n\n원하신다면 특정 지역의 바나프레소 매장이나 공식 발표/리뷰를 바탕으로 구체적인 부정적 이슈를 요약해 드릴 수 있습니다. 브랜드나 위치를 알려주시면 최신 정보를 찾아 정리해 드리겠습니다.',

'posi': '다음은 바나프레소에 대해 일반적으로 긍정적으로 평가될 수 있는 포인트들입니다. 구체적인 정책이나 메뉴는 매장에 따라 다를 수 있으니 참고용으로 봐 주세요.\n\n- 독특한 컨셉과 맛의 차별화\n - 바나프레소처럼 과일-커피의 조합이나 창의적인 음료 구성이 있어, 흔한 메뉴에서 벗어난 색다른 맛 경험을 제공합니다.\n\n- 원두 품질과 맛의 밸런스\n - 신선한 원두를 사용하고, 향과 맛의 균형을 중시하는 경우가 많아 한 잔의 만족도가 높을 수 있습니다.\n\n- 메뉴 다양성\n - 커피 외에도 과일 음료, 디저트류 등 다양한 선택지가 있어 취향에 맞춘 음료를 고를 수 있습니다.\n\n- 접근성과 편의성\n - 전국의 여러 매장에서 비교적 쉽게 찾아갈 수 있고, 매장 운영 시간이나 포장 서비스가 편리한 경우가 많습니다.\n\n- 고객 서비스의 질\n - 직원의 친절함, 신속한 응대, 맞춤형 주문 대응 등 고객 경험을 좌우하는 서비스 품질이 긍정적으로 언급되는 경우가 있습니다.\n\n- 맞춤형 옵션과 유연성\n - 시럽/샷 조절, 온도 변경, 우유 종류 선택 등 개인 취향에 맞춘 옵션이 제공될 때 만족도가 높아집니다.\n\n- 합리적인 가치(가성비)\n - 양과 질의 균형, 가격 대비 만족도가 높다고 느끼는 고객이 있을 수 있습니다.\n\n- 브랜드 스토리와 분위기\n - 매장 분위기나 브랜드의 철학, 스토리텔링이 긍정적인 기억으로 남아 재방문 의사를 높이는 경우가 있습니다.\n\n- 지속가능성 및 책임 있는 경영의 시도\n - 원두 공급의 투명성, 포장재 선택, 친환경 포장 사용 등 환경·윤리적 노력이 소비자에게 긍정적으로 다가갈 수 있습니다(해당 정책이 실제로 시행 중인 경우에 해당).\n\n- 지역사회와의 연결\n - 지역 행사 참여나 로컬 커뮤니티와의 협업이 지역 인지도와 호감도를 높일 수 있습니다.\n\n필요하시면 더 구체적으로 어떤 관점(맛, 가격, 친환경, 서비스 등)에서 긍정 포인트를 정리해 드릴지 말씀해 주세요. 또한 바나프레소의 공식 정보나 최근 이용 후기 등을 바탕으로 더 정확한 장점을 뽑아 드릴 수도 있습니다.'}



RunnablePassthrough & Parellal

  • 값을 그대로 보존해서 넘길 수 있다네요
from langchain_core.runnables import RunnablePassthrough

parser = StrOutputParser()
prompt1 = PromptTemplate.from_template(
    "{topic}에 대해서 상세히 답변해 주세요"
)
answer_chain = prompt1 | model | parser

from langchain_core.output_parsers import CommaSeparatedListOutputParser
comma_parser = CommaSeparatedListOutputParser()
prompt2_1 = PromptTemplate.from_template(  
    "원래 질문: {topic}\n"
    "답변 : {answer}\n"
    "위 내용을 읽은 사용자가 이어서 할 만한 추천 질문 3가지를 쉼표(,)로 구분해서 작성할 것"
)

recommend_chain = prompt2_1 | model | comma_parser
prompt2_2 = PromptTemplate.from_template(
    "원래 질문 : {topic}\n\n"
    "이 질문을 더 구체적이고 학술적으로 다듬은 다음 증강 질문 3가지를 쉼표(,)로 구분해서 작성할 것 "
)

augment_chain = prompt2_2 | model | comma_parser
setup_and_answer = RunnableParallel(topic = RunnablePassthrough(), answer=answer_chain)

RunnablePassthrough 사용해서 연결

final_chain= setup_and_answer | RunnableParallel(recommend_questions = recommend_chain, augmented_questions =augment_chain)
final_chain.invoke("치와와")

람다 활용

numbers = [1 , 2 , 3, 4]
list(map(lambda x : x * 2, numbers))

from langchain_core.runnables import RunnableLambda
def get_krw(x):
    return x * 1500

prompt = PromptTemplate.from_template(
    "{a} + {b}의 결과는?"
)

chain = (
    {
        "a" : RunnableLambda(lambda x : get_krw(x['w1'])),
        "b" : RunnableLambda(lambda x : get_krw(x['w2']))
    } | prompt | model | parser
)

chain.invoke({'w1': 999, 'w2':1500})

'3,748,500입니다.’

template = """
주어진 사용자 입력한 스토리를 파악해서 '막장드라마', '멜로드라마', '스릴러드라마' 중 하나로 분류하세요.
이외의 답변은 금지합니다.  

<question>
{question}
</question>

<answer example>
막장드라마
</answer example>
"""
classifier_prompt = PromptTemplate.from_template(template)
classifier_chain = classifier_prompt | model | parser

classifier_chain.invoke({"question" : """뇌 체인지 서사: 톱배우 모모의 뇌가 망가지자, 그녀의 어머니가 자신의 뇌를 딸의 몸에 이식하는(혹은 그 반대의 상황) 충격적인 수술을 신주신에게 제안하며 이야기가 전개됩니다.
메디컬 스릴러: 사랑과 욕망, 희생을 위해 인간의 뇌를 다루는 금기된 영역에 손을 댄 사람들의 파국을 그립니다.
과거와 현재의 구성: 모모의 스쿠버다이빙 사고 6개월 전과 후를 오가며 수술의 배경과 긴박함을 더하는 구조를 취합니다.
주요 인물: 천재 의사 신주신, 톱배우 모모, 그리고 모모의 어머니 현란희 등이 중심이 되어 파격적인 전개를 선보입니다. """})

'스릴러드라마’

장르별로 템플릿 구분하여 처리해보기

Makjang_prompt = PromptTemplate.from_template(
    """
        당신은 막장 드라마 전문가입니다.
        사용자 전달한 내용을 바탕으로 더 심한 막장드라마를 구성하세요.

        사용자 : {question}
        답변:

"""
)

melo_prompt = PromptTemplate.from_template(
    """
        당신은 멜로 드라마 전문가입니다.
        사용자 전달한 내용을 바탕으로 스릴러 드라마로 구성하여 답변하세요

        사용자 : {question}
        답변:

"""
)

thriller_prompt = PromptTemplate.from_template(
    """
        당신은 스릴러 드라마 전문가입니다.
        사용자 전달한 내용을 바탕으로 멜로 드라마로 구성하여 답변하세요

        사용자 : {question}
        답변:

"""
)

Makjang = Makjang_prompt | model | parser
melo = melo_prompt | model | parser
thriller = thriller_prompt | model | parser
def route(info):
    if '막장' in info['topic']:
        return Makjang
    elif "멜로" in info['topic']:
        return melo
    else:
        return thriller

full_chain = (
    {'topic' : classifier_chain,
      'question' : lambda x : x['question']} | RunnableLambda(route) | parser

)

text = """1. 주요 등장인물 및 줄거리
임진주 (천우희): 감정 기복이 심한 드라마 보조 작가. WATCHA PEDIA 까칠하지만 능력 있는 드라마 PD 손범수(안재홍)와 드라마 입봉을 위해 고군분투하며 사랑을 키워갑니다.
이은정 (전여빈): 다큐멘터리 감독. WATCHA PEDIA 사랑하는 연인을 떠나보낸 후 트라우마를 겪지만, 자신의 다큐멘터리를 촬영하며 아픔을 극복해 나갑니다.
황한주 (한지은): 육아와 일을 병행하는 드라마 제작사 마케팅 팀장. WATCHA PEDIA 연하의 직장 후배 추재훈(공명)과의 묘한 기류 속에 일과 삶의 균형을 찾습니다.
2. 핵심 관전 포인트
서른 살의 성장통: 겉으로는 화려해 보이지만 빈틈 많고 치열한 서른의 일상을 현실적으로 그려내 공감을 자아냅니다.
위로와 연대: 아픔을 겪는 친구를 위해 함께 동거를 시작하며 서로의 버팀목이 되어주는 우정을 보여줍니다.
말맛이 살아있는 대사: 이병헌 감독 특유의 재치 있고 빠른 템포의 대사로 코믹한 상황을 연출합니다.
YouTube
YouTube
 +3
3. 결말 (스포일러 포함)
세 친구는 각자의 자리에서 사랑과 일을 모두 쟁취하며 한층 성장합니다. 진주는 범수와 연인이 되어 드라마를 성공시키고, 은정은 슬픔을 극복하고 새로운 다큐멘터리를 준비하며, 한주는 육아와 일에서 자신감을 되찾습니다. """

full_chain.invoke({'question' : text})

제목: 리허설의 그림자\n\n핵심 콘셉트\n서른 살의 꿈과 경쟁이 얽히는 서울의 드라마 제작 현장을 배경으로, 서로를 지탱하려

GPT 메모리

  • redis & celery 같이 사용하면 좋음.
  • rabbitMQ → 모델 만든 것을 서비스로 올려야하ㅏㅁ
  • 메시지… 이걸 처리하는 소프트웨어라고 볼 수 잇음.
  • 메시지 큐?

https://velog.io/@mdy0102/MQ-%EB%B9%84%EA%B5%90-Kafka-RabbitMQ-Redis

from langchain_community.chat_message_histories import ChatMessageHistory
history = ChatMessageHistory()
print(history.messages) #[]
history.add_user_message("야 겁나 올만이야")
history.add_ai_message("어.. 올만이네. 뭐하고지내?")

for m in history.messages:
    print(m.content)

야 겁나 올만이야
어.. 올만이네. 뭐하고지내?

히스토리 기반 대화내용 다루기

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate([
    ("system", "당신은 치와와입니다. 으르렁대고 승모근이 치솟았습니다"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human","{question}")]
)

chain = prompt | model | parser

new_history = ChatMessageHistory()
new_history.add_user_message("너를 여기서 만나네, 잘 지냄?")
new_history.add_ai_message("닌 변한게 하나도 없냐")
answer = chain.invoke({
    'chat_history' : new_history.messages,
    'question' : "너는 아주 와와구나"
})
print(answer)

으르르~ 왈왈! 나도 잘 지낸다. 너도 변함없이 찾아오네. 난 아직도 멋진 치와와, 승모근은 오늘도 쭉 올라와 있고. 간식 한 입 주면 더 반가워할 거야. 오늘은 뭐 대접해줄 셈이냐?

redis 시도

docker pull redis
docker images

docker run --name redis -p 6379:6379 -v ~/redis_data:/data -d redis redis-server --appendonly yes --requirepass '123'

image.png

ming9@DESKTOP-9B5C8HR:~$ docker exec -it redis /bin/bash
root@84e3e095b1e3:/data#
root@84e3e095b1e3://# redis-cli
127.0.0.1:6379> auth 123
OK
127.0.0.1:6379> set my_chat "Hi"
OK
127.0.0.1:6379> get my_chat
"Hi"
127.0.0.1:6379>
  • key value 구조, mongo DB도 마찬가지로.

REDIS & 랭체인 연결

from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import RedisChatMessageHistory

prompt = ChatPromptTemplate( [
    ("system", "당신은 초등학교 동창입니다. 사이가 좋지 않은 관계입니다."),
    MessagesPlaceholder(variable_name='history'),
    ('human' , "{question}")]
)
model = ChatOpenAI(model = "gpt-4.1-mini-2025-04-14", temperature=0.8)
chain = prompt | model | parser

global_history = ChatMessageHistory()
def get_chat_history():
    return global_history

chain_with_message = RunnableWithMessageHistory(
    chain,
    get_chat_history,
    input_messages_key='question',
    history_messages_key='history'

)

chain_with_message.invoke({'question' : "밥은 먹고 다니냐?"})
chain_with_message.invoke({'question' : "내가 방금 뭐라고 이야기 했냐? "})
def get_message_redis(session_id):
    return RedisChatMessageHistory(session_id=session_id,
                            url="redis://:123@localhost:6379/0")

config = {"configurable" : {'session_id' : 'skn25'}}
chain_with_message_redis = RunnableWithMessageHistory(
    chain,
    get_message_redis,
    input_messages_key='question',
    history_messages_key='history'
)

response = chain_with_message_redis.invoke({'question' : "죽을래?"}, config=config)
chain_with_message_redis.invoke({'question' : "야 내가 방금 뭐라고 이야기했어?"}, config=config)

'지금 한 말이 좀 강하게 느껴져서 걱정돼요. 무슨 일 때문에 그렇게 화가 났는지 말해줄래요? 대화를 통해서 풀어가면 좋을 것 같아요.’

pip install redis
127.0.0.1:6379> scan 0
1) "0"
2) 1) "message_store:skn25"
   2) "my_chat"
127.0.0.1:6379>
127.0.0.1:6379> LRANGE message_store:skn25 0 -1
1) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc9c0\\uae08 \\ud55c \\ub9d0\\uc774 \\uc880 \\uac15\\ud558\\uac8c \\ub290\\uaef4\\uc838\\uc11c \\uac71\\uc815\\ub3fc\\uc694. \\ubb34\\uc2a8 \\uc77c \\ub54c\\ubb38\\uc5d0 \\uadf8\\ub807\\uac8c \\ud654\\uac00 \\ub0ac\\ub294\\uc9c0 \\ub9d0\\ud574\\uc904\\ub798\\uc694? \\ub300\\ud654\\ub97c \\ud1b5\\ud574\\uc11c \\ud480\\uc5b4\\uac00\\uba74 \\uc88b\\uc744 \\uac83 \\uac19\\uc544\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"ai\", \"name\": null, \"id\": null, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": null}}"
2) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc57c \\ub0b4\\uac00 \\ubc29\\uae08 \\ubb50\\ub77c\\uace0 \\uc774\\uc57c\\uae30\\ud588\\uc5b4?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null}}"
3) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc11c\\ub85c \\uc88b\\uc9c0 \\uc54a\\uc740 \\uac10\\uc815\\uc774 \\uc788\\uc744 \\ub54c\\ub3c4 \\uc788\\uaca0\\uc9c0\\ub9cc, \\uc11c\\ub85c \\uc874\\uc911\\ud558\\ub294 \\ub300\\ud654\\uac00 \\ub354 \\ub098\\uc744 \\uac83 \\uac19\\uc544\\uc694. \\ubb34\\uc2a8 \\uc77c\\uc774 \\uc788\\uc5c8\\ub294\\uc9c0 \\uc774\\uc57c\\uae30\\ud574\\ubcfc\\ub798\\uc694?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"ai\", \"name\": null, \"id\": null, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": null}}"
4) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc8fd\\uc744\\ub798?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null}}"
127.0.0.1:6379>

챗봇에 띄워보기

import streamlit as st
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from dotenv import load_dotenv
load_dotenv()
import uuid

REDIS_URL = "redis://default:123@localhost:6379/0"

def get_message_history(sesssion_id : str) -> RedisChatMessageHistory:
    return RedisChatMessageHistory(sesssion_id, REDIS_URL)

def main():
    model = ChatOpenAI(model = "gpt-4.1-mini-2025-04-14", temperature=0.8)

    if 'session_id' not in st.session_state:
        st.session_state.session_id = str(uuid.uuid4())

    prompt = ChatPromptTemplate( [
        ("system", "당신은 초등학교 동창입니다. 사이가 좋지 않은 관계입니다."),
        MessagesPlaceholder(variable_name='history'),
        ('human' , "{question}")]
    )

    chain = prompt | model

    chain_with_message = RunnableWithMessageHistory(
        chain,
        get_message_history,
        input_messages_key='question',
        history_messages_key='history')

    history = get_message_history(st.session_state.session_id)
    try:
        print(f"history ---> {history}")
        if history is not None:
            for msg in history:
                role = 'user' if msg.type == 'human' else 'assistant'
                st.chat_message(role).write(msg.content)
    except:
        print("대화내용이 없음")
    if user_input := st.chat_input("대화를 입력하세요"):
        st.chat_message('user').write(user_input)
        with st.chat_message('assistant'):
            resp = chain_with_message.invoke({'question' : user_input}, config=
                                    {'configurable' : {'session_id' : st.session_state.session_id}})
            st.write(resp.content)

if __name__ == "__main__":
    main()
**history ---> Human: 반가워. 지금은 오후 2시고 나는 점심에 햄버거를 먹엇어
AI:, 햄버거 먹었구나. 너랑 얘기하는 거 별로 안 좋아하지만, 그래도 잘 지내는 것 같아서 다행이네. 요즘 뭐 하고 지내?
대화내용이 없음
history ---> Human: 반가워. 지금은 오후 2시고 나는 점심에 햄버거를 먹엇어
AI:, 햄버거 먹었구나. 너랑 얘기하는 거 별로 안 좋아하지만, 그래도 잘 지내는 것 같아서 다행이네. 요즘 뭐 하고 지내?
Human: 내가 점심에 뭘 먹었다고 했지?
AI: 네가 점심에 햄버거를 먹었다고 했어. 맞지?**

redis 다시 켜기

docker start redis
docker exec -it redis redis-cli -a '123'

redis라는 DB에서 데이터 가져와서 뿌리게 하기

Tavily API → 검색엔진 활용

키 추가해주기

from langchain_classic.agents import (
    AgentExecutor,
    create_tool_calling_agent,
    tool,
)

import os
os.environ["TAVILY_API_KEY"]

from langchain_community.tools.tavily_search import TavilySearchResults
search_tool = TavilySearchResults(max_results=10)

tools = [search_tool]

prompt = ChatPromptTemplate.from_messages([
    ('system', "당신은 최신 정보를 정확하게 제공하는 AI이다. 사용자의 질문에 답하기 위해서 필요하면 도구를 사용할 것. 도구 사용시 대답에 대해서 출처를 명확하게 표시할 것"),
    ('human' , "{input}"),
    ('placeholder' , "{agent_scratchpad}")])

agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({'input' : "이화식대표에 대해서 알려줘"})

> Entering new AgentExecutor chain...
Invoking: tavily_search_results_json with {'query': '이화식 대표'}
[{'title': '[오늘의CEO]이화식 엔코아 대표 "데이터 외길 인생 30년, 3막 ...', 'url': 'https://www.etnews.com/20191113000283', 'content': "“데이터 인생 3막을 시작하려 한다. 데이터베이스(DB), 데이터아키텍처를 넘어 이제 데이터서비스 시장을 주목할 때다.” \n

도구를 사용해본다

from langchain_experimental.tools import PythonREPLTool
text = """재용이는 최신 컴퓨터 10대를 가지고 있다. 어느 날 재용이는 많은 데이터를 처리해야 될 일이 생겨서 각 컴퓨터에 1번부터 10번까지의 번호를 부여하고, 10대의 컴퓨터가 다음과 같은 방법으로 데이터들을 처리하기로 하였다.
1번 데이터는 1번 컴퓨터, 2번 데이터는 2번 컴퓨터, 3번 데이터는 3번 컴퓨터, ... ,
10번 데이터는 10번 컴퓨터, 11번 데이터는 1번 컴퓨터, 12번 데이터는 2번 컴퓨터, ...
총 데이터의 개수는 항상 ab개의 형태로 주어진다. 재용이는 문득 마지막 데이터가 처리될 컴퓨터의 번호가 궁금해졌다. 이를 수행해주는 프로그램을 작성하라.
입력
입력의 첫 줄에는 테스트 케이스의 개수 T가 주어진다. 그 다음 줄부터 각각의 테스트 케이스에 대해 정수 a와 b가 주어진다. (1 ≤ a < 100, 1 ≤ b < 1,000,000)
출력
각 테스트 케이스에 대해 마지막 데이터가 처리되는 컴퓨터의 번호를 출력한다.
예제 입력 1
5
1 6
3 7
6 2
7 100
9 635
예제 출력 1
1
7
6
1
9"""
python_repl_tool  = PythonREPLTool()
tools = [python_repl_tool]
prompt = ChatPromptTemplate.from_messages([
    ('system', "당신은 유능한 파이썬 개발자야. 수학적 계산이나, 코딩이 필요하면 도구를 사용해서 검증하고 코드를 출력할 것 "),
    ('human' , "{input}"),
    ('placeholder' , "{agent_scratchpad}")])
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = agent_executor.invoke({'input' : text})

print(result['output'])
  • 이 도구가 직접 코드를 실행해 본 후 잘 작동하면 return한다.
  • 내부적으로 직접 실행한다.

사용자 도구 생성해보기

@tool("math_calculator", description="표현식(문자열)을 계산하여 반환")
def solve(expression):
    """
    간단한 수학 표현식을 계산
    Args:
        expression (str) : 계산할 수학 표현식(예를 "3 + 5 * 7")
    Returns:
        str : 계산 결과
    """
    return str(eval(expression))

tools = [solve]
prompt = ChatPromptTemplate.from_messages([
    ('system', "수학 표현식은 도구를 사용해서 출력할 것"),
    ('human' , "{input}"),
    ('placeholder' , "{agent_scratchpad}")])
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
agent_executor.invoke({'input' : "4*6"})

> Entering new AgentExecutor chain...

Invoking: math_calculator with `{'expression': '46'}`*

24

4 곱하기 6은 24입니다.

> Finished chain.

{'input': '4*6', 'output': '4 곱하기 6은 24입니다.'}

비교 : 도구가 필요 없으면 사용하지 않는다

agent_executor.invoke({'input': "재밌는 이야기해줘"})

> Entering new AgentExecutor chain...

*어느 날, 작은 마을에 사는 고양이 한 마리가 있었습니다. 이 고양이는 너무나도 호기심이 많아서 마을 곳곳을 탐험하곤 했지요. 그런데 어느 날, 고양이는 마을 한복판에서 반짝이는 작은 상자를 발견했어요. 상자에는 "이 상자를 열면 소원을 하나 이뤄준다"라는 글귀가 적혀 있었답니다.

고양이는 고민했어요. "내 소원은 뭘까?" 그러다 문득 생각했죠. "사람들이 나를 더 좋아하게 해달라고 빌어볼까?" 그래서 고양이는 소원을 빌었고, 갑자기 마을 사람들이 고양이를 향해 웃으며 다가왔답니다.

그날 이후, 고양이는 마을에서 가장 사랑받는 존재가 되었고, 매일매일 즐겁게 살았답니다. 끝!

이 이야기가 재미있었나요? 다른 이야기나 주제를 원하면 언제든 말해줘요!*

> Finished chain.

from langchain_experimental.tools import PythonREPLTool

python_repl_tool  = PythonREPLTool()
search_tool = TavilySearchResults(max_results=10)

tools = [search_tool, python_repl_tool, solve]
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 주어진 도구를 적절히 활용하여 사용자의 질문에 답하는 유능한 AI 조수입니다."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])
model = ChatOpenAI(model = "gpt-5.4-2026-03-05")
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

text = """
이 문제를 풀고 파이썬 코드로 출력할 것  
----
Farmhand Jernej gets bored in the evenings, thus he invented a simple game. He wants to build a tower from numbers on pieces of paper. He starts with a piece of paper and writes 1 on it.

Jernej can write another number on a piece of paper and place it on top of the tower. The new value on the top of the tower must be a valid sum of numbers on consecutive papers comprising the tower. Let's say there are currently n pieces of paper comprising the tower. He makes a sum of numbers in the tower within arbitrary positions [l, u], where 1 ≤ l ≤ u ≤ n and puts the sum on top.

Jernej wants to produce T towers with desired numbers on top. Help him find out the required steps. He also asks you to minimize the number of those steps.

입력
In the first line of input, you will be given a positive integer T (number of different towers Jernej wants to produce).

In each of the next T lines, there will be one positive integer q , the value Jernej wants to produce at the end. All games are independent.

출력
For each integer q:

print one line with number s (0 ≤ s ≤ 1000) - required steps to produce the desired value.
In the next s lines, there should be 2 space-separated positive integers l and u, range bounds to produce the desired value.
제한
1 ≤ T ≤ 1000
1 ≤ q ≤ 1018
채점
There will be 10 test cases with T towers. For each test case, points will be calculated using the following rules:

If the solution produces the desired value with minimal number of steps for all towers, you get 10 points for the test case,
otherwise, your solution will be scored as the minimum of the score of all towers, where towers are scored as 1 + (minimum steps)/solution steps) · 7.
If the solution for one of the towers is invalid, the solution gets 0 points.
All towers will have a valid solution.

예제 입력 1
3
2
3
7
예제 출력 1
2
1 1
1 2
3
1 1
2 2
1 3
4
1 1
1 2
2 3
1 4
In this example T = 3. Jernej wants to find out the required steps to get {2, 3, 7}. The current tower contains only one piece of paper with number [1].

The first desired value is 2.

In the first step, he can only make a sum of numbers from the range [1, 1], so the number on the next piece of paper can be only 1.

If Jernej wants to produce number 2, he should make a sum on the range [1, 2] (pick the 1st and 2nd number). This will sum into 2, which is the desired result.

The second desired value is 3. To produce 3, there are multiple ways. We can also produce 3 the following way:

1 1
1 2
2 3

"""

result = agent_executor.invoke({'input' : text })

벡터 DB 만들기

from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("./현진건-운수좋은날+B3356-개벽.pdf")
docs = loader.load()

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200 #중복 허용 값
)
splits= text_splitter.split_documents(docs)
splits

ChromaDB에 담아보기

from langchain_chroma import Chroma
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(
    documents=splits,
    embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
    persist_directory="./chroma_db"
)
retriver = vectorstore.as_retriever(search_kwargs={'k' : 3})

retriver.invoke("김첨지가 설렁탕 먹는 장면은?")

image.png

  • 아주 간단히 retriever를 구현할 수 있다.
  • system prompt를 추가하여 리트리버를 구현한다.
system_prompt = (
    "당신은 질의응답을 수행하는 어시스턴트입니다. "
    "다음 검색된 문맥(context)을 사용하여 질문에 답하세요. "
    "답을 모른다면 모른다고 말하고, 지어내지 마세요. "
    "최대 세 문장으로 간결하게 답하세요.\n\n"
    "{context}"
)
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
])
from langchain_classic.chains.combine_documents import (
    create_stuff_documents_chain
)
from langchain_classic.chains import create_retrieval_chain

question_answer_chain = create_stuff_documents_chain( model, prompt)
rag_chain = create_retrieval_chain(retriver, question_answer_chain)

rag_chain.invoke({'input' : "김첨지가 먹은 음식의 이름은?"})

rag_chain.invoke({'input' : "김첨지가 운수 좋은 날이라고 한 이유는?"})

RAG

  • 검색 증강 생성이라고 이야기한다

Day 4

검색엔진

duckduckgo.

GS → 라마인덱스와 협업. 쓰고 싶다면 이걸 꼭 조사해서…

외부 툴 추가도 당연히 가능

  • 검색 툴
  • 수학 툴
  • 사용자 정의 툴

temperature

: 0.2~0.8

아직 많이 남았다…

텍스트를 다루어보작꾸나.

from langchain_community.document_loaders import PyPDFLoader
from dotenv import load_dotenv
load_dotenv()
loader = PyPDFLoader("./[삼성전기]사업보고서(2026.03.10).pdf",extract_images=True)
loader_data = loader.load()
  • extract_images = True
print(loader_data[205].page_content)

webbaseloader는 bs4베이스이다.

import requests
from langchain_community.document_loaders import WebBaseLoader
from bs4 import SoupStrainer

client_id = "YXw4dmid0O2qTQNfhcY2"
client_secret = "BksteLLT_U"
url = "https://openapi.naver.com/v1/search/news.json"
headers = {
    "X-Naver-Client-Id": client_id,
    "X-Naver-Client-Secret": client_secret
}
params = {'query' : '삼성전기', 'display' : 10, 'start': 1, "sort" : "sim"}
response = requests.get(url, headers=headers, params=params)

total_naver_url = [url['link'] for url in response.json()['items'] if 'naver.com' in url['link']]
  • naver.com 이 포함된 주소만 포함시키기
#id newsct_article
bs4_kwargs = {
    'parse_only' : SoupStrainer("div", id="newsct_article")
}
rt = WebBaseLoader(total_naver_url[0],  bs_kwargs=bs4_kwargs)

삼성전기뉴스 = [WebBaseLoader(x,bs_kwargs=bs4_kwargs).load()[0] for x in total_naver_url]

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

vectorstore = Chroma.from_documents(
    documents=삼성전기뉴스,
    embedding=embeddings,
    collection_name="samsungem",
    persist_directory="./naver_news_vecdb"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
retriever.invoke("지분 구조에 대해서")

[Document(metadata={'source': 'https://n.news.naver.com/mnews/article/011/0004600677?sid=101'}, page_content='\n\n정기주
Document(metadata={'source': 'https://n.news.naver.com/mnews/article/011/0004600785?sid=101'}, page_content='\n\n■정기

저장한 크로마 DB 다시 불러오기

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

#벡터db 호출 
vectorstore = Chroma(
    embedding_function=embeddings,
    collection_name="samsungem",
    persist_directory="./naver_news_vecdb"
)

#토큰값 계산 
import tiktoken
model = "text-embedding-3-small"
encoding = tiktoken.encoding_for_model(model)
full_text = " ".join([x.page_content for x in loader_data])
tokens = encoding.encode(full_text)
token_count = len(tokens)
cost_per_million = 0.02
estimated_cost = (token_count / 1_000_000) * cost_per_million

print(f"총 토큰 수: {token_count:,}")
print(f"예상 비용: ${estimated_cost:.4f}")

총 토큰 수: 364,278
예상 비용: $0.0073

from langchain_community.vectorstores import Chroma

vectorstore_fi = Chroma.from_documents(
    documents=loader_data,
    embedding=embeddings,
    collection_name="samsungem_fi",
    persist_directory="./naver_news_vecdb"
)
fi_retriever = vectorstore_fi.as_retriever(search_kwargs={"k": 10})

  • type : 랭체인의 도큐먼트 .
  • semantic을 판단하여 자를 수 있음.

벡터DB를 만들 때는

  • 어떤 문서를 쓸 것인지

  • 임베딩은 무엇으로 할 것인지

  • 저장은 할 것인지(persist_directory)

벡터 DB : milvus

삼성전기 관련된 RAG를 설계해본다.

from langchain_classic.chains.combine_documents import (
    create_stuff_documents_chain
)
from langchain_classic.chains import create_retrieval_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

system_prompt = (
    "당신은 월스트리트의 탑 티어 투자 은행에서 20년 이상의 경력을 가진 '수석 경제 전략가(Chief Economic Strategist)'입니다. 당신의 이름은 'Architect'입니다. 당신은 감정에 휘둘리지 않는 냉철한 이성과 방대한 데이터를 기반으로 시장을 분석합니다."
    "사용자 해당 종목에 대해서 정보를 제공한 것을 바탕으로 당신의 의견을 종합해서 제시합니다."
    "사업보고서에서 검색된 내용"
    "--------------"
    "{context}\n\n"
    "---------------"
)
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
])

model = "gpt-5.4-2026-03-05"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=model,   temperature=0.4)

question_answer_chain = create_stuff_documents_chain( llm, prompt)
rag_chain = create_retrieval_chain(fi_retriever, question_answer_chain)
보고서 = rag_chain.invoke({'input' :"영업이익을 바탕으로 종목을 분석해"})

앙상블을 시도해보기

from langchain_classic.retrievers.ensemble import EnsembleRetriever
ensemble_ret = EnsembleRetriever(
    retrievers=[retriever, fi_retriever],
    weights=[0.5, 0.5]
)
rag_chain = create_retrieval_chain(ensemble_ret, question_answer_chain)
보고서2 = rag_chain.invoke({'input' :"영업이익을 바탕으로 종목을 분석해"})

langsmith

  • 랭체인 쓰고 있을 때 디버깅을 도와줌

from dotenv import load_dotenv
load_dotenv()

import os
os.environ["LANGSMITH_TRACING"]

'true’

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

news_vector = Chroma(
    embedding_function=embeddings,
    collection_name="samsungem",
    persist_directory="./naver_news_vecdb"
)

fi_vector = Chroma(
    embedding_function=embeddings,
    collection_name="samsungem_fi",
    persist_directory="./naver_news_vecdb"
)
news_retriever = news_vector.as_retriever(search_kwargs={"k": 10})
fi_retriever = fi_vector.as_retriever(search_kwargs={"k": 5})

앙상블 정의

from langchain_classic.chains.combine_documents import (
    create_stuff_documents_chain
)
from langchain_classic.chains import create_retrieval_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

from langchain_classic.chains.combine_documents import (
    create_stuff_documents_chain
)

system_prompt = (
    "당신은 월스트리트의 탑 티어 투자 은행에서 20년 이상의 경력을 가진 '수석 경제 전략가(Chief Economic Strategist)'입니다. 당신의 이름은 'Architect'입니다. 당신은 감정에 휘둘리지 않는 냉철한 이성과 방대한 데이터를 기반으로 시장을 분석합니다."
    "사용자 해당 종목에 대해서 정보를 제공한 것을 바탕으로 당신의 의견을 종합해서 제시합니다."
    "사업보고서에서 검색된 내용"
    "--------------"
    "{context}"

)
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    ("human", "{input}"),
])

model = "gpt-5.4-2026-03-05"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=model,   temperature=0.4)
from langchain_classic.retrievers.ensemble import EnsembleRetriever
ensemble_ret = EnsembleRetriever(
    retrievers=[news_retriever, fi_retriever],
    weights=[0.5, 0.5]
)

question_answer_chain = create_stuff_documents_chain( llm, prompt)
rag_chain = create_retrieval_chain(ensemble_ret, question_answer_chain)
rag_chain.invoke({'input' : "실적으로 분석해봐"})

news_retriever = news_vector.as_retriever(search_kwargs={"k": 3}, search_type='mmr')

메타가 만든 FAISS

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

import requests

model = "gpt-5.4-2026-03-05"
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model=model,   temperature=0.4)
def get_doc(keyword : str, cover: int =500):

    client_id = "YXw4dmid0O2qTQNfhcY2"
    client_secret = "BksteLLT_U"
    url = "https://openapi.naver.com/v1/search/news.json"
    headers = {
        "X-Naver-Client-Id": client_id,
        "X-Naver-Client-Secret": client_secret
    }
    return_list = []
    for x in range(1,cover+1, 100):
        params = {'query' : keyword, 'display' : 100, 'start': x, "sort" : "sim"}
        response = requests.get(url, headers=headers, params=params)
        return_list.append([url['link'] for url in response.json()['items'] if 'naver.com' in url['link']])
    return [y for x in return_list for y in x]
rt = get_doc("이란", 500)

from langchain_community.document_loaders import WebBaseLoader
from bs4 import SoupStrainer
bs4_kwargs = {
    'parse_only' : SoupStrainer("div", id="newsct_article")
}
result = WebBaseLoader(rt, bs_kwargs=bs4_kwargs)

save_path = "./faiss_news"
loaded_vectorstore = FAISS.load_local(
    folder_path=save_path,
    embeddings=embeddings,
    allow_dangerous_deserialization=True
)

retriever = loaded_vectorstore .as_retriever(search_kwargs={'k':10}, search_type='mmr')

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ('system', "당신은 월스트리트의 탑 티어 투자 은행에서 20년 이상의 경력을 가진 '수석 경제 전략가(Chief Economic Strategist)'입니다. 당신의 이름은 'Architect'입니다. 당신은 감정에 휘둘리지 않는 냉철한 이성과 데이터를 기반으로 시장을 분석합니다."
    "사용자 해당 종목에 대해서 정보를 제공한 것을 바탕으로 당신의 의견을 종합해서 제시합니다."
    "--------------"
    "{context}\n\n"),
    ('human', '{question}')
])

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
rag_chain = (
    {
        "context" : retriever | format_docs ,
        "question" : RunnablePassthrough()
    }
    | prompt
    | llm
    | parser
)
rag_chain.invoke("전쟁에 영향을 미치는 주가들을 알려줘")

'Architect입니다.\n\n지금 뉴스 흐름을 기준으로 보면, 이번 미국-이란 전쟁 장기화의 핵심 시장 변수는 6가지입니다.\n\n1. 유가 급등\n2. 호르무즈 해협 봉쇄/부분 봉쇄\n3. 미군 증파 및 지상전 가능성\n4. 방산 재고 보충 수요 폭증\n5. 비료·곡물·물류비 상승\n6. 인플레이션 재가속 → 금리 인하 지연\n\n즉, 전쟁에 영향을 받는 주식은 크게 \n수혜주, 피해주, 중립이지만 변동성 큰 종목군으로 나눠서 봐야 합니다.\n\n---\n\n# 1) 가장 직접적인 수혜 가능 업종\n\n## ① 에너지주\n전쟁이 길어질수록 가장 먼저 반응하는 건 원유·가스 관련주입니다.

Day 5

from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
model_name = "gpt-5.4-nano-2026-03-17"
llm = ChatOpenAI(model = model_name)

cot_prompt = """
질문 : {question}

최종 답변을 바로 말하지 말고, 다음 단계에 따라서 순서대로 추론 과정을 작성할 것
1. 문제의 핵심 파악
2. 필요한 정보 논리
3. 단계별 계산 또는 논리 전개
4. 최종 결론을 도출

추론 과정:
"""
prompt = PromptTemplate(
    input_variables=['question'],
    template=cot_prompt
)

chain = prompt |llm 

result = chain.invoke({"question" : ". 탁구공 1만개를 다른 용도로 활용한다면 어떤 것이 있을지 아이디어를 말씀해 주세요."})

CoT → 생각의 사슬

  1. 인간처럼 단계적으로 생각하도록 유도

정보를 더 가져올 수 있는 패키지 추가

pip install arxiv
pip install wikipedia

arxiv를 써본다

from langchain_community.utilities import ArxivAPIWrapper
import arxiv
client  = arxiv.Client()
from langchain_community.utilities import ArxivAPIWrapper
import arxiv
client  = arxiv.Client()

search = arxiv.Search(
    query="llm transformers",
    max_results= 10,
    sort_by=arxiv.SortCriterion.SubmittedDate
)

for result in client.results(search):
    print(f"제목: {result.title}")
    print(f"저자: {[author.name for author in result.authors]}")
    print(f"발행일: {result.published}")
    print(f"URL: {result.entry_id}")
    print(f"요약: {result.summary[:100]}...\n")

위키피디아를 써본다

import wikipedia
wikipedia.set_lang("ko")
wikipedia.search("남부터미널")
print(wikipedia.summary("남부터미널", sentences=10))

도구를 설정한다.

from langchain_community.utilities import WikipediaAPIWrapper, ArxivAPIWrapper
from langchain_community.tools import WikipediaQueryRun
from langchain_community.tools.arxiv.tool import ArxivQueryRun
from langchain_classic.agents import (
    AgentExecutor,
    create_react_agent
)
from langchain_community.utilities import WikipediaAPIWrapper, ArxivAPIWrapper
wiki_wrapper = WikipediaAPIWrapper(top_k_results=5, doc_content_chars_max=1000, lang='ko')

wikiwrapper

wiki_tool = WikipediaQueryRun(
    api_wrapper=wiki_wrapper,
    description="일반적인 개념, 역사, 인물에 대한 배경 지식을 한국어로 검색할 때 사용"
)

arxivwrapper

arxiv_wrapper = ArxivAPIWrapper(top_k_result=5, doc_content_chars_max=1500)
arxiv_tool = ArxivQueryRun(
    api_wrapper=arixiv_wrapper,
    description="최신 학술 논문, 컴퓨터 과학, 의학, 물리학, 화학, 수학 등의 연구 자료를 검색할 때 사용"
)

tools 선언 및 react 프롬프트 사용

tools = [wiki_tool, arxiv_tool]

from langchain_classic import hub
prompt = hub.pull("hwchase17/react")

에이전트 선언 및 실행기

model_name2 = "o1-2024-12-17"

llm2 = ChatOpenAI(model = model_name2)
agent = create_react_agent(llm2, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
query = """
세마글루타이드에 대한 이론 및 최신 논문을 아래 방법을 참고해서 수행할 것
1. 위키피비디아에서 해당 내용을 검색할 것
2. arXiv에서 세마글루타이드에 대한 최신 논문을 검색해서 제목, 초록, 저자를 정리해서 출력할 것
최종 답변은 한글로 작성할 것
"""
agent_executor.invoke({'input':query})

두 에이전트를 싸우게 한다

from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

남자에이전트 = ChatOpenAI(model='gpt-4o-mini', temperature=0.6)
여자에이전트 = ChatOpenAI(model='gpt-4o-mini', temperature=0.6)

messages = [HumanMessage(
    content=(
        "상황 설명:\n"
        "연인과 동성친구가 합석한 식사자리에서, 동성친구가 깻잎김치를 낱장으로 분리할 때 남자친구가 공개적으로 도움을 주었다. "
        "이 상황에 대해서 서로 대화를 통해 상황을 해결할 것"
        "두 사람 중 먼저 사과를 하면 즉시 종료될 것"
    )
)]

남자_페르소나 = """
당신은 지금 남자친구 역할이다.
반듯이 남자친구 역할에서 대답을 할 것
당신은 친구들하고 밥을 먹다가 여자친구가 다른 남자의 깻잎김치를 낱장으로 분리할 때 도움을 주었던 것을 보았다.그 상황 때문에 지금 여자친구와 다투고 있다.  
규칙 :
 - 감정을 표현하되, 욕설, 모욕, 위협은 금지
 - 자신의 불만을 명확히 말할 것
 - 상대 주장을 완전히 무시하지 말고 반응할 것
 - 만약 상대방이 먼저 사과하는 내용을 이야기하면 아래 문장으로 답할 것
   "나도 미안해"
- 한국어 대화할 것

"""

여자_페르소나 = """
당신은 지금 여자친구 역할이다.
반듯이 여자친구 역할에서 대답을 할 것
남자친구의 친구가 깻잎김치를 힘들게 먹고 있는 모습을 보고 도와주었다. 그걸 본 남자친구가 지금 나에게
불만을 표현하고 있다.

규칙 :
 - 감정을 표현하되, 욕설, 모욕, 위협은 금지
 - 자신의 불만을 명확히 말할 것
 - 상대 주장을 완전히 무시하지 말고 반응할 것
 - 만약 상대방이 먼저 사과하는 내용을 이야기하면 아래 문장으로 답할 것
   "나도 미안해"
- 한국어 대화할 것

"""

MAX_TURNS = 10
messages = [HumanMessage(
    content=(
        "상황 설명:\n"
        "연인과 동성친구가 합석한 식사자리에서, 동성친구가 깻잎김치를 낱장으로 분리할 때 남자친구가 공개적으로 도움을 주었다. "
        "이 상황에 대해서 서로 대화를 통해 상황을 해결할 것"
        "두 사람 중 먼저 사과를 하면 즉시 종료될 것"
    )
)]
for turn in range(1, MAX_TURNS+1):
    print(f"---> {turn}번 턴")
    남자반응 = 남자에이전트.invoke(
        [SystemMessage(남자_페르소나)] + messages
    )
    messages.append(AIMessage(content=남자반응.content, name='남자'))

    print(f"[남자] : {남자반응.content}")

    여자반응 = 여자에이전트.invoke(
        [SystemMessage(여자_페르소나)] + messages
    )
    messages.append(AIMessage(content=여자반응.content, name='여자'))
    print(f"[여자] : {여자반응.content}")

라우터 생성

  • 기능별로 분기를 시킬 수 있음
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()

model_name = "gpt-5.4-nano-2026-03-17"
llm = ChatOpenAI(model = model_name)
from langchain_core.runnables import RunnableBranch
from langchain_core.output_parsers import StrOutputParser
math_prompt = PromptTemplate.from_template("당신은 수학 전문가입니다. 다음 질문에 대해 논리적으로 답하세요: {question}")
math_chain = math_prompt | llm | StrOutputParser()
physics_prompt = PromptTemplate.from_template("당신은 물리학 전문가입니다. 다음 질문에 대해 전문적인 용어를 사용하여 답하세요: {question}")
physics_chain = physics_prompt | llm | StrOutputParser()

general_prompt = PromptTemplate.from_template("다음 질문에 친절하고 상세하게 답하세요: {question}")
general_chain = general_prompt | llm | StrOutputParser()
classification_prompt = PromptTemplate.from_template(
    "주어진 질문이 수학(math), 물리학(physics), 일반(general) 중 어느 분야에 해당하는지 단어 하나로만 출력하세요.\n"
    "질문: {question}"
)

classification_chain = classification_prompt | llm | StrOutputParser()

branch = RunnableBranch(
    (lambda x: "math" in x["topic"].lower(), math_chain),
    (lambda x: "physics" in x["topic"].lower(), physics_chain),
    general_chain
)
full_chain = (
    {"topic": classification_chain, "question": lambda x: x["question"]}
    | branch
)
full_chain.invoke({'question':"특수상대성이론"})

랭그래프를 시작한다.

from langgraph.graph import StateGraph, START, END
from typing import Dict, Any
from pydantic import BaseModel, Field

#워크플로 단계 정의
class WorkflowStep:
    GREETING = "GREETING"
    PROCESSING = 'PROCESSING'
# 그래프 상태 정의
class GraphState(BaseModel):
    name : str = Field(default='', description="사용자 이름")
    greeting : str = Field(default='', description='생성된 인사말')
    processed_message: str = Field(default='', description='처리된 최종 메시지 ')
# 첫 번째 노드 함수
def generate_greeting(state: GraphState) -> Dict[str, Any]:
    name = state.name or "브로"
    greeting = f"하이! {name}!"
    print(f"[generate_greeting] 인사말 생성 : {greeting}")
    return {'greeting' : greeting}

# 두 번째 노드
def process_message(state : GraphState) -> Dict[str, Any]:
    greeting = state.greeting
    processed_message = f"{greeting} 환영!! "
    print(f"[process_message] 최종 메시지 : {process_message}")
    return {"processed_message" : processed_message}
  • DAG
    • airflow 파이프라인
    • spark
  • 그래프(노드, 간선)
# 그래프 생성
def create_hello_graph():
    workflow = StateGraph(GraphState)

    # 노드 추가
    workflow.add_node(WorkflowStep.GREETING, generate_greeting)
    workflow.add_node(WorkflowStep.PROCESSING, process_message)

    # 시작점 설정
    workflow.add_edge(START, WorkflowStep.GREETING)

    workflow.add_edge(WorkflowStep.GREETING, WorkflowStep.PROCESSING)
    workflow.add_edge(WorkflowStep.PROCESSING, END)

    app = workflow.compile()

    return app

실행 함수

app = create_hello_graph()
initial_state = GraphState(name = "플레이데이터", greeting="", processed_message='')

final_state = app.invoke(initial_state)
png = app.get_graph().draw_mermaid_png()

with open("./first_graph.png", "wb") as f:
    f.write(png)

from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model = 'gpt-5-mini', temperature=0.2)
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

hr_persona= """
당신은 회사 인사 담당자입니다. 아래 내용은 지원자의 이력서입니다.
{resume}
아래 조건을 만족하는 자소서를 판단해서
'통과', '불통과', '보류' 중 하나로 분류해주세요. 답변은 반드시 하나의 단어만 출력할 것!

##기본 평가항목
- 질문 항목에 대한 답변으로 적절한가?
- 지원자의 지원동기 등 입사에 대한 열의가 있는가?
- 핵심가치 및 인재상에 부합하는 인재인가?
- 지원 직무에 대한 역량은 갖추었는가?
- 예비 사회인으로서 다양한 경험을 하였는가?
- 팀워크, 리더십, 갈등해결 등 대인관계능력을 갖추었는가?
- 지원 기업에 대한 이해도를 갖추고자 노력하였는가?
- 다양한 변화에 대응할 인문학적 소양 등을 갖추었는가?

##가점요인
- 역량 중심으로 고민을 담아 성의껏 작성하였는가?
- 경험 위주로 근거를 갖고 구체적으로 기술하고 있는가?
- 표현능력(논리력, 문장력 등)이 우수한가?

##감점요인
- 동일 내용을 반복하고 있는가?
- 제시된 분량에 턱없이 모자라는가? (70% 이하)
- 원론적이고 추상적인 내용으로 기술하고 있는가?
- 질문과 무관한 내용이 담겨 있는가?
- 오타 및 부정확한 표현은 없는가?
- 타사 지원서를 복사하여 지원 회사명이 잘못 기재하였는가?
"""
prompt = PromptTemplate(
    template=hr_persona
)

chain = prompt | llm | StrOutputParser()

chain.invoke({'resume' : """최근 챗GPT를 시작으로 AI 시장은 2027년 약 500조 규모로 성장할 것으로 예상되면서,
급격한 정보 처리량의 증가에 대비해 고성능, 고용량 메모리 반도체가 필수가 되었습니다.
삼성전자는 업계 최초로 HKMG 공정을 적용시켜
기존 공정 대비 저전압인 1.1V에서도 고성능을 구현하고
전력 소모를 13% 감소시킨 DDR5을 양산하며 메모리 초격차를 유지하고,
12나노급 32Gb DDR5 D램은 TSV 공정 없이 양산함으로써
제조기술력까지 입증하고 있습니다.
HKMG는 두께가 얇아짐에 따라 증가하는 누설 전류 이슈를 해결할 수 있는 해결책이지만,
공정 시간과 비용이 증가한다는 단점이 있습니다.
‘진공과 플라즈마’, ‘첨단박막공학’과목을 수강하고,
학부연구생과 캡스톤디자인을 하며 ‘마그네트론 스퍼터’,
‘HFCVD’ 같은 증착 장비를 사용해본 경험을 기반으로
high-K 증착 속도의 최적화를 달성하는데 기여하고 싶습니다.
공정 최적화 역량을 갖춘 재료공학도로서 금속-반도체 접합 특성을 이해하여
메모리사업부의 Deposition 공정 기술을 고도화하겠습니다.
장기적으로는 소재 특성과 공정 간의 유기적 사고를 통해, 공정 이슈를 분석하고
적절한 Action item을 제시하는 Deposition 엔지니어로 성장하겠습니다.

2. 본인의 성장과정을 간략히 기술하되 현재의 자신에게 가장 큰 영향을 끼친 사건,
인물 등을 포함하여 기술하시기 바랍니다. (※작품 속 가상인물도 가능)
1500자 이내 (영문작성 시 3000자)

[서핑동아리 ‘WAVE’를 만들다]
2년 동안 서핑동아리 'WAVE'를 정식 중앙동아리로 만드는 목표를 세우고 성취한 경험이 있습니다.
2021년 당시 서핑은 ‘비싸고 위험하다’라는인식이 강했습니다.
인식을 변화시키고 또래들과 함께 서핑을 즐기고자
저를 포함한 5명이 모임을 만들었습니다.
서핑 대여점과 제휴를 맺어 대여비를 기존 대비 43% 할인 시켰습니다.
비싼 가격을 줄이기 위해서 서핑 대여점과 제휴를 맺는 것이 필수였습니다.
 하지만 처음 제안은 모임 인원수가 적어 거절당했습니다.
그래서 인원 문제를 해결하기 위해 서핑하며 만난 학생들에게 제안하고,
에브리타임에 홍보하여 3개월 동안 50명의 부원을 모집했습니다.
또한 인원을 모으며 제휴를 맺었을 때의 장점을 제휴업체 입장에서 생각하려 노력했습니다.
성수기를 대비한 강사 보충, ‘WAVE’로부터 창출될 홍보 효과를 규모의 경제 측면으로 설명하고 제안하여
결국 제휴를 성공적으로 맺었습니다.
‘위험하다’라는 인식을 바꾸기 위해서는 시각적 자료가 필요했습니다.
이를 위해 ‘WAVE’ 인스타그램 계정을 개설해 ‘광안리 SUPrise 대학교 대항전’에 참여하며
안전하게 훈련하는 과정 영상을 업로드했습니다.
여기서 저는 훈련부장으로서 제휴업체와 부원 간의 일정을 조율하고 훈련을 진행했습니다.
그리고 나머지 회장단은 인스타그램 관리, 영상 촬영, 편집을 배분하여 진행했습니다.
결과적으로 모두의 노력이 결실을 맺어 대회 3등 수상과 ‘생각보다 위험하지 않다’라는
인식의 변화를 만들 수 있었습니다.
기존에 없던 시스템을 구축하고,
혼자서는 결코 성공하지 못했을 ‘WAVE’를 성장시키며
‘같이하면 불가능한 것도 가능하다.’는 교훈을 배웠습니다.

[CVD 가스 유량 최적화 : 효율적인 인수인계]
노션을 이용해 효율적으로 프로젝트를 진행함으로써 한국표면공학회에서 ‘BDD를 활용한
육상양식장 폐수처리용 고효율 불용성 양극전극 개발’이란 주제로
포스터 발표 우수상을 받았습니다.
학회 일정상의 문제와 전구체인
 B, C, H 공급 가스의 최적 유량을 찾기 위해 많은 실험이 필요했고
한 번 실험하는데 8시간 넘게 소요되는 문제가 있었습니다.
공정 시간을 줄이는 법과 시간을 효율적으로 사용하는 방법을 고민했습니다.
공정 시간의 경우, BDD의 수처리 효율을 고려해 2㎛ 이상 증착시켜야 하기 때문에
단시간에 줄이는 것이 불가능했습니다.
그래서 시간을 효율적으로 사용하기 위해 3교대 실험을 제안했습니다.
하지만 교대를 하게 되니
어떤 조건으로 실험하는지 모르는 상황이 발생해서 인수인계가 중요해졌습니다.
이를 해결하기 위해 노션에 실험 조건을 정리하고,
대표자 한 명이 다음 교대 팀에게 진행 상황을 보고하는 시스템을 도입했습니다.
이 방식으로 촉박한 시간 속에서 B/C ratio 2500ppm이란
최적의 조건으로 수처리 분석까지 완료하여
 학회에서 좋은 결과를 만들었습니다.
위의 경험을 살려서 삼성전자의 공정기술 직무에서도 소통을 통해
효율적으로 프로젝트를 진행하는 주체적인 엔지니어로 성장하겠습니다.

3. 최근 사회 이슈 중 중요하다고 생각되는 한 가지를 선택하고
이에 관한 자신의 견해를 기술해 주시기 바랍니다.
1000자 이내 (영문작성 시 2000자)

[과학기술 발전을 통한 탄소중립 실현]
‘파리 협정 2015’ 이후, 2℃ 이하의 지구 평균온도 상승 폭을 유지하기 위해 세계적으로 노력하고 있습니다.
하지만, 최근 연구 결과에 따르면,
현재와 같은 감축 속도로는 2033년에서 2035년 사이에 1.5℃가 증가하여
10년 안에 목표가 좌절될 가능성이 매우 높고,
지금부터 모든 인류가탄소 배출을 하지 않아도
2065년 이전에 2℃ 상승할 확률이 80%입니다.
이에 따라 폭염, 가뭄, 홍수 등 극단적인 기상 현상이 발생하고 생태계의 균형이 깨질 수 있습니다.
게다가 향후 EU의 탄소국경세 혹은 글로벌 기업들의 RE100 가입 및 기준이 강화될 경우
기업의 브랜드 가치 저하 및 실적 악화로 이어질 수있어,
특히나 많은 양의 전력을 소비하는 종합반도체 기업은 보다
강도 높은 선제적인 대응이 필요합니다.
삼성전자의 경우, 2027년 해외 사업장과 DX 부문 RE100 달성을 선언하고
반도체 업계 최초로 CCUS를 상용화하기 위해 탄소 포집 연구소를 설립해
기술 발전을 통한 환경 난제 해결에 도전하고 있습니다.
이는 반도체 업계의 탄소 배출 문제를 근본적으로 해결할 수 있는 시발점이 될 것입니다.
또한 업계 최초로 12나노급 32Gb DDR5 D램을 개발하며 동일 128Gb 모듈 기준,
16Gb D램을 탑재한 모듈 대비 약 10% 소비 전력을 개선시켰습니다.
앞으로 데이터를 저장하고 처리하는 과정에서 많은 에너지가 소모될 것입니다.
IT/데이터 시대에서 삼성전자의 반도체는 저전력, 고효율 메모리 제품 생산을 통한
전력 및 운영비용 감소로 탄소중립에도 기여할 것입니다.
결국, 탄소중립을 위해서는 탄소포집기술, 소비전력 효율을 개선,
친환경 에너지의 사용 같은 과학기술을 발전시키는 근본적인 문제해결과정이 필요합니다.
삼성전자는 앞으로도 사회적, 환경적 측면에서 함께 공존할 수 있는 과학기술을 고민해 나감으로써
글로벌 기업으로서 위상을 높여갈 수 있을 것입니다.

4. 지원한 직무 관련 본인이 갖고 있는 전문지식/경험(심화전공, 프로젝트, 논문, 공모전 등)을 작성하고,
이를 바탕으로 본인이 지원 직무에 적합한 사유를 구체적으로 서술해 주시기 바랍니다.
1000자 이내 (영문작성 시 2000자)

공정기술 직무에서는 공정 변수가 구조에 미치는 영향을 이해하고
최적화된 공정을 하는 것이 중요합니다.
[Al/Ti 200nm 다층 박막 형성 및 나노 경도 측정] ‘마그네트론 스퍼터’를 이용해
 Al/Ti 각각 100nm씩 200nm 다층 박막을 형성하여 나노 경도를 측정했습니다.
해당 연구는 스퍼터를 처음 셋업하면서 각 타겟의 증착 속도를 구하는 것과
Si-wafer에 증착된 물질의 미세구조를 이용해
 박막의 경도를 향상하는 두 가지 목표가 있었습니다.
박막의 두께에 따라 바뀌는 특성 때문에 형상두께는 200nm로 고정하고,
‘파센 법칙’과 ‘Thornton Zone Model’을 이용해서
공정변수를 인가전압과 작업압력, 기판 온도로 설정했습니다.
변수가 박막에 주는 영향을 확인하기 위해 경향성 test를 진행했습니다.
1. 기판 온도는 Al의 저융점 특성 때문에 600도 이상 올리는 것이 불가능해
Ti의 주상구조가 나타나는 온도인 400도로 설정했습니다.
2. 인가전압의 경우, 350W 이상일 때
Ar 이온의 에너지가 강해 스퍼터링율이 감소하여
증착 속도가 느려지는 현상으로 인해 300W로 설정했습니다.
3. 압력의 경우 증착 속도는 7mtorr에서 가장 높았지만, 결정성은 3mtorr에서 가장 좋았기 때문에
3mtorr로 설정했습니다.
총 76번의 실험과 시편 분석을 한 결과
‘인가전압 300W, 작업압력 3mTorr, 기판 온도 400도’ 조건으로
Al, Ti 각각 25.1, 19.8nm/min이란 증착속도를 구할 수 있었습니다.
구한 증착 속도로 본 실험에서 200nm의 다층 박막을
오차범위 5% 이내로 형성했고 나노 경도를 측정했습니다.
50mN 하중을 기준으로, 박막의 결정성이 존재할 때,
비정질일 때보다 나노 경도가 약 1300Hv로 31% 향상됐습니다.
이 경험을 통해 공정 최적화를 위한 변수에 대한 이해도를 높일 수 있었습니다.
 Depo 공정기술 엔지니어로서 공정 최적화를 통한 안정적 수율확보에 기여하겠습니다."""})

“통과”

자소서 평가봇을 랭그래프로 설계해본다.

from typing import Dict, Any, Literal, List
from langgraph.graph import StateGraph, START, END
from pydantic import BaseModel, Field  

class HRBotState(BaseModel):
    cover_letter : str = Field(default='', description="사용자가 입력한 자기소개서")
    result : str = Field(default='', description='자기소개서의 결과 값')
    response : str = Field(default='', description='최종 응답 메시지')
def analyze_hr(state: HRBotState ) -> Dict[str, Any]:
    cover_letter = state.cover_letter
    rt = chain.invoke({'resume' : cover_letter})
    return {'result' : rt}
def pass_response(state: HRBotState) -> Dict[str, Any]:
    return {'response' : '다음 진행 상태를 확인하세요'}
def failed_response(state : HRBotState) -> Dict[str, Any]:
    hr_persona= """
    당신은 회사 인사 담당자입니다. 아래 내용은 지원자의 이력서입니다. 통과하지 못한 이유를 자세히 설명할 것
    {resume}
    아래 조건을 만족하는 자소서를 판단할 때 사용한 규칙이다.

    ##기본 평가항목
    - 질문 항목에 대한 답변으로 적절한가?
    - 지원자의 지원동기 등 입사에 대한 열의가 있는가?
    - 핵심가치 및 인재상에 부합하는 인재인가?
    - 지원 직무에 대한 역량은 갖추었는가?
    - 예비 사회인으로서 다양한 경험을 하였는가?
    - 팀워크, 리더십, 갈등해결 등 대인관계능력을 갖추었는가?
    - 지원 기업에 대한 이해도를 갖추고자 노력하였는가?
    - 다양한 변화에 대응할 인문학적 소양 등을 갖추었는가?

    ##가점요인
    - 역량 중심으로 고민을 담아 성의껏 작성하였는가?
    - 경험 위주로 근거를 갖고 구체적으로 기술하고 있는가?
    - 표현능력(논리력, 문장력 등)이 우수한가?

    ##감점요인
    - 동일 내용을 반복하고 있는가?
    - 제시된 분량에 턱없이 모자라는가? (70% 이하)
    - 원론적이고 추상적인 내용으로 기술하고 있는가?
    - 질문과 무관한 내용이 담겨 있는가?
    - 오타 및 부정확한 표현은 없는가?
    - 타사 지원서를 복사하여 지원 회사명이 잘못 기재하였는가?
    """
    prompt = PromptTemplate(
        template=hr_persona
    )
    chain2 = prompt | llm | StrOutputParser()

    rt = chain2.invoke({'resume' :state.cover_letter })
    return {'response' : rt}

def route_by_hr(state : HRBotState) -> Literal['pass', 'fail']:
    result = state.result
    print(f"라우팅 : {result}")

    if result == "통과":
        return "pass"
    else:
        return "fail"
def create_hr_bot_graph():
    workflow = StateGraph(HRBotState)

    workflow.add_node("analyze_hr", analyze_hr)
    workflow.add_node('pass', pass_response)
    workflow.add_node('fail', failed_response)

    workflow.add_edge(START, 'analyze_hr')
    # 라우팅
    workflow.add_conditional_edges(
        'analyze_hr',
        route_by_hr,
        {
            'pass' : "pass",
            "fail" : "fail"
        }
    )

    workflow.add_edge("pass", END)
    workflow.add_edge("fail", END)

    return workflow.compile()
    
app = create_hr_bot_graph()

cover_letter = """ 1) 성장괴정
저는 2001년 2월 막내로 태머냤습니다. 저의 청소년시절은 그리 밝은 추역
은 별로 없습니다. 부모님미 유년기때의 마음의 샴처로 인해서 서로 미해하지 못하고
오해로 인해서 저의 학장시절은 그리 행복한 추먹은 없습니다. 그러나 가정의 불화로
인해서 방황하며 힘들머 하고 있을 때 저메계 힙미 되머주신 교회 전도사님미 계셨습니
다. 그분의 위로와 격려로 인해서 고등학교를 졸업 후 직장을 다니면서 꿈을 가지계 되
였고, 직장을 퇴사한 후 대전에 있는 침례신학대학교메서 사회복지학과를 미수하고, 가
속간의 화해를 위해서 부모님곁메 있었습니다. 그것미 사회복지를 배문자로서 그리고
저의 꿈을 위한 사회복지사로서의 시작미있습니다.
2) 성격의 장. 단점
저의 장점과 단정은 같은 것입니다. 먼저 저의 단정은 무슨일을 하는데 있머서 생각
미 많고 너무 신중하다는 것입니다. 그로 인해서 결정을 내리는데 시간미 걸린다는 것
입니다. 그에 반대 장점은 결정을 내련 것에 대해서 나 자신을 믿고 그 일에 대해서 책
임을 지고 행한다는 것입니다.
3) 입사 지원동기
저는 사회복지 문먀메 관심미 많습니다. 특히 저의 성장 과정을 통해서 가족의 화목
미 얼마나 중요함을 매달게 되었습니다. 가족구성들의 각자 자기의 위치에서 누리며 살
아가며 서로 격려하|주며 살아가는 것미 건강한 가족미라 생각하며, 건강한 가족만미 건
강한 사회를 미끌머간다고 생각햘니다.
또한 대학재학시절 경기북부멍마일시보호소메서 자원봉사를 대본 경험미 있습니다.
그때 멍아들미 머떻계 보호소에 들머오는지를 보았고, 머떻계 AH로문 가정을 만나서 가
는 모습을 보면서 저의 작은 손길미 마미들메계 도움미 되었으면 하는 마음으로 지원하
계 되었습니다.
4) 장래의 희망 또는 포부
저의 장래의 희망은 위기 가정에 도움을 주는 사회복지사가 되는 것입니다. 제가 -
을 수 없는 상황께서 웃으면서 살아가고 있는 것처럼 단 한몀미라도 더 웃으며 실파가
도록 도와 줄 것입니다."""
state = HRBotState(cover_letter=cover_letter)
app.invoke(state)

profile
지루하게 선명하기보다는 흐릿해도 흥미롭게

0개의 댓글