패턴 05. 웹 리서치 통합 — force_tool과 HITL 게이팅
학습 목표
이 튜토리얼을 마치면 다음을 할 수 있습니다.
force_tool: web.search와mode: plan의 조합으로 안전한 웹 검색 노드를 설계할 수 있다- HITL 승인 흐름을 이해하고
when: "approvals_resolved"엣지를 올바르게 연결할 수 있다 guardrails.tool_call_budget으로 노드별 웹 검색 횟수를 제한할 수 있다defaults.max_web_search_calls와dedupe_web_search를 설정할 수 있다BUDGET_INCONSISTENT,HITL_GATING_VIOLATION,FORCE_TOOL_NO_BUDGET에러를 예방하고 해결할 수 있다
사전 준비
04_judge_and_loop.md완료blog_with_judge.yaml존재- euleragent 에이전트 생성 완료
euleragent agent list
euleragent pattern validate blog_with_judge.yaml
1. 왜 HITL이 필요한가?
웹 검색은 외부 네트워크에 연결하는 도구입니다. 에이전트가 제멋대로 수십 개의 검색 쿼리를 실행하면:
- 비용이 예기치 않게 증가할 수 있습니다 (유료 검색 API)
- 개인정보나 내부 정보가 쿼리에 포함될 수 있습니다
- 악의적이거나 편향된 검색 쿼리가 실행될 수 있습니다
euleragent는 deny-all 기본 정책을 따릅니다. 웹 검색은 명시적 허가가 없으면 실행되지 않습니다.
force_tool: web.search + mode: plan의 조합이 이 안전장치를 구현합니다.
2. 핵심 개념: force_tool + mode: plan
mode: plan의 의미
mode: execute (기본)
LLM → 도구 직접 실행 → 결과
mode: plan (HITL 모드)
LLM → 도구 실행 "제안" → ⏸ PAUSE
사람이 제안을 검토 → ✓ 승인 또는 ✗ 거절
승인된 도구만 실행 → 결과
force_tool: web.search의 의미
force_tool은 해당 노드에서 이 도구를 반드시 사용하도록 강제합니다. mode: plan과 함께 사용하면 "이 도구의 사용 제안을 반드시 HITL 승인받아야 한다"는 의미가 됩니다.
runner:
mode: plan # 계획만, 실행은 HITL 승인 후
force_tool: web.search # web.search를 반드시 제안해야 함
min_proposals: 3 # 최소 3개의 검색 쿼리를 제안해야 통과
HITL_GATING_VIOLATION
force_tool이 있는데 mode: execute를 사용하면 HITL_GATING_VIOLATION 에러가 발생합니다. 안전하지 않은 설정이기 때문입니다.
# 잘못된 예 — HITL_GATING_VIOLATION
runner:
mode: execute # execute + force_tool = 위반!
force_tool: web.search # HITL 없이 바로 실행?? 안 됨
# 올바른 예
runner:
mode: plan # 반드시 plan
force_tool: web.search
3. 예산 설정: guardrails와 defaults
노드별 예산 (guardrails.tool_call_budget)
nodes:
- id: research
kind: llm
runner:
mode: plan
force_tool: web.search
guardrails:
tool_call_budget:
web.search: 5 # 이 노드에서 web.search는 최대 5회
전역 예산 (defaults)
defaults:
max_web_search_calls: 10 # 전체 실행에서 web.search 최대 10회
max_total_tool_calls: 30 # 전체 실행에서 모든 도구 합산 최대 30회
dedupe_web_search: true # 동일한 쿼리 중복 실행 방지
BUDGET_INCONSISTENT 에러
노드별 예산이 전역 예산보다 클 때 발생합니다.
# 잘못된 예 — BUDGET_INCONSISTENT
defaults:
max_web_search_calls: 5 # 전역 최대 5회
nodes:
- id: research
guardrails:
tool_call_budget:
web.search: 8 # 노드 예산이 전역 예산(5)보다 큼! 에러
# 올바른 예
defaults:
max_web_search_calls: 10 # 전역 10회
nodes:
- id: research
guardrails:
tool_call_budget:
web.search: 5 # 노드 5회 ≤ 전역 10회 ✓
FORCE_TOOL_NO_BUDGET
force_tool이 설정됐는데 해당 도구의 예산(budget)이 0이거나 설정되지 않은 경우:
# 잘못된 예 — FORCE_TOOL_NO_BUDGET
runner:
force_tool: web.search
guardrails:
tool_call_budget:
web.search: 0 # 예산 0인데 force? 에러
4. approvals_resolved 엣지
웹 검색 노드는 pause 상태가 됩니다. 승인이 완료된 후 다음 노드로 이동하려면 when: "approvals_resolved" 조건을 사용합니다.
edges:
- from: research # 웹 검색 노드 (pause 가능)
to: write # 다음 노드
when: "approvals_resolved" # 모든 승인이 완료됐을 때
# 잘못된 예
edges:
- from: research
to: write
when: "true" # 웹 검색 노드에서 true를 쓰면 pause 없이 바로 넘어감!
5. 패턴 설계
블로그 작성 패턴에 실제 웹 검색을 추가합니다.
[research] ──HITL PAUSE──► 승인 후 실행 → [draft] → [evaluate] → finalize
(web.search, │
mode: plan) └── revise → [evaluate]
완전한 흐름:
┌─────────────────────────────────────────────────────────────────┐
│ blog.web_research 패턴 흐름도 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [research] ◄─── HITL PAUSE │
│ │ 웹 검색 제안 (llm/plan, force_tool=web.search) │
│ │ min_proposals: 3 budget: web.search ≤ 5 │
│ │ when: approvals_resolved │
│ ▼ │
│ [draft] │
│ │ 수집된 정보로 초안 작성 (llm/execute) │
│ │ when: true │
│ ▼ │
│ [evaluate] ──── when: judge.route == finalize ─────────────────┐
│ │ 품질 평가 (judge/evaluator_v1) │
│ │ when: judge.route == revise │
│ ▼ │
│ [revise] │
│ │ 초안 개선 (llm/execute) │
│ └────────────────────────► [evaluate] (최대 3회) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ [FINALIZE] blog_post.md 저장 │◄──┘
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6. YAML 작성
blog_web_research.yaml 파일을 생성합니다.
id: blog.web_research
version: 1
category: writing
description: "웹 검색 + Judge 루프가 결합된 고품질 블로그 작성 패턴"
defaults:
max_iterations: 3
max_total_tool_calls: 30
# 전체 실행에서 웹 검색 최대 횟수
# 노드별 예산의 합보다 크거나 같아야 함
max_web_search_calls: 10
# 중복 쿼리 방지 — 동일한 쿼리가 다시 제안되면 자동으로 제외
dedupe_web_search: true
pass_threshold: 0.85
nodes:
# ── 노드 1: research (웹 검색, HITL) ──
- id: research
kind: llm
runner:
# mode: plan은 필수! execute이면 HITL_GATING_VIOLATION
mode: plan
force_tool: web.search # 반드시 web.search를 제안해야 함
min_proposals: 3 # 최소 3개의 검색 쿼리 제안 필수
prompt:
system_append: |
당신은 기술 리서처입니다.
주어진 주제에 대해 최신 정보를 찾기 위한 웹 검색 쿼리를 제안하세요.
제안 요구사항:
- 최소 3개, 최대 5개의 검색 쿼리
- 각 쿼리는 서로 다른 측면을 다뤄야 함
- 영어 쿼리를 사용 (검색 결과 품질 향상)
- 너무 광범위하거나 너무 좁지 않은 쿼리
예시 쿼리 형식:
- "[기술명] tutorial best practices [현재연도]"
- "[기술명] vs [대안기술] comparison"
- "[기술명] real world use case example"
# 노드별 웹 검색 예산
# max_web_search_calls(10) 이하여야 함 — BUDGET_INCONSISTENT 방지
guardrails:
tool_call_budget:
web.search: 5 # 이 노드에서 최대 5회
artifacts:
primary: research_results.md
# ── 노드 2: draft ──
- id: draft
kind: llm
runner:
mode: execute
# 초안 작성 시 추가 검색 금지 — 수집된 정보만 사용
exclude_tools: [web.search, web.fetch, shell.exec]
prompt:
system_append: |
당신은 기술 블로그 작가입니다.
웹 검색으로 수집된 최신 정보를 바탕으로 블로그 포스트를 작성하세요.
요구사항:
- 길이: 1000-1500 단어
- 최신 정보를 활용 (검색 결과에서 날짜, 버전 등 인용)
- 독자: 현업 개발자
- 실용적 예시와 코드 포함
- 마크다운 포맷
artifacts:
primary: blog_post.md
# ── 노드 3: evaluate (Judge) ──
- id: evaluate
kind: judge
judge:
schema: evaluator_v1
route_values: [finalize, revise]
prompt:
system_append: |
블로그 포스트의 품질을 다음 기준으로 평가하세요:
1. 최신성 (25%): 검색으로 수집한 최신 정보가 잘 반영됐는가?
2. 기술 정확성 (30%): 기술 정보가 정확한가?
3. 실용성 (25%): 독자가 바로 적용할 수 있는가?
4. 가독성 (20%): 구성, 흐름, 명확성
score >= 0.85 → finalize
score < 0.85 → revise
# ── 노드 4: revise ──
- id: revise
kind: llm
runner:
mode: execute
exclude_tools: [web.search, web.fetch, shell.exec]
prompt:
system_append: |
편집장의 피드백을 반영하여 블로그 포스트를 개선하세요.
기존 검색 결과를 최대한 활용하되, 누락된 정보가 있으면 명시하세요.
artifacts:
primary: blog_post.md
edges:
# research 완료 후 승인이 완료됐을 때만 draft로 이동
- from: research
to: draft
when: "approvals_resolved" # 반드시 approvals_resolved 사용!
- from: draft
to: evaluate
when: "true"
- from: evaluate
to: finalize
when: "judge.route == finalize"
- from: evaluate
to: revise
when: "judge.route == revise"
- from: revise
to: evaluate
when: "true"
finalize:
artifact: blog_post.md
7. 검증
euleragent pattern validate blog_web_research.yaml
예상 출력:
Validating pattern: blog_web_research.yaml
Stage 1 (Schema) PASS
Stage 2 (Structural) PASS force_tool + mode:plan combination valid ✓
Stage 3 (IR Analysis) PASS
HITL gates: research (web.search, min_proposals=3) ✓
Budget check: research.web.search(5) ≤ max_web_search_calls(10) ✓
dedupe_web_search: enabled ✓
Cycle bounded: max_iterations=3 ✓
Validation complete: 0 errors, 0 warnings
8. 실행 및 승인 흐름
실행
cp blog_web_research.yaml .euleragent/patterns/
euleragent pattern run blog.web_research my-agent \
--task "Kubernetes Operator 패턴 완전 가이드 — 언제, 왜, 어떻게 사용하는가" \
--project default
예상 출력 (research에서 pause):
[run:h8f4c2b6] Starting pattern: blog.web_research
⏸ research PAUSED — Waiting for HITL approval (web.search × 4)
승인 목록 확인
euleragent approve list --run-id h8f4c2b6
출력:
Pending Approvals for run: h8f4c2b6
─────────────────────────────────────────────────
Node: research (budget: web.search ≤ 5, proposed: 4)
#1 web.search "Kubernetes Operator pattern tutorial 2025"
#2 web.search "Kubernetes custom resource definition operator development"
#3 web.search "Kubernetes Operator vs Helm chart when to use"
#4 web.search "Kubernetes Operator real world examples production"
4 tool calls pending. Budget remaining: 1/5.
Options:
Accept all: euleragent approve accept-all --run-id h8f4c2b6 --execute
Accept some: euleragent approve accept --run-id h8f4c2b6 --item 1 --item 2 --item 3 --execute
Reject all: euleragent approve reject-all --run-id h8f4c2b6
개별 쿼리 거절 예시
# #3번 쿼리는 거절 (범위가 너무 넓음)
euleragent approve reject --run-id h8f4c2b6 --item 3 \
--reason "Operator vs Helm 비교는 나중에 별도로"
# 나머지 승인 후 실행
euleragent approve accept --run-id h8f4c2b6 --item 1 --item 2 --item 4 --execute
재개
euleragent pattern resume h8f4c2b6 --execute
예상 출력:
[run:h8f4c2b6] Resuming from: research
web.search "Kubernetes Operator pattern tutorial 2025" OK (15 results)
web.search "Kubernetes custom resource definition..." OK (11 results)
web.search "Kubernetes Operator real world examples..." OK (9 results)
(item #3 rejected — skipped)
✓ research Completed — 3 searches, research_results.md generated
✓ draft Completed — 1,247 words
✓ evaluate Completed — score: 0.91 → route: finalize
✓ finalize Completed
Artifact: .euleragent/runs/h8f4c2b6/artifacts/blog_post.md
9. dedupe_web_search 효과 확인
dedupe_web_search: true가 어떻게 동작하는지 확인합니다.
Judge가 revise를 결정하고 revise 후 다시 evaluate를 통과했다가, 만약 패턴이 다시 research로 돌아오는 설계였다면 중복 쿼리를 자동으로 필터링합니다. 이벤트 스트림에서 확인:
cat .euleragent/runs/h8f4c2b6/pattern_events.jsonl | grep "dedupe"
{"ts":"2026-02-23T14:33:12Z","event":"tool.dedupe","tool":"web.search","query":"Kubernetes Operator pattern tutorial 2025","reason":"already_executed_in_run","skipped":true}
10. 의도적으로 BUDGET_INCONSISTENT 발생시키기
실습으로 에러를 발생시켜봅니다.
blog_web_research.yaml에서 research 노드의 web.search 예산을 전역 예산보다 크게 설정합니다.
defaults:
max_web_search_calls: 5 # 전역 5회로 낮춤
nodes:
- id: research
guardrails:
tool_call_budget:
web.search: 8 # 노드 예산(8) > 전역 예산(5) → 에러
검증:
euleragent pattern validate blog_web_research_broken.yaml
예상 출력:
Validating pattern: blog_web_research_broken.yaml
Stage 1 (Schema) PASS
Stage 2 (Structural) PASS
Stage 3 (IR Analysis) FAIL
ERROR [BUDGET_INCONSISTENT]
Node 'research': tool_call_budget.web.search (8) exceeds
defaults.max_web_search_calls (5).
The node budget cannot exceed the global cap.
Fix options:
1. Reduce node budget: guardrails.tool_call_budget.web.search ≤ 5
2. Increase global cap: defaults.max_web_search_calls ≥ 8
Validation complete: 1 error, 0 warnings
해결: 노드 예산을 줄이거나(≤5) 전역 예산을 올립니다(≥8).
11. 주요 개념 설명
min_proposals의 역할
min_proposals: 3은 LLM이 최소 3개의 검색 쿼리를 제안해야 한다는 의미입니다. LLM이 1개만 제안하면 런타임이 더 많은 제안을 요청합니다. 이는 너무 좁은 리서치를 방지합니다.
force_tool이 없는 plan 노드
mode: plan이지만 force_tool이 없는 노드도 가능합니다. 이 경우 LLM이 자율적으로 도구를 선택해서 제안합니다. HITL 승인은 여전히 발생합니다.
여러 force_tool 지정
하나의 노드에 여러 force_tool은 지원되지 않습니다. 여러 도구를 강제하려면 노드를 분리해야 합니다.
dedupe_web_search의 범위
dedupe_web_search는 단일 run 내에서만 동작합니다. 다른 run의 쿼리는 중복으로 감지하지 않습니다.
12. 실습 과제: BUDGET_INCONSISTENT 에러 의도적으로 발생시키고 해결하기
다음 시나리오를 시도해보세요.
시나리오 1: 여러 research 노드의 예산 합산
defaults:
max_web_search_calls: 8 # 전역 8회
nodes:
- id: broad_research
guardrails:
tool_call_budget:
web.search: 5
- id: narrow_research
guardrails:
tool_call_budget:
web.search: 5 # 두 노드 합산(10) > 전역(8)
# → BUDGET_INCONSISTENT?
검증 후 실제로 어떤 에러가 나오는지 확인하세요. (힌트: 개별 노드 예산 vs 전역 예산의 관계)
시나리오 2: FORCE_TOOL_NO_BUDGET
nodes:
- id: research
runner:
mode: plan
force_tool: web.search
# guardrails 섹션 없음
# → FORCE_TOOL_NO_BUDGET?
기본 예산이 자동으로 적용되는지, 아니면 에러가 발생하는지 확인하세요.
euleragent pattern validate scenario2.yaml --format json
다음 단계
웹 검색을 안전하게 통합하는 방법을 배웠습니다. 이제 더 강력한 인간 개입 — 사람이 직접 내용을 검토하고 편집하는 게이트를 만들어봅니다.
- 다음 튜토리얼: 06_human_gate.md —
force_tool: file.write로 사람이 반드시 확인해야 하는 노드를 설계합니다 - 다중 경로: 07_multi_route.md — Judge에서 3개 이상의 경로로 분기합니다