BaseComponent는 define-by-run(순수 파이썬 제어) 철학을 유지하면서도, 컴포넌트 실행을 관측 가능(observable) 하게 만들기 위한 선택적(opt-in) 표준 레이어입니다.
- 파이프라인은
step(run: RunContext) -> RunContext형태의 그냥 함수/콜러블만으로도 충분히 동작합니다. BaseComponent는 그 위에 추적(hooks), 에러 표준화, 이름/형식 통일을 얹어주는 역할을 합니다.
즉, 필수는 아니지만, 라이브러리/팀 단위 개발에서 “운영 가능한 형태”로 만들고 싶을 때 유용합니다.
Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대신:
- 사용자는 Python
if/for/while로 제어한다. - 라이브러리는 관측성은 hook 이벤트로 제공한다.
BaseComponent는 각 컴포넌트 실행의 start/end/error를 이벤트로 남깁니다.
현실에서는 ValueError, KeyError, 외부 라이브러리 예외 등이 섞여서 올라옵니다.
BaseComponent는:
Lang2SQLError(ValidationError, IntegrationMissingError 등)는 그대로 유지- 그 외 예외는
ComponentError로 표준 래핑(+ 원인 예외를cause로 보존)
→ 사용자/운영자 관점에서 “어디서 터졌는지”가 분명해집니다.
라이브러리 제공 컴포넌트를 모두 BaseComponent 기반으로 만들면:
- 로그/트레이스의 포맷이 통일
- 테스트/디버깅 경험이 일정
- 문서/타입 힌트가 일관
Lang2SQL의 기본 철학은 아래 2개입니다.
-
제어는 파이썬으로 루프/분기/재시도/서브플로우 호출은 “프레임워크 DSL”이 아니라 Python으로 표현합니다.
-
상태는 RunContext 하나로 파이프라인이 커져도, step 간 연결이 깨지지 않도록
RunContext를 I/O로 둡니다.
BaseComponent는 이 철학을 해치지 않습니다.
컴포넌트의 실행을 감싸서 이벤트만 남길 뿐, 그래프/스키마/실행 모델을 강제하지 않습니다.
BaseComponent(name: str | None = None, hook: TraceHook | None = None)name: 이벤트에 찍힐 컴포넌트 이름 (기본값: 클래스명)hook: 이벤트 수신자. 기본값은NullHook()(아무것도 하지 않음)
class MyComp(BaseComponent):
def run(self, run: RunContext) -> RunContext:
...
return runcomp(run)을 호출하면 내부적으로 아래를 자동 수행합니다.
-
component.run start 이벤트 발행 -
self.run(...)실행 -
성공 시
end 이벤트+duration_ms -
실패 시
error 이벤트- 도메인 예외(
Lang2SQLError)는 그대로 raise - 그 외 예외는
ComponentError로 래핑해서 raise
- 도메인 예외(
Lang2SQL의 기본 step 규약은 단순합니다.
RunContext를 받으면 RunContext를 반환한다. (
return run을 습관처럼)
왜냐하면 “None 반환”은 인간이 보기엔 자연스럽지만, 팀/사용자 관점에서는 실수를 만들기 쉽습니다.
return None은 “의도적”인지 “실수(반환 누락)”인지 구분이 안 됨- Flow/컴포넌트 조합에서 결과가 조용히 깨지기 쉬움
그래서 Lang2SQL은 fail-fast 스타일을 권장합니다.
- 라이브러리 기본 제공 컴포넌트( retriever/builder/generator/validator )
- 팀/제품 환경에서 관측성(트레이싱)이 필요한 경우
- 예외 표준화가 중요한 경우(운영/테스트/디버깅)
policy,eval, metric 계산처럼 순수 함수 성격이 강한 로직- “유저가 빠르게 붙여 넣어 쓰는” 초경량 커스텀 로직
- 실행 단위가 너무 작아 이벤트가 과도해지는 경우
즉, 핵심 파이프라인 축은 BaseComponent로 잡고, 그 외의 작은 로직은 함수로 두는 혼합형이 가장 자연스럽습니다.
유저에게 “클래스 상속 + run 메서드 작성”이 부담인 경우가 많습니다. 그래서 함수/콜러블을 그대로 유지하면서도 트레이싱을 얻고 싶다면 래퍼를 제공합니다.
from __future__ import annotations
from typing import Callable, Any, Optional
from .base import BaseComponent
from .context import RunContext
class FunctionalComponent(BaseComponent):
"""
Wrap a callable(run: RunContext) -> RunContext into a BaseComponent,
so it becomes traceable and error-normalized.
"""
def __init__(
self,
fn: Callable[[RunContext], RunContext],
*,
name: str | None = None,
hook=None,
) -> None:
super().__init__(name=name or getattr(fn, "__name__", "FunctionalComponent"), hook=hook)
self._fn = fn
def run(self, run: RunContext) -> RunContext:
return self._fn(run)def my_retriever(run: RunContext) -> RunContext:
run.schema_selected = ...
return run
retriever = FunctionalComponent(my_retriever, name="MyRetriever", hook=hook)이 방식의 장점: 유저는 “함수 스타일” 그대로 유지하면서, 운영/디버깅을 위한 트레이싱을 얻게 됩니다.
컴포넌트/플로우 실행 시점에 이벤트(Event) 를 받는 인터페이스입니다.
start/end/error시점 기록- 소요 시간(duration_ms)
- 입력/출력 요약(input_summary/output_summary)
- 필요하면
data에 구조화된 값을 추가
가장 쉬운 건 MemoryHook입니다.
from lang2sql.core.hooks import MemoryHook
from lang2sql.flows.baseline import SequentialFlow
hook = MemoryHook()
flow = SequentialFlow(steps=[...], hook=hook) # 또는 컴포넌트마다 hook 주입
out = flow.run("지난달 매출")
# 이벤트 확인
for e in hook.snapshot():
print(e.phase, e.component, e.duration_ms, e.error)운영에서는 MemoryHook 대신 다음이 일반적입니다.
- 로그로 흘리는 Hook (stdout / JSON log)
- APM/Tracing으로 보내는 Hook (OpenTelemetry span 등)
- 필터링 Hook (특정 컴포넌트만 샘플링)
핵심은: 관측성은 hook 구현체에서 제어하고, 파이프라인/컴포넌트 코드는 최대한 “비즈니스 로직”만 갖도록 분리합니다.
“깨진다”기보다는 이벤트가 더 많이 찍힙니다.
-
flow_b안에flow_a를 step으로 넣으면flow_b이벤트 2개(시작/끝)flow_a이벤트 2개(시작/끝)a1/a2컴포넌트 이벤트도 각각 찍힘(컴포넌트가 BaseComponent라면)
이게 싫다면 두 가지 선택지가 있습니다.
- 상위 레벨(Flow)만 트레이싱하고 내부는 함수로 둔다
- Hook에서 필터링/샘플링한다 (예: component 이름 prefix로 제외)
추가 문법 없이 해결하려면 2번이 가장 현실적입니다.
class Retriever(BaseComponent):
def __init__(self, catalog, top_k=8, ...):
self.catalog = catalog # 고정 설정
self.top_k = top_k
def run(self, run: RunContext) -> RunContext:
# 요청마다 달라지는 값은 run에서 읽고 run에 쓴다
...
return run- 가독성(계약이 분명)
- 실수 방지(fail-fast)
- flow 합성 시 안정
- BaseComponent로 감싸는 건 선택
- 운영에서 꼭 추적이 필요할 때만 FunctionalComponent로 감싼다
A. 운영/디버깅/협업에서 차이가 큽니다. 문제 났을 때 “어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로” 터졌는지 자동으로 남는 게 핵심 가치입니다.
A. 필수 아닙니다. 초급 유저는 SequentialFlow + 프리셋 컴포넌트만으로 충분히 쓰게 하고, 고급/운영 유저에게 BaseComponent/Hook을 제공하는 구성이 가장 자연스럽습니다.
A. 맞습니다. policy(metrics) -> action 같은 건 순수 함수로 두는 걸 권장합니다.
필요하면 FunctionalComponent(policy_fn)처럼 감싸서 추적만 추가할 수 있습니다.