StructVerify 사용법

내 규정집(PDF)이나 회사 데이터를 기준으로 문서를 대조하고, 평범한 True/False를 읽는 파이썬 라이브러리입니다. 모든 예시는 실행 결과와 함께 제공됩니다.

StructVerify란?

기업과 기관에서는 시험성적서가 안전기준을 충족하는지, 보고서의 수치가 실제 데이터와 일치하는지를 사람이 규정집과 문서를 펼쳐 놓고 일일이 대조합니다. StructVerify는 이 대조 작업을 코드로 옮긴 라이브러리입니다. 사용자가 기준이 되는 자료(규정집 PDF, 참조 CSV, 데이터베이스 등)를 연결하면, 라이브러리가 검사 대상 문서에서 수치가 담긴 문장을 찾아내고, 기준 자료에서 근거를 검색한 뒤, 문장 하나하나에 대해 준수/위반 또는 일치/불일치를 판정합니다.

내부적으로는 LLM과 규칙 기반 코드가 역할을 나눕니다. LLM은 문장에서 지표·수치·단위를 추출하고 기준 자료에서 관련 조항·데이터를 찾는 일을 맡고, 실제 판정은 코드가 결정론적으로 계산합니다 ("기준 90 이하인데 측정값이 95"라는 판단에 모델의 변동성이 개입하지 않습니다). 그래서 결과 객체에서 .compliant, .ok 같은 불리언 값을 바로 읽어 후속 로직에 사용할 수 있습니다.

시작에 필요한 것은 세 가지입니다. Python 3.10 이상, pip install structverify, 그리고 판정에 사용할 LLM provider API 키 하나(Upstage·OpenAI·Gemini·HCX 중 택일). 아래 순서대로 따라가면 5분 안에 첫 검증을 실행할 수 있습니다.

github.com/2026-StructVerify-Lab/structverify · pypi.org/project/structverify · 상세 인자·설정값은 레퍼런스 매뉴얼 참조

설치

pip install structverify                      # 코어 — 의존성 7개 (MIT/Apache만)

# 필요한 것만 골라서:
pip install "structverify[db]"            # 회사 DB (SQLAlchemy)
pip install "structverify[training]"      # QLoRA 학습 (NVIDIA)
pip install "structverify[training-mac]" # LoRA 학습 (Apple Silicon·MLX)
pip install "structverify[all]"           # 허용적 라이선스 전부 (AGPL 제외)

extras 전체 목록(kosis·korean·url·docs·pdf-ocr·graph·platform)은 레퍼런스 매뉴얼 1장 참조. Python 3.10 이상이 필요합니다. 3.9 이하에서는 pip이 "No matching distribution found"를 냅니다.

API 키 준비

판정에 LLM을 쓰기 때문에 provider 키가 하나 필요합니다. 처음 사용한다면 Upstage를 권장합니다 (가입 시 무료 크레딧이 제공되며 한국어 성능이 안정적입니다):

  1. console.upstage.ai 가입 → API Keys 메뉴에서 키 발급 (up_로 시작)
  2. 프로젝트 폴더에 .env 파일을 만들거나, 셸에서 export:
# 방법 A — .env 파일 (권장: 라이브러리가 자동으로 읽음)
echo 'UPSTAGE_API_KEY=up_여기에_발급받은_키' > .env

# 방법 B — 환경변수
export UPSTAGE_API_KEY=up_여기에_발급받은_키

# 방법 C — 코드에서 직접 (환경변수 불필요)
# sv.Ruleset.from_file("...", provider="upstage", api_key="up_...")

다른 provider를 쓰면 키 변수만 다릅니다: openai→OPENAI_API_KEY · gemini→GEMINI_API_KEY · hcx→NCP_API_KEY. 설정 확인: python -c "import os; print(bool(os.getenv('UPSTAGE_API_KEY')))"

5분 튜토리얼

아래 코드는 준비물 없이 그대로 복붙하면 돌아갑니다, 미니 규정집을 문자열로 넣어서요. (키 준비만 위에서 끝냈다면.)

import structverify as sv

# ① 규정집을 색인 — 여기선 문자열로. 내 PDF가 있으면 Ruleset.from_file("규정집.pdf", ...)
rules = sv.Ruleset.from_text("""
제1조 (총납) 어린이제품의 납 총함량은 100mg/kg 이하여야 한다.
제2조 (용출 납) 입에 넣어 사용하는 제품의 납 용출량은 90mg/kg 이하여야 한다.
제3조 (프탈레이트) DEHP 등 프탈레이트계 가소제는 0.1% 이하여야 한다.
""", provider="upstage")
print("색인된 조항:", len(rules))

# ② 측정 문장 검사 → Verdict 객체
v = rules.check("용출 납은 95mg/kg으로 측정되었다")
print("준수?", v.compliant)
print("조항 :", v.article)
print("기준 vs 측정:", v.rule_value, "vs", v.claim_value, v.unit)
색인된 조항: 3
준수? False
조항 : 제2조 (용출 납)
기준 vs 측정: 90.0 vs 95.0 mg/kg
규정집에 '총납 100'(제1조)과 '용출 납 90'(제2조)이 따로 있는데 '용출'을 알아채고 제2조를 적용한 점에 주목하십시오. 키워드 매칭이 아니라 의미 검색과 LLM 판정을 사용하기 때문입니다. 실제 PDF 규정집으로 바꾸려면 from_text(...)를 from_file("규정집.pdf", provider="upstage")로 바꾸면 끝입니다.

어떤 모드를 쓰지?

StructVerify의 두 축은 "무엇을 기준으로 문서를 검사하느냐"로 갈립니다:

내가 가진 것쓰는 것결과
규정집·기준서 (PDF/txt): "이 문서가 규칙을 지켰나?"Ruleset → .check()/.check_file()Verdict: compliant/violation
정답 수치 데이터 (CSV·DB·문서·KOSIS): "이 문서의 숫자가 맞나?"Verifier/sv.verify() + DataSourceReport: match/mismatch

둘 다 해당되면 둘 다 쓰면 됩니다. 같은 provider 설정을 공유합니다. 시험성적서 검사는 전자, 연차보고서 팩트체크는 후자가 전형적인 예.

규정 준수 검사 (Ruleset)

문서 전체 일괄 판정

for v in rules.check_file("시험성적서.txt"):
    if v.is_unverifiable:
        continue
    mark = "위반" if v.violated else "준수"
    print(f"{mark} {v.article:24} 기준 {v.rule_value} vs 측정 {v.claim_value} {v.unit or ''}")
준수 제3조 (총납)              기준 100.0 vs 측정 87.0 mg/kg
위반 제9조 (유해원소 용출)      기준 90.0 vs 측정 95.0 mg/kg
준수 제12조 (프탈레이트)        기준 0.1 vs 측정 0.03 %

큰 규정집은 agent=True (ReAct 루프)

검색 → 판정 → (애매하면) 검색어 재구성 → 재검색을 스스로 반복합니다. 조항이 많고 조건이 갈리는 규정집에서 유효:

rules = sv.Ruleset.from_file("제품안전기준.pdf", provider="upstage", agent=True)

# 까다로운 케이스 — 니켈은 제품 종류별로 한도가 다름 (눈화장 35 / 색조 30 / 그밖 10)
v = rules.check("눈 화장용 아이섀도의 니켈 함량은 32㎍/g으로 측정되었다")
print(v.verdict, "|", v.article, "|", v.iterations, "회 만에 확정")
compliant | 제4조 (니켈) — 눈 화장용 제품 35㎍/g 적용 | 2 회 만에 확정

파일 없이 문자열이면 Ruleset.from_text(text, ...). 검사 단위 오버라이드는 rules.check(stmt, agent=True, top_k=8).

사실 검증 (verify / Verifier)

이번엔 규정집이 아니라 정답 데이터(CSV·DB·문서·KOSIS)를 기준으로, 문서 속 수치 주장을 참/거짓 판정합니다. 먼저 정답 CSV를 이렇게 준비하세요. 헤더 이름 그대로 쓰는 게 제일 간단합니다:

# reference.csv — 한 행 = 지표 하나의 공식 값
indicator,time_period,region,value,unit
누적 고객 수,2023,,1500000,명
평균 주문 금액,2023,,145274,달러
부채비율,2023,,543.0,%

time_period·region은 없으면 비워도 됩니다. 회사 CSV의 헤더가 다르면 DataSource.csv(path, columns={"indicator": "지표명", "value": "금액"})처럼 매핑만 주면 돼요.

import structverify as sv

engine = sv.Verifier(provider="upstage",
                     data=sv.DataSource.csv("reference.csv"))   # 지표·연도·값·단위 행

report = engine.verify("누적 고객 수는 150만 명이며, 평균 주문 금액은 20만 달러입니다.")

print("문서 통과?", report.ok)
for r in report:
    print(f" {r.verdict:12s} {r.claim[:24]}… (실제 {r.value} {r.unit})")
문서 통과? False
  match        누적 고객 수는 150만 명… (실제 1500000 명)
  mismatch     평균 주문 금액은 20만 달러… (실제 145274 달러)

파일은 engine.verify_file("연차보고서.pdf") (PDF·DOCX·txt), 일회성은 한 줄:

report = sv.verify("2023년 매출은 500억 원이다.", provider="upstage", data="kpi.csv")
if not report.ok:
    print("거짓 주장:", [r.claim for r in report.mismatches])

정답 데이터 연결 (DataSource)

# 회사 CSV (지표,연도,지역,값,단위 행) — 컬럼명이 다르면 매핑
sv.DataSource.csv("kpi.csv", columns={"value": "amount"})

# 정돈된 DB 표 — SQLAlchemy DSN이면 어떤 DB든 (sqlite·postgres·Snowflake·mysql)
sv.DataSource.db("snowflake://user:pw@account/DB/SCHEMA?warehouse=WH",
                 table="REPORTING.KPI",
                 columns={"indicator": "NAME", "time_period": "YEAR", "value": "VALUE"})

# 원시 트랜잭션 테이블 — 지표 정의 없이 (에이전틱 text-to-SQL)
sv.DataSource.db(dsn, agentic=True, tables=["ORDERS", "LINEITEM", "CUSTOMER"])

# 회사 문서 폴더 (PDF/txt) — 의미검색 정답
sv.DataSource.docs("./policies")

# 내장 공공통계 (KOSIS) — extra [kosis] 필요
sv.DataSource.kosis()
에이전틱 모드(agentic=True)가 핵심. "총 매출=SUM(가격×할인)" 같은 지표 정의를 사람이 안 줘도 에이전트가 스키마를 조사해 claim마다 읽기전용 집계 SELECT를 쓰고, 비율·증감율까지 계산합니다. 지표가 방대하면 임베딩 의미검색으로 자동 전환 (use_embedding="auto", embed_threshold=200으로 제어), 인덱스는 첫 사용 때만 구축 후 디스크 캐시.

결과 다루기 (Report · Result · Verdict)

report.ok            # mismatch가 하나도 없으면 True — bool(report)와 동일
report.all_match     # 더 엄격: 모든 주장이 적극적으로 확인됨
report.mismatches    # 반박된 주장만 (조치가 필요한 것들) · .matches / .unverifiable

for r in report:      # 각 주장 = Result
    r.verdict        # "match" | "mismatch" | "unverifiable"
    r.claim, r.reason, r.confidence
    r.value, r.unit, r.source      # 대조한 공식 값·단위·출처

print(json.dumps(report.to_dict(), ensure_ascii=False, indent=2)[:200])
{
  "ok": false,
  "total": 2,
  "matches": 1,
  "mismatches": 1,
  "unverifiable": 0,
  "results": [{"verdict": "match", "claim": "누적 고객 수는 150만 명…", ...
판정사실 검증 (Result)규정 준수 (Verdict)
긍정match: 정답 데이터와 일치 (허용오차 내)compliant: 기준 충족 (.compliant)
부정mismatch: 데이터와 어긋남, 문서가 틀림violation: 기준 위반 (.violated)
불가unverifiable: 연결된 데이터로 확인 불가 (거짓 아님)

설정과 로깅

# 로깅 — 콘솔 + 파일. verbose=True면 내부 상세까지
sv.configure_logging("run.log", level="INFO", verbose=False)

# config를 미리 만들어 세부 조정 후 재사용
cfg = sv.build_config(provider="upstage", api_key="up_...", tolerance=2.0, data=data)
cfg["llm"]["max_concurrency"] = 2          # 429(rate limit) 나면 낮추기
cfg["llm"]["min_call_interval_ms"] = 800   # 호출 간 간격(ms)
cfg["agent"]["loop"]["max_iterations"] = 6 # claim당 에이전트 반복 상한
engine = sv.Verifier(config=cfg)

provider는 upstage · openai · gemini · hcx. 전체 기본값은 패키지 동봉 config/default.yaml, 키별 설명은 레퍼런스 매뉴얼 7장(config 설정값) 참조.

진행상황 보기 (progress_dashboard)

with sv.progress_dashboard():                 # 로컬 웹페이지(localhost:8765) + 터미널
    report = engine.verify_file("report.pdf")

with sv.progress_dashboard(web=False): ...   # 터미널 진행바만
with sv.progress_dashboard(web=False, terminal=False): ... # 완전 조용

코드 수정 없이 환경변수로: SV_PROGRESS=off|terminal|web. 색 끄기 NO_COLOR=1.

학습 루프 (LearningLoop GPU는 학습 단계만)

검증에서 나온 확정 정답으로 자체 7B 모델을 LoRA 파인튜닝합니다. 감독 에이전트 3종 (DataCurator · TrainDoctor · EvalGate)이 데이터 품질·학습 이상·회귀를 관리합니다. 왜/무엇을 학습하는지는 학습 가이드 참조, 이 절에서는 실행 흐름만 다룹니다.

① 데이터 수집 → 품질검사

from structverify.training import LearningLoop

loop = LearningLoop(engine, base_model="unsloth/Qwen2.5-7B-Instruct")
loop.add_seed()                          # 내장 시드 (한국어 숫자 파싱·SQL·판정 톤)
loop.add_reports(reports)                 # 검증 운영 Report들 — 확정 정답이 핵심 재료
loop.add_jsonl("corrections.jsonl")      # 사람 교정·합성 예시 (chat jsonl)

ds, curation = loop.prepare("train.jsonl")
[DataCurator] 총 10038 → 통과 9987 · 격리 51
  태스크 분포(통과): schema 3302, sql 7, verdict 6678
   태스크 'sql' 7건 (0.1%) — 편중. 이 태스크는 학습 효과가 미미할 수 있음
  격리(중복): 주장과 근거 데이터를 대조해 판정. 주장: "시장 점유율은 16.0%…"
  … 외 39건

② 학습: 실시간 감독 + 조기 종료

# backend 자동 감지: NVIDIA→QLoRA(unsloth) · Apple Silicon→MLX
# run=False(기본)면 GPU 머신에서 실행할 명령만 반환(핸드오프)
loop.train("train.jsonl", "./adapter", backend="auto",
           run=True, early_stop=True, patience=200)
 학습 엔진 준비 — unsloth · NVIDIA L4 (22GB) · bf16
 베이스 모델 로드 — unsloth/Qwen2.5-7B-Instruct (4bit)
 데이터 준비·토큰화 중 …
 실시간 감독 시작 — patience 200 · 조기 종료 ON
 516/2496 ▓▓░░░░░░░░░░ 21% · loss 0.118 ▁▃▃▇█▆▆▄ · ETA 2:17:26 · 1 · 수렴 완료
 step 516: 200스텝 동안 이동평균 개선 없음 (최저 0.1164, step 316).
  더 학습해도 이득이 없어 조기 종료합니다.
 실시간 감독 요약 — 516스텝 관찰
  loss 2.802 → 0.118 · 스파이크 1건 (step 146) — 모두 복귀함
 완료 — 어댑터: ./adapter (trainer_state.json · training_meta.json 포함)

같은 데이터로 조기 종료 없이 2,496스텝(약 3시간)을 학습한 결과와 품질이 동일했으며, GPU 시간은 79% 절약되었습니다.

③ 사후 진단

loop.diagnose("./adapter/trainer_state.json")
 TrainDoctor — 정상
  steps=516 · loss 2.8019→0.1177 · min=0.0876
  [정보] 일시적 spike 1건 @ step [146] — 모두 즉시 복귀 (정상 노이즈)
  [정보] 실질 수렴 @ step ~130 — 전체 516스텝 중 75%는 개선 없이 소모됨
  → 다음 학습은 조기 종료(기본값)에 맡기거나 steps=169 수준으로 — GPU 시간 75% 절약

④ 스모크 테스트 (진짜 배웠나)

학습 데이터에 없는 새 문장으로 일반화를 확인합니다 (GPU 머신에서):

python -m structverify.training.recipe.sample --adapter ./adapter
[schema] 당사와 협력하는 물류 파트너는 2,300개사에 달했습니다.
  → {"indicator": "협력하는 물류 파트너 수", "value": 2300, "unit": "개사", ...}

[verdict] 분기 반품률은 12.5%였습니다.   (근거: 8.3 %)
  → {"verdict": "mismatch", "reason": "주장은 실제 8.3%와 약 51% 차이로 불일치합니다."}

(12.5−8.3)/8.3 = 50.6%, 근거문의 산술까지 정확합니다.

학습 모델 주입 (어댑터를 파이프라인에 꽂기)

# ① 어댑터를 OpenAI 호환으로 서빙 (Ollama 또는 vLLM)
#    Modelfile: FROM qwen2.5:7b-instruct / ADAPTER ./adapter
ollama create sv-tuned -f Modelfile && ollama serve

# ② config 주입 — 티어 단위 "부분" 주입 (학습된 자리만 점진 전환)
cfg = sv.build_config(provider="upstage", api_key="none", data=data)
cfg["llm"]["base_url"] = "http://GPU서버:11434/v1"
cfg["llm"]["models"]   = {"structured": "sv-tuned"}   # 스키마·판정 자리만
tuned = sv.Verifier(config=cfg)

# ③ EvalGate — 채택은 증명 후에 (학습 전/후를 같은 평가셋으로)
gate = loop.evaluate(eval_set, tuned_engine=tuned)
 EvalGate —  채택
  before: 정확도 82.0% (41/50)     ← 기존 클라우드 엔진
  after : 정확도 91.0% (45/50)     ← 학습 모델 주입 엔진
  Δ정확도 +9.0% (margin +0.0%) — 개선 확인, 채택
티어(heavy/light/structured)와 파이프라인 자리의 매핑은 학습 가이드, 모델이 쓰이는 자리 지도를 참조하십시오.

전체 예제 (복사해서 바로 실행)

시나리오별 완결 코드입니다. 펼친 뒤 코드 블록 우상단의 복사 버튼으로 통째로 가져가세요 (코드 블록마다 복사 버튼이 있습니다). 시연 데이터를 쓰는 예제는 저장소의 examples/demo/ 데이터를 참조하며, 경로만 내 파일로 바꾸면 됩니다.

01_quickstart_conformance.py: 규정 준수 빠른 시작, 색인·검사·일괄 판정 필요: API 키
"""① 빠른 시작 — 규정 준수 검사 (대표 사용례).

내 규정집(PDF/txt)을 색인하고, 문서가 규정을 지켰는지 True/False로 읽는다.

필요: pip install structverify   +   UPSTAGE_API_KEY (.env 또는 환경변수)
실행: python examples/01_quickstart_conformance.py
"""
import structverify as sv

sv.configure_logging(level="WARNING")   # 결과만 보이게

# ── 1) 규정집 색인 — provider만 정하면 끝 (조항 단위 청킹 + 임베딩) ──
rules = sv.Ruleset.from_file("examples/demo/식품영양강조표시기준.txt", provider="upstage")
print(f"색인된 조항: {len(rules)}개")

# ── 2) 문장 하나 검사 → Verdict ──
v = rules.check("이 제품은 나트륨 함량이 100g당 200mg이므로 '低나트륨'으로 표시하였다.")
print("준수 여부 :", v.compliant)          # bool — if 문에 바로 사용
print("적용 조항 :", v.article)
print("기준 vs 주장:", v.rule_value, "vs", v.claim_value, v.unit or "")
print("근거     :", v.reason)

# ── 3) 문서 전체 — 측정값이 있는 줄마다 자동 판정 ──
for r in rules.check_file("examples/demo/영양성분_분석서.txt"):
    if r.is_unverifiable:
        continue
    print((" 준수" if r.compliant else " 위반"), r.article, "—", (r.claim or "")[:40])

# 큰 규정집이면 agent=True — 검색→판정→검색어 재구성→재검색을 스스로 반복(ReAct):
#   rules = sv.Ruleset.from_file("big_rulebook.pdf", provider="upstage", agent=True)
#   v.iterations 로 몇 번 만에 확정했는지 확인 가능
02_factcheck_csv.py: 사실 검증, CSV 정답 데이터와 Report 다루기 필요: API 키
"""② 사실 검증 — 회사 CSV를 정답 데이터로.

참조 통계 CSV(지표·연도·값·단위 행)를 정답으로 연결하고, 문서 속 수치 주장을
자동으로 참/거짓 판정한다.

필요: pip install structverify   +   UPSTAGE_API_KEY
실행: python examples/02_factcheck_csv.py
"""
import structverify as sv

sv.configure_logging(level="WARNING")

# ── 1) 정답 데이터 연결 + 엔진 준비 (재사용 가능) ──
engine = sv.Verifier(
    provider="upstage",
    data=sv.DataSource.csv("examples/demo/factcheck_public_enterprise.csv"),
)

# ── 2) 텍스트 검증 → Report (주장별 Result의 컬렉션) ──
report = engine.verify(
    "한국전력공사의 2023년 부채비율은 600%에 달했다. "
    "같은 해 임직원 수는 2만 3천여 명이었다."
)

# ── 3) 결과 읽기 ──
print("문서 통과? ", report.ok)            # 거짓(mismatch) 주장이 없으면 True
for r in report:
    print(f" {r.verdict:13s} {r.claim[:38]}…")
    if r.is_mismatch:
        print(f"    → 실제 값: {r.value} {r.unit or ''} ({r.source}) — {r.reason[:60]}")

print("반박된 주장만:", len(report.mismatches), "건")
print(report.to_dict()["summary"] or "")    # JSON 직렬화도 지원

# 파일 검증은 engine.verify_file("연차보고서.pdf") — PDF/DOCX/txt 지원
# 일회성이면 sv.verify("문장", provider="upstage", data="kpi.csv") 한 줄로도 충분
03_database_agentic.py: 회사 DB, 정돈 표와 에이전틱 text-to-SQL (sqlite 셀프컨테인드) 필요: API 키 + [db]
"""③ 회사 DB 검증 — 정돈된 표 & 에이전틱 모드.

SQLAlchemy DSN이면 어떤 DB든 연결된다 (sqlite·postgres·mysql·Snowflake).
이 예제는 sqlite 파일을 즉석에서 만들어 셀프컨테인드로 시연한다.

필요: pip install "structverify[db]"   +   UPSTAGE_API_KEY
실행: python examples/03_database_agentic.py
"""
import sqlite3

import structverify as sv

sv.configure_logging(level="WARNING")

# ── 0) 시연용 DB 준비 (실전에서는 이미 있는 회사 DB에 연결) ──
db = "examples/_demo_company.db"
con = sqlite3.connect(db)
con.executescript("""
DROP TABLE IF EXISTS kpi;
CREATE TABLE kpi (name TEXT, year INT, value REAL, unit TEXT);
INSERT INTO kpi VALUES
  ('누적 고객 수', 2023, 1500000, '명'),
  ('평균 주문 금액', 2023, 145274, '달러'),
  ('연간 순매출', 2023, 3304, '억 달러');
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (order_id INT, order_date TEXT, amount REAL);
INSERT INTO orders SELECT value, '2023-0' || (value % 9 + 1) || '-01', value * 137.5
  FROM (WITH RECURSIVE n(value) AS (SELECT 1 UNION ALL SELECT value+1 FROM n LIMIT 200) SELECT value FROM n);
""")
con.commit(); con.close()

# ── A) 정돈된 지표 표 — 컬럼 매핑만 주면 끝 ──
tidy = sv.DataSource.db(
    f"sqlite:///{db}", table="kpi",
    columns={"indicator": "name", "time_period": "year", "value": "value", "unit": "unit"},
)
report = sv.verify("2023년 평균 주문 금액은 20만 달러였다.", provider="upstage", data=tidy)
print("[정돈 표] 판정:", report[0].verdict, "— 실제", report[0].value, report[0].unit)

# ── B) 에이전틱 모드 — 원시 트랜잭션 테이블, 지표 정의 없이 ──
#    에이전트가 스키마를 조사해 claim마다 읽기전용 집계 SELECT를 자동 생성한다.
raw = sv.DataSource.db(f"sqlite:///{db}", agentic=True, tables=["orders"])
report = sv.verify("2023년 총 주문 건수는 200건이다.", provider="upstage", data=raw)
print("[에이전틱] 판정:", report[0].verdict, "—", report[0].reason[:70])

# 임베딩 의미검색 제어 (지표가 수백 개 이상일 때):
#   sv.DataSource.db(dsn, table="BIG_KPI", use_embedding="auto", embed_threshold=200)
# Snowflake 예:
#   sv.DataSource.db("snowflake://user:pw@account/DB/SCHEMA?warehouse=WH", table="REPORTING.KPI", ...)
04_config_logging_progress.py: 설정 세부 조정·로깅·실시간 대시보드 필요: API 키
"""④ 설정·로깅·진행상황 — 운영에서 쓰는 손잡이들.

필요: pip install structverify   +   UPSTAGE_API_KEY
실행: python examples/04_config_logging_progress.py
"""
import structverify as sv

# ── 1) 로깅 — 콘솔 + 파일, verbose로 상세도 제어 ──
sv.configure_logging("verification.log", level="INFO", verbose=False)

# ── 2) config를 미리 만들어 재사용 — 세부 키는 dict로 직접 조정 ──
cfg = sv.build_config(
    provider="upstage",              # upstage | openai | gemini | hcx
    tolerance=2.0,                   # 수치 허용오차(%) — verification.tolerance_percent
    data="examples/demo/factcheck_public_enterprise.csv",
)
cfg["llm"]["max_concurrency"] = 2          # 429(rate limit) 나면 낮추기
cfg["llm"]["min_call_interval_ms"] = 800   # 호출 간 간격(ms)도 함께
cfg["agent"]["loop"]["max_iterations"] = 6 # claim당 에이전트 반복 상한

engine = sv.Verifier(config=cfg)

# ── 3) 진행상황 — 로컬 웹 대시보드 + 터미널 진행바 ──
#    환경변수로도 제어: SV_PROGRESS=off|terminal|web (인자보다 우선)
with sv.progress_dashboard(web=False):      # 터미널만. 웹까지: sv.progress_dashboard()
    report = engine.verify("한국전력공사의 2023년 부채비율은 600%에 달했다.")

print("판정:", [r.verdict for r in report], "→ verification.log에 로그 저장됨")
05_training_loop.py: 감독 학습 루프 전체, GPU 없이 실행 가능 필요: 없음
"""⑤ 감독 학습 루프 — 정답 데이터로 자체 모델 기르기.

이 파일은 GPU 없이 끝까지 실행된다 (학습은 핸드오프 명령만 출력).
GPU 머신(Colab T4/L4, RTX 3060+)에서는 run=True로 바꾸면 그 자리에서 학습.

필요: pip install structverify            # 코어는 GPU 불필요
       pip install "structverify[training]"      # NVIDIA에서 실제 학습 시
       pip install "structverify[training-mac]" # Apple Silicon에서 실제 학습 시
실행: python examples/05_training_loop.py
"""
import json

from structverify.training import LearningLoop
from structverify.training.dataset import write_jsonl
from structverify.training.tasks import build_example

# ── 1) 학습 데이터 만들기 — 정답 표에서 match/mismatch 예시를 프로그램 생성 ──
#    실전에서는 검증 운영 결과를 loop.add_reports(reports)로 넣는 게 핵심 재료.
GROUND_TRUTH = [
    {"indicator": "누적 고객 수", "value": 1_500_000, "unit": "명"},
    {"indicator": "평균 주문 금액", "value": 145_274, "unit": "달러"},
    {"indicator": "유럽 순매출 비율", "value": 20.05, "unit": "%"},
]
rows = []
for g in GROUND_TRUTH:
    v, u = g["value"], g["unit"]
    rows.append(build_example(                      # match 예시
        "verdict", claim_text=f"{g['indicator']}는 {v:,}{u}입니다.",
        evidence_value=v, evidence_unit=u,
        output=json.dumps({"verdict": "match",
                           "reason": f"주장이 공식 수치 {v:,}{u}와 일치합니다."}, ensure_ascii=False)))
    rows.append(build_example(                      # mismatch 예시 (40% 부풀림)
        "verdict", claim_text=f"{g['indicator']}는 {round(v*1.4):,}{u}에 달합니다.",
        evidence_value=v, evidence_unit=u,
        output=json.dumps({"verdict": "mismatch",
                           "reason": f"주장은 실제 {v:,}{u}와 약 40% 차이로 불일치합니다."}, ensure_ascii=False)))
write_jsonl(rows, "gt_dataset.jsonl")

# ── 2) 수집 → 품질검사 → 학습 ──
loop = LearningLoop(base_model="unsloth/Qwen2.5-7B-Instruct")
loop.add_seed().add_jsonl("gt_dataset.jsonl")        # 시드(한국어 파싱·SQL·판정 톤) + 내 데이터
ds, curation = loop.prepare("train.jsonl")           # 중복·라벨오류·편중 격리 + 사유 리포트

# run=False: 학습 머신에서 실행할 명령만 출력(핸드오프). GPU 머신이면 run=True.
# 학습 중에는 실시간 감독 한 줄 UI가 뜨고, 수렴하면 스스로 조기 종료한다.
info = loop.train("train.jsonl", "./adapter", backend="auto",
                  run=False, steps=500, early_stop=True, patience=200)

# ── 3) 학습 후 (GPU 머신에서) ──
# loop.diagnose("./adapter/trainer_state.json")      # 사후 진단 + 처방
# 스모크 테스트: python -m structverify.training.recipe.sample --adapter ./adapter
# 자가평가/채택: loop.evaluate(eval_set, tuned_engine=tuned) → .accepted
06_custom_model_injection.py: 학습 어댑터 주입과 EvalGate 채택 필요: 어댑터 서빙
"""⑥ 학습된 모델 주입 — 어댑터를 파이프라인에 꽂고 EvalGate로 채택.

사전 준비 (GPU 서버에서 어댑터를 OpenAI 호환으로 서빙):
    # Ollama: Modelfile → FROM qwen2.5:7b-instruct / ADAPTER ./adapter
    ollama create sv-tuned -f Modelfile && ollama serve
    # vLLM:
    vllm serve Qwen/Qwen2.5-7B-Instruct --enable-lora --lora-modules sv-tuned=./adapter

필요: pip install structverify   (+ 위 서빙이 떠 있어야 실제 실행됨)
실행: python examples/06_custom_model_injection.py
"""
import structverify as sv
from structverify.training import EvalGate

data = sv.DataSource.csv("examples/demo/factcheck_public_enterprise.csv")

# ── 1) 기존(클라우드) 엔진 — 비교 기준 ──
base = sv.Verifier(provider="upstage", data=data)

# ── 2) 학습 모델 주입 — base_url + 티어별 "부분" 주입이 핵심 ──
#    llm.models는 티어 단위로 덮어쓸 수 있어서, 학습된 자리만 자체 모델로 점진 전환:
#      structured = 스키마 추출·판정 ·  heavy = 에이전틱 SQL·설명 ·  light = 분류·랭킹
cfg = sv.build_config(provider="upstage", api_key="none", data=data)
cfg["llm"]["base_url"] = "http://localhost:11434/v1"      # 자체 서빙 주소 (Ollama/vLLM)
cfg["llm"]["models"] = {"structured": "sv-tuned"}         # 판정·추출 자리만 교체
tuned = sv.Verifier(config=cfg)

# ── 3) EvalGate — 채택은 증명 후에 (학습 전/후를 같은 평가셋으로 채점) ──
eval_set = [
    {"text": "한국전력공사의 2023년 부채비율은 600%에 달했다.", "expected": "mismatch"},
    {"text": "한국전력공사의 2023년 임직원 수는 23,000여 명이다.", "expected": "match"},
    # ... 라벨된 문장 30~50개 권장
]
gate = EvalGate(base).evaluate(eval_set, tuned_engine=tuned, margin=0.0)
print(gate.summary())            # before/after 정확도 + 채택 / 거부
if gate.accepted:
    engine = tuned               # 다음 검증부터 학습된 모델 사용 (나빠졌으면 자동 거부)

문제 해결

증상원인 · 해결
No matching distribution found for structverifyPython이 3.9 이하: python --version 확인 후 3.10+ 설치. (버전이 맞는데도 나오면 pip install --no-cache-dir -i https://pypi.org/simple structverify)
키 관련 에러 (api_key / 401)환경변수 이름이 provider와 맞는지 확인 (upstage→UPSTAGE_API_KEY). .env는 실행 위치 기준으로 읽힘: 스크립트를 다른 폴더에서 실행하면 못 찾습니다. 확실하게 하려면 api_key= 인자로 직접.
429 (rate limit) 에러가 반복됨동시 호출을 줄이세요: cfg["llm"]["max_concurrency"] = 1, cfg["llm"]["min_call_interval_ms"] = 1000 (설정과 로깅 섹션 참조)
전부 unverifiable로 나옴거짓이 아니라 "연결된 데이터로 확인 불가"입니다. ① 정답 데이터에 해당 지표가 실제로 있는지, ② CSV 헤더/컬럼 매핑이 맞는지, ③ 지표 이름이 문서 표현과 너무 다르면 임베딩 검색을 켜보세요(use_embedding="true").
느리다문서 하나에 LLM을 수십 번 호출합니다: 정상. 진행 상황은 with sv.progress_dashboard():로 확인, 급하면 agent.loop.max_iterations를 낮추세요.
내부 동작이 보고 싶다sv.configure_logging(level="DEBUG", verbose=True): 검색·판정 과정이 전부 로그로.

API 치트시트

하고 싶은 것호출
규정 준수: 문장 하나sv.Ruleset.from_file(pdf, provider=...).check(stmt) → Verdict
규정 준수: 문서 전체rules.check_file(path) → list[Verdict]
사실 검증: 일회성sv.verify(text, provider=..., data=...) → Report
사실 검증: 재사용 엔진sv.Verifier(provider=..., data=...).verify_file(pdf)
정답 데이터DataSource.csv / db(…, agentic=True) / docs / kosis
설정 · 로깅sv.build_config(...) · sv.configure_logging(...)
진행상황with sv.progress_dashboard(): ... · SV_PROGRESS
학습LearningLoop(...).add_seed().add_reports(...).prepare() → .train() → .diagnose() → .evaluate()
어댑터 확인python -m structverify.training.recipe.sample --adapter ./adapter

StructVerify v0.3.4 · MIT License