Skip to content

Commit 286b1b7

Browse files
authored
Merge pull request #217 from CausalInferenceLab/docs/tutorial-restructure
docs: v2 API 기준 한국어 문서 및 튜토리얼 정비
2 parents 6a7151d + 32bffdb commit 286b1b7

16 files changed

+1442
-3945
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,19 @@ lang2sql query "질문" --vectordb-type faiss --vectordb-location ./dev/table_in
137137
lang2sql query "질문" --vectordb-type pgvector --vectordb-location "postgresql://user:pass@host:5432/db"
138138
```
139139

140-
참고: DataHub 없이도 미리 준비된 VectorDB(FAISS 디렉토리 혹은 pgvector 컬렉션)를 바로 사용할 수 있습니다. 자세한 준비 방법은 [DataHub 없이 시작하기](docs/tutorials/getting-started-without-datahub.md) 참고하세요.
140+
참고: DataHub 없이도 미리 준비된 VectorDB(FAISS 파일 혹은 pgvector 컬렉션)를 바로 사용할 수 있습니다. 자세한 준비 방법은 [벡터 검색 튜토리얼](docs/tutorials/03-vector-search.md) 참고하세요.
141141

142-
### 처음 시작하기 (DataHub 없이)
142+
### 처음 시작하기
143143

144-
튜토리얼 본문이 길어져 별도 문서로 분리되었습니다. 아래 문서를 참고하세요.
144+
튜토리얼은 난이도 순서로 구성되어 있습니다.
145145

146-
- [DataHub 없이 시작하기 튜토리얼](docs/tutorials/getting-started-without-datahub.md)
146+
| 번호 | 문서 | 내용 |
147+
|------|------|------|
148+
| 01 | [빠른 시작](docs/tutorials/01-quickstart.md) | 5분 안에 NL2SQL 실행 |
149+
| 02 | [Baseline 파이프라인](docs/tutorials/02-baseline.md) | 실제 DB 연결, DB Explorer |
150+
| 03 | [벡터 검색](docs/tutorials/03-vector-search.md) | FAISS/pgvector 인덱싱 |
151+
| 04 | [하이브리드 검색](docs/tutorials/04-hybrid.md) | BM25 + Vector, EnrichedNL2SQL |
152+
| 05 | [고급](docs/tutorials/05-advanced.md) | 수동 조합, 커스텀 어댑터, Hook |
147153

148154
### 자연어 쿼리 실행
149155

docs/BaseComponent_ko.md

Lines changed: 81 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
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

195178
hook = 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-
# 이벤트 확인
201182
for 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

269239
A. **운영/디버깅/협업에서** 차이가 큽니다.
270-
문제 났을 때 어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로 터졌는지 자동으로 남는 게 핵심 가치입니다.
240+
문제 났을 때 "어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로" 터졌는지 자동으로 남는 게 핵심 가치입니다.
271241

272-
### Q. BaseComponent를 유저가 직접 써야 하나?
242+
### Q. "BaseComponent를 유저가 직접 써야 하나?"
273243

274244
A. 필수 아닙니다.
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

Comments
 (0)