์ด๋ฒˆ ํŽธ์€ ์‹ค๋ฌดํ˜• ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ 1ํŽธ์œผ๋กœ, ๋“ค์–ด์˜ค๋Š” ๊ณ ๊ฐ ๋ฌธ์˜๋ฅผ ์ž๋™ ๋ถ„๋ฅ˜ํ•ด billing / bug / feature / other๋กœ ๋ผ์šฐํŒ…ํ•˜๋Š” ํ‹ฐ์ผ“ ํŠธ๋ฆฌ์•„์ง€ ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค.

  • ์ด์ „ ํŽธ: ๐Ÿค— 15. ๋ณธํŽธ 08
  • ๋‹ค์Œ ํŽธ ์˜ˆ๊ณ : ํŠธ๋ฆฌ์•„์ง€ ๊ฒฐ๊ณผ๋ฅผ Slack/Notion/n8n์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ์ž๋™ํ™” ํ™•์žฅ

ํ•œ ์ค„ ๋ชฉํ‘œ

์ž…๋ ฅ(JSON Lines) โ†’ ๋ถ„๋ฅ˜ ์—์ด์ „ํŠธ ์‹คํ–‰ โ†’ ๊ฒฐ๊ณผ(JSON) ์ƒ์„ฑ โ†’ ์„ฑ๊ณต ํŒ์ •๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์žฌํ˜„ํ•œ๋‹ค.

flowchart LR
  I[tickets.jsonl] --> A[CodeAgent]
  A --> T[๋ถ„๋ฅ˜ ๊ทœ์น™ + ์ƒ˜ํ”Œ ๊ทผ๊ฑฐ]
  T --> O[triage_output.json]
  O --> C[์ •ํ™•๋„/ํ˜•์‹ ์ฒดํฌ]
  C --> R[์ˆ˜๋™๊ฒ€ํ†  ๋˜๋Š” ๋‹ค์Œ ์ž๋™ํ™”]

0) ์‹ค์Šต ๋ฒ”์œ„(๊ณ ์ •)

  • ๋ฒ”์œ„ ํฌํ•จ
    • ๋‹จ์ผ ํŒŒ์ผ ์ž…๋ ฅ(tickets.jsonl)์„ ์ฝ์–ด ๋ฌธ์˜๋ฅผ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜
    • ๊ฐ ํ‹ฐ์ผ“์— category, priority, reason ํ•„๋“œ ์ƒ์„ฑ
    • ๊ฒฐ๊ณผ๋ฅผ triage_output.json์œผ๋กœ ์ €์žฅ
  • ๋ฒ”์œ„ ์ œ์™ธ
    • DB/Slack/Notion ์—ฐ๋™
    • ๋‹ค๊ตญ์–ด ๋ถ„๋ฅ˜ ๊ณ ๋„ํ™”

1) ํ™˜๊ฒฝ ์ค€๋น„

  • ๋„๊ตฌ: ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: Python 3.10+, ๊ฐ€์ƒํ™˜๊ฒฝ
  • ์‹คํ–‰๋ช…๋ น:
mkdir -p ~/hf-agents-lab16
cd ~/hf-agents-lab16
python3 -m venv .venv
source .venv/bin/activate
pip install -U smolagents litellm
  • ์„ฑ๊ณตํŒ์ •:
    • (.venv) ํ”„๋กฌํ”„ํŠธ ํ‘œ์‹œ
    • pip install ์—๋Ÿฌ ์—†์Œ

2) ๋ชจ๋ธ ํ‚ค ์„ค์ •

  • ๋„๊ตฌ: ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: API ํ‚ค, ๋ชจ๋ธ ID
  • ์‹คํ–‰๋ช…๋ น:
export OPENAI_API_KEY="YOUR_API_KEY"
export MODEL_ID="openai/gpt-4o-mini"
  • ์„ฑ๊ณตํŒ์ •:
echo "$MODEL_ID"
python - <<'PY'
import os
print(bool(os.getenv("OPENAI_API_KEY")))
PY
  • ๋ชจ๋ธ ID ์ถœ๋ ฅ๋จ
  • True ์ถœ๋ ฅ

3) ์‹ค์Šต ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๋งŒ๋“ค๊ธฐ

  • ๋„๊ตฌ: ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: ์ƒ˜ํ”Œ ํ‹ฐ์ผ“ 6๊ฐœ
  • ์‹คํ–‰๋ช…๋ น:
cat > tickets.jsonl <<'JSONL'
{"ticket_id":"T-1001","text":"๊ฒฐ์ œ๋Š” ๋๋Š”๋ฐ ์˜์ˆ˜์ฆ ๋ฉ”์ผ์ด ์•ˆ ์™”์–ด์š”"}
{"ticket_id":"T-1002","text":"์•ฑ ์—…๋ฐ์ดํŠธ ํ›„ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด ์•ฑ์ด ๊บผ์ง‘๋‹ˆ๋‹ค"}
{"ticket_id":"T-1003","text":"๋Œ€์‹œ๋ณด๋“œ์— CSV ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๋ถ€ํƒํ•ฉ๋‹ˆ๋‹ค"}
{"ticket_id":"T-1004","text":"์š”๊ธˆ์ œ ์—…๊ทธ๋ ˆ์ด๋“œํ–ˆ๋Š”๋ฐ ๋ฐ˜์˜์ด ์•ˆ ๋ฉ๋‹ˆ๋‹ค"}
{"ticket_id":"T-1005","text":"์ฒจ๋ถ€ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ 500 ์—๋Ÿฌ๊ฐ€ ๋– ์š”"}
{"ticket_id":"T-1006","text":"์„œ๋น„์Šค ์†Œ๊ฐœ ์ž๋ฃŒ๋ฅผ ๋ฐ›์•„๋ณผ ์ˆ˜ ์žˆ๋‚˜์š”?"}
JSONL
  • ์„ฑ๊ณตํŒ์ •:
wc -l tickets.jsonl
  • 6 tickets.jsonl ์ถœ๋ ฅ

4) ์—์ด์ „ํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

  • ๋„๊ตฌ: ์—๋””ํ„ฐ/ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: ์•„๋ž˜ ์ฝ”๋“œ
  • ์‹คํ–‰๋ช…๋ น:
cat > lab16_ticket_triage.py <<'PY'
from __future__ import annotations
 
import json
import os
from pathlib import Path
from smolagents import CodeAgent, LiteLLMModel
 
INPUT_PATH = Path("tickets.jsonl")
OUTPUT_PATH = Path("triage_output.json")
 
 
def load_tickets(path: Path):
    rows = []
    for line in path.read_text(encoding="utf-8").splitlines():
        if line.strip():
            rows.append(json.loads(line))
    return rows
 
 
def build_agent() -> CodeAgent:
    model_id = os.getenv("MODEL_ID", "openai/gpt-4o-mini")
    model = LiteLLMModel(model_id=model_id)
 
    system_prompt = """
๋„ˆ๋Š” ๊ณ ๊ฐ์ง€์› ํ‹ฐ์ผ“ ๋ถ„๋ฅ˜ ์—์ด์ „ํŠธ๋‹ค.
์นดํ…Œ๊ณ ๋ฆฌ๋Š” billing/bug/feature/other ์ค‘ ํ•˜๋‚˜๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
priority๋Š” high/medium/low ์ค‘ ํ•˜๋‚˜๋งŒ ์‚ฌ์šฉํ•œ๋‹ค.
๋ฐ˜ํ™˜์€ ๋ฐ˜๋“œ์‹œ JSON ๋ฐฐ์—ด์ด๋ฉฐ ๊ฐ ์›์†Œ๋Š” ์•„๋ž˜ ํ‚ค๋ฅผ ํฌํ•จํ•œ๋‹ค.
- ticket_id
- category
- priority
- reason (ํ•œ๊ธ€ 1๋ฌธ์žฅ)
""".strip()
 
    return CodeAgent(model=model, tools=[], system_prompt=system_prompt)
 
 
def main() -> None:
    tickets = load_tickets(INPUT_PATH)
    agent = build_agent()
 
    prompt = f"""
๋‹ค์Œ ํ‹ฐ์ผ“ ๋ชฉ๋ก์„ ๊ทœ์น™์— ๋งž์ถฐ ๋ถ„๋ฅ˜ํ•ด.
ํ‹ฐ์ผ“ ๋ชฉ๋ก:
{json.dumps(tickets, ensure_ascii=False)}
""".strip()
 
    result = agent.run(prompt)
 
    # smolagents ์ถœ๋ ฅ์ด ๋ฌธ์ž์—ด์ผ ์ˆ˜ ์žˆ์–ด ๋ณด์ •
    if isinstance(result, str):
        start = result.find("[")
        end = result.rfind("]")
        if start != -1 and end != -1 and end > start:
            result = json.loads(result[start:end+1])
        else:
            raise ValueError("JSON ๋ฐฐ์—ด ํŒŒ์‹ฑ ์‹คํŒจ")
 
    OUTPUT_PATH.write_text(
        json.dumps(result, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
 
    print(f"saved: {OUTPUT_PATH}")
    print(f"count: {len(result)}")
 
 
if __name__ == "__main__":
    main()
PY
  • ์„ฑ๊ณตํŒ์ •:
    • lab16_ticket_triage.py ํŒŒ์ผ ์ƒ์„ฑ
    • ์ฝ”๋“œ์— CodeAgent, LiteLLMModel ์กด์žฌ

5) ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ ํ™•์ธ

  • ๋„๊ตฌ: Python, ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: tickets.jsonl
  • ์‹คํ–‰๋ช…๋ น:
python lab16_ticket_triage.py
cat triage_output.json
  • ์„ฑ๊ณตํŒ์ •:
    • saved: triage_output.json ์ถœ๋ ฅ
    • ๊ฒฐ๊ณผ ํ•ญ๋ชฉ ์ˆ˜ 6
    • ๊ฐ ํ•ญ๋ชฉ์— ticket_id/category/priority/reason 4๊ฐœ ํ‚ค ์กด์žฌ

6) ์ž๋™ ๊ฒ€์ฆ(์„ฑ๊ณต ํŒ์ • ์Šคํฌ๋ฆฝํŠธ)

  • ๋„๊ตฌ: Python
  • ์ž…๋ ฅ: triage_output.json
  • ์‹คํ–‰๋ช…๋ น:
python - <<'PY'
import json
from pathlib import Path
 
data = json.loads(Path("triage_output.json").read_text(encoding="utf-8"))
allowed_cat = {"billing", "bug", "feature", "other"}
allowed_pri = {"high", "medium", "low"}
 
assert isinstance(data, list) and len(data) == 6, "๊ฑด์ˆ˜ ๋ถˆ์ผ์น˜"
for row in data:
    assert set(["ticket_id","category","priority","reason"]).issubset(row.keys()), f"ํ‚ค ๋ˆ„๋ฝ: {row}"
    assert row["category"] in allowed_cat, f"category ์˜ค๋ฅ˜: {row}"
    assert row["priority"] in allowed_pri, f"priority ์˜ค๋ฅ˜: {row}"
print("PASS: ํ˜•์‹/๊ฐ’ ๊ฒ€์ฆ ์™„๋ฃŒ")
PY
  • ์„ฑ๊ณตํŒ์ •:
    • PASS: ํ˜•์‹/๊ฐ’ ๊ฒ€์ฆ ์™„๋ฃŒ ์ถœ๋ ฅ

ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… (์ตœ์†Œ 3๊ฐœ)

  1. AuthenticationError / 401
  • ์›์ธ: API ํ‚ค ๋ฏธ์„ค์ • ๋˜๋Š” ์ž˜๋ชป๋œ ํ‚ค
  • ํ•ด๊ฒฐ:
echo ${OPENAI_API_KEY:+SET}
export OPENAI_API_KEY="์ •์ƒํ‚ค"
  1. ValueError: JSON ๋ฐฐ์—ด ํŒŒ์‹ฑ ์‹คํŒจ
  • ์›์ธ: ๋ชจ๋ธ์ด ์„ค๋ช…๋ฌธ+JSON ํ˜ผํ•ฉ ์ถœ๋ ฅ
  • ํ•ด๊ฒฐ:
    • ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— โ€œJSON ๋ฐฐ์—ด๋งŒ ์ถœ๋ ฅโ€ ๋ฌธ๊ตฌ ๊ฐ•ํ™”
    • ํ•„์š” ์‹œ json schema ์˜ˆ์‹œ๋ฅผ ํ”„๋กฌํ”„ํŠธ์— ์ถ”๊ฐ€
  1. ๋ถ„๋ฅ˜ ๋ผ๋ฒจ์ด ๊ทœ์น™ ์™ธ ๊ฐ’์œผ๋กœ ๋‚˜์˜ด (์˜ˆ: payment_issue)
  • ์›์ธ: ํ—ˆ์šฉ ๋ผ๋ฒจ ์ง‘ํ•ฉ ๊ฐ•์ œ ๋ถ€์กฑ
  • ํ•ด๊ฒฐ:
    • ํ”„๋กฌํ”„ํŠธ์— โ€œํ—ˆ์šฉ๊ฐ’ ์™ธ ๊ธˆ์ง€โ€ ๋ช…์‹œ
    • 6๋‹จ๊ณ„ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ๋กœ ์‹คํŒจ ์ฆ‰์‹œ ํƒ์ง€
  1. ์†๋„๊ฐ€ ๋А๋ฆฌ๊ฑฐ๋‚˜ ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ
  • ์›์ธ: ์ž…๋ ฅ ํ‹ฐ์ผ“ ์ˆ˜ ์ฆ๊ฐ€/๋ชจ๋ธ ์‘๋‹ต ์ง€์—ฐ
  • ํ•ด๊ฒฐ:
    • ๋ฐฐ์น˜ ๋ถ„ํ• (์˜ˆ: 50๊ฐœ ๋‹จ์œ„)
    • ์žฌ์‹œ๋„ ๋กœ์ง(๋ฐฑ์˜คํ”„) ์ถ”๊ฐ€

์šด์˜ ํ™•์žฅ ํฌ์ธํŠธ (๋‹ค์Œ ํŽธ ์—ฐ๊ฒฐ)

  • triage_output.json์„ n8n Webhook์œผ๋กœ POST
  • priority=high๋งŒ ์Šฌ๋ž™ ์•Œ๋ฆผ ์ฑ„๋„๋กœ ๋ถ„๊ธฐ
  • ๋ถ„๋ฅ˜ ์ •ํ™•๋„ ์ƒ˜ํ”Œ์…‹(์ •๋‹ต ๋ผ๋ฒจ) ๋ถ™์—ฌ ์ฃผ๊ฐ„ ํ’ˆ์งˆ ๋ฆฌํฌํŠธ ์ž๋™ํ™”

์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ๊ฐ€์ƒํ™˜๊ฒฝ/ํŒจํ‚ค์ง€ ์„ค์น˜ ์™„๋ฃŒ
  • API ํ‚ค/๋ชจ๋ธ ID ์„ค์ • ์™„๋ฃŒ
  • tickets.jsonl 6๊ฑด ์ƒ์„ฑ ํ™•์ธ
  • triage_output.json ์ €์žฅ ํ™•์ธ
  • ์ž๋™ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ PASS
  • ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 3๊ฐœ ์ด์ƒ ์ ๊ฒ€ ์™„๋ฃŒ

์ฐธ๊ณ  ๋งํฌ (์šฐ์„ ์ˆœ์œ„)

  1. https://github.com/huggingface/agents-course
  2. https://huggingface.co/learn/agents-course
  3. https://huggingface.co/docs/smolagents

์ƒ์„ฑํ˜• AI ํ™œ์šฉ ๊ณ ์ง€

์ด ๋ฌธ์„œ๋Š” ์ƒ์„ฑํ˜• AI๋ฅผ ํ™œ์šฉํ•ด ์ดˆ์•ˆ์„ ์ž‘์„ฑํ•˜๊ณ , ์‹ค์Šต ์ ˆ์ฐจ/์ฝ”๋“œ/๊ฒ€์ฆ ๋‹จ๊ณ„๋ฅผ ๊ตฌ์กฐํ™”ํ–ˆ๋‹ค. ์ตœ์ข… ๋ฐ˜์˜ ์ „ ์‚ฌ๋žŒ ๊ฒ€ํ† ๋กœ ๋ช…๋ น ์žฌํ˜„์„ฑ, ๋งํฌ ์œ ํšจ์„ฑ, ํฌ๋งท ์ผ๊ด€์„ฑ์„ ์ ๊ฒ€ํ–ˆ๋‹ค.