22
33` BaseComponent ` 는 ** define-by-run(순수 파이썬 제어)** 철학을 유지하면서도, 컴포넌트 실행을 ** 관측 가능(observable)** 하게 만들기 위한 ** 선택적(opt-in) 표준 레이어** 입니다.
44
5- * 파이프라인은 ` step(run: RunContext) -> RunContext ` 형태의 ** 그냥 함수/콜러블 ** 만으로도 충분히 동작합니다.
5+ * 파이프라인은 그냥 함수/콜러블만으로도 충분히 동작합니다.
66* ` BaseComponent ` 는 그 위에 ** 추적(hooks), 에러 표준화, 이름/형식 통일** 을 얹어주는 역할을 합니다.
77
8- 즉, ** 필수는 아니지만** , 라이브러리/팀 단위 개발에서 “ 운영 가능한 형태” 로 만들고 싶을 때 유용합니다.
8+ 즉, ** 필수는 아니지만** , 라이브러리/팀 단위 개발에서 " 운영 가능한 형태" 로 만들고 싶을 때 유용합니다.
99
1010---
1111
1212## 왜 필요한가?
1313
14- ### 1) 관측성(Tracing)을 “ 그래프 엔진 없이” 얻기 위해
14+ ### 1) 관측성(Tracing)을 " 그래프 엔진 없이" 얻기 위해
1515
16- Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대신:
16+ Lang2SQL은 그래프 엔진을 강제하지 않습니다. 대신:
1717
1818* 사용자는 Python ` if/for/while ` 로 제어한다.
1919* 라이브러리는 관측성은 ** hook 이벤트** 로 제공한다.
2020
2121` BaseComponent ` 는 각 컴포넌트 실행의 ` start/end/error ` 를 이벤트로 남깁니다.
2222
23- ### 2) 에러를 “ 도메인 친화적으로” 정리하기 위해
23+ ### 2) 에러를 " 도메인 친화적으로" 정리하기 위해
2424
2525현실에서는 ` ValueError ` , ` KeyError ` , 외부 라이브러리 예외 등이 섞여서 올라옵니다.
2626
@@ -29,9 +29,9 @@ Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대
2929* ` Lang2SQLError ` (ValidationError, IntegrationMissingError 등)는 ** 그대로 유지**
3030* 그 외 예외는 ` ComponentError ` 로 ** 표준 래핑** (+ 원인 예외를 ` cause ` 로 보존)
3131
32- → 사용자/운영자 관점에서 “ 어디서 터졌는지” 가 분명해집니다.
32+ → 사용자/운영자 관점에서 " 어디서 터졌는지" 가 분명해집니다.
3333
34- ### 3) “ 컴포넌트 단위 표준” 을 만들기 위해
34+ ### 3) " 컴포넌트 단위 표준" 을 만들기 위해
3535
3636라이브러리 제공 컴포넌트를 모두 BaseComponent 기반으로 만들면:
3737
@@ -41,21 +41,6 @@ Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대
4141
4242---
4343
44- ## 철학: Define-by-run + Minimal core
45-
46- Lang2SQL의 기본 철학은 아래 2개입니다.
47-
48- 1 . ** 제어는 파이썬으로**
49- 루프/분기/재시도/서브플로우 호출은 “프레임워크 DSL”이 아니라 Python으로 표현합니다.
50-
51- 2 . ** 상태는 RunContext 하나로**
52- 파이프라인이 커져도, step 간 연결이 깨지지 않도록 ` RunContext ` 를 I/O로 둡니다.
53-
54- ` BaseComponent ` 는 이 철학을 해치지 않습니다.
55- 컴포넌트의 실행을 감싸서 이벤트만 남길 뿐, 그래프/스키마/실행 모델을 강제하지 않습니다.
56-
57- ---
58-
5944## BaseComponent가 제공하는 API
6045
6146### 생성자
@@ -67,21 +52,27 @@ BaseComponent(name: str | None = None, hook: TraceHook | None = None)
6752* ` name ` : 이벤트에 찍힐 컴포넌트 이름 (기본값: 클래스명)
6853* ` hook ` : 이벤트 수신자. 기본값은 ` NullHook() ` (아무것도 하지 않음)
6954
70- ### 구현해야 하는 것: ` run() `
55+ ### 구현해야 하는 것: ` _run() `
56+
57+ 서브클래스는 ` _run() ` 을 구현합니다. 인자 타입과 반환 타입은 각 컴포넌트에 맞게 자유롭게 정의합니다.
7158
7259``` python
73- class MyComp (BaseComponent ):
74- def run (self , run : RunContext) -> RunContext:
75- ...
76- return run
60+ class MyRetriever (BaseComponent ):
61+ def __init__ (self , catalog : list , ** kwargs ):
62+ super ().__init__ (** kwargs)
63+ self ._catalog = catalog
64+
65+ def _run (self , query : str ) -> list[dict ]:
66+ # 비즈니스 로직
67+ return [t for t in self ._catalog if query in t[" description" ]]
7768```
7869
79- ### 실행: ` __call__ `
70+ ### 호출: ` run() ` / ` __call__ `
8071
81- ` comp( run) ` 을 호출하면 내부적으로 아래를 자동 수행합니다.
72+ ` comp. run(query) ` 또는 ` comp(query) ` 를 호출하면 내부적으로 아래를 자동 수행합니다.
8273
8374* ` component.run start 이벤트 발행 `
84- * ` self.run (...) ` 실행
75+ * ` self._run (...) ` 실행
8576* 성공 시 ` end 이벤트 ` + ` duration_ms `
8677* 실패 시 ` error 이벤트 `
8778
@@ -90,90 +81,84 @@ class MyComp(BaseComponent):
9081
9182---
9283
93- ## 권장 규약: RunContext in → RunContext out
84+ ## 타입 인자 패턴
9485
95- Lang2SQL의 기본 step 규약은 단순합니다 .
86+ Lang2SQL의 컴포넌트는 ** 명시적 타입 인자 ** 를 받고, ** 명시적 타입 결과 ** 를 반환합니다 .
9687
97- > ** RunContext를 받으면 RunContext를 반환한다.**
98- > (` return run ` 을 습관처럼)
99-
100- 왜냐하면 “None 반환”은 인간이 보기엔 자연스럽지만, 팀/사용자 관점에서는 실수를 만들기 쉽습니다.
88+ ``` python
89+ # 라이브러리 내장 컴포넌트 시그니처 예시
90+ KeywordRetriever._run(query: str ) -> list[CatalogEntry]
91+ SQLGenerator._run(query: str , schemas: list[CatalogEntry], context: str = " " ) -> str
92+ SQLExecutor._run(sql: str ) -> list[dict ]
93+ ```
10194
102- * ` return None ` 은 “의도적”인지 “실수(반환 누락)”인지 구분이 안 됨
103- * Flow/컴포넌트 조합에서 결과가 조용히 깨지기 쉬움
95+ ### 구성(config)은 ` __init__ ` 에, 요청별 데이터는 ` _run() ` 인자에
10496
105- 그래서 Lang2SQL은 ** fail-fast** 스타일을 권장합니다.
97+ ``` python
98+ class SQLGenerator (BaseComponent ):
99+ def __init__ (self , llm : LLMPort, db_dialect : str = " default" , ** kwargs ):
100+ super ().__init__ (** kwargs)
101+ self ._llm = llm # 고정 설정
102+ self ._dialect = db_dialect
103+
104+ def _run (self , query : str , schemas : list[CatalogEntry], context : str = " " ) -> str :
105+ # 요청마다 달라지는 값은 _run() 인자로 받는다
106+ ...
107+ ```
106108
107109---
108110
109111## 언제 BaseComponent를 쓰는가?
110112
111- ### ✅ BaseComponent를 쓰는 게 좋은 경우
113+ ### BaseComponent를 쓰는 게 좋은 경우
112114
113- * 라이브러리 기본 제공 컴포넌트( retriever/builder/ generator/validator )
115+ * 라이브러리 기본 제공 컴포넌트(retriever/generator/executor )
114116* 팀/제품 환경에서 ** 관측성(트레이싱)이 필요한 경우**
115117* 예외 표준화가 중요한 경우(운영/테스트/디버깅)
116118
117- ### ✅ BaseComponent 없이 함수로 두는 게 좋은 경우
119+ ### BaseComponent 없이 함수로 두는 게 좋은 경우
118120
119121* ` policy ` , ` eval ` , metric 계산처럼 ** 순수 함수 성격** 이 강한 로직
120- * “ 유저가 빠르게 붙여 넣어 쓰는” 초경량 커스텀 로직
122+ * " 유저가 빠르게 붙여 넣어 쓰는" 초경량 커스텀 로직
121123* 실행 단위가 너무 작아 이벤트가 과도해지는 경우
122124
123125즉, ** 핵심 파이프라인 축** 은 BaseComponent로 잡고,
124126그 외의 작은 로직은 함수로 두는 혼합형이 가장 자연스럽습니다.
125127
126128---
127129
128- ## FunctionalComponent: “함수도 트레이싱하고 싶다”
130+ ## 커스텀 컴포넌트 예시
129131
130- 유저에게 “클래스 상속 + run 메서드 작성”이 부담인 경우가 많습니다.
131- 그래서 ** 함수/콜러블을 그대로 유지하면서 ** 도 트레이싱을 얻고 싶다면 래퍼를 제공합니다.
132+ ``` python
133+ from lang2sql.core.base import BaseComponent
132134
133- ### 예시: FunctionalComponent
135+ class UpperCaseSQL (BaseComponent ):
136+ """ SQL을 대문자로 변환하는 후처리 컴포넌트."""
137+ def _run (self , sql : str ) -> str :
138+ return sql.upper()
134139
135- ``` python
136- from __future__ import annotations
137- from typing import Callable, Any, Optional
138-
139- from .base import BaseComponent
140- from .context import RunContext
141-
142- class FunctionalComponent (BaseComponent ):
143- """
144- Wrap a callable(run: RunContext) -> RunContext into a BaseComponent,
145- so it becomes traceable and error-normalized.
146- """
147-
148- def __init__ (
149- self ,
150- fn : Callable[[RunContext], RunContext],
151- * ,
152- name : str | None = None ,
153- hook = None ,
154- ) -> None :
155- super ().__init__ (name = name or getattr (fn, " __name__" , " FunctionalComponent" ), hook = hook)
156- self ._fn = fn
157-
158- def run (self , run : RunContext) -> RunContext:
159- return self ._fn(run)
140+ upper = UpperCaseSQL()
141+ print (upper.run(" select 1" )) # SELECT 1
160142```
161143
162- ### 사용 예
144+ hook을 주입하면 실행 추적도 자동으로 됩니다:
163145
164146``` python
165- def my_retriever (run : RunContext) -> RunContext:
166- run.schema_selected = ...
167- return run
147+ from lang2sql import MemoryHook
168148
169- retriever = FunctionalComponent(my_retriever, name = " MyRetriever" , hook = hook)
170- ```
149+ hook = MemoryHook()
150+ upper = UpperCaseSQL(hook = hook)
151+ upper.run(" select 1" )
171152
172- > 이 방식의 장점: 유저는 “함수 스타일” 그대로 유지하면서, 운영/디버깅을 위한 트레이싱을 얻게 됩니다.
153+ for e in hook.snapshot():
154+ print (e.component, e.phase, e.duration_ms)
155+ # UpperCaseSQL start 0.0
156+ # UpperCaseSQL end 0.1
157+ ```
173158
174159---
175160
176- ## 훅(Tracing) 시스템이 뭐고, 왜 필요한가?
161+ ## 훅(Tracing) 시스템
177162
178163### Hook이란?
179164
@@ -182,22 +167,18 @@ retriever = FunctionalComponent(my_retriever, name="MyRetriever", hook=hook)
182167* ` start/end/error ` 시점 기록
183168* 소요 시간(duration_ms)
184169* 입력/출력 요약(input_summary/output_summary)
185- * 필요하면 ` data ` 에 구조화된 값을 추가
186170
187171### 어디서 확인하나?
188172
189173가장 쉬운 건 ` MemoryHook ` 입니다.
190174
191175``` python
192- from lang2sql.core.hooks import MemoryHook
193- from lang2sql.flows.baseline import SequentialFlow
176+ from lang2sql import MemoryHook, HybridNL2SQL
194177
195178hook = MemoryHook()
179+ pipeline = HybridNL2SQL(catalog = catalog, llm = llm, db = db, embedding = embedding, hook = hook)
180+ pipeline.run(" 지난달 매출" )
196181
197- flow = SequentialFlow(steps = [... ], hook = hook) # 또는 컴포넌트마다 hook 주입
198- out = flow.run(" 지난달 매출" )
199-
200- # 이벤트 확인
201182for e in hook.snapshot():
202183 print (e.phase, e.component, e.duration_ms, e.error)
203184```
@@ -210,13 +191,13 @@ for e in hook.snapshot():
210191* APM/Tracing으로 보내는 Hook (OpenTelemetry span 등)
211192* 필터링 Hook (특정 컴포넌트만 샘플링)
212193
213- 핵심은: ** 관측성은 hook 구현체에서 제어** 하고, 파이프라인/컴포넌트 코드는 최대한 “ 비즈니스 로직” 만 갖도록 분리합니다.
194+ 핵심은: ** 관측성은 hook 구현체에서 제어** 하고, 파이프라인/컴포넌트 코드는 최대한 " 비즈니스 로직" 만 갖도록 분리합니다.
214195
215196---
216197
217198## 중첩(서브플로우/래핑)하면 트레이싱이 깨지나?
218199
219- “ 깨진다” 기보다는 ** 이벤트가 더 많이 찍힙니다.**
200+ " 깨진다" 기보다는 ** 이벤트가 더 많이 찍힙니다.**
220201
221202* ` flow_b ` 안에 ` flow_a ` 를 step으로 넣으면
222203
@@ -235,49 +216,33 @@ for e in hook.snapshot():
235216
236217## 베스트 프랙티스
237218
238- ### 1) 구성(config)은 ` __init__ ` 에, 요청별 상태는 ` RunContext ` 에
219+ ### 1) 구성(config)은 ` __init__ ` 에, 요청별 데이터는 ` _run() ` 인자에
239220
240- ``` python
241- class Retriever (BaseComponent ):
242- def __init__ (self , catalog , top_k = 8 , ...):
243- self .catalog = catalog # 고정 설정
244- self .top_k = top_k
221+ 고정 설정(모델, 카탈로그, DB 연결 등)은 생성자에서 받고,
222+ 요청마다 달라지는 값(쿼리, 스키마 목록 등)은 ` _run() ` 인자로 전달합니다.
245223
246- def run (self , run : RunContext) -> RunContext:
247- # 요청마다 달라지는 값은 run에서 읽고 run에 쓴다
248- ...
249- return run
250- ```
224+ ### 2) ` _run() ` 의 반환값은 명시적으로
251225
252- ### 2) RunContext가 들어오면 무조건 ` return run `
226+ 반환 타입을 명확히 정의하면 Flow에서 컴포넌트를 조합할 때 안전합니다.
253227
254- * 가독성(계약이 분명)
255- * 실수 방지(fail-fast)
256- * flow 합성 시 안정
257-
258- ### 3) “작은 로직(policy/eval)은 그냥 함수”
228+ ### 3) "작은 로직(policy/eval)은 그냥 함수"
259229
260230* BaseComponent로 감싸는 건 선택
261- * 운영에서 꼭 추적이 필요할 때만 FunctionalComponent로 감싼다
231+ * 운영에서 꼭 추적이 필요할 때만 감싼다
262232
263233---
264234
265235## FAQ
266236
267- ### Q. “ 그냥 함수만 써도 되는데 왜 굳이 BaseComponent?”
237+ ### Q. " 그냥 함수만 써도 되는데 왜 굳이 BaseComponent?"
268238
269239A. ** 운영/디버깅/협업에서** 차이가 큽니다.
270- 문제 났을 때 “ 어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로” 터졌는지 자동으로 남는 게 핵심 가치입니다.
240+ 문제 났을 때 " 어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로" 터졌는지 자동으로 남는 게 핵심 가치입니다.
271241
272- ### Q. “ BaseComponent를 유저가 직접 써야 하나?”
242+ ### Q. " BaseComponent를 유저가 직접 써야 하나?"
273243
274244A. 필수 아닙니다.
275- 초급 유저는 ** SequentialFlow + 프리셋 컴포넌트** 만으로 충분히 쓰게 하고,
245+ 초급 유저는 ** 프리셋 Flow + 프리셋 컴포넌트** 만으로 충분히 쓰게 하고,
276246고급/운영 유저에게 BaseComponent/Hook을 제공하는 구성이 가장 자연스럽습니다.
277247
278- ### Q. “policy는 RunContext를 몰라도 되는데?”
279-
280- A. 맞습니다. ` policy(metrics) -> action ` 같은 건 순수 함수로 두는 걸 권장합니다.
281- 필요하면 ` FunctionalComponent(policy_fn) ` 처럼 감싸서 추적만 추가할 수 있습니다.
282-
283248---
0 commit comments