AI 하네스 엔지니어링: symbolic-search-runtime
Updated:
Motivation
Text-to-SQL은 데이터 분야에서 꾸준히 언급이 되어왔고 지금도 관심이 많은 태스크입니다.
특히, 모호하거나 복잡한 요청의 경우 반환된 SQL 쿼리가 이해가지 않는 경우가 존재했습니다. 예를 들면, “각 도시마다 날씨의 특징은?”이라는 문제를 해결하는데 하나의 SQL로 해결하기는 어렵습니다. 기온, 풍량 등 여러 관점의 분석이 필요하기 때문입니다.
이전에는 Langchain, Langgraph를 이용해 단일 에이전트, 멀티 에이전트 기반으로 구현을 해왔습니다. 에이전트는 쿼리 생성, Tool Calling 등 여러 방식을 사용하지만, 생성된 쿼리 결과물에 대해 왜 이렇게 구현했는지 설명하기 어려운 경우가 다소 존재했습니다.
복잡한 최적화 문제, 다양한 경로를 탐색해야 하는 문제의 경우, 분할 정복 방식을 적용해 작은 문제들로 분해하고 RLM(Recursive Language Model)의 REPL 아이디어를 SQL로 잘 이용하는 방향으로 Opencode를 이용해 하네스 엔지니어링을 시작하게 되었습니다.
AI 하네스(AI Harness)
AI가 작업을 수행할 수 있도록 실행 환경(Runtime)을 설계하고 오케스트레이션하는 AI 엔지니어링입니다.
Overall Architecture
symbolic-search-runtime은 사용자 요청을 하위 문제(sub-task)로 분해하고, 독립적으로 해결하여 결과를 병합하는 분할 정복 방식에 기반합니다. 하위 문제들은 DAG로 표현되며 여러 경로를 탐색하여 가장 좋은 결과를 선택하여 사용자에게 반환합니다.
Flow
flowchart LR
User([User])
User --> API["query() / CLI"]
API --> ProblemSpec
ProblemSpec --> Planner
Planner --> DAG["Task DAG"]
DAG --> Scheduler
Scheduler --> Engine["Execution Engine"]
Engine --> Aggregator
Aggregator --> Decision{"Ambiguous?"}
Decision -->|Yes| Clarification["Need Clarification"]
Decision -->|No| User
Clarification --> User
전체 플로우는 위와 같습니다.
- 사용자로부터 요청을 받으면 LLM은 ProblemSpec을 작성하여 Planner에게 분할을 요청합니다.
- Planner는 하위 문제로 분할하면서 DAG를 구성합니다. 각 태스크들은 A, B와 같은 노드에 할당되며 부모 노드에게 자식 노드가 생기면 A.A, A.B와 같이 할당됩니다.
- DAG는 Scheduler에 의해 실행 스케줄이 결정됩니다. 의존성이 다른 노드의 경우 병렬 처리가 가능하기 때문에 이를 이용하여 처리 시간을 줄입니다.
- Scheduler는 RLMAgent에 의해 DB Engine을 이용해 SQL을 실행합니다. 각 결과는 부모 노드로 전달됩니다.
- Aggregator에서 결과를 수집하고 Confidence를 보정합니다. 기준점을 통과하지 못하는 경우 사용자 요청이 모호하다는 판단을 내리고 재질문을 할 수도 있습니다.
이 아키텍처에서 중요한 점은 LLM이 전체 시스템을 제어하지 않는다는 것입니다.
Runtime이 Planner, Scheduler, Execution Engine을 오케스트레이션하고, LLM은 Sub-task와 SQL 생성이라는 역할만 담당합니다. 이를 통해 실행 흐름을 코드로 제어할 수 있고, 병렬 실행이나 재시도 정책도 Runtime에서 일관되게 관리할 수 있습니다.
즉, LLM이 시스템을 제어하는 구조가 아니라 Runtime이 LLM을 활용하는 구조라는 점이 이 프로젝트의 가장 큰 특징입니다.
RLMAgent
flowchart TD
Start([Task])
Start --> Context["Build Context
• Task Description
• Schema
• Parent Results"]
Context --> Prompt["Build Prompt"]
Prompt --> LLM["LLM"]
LLM --> SQL["Extract SQL"]
SQL --> Syntax{"Syntax Valid?"}
Syntax -- No --> SyntaxFB["Feedback
Syntax Error"]
SyntaxFB --> Prompt
Syntax -- Yes --> Execute["Execute SQL"]
Execute --> Exec{"Execution Success?"}
Exec -- No --> ExecFB["Feedback
Execution Error"]
ExecFB --> Prompt
Exec -- Yes --> Validate["Validate Result"]
Validate --> Quality{"Result Acceptable?"}
Quality -- No --> QualityFB["Feedback
Quality Issue"]
QualityFB --> Prompt
Quality -- Yes --> Confidence["Confidence Calibration"]
Confidence --> Decision{"Confidence ≥ Threshold?"}
Decision -- No --> RetryFB["Feedback
Low Confidence"]
RetryFB --> Prompt
Decision -- Yes --> Result["Node Result"]
Result --> End([Return])
플로우가 많아보이지만 결국 SQL 문법, 실행 결과들을 기반으로 결과 및 점수를 계산합니다.
RLMAgent는 RLM의 REPL 아이디어를 SQL 실행에 적용했습니다. SQL을 생성한 뒤 실행하고, 실행 결과를 새로운 Context로 사용하여 SQL을 다시 생성합니다.
즉, “Generate → Execute → Observe → Repair” 루프를 반복하며 SQL을 점진적으로 개선합니다.
Confidence
처음 구현때는 Confidence와 같은 스코어링에 대해 생각을 하지 않았지만 모든 경로에 대해 탐색을 하는 것은 비효율적이기 때문에 해당 경로에 대한 평가가 필요했습니다.
초기 Confidence는 LLM이 스스로 평가한 값으로 시작하지만, 실행 과정에서 얻은 신호를 이용해 지속적으로 보정됩니다.
Confidence =
LLM Confidence
+ Execution Success
+ Result Validation
- Penalty
예를 들어 SQL 생성에 성공하더라도 실행 과정에서 오류가 발생하거나, 결과가 비어 있거나, 품질 검증에 실패하면 Confidence는 감소합니다. 반대로 실행이 정상적으로 완료되고 결과가 기대한 형태와 일치한다면 Confidence는 증가합니다.
이렇게 계산된 Confidence는 단순한 신뢰도 지표를 넘어 탐색 경로를 계속 유지할지, 해당 경로를 종료할지, 또는 사용자에게 추가 질문을 요청할지를 결정하는 기준으로 활용됩니다.
End-to-End Example
아래 코드는 Databricks 환경에서 실행되었습니다.
from syrch import query
response = query(
question="각 도시마다 날씨의 특징은 어때?",
db_path="samples.accuweather.forecast_daily_calendar_metric",
executor_type="spark",
model="databricks-qwen3-next-80b-a3b-instruct",
base_url="https://<workspace>.cloud.databricks.com/ai-gateway/mlflow/v1",
api_key="",
verbose=True
)
print(response.answer)
print(f"Confidence: {response.confidence}")
print(f"Token Cost: {response.token_cost}")
INFO:syrch.search.scheduler:Layer 0: dispatching ['F.B', 'F.D.C', 'A', 'F.C', 'F.A', 'E', 'F.D.A', 'F.D.B', 'B', 'D', 'C']
INFO:syrch.search.scheduler: [F.D.C] OK confidence=1.00 tokens=571
INFO:syrch.search.scheduler: [A] OK confidence=1.00 tokens=137
INFO:syrch.search.scheduler: [F.A] OK confidence=0.95 tokens=128
INFO:syrch.search.scheduler: [F.C] OK confidence=0.95 tokens=160
INFO:syrch.search.scheduler: [F.B] OK confidence=0.95 tokens=251
INFO:syrch.search.scheduler: [D] OK confidence=1.00 tokens=61
INFO:syrch.search.scheduler: [B] OK confidence=0.95 tokens=70
INFO:syrch.search.scheduler: [E] OK confidence=1.00 tokens=84
INFO:syrch.search.scheduler: [F.D.A] OK confidence=0.95 tokens=120
INFO:syrch.search.scheduler: [F.D.B] OK confidence=0.95 tokens=521
INFO:syrch.search.scheduler: [C] OK confidence=1.00 tokens=93
INFO:syrch.search.scheduler:Layer 1: dispatching ['F.D.D']
INFO:syrch.search.scheduler: [F.D.D] OK confidence=0.87 tokens=2075
### 1. 각 도시마다 날씨의 특징 요약
각 도시의 날씨는 기후대와 지리적 위치에 따라 극단적인 차이를 보입니다. **사하라 사막 근처 도시**(예: 바그다드, 아테네)는 **매우 더우면서 건조하고 일조량이 풍부**하며, **동남아시아 도시**(예: 방콕, 싱가포르)는 **뜨겁고 습하며 비가 자주 오고 구름이 많음**을 특징으로 합니다. **유럽 도시**(예: 암스테르담, 베이징)는 **온화한 온도와 중간 수준의 습도 및 강수량**을 보이며, **풍속은 해안 도시에서 더 강함**을 확인할 수 있습니다. UV 지수와 대기오염 수준은 도시의 인구 밀도와 산업 활동과 밀접한 관련이 있습니다.
---
### 2. 데이터 기반 증거
#### ✅ **기온 및 일조량**
- **가장 덥고 일조량이 가장 많은 도시**:
- **바그다드**(38.4°C, 일조 780분), **아테네**(31.4°C, 일조 767분)
→ **건조한 사막/지중해성 기후**
- **가장 습하고 일조량이 가장 적은 도시**:
- **방콕**(29.2°C, 일조 **23분**), **싱가포르**(28.2°C, 일조 **~40분** 이하)
→ **열대 우림 기후**, 하루 중 대부분이 흐림
#### ✅ **강수 및 구름**
- **강수 확률 및 강수량이 가장 높은 도시**:
- **방콕**(강수 확률 90.5%, 강수량 10.9mm/일), **싱가포르**(강수 확률 92%, 강수량 9.1mm/일)
→ **연중 내내 비 오는 날이 많음** (비 빈도 93% 이상)
- **가장 건조한 도시**:
- **바그다드**, **아테네**: 강수 확률 **<5%**, 강수량 **0mm**
→ **연간 강수량 거의 없음**
#### ✅ **구름 덮개**
- **구름이 가장 많은 도시**:
- **방콕**(97%), **싱가포르**(82.5%), **코르카**(89.7%)
→ **하늘 대부분이 흐림**
- **구름이 거의 없는 도시**:
- **바그다드**, **아테네**: **0% 평균 구름** → 맑은 하늘 지속
#### ✅ **풍속**
- **풍속이 가장 강한 도시**:
- **케이프타운**(풍속 평균 3.7m/s, 최대 풍속 17.4m/s) → **해안 지역의 강풍 영향**
- **상하이**, **베이징**도 풍속이 비교적 높음 (풍속 평균 3.8m/s 이상)
- **풍속이 약한 도시**:
- **방콕**(2.9m/s), **싱가포르**(2.8m/s) → **열대 우림 지역의 낮은 바람**
#### ✅ **자외선(UV) 및 대기오염**
- **UV 지수 최고 도시**:
- **바그다드**(UV 최대 11.5), **아테네**(11.1), **상하이**(11.4) → **자외선 위험 높음**, 하루에 **UV 8 이상인 날이 80% 이상**
- **대기오염이 가장 심한 도시**:
- **도쿄**(AQI 31.5), **상하이**(24.1), **베이징**(24.1)
→ **대도시의 공기 오염** 영향
- **대기오염이 가장 낮은 도시**:
- **케이프타운**(AQI 12.3) → **상대적으로 청정한 공기**
#### ✅ **극단적 날씨 빈도**
- **비 오는 날 빈도**:
- **방콕**(93.3%), **베이징**(80%), **암스테르담**(66.7%)
- **눈/얼음 가능성**:
- **모든 도시에서 눈/얼음 확률 = 0%** → **한국 이하 위도 지역**이므로 **눈 없음**
- **고UV 빈도**:
- **바그다드, 아테네, 상하이, 도쿄**: 고UV 날짜 비율 **>90%**
- **싱가포르, 방콕**: 고UV 날비율 **6.7% 이하** → 구름이 많아 UV 차단됨
---
### 3. 핵심 결과를 도출한 SQL 쿼리
```sql
-- [F.A] 기본 기후 지표 (온도, 강수, 풍속, 일조, 습도, 구름)
SELECT
city_name, country_code,
AVG(temperature_avg) AS avg_temperature,
AVG(precipitation_lwe_total) AS avg_precipitation_total,
AVG(wind_speed_avg) AS avg_wind_speed,
AVG(minutes_of_sun_total) AS avg_sun_minutes,
AVG(humidity_relative_avg) AS avg_humidity,
AVG(cloud_cover_perc_avg) AS avg_cloud_cover
FROM samples.accuweather.forecast_daily_calendar_metric
GROUP BY city_name, country_code
ORDER BY city_name;
-- [F.C] 비/고UV 빈도
SELECT
city_name, country_code,
AVG(CASE WHEN has_rain THEN 1.0 ELSE 0.0 END) AS rain_frequency,
AVG(CASE WHEN index_uv_max > 8 THEN 1.0 ELSE 0.0 END) AS high_uv_frequency
FROM samples.accuweather.forecast_daily_calendar_metric
GROUP BY city_name, country_code
ORDER BY city_name;
-- [F.D.D] 극단적 지표 (UV, 풍속, 구름, 대기오염 등)
SELECT
city_name, country_code,
MAX(temperature_max) as temp_extreme_risk,
MAX(cloud_cover_perc_max) as cloud_extreme_risk,
MAX(wind_gust_max) as wind_extreme_risk,
MAX(index_uv_max) as uv_risk,
MAX(index_air_quality_24hr_max) as air_quality_risk,
AVG(precipitation_probability) as precip_risk,
AVG(rain_probability) as rain_risk
FROM samples.accuweather.forecast_daily_calendar_metric
GROUP BY city_name, country_code
ORDER BY city_name;
```
---
### ✅ 종합 결론
> **도시별 날씨 특징은 기후대에 따라 명확히 구분됩니다**:
> - **사막/지중해**: **뜨겁고 건조하며 맑음** → 바그다드, 아테네
> - **열대우림**: **뜨겁고 습하며 비 많고 구름 많음** → 방콕, 싱가포르
> - **온대**: **온화하고 계절 변화 있음** → 암스테르담, 베이징
> - **해안 도시**: **풍속이 강함** → 케이프타운
> - **대도시**: **대기오염 및 UV 위험 높음** → 도쿄, 상하이, 베이징
이러한 특징은 **기후 변화 대응, 관광 계획, 건강 관리** 등에 중요한 기초 데이터로 활용될 수 있습니다.
Confidence: 0.991
Token Cost: 4933
포괄적인 질문을 요청했고 syrch는 다양한 경로를 탐색하면서 정보를 취합하고 그 중 필요한 정보를 기반으로 답변을 생성하여 사용자에게 반환했습니다.
Use
Installation
# Core (CLI + SQLite)
pip install syrch
# Databricks SQL Warehouse (external connection)
pip install "syrch[databricks-sql]"
# Spark executor (Databricks Runtime, EMR, standalone)
pip install "syrch[spark]"
# Development (tests + lint)
pip install -e ".[dev]"
# Everything
pip install "syrch[all]"
Python API (Library Mode)
Use directly from Databricks notebooks or Python scripts:
from syrch import query
result = query(
question="What discount × shipping combo maximizes revenue?",
executor_type="databricks-sql",
model="gpt-4o",
)
print(result.answer)
print(result.sql)
print(result.confidence)
print(result.data)
CLI Usage
# Install
pip install syrch
# Inspect database schema
syrch schema wikipedia_clickstream.sqlite
syrch schema orders_10dim.sqlite -t orders_10dim
# Show default config
syrch config
# Solve a problem (requires LLM API key)
export OPENAI_API_KEY="sk-..."
syrch search -q "What discount × shipping combo maximizes revenue for top 10% customers?"
# With config file
syrch search -q "..." --config syrch.yml
# With options
syrch search -q "Which click type generates the most traffic?" \
--db wikipedia_clickstream.sqlite \
--max-depth 3 \
--high-conf 0.85 \
--max-attempts 3 \
--verbose
# Grid search over hyperparameters (54 cells)
syrch search -q "..." --db orders_10dim.sqlite --grid
# Benchmark against expected results
syrch eval -q "..." --db orders_10dim.sqlite --expected expected.csv
# Run benchmark suite
syrch benchmark benchmarks/orders.jsonl
Discussion
한계점
실제 비즈니스 데이터를 가지고 실험해본 것은 아니기 때문에 추가 검증이 필요합니다. 특히, 비즈니스에는 굉장히 많은 테이블을 조인하고 비즈니스 도메인 지식이 반영되어야 하기 때문에 이에 대한 개선이 필요한 상황입니다.
또한, Planner는 테이블 스키마 정보를 사용하지않고 ProblemSpec만 보고 태스크 분해를 시작하기 때문에 막상 execution 단계에서 SQL 쿼리를 생성하지 못할 수도 있습니다. Planner에게 스키마 정보를 주는 것이 이점인지 확인이 필요합니다.
마지막으로, Clarification의 위치입니다. Clarification는 플로우 마지막에 위치해있기 때문에 추가 질문 요청을 마지막에서야 파악할 수 있습니다. 이를 Planner 위치로 올리는 것이 맞는 판단인지도 파악해봐야 합니다.
향후 계획
일단, 현재 수준에선 플로우를 수정하는 것보다는 작은 수정을 진행하려고 합니다. 프로덕션 환경에서 사용하려면 100개 이상의 테이블로부터 필요한 테이블만 고를 수 있는 기능이 필요하다고 생각되며 Execution도 다양한 DB를 지원할 수 있도록 개선할 생각입니다.
Comments