LlamaIndex 에이전트
LlamaIndex 에이전트 (Agents)
LLM으로 뭔가를 "스스로 결정해서" 처리하게 만들고 싶을 때, 그 중심에 서는 개념이 바로 에이전트예요. LlamaIndex에서는 에이전트를 LLM과 메모리, 그리고 도구(tool)를 이용해 외부 사용자의 입력을 처리하는 시스템으로 정의해요. 여기서 "에이전틱(agentic)"이라는 말과 구분해서 보면 더 명확한데요, 에이전틱은 "처리 과정에 LLM의 의사결정이 들어간 시스템"이라는 더 넓은 상위 개념이고, 에이전트는 그중에서도 LLM·메모리·도구를 갖춘 구체적인 시스템을 가리켜요.
에이전트를 만드는 건 몇 줄이면 끝나요.
import asyncio
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.llms.openai import OpenAI
# Define a simple calculator tool
def multiply(a: float, b: float) -> float:
"""Useful for multiplying two numbers."""
return a * b
# Create an agent workflow with our calculator tool
agent = FunctionAgent(
tools=[multiply],
llm=OpenAI(model="gpt-4o-mini"),
system_prompt="You are a helpful assistant that can multiply two numbers.",
)
async def main():
# Run the agent
response = await agent.run("What is 1234 * 4567?")
print(str(response))
# Run the agent
if __name__ == "__main__":
asyncio.run(main())
출처: 공식문서
에이전트가 도는 과정
이 에이전트를 호출하면 특정한 동작 루프가 시작돼요.
- 에이전트가 가장 최신 메시지와 채팅 히스토리를 가져와요.
- 도구 스키마와 채팅 히스토리가 API로 전송돼요.
- 에이전트가 직접 응답을 하거나, 아니면 도구 호출 목록을 반환해요.
- 모든 도구 호출이 실행돼요.
- 도구 호출 결과가 채팅 히스토리에 추가돼요.
- 갱신된 히스토리로 에이전트를 다시 호출해서, 직접 응답하거나 또 다른 호출을 선택하게 해요.
FunctionAgent는 LLM 제공자(provider)의 함수/도구 호출 기능을 이용해 도구를 실행하는 유형의 에이전트예요. 이와 달리 ReActAgent나 CodeActAgent는 도구를 실행할 때 다른 프롬프팅 전략을 사용해요.
💡 팁: 일부 모델은 LLM 출력 스트리밍을 지원하지 않아요. 스트리밍이 기본값으로 켜져 있는데, 오류가 나면
FunctionAgent(..., streaming=False)로 스트리밍을 끌 수 있어요.
도구 (Tools)
도구는 단순한 파이썬 함수로 정의할 수도 있고, FunctionTool이나 QueryEngineTool 같은 클래스로 더 세부적으로 커스터마이즈할 수도 있어요. LlamaIndex는 또 일반적인 API를 위한 도구 묶음을 Tool Specs라는 이름으로 미리 제공해요.
도구 설정 방법은 도구 가이드에서 더 자세히 다뤄요.
메모리 (Memory)
메모리는 에이전트를 만들 때 빠질 수 없는 구성 요소예요. 기본적으로 모든 LlamaIndex 에이전트는 ChatMemoryBuffer를 메모리로 사용해요.
이걸 커스터마이즈하려면 에이전트 밖에서 선언해서 넘겨주면 돼요.
from llama_index.core.memory import ChatMemoryBuffer
memory = ChatMemoryBuffer.from_defaults(token_limit=40000)
response = await agent.run(..., memory=memory)
메모리 설정 방법은 메모리 가이드에서 확인할 수 있어요.
멀티모달 에이전트
이미지와 텍스트 같은 **여러 모달리티(modality)**를 지원하는 LLM도 있어요. 콘텐츠 블록이 있는 채팅 메시지를 이용하면, 이미지를 에이전트에 넘겨 추론하게 할 수 있어요.
예를 들어 이 프레젠테이션의 슬라이드 스크린샷이 있다고 해볼게요. 이 이미지를 에이전트에 넘기면, 에이전트가 이미지를 읽고 그에 맞게 동작하는 걸 확인할 수 있어요.
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.llms import ChatMessage, ImageBlock, TextBlock
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4o-mini", api_key="sk-...")
def add(a: int, b: int) -> int:
"""Useful for adding two numbers together."""
return a + b
workflow = FunctionAgent(
tools=[add],
llm=llm,
)
msg = ChatMessage(
role="user",
blocks=[
TextBlock(text="Follow what the image says."),
ImageBlock(path="./screenshot.png"),
],
)
response = await workflow.run(msg)
print(str(response))
이때 msg의 blocks에 텍스트와 함께 ImageBlock을 넣어 이미지를 전달하는 게 핵심이에요.
멀티 에이전트 시스템
여러 에이전트를 조합해 멀티 에이전트 시스템을 만들 수도 있어요. 이때 각 에이전트는 작업을 조율하기 위해 다른 에이전트에게 제어권을 넘겨줄(hand off) 수 있어요.
from llama_index.core.agent.workflow import AgentWorkflow
multi_agent = AgentWorkflow(agents=[FunctionAgent(...), FunctionAgent(...)])
resp = await agent.run("query")
이것은 멀티 에이전트 시스템을 만드는 한 가지 방법일 뿐이에요. 멀티 에이전트 시스템 문서에서 더 자세히 다뤄요.
직접 만드는 에이전트
FunctionAgent, ReActAgent, CodeActAgent, AgentWorkflow 같은 에이전트 클래스는 많은 세부 사항을 감춰주지만, 때로는 더 저수준의 에이전트를 직접 만들고 싶을 때도 있어요.
LLM 객체를 직접 사용하면 기본적인 에이전트 루프를 빠르게 구현하면서, 도구 호출과 오류 처리를 완전히 제어할 수 있어요.
from llama_index.core.llms import ChatMessage
from llama_index.core.tools import FunctionTool
from llama_index.llms.openai import OpenAI
def select_song(song_name: str) -> str:
"""Useful for selecting a song."""
return f"Song selected: {song_name}"
tools = [FunctionTool.from_defaults(select_song)]
tools_by_name = {t.metadata.name: t for t in [tool]}
# call llm with initial tools + chat history
chat_history = [ChatMessage(role="user", content="Pick a random song for me")]
resp = llm.chat_with_tools([tool], chat_history=chat_history)
# parse tool calls from response
tool_calls = llm.get_tool_calls_from_response(
resp, error_on_no_tool_call=False
)
# loop while there are still more tools to call
while tool_calls:
# add the LLM's response to the chat history
chat_history.append(resp.message)
# call every tool and add its result to chat_history
for tool_call in tool_calls:
tool_name = tool_call.tool_name
tool_kwargs = tool_call.tool_kwargs
print(f"Calling {tool_name} with {tool_kwargs}")
tool_output = tool(**tool_kwargs)
chat_history.append(
ChatMessage(
role="tool",
content=str(tool_output),
# most LLMs like OpenAI need to know the tool call id
additional_kwargs={"tool_call_id": tool_call.tool_id},
)
)
# check if the LLM can write a final response or calls more tools
resp = llm.chat_with_tools([tool], chat_history=chat_history)
tool_calls = llm.get_tool_calls_from_response(
resp, error_on_no_tool_call=False
)
# print the final response
print(resp.message.content)
루프가 끝나면 최종 응답이 resp.message.content에 담겨요. tool_call_id를 additional_kwargs로 넣는 건 OpenAI 같은 LLM이 어떤 도구 호출에 대한 결과인지 알아야 하기 때문이에요.
더 알아보기
- 에이전트 모듈 가이드에서 예제와 모듈의 전체 목록을 확인할 수 있어요.
- 에이전트 가이드에서 에이전트의 개념과 기능을 더 깊이 배울 수 있어요.