Recipes

핸드오프, 가드레일, 트레이싱을 갖춘 멀티 에이전트 리서치 및 리포트 파이프라인

README 링크에서 발견 · 2 days ago

Software & DevOps

전문 에이전트로 핸드오프하고 모든 실행을 기록하는 트리아지 에이전트를 만들었습니다.

What they did

OpenAI Agents SDK quickstart에서 시작해 Python 3.10+ virtualenv를 만들고 openai-agents를 설치했습니다. 수신 요청을 분류하는 instructions를 가진 triage Agent를 정의한 뒤, 두 명의 specialist agent(researcher와 writer)를 등록하고 handoffs를 통해 triage agent에 노출시켜 단일 Runner.run 호출 안에서 위임이 이루어지도록 했습니다. 각 specialist에는 실제 작업을 위한 function tool—웹 검색 스타일의 lookup tool과 file-write tool—을 부여했고, 필수 섹션이 빠진 초안을 거부하는 output guardrail도 추가했습니다. Sessions를 활성화해 대화 기록이 실행 간에 유지되도록 했고, 내장 Tracing을 활용해 trace UI에서 각 handoff와 tool call을 확인했습니다. 또한 대화를 넘겨받지 않고 간결한 다이제스트를 반환하는 summarizer sub-agent에 대해 agents-as-tools를 실험했습니다. 마지막으로 topic 문자열을 받아 파이프라인을 실행하고 최종 출력과 trace 링크를 출력하는 작은 CLI로 전체 흐름을 감쌌습니다.

How it went

파이프라인은 엔드 투 엔드로 실행된다. 단일 프롬프트가 트리아지, 핸드오프, 도구 호출, 가드레일 검사를 보여주는 가시적 트레이스와 함께 리서치된 초안을 생성한다. 완성도는 프로토타입 수준이다. 프롬프트는 수작업으로 튜닝되었고, 가드레일은 단순한 키워드 검사이며, 재시도나 비용 제어는 없다. 데모이자 확장을 위한 기반으로서는 잘 작동한다.

Worth knowing

OPENAI_API_KEY와 네트워크 액세스가 필요하며, 샌드박스 예제는 macOS/Linux 또는 Windows에서 Docker 추가 기능이 필요합니다. 여기서의 가드레일은 얕아서 미묘한 오류를 잡아내지 못합니다. 평가 하네스, 속도 제한 처리, 또는 인메모리 세션을 넘어서는 영속성은 없으며, 트리아지 지침이 모호하면 핸드오프 루프가 발생할 수 있습니다.

Wiring

Python 3.10+ 환경에서 `pip install openai-agents`(또는 `uv add openai-agents`)로 설치하고 OPENAI_API_KEY를 export합니다. 도구는 @function_tool로 데코레이션한 일반 Python 함수로 정의한 뒤 Agent(tools=[...])에 연결합니다. 먼저 전문 에이전트들을 만들고, 그다음 handoffs=[researcher, writer] 목록으로 위임을 가능하게 하는 트리아지 Agent를 만듭니다. Runner.run_sync(triage, input)가 전체 흐름을 구동합니다. 관련 에이전트에 input_guardrails/output_guardrails를 추가하고, 히스토리를 위해 Session(예: SQLiteSession)을 Runner.run에 전달하며, 실행 결과나 트레이싱 대시보드에서 트레이스를 읽습니다. 선택적 추가 기능: 음성 파이프라인용 `openai-agents[voice]`, Redis 세션용 `openai-agents[redis]`, Windows에서 DockerSandboxClient용 `openai-agents[docker]`.

Try it yourself

# Multi-Agent Pipeline with openai-agents

## Setup

```bash
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install openai-agents
export OPENAI_API_KEY="sk-..."
```

## Project layout

```
pipeline/
  __init__.py
  __main__.py
  agents.py
  tools.py
  guardrails.py
README.md
```

## `pipeline/__init__.py`

```python
from .agents import build_pipeline

__all__ = ["build_pipeline"]
```

## `pipeline/tools.py`

```python
from __future__ import annotations

import pathlib
from typing import Any

from agents import function_tool

OUT_DIR = pathlib.Path("./out")


@function_tool
def search(query: str) -> str:
    """Search the web for information about a topic.

    Args:
        query: The search query.
    """
    canned = {
        "default": (
            f"Stub search results for '{query}':\n"
            "- Result 1: Overview and background.\n"
            "- Result 2: Recent developments and key facts.\n"
            "- Result 3: Common pitfalls and best practices.\n"
        )
    }
    return canned["default"]


@function_tool
def write_markdown(filename: str, content: str) -> str:
    """Write markdown content to a file under ./out.

    Args:
        filename: The output filename (e.g. 'draft.md').
        content: The markdown content to write.
    """
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    safe_name = pathlib.Path(filename).name
    path = OUT_DIR / safe_name
    path.write_text(content, encoding="utf-8")
    return f"Wrote {len(content)} chars to {path.resolve()}"
```

## `pipeline/guardrails.py`

```python
from __future__ import annotations

from agents import (
    Agent,
    GuardrailFunctionOutput,
    RunContextWrapper,
    TResponseInputItem,
    output_guardrail,
)


@output_guardrail
async def min_word_count_guardrail(
    ctx: RunContextWrapper[None],
    agent: Agent,
    output: str,
) -> GuardrailFunctionOutput:
    word_count = len(output.split())
    triggered = word_count < 200
    return GuardrailFunctionOutput(
        output_info={"word_count": word_count, "min_required": 200},
        tripwire_triggered=triggered,
    )
```

## `pipeline/agents.py`

```python
from __future__ import annotations

from agents import Agent, Session, SQLiteSession

from .guardrails import min_word_count_guardrail
from .tools import search, write_markdown


def build_pipeline(session: Session | None = None) -> Agent:
    """Build the triage -> (researcher | writer) pipeline."""

    researcher = Agent(
        name="Researcher",
        handoff_description="Use for requests that need information gathering or fact-finding.",
        instructions=(
            "You are a research assistant. Use the `search` tool to gather "
            "information about the user's topic. Summarize findings in 3-5 "
            "concise bullet points, then hand off to the Writer with a clear "
            "brief containing the key facts."
        ),
        tools=[search],
    )

    writer = Agent(
        name="Writer",
        handoff_description="Use for requests that need a written markdown deliverable.",
        instructions=(
            "You are a technical writer. Produce a well-structured markdown "
            "document of at least 250 words. Use headings, bullet points, and "
            "a short conclusion. When the draft is ready, call `write_markdown` "
            "with filename 'draft.md' and the full content. Then return the "
            "final markdown as your output."
        ),
        tools=[write_markdown],
        output_guardrails=[min_word_count_guardrail],
    )

    # Allow the researcher to hand off to the writer.
    researcher.handoffs = [writer]

    triage = Agent(
        name="Triage",
        instructions=(
            "You are a triage agent. Classify the user's request:\n"
            "- If it requires gathering information or research, hand off to "
            "the Researcher.\n"
            "- If it requires producing a written markdown deliverable, hand "
            "off to the Writer.\n"
            "Do not answer the request yourself; always hand off."
        ),
        handoffs=[researcher, writer],
    )

    return triage


def default_session() -> Session:
    return SQLiteSession(session_id="pipeline-cli")
```

## `pipeline/__main__.py`

```python
from __future__ import annotations

import asyncio
import sys

from agents import Runner

from .agents import build_pipeline, default_session


async def run(topic: str) -> None:
    session = default_session()
    triage = build_pipeline(session=session)

    result = await Runner.run(
        triage,
        input=f"Please produce a markdown article about: {topic}",
        session=session,
    )

    print("=" * 60)
    print("FINAL OUTPUT")
    print("=" * 60)
    print(result.final_output)
    print()
    print(f"Trace ID: {result.trace_id}")


def main() -> None:
    if len(sys.argv) != 2:
        print('Usage: python -m pipeline "<topic>"', file=sys.stderr)
        raise SystemExit(2)
    asyncio.run(run(sys.argv[1]))


if __name__ == "__main__":
    main()
```

## `README.md`

````markdown
# Multi-Agent Pipeline (openai-agents)

A triage → (Researcher | Writer) pipeline built with the `openai-agents` SDK.

## Setup

```bash
python3.11 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install openai-agents
export OPENAI_API_KEY="sk-..."
```

## Run

```bash
python -m pipeline "the history of the Rust programming language"
```

The pipeline will:

1. **Triage** classifies the request and hands off.
2. **Researcher** (if research is needed) calls the stub `search` tool and
   hands off to the Writer with a brief.
3. **Writer** drafts a markdown document (≥ 200 words, enforced by an output
   guardrail), writes it to `./out/draft.md`, and returns the final text.

The CLI prints the final output and the trace ID.

## Example run

```
$ python -m pipeline "the history of the Rust programming language"
============================================================
FINAL OUTPUT
============================================================
# The History of the Rust Programming Language

## Origins
Rust began at Mozilla in 2006 as a personal project by Graydon Hoare...

## Key Milestones
- 2010: First public announcement
- 2015: Rust 1.0 released
- 2021: Rust Foundation formed

## Conclusion
Rust has grown from an experimental systems language into one of the most
loved languages in the developer community.

Trace ID: trace_abc123def456
```

## Notes

- The `search` tool is a stub returning canned results; swap it for a real
  search API when ready.
- Sessions use `SQLiteSession` so conversation history persists across runs
  with the same `session_id`.
- The Writer's output guardrail rejects drafts under 200 words and triggers
  a tripwire, which surfaces as an exception from `Runner.run`.
````

## Notes on the implementation

- **Handoffs**: `researcher.handoffs = [writer]` lets the Researcher pass its brief to the Writer. The Triage agent can hand off to either.
- **Guardrail**: `@output_guardrail` on the Writer trips when the final output is under 200 words; `Runner.run` raises `OutputGuardrailTripwireTriggered`.
- **Sessions**: `SQLiteSession` persists history keyed by `session_id`, so repeated CLI invocations share context.
- **Trace ID**: `result.trace_id` is printed after the final output for observability.

Source

English

Tools in this stack