์ด๋ฒˆ ํŽธ์€ ์‹ค๋ฌดํ˜• ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ/ํ•ธ์ฆˆ์˜จ์œผ๋กœ, ์‚ฌ๋‚ด ์ •์ฑ… ๋ฌธ์„œ(ํ™˜๋ถˆ/์š”๊ธˆ์ œ/๋ณด์•ˆ)๋ฅผ ์ฝ๊ณ  ์งˆ๋ฌธ์— ๋‹ตํ•˜๋Š” FAQ ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค.

  • ์ด์ „ ํŽธ: ๐Ÿค— 18. ๋ณธํŽธ 10
  • ๋‹ค์Œ ํŽธ ์˜ˆ๊ณ : FAQ ์‘๋‹ต ํ’ˆ์งˆ ์ ์ˆ˜ํ™”(์ •ํ™•์„ฑ/๊ทผ๊ฑฐ/๊ธˆ์ง€์–ด) ์ž๋™ ๋ฆฌํฌํŠธ

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

๋กœ์ปฌ ์ •์ฑ… ํŒŒ์ผ(JSON) + ์งˆ๋ฌธ์…‹(JSONL) + smolagents CodeAgent๋กœ ๋‹ต๋ณ€ ์ƒ์„ฑ ํ›„, ํ˜•์‹/๊ทผ๊ฑฐ/์ •ํ™•๋„ ๊ธฐ์ค€์„ ์ž๋™ ๊ฒ€์ฆํ•œ๋‹ค.

flowchart LR
  A[policy_kb.json] --> B[Python Tool: search_policy]
  C[questions.jsonl] --> D[CodeAgent]
  B --> D
  D --> E[answers.json]
  E --> F[validator.py]
  F --> G{PASS/FAIL}

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

  • ๋ฒ”์œ„ ํฌํ•จ
    • ์ •์ฑ… KB ์กฐํšŒ ๋„๊ตฌ 1๊ฐœ(search_policy)๋ฅผ ๋งŒ๋“ค์–ด ์—์ด์ „ํŠธ์— ์—ฐ๊ฒฐ
    • ์งˆ๋ฌธ 8๊ฑด์— ๋Œ€ํ•ด answer, evidence(๊ทผ๊ฑฐ policy_id) ์ถœ๋ ฅ
    • ์ž๋™ ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ๋กœ PASS/FAIL ํŒ๋‹จ
  • ๋ฒ”์œ„ ์ œ์™ธ
    • ๋ฒกํ„ฐDB/์ž„๋ฒ ๋”ฉ
    • ์™ธ๋ถ€ API ์—ฐ๋™(n8n, Slack)
    • ๋‹ค๊ตญ์–ด ๋‹ต๋ณ€ ํŠœ๋‹

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

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

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("OPENAI_API_KEY set:", bool(os.getenv("OPENAI_API_KEY")))
PY
  • ๋ชจ๋ธ ID ๋ฌธ์ž์—ด ์ถœ๋ ฅ
  • OPENAI_API_KEY set: True ์ถœ๋ ฅ

3) ์ •์ฑ… KB์™€ ์งˆ๋ฌธ์…‹ ๋งŒ๋“ค๊ธฐ

  • ๋„๊ตฌ: ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: ์ •์ฑ… 6๊ฐœ + ์งˆ๋ฌธ 8๊ฐœ
  • ์‹คํ–‰๋ช…๋ น:
cat > policy_kb.json <<'JSON'
[
  {"policy_id":"P-REFUND-7D","topic":"refund","content":"๊ฒฐ์ œ ํ›„ 7์ผ ์ด๋‚ด, ์‚ฌ์šฉ๋Ÿ‰ 10% ๋ฏธ๋งŒ์ด๋ฉด ํ™˜๋ถˆ ๊ฐ€๋Šฅํ•˜๋‹ค."},
  {"policy_id":"P-REFUND-EXC","topic":"refund","content":"๋””์ง€ํ„ธ ๋‹ค์šด๋กœ๋“œ ์ƒํ’ˆ์€ ๊ฒฐ์ œ ์ฆ‰์‹œ ์‚ฌ์šฉ์œผ๋กœ ๊ฐ„์ฃผ๋˜์–ด ํ™˜๋ถˆ ๋Œ€์ƒ์—์„œ ์ œ์™ธ๋œ๋‹ค."},
  {"policy_id":"P-PLAN-UP","topic":"plan","content":"์š”๊ธˆ์ œ ์—…๊ทธ๋ ˆ์ด๋“œ๋Š” ์ฆ‰์‹œ ๋ฐ˜์˜๋˜๋ฉฐ, ์ฐจ์•ก์€ ์ผํ•  ๊ณ„์‚ฐ๋œ๋‹ค."},
  {"policy_id":"P-PLAN-DOWN","topic":"plan","content":"์š”๊ธˆ์ œ ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ๋Š” ๋‹ค์Œ ๊ฒฐ์ œ ์ฃผ๊ธฐ๋ถ€ํ„ฐ ์ ์šฉ๋œ๋‹ค."},
  {"policy_id":"P-SEC-MFA","topic":"security","content":"๊ด€๋ฆฌ์ž ๊ณ„์ •์€ MFA๋ฅผ ๋ฐ˜๋“œ์‹œ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•œ๋‹ค."},
  {"policy_id":"P-SEC-LOG","topic":"security","content":"๋ณด์•ˆ ๋กœ๊ทธ๋Š” ์ตœ์†Œ 90์ผ๊ฐ„ ๋ณด๊ด€ํ•œ๋‹ค."}
]
JSON
 
cat > questions.jsonl <<'JSONL'
{"qid":"Q1","question":"๊ฒฐ์ œ 3์ผ ์ง€๋‚ฌ๊ณ  ๊ฑฐ์˜ ์•ˆ ์ผ๋Š”๋ฐ ํ™˜๋ถˆ ๊ฐ€๋Šฅํ•ด?","expected_policy":"P-REFUND-7D"}
{"qid":"Q2","question":"๋‹ค์šด๋กœ๋“œํ˜• ๋ฆฌํฌํŠธ ์ƒ€๋Š”๋ฐ ๋ฐ”๋กœ ํ™˜๋ถˆ๋ผ?","expected_policy":"P-REFUND-EXC"}
{"qid":"Q3","question":"์š”๊ธˆ์ œ ์˜ฌ๋ฆฌ๋ฉด ์–ธ์ œ ๋ฐ˜์˜๋ผ?","expected_policy":"P-PLAN-UP"}
{"qid":"Q4","question":"์š”๊ธˆ์ œ ๋‚ด๋ฆฌ๋ฉด ๋ฐ”๋กœ ๋‚ด๋ ค๊ฐ€?","expected_policy":"P-PLAN-DOWN"}
{"qid":"Q5","question":"๊ด€๋ฆฌ์ž ๊ณ„์ •์— 2๋‹จ๊ณ„ ์ธ์ฆ ํ•„์ˆ˜์•ผ?","expected_policy":"P-SEC-MFA"}
{"qid":"Q6","question":"๋ณด์•ˆ ๋กœ๊ทธ๋Š” ์–ผ๋งˆ๋‚˜ ๋ณด๊ด€ํ•ด์•ผ ํ•ด?","expected_policy":"P-SEC-LOG"}
{"qid":"Q7","question":"ํ™˜๋ถˆ์€ ๋ฌด์กฐ๊ฑด 30์ผ ์ด๋‚ด๋ฉด ๋ผ?","expected_policy":"P-REFUND-7D"}
{"qid":"Q8","question":"์—…๊ทธ๋ ˆ์ด๋“œ ์ฐจ์•ก ๊ณ„์‚ฐ ๋ฐฉ์‹์€?","expected_policy":"P-PLAN-UP"}
JSONL
  • ์„ฑ๊ณตํŒ์ •:
python - <<'PY'
import json
print('kb:', len(json.load(open('policy_kb.json'))))
print('q :', sum(1 for _ in open('questions.jsonl')))
PY
  • kb: 6, q : 8 ์ถœ๋ ฅ

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

  • ๋„๊ตฌ: ์—๋””ํ„ฐ/ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: ์•„๋ž˜ ์ฝ”๋“œ
  • ์‹คํ–‰๋ช…๋ น:
cat > lab19_policy_faq_agent.py <<'PY'
from __future__ import annotations
 
import json
import os
from pathlib import Path
from typing import List, Dict, Any
 
from smolagents import CodeAgent, LiteLLMModel, tool
 
KB_PATH = Path("policy_kb.json")
Q_PATH = Path("questions.jsonl")
OUT_PATH = Path("answers.json")
 
KB: List[Dict[str, Any]] = json.loads(KB_PATH.read_text(encoding="utf-8"))
 
@tool
def search_policy(query: str) -> str:
    """์งˆ๋ฌธ(query)์— ๊ฐ€์žฅ ๊ด€๋ จ ๋†’์€ ์ •์ฑ… 3๊ฐœ๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค."""
    q = query.lower()
    scored = []
    for row in KB:
        txt = (row["topic"] + " " + row["content"]).lower()
        score = 0
        for token in ["ํ™˜๋ถˆ", "refund", "์š”๊ธˆ์ œ", "upgrade", "down", "๋ณด์•ˆ", "mfa", "๋กœ๊ทธ"]:
            if token in q and token in txt:
                score += 1
        if score == 0:
            # ํ† ํฐ์ด ํ•˜๋‚˜๋„ ์•ˆ ๋งž์œผ๋ฉด ์•ฝํ•œ ๊ธฐ๋ณธ์ ์ˆ˜
            score = 0.1
        scored.append((score, row))
    top = [r for _, r in sorted(scored, key=lambda x: x[0], reverse=True)[:3]]
    return json.dumps(top, ensure_ascii=False)
 
 
def load_questions() -> List[Dict[str, Any]]:
    items = []
    for line in Q_PATH.read_text(encoding="utf-8").splitlines():
        if line.strip():
            items.append(json.loads(line))
    return items
 
 
def build_agent() -> CodeAgent:
    model = LiteLLMModel(model_id=os.getenv("MODEL_ID", "openai/gpt-4o-mini"))
    system_prompt = """
๋„ˆ๋Š” ์ •์ฑ… FAQ ๋‹ต๋ณ€ ์—์ด์ „ํŠธ๋‹ค.
๋ฐ˜๋“œ์‹œ search_policy ๋„๊ตฌ๋ฅผ ๋จผ์ € ํ˜ธ์ถœํ•ด ๊ทผ๊ฑฐ๋ฅผ ์ฐพ๋Š”๋‹ค.
์ถœ๋ ฅ์€ JSON ๋ฐฐ์—ด๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
๊ฐ ์›์†Œ ํ‚ค:
- qid
- answer (ํ•œ๊ธ€ 1~2๋ฌธ์žฅ)
- evidence (policy_id 1๊ฐœ)
๊ธˆ์ง€:
- KB์— ์—†๋Š” ์ •์ฑ…์„ ์‚ฌ์‹ค์ฒ˜๋Ÿผ ๋‹จ์ •
- evidence ๋ˆ„๋ฝ
""".strip()
    return CodeAgent(model=model, tools=[search_policy], system_prompt=system_prompt)
 
 
def main() -> None:
    questions = load_questions()
    agent = build_agent()
 
    prompt = f"""
์•„๋ž˜ ์งˆ๋ฌธ ๋ชฉ๋ก์— ๋‹ต๋ณ€ํ•ด.
์งˆ๋ฌธ ๋ชฉ๋ก:
{json.dumps(questions, ensure_ascii=False)}
 
๋ฐ˜๋“œ์‹œ JSON ๋ฐฐ์—ด๋งŒ ์ถœ๋ ฅํ•ด.
""".strip()
 
    result = agent.run(prompt)
 
    if isinstance(result, str):
        s = result.find("[")
        e = result.rfind("]")
        if s != -1 and e != -1 and e > s:
            result = json.loads(result[s:e+1])
        else:
            raise ValueError("JSON ๋ฐฐ์—ด ํŒŒ์‹ฑ ์‹คํŒจ")
 
    OUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"saved: {OUT_PATH}")
    print(f"count: {len(result)}")
 
 
if __name__ == "__main__":
    main()
PY
  • ์„ฑ๊ณตํŒ์ •:
    • lab19_policy_faq_agent.py ์ƒ์„ฑ
    • search_policy ๋„๊ตฌ ์ •์˜ ํ™•์ธ

5) ์‹คํ–‰ ๋ฐ ์ถœ๋ ฅ ํ™•์ธ

  • ๋„๊ตฌ: Python, ํ„ฐ๋ฏธ๋„
  • ์ž…๋ ฅ: policy_kb.json, questions.jsonl
  • ์‹คํ–‰๋ช…๋ น:
python lab19_policy_faq_agent.py
cat answers.json
  • ์„ฑ๊ณตํŒ์ •:
    • saved: answers.json
    • count: 8
    • ๊ฐ ํ•ญ๋ชฉ์— qid/answer/evidence ํ‚ค ์กด์žฌ

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

  • ๋„๊ตฌ: Python
  • ์ž…๋ ฅ: answers.json, questions.jsonl
  • ์‹คํ–‰๋ช…๋ น:
cat > validator.py <<'PY'
import json
from pathlib import Path
 
answers = json.loads(Path("answers.json").read_text(encoding="utf-8"))
expected = [json.loads(x) for x in Path("questions.jsonl").read_text(encoding="utf-8").splitlines() if x.strip()]
emap = {x["qid"]: x["expected_policy"] for x in expected}
 
assert isinstance(answers, list), "answers must be list"
assert len(answers) == len(expected), "์งˆ๋ฌธ/์‘๋‹ต ๊ฑด์ˆ˜ ๋ถˆ์ผ์น˜"
 
for row in answers:
    assert {"qid", "answer", "evidence"}.issubset(row.keys()), f"ํ‚ค ๋ˆ„๋ฝ: {row}"
    assert row["qid"] in emap, f"์•Œ ์ˆ˜ ์—†๋Š” qid: {row['qid']}"
    assert isinstance(row["answer"], str) and len(row["answer"].strip()) > 0, f"๋นˆ answer: {row}"
    assert isinstance(row["evidence"], str) and row["evidence"].startswith("P-"), f"evidence ํ˜•์‹ ์˜ค๋ฅ˜: {row}"
 
# ๋‹จ์ˆœ ์ •๋‹ต๋ฅ 
ok = sum(1 for row in answers if row["evidence"] == emap[row["qid"]])
acc = ok / len(expected)
print(f"accuracy={acc:.2f} ({ok}/{len(expected)})")
 
assert acc >= 0.75, "์ •๋‹ต๋ฅ  0.75 ๋ฏธ๋งŒ"
print("PASS: ํ˜•์‹/๊ทผ๊ฑฐ/์ •๋‹ต๋ฅ  ๊ฒ€์ฆ ์™„๋ฃŒ")
PY
 
python validator.py
  • ์„ฑ๊ณตํŒ์ •:
    • accuracy=... ์ถœ๋ ฅ
    • PASS: ํ˜•์‹/๊ทผ๊ฑฐ/์ •๋‹ต๋ฅ  ๊ฒ€์ฆ ์™„๋ฃŒ ์ถœ๋ ฅ

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

  1. AuthenticationError / 401 Unauthorized
  • ์›์ธ: API ํ‚ค ๋ˆ„๋ฝ/๋งŒ๋ฃŒ
  • ํ•ด๊ฒฐ:
echo ${OPENAI_API_KEY:+SET}
export OPENAI_API_KEY="์ •์ƒํ‚ค"
  1. ValueError: JSON ๋ฐฐ์—ด ํŒŒ์‹ฑ ์‹คํŒจ
  • ์›์ธ: ๋ชจ๋ธ์ด ์„ค๋ช…๋ฌธ + JSON์„ ํ˜ผํ•ฉ ์ถœ๋ ฅ
  • ํ•ด๊ฒฐ:
    • ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— JSON ๋ฐฐ์—ด๋งŒ ์ถœ๋ ฅ ๋ฐ˜๋ณต ๋ช…์‹œ
    • ๊ฒฐ๊ณผ์—์„œ [~] ๊ตฌ๊ฐ„ ์ถ”์ถœ ๋ณด์ • ์œ ์ง€(ํ˜„์žฌ ์ฝ”๋“œ ๋ฐ˜์˜)
  1. evidence๊ฐ€ P- ํ˜•์‹์ด ์•„๋‹˜
  • ์›์ธ: ๋ชจ๋ธ์ด ์ •์ฑ… ID ๋Œ€์‹  ์ž์—ฐ์–ด๋ฅผ ๋ฐ˜ํ™˜
  • ํ•ด๊ฒฐ:
    • ํ”„๋กฌํ”„ํŠธ์— evidence๋Š” policy_id 1๊ฐœ ๊ฐ•์ œ
    • validator.py์—์„œ ํ˜•์‹ ๊ฒ€์ฆ์œผ๋กœ ์ฆ‰์‹œ ์‹คํŒจ ์ฒ˜๋ฆฌ
  1. ์ •๋‹ต๋ฅ ์ด 0.75 ๋ฏธ๋งŒ์œผ๋กœ FAIL
  • ์›์ธ: search_policy ํ† ํฐ ๋งค์นญ ๋‹จ์ˆœํ™”๋กœ ํšŒ์ˆ˜์œจ ์ €ํ•˜
  • ํ•ด๊ฒฐ:
    • ๋™์˜์–ด ํ† ํฐ ์ถ”๊ฐ€(์˜ˆ: ์—…๊ทธ๋ ˆ์ด๋“œ/์ƒํ–ฅ, ๋‹ค์šด๊ทธ๋ ˆ์ด๋“œ/ํ•˜ํ–ฅ)
    • top-k๋ฅผ 3โ†’4๋กœ ์กฐ์ • ํ›„ ์žฌํ‰๊ฐ€
  1. ModuleNotFoundError: smolagents
  • ์›์ธ: ๊ฐ€์ƒํ™˜๊ฒฝ ๋ฏธํ™œ์„ฑํ™” ๋˜๋Š” ์„ค์น˜ ๋ˆ„๋ฝ
  • ํ•ด๊ฒฐ:
source .venv/bin/activate
pip install -U smolagents litellm

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

  • ์ •์ฑ… KB๋ฅผ Markdown ํด๋”์—์„œ ์ž๋™ ์ˆ˜์ง‘ํ•ด policy_kb.json๋กœ ๋นŒ๋“œ
  • accuracy ์ถ”์ด๋ฅผ ์ฃผ๊ฐ„ ๋ฆฌํฌํŠธ๋กœ ์ €์žฅ(ํ’ˆ์งˆ ํšŒ๊ท€ ๊ฐ์‹œ)
  • ์‹คํŒจ ์ผ€์ด์Šค๋ฅผ ๋ณ„๋„ ํ๋กœ ๋ณด๋‚ด ์‚ฌ๋žŒ ๊ฒ€ํ† (HITL) ์ ์šฉ

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

  • ํ™˜๊ฒฝ/ํŒจํ‚ค์ง€ ์„ค์น˜ ์™„๋ฃŒ
  • API ํ‚ค/๋ชจ๋ธ ์„ค์ • ์™„๋ฃŒ
  • KB/์งˆ๋ฌธ์…‹ ํŒŒ์ผ ์ƒ์„ฑ ์™„๋ฃŒ
  • answers.json ์ƒ์„ฑ ํ™•์ธ
  • validator.py PASS ํ™•์ธ
  • ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 3๊ฐœ ์ด์ƒ ์ ๊ฒ€ ์™„๋ฃŒ

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

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

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

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