> EulerAgent > 튜토리얼 > 패턴 > 웹 리서치 통합

패턴 05. 웹 리서치 통합 — force_tool과 HITL 게이팅

학습 목표

이 튜토리얼을 마치면 다음을 할 수 있습니다.

사전 준비

euleragent agent list
euleragent pattern validate blog_with_judge.yaml

1. 왜 HITL이 필요한가?

웹 검색은 외부 네트워크에 연결하는 도구입니다. 에이전트가 제멋대로 수십 개의 검색 쿼리를 실행하면:

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

다음 단계

웹 검색을 안전하게 통합하는 방법을 배웠습니다. 이제 더 강력한 인간 개입 — 사람이 직접 내용을 검토하고 편집하는 게이트를 만들어봅니다.

← 이전: Judge 노드와 품질 루프 목록으로 다음: 명시적 인간 검토 게이트 →