Agent RAG 구현 레포트
작성일: 2025-10-12 프로젝트: 농협 대출 상담 Agent (Hybrid Search + Tavily Web Search)
📋 목차
구현 개요
프로젝트 목표
Vercel AI SDK Python 샘플을 기반으로, 농협 대출 상품 검색과 웹 검색을 결합한 AI Agent 시스템 구현
기술 스택
- 백엔드: FastAPI + OpenAI Function Calling
- 프론트엔드: Next.js + Vercel AI SDK
- 도구: Hybrid Search (BM25 + Vector), Tavily Search API
- 프로토콜: Data Stream Protocol (실시간 스트리밍)
완료된 작업
✅ 1. 프로젝트 클론 및 구조 파악
git clone https://github.com/vercel-labs/ai-sdk-preview-python-streaming.git agent_app파악한 구조:
- FastAPI 백엔드:
api/index.py - 도구 시스템:
api/utils/tools.py - Next.js 프론트엔드:
app/,components/ - 기존 weather tool 제거 필요
✅ 2. 환경 설정
파일: agent_app/.env.local
OPENAI_API_KEY=sk-proj-...
DATABASE_URL=postgresql://...
TAVILY_API_KEY=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✅ 3. 도구 구현
파일: agent_app/api/utils/tools.py
hybrid_search_tool
- 기존
hybrid_search.py활용 - 농협 대출 상품 검색 (BM25 + Vector + RRF)
- 파라미터:
query,limit
def hybrid_search_tool(query: str, limit: int = 3) -> List[Dict]:
"""농협 대출 상품 하이브리드 검색 도구"""
try:
results = execute_hybrid_search(query, limit=limit)
return results
except Exception as e:
print(f"Hybrid search error: {e}")
return []tavily_search_tool
- Tavily API 통합
- 최신 금융 정보 웹 검색
- 파라미터:
query,max_results
def tavily_search_tool(query: str, max_results: int = 3) -> List[Dict]:
"""Tavily 웹 검색 도구"""
url = "https://api.tavily.com/search"
payload = {
"api_key": TAVILY_API_KEY,
"query": query,
"max_results": max_results,
"search_depth": "basic"
}
response = requests.post(url, json=payload, timeout=10)
return response.json().get("results", [])✅ 4. FastAPI 백엔드 통합
파일: agent_app/api/index.py
구현 내용:
- Weather tool 제거
- Hybrid Search와 Tavily Search 추가
- OpenAI Function Calling 설정
- Data Stream Protocol 구현
# 도구 정의
available_tools = {
"hybrid_search_tool": hybrid_search_tool,
"tavily_search_tool": tavily_search_tool,
}
# Function Calling 설정
tools=[
{
"type": "function",
"function": {
"name": "hybrid_search_tool",
"description": "농협 대출 상품을 검색합니다.",
"parameters": {...}
}
},
{
"type": "function",
"function": {
"name": "tavily_search_tool",
"description": "웹에서 최신 금융 정보를 검색합니다.",
"parameters": {...}
}
}
]✅ 5. package.json 수정
파일: agent_app/package.json
변경 전:
"fastapi-dev": "pip3 install -r requirements.txt && python3 -m uvicorn api.index:app --reload"변경 후:
"fastapi-dev": "cd .. && uv run uvicorn agent_app.api.index:app --reload --host 0.0.0.0 --port 8000"이유: macOS의 externally-managed Python 환경 문제 해결
✅ 6. 테스트 스크립트 작성
파일: agent_app/test_tools.py
테스트 결과:
✅ Hybrid Search: 성공
- 의사 전용 대출 검색
- Top 3 결과 반환
- RRF 점수 계산
✅ Tavily Search: 성공
- 2025년 한국 기준금리 검색
- 고품질 출처 (KDI, Trading Economics)
- Relevance score 제공✅ 7. 문서화
파일:
agent_app/README.md: 전체 프로젝트 문서agent_app/QUICKSTART.md: 빠른 시작 가이드search_app/CLAUDE.md: 프로젝트 가이드 업데이트
✅ 8. 프론트엔드 설치
pnpm install # 527개 패키지 설치 완료발견된 문제점
❌ 문제 1: 스트리밍 응답이 전달되지 않음
증상:
- 브라우저에서 "안녕" 입력 시 "thinking..." 상태로 멈춤
- 응답이 전혀 표시되지 않음
- curl 테스트 결과:
transfer closed with outstanding read data remaining
로그 분석:
$ curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"안녕"}]}'
# 결과: HTTP/1.1 200 OK (연결은 성공)
# 하지만: transfer closed with outstanding read data remaining원인 분석:
None 값 JSON 인코딩 오류
python# 문제 코드 yield '0:{text}\n'.format(text=json.dumps(choice.delta.content, ensure_ascii=False)) # choice.delta.content가 None일 경우 오류 발생스트림 종료 처리 누락
chunk.choices == []조건이 제대로 동작하지 않음- finish_reason 처리 로직 불완전
도구 호출 델타 누적 로직 오류
- tool_call 인자가 점진적으로 전달되는데 처리 미흡
❌ 문제 2: 여러 uvicorn 프로세스 실행 중
확인된 프로세스:
$ ps aux | grep uvicorn
# 발견된 프로세스들:
1. /agent_app.api.index:app --reload --host 0.0.0.0 --port 8000
2. /api.index:app --reload (이전 실행)
3. 기타 오래된 프로세스들잠재적 영향:
- 포트 충돌 가능성
- 메모리 낭비
- 혼란스러운 로그
문제 해결 시도
🔧 수정 1: 스트리밍 로직 전면 개선
파일: agent_app/api/index.py
변경 내용:
# 개선된 스트리밍 로직
for chunk in stream:
delta = chunk.choices[0].delta if chunk.choices else None
finish_reason = chunk.choices[0].finish_reason if chunk.choices else None
# 텍스트 델타 처리 (None 체크 추가)
if delta and delta.content:
yield '0:{text}\n'.format(text=json.dumps(delta.content, ensure_ascii=False))
# 도구 호출 델타 처리 (점진적 누적)
if delta and delta.tool_calls:
for tool_call in delta.tool_calls:
if tool_call.id:
# 새로운 도구 호출 시작
draft_tool_calls_index += 1
draft_tool_calls.append({
"id": tool_call.id,
"name": tool_call.function.name or "",
"arguments": tool_call.function.arguments or ""
})
else:
# 기존 도구 호출에 인자 추가
if draft_tool_calls_index >= 0:
draft_tool_calls[draft_tool_calls_index]["arguments"] += (
tool_call.function.arguments or ""
)
# 도구 호출 완료 처리
if finish_reason == "tool_calls":
for tool_call in draft_tool_calls:
# 도구 호출 정보 전송
yield '9:{{"toolCallId":"{id}","toolName":"{name}","args":{args}}}\n'.format(...)
# 도구 실행 및 결과 전송
try:
tool_result = available_tools[tool_call["name"]](**json.loads(tool_call["arguments"]))
yield 'a:{{"toolCallId":"{id}","toolName":"{name}","args":{args},"result":{result}}}\n'.format(...)
except Exception as e:
# 오류 처리 추가
error_result = {"error": str(e)}
yield 'a:{...}\n'.format(...)
# 스트림 종료 처리 (명확한 조건)
if finish_reason == "stop":
yield 'e:{{"finishReason":"stop","usage":{{"promptTokens":0,"completionTokens":0}},"isContinued":false}}\n'
elif finish_reason == "tool_calls":
yield 'e:{{"finishReason":"tool-calls","usage":{{"promptTokens":0,"completionTokens":0}},"isContinued":false}}\n'개선 사항:
- ✅ None 값 체크 추가 (
if delta and delta.content) - ✅ 도구 호출 인자 점진적 누적 로직 수정
- ✅ 명확한 finish_reason 처리
- ✅ 예외 처리 추가 (도구 실행 실패 시)
- ✅ 스트림 종료 보장
🔄 수정 2: uvicorn 자동 재시작
FastAPI의 --reload 옵션으로 인해 코드 수정 후 자동 재시작됨:
INFO: Waiting for application startup.
INFO: Application startup complete.현재 상태
서버 상태
- ✅ 프론트엔드: http://localhost:3000 (실행 중)
- ⚠️ 백엔드: http://localhost:8000 (실행 중이나 응답 불완전)
테스트 결과
# 도구 단독 테스트
$ uv run python test_tools.py
✅ 모든 도구 정상 작동
# API 엔드포인트 테스트
$ curl -X POST http://localhost:8000/api/chat -d '...'
⚠️ 연결은 성공하나 스트림 전송 불완전로그 확인
[1] INFO: Started server process [39300]
[1] INFO: Waiting for application startup.
[1] INFO: Application startup complete.해결 방법
🚀 즉시 시도할 방법
방법 1: 서버 완전 재시작
# 모든 백그라운드 프로세스 종료
pkill -f "pnpm dev"
pkill -f uvicorn
# 깨끗하게 재시작
cd agent_app
pnpm dev방법 2: 원본 코드와 비교
Vercel 샘플의 원본 api/index.py와 현재 코드를 비교하여 누락된 부분 확인
원본 코드 특징:
# 원본은 tool_calls 처리 후 메시지에 도구 결과 추가하고 재귀 호출
messages.append(...)
for tool_call in draft_tool_calls:
messages.append(...)
# 그리고 다시 LLM 호출
# 우리 코드는 이 재귀 호출이 누락됨!방법 3: 단순화된 테스트
도구 없이 단순 텍스트 응답만 테스트:
# tools 배열을 비우고 테스트
tools=[]방법 4: 로깅 추가
디버깅을 위한 상세 로그 추가:
import logging
logging.basicConfig(level=logging.DEBUG)
for chunk in stream:
logging.debug(f"Chunk: {chunk}")
# ...🔍 추가 확인 필요 사항
OpenAI API 응답 형식 확인
- 실제로
chunk.choices[0].delta.content가 어떤 값을 반환하는지 - tool_calls 델타가 어떤 형식으로 오는지
- 실제로
Data Stream Protocol 형식 검증
0:text\n형식이 정확한지- 프론트엔드가 기대하는 형식과 일치하는지
프론트엔드 에러 확인
- 브라우저 개발자 도구 콘솔
- 네트워크 탭에서 실제 응답 확인
파일 변경 사항
생성된 파일
- ✅
agent_app/.env.local- 환경 변수 - ✅
agent_app/api/utils/tools.py- 도구 구현 - ✅
agent_app/test_tools.py- 테스트 스크립트 - ✅
agent_app/README.md- 프로젝트 문서 - ✅
agent_app/QUICKSTART.md- 빠른 시작 가이드 - ✅
agent_app/IMPLEMENTATION_REPORT.md- 이 레포트
수정된 파일
- ✅
agent_app/api/index.py- 백엔드 메인 로직 - ✅
agent_app/package.json- npm 스크립트 - ✅
search_app/CLAUDE.md- 프로젝트 가이드
삭제된 코드
- ❌ Weather tool 관련 코드 (원본 샘플에서)
다음 단계
우선순위 1: 스트리밍 문제 해결 🔥
- 서버 완전 재시작
- 브라우저 개발자 도구로 실제 응답 확인
- 필요시 원본 코드 참고하여 재귀 호출 로직 추가
우선순위 2: 동작 검증
- 단순 텍스트 응답 테스트
- Hybrid Search 도구 호출 테스트
- Tavily Search 도구 호출 테스트
- 복합 질문 (두 도구 조합) 테스트
우선순위 3: 최적화
- 불필요한 uvicorn 프로세스 정리
- 로깅 추가 (디버깅용)
- 에러 처리 강화
- 성능 측정
결론
달성한 것 ✅
- Agent 아키텍처 완전 구현
- 두 가지 도구 (Hybrid Search, Tavily Search) 통합
- 백엔드/프론트엔드 통합 환경 구축
- 포괄적인 문서화
남은 과제 ⚠️
- 스트리밍 프로토콜 응답 불완전
- 도구 호출 후 LLM 재귀 호출 로직 구현 필요 가능성
- 실제 사용자 테스트 필요
교훈 📚
OpenAI Streaming API는 복잡함
- 델타 방식의 점진적 업데이트
- None 값 처리 필수
- finish_reason 타이밍 중요
Data Stream Protocol 이해 필요
0:text,9:tool_call,a:tool_result,e:finish형식- 프론트엔드가 기대하는 정확한 형식 필수
도구 시스템은 잘 작동함
- hybrid_search_tool: 완벽 작동
- tavily_search_tool: 완벽 작동
- 문제는 순전히 스트리밍 레이어에 있음
작성자: Claude Code 마지막 업데이트: 2025-10-12 11:47 PM KST