
๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ๊ณผ AI ์์ด์ ํธ๋ฅผ ๊ฒฐํฉํ์ฌ ์ฌ์ธต ๊ฒ์(Deep Research) ์์คํ ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํ์ต๋๋ค. ์ด ๊ธ์์๋ OpenAI SDK์ Agent ํ๋ ์์ํฌ๋ฅผ ํ์ฉํ์ฌ Plan โ Search โ Report 3๋จ๊ณ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ฑํ๋ ์ค์ ๊ตฌํ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
ํต์ฌ์ Python์
asyncio๋ฅผ ํตํ ๋ณ๋ ฌ ์ฒ๋ฆฌ์ Pydantic ๊ธฐ๋ฐ Structured Output์ ํตํ ์๋ต ์ ์ด์ ๋๋ค.
async def๋ก ์ ์๋ ํจ์๋ ๋น๋๊ธฐ ํจ์๋ก, await ํค์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ์์
์ ์ํํ ์ ์์ต๋๋ค. ์ด๋ฌํ ํจ์๋ ์ฝ๋ฃจํด(Coroutine)์ ์์ฑํฉ๋๋ค.
async def example_async_function():
result = await some_async_operation()
return result
await ํค์๋๋ฅผ ์ฌ์ฉํ์ฌ ์คํ์ ์ผ์ ์ค์งํ๊ณ ๋์ค์ ๋ค์ ์์ํ ์ ์์ต๋๋ค.Event Loop๋ ๋น๋๊ธฐ ์์ ์ ์ค์ผ์ค๋งํ๊ณ ์คํํ๋ ์ญํ ์ ํฉ๋๋ค. ํน์ coroutine์ด waiting ์ํ๋ผ๋ฉด ๋ค๋ฅธ coroutine์ ์คํํ์ฌ ํจ์จ์ ์ธ ๋์์ฑ(concurrency)์ ์ ๊ณตํฉ๋๋ค.
๐ก
Event Loop๋ ๋จ์ผ ์ค๋ ๋ ๋ด์์ ์ฌ๋ฌ ์์ ์ ๋์์ ์ฒ๋ฆฌํ ์ ์๊ฒ ํด์ฃผ๋ ํต์ฌ ๋ฉ์ปค๋์ฆ์ ๋๋ค. I/O bound ์์ ์์ ํนํ ํจ๊ณผ์ ์ ๋๋ค.
OpenAI SDK๋ AI ์์ด์ ํธ๋ฅผ ์ฝ๊ฒ ๊ตฌ์ถํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. @function_tool ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํตํด Python ํจ์๋ฅผ tool๋ก ๋ฑ๋กํ ์ ์์ต๋๋ค.
from agents import Agent, function_tool
@function_tool
def my_tool_function(param: str) -> str:
"""๋๊ตฌ ์ค๋ช
"""
return f"๊ฒฐ๊ณผ: {param}"
agent = Agent(
name="MyAgent",
instructions="๋น์ ์ ์ญํ ์...",
tools=[my_tool_function],
model="gpt-4o-mini"
)
Agent ์์ฒด๋ ๋ค๋ฅธ Agent์ tool๋ก ๋ฑ๋กํ ์ ์์ต๋๋ค. ์ด๋ as_tool() ๋ฉ์๋๋ฅผ ์ฌ์ฉํฉ๋๋ค.
agent1.as_tool(
tool_name="sales_agent1",
tool_description="์์
๊ด๋ จ ์ง๋ฌธ์ ๋ต๋ณํ๋ ์์ด์ ํธ"
)
Handoffs๋ Agent ๊ฐ์ ์์ ์ ๋ฌ์ ๊ด๋ฆฌํ๋ ๊ธฐ๋ฅ์ ๋๋ค. ํน์ ์ญํ ์ ๊ฐ์ง Agent์๊ฒ ์์ ์ ์ ๋ฌํ๊ณ , ํด๋น Agent์ ์์ ์ด ๋๋๋ฉด ๋ค๋ฅธ Agent์๊ฒ ์์ ์ ์ ๋ฌํ ์ ์์ต๋๋ค.
handoffs_description ๋งค๊ฐ๋ณ์์ ์์ด์ ํธ์ ์ญํ ๋๋ ์๋ฌด๋ฅผ ์ค๋ช
ํฉ๋๋ค.
๊ฐ๋๋ ์ผ ์์ฒด๋ฅผ Agent๋ก ๊ตฌํํ์ฌ ์ ๋ ฅ ๊ฒ์ฆ ๋ก์ง์ ๊ตฌ์กฐํํ ์ ์์ต๋๋ค.
from pydantic import BaseModel
from agents import Agent, input_guardrail, Runner, GuardrailFunctionOutput
class NameCheckOutput(BaseModel):
is_name_in_message: bool
name: str
guardrail_agent = Agent(
name="Name check",
instructions="Check if the user is including someone's personal name in what they want you to do.",
output_type=NameCheckOutput, # Structured output
model="gpt-4o-mini"
)
@input_guardrail
async def guardrail_against_name(ctx, agent, message):
result = await Runner.run(guardrail_agent, message, context=ctx.context)
is_name_in_message = result.final_output.is_name_in_message
return GuardrailFunctionOutput(
output_info={"found_name": result.final_output},
tripwire_triggered=is_name_in_message
)
๐ก
Pydantic ๋ชจ๋ธ์output_type์ผ๋ก ์ง์ ํ๋ฉด, Agent๊ฐ ์์ ํ์์ด ์๋ ์ ํด์ง ๊ตฌ์กฐ๋ก๋ง ์๋ตํ๋๋ก ๊ฐ์ ํ ์ ์์ต๋๋ค. ์ด๋ ๊ฐ๋ ฅํ ๊ฐ๋๋ ์ผ ์ญํ ์ ํฉ๋๋ค.
Deep Research๋ OpenAI SDK์ ๋ด์ฅ WebSearchTool๊ณผ Agent์ Structured Output์ ํ์ฉํ์ฌ ์ฌ์ธต ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ํจํด์
๋๋ค.
graph LR
A[์ฌ์ฉ์ ์ฟผ๋ฆฌ] --> B[Planner Agent]
B -->|๊ฒ์ ๊ณํ| C[Search Agent]
C -->|๋ณ๋ ฌ ๊ฒ์| D[๊ฒ์ ๊ฒฐ๊ณผ๋ค]
D --> E[Writer Agent]
E --> F[์ต์ข
๋ฆฌํฌํธ]
๊ฒ์ ๊ณํ์ ๊ตฌ์กฐํํ๊ธฐ ์ํด Pydantic ๋ชจ๋ธ์ ์ ์ํฉ๋๋ค.
from pydantic import BaseModel, Field
class WebSearchItem(BaseModel):
reason: str = Field(description="์ด ๊ฒ์์ด ํ์ํ ์ด์ ")
query: str = Field(description="๊ฒ์์ด")
class WebSearchPlan(BaseModel):
searches: list[WebSearchItem] = Field(description="์ํํ ์น ๊ฒ์ ๋ชฉ๋ก")
from agents import Agent
HOW_MANY_SEARCHES = 5
planner_agent = Agent(
name="PlannerAgent",
instructions=f"You are a helpful research assistant. Given a query, come up with a set of web searches to perform to best answer the query. Output {HOW_MANY_SEARCHES} terms to query for.",
model="gpt-4o-mini",
output_type=WebSearchPlan, # โ Structured Output (Format Guardrail)
)
๐ก
output_type์ ์ง์ ํ๋ฉด LLM์ด ๋ฐ๋์ ํด๋น ์คํค๋ง์ ๋ง๋ JSON๋ง ๋ฐํํฉ๋๋ค. ์ด๋ ํ์ฑ ์ค๋ฅ๋ฅผ ์์ฒ ์ฐจ๋จํ๋ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ ๋๋ค.
result = await Runner.run(planner_agent, f"Query: {query}")
search_plan = result.final_output_as(WebSearchPlan)
print(f"Will perform {len(search_plan.searches)} searches")
Search Agent๋ OpenAI์ ๋ด์ฅ WebSearchTool์ ์ฌ์ฉํ์ฌ ์ค์ ์น ๊ฒ์์ ์ํํฉ๋๋ค.
from agents import Agent, WebSearchTool
search_agent = Agent(
name="Search agent",
instructions="๊ฒ์์ด์ ๋ํด 2-3๋ฌธ๋จ์ผ๋ก ์์ฝํ์ธ์.",
tools=[WebSearchTool(search_context_size="low")], # OpenAI ๋ด์ฅ ๊ฒ์ ํด
model="gpt-4o-mini",
)
์ฌ๋ฌ ๊ฒ์์ด๋ฅผ ๋ณ๋ ฌ(Parallel)๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด Deep Research์ ํต์ฌ์
๋๋ค. asyncio.create_task์ asyncio.gather๋ฅผ ์ฌ์ฉํฉ๋๋ค.
import asyncio
async def perform_searches(search_plan: WebSearchPlan) -> list[str]:
"""๊ฒ์ ๊ณํ์ ๋ชจ๋ ๊ฒ์์ ๋ณ๋ ฌ๋ก ์คํ"""
# ๊ฐ ๊ฒ์ ์์ดํ
์ Task๋ก ์์ฑ
tasks = [
asyncio.create_task(search(item))
for item in search_plan.searches
]
# ๋ณ๋ ฌ๋ก ์คํ ๋ฐ ๊ฒฐ๊ณผ ์์ง
results = await asyncio.gather(*tasks)
return results
async def search(item: WebSearchItem) -> str:
"""๊ฐ๋ณ ๊ฒ์ ์ํ"""
input_text = f"Search term: {item.query}\nReason for searching: {item.reason}"
result = await Runner.run(search_agent, input_text)
return str(result.final_output)
๐ก
asyncio.gather(*tasks)๋ ๋ชจ๋ Task๋ฅผ ๋ณ๋ ฌ๋ก ์คํํ๊ณ , ๋ชจ๋ ๊ฒฐ๊ณผ๊ฐ ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค. 5๊ฐ์ ๊ฒ์์ ์์ฐจ์ ์ผ๋ก ํ๋ฉด 5๋ฐฐ์ ์๊ฐ์ด ๊ฑธ๋ฆฌ์ง๋ง, ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ํฐ ์ฑ๋ฅ ํฅ์์ ์ป์ ์ ์์ต๋๋ค.
์ค์ ํ๋ก์ ํธ์์๋ asyncio.as_completed๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฃ๋๋ ๊ฒ์๋ถํฐ ์ฒ๋ฆฌํ๊ณ ์งํ ์ํฉ์ ์ถ์ ํ ์ ์์ต๋๋ค.
async def perform_searches(self, search_plan: WebSearchPlan) -> list[str]:
"""๊ฒ์์ ๋ณ๋ ฌ๋ก ์ํํ๊ณ ์งํ ์ํฉ์ ์ถ์ """
num_completed = 0
tasks = [
asyncio.create_task(self.search(item))
for item in search_plan.searches
]
results = []
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
results.append(result)
num_completed += 1
print(f"Searching... {num_completed}/{len(tasks)} completed")
return results
๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ข ํฉํ์ฌ ์ต์ข ๋ฆฌํฌํธ๋ฅผ ์์ฑํ๋ Agent์ ๋๋ค.
from pydantic import BaseModel, Field
class ReportData(BaseModel):
short_summary: str = Field(
description="A short 2-3 sentence summary of the findings."
)
markdown_report: str = Field(description="The final report")
follow_up_questions: list[str] = Field(
description="Suggested topics to research further"
)
writer_agent = Agent(
name="WriterAgent",
instructions=(
"You are a senior researcher tasked with writing a cohesive report for a research query. "
"You will be provided with the original query, and some initial research done by a research assistant.\n"
"You should first come up with an outline for the report that describes the structure and "
"flow of the report. Then, generate the report and return that as your final output.\n"
"The final output should be in markdown format, and it should be lengthy and detailed. Aim "
"for 5-10 pages of content, at least 1000 words.\n"
"๋ฌด์กฐ๊ฑด ํ๊ตญ์ด๋ก ์์ฑํด์ค."
),
model="gpt-4o-mini",
output_type=ReportData,
)
async def write_report(query: str, search_results: list[str]) -> ReportData:
"""๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํ์ผ๋ก ์ต์ข
๋ฆฌํฌํธ ์์ฑ"""
input_text = f"Original query: {query}\nSummarized search results: {search_results}"
result = await Runner.run(writer_agent, input_text)
return result.final_output_as(ReportData)
๋ชจ๋ ๋จ๊ณ๋ฅผ ํตํฉํ๋ ResearchManager ํด๋์ค์
๋๋ค.
class ResearchManager:
def __init__(self, api_key: str):
"""Initialize the ResearchManager with an OpenAI API key"""
self.api_key = api_key
async def run(self, query: str):
"""Run the deep research process, yielding the status updates and the final report"""
# 1. Planning phase
search_plan = await self.plan_searches(query)
# 2. Searching phase
search_results = await self.perform_searches(search_plan)
# 3. Writing phase
report = await self.write_report(query, search_results)
return report
Gradio app ์ ํ๊น ํ์ด์ค์ ์ ๋ก๋ํ๊ธฐ ์ํด ๋์ ์ผ๋ก API Key๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํด ํ๊ฒฝ ๋ณ์๋ฅผ ์์๋ก ์ค์ ํ๋ ํจํด์ ์ฌ์ฉํฉ๋๋ค.
async def plan_searches(self, query: str) -> WebSearchPlan:
"""Plan the searches to perform for the query"""
original_key = os.environ.get("OPENAI_API_KEY")
os.environ["OPENAI_API_KEY"] = self.api_key
try:
result = await Runner.run(planner_agent, f"Query: {query}")
return result.final_output_as(WebSearchPlan)
finally:
# Restore original key
if original_key:
os.environ["OPENAI_API_KEY"] = original_key
else:
os.environ.pop("OPENAI_API_KEY", None)
๐ก
API Key๋ฅผ ๋ค๋ฃฐ ๋๋ ๋ฐ๋์ try-finally ๋ธ๋ก์ ์ฌ์ฉํ์ฌ ์๋ ํ๊ฒฝ ๋ณ์๋ฅผ ๋ณต์ํด์ผ ํฉ๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ๋ค๋ฅธ ์ฝ๋์์ ์๋ชป๋ API Key๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ค์๊ฐ ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์ ๊ณตํ๋ Gradio UI๋ฅผ ๊ตฌ์ถํ์ต๋๋ค.
import gradio as gr
async def run(query: str, api_key: str):
"""Run research and yield status updates"""
# Validate API key
if not api_key or not api_key.strip():
yield ("", "โ **Error: Please provide a valid OpenAI API key**", "")
return
# Clear query box and show initial status
yield ("", "๐ **Starting research...**", "")
status_text = ""
final_report = ""
async for chunk in ResearchManager(api_key).run(query):
# Check if this chunk contains the final report
if "---" in chunk:
parts = chunk.split("## ๐ Research Report")
if len(parts) == 2:
status_text = parts[0]
final_report = "## ๐ Research Report" + parts[1]
yield ("", status_text, final_report)
else:
status_text = chunk
yield ("", status_text, final_report)
.status-container {
animation: fadeBlur 1.5s ease-in-out infinite;
}
@keyframes fadeBlur {
0%, 100% {
opacity: 1;
filter: blur(0px);
}
50% {
opacity: 0.6;
filter: blur(0.5px);
}
}
OpenAI SDK์ Agent ํ๋ ์์ํฌ์ Python AsyncIO๋ฅผ ๊ฒฐํฉํ์ฌ ํจ์จ์ ์ธ Deep Research ์์คํ ์ ๊ตฌํํ์ต๋๋ค.
ํต์ฌ์ ๋ค์ ์ธ ๊ฐ์ง์ ๋๋ค:
- Structured Output์ ํตํ ์ ์ด ๊ฐ๋ฅ์ฑ: Pydantic ๋ชจ๋ธ๋ก LLM ์๋ต์ ๊ตฌ์กฐํ
- ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ํตํ ์ฑ๋ฅ: AsyncIO๋ก ์ฌ๋ฌ ๊ฒ์์ ๋์์ ์คํ
- ๋ช ํํ ๋จ๊ณ ๋ถ๋ฆฌ: Plan โ Search โ Report์ 3๋จ๊ณ ํ์ดํ๋ผ์ธ
์ด ํจํด์ ๋จ์ํ Q&A๋ฅผ ๋์ด์, ๋ ผ๋ฆฌ์ ์ด๊ณ ์ฒด๊ณ์ ์ธ ์ฌ์ธต ๋ฆฌ์์น ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํ๋ AI ์์คํ ์ ๊ตฌ์ถํ๋ ๋ฐ ํจ๊ณผ์ ์ ๋๋ค.