LangSmith ์ต์ ๊ธฐ์ค ์ด๋ณด์ ์๋ฒฝ ๊ฐ์ด๋: ํ์ ์ฌ์ ์ค์น, @traceable ๋ฐ์ฝ๋ ์ดํฐ ๋ฐ wrap_openai ๋ฌด์ค์ ํธ๋ ์ด์ฑ, LangChain/LangGraph ์๋ ์ฐ๋, ๋ฐ์ดํฐ์
๊ตฌ์ถ ๋ฐ evaluate() LLM-as-a-Judge ํ๊ฐ, Prompt Hub ํ๋กฌํํธ ๋ฒ์ ๊ด๋ฆฌ, ์ ์ ํผ๋๋ฐฑ ์์ง ๋ฐ ํ๋ก๋์
๋ชจ๋ํฐ๋ง ์ค์ ๊ฐ์ด๋
## 1. ํ์ ์ฌ์ ์ค์น ์๊ฑด & ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ (Prerequisites)
### ๐ป ์์คํ
์ค์น ๋ฐ ์คํ ์๊ตฌ์ฌ์ (System Requirements)
| ํญ๋ชฉ | ์ต์ ์ฌ์ (Minimum) | ๊ถ์ฅ ์ฌ์ (Recommended) | ์ฐธ๊ณ ๋ฐ ๋น๊ณ |
| :--- | :--- | :--- | :--- |
| **์ด์์ฒด์ (OS)** | Linux, macOS, Windows 10/11 | Ubuntu 22.04 LTS, macOS 13+, Windows 11 | ๋ชจ๋ ๋ชจ๋ 64-bit OS |
| **CPU** | 1 ์ฝ์ด ์ด์ | 2 ์ฝ์ด ~ 4 ์ฝ์ด ์ด์ | ๋ฐฑ๊ทธ๋ผ์ด๋ ๋น๋๊ธฐ ํธ๋ ์ด์ค ์ ์ก ์ค๋ ๋ ์ฒ๋ฆฌ |
| **๋ฉ๋ชจ๋ฆฌ (RAM)** | 1 GB ์ด์ | 2 GB ~ 4 GB ์ด์ | ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ(Run) ๋ฒํผ๋ง ๋ฐ ๋ฐ์ดํฐ์
์บ์ |
| **๋์คํฌ (Storage)** | 500 MB ์ด์์ ์ฌ์ ๊ณต๊ฐ | 2 GB ์ด์์ ์ฌ์ ๊ณต๊ฐ | LangSmith SDK ๋ฐ ๋ก์ปฌ ์บ์ |
| **๋คํธ์ํฌ / ๋ฐํ์** | Python 3.9 ~ 3.12+, langsmith SDK | ์์๋ฐ์ด๋ HTTPS ํต์ (`api.smith.langchain.com:443`) | LangSmith ๊ณ์ ๋ฐ API Key (`lsv2_pt_...`) |
**LangSmith**๋ LangChain ์ํ๊ณ์์ ์ ๊ณตํ๋ **LLM ์์ง๋์ด๋ง ๋ฐ ํตํฉ ๊ด์ธก์ฑ(Observability) ํ๋ซํผ**์
๋๋ค. LLM ์ ํ๋ฆฌ์ผ์ด์
์ ๋ชจ๋ ์
์ถ๋ ฅ ํธ์ถ ํ๋ฆ(ํธ๋ ์ด์ค), ์ง์ฐ ์๊ฐ, ํ ํฐ ์๋ชจ๋, ๋น์ฉ, ์๋ ํ๊ฐ(Evaluation), ๊ณจ๋ ๋ฐ์ดํฐ์
๋ฐ ํ๋กฌํํธ ๋ฒ์ ๊ด๋ฆฌ๋ฅผ ํ๋์ ์น ๋์๋ณด๋์์ ์๋ฒฝํ๊ฒ ์ถ์ ํ ์ ์์ต๋๋ค.
์ด๋ณด์๋ ์ค๋ฅ ์์ด ์ฒ์๋ถํฐ ๋๊น์ง ์ค์ตํ ์ ์๋๋ก ํ์ ์ฌ์ ํ๊ฒฝ ๊ตฌ์ฑ๋ถํฐ ์์ํฉ๋๋ค.
---
### ๐ 1. Python ๋ฒ์ & ๊ฐ์ํ๊ฒฝ (Virtual Environment)
LangSmith SDK๋ **Python 3.9 ~ 3.12+ (๊ถ์ฅ: Python 3.11 ๋๋ 3.12)** ํ๊ฒฝ์ ์ง์ํฉ๋๋ค. ์ ์ญ ํจํค์ง ์ค์ผ ๋ฐ ๋ฒ์ ์ถฉ๋์ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ฐ์ํ๊ฒฝ ์์ฑ์ด **ํ์**์
๋๋ค.
```bash
# 1. ํ์ค venv ๊ฐ์ํ๊ฒฝ ์์ฑ
python -m venv .venv
# 2. ๊ฐ์ํ๊ฒฝ ํ์ฑํ (Windows PowerShell)
# โป ์ต์ด 1ํ ์คํฌ๋ฆฝํธ ์คํ ๊ถํ ๋ถ์ฌ: Set-ExecutionPolicy Unrestricted -Scope Process
.venv\Scripts\Activate.ps1
# ๊ฐ์ํ๊ฒฝ ํ์ฑํ (macOS / Linux / WSL)
source .venv/bin/activate
# 3. ๋๋ ์ฐจ์ธ๋ ์ด๊ณ ์ ํจํค์ง ๊ด๋ฆฌ์ uv ์ฌ์ฉ ์ (๊ฐ๋ ฅ ์ถ์ฒ)
pip install uv
uv venv
# Windows: .venv\Scripts\activate / macOS: source .venv/bin/activate
uv pip install langsmith langchain-core langchain-openai openai python-dotenv
```
---
### ๐ ๏ธ 2. OS๋ณ C/C++ ๋ค์ดํฐ๋ธ ๋น๋ ๋๊ตฌ (Native Compiler)
`pydantic-core`, `tiktoken` ๋ฑ ๊ณ ์ฑ๋ฅ C++/Rust ๋ฐ์ธ๋ฉ ํจํค์ง ์ค์น ์ ๋ฐ์ํ ์ ์๋ ์ปดํ์ผ ์ค๋ฅ๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๊ถ์ฅ๋ฉ๋๋ค.
| ์ด์์ฒด์ (OS) | ํ์ ์ค์น ๋๊ตฌ | ์ค์น ๋ฐฉ๋ฒ |
| :--- | :--- | :--- |
| **Windows** | Microsoft C++ Build Tools | [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) ์ค์น ์ **"C++๋ฅผ ์ฌ์ฉํ ๋ฐ์คํฌํฑ ๊ฐ๋ฐ"** ์ํฌ๋ก๋ ์ฒดํฌ |
| **macOS** | Xcode Command Line Tools | ํฐ๋ฏธ๋์์ `xcode-select --install` ์คํ |
| **Linux (Ubuntu/Debian)** | `build-essential` & `python3-dev` | `sudo apt update && sudo apt install -y build-essential python3-dev` |
---
### ๐ฆ 3. ํ์ ํต์ฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ผ๊ด ์ค์น
```bash
# 1. LangSmith ์ฝ์ด SDK
pip install langsmith
# 2. ์ค์ต์ ์ฌ์ฉํ LangChain ๋ฐ OpenAI SDK
pip install langchain-core langchain-openai openai
# 3. ํ๊ฒฝ๋ณ์ ๊ด๋ฆฌ ๋ฐ ํ๋กฌํํธ ํ๋ธ ์ฐ๋
pip install python-dotenv langchainhub
```
---
### ๐ 4. LangSmith ๊ณ์ ์์ฑ ๋ฐ API ํค ๋ฐ๊ธ
1. [https://smith.langchain.com](https://smith.langchain.com) ์ ์ ์ํ์ฌ ๋ฌด๋ฃ ํ์๊ฐ์
์ ์๋ฃํฉ๋๋ค.
2. ์ข์ธก ํ๋จ **Settings (์ค์ )** โ **API Keys** ํญ์ผ๋ก ์ด๋ํฉ๋๋ค.
3. **Create API Key** ๋ฒํผ์ ํด๋ฆญํ์ฌ ์ ํค(`lsv2_pt_...` ํ์)๋ฅผ ์์ฑํ๊ณ ๋ณต์ฌํฉ๋๋ค.
---
### โ๏ธ 5. ํ๊ฒฝ ๋ณ์ ๊ตฌ์ฑ (`.env`)
ํ๋ก์ ํธ ๋ฃจํธ ํด๋์ `.env` ํ์ผ์ ์์ฑํ๊ณ ์๋ ์ค์ ๊ฐ์ ์
๋ ฅํฉ๋๋ค:
```bash
# 1. LangSmith ์ค์๊ฐ ๊ด์ธก ํ์ฑํ (ํ์)
LANGCHAIN_TRACING_V2="true"
# 2. LangSmith API ํค (lsv2_pt_...)
LANGCHAIN_API_KEY="lsv2_pt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 3. ํธ๋ ์ด์ค๋ฅผ ๊ธฐ๋กํ ํ๋ก์ ํธ ์ด๋ฆ (์ง์ ํ์ง ์์ผ๋ฉด 'default' ํ๋ก์ ํธ์ ์ ์ฅ)
LANGCHAIN_PROJECT="lucky-langsmith-dev"
# 4. LangSmith ์์ง ์๋ํฌ์ธํธ URL
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
# 5. LLM ํธ์ถ์ ์ฌ์ฉํ OpenAI API ํค
OPENAI_API_KEY="sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
Python ์ฝ๋ ์์ ๋ถ๋ถ์์ `.env` ํ์ผ์ ์ฆ์ ๋ก๋ํ ์ ์๋๋ก ํฉ๋๋ค:
```python
from dotenv import load_dotenv
load_dotenv()
```
---
## 2. ์ง๊ด์ ์ธ ๋ฉํ ๋ชจ๋ธ (Mental Model & Architecture)
LangSmith๋ ๋จ์ํ ๋ก๊ทธ ๋ทฐ์ด๊ฐ ์๋๋๋ค. LLM ์ ํ๋ฆฌ์ผ์ด์
์ **๊ฐ๋ฐ โ ๋๋ฒ๊น
โ ํ๊ฐ โ ๋ชจ๋ํฐ๋ง** ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด์ ๊ด์ฅํ๋ ํ๋ซํผ์
๋๋ค.
```text
[LangSmith ์ ์ฒด ๋ผ์ดํ์ฌ์ดํด ์ํคํ
์ฒ]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Tracing & Observability โ
โ โ
โ User App (LangChain, LangGraph, ์์ OpenAI, ์ปค์คํ
Python) โ
โ โ โ
โ โโโ [Parent Run: Agent] (์ ์ฒด ์คํ ์๊ฐ, ์ด ๋น์ฉ) โ
โ โ โโโ [Child Run: Prompt Template] (ํ๋กฌํํธ ๊ตฌ์ฑ) โ
โ โ โโโ [Child Run: LLM Call] (์
์ถ๋ ฅ ํ ํฐ, ์ง์ฐ ์๊ฐ, ์์ฑ๋ฌธ) โ
โ โ โโโ [Child Run: Tool Execution] (๋๊ตฌ ํ๋ผ๋ฏธํฐ ๋ฐ ๋ฐํ๊ฐ) โ
โ โ โ
โ โผ (๋ฐฑ๊ทธ๋ผ์ด๋ ๋น๋๊ธฐ ์ ์ก) โ
โ LangSmith Web Dashboard (Runs, Latency, Token/Cost, Error Stack) โ
โโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโ
โ โ
โผ (ํด๋ฆญ ํ ๋ฒ์ผ๋ก ์ถ์ถ) โผ (ํ์ง ๋ชจ๋ํฐ๋ง)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. Datasets & Evaluation โ โ 3. Feedback & Prompt Hub โ
โ โ โ โ
โ โข Golden Dataset ์์ฑ & ๊ด๋ฆฌ โ โ โข User Feedback (๐/๐, ๋ณ์ ) โ
โ โข evaluate() ์๋ ๋ฐฐ์น ํ๊ฐ ์คํ โ โ โข Prompt Hub ๋ฒ์ ์ ์ด (Push/Pull)โ
โ โข LLM-as-a-Judge ์๋ ์ฑ์ โ โ โข Annotation Queue (ํด๋จผ ๋ฆฌ๋ทฐ) โ
โ โข ํ๋กฌํํธ/๋ชจ๋ธ ๋ณ๊ฒฝ ์ A/B ๋น๊ต โ โ โข ์ค์๊ฐ ์๋ฆผ & ๋ฉํ๋ฐ์ดํฐ ํํฐ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### ๐ LangSmith์ 4๋ ํต์ฌ ๊ฐ๋
1. **Run (์คํ ๋จ์)**: ์ฒด์ธ, LLM ํธ์ถ, ๋๊ตฌ ์คํ ๊ฐ๊ฐ์ ๋จ์ผ ์์
์คํฌ(Span).
2. **Trace (ํธ๋ ์ด์ค ํธ๋ฆฌ)**: ํ๋์ ์ต์ข
์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์คํ๋ ๋ชจ๋ Run๋ค์ ๊ณ์ธต์ ๋ถ๋ชจ-์์ ํธ๋ฆฌ ๊ตฌ์กฐ.
3. **Dataset (๋ฐ์ดํฐ์
)**: ์
๋ ฅ๊ณผ ์ ๋ต(Ground Truth)์ ๋ชจ์. ํ๋กฌํํธ ๊ฐ์ ์ด๋ ๋ชจ๋ธ ๊ต์ฒด ์ ํ๊ท ํ
์คํธ(Regression Test)์ ๊ธฐ์ค์ .
4. **Evaluator (ํ๊ฐ๊ธฐ)**: LLM ์ถ๋ ฅ์ ์ ํ์ฑ, ํ ๋ฃจ์๋ค์ด์
์ ๋ฌด, ์๋ต ๊ธธ์ด ๋ฑ์ ์๋์ผ๋ก ์ฑ์ ํ๋ Python ํจ์ ๋๋ LLM ์ฌ์ฌ์์(LLM-as-a-Judge).
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] ํฌ๋ก์๋: ์ฝ๋ ๋ณ๊ฒฝ ์๋ ๋ฌด์ค์ (Zero-Code) ์๋ ํธ๋ ์ด์ฑ
LangChain์ด๋ LangGraph๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, **๋จ ํ ์ค์ ๋ก๊น
์ฝ๋ ์์ด๋** ํ๊ฒฝ ๋ณ์ ์ค์ ๋ง์ผ๋ก ๋ชจ๋ ํธ์ถ ํ๋ฆ์ด LangSmith์ ์๋ ๊ธฐ๋ก๋ฉ๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`01_zero_code_trace.py`)
```python
import os
from dotenv import load_dotenv
# 1. ํ๊ฒฝ ๋ณ์ ๋ก๋ (.env์ ์ค์ ๋ LANGCHAIN_TRACING_V2="true" ํ์ฑํ)
load_dotenv()
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
def run_zero_code_pipeline():
print("=" * 60)
print("1๋จ๊ณ: LangSmith ๋ฌด์ค์ (Zero-Code) ์๋ ํธ๋ ์ด์ฑ ์คํ")
print(f"ํ์ฌ ๊ธฐ๋ก ํ๋ก์ ํธ: {os.getenv('LANGCHAIN_PROJECT', 'default')}")
print("=" * 60)
# 2. ํ๋กฌํํธ ํ
ํ๋ฆฟ ์์ฑ
prompt = ChatPromptTemplate.from_messages([
("system", "๋น์ ์ IT ๊ธฐ์ ์ ์๊ธฐ ์ฝ๊ฒ 3์ค๋ก ๋น์ ํด ์ฃผ๋ ๊ฐ๋ฐ ์ ๋ฌธ๊ฐ์
๋๋ค."),
("human", "{technology} ๊ธฐ์ ์ด ๋ฌด์์ธ์ง ํต์ฌ์ ์ค๋ช
ํด ์ฃผ์ธ์.")
])
# 3. ๋ชจ๋ธ ๋ฐ ์ถ๋ ฅ ํ์ ๊ตฌ์ฑ
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
output_parser = StrOutputParser()
# 4. LCEL ํ์ดํ๋ผ์ธ ์ฐ๊ฒฐ (Prompt โ Model โ Parser)
chain = prompt | model | output_parser
# 5. ์ฒด์ธ ์คํ (LangSmith๊ฐ ์๋์ผ๋ก ์ ์ฒด ํ์ดํ๋ผ์ธ ์คํฌ์ ์บก์ฒ)
tech_name = "Docker"
print(f"\n[์ง๋ฌธ ์ ์ก] ๊ธฐ์ ๋ช
: {tech_name}\n")
response = chain.invoke({"technology": tech_name})
print("[LLM ๋ต๋ณ ๊ฒฐ๊ณผ]")
print(response)
print("\n" + "=" * 60)
print("โ
์คํ ์๋ฃ! smith.langchain.com ์์ ์ค์๊ฐ ํธ๋ ์ด์ค๋ฅผ ํ์ธํ์ธ์.")
print("=" * 60)
if __name__ == "__main__":
run_zero_code_pipeline()
```
### ๐ป ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
============================================================
1๋จ๊ณ: LangSmith ๋ฌด์ค์ (Zero-Code) ์๋ ํธ๋ ์ด์ฑ ์คํ
ํ์ฌ ๊ธฐ๋ก ํ๋ก์ ํธ: lucky-langsmith-dev
============================================================
[์ง๋ฌธ ์ ์ก] ๊ธฐ์ ๋ช
: Docker
[LLM ๋ต๋ณ ๊ฒฐ๊ณผ]
1. ๋์ปค๋ ํ๋ก๊ทธ๋จ์ ์คํํ๋ ๋ฐ ํ์ํ ๋ชจ๋ ๊ฒ์ ํ๋๋ก ํฌ์ฅํ '๊ท๊ฒฉํ๋ ์ด์ฟ์ง ์ปจํ
์ด๋'์
๋๋ค.
2. ์ด๋ค ์ปดํจํฐ ํ๊ฒฝ(๋ด ๋
ธํธ๋ถ, ์๋ฒ, ํด๋ผ์ฐ๋)์์๋ "๋ด ์ปดํจํฐ์์๋ ์ ๋๋๋ฐ..."๋ผ๋ ๋ฌธ์ ์์ด ๋์ผํ๊ฒ ์คํ๋ฉ๋๋ค.
3. ๋ฌด๊ฑฐ์ด ๊ฐ์๋จธ์ (VM)๊ณผ ๋ฌ๋ฆฌ ์ด์์ฒด์ ๋ฅผ ํต์งธ๋ก ๋ณต์ ํ์ง ์๊ณ ์ปค๋์ ๊ณต์ ํ์ฌ ๊ฐ๋ณ๊ณ ์ด๊ณ ์์ผ๋ก ์๋ํฉ๋๋ค.
============================================================
โ
์คํ ์๋ฃ! smith.langchain.com ์์ ์ค์๊ฐ ํธ๋ ์ด์ค๋ฅผ ํ์ธํ์ธ์.
============================================================
```
> **๋์๋ณด๋ ํ์ธ ํฌ์ธํธ**: [smith.langchain.com](https://smith.langchain.com) ํ๋ก์ ํธ ๋ชฉ๋ก์์ `lucky-langsmith-dev`๋ก ๋ค์ด๊ฐ๋ฉด ๋ฐฉ๊ธ ์คํ๋ `RunnableSequence` ํธ๋ฆฌ๊ฐ ์์ฑ๋์ด ์์ผ๋ฉฐ, ํ๋กฌํํธ ์
๋ ฅ๊ฐ, ์ฌ์ฉ๋ ํ ํฐ ์(Prompt + Completion), ์์ ์๊ฐ(Latency, ์: 850ms), ์ค์๊ฐ ์ถ์ ๋น์ฉ($0.0001 ๋ฑ)์ด ์๋ฒฝํ๊ฒ ์๊ฐํ๋ฉ๋๋ค.
---
## 4. [2๋จ๊ณ] ์์ Python & ๋น(้) LangChain ํ๊ฒฝ ํธ๋ ์ด์ฑ (`@traceable` & `wrap_openai`)
LangChain ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ์ง ์๊ณ **์์ Python ํจ์**๋ **์์ OpenAI ๊ณต์ SDK**๋ง ์ฌ์ฉํ๋ ํ๋ก์ ํธ์์๋ LangSmith์ ๊ฐ๋ ฅํ ๊ด์ธก ๊ธฐ๋ฅ์ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ต๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`02_traceable_decorator.py`)
```python
import os
import json
from dotenv import load_dotenv
load_dotenv()
from openai import OpenAI
from langsmith import traceable
from langsmith.wrappers import wrap_openai
# 1. wrap_openai: ์์ OpenAI ํด๋ผ์ด์ธํธ๋ฅผ ๊ฐ์ธ์ ์
์ถ๋ ฅ ๋ฐ ํ ํฐ ์๋ฅผ ์๋ ์์ง
client = wrap_openai(OpenAI())
# 2. ์ปค์คํ
๋น์ฆ๋์ค ํจ์์ @traceable ๋ฐ์ฝ๋ ์ดํฐ ์ ์ฉ
@traceable(name="์ ์ฒ๋ฆฌ_์ฌ์ฉ์_์
๋ ฅ_๊ฒ์ฆ", run_type="tool", tags=["preprocessing", "validation"])
def sanitize_user_input(user_query: str) -> str:
"""์ฌ์ฉ์ ์
๋ ฅ ๋ฌธ์์ด์ ๊ณต๋ฐฑ์ ์ ๊ฑฐํ๊ณ ๊ธ์น์ด๋ฅผ ํํฐ๋งํ๋ ๋๊ตฌ ํจ์"""
cleaned = user_query.strip()
if len(cleaned) < 2:
raise ValueError("์
๋ ฅ ์ง์์ด๊ฐ ๋๋ฌด ์งง์ต๋๋ค.")
return cleaned
@traceable(name="์ฌ๋ด_์ง์_๊ฒ์๊ธฐ", run_type="retriever", tags=["mock-rag", "search"])
def retrieve_internal_docs(query: str) -> list[str]:
"""๊ฐ์ด RAG ๊ฒ์๊ธฐ ์๋ฎฌ๋ ์ด์
"""
mock_knowledge_base = {
"๋ฐฐํฌ": "์ฌ๋ด ๋ฐฐํฌ๋ ๋งค์ฃผ ๋ชฉ์์ผ ์คํ 3์ CI/CD ํ์ดํ๋ผ์ธ์ ํตํด ์งํ๋ฉ๋๋ค.",
"ํด๊ฐ": "์ฐ์ฐจ ์ ์ฒญ์ ์ฌ๋ด ๊ทธ๋ฃน์จ์ด ์ ์๊ฒฐ์ฌ๋ฅผ ํตํด ์ต์ 3์ผ ์ ์ ์ถํด์ผ ํฉ๋๋ค.",
"๋ณด์": "์ฌ๋ด Wi-Fi ์ ์ ์์๋ 2๋จ๊ณ OTP ์ธ์ฆ์ด ํ์์
๋๋ค."
}
for keyword, doc in mock_knowledge_base.items():
if keyword in query:
return [doc]
return ["๊ด๋ จ ๋ฌธ์๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค."]
# 3. ์ต์์ ์ค์ผ์คํธ๋ ์ด์
ํจ์ (Parent Span)
@traceable(
name="๊ณ ๊ฐ์ง์_AI_์ด์์คํดํธ",
run_type="chain",
tags=["customer-support", "v1.2"],
metadata={"environment": "development", "user_tier": "VIP"}
)
def customer_support_pipeline(raw_query: str) -> str:
# ํ์ ์คํฌ 1: ์
๋ ฅ ๊ฒ์ฆ
clean_query = sanitize_user_input(raw_query)
# ํ์ ์คํฌ 2: ๋ฌธ์ ๊ฒ์
retrieved_context = retrieve_internal_docs(clean_query)
# ํ์ ์คํฌ 3: OpenAI API ํธ์ถ (wrap_openai์ ์ํด ์๋์ผ๋ก LLM Run์ผ๋ก ๊ธฐ๋ก)
system_instruction = (
"๋น์ ์ ์น์ ํ ์ฌ๋ด ์ง์ AI์
๋๋ค. "
f"์ฐธ๊ณ ๋ฌธ์: {retrieved_context[0]}\n์ฐธ๊ณ ๋ฌธ์์ ๊ทผ๊ฑฐํ์ฌ ์ง๋ฌธ์ ๋ตํ์ธ์."
)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_instruction},
{"role": "human", "content": clean_query}
],
temperature=0.0
)
return response.choices[0].message.content
def main():
print("=" * 60)
print("2๋จ๊ณ: ์์ Python & OpenAI ํธ๋ ์ด์ฑ (@traceable & wrap_openai)")
print("=" * 60)
sample_query = " ์ด๋ฒ ์ฃผ ๋ฐฐํฌ ์ผ์ ์ด ์ด๋ป๊ฒ ๋๋์? "
print(f"\n[์ฌ์ฉ์ ์๋ณธ ์
๋ ฅ]: '{sample_query}'\n")
result = customer_support_pipeline(sample_query)
print("[AI ์ต์ข
์๋ด ๋ต๋ณ]")
print(result)
print("\n" + "=" * 60)
print("โ
๊ณ์ธต์ ์คํฌ ํธ๋ฆฌ(Parent โ Children)๊ฐ LangSmith์ ๊ธฐ๋ก๋์์ต๋๋ค.")
print("=" * 60)
if __name__ == "__main__":
main()
```
### ๐ป ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
============================================================
2๋จ๊ณ: ์์ Python & OpenAI ํธ๋ ์ด์ฑ (@traceable & wrap_openai)
============================================================
[์ฌ์ฉ์ ์๋ณธ ์
๋ ฅ]: ' ์ด๋ฒ ์ฃผ ๋ฐฐํฌ ์ผ์ ์ด ์ด๋ป๊ฒ ๋๋์? '
[AI ์ต์ข
์๋ด ๋ต๋ณ]
์ด๋ฒ ์ฃผ ์ฌ๋ด ๋ฐฐํฌ๋ ๋ชฉ์์ผ ์คํ 3์์ CI/CD ํ์ดํ๋ผ์ธ์ ํตํด ์งํ๋ ์์ ์
๋๋ค. ์ถ๊ฐ๋ก ๊ถ๊ธํ ์ ์ด ์์ผ์๋ฉด ์ธ์ ๋ ๋ฌธ์ํด ์ฃผ์ธ์!
============================================================
โ
๊ณ์ธต์ ์คํฌ ํธ๋ฆฌ(Parent โ Children)๊ฐ LangSmith์ ๊ธฐ๋ก๋์์ต๋๋ค.
============================================================
```
> **๋์๋ณด๋ ํธ๋ฆฌ ๊ตฌ์กฐ ํ์ธ**:
> `๊ณ ๊ฐ์ง์_AI_์ด์์คํดํธ (chain)`
> โโโ `์ ์ฒ๋ฆฌ_์ฌ์ฉ์_์
๋ ฅ_๊ฒ์ฆ (tool)`
> โโโ `์ฌ๋ด_์ง์_๊ฒ์๊ธฐ (retriever)`
> โโโ `ChatOpenAI / ChatCompletion (llm)`
> ๊ฐ ์คํฌ๋ณ ๊ฐ๋ณ ์์ ์๊ฐ๊ณผ ํ๋ผ๋ฏธํฐ๊ฐ ์ ํํ ๋ถ๋ฆฌ๋์ด ๋ณ๋ชฉ ์ง์ ์ ์ฆ์ ์ฐพ์๋ผ ์ ์์ต๋๋ค.
---
## 5. [3๋จ๊ณ] ๊ณจ๋ ๋ฐ์ดํฐ์
๊ตฌ์ถ & ์๋ ํ๊ฐ (`evaluate()` & LLM-as-a-Judge)
ํ๋กฌํํธ๋ ๋ชจ๋ธ์ ๋ณ๊ฒฝํ์ ๋ "์ด์ ๋ณด๋ค ๋์์ก๋์ง, ํน์ ๊ธฐ์กด์ ์ ๋๋ ๊ฒ์ด ๋ง๊ฐ์ก๋์ง(Regression)" ํ์ธํ๋ ค๋ฉด **ํ
์คํธ ๋ฐ์ดํฐ์
**๊ณผ **์๋ํ๋ ํ๊ฐ(Evaluation)**๊ฐ ํ์์
๋๋ค.
LangSmith์ `evaluate()` ํจ์๋ ๊ณจ๋ ๋ฐ์ดํฐ์
์ ๋ชจ๋ ์์ ๋ฅผ ์คํํ๊ณ ์ปค์คํ
ํ๊ฐ๊ธฐ ๋ฐ LLM ์ฌ์ฌ์์์ ํตํด ์ ์๋ฅผ ์ง๊ณํฉ๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`03_dataset_and_evaluate.py`)
```python
import os
from dotenv import load_dotenv
load_dotenv()
from langsmith import Client, evaluate
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 1. LangSmith ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
client = Client()
DATASET_NAME = "lucky_qa_golden_dataset_v1"
def create_or_get_dataset():
"""ํ
์คํธ์ฉ ๊ณจ๋ ๋ฐ์ดํฐ์
์ด ์์ผ๋ฉด ์ ๊ท ์์ฑํ๊ณ ์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์
"""
if client.has_dataset(dataset_name=DATASET_NAME):
print(f"๊ธฐ์กด ๋ฐ์ดํฐ์
๋ฐ๊ฒฌ: '{DATASET_NAME}' ์ฌ์ฉ")
return client.read_dataset(dataset_name=DATASET_NAME)
print(f"์ ๊ท ๋ฐ์ดํฐ์
์์ฑ ์ค: '{DATASET_NAME}'")
dataset = client.create_dataset(
dataset_name=DATASET_NAME,
description="LuckyTechWorld ๊ฐ๋ฐ Q&A ํ๊ฐ์ฉ ๊ณจ๋ ๋ฒค์น๋งํฌ ๋ฐ์ดํฐ์
"
)
# ํ๊ฐ์ฉ ์์ ๋ฑ๋ก (์
๋ ฅ ์ง๋ฌธ ๋ฐ ๊ธฐ๋๋๋ ์ ๋ต/ํค์๋)
examples = [
(
{"question": "HTTP ์ํ ์ฝ๋ 404์ ์๋ฏธ๋ ๋ฌด์์ธ๊ฐ์?"},
{"expected": "์์ฒญํ ๋ฆฌ์์ค๋ฅผ ์๋ฒ์์ ์ฐพ์ ์ ์์ (Not Found)"}
),
(
{"question": "REST API์์ ๋ฆฌ์์ค๋ฅผ ์์ ํ ๋ ์ฃผ๋ก ์ฐ๋ HTTP ๋ฉ์๋ 2๊ฐ์ง๋?"},
{"expected": "PUT, PATCH"}
),
(
{"question": "SQL์์ ์ค๋ณต๋ ํ์ ์ ๊ฑฐํ๊ณ ์กฐํํ ๋ ์ฌ์ฉํ๋ ํค์๋๋?"},
{"expected": "DISTINCT"}
)
]
for inputs, outputs in examples:
client.create_example(
inputs=inputs,
outputs=outputs,
dataset_id=dataset.id
)
print(f"์ด {len(examples)}๊ฐ์ ํ๊ฐ ์์ ๊ฐ ๋ฐ์ดํฐ์
์ ๋ฑ๋ก๋์์ต๋๋ค.")
return dataset
# 2. ํ๊ฐ ๋์์ด ๋ ์ค์ ์ดํ๋ฆฌ์ผ์ด์
ํ๊ฒ ํจ์
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
prompt = ChatPromptTemplate.from_messages([
("system", "๋น์ ์ ์ํํธ์จ์ด ์์ง๋์ด๋ง ๋ฉด์ ๊ด์
๋๋ค. ๊ฐ๊ฒฐํ๊ณ ์ ํํ๊ฒ ๋ต๋ณํ์ธ์."),
("human", "{question}")
])
target_chain = prompt | llm | StrOutputParser()
def target_application(inputs: dict) -> dict:
"""evaluate()๊ฐ ํธ์ถํ ๋์ ํจ์"""
response_text = target_chain.invoke({"question": inputs["question"]})
return {"answer": response_text}
# 3. ์ปค์คํ
ํ๊ฐ๊ธฐ ์ ์ (1: ํค์๋ ํฌํจ ์ฌ๋ถ ๊ฒ์ฆ)
def keyword_match_evaluator(run, example) -> dict:
"""์์ ์ ๋ต์ ํต์ฌ ํค์๋๊ฐ ์ค์ ๋ต๋ณ์ ํฌํจ๋์ด ์๋์ง ์ฑ์ (0.0 ~ 1.0)"""
expected = example.outputs["expected"].lower()
actual = run.outputs["answer"].lower()
# ๋จ์ ํฌํจ ์ฌ๋ถ ๋๋ ๋ถํ ๋งค์นญ
keywords = [kw.strip() for kw in expected.replace("(", "").replace(")", "").split() if len(kw.strip()) > 1]
matched = sum(1 for kw in keywords if kw in actual)
score = matched / len(keywords) if keywords else 1.0
return {
"key": "keyword_coverage",
"score": round(score, 2),
"comment": f"์ด {len(keywords)}๊ฐ ํค์๋ ์ค {matched}๊ฐ ์ผ์น"
}
# 4. ์ปค์คํ
ํ๊ฐ๊ธฐ ์ ์ (2: ๋ต๋ณ ๊ธธ์ด ์ ์ ์ฑ ๊ฒ์ฆ)
def length_evaluator(run, example) -> dict:
actual = run.outputs["answer"]
is_concise = 10 <= len(actual) <= 300
return {
"key": "is_concise",
"score": 1.0 if is_concise else 0.0,
"comment": f"๋ต๋ณ ๊ธ์ ์: {len(actual)}์"
}
def main():
print("=" * 60)
print("3๋จ๊ณ: ๋ฐ์ดํฐ์
๊ตฌ์ถ & evaluate() ์๋ ํ๊ฐ ์คํ")
print("=" * 60)
# ๋ฐ์ดํฐ์
์ค๋น
create_or_get_dataset()
print("\n[ํ๊ฐ ๋ฐฐ์น ์์] ๋ฐ์ดํฐ์
์ ๋์์ผ๋ก target_application ์คํ ์ค...")
# evaluate() ์คํ: LangSmith ๋์๋ณด๋์ ์คํ(Experiment) ๊ฒฐ๊ณผ ์์ฑ
results = evaluate(
target_application,
data=DATASET_NAME,
evaluators=[keyword_match_evaluator, length_evaluator],
experiment_prefix="prompt-v1-gpt4omini",
max_concurrency=2
)
print("\n" + "=" * 60)
print("โ
ํ๊ฐ ์๋ฃ! ๋์๋ณด๋ Experiments ํญ์์ ๋งคํธ๋ฆญ์ค ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ์ธ์.")
print("=" * 60)
if __name__ == "__main__":
main()
```
### ๐ป ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
============================================================
3๋จ๊ณ: ๋ฐ์ดํฐ์
๊ตฌ์ถ & evaluate() ์๋ ํ๊ฐ ์คํ
============================================================
์ ๊ท ๋ฐ์ดํฐ์
์์ฑ ์ค: 'lucky_qa_golden_dataset_v1'
์ด 3๊ฐ์ ํ๊ฐ ์์ ๊ฐ ๋ฐ์ดํฐ์
์ ๋ฑ๋ก๋์์ต๋๋ค.
[ํ๊ฐ ๋ฐฐ์น ์์] ๋ฐ์ดํฐ์
์ ๋์์ผ๋ก target_application ์คํ ์ค...
View the evaluation results for experiment: 'prompt-v1-gpt4omini-xxxx' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
============================================================
โ
ํ๊ฐ ์๋ฃ! ๋์๋ณด๋ Experiments ํญ์์ ๋งคํธ๋ฆญ์ค ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ์ธ์.
============================================================
```
> **ํ๊ฐ ๊ฒฐ๊ณผ ๋ถ์**: ์ฝ์์ ์ถ๋ ฅ๋ ๋งํฌ๋ก ์ด๋ํ๋ฉด 3๊ฐ์ ํ
์คํธ ์ผ์ด์ค์ ๋ํด ๋ชจ๋ธ์ด ๋์ถํ `answer`, ๊ทธ๋ฆฌ๊ณ ๊ฐ ํ๊ฐ๊ธฐ(`keyword_coverage`, `is_concise`)์ ํ๊ท ์ ์(์: 1.0, 0.85)๊ฐ ํ
์ด๋ธ๋ก ์๊ฐํ๋ฉ๋๋ค. ํฅํ `gpt-4o`๋ก ๋ชจ๋ธ์ ๋ณ๊ฒฝํ๊ฑฐ๋ ํ๋กฌํํธ๋ฅผ ์์ ํ์ ๋ ๋ ์คํ์ ๋๋ํ ๋๊ณ ๋น๊ต(Diff)ํ ์ ์์ต๋๋ค.
---
## 6. [4๋จ๊ณ] Prompt Hub ์ฐ๋: ํ๋กฌํํธ ๋ฒ์ ๊ด๋ฆฌ ๋ฐ ํ ํ์
์ฝ๋ ์์ ๊ธด ํ๋กฌํํธ๋ฅผ ํ๋์ฝ๋ฉํ๋ฉด ์์ ํ ๋๋ง๋ค ์๋ฒ๋ฅผ ์ฌ๋ฐฐํฌํด์ผ ํฉ๋๋ค. **LangChain Prompt Hub**๋ฅผ ์ฌ์ฉํ๋ฉด ์น์์์ ํ๋กฌํํธ๋ฅผ ๋ฒ์ ๋ณ๋ก ๊ด๋ฆฌํ๊ณ ๋ฐํ์์ ๋์ ์ผ๋ก ๋น๊ฒจ์ฌ ์ ์์ต๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`04_prompt_hub_usage.py`)
```python
from dotenv import load_dotenv
load_dotenv()
from langchain import hub
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
def demonstrate_prompt_hub():
print("=" * 60)
print("4๋จ๊ณ: Prompt Hub ์ฐ๋ (ํ๋กฌํํธ ๋์ ๋ก๋ & ์คํ)")
print("=" * 60)
# 1. ์ปค๋ฎค๋ํฐ ๋๋ ๋ณธ์ธ ๊ณ์ ์ ๊ฒ์ฆ๋ ํ๋กฌํํธ ๊ฐ์ ธ์ค๊ธฐ (hub.pull)
# ์: rlm/rag-prompt (LangChain ๊ณต์ RAG ํ์ค ํ๋กฌํํธ)
prompt_handle = "rlm/rag-prompt"
print(f"\n[Prompt Hub] '{prompt_handle}' ํ
ํ๋ฆฟ ๋ค์ด๋ก๋ ์ค...")
pulled_prompt = hub.pull(prompt_handle)
print("\n[๊ฐ์ ธ์จ ํ๋กฌํํธ ํ
ํ๋ฆฟ ๊ตฌ์กฐ]")
for msg in pulled_prompt.messages:
print(f"- {msg.prompt.template}")
# 2. ๋ค์ด๋ก๋๋ฐ์ ํ๋กฌํํธ๋ก ์ฒด์ธ ๊ตฌ์ฑ
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
rag_chain = pulled_prompt | llm | StrOutputParser()
# 3. ๋ณ์ ์ฃผ์
๋ฐ ์คํ
sample_context = (
"LuckyTechWorld๋ ๊ฐ๋ฐ์๋ค์ ์ํ ์ต๊ณ ์ ์นํธ์ํธ์ ์น ๊ฐ๋ฐ ๋๊ตฌ๋ฅผ ๋ฌด๋ฃ๋ก ์ ๊ณตํ๋ ๊ธฐ์ ํ๋ธ์
๋๋ค."
)
sample_question = "LuckyTechWorld๋ ์ด๋ค ์๋น์ค์ธ๊ฐ์?"
print(f"\n[์ง๋ฌธ ์ ์ก]: {sample_question}\n")
output = rag_chain.invoke({
"context": sample_context,
"question": sample_question
})
print("[์๋ต ๊ฒฐ๊ณผ]")
print(output)
print("\n" + "=" * 60)
print("โ
Prompt Hub ์ฐ๋ ์ฑ๊ณต! ํ๋กฌํํธ ํ
ํ๋ฆฟ ๋ณ๊ฒฝ ์ฌํญ์ด ๋ฌด๋ฐฐํฌ๋ก ๋ฐ์๋ฉ๋๋ค.")
print("=" * 60)
if __name__ == "__main__":
demonstrate_prompt_hub()
```
### ๐ป ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
============================================================
4๋จ๊ณ: Prompt Hub ์ฐ๋ (ํ๋กฌํํธ ๋์ ๋ก๋ & ์คํ)
============================================================
[Prompt Hub] 'rlm/rag-prompt' ํ
ํ๋ฆฟ ๋ค์ด๋ก๋ ์ค...
[๊ฐ์ ธ์จ ํ๋กฌํํธ ํ
ํ๋ฆฟ ๊ตฌ์กฐ]
- You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
[์ง๋ฌธ ์ ์ก]: LuckyTechWorld๋ ์ด๋ค ์๋น์ค์ธ๊ฐ์?
[์๋ต ๊ฒฐ๊ณผ]
LuckyTechWorld๋ ๊ฐ๋ฐ์๋ฅผ ์ํ ์ ์ฉํ ์นํธ์ํธ์ ์น ๊ฐ๋ฐ ๋๊ตฌ๋ฅผ ๋ฌด๋ฃ๋ก ์ ๊ณตํ๋ ๊ธฐ์ ํ๋ธ์
๋๋ค.
============================================================
โ
Prompt Hub ์ฐ๋ ์ฑ๊ณต! ํ๋กฌํํธ ํ
ํ๋ฆฟ ๋ณ๊ฒฝ ์ฌํญ์ด ๋ฌด๋ฐฐํฌ๋ก ๋ฐ์๋ฉ๋๋ค.
============================================================
```
> **ํ๋กฌํํธ ํธ์(Push) ํ**: ๋ด๊ฐ ๋ง๋ ํ๋กฌํํธ๋ฅผ Hub์ ์
๋ก๋ํ๋ ค๋ฉด `hub.push("๋ด์กฐ์ง๋ช
/ํ๋กฌํํธ์ด๋ฆ", prompt_object)`๋ฅผ ํธ์ถํ๋ฉด ๋์๋ณด๋์ ์ฆ์ ์ ์ปค๋ฐ ๋ฒ์ ์ผ๋ก ์ ์ฅ๋ฉ๋๋ค.
---
## 7. [5๋จ๊ณ] ์ค์ ํผ๋๋ฐฑ ์์ง(User Feedback) & ๋ฐํ์ ๋ฉํ๋ฐ์ดํฐ ํ๊น
์ค์ ์ด์ ํ๊ฒฝ์์๋ ์ฌ์ฉ์๊ฐ ์น ํ๋ฉด์์ ๋๋ฅด๋ **์ข์์/์ซ์ด์(Thumbs Up/Down)** ๋๋ ๋ณ์ ํ๊ฐ๋ฅผ ํน์ LLM ํธ์ถ(Run ID)์ ์ฐ๊ฒฐํ์ฌ ํ์ง ์ ํ๋ฅผ ์ถ์ ํ ์ ์์ด์ผ ํฉ๋๋ค.
### ๐ ์์ ํ ์คํ ์ฝ๋ (`05_user_feedback_and_metadata.py`)
```python
from dotenv import load_dotenv
load_dotenv()
from langsmith import Client
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tracers.context import collect_runs
client = Client()
def run_feedback_pipeline():
print("=" * 60)
print("5๋จ๊ณ: ์ค์ ๋ฐํ์ ๋ฉํ๋ฐ์ดํฐ ์ฃผ์
& ์ฌ์ฉ์ ํผ๋๋ฐฑ(Feedback) ๋ฑ๋ก")
print("=" * 60)
# 1. ํ๋กฌํํธ ๋ฐ ๋ชจ๋ธ ๊ตฌ์ฑ
prompt = ChatPromptTemplate.from_messages([
("system", "์ฌ์ฉ์์ ์ง๋ฌธ์ ์น์ ํ๊ณ ์ ์คํ๊ฒ ๋ต๋ณํ์ธ์."),
("human", "{question}")
])
model = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
chain = prompt | model
# 2. ์คํ ๋ฉํ๋ฐ์ดํฐ์ ํ๊ทธ๋ฅผ ์ฃผ์
ํ๋ฉฐ ํธ์ถํ๊ณ , collect_runs()๋ก run_id๋ฅผ ์บก์ฒ
# (์ค๋ฌด์์๋ user_id, session_id, ํ๊ฒฝ, ํด๋ผ์ด์ธํธ OS ๋ฑ์ ์ฃผ์
)
invocation_config = {
"tags": ["production-chat", "mobile-app"],
"metadata": {
"user_id": "user_lucky_777",
"session_id": "sess_abc_1234",
"app_version": "2.4.0",
"tier": "enterprise"
}
}
user_query = "LangSmith์์ ํผ๋๋ฐฑ์ ์ด๋ป๊ฒ ์ฐ๊ฒฐํ๋์?"
with collect_runs() as cb:
response = chain.invoke({"question": user_query}, config=invocation_config)
# ์คํ๋ Run์ ๊ณ ์ ID ์ถ์ถ
captured_run = cb.traced_runs[0]
run_id = captured_run.id
print(f"\n[AI ์๋ต ์๋ฃ]: {response.content[:80]}...")
print(f"[์บก์ฒ๋ LangSmith Run ID]: {run_id}")
# 3. ์ฌ์ฉ์ ํผ๋๋ฐฑ ์๋ฎฌ๋ ์ด์
(์น ํ๋ก ํธ์๋์์ '์ข์์' ํด๋ฆญ ์ ๋ฐฑ์๋ API ํธ์ถ)
print("\n[์ฌ์ฉ์ ์ก์
๋ฐ์] ์ฌ์ฉ์๊ฐ '๐ ์ข์์' ๋ฒํผ์ ํด๋ฆญํ์ต๋๋ค.")
client.create_feedback(
run_id=run_id,
key="user_satisfaction",
score=1.0, # ์ข์์: 1.0, ์ซ์ด์: 0.0
comment="๋ต๋ณ์ด ๋งค์ฐ ์ ํํ๊ณ ์์ธํ์ฌ ํฐ ๋์์ด ๋์์ต๋๋ค.",
source_info={"source": "user_ui_thumbs_button"}
)
print("โ
ํผ๋๋ฐฑ ๋ฐ์ดํฐ๊ฐ ํด๋น Run์ ์ฑ๊ณต์ ์ผ๋ก ๊ฒฐํฉ๋์์ต๋๋ค.")
print("=" * 60)
if __name__ == "__main__":
run_feedback_pipeline()
```
### ๐ป ์์ ์ฝ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ
```text
============================================================
5๋จ๊ณ: ์ค์ ๋ฐํ์ ๋ฉํ๋ฐ์ดํฐ ์ฃผ์
& ์ฌ์ฉ์ ํผ๋๋ฐฑ(Feedback) ๋ฑ๋ก
============================================================
[AI ์๋ต ์๋ฃ]: LangSmith์์ ํผ๋๋ฐฑ์ ์ฐ๊ฒฐํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ๋จผ์ ๊ฐ LLM ์คํ์ Run ID๋ฅผ ์บก์ฒํ ๋ค...
[์บก์ฒ๋ LangSmith Run ID]: 7a9e102f-b258-4c17-8149-6f91f2be88dd
[์ฌ์ฉ์ ์ก์
๋ฐ์] ์ฌ์ฉ์๊ฐ '๐ ์ข์์' ๋ฒํผ์ ํด๋ฆญํ์ต๋๋ค.
โ
ํผ๋๋ฐฑ ๋ฐ์ดํฐ๊ฐ ํด๋น Run์ ์ฑ๊ณต์ ์ผ๋ก ๊ฒฐํฉ๋์์ต๋๋ค.
============================================================
```
> **๋์๋ณด๋ ํํฐ ํ์ฉ**: LangSmith ํ๋ฉด์์ `feedback_key:user_satisfaction and feedback_score:0`์ผ๋ก ํํฐ๋งํ๋ฉด, **์ฌ์ฉ์๊ฐ ๋ถ๋ง์ ํ์ํ ์ง๋ฌธ๋ง ๋ชจ์์** ์ฆ์ ๋ฌธ์ ์ ์ ๋ถ์ํ๊ณ ํ
์คํธ ๋ฐ์ดํฐ์
์ ์คํจ ์ผ์ด์ค๋ก ์ถ๊ฐํ ์ ์์ต๋๋ค.
---
## 8. ์ค๋ฌด ์ฃผ์์ฌํญ & ํ๋ก๋์
์ฒดํฌ๋ฆฌ์คํธ (Best Practices)
### ๐จ 1. ์ด๋ณด์๊ฐ ํํ ๊ฒช๋ ์ค์ & ํธ๋ฌ๋ธ์ํ
1. **ํธ๋ ์ด์ค๊ฐ ๋์๋ณด๋์ ์ ํ ๋ํ๋์ง ์๋ ๊ฒฝ์ฐ**:
- `LANGCHAIN_TRACING_V2="true"` ์ฒ ์๋ฅผ ํ์ธํ์ธ์ (`"True"`, `"1"`๋ ๊ฐ๋ฅ).
- `LANGCHAIN_API_KEY`๊ฐ ์ฌ๋ฐ๋ฅธ ๊ณ์ ์ ํ์ฑ ํค์ธ์ง ํ์ธํ์ธ์.
- ๋จ๊ธฐ ์คํฌ๋ฆฝํธ ์คํ ํ ์ฆ์ ํ๋ก์ธ์ค๊ฐ ์ข
๋ฃ๋๋ฉด ๋ฐฑ๊ทธ๋ผ์ด๋ ํ๊ฐ ์ ์ก๋๊ธฐ ์ ์ ๋๋ ์ ์์ต๋๋ค. ์คํฌ๋ฆฝํธ ๋์ `from langsmith import Client; Client().flush()`๋ฅผ ํธ์ถํ์ฌ ๋ฒํผ๋ฅผ ์์ ํ ๋น์ฐ์ธ์.
2. **๋น๋๊ธฐ(`asyncio`) ํ๊ฒฝ์์ ๋ถ๋ชจ-์์ ํธ๋ฆฌ ๋๊น**:
- ์์ Python์์ ํ์คํฌ๋ฅผ ๋น๋๊ธฐ๋ก ์์ฑํ ๋ ์ปจํ
์คํธ๊ฐ ์ ์ค๋ ์ ์์ต๋๋ค. ๋น๋๊ธฐ ํจ์์๋ ๋ฐ๋์ `@traceable`์ ๋ฌ์์ฃผ๊ฑฐ๋ `asyncio` ํ์คํฌ ๋ด์์ contextvars๊ฐ ๋ณต์ฌ๋๋๋ก ๊ตฌ์ฑํ์ธ์.
---
### ๐ก๏ธ 2. ๊ฐ์ธ์ ๋ณด ๋ฐ ๋ฏผ๊ฐ์ ๋ณด ๋ณดํธ (PII Masking)
ํ๋ก๋์
ํ๊ฒฝ์์ ์ฌ์ฉ์์ ๋น๋ฐ๋ฒํธ, ์ฃผ๋ฏผ๋ฒํธ, ์ ์ฉ์นด๋ ๋ฒํธ ๋ฑ์ด LangSmith ๋์๋ณด๋์ ํ๋ฌธ์ผ๋ก ๋จ์ง ์๋๋ก ๋ง์คํน ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
```python
# 1. ํน์ ํจ์์ ์
์ถ๋ ฅ์ LangSmith์ ๊ธฐ๋กํ์ง ์๊ณ ์จ๊น ์ฒ๋ฆฌ
@traceable(name="๊ฐ์ธ์ ๋ณด_์ฒ๋ฆฌ_ํจ์", hide_inputs=True, hide_outputs=True)
def process_sensitive_data(user_ssn: str, secret_key: str):
# ๋น์ฆ๋์ค ๋ก์ง ์ํ
return "์ฒ๋ฆฌ ์๋ฃ"
```
---
### ๐ฐ 3. ํ๋ก๋์
๋น์ฉ ๋ฐ ์ฑ๋ฅ ์ต์ ํ ์ฒดํฌ๋ฆฌ์คํธ
| ์ ๊ฒ ํญ๋ชฉ | ๊ถ์ฅ ์ค์ ๋ฐ ์ค๋ฌด ๊ฐ์ด๋ |
| :--- | :--- |
| **ํ๊ฒฝ๋ณ ํ๋ก์ ํธ ๋ถ๋ฆฌ** | `LANGCHAIN_PROJECT`๋ฅผ `app-dev`, `app-staging`, `app-prod`๋ก ๋ช
ํํ ๋ถ๋ฆฌํ์ฌ ์ด์ ๋ฐ์ดํฐ์ ํ
์คํธ ๋ฐ์ดํฐ๊ฐ ์์ด์ง ์๋๋ก ๊ด๋ฆฌํฉ๋๋ค. |
| **ํธ๋ ์ด์ฑ ์ํ๋ง (Sampling)** | ๋๊ท๋ชจ ํธ๋ํฝ ๋ฐ์ ์ ๋ชจ๋ ์์ฒญ์ ๊ธฐ๋กํ๋ฉด ๋น์ฉ๊ณผ ๋์ญํญ์ด ์๋ชจ๋ฉ๋๋ค. ํ๊ฒฝ ๋ณ์ `LANGCHAIN_TRACING_SAMPLING_RATE="0.1"` (10% ์ํ๋ง) ์ค์ ์ ํตํด ํ๋ก๋์
๋ถ๋ด์ ๊ฒฝ๊ฐํฉ๋๋ค. |
| **ํ๊ทธ์ ๋ฉํ๋ฐ์ดํฐ ๊ท๊ฒฉํ** | `tags=["release-v2", "free-tier"]`, `metadata={"tenant_id": "...", "region": "ap-northeast-2"}`๋ฅผ ํ์คํํ์ฌ ๋ฌธ์ ๋ฐ์ ์ ์ ์ํ ์ธ๋ถ ํํฐ๋ง์ด ๊ฐ๋ฅํ๋๋ก ๊ตฌ์ฑํฉ๋๋ค. |
| **์๋ ํ๊ท ๋ฐฉ์ง (CI/CD)** | GitHub Actions ํ์ดํ๋ผ์ธ์์ ์ ํ๋กฌํํธ ๋ฐฐํฌ ์ `evaluate()` ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ์ฌ ๊ณจ๋ ๋ฐ์ดํฐ์
์ ํฉ๊ฒฉ ์ ์(์: 95์ ์ด์)๋ฅผ ํต๊ณผํ ๋๋ง ๋จธ์ง๋๋๋ก ๋ณดํธํฉ๋๋ค. |
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์