Azure AI Foundry Agent Service와 W&B Weave로 AI 에이전트 구축 및 평가
Azure AI Foundry Agent Service, SerpAPI, W&B Weave를 사용해 실시간으로 도구를 활용하는 AI 에이전트를 구축하고 평가하는 실습 가이드. 이 글은 AI 번역본입니다. 오역이 의심되면 댓글로 알려주세요.
Created on September 12|Last edited on September 12
Comment
구축 지능형 AI 에이전트 실시간 정보를 가져오고, 외부 API에 연결하며, 즉석에서 복잡한 워크플로를 오케스트레이션하는 에이전트를 만드는 일은 결코 간단하지 않습니다. 하지만 Microsoft의 Azure AI Foundry Agent Service를 사용하면 빠르게 구성하고 견고한 에이전트 애플리케이션 배포 Azure 생태계가 제공하는 보안, 확장성, 통합의 모든 이점을 그대로 누리면서.
이 가이드는 Foundry Agent Service를 사용해 사용자의 선호도에 따라 호텔과 주변 식당을 찾아 여행 계획을 도와주는 여행 보조 에이전트를 구축하는 과정을 다룹니다. 에이전트는 Azure Functions에 호스팅된 실제 API와 상호작용하여 실시간 데이터를 가져오고, 사용자의 입력을 기반으로 필요한 정보를 추론한 뒤, 적절한 도구를 호출해 추천을 제공합니다. 또한 연동을 수행합니다. W&B Weave 대화 전반에서 에이전트의 결정을 이해하고 디버깅할 수 있도록, 입력과 출력 등 에이전트의 동작을 상세히 추적합니다.
Azure 서비스를 설정하고 구성하는 방법, 검색 도구를 등록하는 방법, 실시간 호텔 및 식당 추천을 위한 엔드포인트를 구현하는 방법, 그리고 마지막으로 Python을 사용해 에이전트에 연결하고 상호작용하는 방법을 학습하게 됩니다.
목차
목차에이전트란 무엇일까요? Azure AI Foundry Agent Service로 에이전트 구축하기Azure Functions로 도구 호스팅하기 Azure CLI 설정하기 도구 함수 작성하기 Azure AI Foundry Agent Service에서 에이전트에 도구 연결하기 에이전트 프롬프트 작성하기 Python으로 에이전트에 연결하기 BrowseComp로 웹 검색 기능 평가 Weave로 버그를 빠르게 드러내기 결론
에이전트란 무엇일까요?
AI 에이전트 계획 수립, 외부 도구 활용, 기억 유지, 시간에 따른 적응을 통해 특정 목표를 달성하도록 설계된 지능형 시스템입니다. 고정된 사전 규칙을 따르는 전통적인 프로그래밍 기반 자동화와 달리, AI 에이전트는 정보를 동적으로 처리하고 의사 결정을 내리며 피드백에 기반해 접근 방식을 끊임없이 개선합니다.
동안 챗봇 주로 대화를 중심으로 매 단계마다 사용자 입력이 필요한 챗봇과 달리, AI 에이전트는 독립적으로 동작합니다. 단순히 응답을 생성하는 데 그치지 않고, 실제로 행동을 수행하고 외부 시스템과 상호작용하며, 지속적인 감독 없이도 다단계 워크플로를 스스로 관리합니다.
AI 에이전트의 핵심 구성 요소는 다음과 같습니다:
- 도구: API, 데이터베이스, 소프트웨어에 연결해 기능을 확장하세요.
- 메모리: 작업 전반에 걸쳐 정보를 저장해 일관성과 회상을 개선하세요.
- 지속 학습과거 성과를 바탕으로 전략을 조정하고 개선하세요.
- 오케스트레이션: 다단계 프로세스를 관리하고, 작업을 분해하며, 다른 에이전트와 조율하세요.
Azure AI Foundry Agent Service로 에이전트 구축하기
에이전트를 구축하는 단계가 꽤 많으니, 지금 바로 튜토리얼을 시작해 봅시다.
시작하려면 Azure 계정을 만들고 결제를 설정한 뒤 Azure AI Foundry로 이동하세요. 다음으로 프로젝트를 생성합니다:

이제 Azure AI Foundry 왼쪽의 “Agents” 창으로 이동하세요. 새 에이전트를 만들려면 “New Agent” 버튼을 클릭합니다.

에이전트를 생성하면 다음과 같은 창이 표시됩니다:

이 페이지에서는 에이전트의 기본 모델을 조정하고, 동작을 구성하며, Knowledge 섹션을 통해 외부 데이터 소스에 연결하고, Actions로 런타임 도구 사용을 활성화할 수 있습니다. 또한 Temperature와 Top P 슬라이더를 조정해 에이전트의 창의성과 결정론적 성향을 미세 조정할 수 있습니다.
이 튜토리얼에서는 에이전트를 다양한 “Actions”(즉, Tools)에 연결해 특정 데이터 소스에서 실시간 정보를 가져오도록 하는 방법을 살펴봅니다. 에이전트를 구성하기에 앞서, 에이전트가 호출할 기반 API를 먼저 구축하겠습니다.
하나의 엔드포인트가 호텔 검색을 처리합니다. 이 엔드포인트는 위치, 체크인·체크아웃 날짜, 성인 수, 선택적인 통화를 입력으로 받습니다. 그런 다음 SerpAPI에 호텔 데이터를 요청하고, 응답을 파싱해 호텔 이름, 요금, 좌표, 예약 링크를 반환합니다. 결과가 없으면 해당하는 안내 메시지를 반환합니다.
다른 엔드포인트는 쿼리, 위치 좌표, 선택적인 요리를 기반으로 음식 추천을 제공합니다. 검색 문자열을 정규화하고 SerpAPI를 사용해 주변 음식점을 찾은 뒤, 이름, 평점, 리뷰 수를 반환합니다. 일치하는 결과가 없으면 오류 메시지로 응답합니다. 이 엔드포인트들은 나중에 에이전트에 추가되어, 사용자의 여행 관련 질의에 응답할 때 활용됩니다.
Azure Functions로 도구 호스팅하기
도구를 호스팅하기 위해 Azure Functions를 사용하고, 에이전트에 실시간 데이터를 제공하기 위해 Serper API를 함께 사용하겠습니다. 먼저 여기에서 Serper API 키를 발급받아야 합니다. 또한 로컬 개발 환경이 Azure AI Functions를 배포할 준비가 되어 있는지 확인해야 합니다. Azure Functions 배포 요구 사항에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
Azure CLI 설정하기
먼저 Azure CLI를 설치해야 합니다. 저는 Mac을 사용하므로 macOS 기준 설치 단계를 보여드리겠습니다. 다른 플랫폼을 사용 중이라면 다음을 참고하세요. 문서 다른 플랫폼의 경우.
brew update && brew install azure-cli
그다음 Azure 계정에 로그인하세요:
az login
이제 Azure Functions용 패키지 몇 가지를 설치하겠습니다:
brew tap azure/functionsbrew install azure-functions-core-tools@4brew link --overwrite azure-functions-core-tools@4
참고로 위 패키지들을 설치하려면 Xcode 명령줄 도구를 업데이트해야 할 수 있습니다. 또한 현재 Mac 기기에서 로컬 Functions 실행을 방해하는 몇 가지 버그가 있으므로, 이 튜토리얼에서는 로컬에서 도구를 테스트하는 단계를 생략하겠습니다.
다음으로 Azure의 구독 ID를 확인해야 합니다. 아래 명령을 실행하면 확인할 수 있습니다:
az account show --query id -o tsv
그다음 이 값을 복사한 뒤, 다음 명령어로 환경 변수에 내보내세요:
export AZURE_SUBSCRIPTION_ID=your_id_from_above
이제 함수들을 호스팅할 보일러플레이트로 사용할 템플릿을 만들 준비가 되었습니다. 다음 명령을 실행해 시작하세요:
azd init --template functions-quickstart-python-http-azd -e flexquickstart-py
다음으로 다음 명령으로 가상 환경을 설정하세요:
python3 -m venv .venv source .venv/bin/activate
필요한 Azure 리소스 공급자가 등록되어 있는지 확인하세요:
az provider register --namespace Microsoft.App
도구 함수 작성하기
다음으로, 우리는 교체하겠습니다 function_app.py 여행 에이전트가 사용할 도구의 로직이 담긴 다음 코드를 파일로 저장하세요:
import azure.functions as funcimport loggingimport requestsimport jsonAPI_KEY = "your_serper_api_key"app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)@app.route(route="foodsearch", methods=["POST"])def food_search(req: func.HttpRequest) -> func.HttpResponse:logging.info("🔵 food_search triggered")try:data = req.get_json()query = data.get("query")latitude = data.get("latitude")longitude = data.get("longitude")cuisine = data.get("cuisine")if not (query and latitude and longitude):return func.HttpResponse(json.dumps({"error": "Missing required parameters. Must include query, latitude, and longitude."}),status_code=400)search_query = f"{query} {cuisine}" if cuisine else querysearch_params = f"ll=@{latitude},{longitude},14z"url = f"https://serpapi.com/search?engine=google_local&q={requests.utils.quote(search_query)}&{search_params}&api_key={API_KEY}"logging.info(f"🟠 Requesting: {url}")res = requests.get(url)res.raise_for_status()results = res.json().get("local_results", [])if not results:return func.HttpResponse(json.dumps({"error": "No food places found"}), status_code=404)places = [{"name": r.get("title", "Unknown"),"rating": r.get("rating", "N/A"),"reviews": r.get("reviews", 0)} for r in results]return func.HttpResponse(json.dumps({"food_places": places}), mimetype="application/json", status_code=200)except Exception as e:logging.exception("food_search failed")return func.HttpResponse(json.dumps({"error": "Server error", "details": str(e)}), status_code=500)@app.route(route="hotelsearch", methods=["POST"])def hotel_search(req: func.HttpRequest) -> func.HttpResponse:logging.info("🔵 hotel_search triggered")try:data = req.get_json()location = data.get("location")check_in = data.get("check_in_date")check_out = data.get("check_out_date")adults = data.get("adults")currency = data.get("currency", "USD")if not (location and check_in and check_out and adults):return func.HttpResponse(json.dumps({"error": "Missing required parameters", "received": data}), status_code=400)url = (f"https://serpapi.com/search?engine=google_hotels"f"&q={requests.utils.quote(location)}"f"&check_in_date={check_in}"f"&check_out_date={check_out}"f"&adults={adults}"f"¤cy={currency}"f"&api_key={API_KEY}")logging.info(f"🟠 Requesting: {url}")res = requests.get(url)res.raise_for_status()properties = res.json().get("properties", [])if not properties:return func.HttpResponse(json.dumps({"error": "No hotels found"}), status_code=404)hotels = [{"name": h.get("name", "No Name"),"price": h.get("rate_per_night", {}).get("lowest", "N/A"),"currency": currency,"lat": h.get("gps_coordinates", {}).get("latitude"),"lon": h.get("gps_coordinates", {}).get("longitude"),"link": h.get("link", "N/A")} for h in properties]return func.HttpResponse(json.dumps({"hotels": hotels}), mimetype="application/json", status_code=200)except Exception as e:logging.exception("hotel_search failed")return func.HttpResponse(json.dumps({"error": "Server error", "details": str(e)}), status_code=500)
이 스크립트는 두 가지 Azure Functions를 정의합니다:
- 하나는 호텔 검색용, 그리고
- 하나는 음식 검색용입니다.
각 함수는 들어오는 POST 요청을 수신하고, 요청 본문의 매개변수를 사용하여 SerpAPI에서 관련 데이터를 가져옵니다.
The hotel_search 함수는 여행지, 여행 날짜, 성인 수를 입력으로 받습니다. 이 함수는 SerpAPI를 통해 Google Hotels를 조회하고 호텔 옵션 목록을 반환합니다. 특히 각 호텔의 정확한 위도와 경도를 응답에 포함합니다. 이는 다음 단계에서 음식 검색 도구가 주변 식당을 정밀하게 찾을 수 있도록 해당 좌표를 사용할 것이기 때문에 중요합니다. 모호한 지역명에 의존하는 대신, 호텔의 정확한 지리 좌표에 음식 검색 쿼리를 정렬합니다.
The food_search 함수는 검색 쿼리, 좌표, 그리고 선택적인 음식 종류를 사용해 현지 식당을 검색합니다. SerpAPI의 Google Local 엔드포인트를 호출하여 식당 이름, 평점, 리뷰 수를 반환합니다.
이 도구들을 함께 사용하면 에이전트가 추론 과정을 따라 먼저 호텔을 찾고, 그 호텔의 좌표를 활용해 주변 음식점을 정확하게 추천할 수 있습니다. 덕분에 보조 도구가 같은 도시에 있더라도 실제 사용자 위치에서 멀리 떨어진 장소를 제안하는 등의 흔한 오류를 피할 수 있습니다.
마지막으로, 다음을 추가해야 합니다 requests requirements.txt 파일에 라이브러리를 추가하세요. 이 파일은 우리와 함께 생성되었습니다. function_app.py 파일입니다. 파일의 두 번째 줄에 "requests"를 그냥 추가로 붙여 넣으세요.
이제 다음 명령으로 함수들을 배포할 수 있습니다:
azd up
배포가 완료되면 터미널에 URL이 출력됩니다. 이 URL은 호스팅된 함수들의 기본 주소입니다. 실행 중에 호출될 수 있도록 에이전트 구성에서 이 주소를 사용해 도구를 등록하게 됩니다. 곧 필요하니 반드시 복사해 저장해 두세요.
Azure AI Foundry Agent Service에서 에이전트에 도구 연결하기
이제 함수들이 배포되었으니, 에이전트가 이들과 상호작용하는 방식을 정의하기 위해 OpenAPI 스키마를 작성하겠습니다. 스키마는 각 도구가 어떤 입력을 기대하고 어떤 출력을 반환하는지 명시하는 계약과 같습니다. 이를 통해 에이전트가 도구를 올바르게 호출하는 방법을 이해할 수 있습니다. 각 스키마에는 도구의 메타데이터, 호스팅된 서버 URL, 엔드포인트 경로, HTTP 메서드, 예상 입력 구조, 그리고 예상 응답의 형식이 포함됩니다.
호텔 검색 함수의 경우, 스키마는 위치, 체크인 및 체크아웃 날짜, 성인 수, 선택적 통화 값을 입력으로 받는 POST 요청을 정의합니다. 응답에는 호텔 이름, 가격, 위치 좌표, 예약 링크 등 호텔 상세 정보가 포함됩니다.
다음은 스키마입니다 hotel_search 도구:
{"openapi": "3.0.0","info": {"title": "Hotel Search Tool","version": "1.0.0","description": "Find hotels by location and travel dates."},"servers": [{"url": "url to your api"}],"paths": {"/hotelsearch": {"post": {"operationId": "hotelSearch","summary": "Search for hotels","description": "Search for hotels using location, check-in/check-out dates, and number of adults.","requestBody": {"required": true,"content": {"application/json": {"schema": {"type": "object","required": ["location","check_in_date","check_out_date","adults"],"properties": {"location": {"type": "string","example": "New York, NY"},"check_in_date": {"type": "string","format": "date","example": "2025-07-01"},"check_out_date": {"type": "string","format": "date","example": "2025-07-04"},"adults": {"type": "integer","example": 2},"currency": {"type": "string","example": "USD"}}}}}},"responses": {"200": {"description": "Success","content": {"application/json": {"schema": {"type": "object","properties": {"hotels": {"type": "array","items": {"type": "object","properties": {"name": {"type": "string"},"price": {"type": "string"},"currency": {"type": "string"},"lat": {"type": "number"},"lon": {"type": "number"},"link": {"type": "string"}}}}}}}}},"400": {"description": "Bad request","content": {"application/json": {"schema": {"type": "object","properties": {"error": {"type": "string"}}}}}},"500": {"description": "Server error","content": {"application/json": {"schema": {"type": "object","properties": {"error": {"type": "string"},"details": {"type": "string"}}}}}}}}}}}
음식 검색 함수의 스키마도 POST 요청을 사용하며, 필수로 query를 받고 선택적으로 cuisine, location, latitude, longitude를 받습니다. 응답으로는 음식점의 이름, 평점, 리뷰 수를 포함한 장소 목록 배열을 반환합니다.
다음은 스키마입니다 food_search 도구:
{"openapi": "3.0.0","info": {"title": "Food Search API","version": "1.0.0","description": "Searches for food places using a query and either a location string or coordinates."},"servers": [{"url": "url to your api","description": "Azure Function App endpoint"}],"paths": {"/api/foodsearch": {"post": {"summary": "Search food places","operationId": "foodSearch","requestBody": {"required": true,"content": {"application/json": {"schema": {"type": "object","required": ["query"],"properties": {"query": {"type": "string","example": "pizza"},"cuisine": {"type": "string","example": "Mexican"},"location": {"type": "string","example": "Chicago, IL"},"latitude": {"type": "number","example": 41.8781},"longitude": {"type": "number","example": -87.6298}}}}}},"responses": {"200": {"description": "Successful response","content": {"application/json": {"schema": {"type": "object","properties": {"food_places": {"type": "array","items": {"type": "object","properties": {"name": {"type": "string"},"rating": {"type": "number"},"reviews": {"type": "integer"}}}}}}}}},"400": {"description": "Bad request","content": {"application/json": {"schema": {"type": "object","properties": {"error": {"type": "string"}}}}}},"500": {"description": "Server error","content": {"application/json": {"schema": {"type": "object","properties": {"error": {"type": "string"},"details": {"type": "string"}}}}}}}}}}}
이 OpenAPI 스펙은 에이전트 구성에 연결되어, 에이전트가 사용자 요청에 응답할 때 각 엔드포인트를 올바르게 호출하는 방법을 알 수 있도록 합니다.
이 함수들을 에이전트에 연결하려면, 이제 Azure AI Foundry의 에이전트 빌더 대시보드로 돌아가서 새로 만든 에이전트의 “Actions” 패널 근처에 있는 추가 버튼을 클릭합니다.


“OpenAPI 3.0 specified tool”을 클릭하면 다음 화면이 표시됩니다:

여기에서 도구 이름과 에이전트가 이 도구를 어떻게 사용해야 하는지에 대한 설명을 입력할 수 있습니다. 정보를 입력한 뒤 “next” 버튼을 클릭하면, 앞서 생성한 OpenAPI 스키마 JSON을 입력할 수 있는 다음 화면이 표시됩니다:

스키마를 텍스트 필드에 붙여넣으면 도구를 생성할 수 있습니다. 동일한 절차를 반복해 음식 검색 도구도 추가하세요.
에이전트 프롬프트 작성하기
이제 에이전트가 어떻게 작동해야 하는지에 대한 핵심 지침을 제공합니다. 아래는 제가 작성한 지시 프롬프트입니다:

제가 에이전트에 사용한 전체 지시 프롬프트는 다음과 같습니다:
You are a smart travel assistant designed to help users find hotels and food spots in specific areas. Always respond clearly and helpfully, guiding users step by step if needed. Your main tools are hotel_searcher for hotels and food_searcher for food options. Use them precisely and effectively.When helping a user:Use hotel_searcher to find hotels. Always ensure the location includes the city and state/province (e.g., "Austin, Texas" not just "Austin").Use food_searcher to find nearby places to eat. If the user is referencing a hotel, extract the latitude and longitude from the hotel result and pass them to food_searcher. This ensures an accurate nearby search.If the user is planning a trip, find the hotel first, then use its coordinates to search for food.Always explain what you’re doing in plain language so the user understands. If something is missing (like a city name or query), ask the user clearly and continue once the info is provided.Be efficient, but conversational - your goal is to solve the user’s request using the tools as needed.
여기에 제공하는 지침은 에이전트의 동작을 좌우하는 핵심 요소이며, 사용자 경험에 직접적인 영향을 미칩니다. 이는 단순한 메타데이터가 아니라, 모델이 사용자 입력을 해석하고 도구를 언제 어떻게 사용할지 결정하며, 응답에서 어떤 표현을 사용할지를 정하는 운영 논리입니다. 지침이 명확하고 목적에 맞을수록 에이전트는 더 신뢰할 수 있고 유용해집니다.
이 프롬프트를 다듬는 데 시간을 들이는 것은 에이전트 성능을 높이는 가장 효과적인 방법 중 하나입니다. 작은 문구 수정만으로도 모델이 경계 사례를 더 매끄럽게 처리하거나 작업에 가장 적합한 도구를 우선시하도록 유도할 수 있습니다. 사용자가 혼란을 겪거나, 관련성이 낮은 결과가 나오거나, 에이전트가 실행해야 할 도구 호출을 건너뛰는 경우, 대개 점검해야 할 곳은 지시 프롬프트입니다.
이 예시 프롬프트는 에이전트에게 명확한 행동 계층을 제공합니다. 각 도구를 언제 사용할지, 어떤 문맥을 추출할지, 불완전한 요청을 어떻게 처리할지를 규정합니다. 또한 모델이 해석하기 쉽도록 평이한 언어로 작성되어 일관된 동작을 유지하는 데 핵심이 됩니다. 사용자 피드백이나 실제 테스트 사례를 바탕으로 이를 지속적으로 개선하면 에이전트의 성능이 눈에 띄게 향상됩니다.
좋아요, 이제 에이전트를 테스트할 준비가 됐습니다! 채팅 창에서 빠르게 에이전트를 시험해 볼 수 있도록 “Try in playground” 옵션을 클릭하세요:


상단의 “View Code” 버튼을 클릭하면 에이전트에 접근하는 방법을 보여주는 샘플 코드를 확인할 수 있습니다.

Python으로 에이전트에 연결하기
다중 라운드 채팅 대화를 지원하도록 코드를 약간 수정했습니다. 아래 코드를 수정해 에이전트 구성 페이지에서 확인할 수 있는 에이전트의 고유 ID와, “View Code” 버튼이 제공하는 샘플 코드에서 찾을 수 있는 연결 문자열을 추가하세요. 다음은 에이전트의 응답을 추적하기 위해 Weave를 포함한 코드입니다:
import weaveweave.init("az_agent")from azure.ai.projects import AIProjectClientfrom azure.identity import DefaultAzureCredentialproject_client = AIProjectClient.from_connection_string(credential=DefaultAzureCredential(),conn_str="your_connection_string")agent = project_client.agents.get_agent("asst_MVmqhbpGlAay9Bs2QBf7WLGJ")thread = project_client.agents.create_thread()@weave.op()def ask_foundry_agent(project_client, agent, thread_id, user_input):project_client.agents.create_message(thread_id=thread_id,role="user",content=user_input)project_client.agents.create_and_process_run(thread_id=thread_id,agent_id=agent.id)messages = project_client.agents.list_messages(thread_id=thread_id).text_messagesprint(str(messages))if messages:return messages[-1].text.valueelse:return "No response from assistant."print("Type 'exit' to end the conversation.")while True:user_input = input("You: ")if user_input.strip().lower() in ("exit", "quit"):print("Chat ended.")breakassistant_reply = ask_foundry_agent(project_client, agent, thread.id, user_input)print("Assistant:", assistant_reply)
이 코드는 명령줄 기반의 간단한 채팅 루프를 구성하여, Weave를 통한 다중 턴 대화 추적과 기본 로깅을 사용해 배포된 Azure 에이전트와 실시간으로 상호작용할 수 있게 합니다. 스크립트는 상호작용을 추적하기 위해 Weave를 초기화하는 것으로 시작합니다. weave.init("az_agent").
DefaultAzureCredential을 사용해 Azure에 인증하고, 다음을 통해 특정 에이전트 프로젝트에 연결합니다 AIProjectClient.from_connection_string(...), 여기서 워크스페이스에 맞는 올바른 연결 문자열을 제공해야 합니다. 에이전트 객체는 에이전트의 고유 ID를 사용해 가져오며, 새 스레드를 생성합니다. 이 스레드는 사용자 입력과 어시스턴트 응답 사이의 문맥을 유지해 다중 턴 대화를 가능하게 합니다.
��심 로직은 ask_foundry_agent 함수입니다. 전체적으로 다음을 수행합니다:
1. 다음을 사용해 사용자의 메시지를 에이전트로 보냅니다 create_message.
2. 다음을 사용해 에이전트에서 실행을 트리거합니다 create_and_process_run.
3. 다음을 사용해 스레드에서 어시스턴트의 응답을 가져옵니다 list_messages.
while 루프는 사용자가 "exit" 또는 "quit"을 입력할 때까지 에이전트와 계속 대화할 수 있게 해 주며, 돌아오는 대로 각 에이전트 응답을 출력합니다. 스크립트를 실행하고 모델과 대화한 뒤에는 Weave 안에서 우리의 트레이스를 확인할 수 있습니다.

Weave는 에이전트와의 모든 상호작용에서 정확히 무엇이 일어났는지 확인할 수 있게 해 주기 때문에 유용합니다. 스크린샷에서 함수에 대한 각 호출과 그 전체 입력과 출력까지 추적할 수 있습니다. 여기에는 사용자 입력, 스레드 ID, 에이전트 구성, 그리고 모델의 응답이 포함됩니다. 에이전트가 입력을 어떻게 해석했는지, 무엇을 반환했는지, 얼마나 걸렸는지 확인할 수 있습니다. 덕분에 누락된 응답, 잘못된 도구 사용, 느린 성능 같은 문제를 더 쉽게 디버그할 수 있습니다. 에이전트가 왜 그렇게 작동했는지 추측하는 대신, 트레이스를 확인해 정확히 어떤 값이 들어가고 무엇이 나왔는지 검증할 수 있습니다. 또한 시스템 프롬프트와 도구 지침이 올바르게 포함되었는지도 확인하는 데 도움이 됩니다. 여러 차례에 걸친 상호작용의 경우, 메시지 단위로 전체 흐름을 따라가며 문제가 발생한 지점을 정확히 짚어낼 수 있습니다.
BrowseComp로 웹 검색 기능 평가
내가 Azure Foundry Agents에서 가장 좋아하는 기능 중 하나는 웹 검색 도구에 대한 기본 제공 지원입니다이는 현대적인 AI 에이전트에 필수적인 기능이라고 믿습니다. Bing을 사용해 라이브 검색을 수행할 수 있는 에이전트를 구축한 뒤, 이를 기준으로 벤치마크해 보겠습니다. OpenAI의 BrowseComp 벤치마크는 추론, 끈기, 창의적인 검색 전략이 필요한 찾기 어려운 질문으로 에이전트를 테스트하도록 특별히 설계되었습니다.
BrowseComp에는 단일 쿼리로는 의도적으로 해결하기 어렵게 만든 질문들이 포함되어 있어, 동적 웹 검색에 의존하는 에이전트를 평가하기에 강력한 셋을 제공합니다. 각 에이전트를 Bing Search 지식 소스에 연결하면 실제와 유사한 검색 환경을 시뮬레이션할 수 있으며, 실시간 웹 데이터를 탐색하고 통합하라는 과제를 받았을 때 서로 다른 모델이 얼마나 잘 수행하는지 확인할 수 있습니다.
서로 다른 웹 검색 에이전트를 평가하기 위해, 백본 모델로 gpt-4o를 사용하는 내 에이전트를 벤치마크 대상으로 선택했습니다. Azure AI Foundry에서 에이전트를 설정한 뒤, 드롭다운 메뉴에서 적절한 모델 배포를 선택했습니다. 에이전트가 실시간 웹 데이터에 접근할 수 있도록, 라이브 인터넷 결과로 응답을 근거화할 수 있게 해 주는 Bing Search 지식 소스를 추가하겠습니다. Knowledge 섹션에서 “Add” 버튼을 클릭하세요. 그러면 에이전트에 연결할 수 있는 사용 가능한 데이터 소스 목록이 표시됩니다. 옵션 중에서 “Grounding with Bing Search”를 선택하세요.

이렇게 하면 에이전트가 Bing을 통해 실시간 웹 콘텐츠에 접근할 수 있습니다. 선택한 후에는 기존 Bing Search 연결을 선택하거나 새로 만들라는 프롬프트가 표시됩니다. 이미 API 키로 연결을 설정해 두었다면 해당 연결을 선택하고 “Connect”를 누르면 됩니다.
연결이 완료되면 에이전트가 웹에서 최신 검색 결과를 직접 가져올 수 있어, 온라인에서 찾은 내용을 바탕으로 최신 정보를 제공할 수 있습니다.

이제 Weave와 BrowseComp로 에이전트를 평가할 준비가 되었습니다. 아래 스크립트는 데이터셋의 일부를 준비하고, 에이전트를 초기화한 뒤, 사용할 것입니다. Weave 평가 데이터셋에서 각 에이전트의 성능을 비교하기 위해서입니다. 참고로 저는 평가에 데이터셋의 일부만 사용할 예정이므로, 에이전트의 정확한 성능을 확인하려면 이 스크립트를 전체 데이터셋에 대해 실행할 것을 권장합니다.
평가에 사용할 코드는 다음과 같습니다:
import osimport asyncioimport base64import hashlibimport pandas as pdimport jsonimport weavefrom litellm import completionfrom azure.ai.projects import AIProjectClientfrom azure.identity import DefaultAzureCredentialCONNECTION_STRING = "your_connection_string"AGENT_IDS = {"4o_web_searcher": "asst_vw3S54YgIzoy1WbdJWsceFiz"}# One client instance for efficiencyproject_client = AIProjectClient.from_connection_string(credential=DefaultAzureCredential(),conn_str=CONNECTION_STRING)############################### 1. Data Preparation##############################def derive_key(password: str, length: int) -> bytes:key = hashlib.sha256(password.encode()).digest()return (key * (length // len(key))) + key[: length % len(key)]def decrypt(ciphertext_b64: str, password: str) -> str:encrypted = base64.b64decode(ciphertext_b64)key = derive_key(password, len(encrypted))decrypted = bytes(a ^ b for a, b in zip(encrypted, key))return decrypted.decode(errors="ignore")def load_eval_dataset(n=10):csv_url = "https://openaipublic.blob.core.windows.net/simple-evals/browse_comp_test_set.csv"df = pd.read_csv(csv_url)dataset = []for i, row in df.iterrows():if i >= n:breaktry:question = decrypt(row['problem'], row['canary'])answer = decrypt(row['answer'], row['canary']) # <-- DECRYPT ANSWER TOO!dataset.append({"text": question, "label": answer})except Exception as e:print(f"Failed to decrypt row {i}: {e}")return dataset############################### 2. Agent Interface##############################def agent_inference(agent_id: str, prompt: str, thread=None) -> str:"""Run an inference with Azure AI agent and return the most recent message string value.Uses the last message."""if not thread:thread = project_client.agents.create_thread()# Post user's messageproject_client.agents.create_message(thread_id=thread.id,role="user",content=prompt)project_client.agents.create_and_process_run(thread_id=thread.id,agent_id=agent_id)messages = project_client.agents.list_messages(thread_id=thread.id)if messages.text_messages:latest = messages.text_messages[0].as_dict()text = latest.get("text", {}).get("value", "")return textreturn "[NO RESPONSE]"############################### 3. Weave Model Wrapper##############################class FourOWebSearcherModel(weave.Model):@weave.opdef predict(self, text: str) -> str:return agent_inference(AGENT_IDS["4o_web_searcher"], text)############################### 4. Scoring Function##############################@weave.opdef gpt4o_scorer(label: str, model_output: str) -> dict:"""Score the model's output by comparing it with the ground truth."""query = f"""YOU ARE A LLM JUDGE DETERMINING IF THE FOLLOWING MODEL GENERATED ANSWER IS THE SAME AS THE CORRECT ANSWERModel's Answer: {str(model_output)}Correct Answer: {label}Your task:1. State the model's predicted answer (answer only).2. State the ground truth (answer only).3. Determine if the model's final answer is correct (ignore formatting differences, etc.). RESPOND with the predicted and ground truth answer, followed with a JSON object containing the correctness encapsulated within these delimiters:```json{{ "correctness": true/false }}```"""response = completion(model="gpt-4o-2024-08-06",temperature=0.0,messages=[{"role": "user", "content": query}])# Parse responsetry:result = response.choices[0].message.contentjson_start = result.index("```json") + 7json_end = result.index("```", json_start)correctness = json.loads(result[json_start:json_end].strip()).get("correctness", False)except Exception as e:correctness = Falsereturn {"correctness": correctness, "reasoning": response.choices[0].message.content}############################### 5. Main Evaluation Loop##############################async def run_evaluations():weave.init("azure_agent_eval")dataset = load_eval_dataset(100)print(f"Loaded {len(dataset)} examples.")models = {"4o_web_searcher": FourOWebSearcherModel(),}scorers = [gpt4o_scorer]for model_name, model in models.items():print(f"\n\n=== EVALUATING {model_name.upper()} ===")evaluation = weave.Evaluation(dataset=dataset,scorers=scorers,name=f"{model_name} Evaluation")results = await evaluation.evaluate(model)print(f"Results for {model_name}: {results}")if __name__ == "__main__":# os.environ["OPENAI_API_KEY"] = "your_openai_api_key"asyncio.run(run_evaluations())
BrowseComp은 예외적으로 난도가 높은 벤치마크입니다. 웹 검색 에이전트를 피상적인 조회 작업의 한계를 넘어 테스트하��록 특별히 설계되었습니다. 다수의 문항이 다단계 추론, 창의적인 검색 쿼리, 그리고 난해한 출처 전반에 흩어진 단편 정보를 종합하는 능력을 요구합니다. OpenAI의 자체 테스트에서도, 심지어 고급 모델인 GPT-4o 그리고 GPT-4.5도 브라우징 도구를 갖추고 있음에도 불구하고 성과가 저조했습니다.
Weave로 버그를 빠르게 드러내기
처음에는 평가 스크립트에서 몇 가지 문제가 발생했습니다. 핵심 버그 중 하나는 데이터셋의 레이블을 제대로 복호화하지 않았다는 점이었습니다. 그 결과 모델 출력이 정답과 비교될 때마다 항상 오답으로 처리되었죠. 모델의 응답만 보면 겉보기에는 멀쩡해서 버그가 눈에 잘 띄지 않았지만, 점수가 전반적으로 0%로 나오는 바람에 문제가 드러났습니다.
바로 여기서 Weave가 큰 도움이 됐습니다. 비교 보기에서 무언가 이상하다는 게 바로 드러났습니다. 모델 출력이 뜻모를 값이나 비어 있는 값과 비교되어 평가되고 있었거든요. 특정 트레이스를 열어 보니 text 필드는 복호화되어 있었지만 label 필드는 복호화되지 않았다는 걸 확인했고, 문제는 즉시 명확해졌습니다.

해결 방법은 간단했습니다: 데이터셋을 로드하는 루프에서 질문과 정답 필드 모두에 복호화 함수를 호출하기만 하면 됐습니다. 그 다음부터는 레이블이 Weave에 제대로 표시되어 어떤 예제가 실제로 맞고 틀렸는지 분명하게 확인할 수 있었습니다. Weave 덕분에 디버깅 과정이 빠르고 시각적으로 진행되었고, 추측 없이 직접적인 증거를 바탕으로 몇 초 만에 스크립트를 수정할 수 있었습니다.
제 평가 결과는 다음과 같습니다.

전반적으로, 우리 에이전트는 우리가 만든 Browsecomp의 100개 예제 하위 집합에서 정확도 6%를 기록했습니다. 전체 벤치마크에서 테스트하지 않았기 때문에 최종 점수가 어떻게 나올지는 판단하기 어렵지만, OpenAI가 보고한 일부 점수와 비교하면 유망한 결과로 보입니다. OpenAI에 따르면, 브라우징을 활성화한 GPT-4o 모델은 정확도 1.9%를 기록했는데, 이는 우리 에이전트의 추정 성능에 비해 3배 이상 낮은 수치입니다.

결론
이 가이드는 단순히 여행 도우미를 만드는 데 그치지 않습니다. 실제 세계의 데이터와 도구에 연결했을 때 에이전트가 무엇을 할 수 있는지를 확장하는 데 초점을 맞춥니다. Azure AI Foundry Agent Service의 오케스트레이션 기능에 라이브 함수 엔드포인트, OpenAPI 스키마, 그리고 Weave를 통한 가시성을 결합하면 단순한 실험을 넘어서 실제로 주도적으로 행동하는 에이전트를 배포하게 됩니다. 초점은 대화에서 실행으로 이동합니다. 이런 시스템을 더 많이 구축할수록 중요한 것은 어떤 모델을 쓰느냐가 아니라, 도구와 로직, 데이터가 목적에 맞게 얼마나 매끄럽게 흐르고 결합되느냐입니다.
Add a comment