목차
- 문제와 접근
- Tavily 검색 도구
- Wikipedia 도구
- DuckDuckGo 검색 도구
- 도구 성능 비교 실험
- 커스텀 도구 — calculator
- PythonREPL 도구
- 복합 에이전트 (agent_loop)
- search_and_summarize — 검색 + 요약 파이프라인
- ToolWithFallback — 장애 대비 패턴
- 핵심 개념 총정리
- 자주 나오는 실수/주의사항
- 도구 교체가 결과에 미치는 영향 확인
1. 문제와 접근
외부 도구를 연결하면 모델 바깥의 데이터 품질과 장애가 답변에 영향을 준다. 검색·계산·코드 실행 도구를 조합하면서 어떤 결과를 성공으로 볼지, 대체 도구가 같은 목적을 수행하는지를 구분한다. 제공되는 도구라는 이유만으로 애플리케이션의 요구 조건이 충족된다고 가정하지 않는다.
1
2
| # 설치
pip install tavily-python wikipedia duckduckgo-search numexpr langchain_experimental
|
오늘 사용하는 도구 목록
| 도구 | 역할 | 패키지 |
|---|
| TavilySearchResults | 실시간 웹 검색 (AI 특화) | tavily-python |
| WikipediaQueryRun | 위키피디아 검색 | wikipedia |
| DuckDuckGoSearchResults | 무료 웹 검색 | duckduckgo-search |
| PythonREPLTool | 파이썬 코드 실행 | langchain_experimental |
| calculator | 수식 계산 (커스텀 @tool) | - |
2. Tavily 검색 도구
설정 및 사용
1
2
3
4
5
6
7
| from langchain_community.tools.tavily_search import TavilySearchResults
# .env 에 TAVILY_API_KEY=tvly-dev-xxxx 필요
search_tool = TavilySearchResults(max_results=3)
search_tool.name # → 'tavily_search_results_json'
search_tool.description # → '...'
|
검색 실행
1
2
3
4
5
| results = search_tool.invoke({'query': '2026년 한국 ETF 시장 동향'})
for i, r in enumerate(results, 1):
print(f"[{i}] {r.get('url', 'N/A')}")
print(f" {r.get('content', '')[:100]}...")
|
결과는 [{‘url’: …, ‘content’: …}, …] 형태의 리스트
LLM과 연결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| llm_with_search = llm.bind_tools([search_tool])
query = "2026년 나스닥 전망은?"
messages = [HumanMessage(content=query)]
response = llm_with_search.invoke(messages)
messages.append(response)
if response.tool_calls:
for tc in response.tool_calls:
result = search_tool.invoke(tc['args'])
result_text = str(result)[:2000] # 너무 길면 잘라서 전달
messages.append(ToolMessage(content=result_text, tool_call_id=tc['id']))
final = llm_with_search.invoke(messages)
print(final.content)
|
결과 포매팅 함수
1
2
3
4
5
6
7
| def format_search_results(results, max_summary=100):
formatted = []
for r in results:
content = r.get("content", "")
summary = content[:max_summary] + "..." if len(content) > max_summary else content
formatted.append({"url": r.get('url', ''), "summary": summary})
return formatted
|
3. Wikipedia 도구
1
2
3
4
5
6
7
8
9
10
11
| from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
wiki_api = WikipediaAPIWrapper(
top_k_results = 2, # 상위 2개 문서
doc_content_chars_max = 1000, # 최대 1000자
lang = 'ko' # 한국어
)
wiki_tool = WikipediaQueryRun(api_wrapper=wiki_api)
wiki_tool.invoke('인공지능') # → 위키피디아 텍스트 반환
|
LLM과 연결
1
2
3
4
5
6
7
8
9
10
11
12
13
| llm_with_wiki = llm.bind_tools([wiki_tool])
query = "랭체인에 대해서 알려줘"
messages = [HumanMessage(content=query)]
response = llm_with_wiki.invoke(messages)
messages.append(response)
if response.tool_calls:
for tc in response.tool_calls:
result = wiki_tool.invoke(tc['args'])
messages.append(ToolMessage(content=str(result)[:2000], tool_call_id=tc['id']))
final = llm_with_wiki.invoke(messages)
print(final.content)
|
키워드 문장 추출 유틸
1
2
3
4
5
6
7
| def extract_key_sentences(text, keyword, max_sentences=3):
sentences = [s.strip() for s in text.split(".") if s.strip()]
matched = [s + "." for s in sentences if keyword.lower() in s.lower()]
return matched[:max_sentences]
wiki_result = wiki_tool.invoke("인공지능")
extract_key_sentences(wiki_result, "인공지능")
|
4. DuckDuckGo 검색 도구
1
2
3
4
5
6
7
| from langchain_community.tools import DuckDuckGoSearchResults
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
ddg_wrapper = DuckDuckGoSearchAPIWrapper(max_results=3)
ddg_tool = DuckDuckGoSearchResults(api_wrapper=ddg_wrapper)
ddg_tool.invoke("랭체인 튜토리얼 2026")
|
도구마다 인증 방식과 반환 형식이 다르다. 제공 조건은 사용 시점의 문서에서 확인하고, 속도와 품질은 같은 질문을 실행한 결과로 비교한다.
5. 도구 성능 비교 실험
도구 비교에서는 같은 질문을 반복 실행하고 성공 여부와 결과의 근거를 함께 기록한다. 응답 길이와 한 번의 소요 시간만으로 품질을 결정하지 않으며, 표에 적은 일반적 특성과 실제 측정 결과도 구분한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import time
query = "2026 4월 AI 트렌드"
tools = {
"Tavily" : search_tool,
"Wikipedia": wiki_tool,
"DuckDuckGo": ddg_tool
}
for name, tool in tools.items():
start = time.time()
try:
result = tool.invoke({'query': query} if name == 'Tavily' else query)
elapsed = time.time() - start
result_str = str(result)
print(f"{name}: {elapsed:.2f}sec")
print(f"결과 길이: {len(result_str)}")
print(f"{result_str[:200]}")
except:
print(f"{name}: error")
print("=======")
|
비교 결과를 기록할 항목
| 항목 | 확인 방법 |
|---|
| 응답 시간 | 같은 질문의 반복 실행 시간과 실패 여부 기록 |
| 내용 적합성 | 질문에 필요한 근거와 출처가 포함됐는지 확인 |
| 최신성 | 검색된 자료의 작성·갱신 시점 확인 |
| 반환 형식 | 후속 요약에 필요한 본문과 출처를 추출할 수 있는지 확인 |
위 코드는 한 번의 호출을 보여 준다. 도구별 성능을 비교하려면 반복 실행 결과와 질문 유형을 함께 기록해야 한다.
6. 커스텀 도구 — calculator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import math
from langchain_core.tools import tool
@tool
def calculator(expression):
"""math expression with eval"""
try:
allowed = {
'math': math, 'abs': abs, 'round': round,
'min': min, 'max': max
}
result = eval(expression, {"__builtins__": {}}, allowed)
return str(result)
except:
return "calculation error"
calculator.invoke({'expression': 'math.sqrt(144) + math.pi'})
# → '12.141592653589793'
|
{"__builtins__": {}} 로 위험한 내장 함수 접근 차단
allowed 딕셔너리에 허용할 함수만 명시적으로 등록
7. PythonREPL 도구
LLM이 직접 파이썬 코드를 생성하고 실행시킬 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from langchain_experimental.tools import PythonREPLTool
python_tool = PythonREPLTool()
python_tool.invoke("print(sum(range(1, 101)))") # → 5050
# 복잡한 코드도 가능
code_str = """
import math
data = [23, 45, 12, 67, 34, 89, 56]
avg = sum(data) / len(data)
std = math.sqrt(sum((x - avg)**2 for x in data) / len(data))
print(f"평균: {avg:.1f}, 표준편차: {std:.1f}")
"""
python_tool.invoke(code_str)
|
LLM + PythonREPL 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| def run_code_agent(question, llm_with_tools, python_tool):
messages = [HumanMessage(content=question)]
response = llm_with_tools.invoke(messages)
messages.append(response)
code_text = ""
output = ""
if response.tool_calls:
for tc in response.tool_calls:
code_text = tc['args'].get('query', '')
output = str(python_tool.invoke(tc['args']))
messages.append(ToolMessage(content=output, tool_call_id=tc['id']))
final = llm_with_tools.invoke(messages)
return {"code": code_text, "output": output.strip(), "answer": final.content}
return {"code": "", "output": "", "answer": response.content}
result = run_code_agent("1부터 100까지의 소수를 구해줘", llm_with_python, python_tool)
print(result['code']) # LLM이 생성한 코드
print(result['output']) # 실행 결과
print(result['answer']) # LLM의 최종 답변
|
PythonREPLTool은 편리하지만 보안상 위험
프로덕션 환경에서는 샌드박스 격리 필수
8. 복합 에이전트 (agent_loop)
여러 도구를 하나의 에이전트에 등록하고 LLM이 상황에 맞는 도구를 선택하게 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| all_tools = [search_tool, wiki_tool, ddg_tool, calculator]
llm_agent = llm.bind_tools(all_tools)
tool_map = {t.name: t for t in all_tools}
def agent_loop(query, llm_agent, tool_map, max_turns=3):
messages = [HumanMessage(content=query)]
tools_used = []
for turn in range(max_turns):
response = llm_agent.invoke(messages)
messages.append(response)
if not response.tool_calls:
return {'query': query, 'tools_used': tools_used}
for tc in response.tool_calls:
tool_name = tc['name']
print(f" [{turn+1}] {tool_name}({tc['args']})")
tool_fn = tool_map.get(tool_name)
if tool_fn:
result = tool_fn.invoke(tc['args'])
else:
result = f"도구 {tool_name}를 찾을 수 없습니다"
messages.append(ToolMessage(content=str(result)[:2000], tool_call_id=tc['id']))
final = llm_agent.invoke(messages)
return final.content
|
실행 예시
1
2
3
4
5
6
7
8
9
| test_queries = [
'2026년 AI 스타트업 투자 동향을 알려줘', # → Tavily 사용
'랭체인이 뭔지 설명해줘', # → Wikipedia 사용
'원주율 x 100의 제곱근은?' # → calculator 사용
]
for q in test_queries:
answer = agent_loop(q, llm_agent, tool_map)
print(answer)
|
9. search_and_summarize — 검색 + 요약 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| def search_and_summarize(query, search_tool, llm, max_results=3):
raw_results = search_tool.invoke({'query': query})
sources = []
combined_text = ""
for r in raw_results[:max_results]:
url = r.get('url', 'no source')
content = r.get('content', '')
sources.append(url)
combined_text += f"[출처: {url}]\n{content}\n\n"
summary_prompt = f"""다음 검색 결과를 바탕으로 '{query}'에 대한 리포트를 작성하세요.
검색 결과:
{combined_text[:3000]}
리포트 형식:
1. 핵심 요약 (2~3문장)
2. 주요 포인트 (3개)
3. 출처 목록"""
response = llm.invoke([HumanMessage(content=summary_prompt)])
return {
'query' : query,
'sources' : sources,
'report' : response.content,
'raw_result_count': len(raw_results)
}
|
fallback은 예외를 숨기는 장치보다 대체 결과의 범위를 드러내는 흐름으로 본다. 다른 출처나 더 오래된 정보로 답했다면 그 차이를 결과에 남기고, 빈 결과가 정상적인 검색 실패인지 도구 장애인지 구분한다.
주 도구가 실패했을 때 대체 도구를 자동으로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| class ToolWithFallback:
def __init__(self, primary, fallback=None, max_retries=2):
self.primary = primary
self.fallback = fallback
self.max_retries = max_retries
self.stats = {'primary_ok': 0, 'fallback_ok': 0, 'errors': 0}
def invoke(self, args):
for attempt in range(self.max_retries):
try:
result = self.primary.invoke(args)
if result and str(result).strip():
self.stats['primary_ok'] += 1
return result
except:
print(f"{self.primary.name} error (attempt {attempt+1})")
if self.fallback:
try:
fallback_args = args.get('query', args) if isinstance(args, dict) else args
result = self.fallback.invoke(fallback_args)
self.stats['fallback_ok'] += 1
return f"[fallback: {self.fallback.name}] {result}"
except:
print("fallback fail too")
self.stats['errors'] += 1
return "no result"
# 사용
safe_search = ToolWithFallback(primary=search_tool, fallback=ddg_tool)
result = safe_search.invoke({"query": "랭체인 최신 업데이트"})
|
11. 핵심 개념 총정리
도구 선택 기준
1
2
3
4
5
| 실시간 최신 정보가 필요 → Tavily
백과사전 수준의 설명 필요 → Wikipedia
무료 검색이 필요 → DuckDuckGo
수식 계산 → calculator
코드 실행이 필요 → PythonREPL
|
전체 에이전트 흐름
1
2
3
4
5
6
7
8
9
| 사용자 질문
↓
llm.bind_tools(all_tools).invoke(messages)
↓ tool_calls 있음
tool_map[name].invoke(args) → 결과
↓
ToolMessage(content=result, tool_call_id=id)
↓
llm.invoke(messages) → 최종 답변
|
12. 자주 나오는 실수/주의사항
실수 1: Tavily와 Wikipedia의 인자 형식 혼동
1
2
3
4
5
6
7
| # Tavily → {'query': '...'}
result = search_tool.invoke({'query': '검색어'})
# Wikipedia → 문자열 직접
result = wiki_tool.invoke('검색어')
# 혼동하면 에러 발생 → 도구마다 인자 형식 확인 필수
|
실수 2: 검색 결과를 통째로 LLM에 넘기기
1
2
3
4
5
6
| # 틀린 코드 → 너무 길어서 컨텍스트 초과
result_text = str(result)
messages.append(ToolMessage(content=result_text, ...))
# 올바른 코드 → 잘라서 전달
result_text = str(result)[:2000]
|
실수 3: PythonREPL 보안 위험 무시
1
2
3
| # PythonREPLTool은 서버에서 실제 코드를 실행함
# 사용자 입력이 직접 들어오는 환경에서는 절대 사용 금지
# 신뢰할 수 있는 개발 환경에서만 사용
|
1
2
3
4
5
6
7
8
9
| # 틀린 코드 (attempt → attemp 오타)
for attemp in range(self.max_retries):
...
print(f"error (attempt {attempt+1})") # NameError!
# 변수명을 일치시켜야 함
for attempt in range(self.max_retries):
...
print(f"error (attempt {attempt+1})")
|
13. 도구 교체가 결과에 미치는 영향 확인
도구 선택에는 응답 시간뿐 아니라 반환 형식, 출처 확인 가능성, 실패 처리 방식이 함께 들어간다. 같은 인터페이스로 감싸더라도 제공하는 정보의 범위까지 같아지는 것은 아니다.
검색 결과가 없는 경우와 호출 자체가 실패한 경우를 따로 재현한다. 대체 도구로 전환한 뒤에도 질문에 필요한 정보가 있는지 확인하고, 도구 이름과 전환 이유를 남겨 답변의 생성 경로를 추적한다.