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로 전송돼요.
  • 에이전트가 직접 응답을 하거나, 아니면 도구 호출 목록을 반환해요.
    • 모든 도구 호출이 실행돼요.
    • 도구 호출 결과가 채팅 히스토리에 추가돼요.
    • 갱신된 히스토리로 에이전트를 다시 호출해서, 직접 응답하거나 또 다른 호출을 선택하게 해요.

FunctionAgentLLM 제공자(provider)의 함수/도구 호출 기능을 이용해 도구를 실행하는 유형의 에이전트예요. 이와 달리 ReActAgentCodeActAgent는 도구를 실행할 때 다른 프롬프팅 전략을 사용해요.

💡 팁: 일부 모델은 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))

이때 msgblocks에 텍스트와 함께 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_idadditional_kwargs로 넣는 건 OpenAI 같은 LLM이 어떤 도구 호출에 대한 결과인지 알아야 하기 때문이에요.

더 알아보기