LLM Tool Call ๊ณต๋ถ€

Chain ์˜ˆ์‹œ

์ „์ฒด ์ฝ”๋“œ

from datetime import datetime
from langchain_core.prompts import ChatPromptTemplace
from langchain_core.runnables import RunnableConfig, chain
from langchain_openai import ChatOpenAI
from langchain_community.tools import TavilySearchResults
 
import pprint
 
# ์˜ค๋Š˜ ๋‚ ์งœ ์„ค์ •
today = datetime.today().strftime("%Y-%m-%d")
 
# ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ
prompt = ChatPromptTemplace([
	("system", f"You are a helpful AI assistant. Today's date is {today}"),
	("human", "{user_input}"),
	("placeholder", "{messages}"),
])
 
# ChatOpenAI ๋ชจ๋ธ ์ดˆ๊ธฐํ™”
llm = ChatOpenAI(model="gpt-4o-mini")
 
# Tavily Search ๋„๊ตฌ ์ดˆ๊ธฐํ™” (์ตœ๋Œ€ 2๊ฐœ์˜ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜)
web_search = TavilySearchResults(max_results=2)
 
# LLM์— ๋„๊ตฌ๋ฅผ ๋ฐ”์ธ๋”ฉ
llm_with_tools = llm.bind_tools(tools=[web_search])
 
# LLM ์ฒด์ธ ์ƒ์„ฑ
llm_chain = prompt | llm_with_tools
 
# ๋„๊ตฌ ์‹คํ–‰ ์ฒด์ธ ์ •์˜
@chain
def web_search_chain(user_input: str, config: RunnableConfig):
	input_ = {"user_input": user_input}
	ai_msg = llm_chain.invoke(input_, config=config)
	print("ai_msg: \n", ai_msg)
	print("-" * 100)
	tool_msgs = web_search.batch(ai_msg.tool_calls, config=config)
	print("tool_msgs: \n", tool_msgs)
	print("-" * 100)
	return llm_chain.invoke({**input_, "messages": [ai_msg, *tool_msgs]}, config=config)
 
# ์ฒด์ธ ์‹คํ–‰
response = web_search_chain.invoke("์˜ค๋Š˜ ๋ชจ์—ฃ์ƒน๋™ ์ƒดํŽ˜์ธ์˜ ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?")
 
# ์‘๋‹ต ์ถœ๋ ฅ
pprint(response.content)

โœ… 1. ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ ์ •์˜

prompt = ChatPromptTemplate([
    ("system", f"You are a helpful AI assistant. Today's date is {today}"),
    ("human", "{user_input}"),
    ("placeholder", "{messages}"),
])
  • LangChain์˜ ChatPromptTemplate์„ ์‚ฌ์šฉํ•ด multi-message ํ”„๋กฌํ”„ํŠธ๋ฅผ ์ •์˜.
  • "system" ์—ญํ• ๋กœ ์˜ค๋Š˜ ๋‚ ์งœ ํฌํ•จํ•œ ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณต.
  • "human" ์—ญํ• ๋กœ ์œ ์ € ์ž…๋ ฅ์„ ๋ฐ›์Œ.
  • "placeholder"๋Š” LLM์˜ ์‘๋‹ต์ด๋‚˜ ๋„๊ตฌ ํ˜ธ์ถœ ๊ฒฐ๊ณผ๋ฅผ ์ถ”๊ฐ€๋กœ ๋ผ์›Œ๋„ฃ๊ธฐ ์œ„ํ•ด ๋น„์›Œ๋‘ .

โœ… 2. LLM ๋ฐ ๋„๊ตฌ ๋ฐ”์ธ๋”ฉ

llm = ChatOpenAI(model="gpt-4o-mini")
llm_with_tools = llm.bind_tools(tools=[web_search])
  • OpenAI์˜ gpt-4o-mini ๋ชจ๋ธ์„ ์‚ฌ์šฉ.
  • ์—ฌ๊ธฐ์— web_search ๋„๊ตฌ๋ฅผ ๋ฐ”์ธ๋”ฉํ•ด์„œ ๋ชจ๋ธ์ด ๋„์ค‘์— "tool_call"์„ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •.

โœ… 3. LLM ์ฒด์ธ ๊ตฌ์„ฑ

llm_chain = prompt | llm_with_tools
  • ์•ž์„œ ๋งŒ๋“  ํ”„๋กฌํ”„ํŠธ์™€ ๋„๊ตฌ๊ฐ€ ๋ฐ”์ธ๋”ฉ๋œ LLM์„ ํŒŒ์ดํ”„๋ผ์ธ ํ˜•ํƒœ๋กœ ์—ฐ๊ฒฐํ•ด์„œ ํ•˜๋‚˜์˜ ์ฒด์ธ์œผ๋กœ ๋งŒ๋“ฆ.
  • ์ด ์ฒด์ธ์„ ์‹คํ–‰ํ•˜๋ฉด, ํ”„๋กฌํ”„ํŠธ โ†’ ๋ชจ๋ธ โ†’ ๋„๊ตฌ ํ˜ธ์ถœ ๊ฐ€๋Šฅ ์ƒํƒœ๊นŒ์ง€ ์ด์–ด์ง.

โœ… 4. ๋„๊ตฌ ์‹คํ–‰์„ ํฌํ•จํ•œ ์ „์ฒด ์ฒด์ธ

@chain
def web_search_chain(user_input: str, config: RunnableConfig):
    input_ = {"user_input": user_input}
    ai_msg = llm_chain.invoke(input_, config=config)
    tool_msgs = web_search.batch(ai_msg.tool_calls, config=config)
    return llm_chain.invoke({**input_, "messages": [ai_msg, *tool_msgs]}, config=config)
  • ์œ ์ €์˜ ์งˆ๋ฌธ์„ ๋ฐ›์•„ ์ฒด์ธ์„ ์‹คํ–‰ํ•˜๋Š” ๋ž˜ํผ ํ•จ์ˆ˜.
  • 1๋‹จ๊ณ„: LLM์ด ๋จผ์ € ์งˆ๋ฌธ์— ๋Œ€ํ•ด tool ํ˜ธ์ถœ์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜๋ฉด, ai_msg.tool_calls๋ฅผ ํฌํ•จํ•œ ์‘๋‹ต์„ ์ƒ์„ฑ.
  • 2๋‹จ๊ณ„: web_search.batch(...)๋ฅผ ํ†ตํ•ด tool call๋“ค์„ ์‹ค์ œ๋กœ ์‹คํ–‰.
  • 3๋‹จ๊ณ„: ์‹คํ–‰๋œ tool ๊ฒฐ๊ณผ(tool_msgs)๋ฅผ ๋‹ค์‹œ ํ”„๋กฌํ”„ํŠธ์— ๋„ฃ์–ด LLM์—๊ฒŒ ์ตœ์ข… ์‘๋‹ต์„ ์ƒ์„ฑํ•˜๋„๋ก ํ•จ.

โœ… 5. ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ ์ถœ๋ ฅ

response = web_search_chain.invoke("์˜ค๋Š˜ ๋ชจ์—ฃ์ƒน๋™ ์ƒดํŽ˜์ธ์˜ ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?")
pprint(response.content)
  • "์˜ค๋Š˜ ๋ชจ์—ฃ์ƒน๋™ ์ƒดํŽ˜์ธ์˜ ๊ฐ€๊ฒฉ"์ด๋ผ๋Š” ์งˆ๋ฌธ์„ ์ž…๋ ฅ์œผ๋กœ ์ฃผ๊ณ  ์ฒด์ธ ์ „์ฒด ์‹คํ–‰.
  • LLM์ด ๋„๊ตฌ ์‚ฌ์šฉ์„ ์ œ์•ˆํ•˜๊ณ  โ†’ ๋„๊ตฌ ์‹คํ–‰ โ†’ ๋„๊ตฌ ๊ฒฐ๊ณผ ํฌํ•จํ•˜์—ฌ ์ตœ์ข… ์‘๋‹ต ์ƒ์„ฑ โ†’ ์ถœ๋ ฅ.

๐Ÿ’ก ์š”์•ฝ

์ด ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ˆœ์„œ๋กœ ๋™์ž‘ํ•จ:

  1. ์œ ์ € ์งˆ๋ฌธ ์ž…๋ ฅ
  2. LLM์ด ํŒ๋‹จ โ†’ ๋„๊ตฌ ํ˜ธ์ถœ ํ•„์š”ํ•จ์„ ๊ฐ์ง€ (LLM 1์ฐจ ํ˜ธ์ถœ)
  3. ๋„๊ตฌ ํ˜ธ์ถœ ์ œ์•ˆ (tool_calls)
  4. ์ œ์•ˆ๋œ ๋„๊ตฌ ์‹ค์ œ๋กœ ์‹คํ–‰ (web_search.batch) (๋งŒ์•ฝ, ๋„๊ตฌ ์ž์ฒด๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋˜ LLM์„ ํ˜ธ์ถœํ•œ๋‹ค๋ฉด, ์—ฌ๊ธฐ์„œ๋„ ์กฐ๊ฑด๋ถ€ ํ˜ธ์ถœ)
  5. ๋„๊ตฌ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ ๋‹ค์‹œ ํ”„๋กฌํ”„ํŠธ์— ๋„ฃ์–ด์„œ ์ตœ์ข… ์‘๋‹ต ์ƒ์„ฑ (LLM 2์ฐจ ํ˜ธ์ถœ)
  6. ์‘๋‹ต ์ถœ๋ ฅ

@tool vs. Runnable.as_tool()

1. @tool decorator ๋ฐฉ์‹

  • โ€œ๊ฐ„ํŽธํ•˜๊ณ  ๋น ๋ฅด๊ฒŒโ€ ์‚ฌ์šฉ์ž ์ •์˜ Tool์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๊ณ ์ „์  ๋ฐฉ๋ฒ•.
  • ํ•จ์ˆ˜์— ์ง์ ‘ @tool์„ ๋‹ฌ์•„์„œ, LangChain Tool ๊ฐ์ฒด๋กœ ์ž๋™ ๋ณ€ํ™˜.
  • ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” StructuredTool ๋˜๋Š” Tool ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŽธ์˜ ๊ธฐ๋Šฅ.
  • Signature(ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜) ๋ฅผ ์ž๋™ ํŒŒ์•…ํ•ด์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ์Šคํ‚ค๋งˆ๋ฅผ ๋งŒ๋“ค์–ด์คŒ.
from langchain.tools import tool
 
@tool
def add_numbers(a: int, b: int) -> int:
	"""๋‘ ์ˆ˜๋ฅผ ๋”ํ•˜๋Š” ๋„๊ตฌ."""
	return a + b

์ด๊ฑธ ์“ฐ๋ฉด ๋ฐ”๋กœ Tool ๊ฐ์ฒด์ฒ˜๋Ÿผ ์“ธ ์ˆ˜ ์žˆ๊ณ , ๋“ฑ๋ก๋„ ๋ฐ”๋กœ ๊ฐ€๋Šฅ.

2. Runnable์„ .as_tool()๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ์‹

  • โ€œํ›จ์”ฌ ๋” ์œ ์—ฐํ•˜๊ณ  ๋ณตํ•ฉ์ ์ธโ€ ๋™์ž‘์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•.
  • Runnable์€ LangChain v0.1 ์ดํ›„ ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜๋กœ streaming, batch, composability ๊ฐ™์€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์ง„ ํ”„๋ ˆ์ž„์›Œํฌ.
  • Runnable ๊ฐ์ฒด๋Š” ์›๋ž˜ chain, graph, model call, custom logic ์ „๋ถ€ ๊ฐ์Œ€ ์ˆ˜ ์žˆ์Œ.
  • .as_tool()์„ ํ˜ธ์ถœํ•˜๋ฉด ์ด ๋ณต์žกํ•œ Runnable ๊ฐ์ฒด๋ฅผ Tool ์ธํ„ฐํŽ˜์ด์Šค์— ๋งž๊ฒŒ โ€œํฌ์žฅโ€ ํ•˜๋Š” ๊ฒƒ.
from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, Field
 
def add_fn(inputs: dict) -> int:
	"""๋‘ ์ˆ˜๋ฅผ ๋”ํ•ด์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋„๊ตฌ"""
	return inputs["a"] + inputs["b"]
 
class add_fn_Schema(BaseModel):
	"""Input schema for add_fn."""
	a: int = Field(..., description="Number 1 for addition")
	b: int = Field(..., description="Number 2 for addition")
 
runnable = RunnableLambda(add_fn)
tool = runnable.as_tool(
	name="add_numbers", 
	description="๋‘ ์ˆ˜๋ฅผ ๋”ํ•˜๋Š” ๋„๊ตฌ",
	args_schema=add_fn_Schema,
)

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Runnable์„ ๊ฐ€์ง„ ์ƒํƒœ์—์„œ๋„ Tool์ฒ˜๋Ÿผ ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€ํ™˜.

Tip

args_schema ์ธ์ž์— class๋กœ ์ •์˜ํ•œ add_fn_Schema ๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋Š”๋ฐ, ์ด๋Š” ํ•„์ˆ˜๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์ด๊ฒƒ๋“ค ๋˜ํ•œ prompt์— ์ž…๋ ฅ๋˜๋Š” ์ •๋ณด์ด๋ฏ€๋กœ, ์ข‹์€ ์„ฑ๋Šฅ์„ ์œ„ํ•ด์„œ๋ผ๋ฉด ์ •์˜ํ•ด์„œ ๋„ฃ์–ด์ฃผ์ž.

์š”์•ฝ ๋น„๊ต

ํ•ญ๋ชฉ@tool ๋ฐฉ์‹Runnable.as_tool() ๋ฐฉ์‹
์ฃผ ์šฉ๋„๋‹จ์ˆœ ํ•จ์ˆ˜ Toolํ™”๋ณต์žกํ•œ Runnable โ†’ Tool ๋ณ€ํ™˜
์œ ์—ฐ์„ฑ์ œํ•œ์  (ํ•จ์ˆ˜ ๊ธฐ๋ฐ˜)๋งค์šฐ ๋†’์Œ (๋ชจ๋“  Runnable ๊ฐ€๋Šฅ)
๋‚ด๋ถ€ ์ฒ˜๋ฆฌStructuredTool ์ƒ์„ฑRunnableTool ์ƒ์„ฑ
์‚ฌ์šฉ ๋Œ€์ƒ์ˆœ์ˆ˜ Python ํ•จ์ˆ˜Runnable ๊ฐ์ฒด ์ „์ฒด
ํŠน์ง•๊ฐ„ํŽธํ•˜๊ณ  ๋น ๋ฆ„์œ ์—ฐํ•˜๊ณ  ๊ฐ•๋ ฅ

Tip

  • LangChain ์ตœ์‹  ๋ฒ„์ „์—์„œ๋Š” ๊ฐ€๋Šฅํ•˜๋ฉด Runnable ๊ธฐ๋ฐ˜์„ ๊ถŒ์žฅํ•˜๋Š” ํ๋ฆ„.
  • ์ด์œ ๋Š” LangGraph ๊ฐ™์€ ๋ฉ€ํ‹ฐ์Šคํ… ํ”Œ๋กœ์šฐ๋‚˜ ์ŠคํŠธ๋ฆฌ๋ฐ ์ฒ˜๋ฆฌ๋ฅผ ์ƒ๊ฐํ•˜๋ฉด, ๋‹จ์ˆœ @tool ๋กœ๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ์Œ.
  • ํŠนํžˆ ๋™์ ์œผ๋กœ chain์„ ๋งŒ๋“ค๊ฑฐ๋‚˜, runtime์— ๊ตฌ์กฐ๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฒƒ๊นŒ์ง€ ์ƒ๊ฐํ•œ๋‹ค๋ฉด ๋ฌด์กฐ๊ฑด Runnable-as-tool์ด ๋” ์ข‹์Œ!