LangChain, LangGraph, Pydantic을 통해 함수 호출을 효과적으로 사용하는 방법을 알아봐요.
소개
이번 글에서는 LangChain, LangGraph, 그리고 Pydantic을 사용해서 GenAI 애플리케이션에서 함수 호출을 얼마나 효과적으로 활용할 수 있는지 알려드릴게요. 함수 호출은 LLM이 코드에서 호출된 함수 이름과 인수를 준수하는 구조화된 출력을 생성할 수 있게 해주는 아주 강력한 방법이에요. 덕분에 GenAI 애플리케이션은 단순한 챗봇을 넘어 고급 데이터 검색이나 상호작용 같은 기능까지 확장할 수 있죠.
함수 호출이 무엇인지, 그리고 OpenAI Chat Completions API와 LangChain을 사용해서 이걸 어떻게 구현하는지 알아볼 거예요. 다른 LLM 제공업체들은 함수 호출 방식이 조금씩 다를 수 있지만, 기본적인 개념은 비슷할 거예요.
여기 예제 애플리케이션은 레시피와 작성자 데이터베이스에 접근할 수 있는 요리 보조 프로그램이에요. LangGraph 워크플로 아키텍처, 다양한 Tool, 그리고 실행 방법에 대해서 자세히 설명할게요. 마지막에는 몇 가지 예시 질문을 통해 이 애플리케이션이 여러 Tool을 사용해서 문제를 해결하는 과정을 보여드릴 거예요.
이 예제 애플리케이션은 Neo4j를 메인 데이터베이스로 사용하고 있어요. 여기서 설명하는 Tool은 Cypher 쿼리 언어를 통해 Neo4j 데이터베이스에서 데이터를 검색하지만, 이 개념은 어떤 데이터베이스를 사용하는 LLM 애플리케이션에도 적용할 수 있다는 점 잊지 마세요!
코드는 에 공개되어 있고, 예시 노트북도 함께 제공하고 있어요.
용어 정의
GenAI는 정말 빠르게 발전하고 있어서 아직 정의가 명확하지 않은 부분들이 좀 있어요. 그래서 여기서 사용하는 용어들을 아래에 정의해두었어요.
- Agent: LLM을 사용해서 다음에 어떤 단계를 수행할지 결정하는 시스템
- Workflow: LLM과 Tool이 미리 정의된 경로를 따라서 움직이는 시스템
- Agent Workflow: 미리 정의된 경로와 Agent가 섞여 있는 시스템
- GraphRAG: Graph Database에서 그래프 순회를 통해 정보를 검색하는 방식의 RAG
- Retrieval-Augmented Generation (RAG): 외부 데이터 소스에서 데이터를 가져와서 LLM이 응답을 생성할 때 참고할 수 있도록 컨텍스트로 제공하는 프로세스
- Tool: LLM이 인식하고 호출할 수 있는 기능 (Tool과 기능은 같은 의미로 사용돼요)
- Tool Calling: 함수 호출과 똑같아요. LLM에게 함수 이름과 인수가 포함된 사전 목록 형태의 구조화된 출력을 요청하는 과정을 말해요.
기술 스택
여기서 다루는 개념이 이 기술 스택에만 한정되는 건 아니에요. 하지만 모든 LLM이 함수 호출을 지원하는 건 아니라는 점 기억해주세요.
- LangChain: LLM 인터페이스
- LangGraph: LLM 오케스트레이션 프레임워크
- Neo4j: Graph Database
- OpenAI: LLM 제공업체
- Python
- Pydantic: Python에서 고급 타입 검사와 유효성 검사를 도와주는 라이브러리
Tool 정의
LangChain은 다양한 Tool 정의 방법을 지원하고 있어요. 그중에서도 Pydantic 클래스를 활용하는 방법이 강력하죠. Pydantic을 사용하면 Tool에 대한 설명과 인수를 명시적으로 정의하면서 유효성 검사까지 할 수 있거든요. 게다가 이렇게 정의된 클래스는 반환된 Tool 호출 정보를 처리하고 유효성을 검사하는 데도 활용할 수 있어요. 데모 애플리케이션에서 사용할 수 있는 몇 가지 Tool을 살펴볼까요?
class text2cypher(BaseModel):
"""The default data retrieval tool. Use an LLM to generate a new Cypher query that satisfies the task."""
task: str = Field(..., description="The task the Cypher query must answer.")
class get_most_common_ingredients_an_author_uses(BaseModel):
"""Retrieve the most common ingredients a specific author uses in their recipes."""
author: str = Field(..., description="The full author name to search for.")
@field_validator("author")
def validate_author(cls, v: str) -> str:
return v.lower()
에이전트 워크플로 아키텍처
랭그래프에서는 에이전트 워크플로를 이렇게 조정하고 있어요. LangGraph 워크플로의 각 Node는 프로세스를 포함하는 구성 요소이고, 각 Edge는 Node들 사이의 정보 흐름을 나타내죠. 이 예시에서는 몇 가지 알아둬야 할 Node가 있어요.
- Guardrails
- Planner
- Text2Cypher
- Summarize
이번 글에서는 주로 함수 호출에 집중하겠지만, 이 LangGraph 워크플로에는 몇 가지 다른 기능도 알아두면 좋을 것 같아요.
Planner와 Summarize 사이의 Node는 Map Reduce 방식으로 실행돼요. 발견된 각 Task가 Tool Selection Node에 매핑되고, Tool 실행 결과가 목록으로 상태 변수(여기서는 Cypher)로 축소되는 방식이죠. 이렇게 하면 각 Task를 병렬로 처리할 수 있어서 전체 응답 시간을 줄일 수 있어요.
Text2Cypher Node는 SubGraph Node로 처리돼요. 덕분에 Text2Cypher 프로세스는 자체 상태를 쉽게 유지하고 다른 에이전트 워크플로우에 모듈식으로 포함될 수 있답니다.
다중 Tool 에이전트 워크플로 구성 코드
Guardrails 및 Planner Node
Guardrails Node는 들어오는 질문이 애플리케이션 범위 안에 있는지 판단해요. 그렇지 않다면 기본 메시지가 제공되고 워크플로는 최종 답변 생성 단계로 넘어가죠.
Planner Node는 Guardrails Node로부터 검증된 입력 질문을 받아서, 만족스러운 답변을 제공하는 데 필요한 Task를 식별해요. 이렇게 식별된 Task는 Tool Selection 및 실행 단계를 거쳐 병렬로 처리될 수 있어요.
Guardrails 코드
Planner 코드
Tool Selection Node
Tool Selection Node는 Tool을 개별 Task에 할당하는 역할을 해요. Tool이 Text2Cypher라면 필요한 상태는 Text2Cypher SubGraph로 라우팅되죠. Tool이 미리 정의된 Cypher 쿼리라면 필요한 상태는 대신 미리 정의된 Cypher Executor Node로 라우팅되고요. Tool이 선택되지 않았을 수도 있는데, 이 경우에는 Node가 기본적으로 Text2Cypher로 설정되거나 오류 처리 Node로 라우팅될 수 있어요. 오류 처리 Node는 정상적으로 실패하는 역할을 하죠. Cypher는 Neo4j에서 사용하는 쿼리 언어인데, 더 자세히 알고 싶다면 한번 살펴보세요.
Tool Selection 코드
Tool Node
이 예시에는 다양한 Tool이 있지만, Tool을 호출하는 방법은 딱 두 가지뿐이에요. 하나는 기존 Tool과 일치하지 않는 질문에 대해 새로운 Cypher 문을 생성하는 Text2Cypher예요. 이 Tool은 아래에서 자세히 설명할 Text2Cypher SubGraph를 통해 실행되죠. 다른 하나는 미리 작성된 매개변수화된 Cypher 쿼리로, 입력 Task에서 추출된 매개변수를 가져와서 사전 정의된 Cypher Executor Node를 통해 실행하는 방식이에요.
사전 정의된 Cypher Executor
사전 정의된 Cypher Executor는 LangGraph 에이전트 워크플로우의 Node에요. LLM에서 찾은 매개변수를 사용해서 미리 정의된 Cypher 쿼리를 실행하는 역할을 담당하죠. 각 쿼리는 LLM이 사용할 수 있는 고유한 Tool을 나타내요. 이 Node는 Tool 실행을 담당하는 거예요.
- get_allergen_free_recipes
- get_most_common_ingredients_an_author_uses
- get_recipes_for_diet_restrictions
- get_easy_recipes
- get_mid_difficulty_recipes
- get_difficult_recipes
예를 들어, LLM이 get_allergen_free_recipes를 도구로 선택하면 작업 텍스트에서 찾은 알레르기 유발 물질 목록을 추출해요.
class get_allergen_free_recipes(BaseModel):
"""Retrieve a list of all recipes that do not contain the provided allergens."""
allergens: List[str] = Field(
..., description="A list of allergens that should be avoided in recipes."
)
이 목록은 $allergens 매개변수를 통해 사전 정의된 Cypher 쿼리에 삽입되고 Neo4j 데이터베이스에 대해 실행되어 최종 답변을 생성하기 위한 컨텍스트를 검색합니다.
MATCH (r:Recipe)
WHERE none(i in $allergens WHERE exists(
(r)-[:CONTAINS_INGREDIENT]->(:Ingredient {name: i})))
RETURN r.name AS recipe,
[(r)-[:CONTAINS_INGREDIENT]->(i) | i.name]
AS ingredients
ORDER BY size(ingredients)
LIMIT 20
사전 정의된 Cypher Executor 코드
Text2Cypher
Text2Cypher는 LangGraph 작업흐름의 하위 그래프이지만 도구 선택 단계에서는 도구로 취급돼요. 도구 선택을 담당하는 LLM 에이전트는 Text2Cypher의 세부 사항을 알 필요가 없어요. 단지 자신이 할 수 있는 일과 필요한 입력만 알면 되죠. 하위 그래프 세부정보와 함수 논리를 추상화하면 LLM에서 처리되는 토큰이 크게 줄어듭니다.
Text2Cypher에 대한 자세한 내용은 여기에서 다루지 않지만 자세한 내용은 LangChain 문서를 참고하세요. 이 아키텍처에 영감을 준 것은 Text2CypherRetriever를 사용한 간편한 RAG이고, Neo4j 엔지니어가 작성했어요.
Text2Cypher 하위 그래프 코드
오류 처리 노드
도구 선택 중에 LLM 요청이 도구 반환에 실패하고 기본 도구가 없는 경우 애플리케이션은 오류 처리 노드로 라우팅될 수 있어요. 이 예에서는 실패한 작업에 대해 빈 데이터세트를 추가하기만 하면 애플리케이션이 중단 없이 계속될 수 있죠. 하지만 이로 인해 필요한 컨텍스트가 누락될 수도 있어요.
도구 선택 오류 처리 코드
요약 및 최종 답변 노드
요약 노드는 검색된 데이터를 최종 사용자가 쉽게 소화할 수 있는 형식으로 요약하는 역할을 해요. 모든 데이터가 비동기식으로 검색되면 이 노드에서 데이터가 처리되죠. 그런 다음 이는 실행된 쿼리 및 검색된 데이터와 함께 최종 답변을 형식화하고 반환하는 최종 답변 노드로 전송됩니다.
최종 답변 코드
상황에 맞는 도구
이 섹션에서는 OpenAI Chat Completions API를 사용하여 도구가 메시지와 함께 컨텍스트로 LLM에 제공되는 방식을 설명해요.
일반적인 LLM 요청에는 다음과 같은 몇 가지 메시지 유형이 포함될 수 있어요.
- User→ 사용자 질문과 몇 가지 지침이 포함되어 있어요.
- 시스템(개발자)→ 모델이 일반적으로 어떻게 작동해야 하는지; 이는 사용자 메시지 앞에 제공됩니다.
- 어시스턴트→ 이전 또는 예시 LLM 응답
이러한 메시지는 Python 딕셔너리 목록으로 OpenAI Chat Completions API의 `messages` 인수에 전달돼요. 다음은 OpenAI 문서의 예시입니다.
from openai import OpenAI
client = OpenAI()
completion = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "developer", "content": "You are a helpful assistant."},
{
"role": "user",
"content": "Write a haiku about recursion in programming."
}
]
)
print(completion.choices[0].message)
자세한 내용은 OpenAI의 텍스트 생성 문서를 참고하세요.
함수를 LLM에 컨텍스트로 전달하기 위해 Python 딕셔너리 목록으로 `tools` 인수에 제공됩니다. 다음은 OpenAI 문서의 또 다른 예시입니다.
from openai import OpenAI
client = OpenAI()
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current temperature for a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
}
},
"required": [
"location"
],
"additionalProperties": False
},
"strict": True
}
}]
completion = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What is the weather like in Paris today?"}],
tools=tools # <-- Pass Functions / Tools here
)
print(completion.choices[0].message.tool_calls)
자세한 내용은 OpenAI의 함수 호출 문서를 참고하세요.
LangChain은 LLM 클래스의 `bind_tools()` 메서드를 통해 이에 대한 추상화를 제공하고 있어요. 이는 도구를 Python 딕셔너리로 형식화하고 OpenAI Chat Completions API의 `tools` 인수에 전달하는 작업을 처리하죠. LangChain과 함께 도구 호출의 예를 더 확인해 보세요.
구문 분석 도구 응답
이 예제의 함수는 Pydantic 클래스를 사용하여 정의된다는 점을 기억하세요. 이를 통해 동일한 클래스를 사용하여 반환된 함수 호출을 쉽게 검증하고 기존 오류를 캡처할 수 있어요. 이러한 오류는 수정 루프에서 LLM으로 다시 전달되어 함수 호출을 수정하죠. 또한 모든 문자열을 소문자로 캐스팅하거나 적절한 경우 숫자를 반올림하는 등 결과를 쉽게 사후 처리할 수 있습니다.
LangChain은 이를 처리하는 출력 파서를 제공하는데요, 바로 PydanticToolsParser입니다. 이 파서는 목록의 첫 번째 도구만 반환하는 등의 추가 구성을 허용해요.
tool_selection_chain: Runnable[Dict[str, Any], Any] = (
tool_selection_prompt
| llm.bind_tools(tools=tool_schemas)
| PydanticToolsParser(tools=tool_schemas, first_tool_only=True)
)
Data
이 예제 애플리케이션은 BBC Good Food 레시피에 대한 정보가 포함된 Neo4j Graph Database에 액세스할 수 있어요. 이건 예시 데이터세트로 Neo4j에서 제공하고 있습니다.
Demo
이 에이전트 워크플로우는 사전 정의된 Cypher 도구 또는 Text2Cypher를 사용하여 질문에 답할 수 있어요. 도구 설명이 작업에 필요한 것과 정확히 일치하는 경우에만 사전 정의된 Cypher를 사용하라는 메시지가 표시되죠. 그렇지 않으면 Text2Cypher를 선택해야 해요. 모든 데이터가 검색되면 요약되어 사용자에게 반환됩니다.
메인의 function-calling-medium-article/examples/workflows/multi_tool.ipynb · a-s-g93/function-calling-medium-article
이 LangGraph 에이전트 워크플로는 다음 필드가 포함된 응답을 출력해요. 이 데모에서는 워크플로 내의 중간 상태를 알 필요는 없지만, 이러한 출력 상태는 아래 응답을 분석하는 데 사용될 거예요.
class CypherOutputState(TypedDict):
task: str
statement: str
parameters: Optional[Dict[str, Any]]
errors: List[str]
records: List[Dict[str, Any]]
steps: List[str]
class OutputState(TypedDict):
"""The final output for multi agent workflows."""
answer: str
question: str
steps: List[str]
cyphers: List[CypherOutputState]
Text2Cypher 전용
이 질문은 LLM이 사용할 수 있는 사전 정의된 Cypher 도구와 딱 맞지 않아요. 그래서 이 질문을 해결하려면 Text2Cypher 도구를 선택해야 하죠.
q1 = await agent.ainvoke(
{
"question": "How many authors have written a recipe?"
},
)
LLM은 다음을 반환합니다.
**303 authors have written a recipe.**
워크플로 단계에서는 이 질문에 답하기 위해 Text2Cypher가 사용되었다는 걸 보여줘요.
[
'guardrails',
'planner',
'tool_selection',
'generate_cypher', # <-- Begin Text2Cypher
'validate_cypher',
'execute_cypher',
'text2cypher', # <-- Text2Cypher Complete
'summarize',
'final_answer'
]
사전 정의된 Cypher만
이 질문은 사전 정의된 Cypher 도구 중 하나를 실행해서 답할 수 있어요. LLM은 위에 정의된 Pydantic 클래스 표현을 기반으로 적절한 도구를 선택하죠.
q2 = await agent.ainvoke(
{"question": "What ingredients does Emma Lewis like to use most?"}
)
LLM은 다음을 반환합니다.
- **Olive oil**: 39 recipes
- **Butter**: 37 recipes
- **Garlic clove**: 29 recipes
- **Onion**: 24 recipes
- **Egg**: 21 recipes
- **Lemon**: 21 recipes
- **Vegetable oil**: 18 recipes
- **Plain flour**: 15 recipes
- **Caster sugar**: 13 recipes
- **Thyme**: 11 recipes
워크플로 단계에서는 사전 정의된 Cypher 도구가 사용되었는지 확인해 보세요.
[
'guardrails',
'planner',
'tool_selection',
'execute_predefined_cypher', # <-- Predefined Cypher Tool
'summarize',
'final_answer'
]
Cypher 쿼리는 응답 객체에 반환되니까, 직접 확인할 수도 있어요.
What are the most frequently used ingredients by Emma Lewis?
MATCH (:Author {name: $author})-[:WROTE]->(:Recipe)-[:CONTAINS_INGREDIENT]->(i:Ingredient)
RETURN i.name as name, COUNT(*) as numRecipes
ORDER BY numRecipes DESC
LIMIT 10
Parameters:
{'author': 'emma lewis'}
Text2Cypher 및 사전 정의된 Cypher
이 질문에는 두 가지 독립적인 작업이 포함되어 있어요. 하나는 미리 정의된 Cypher 도구를 사용해서 해결할 수 있지만, 다른 하나는 Text2Cypher가 필요하죠. Planner 노드는 작업을 비동기식으로 라우팅하니까, 애플리케이션은 다음 도구를 병렬로 실행할 수 있어요.
q3 = await agent.ainvoke(
{
"question": "What are some easy recipes I can make? Also can you share how many ingredients you know about?"
},
)
LLM은 다음을 반환합니다.
- **Apricot & Pistachio Frangipane Blondies**
- **Camomile Tea with Honey**
- **Pastry Snakes**
- **Quick Banana Ice Cream Sandwiches**
- **Red Onion with Peanut Butter & Chilli**
I know about 3068 ingredients.
다시 한번 워크플로 단계에서 예상 도구가 사용되었는지 확인해볼까요?
[
'guardrails',
'planner',
'tool_selection',
'execute_predefined_cypher', # <-- Predefined Cypher Executor
'generate_cypher', # <-- # Begin Text2Cypher
'validate_cypher',
'execute_cypher',
'text2cypher', # <-- Text2Cypher Complete
'summarize',
'final_answer'
]
위의 단계는 순차적으로 나타나지만, *planner* 와 *summarize* 단계는 병렬로 실행된다는 점을 기억해주세요.
애플리케이션은 Cypher 쿼리 및 매개변수와 함께 작업을 저장하기도 해요. 이는 추가적인 검증으로 사용될 수 있겠죠?
# The Predefined Cypher Task
What are some easy recipes I can make?
MATCH (r:Recipe {skillLevel: "easy"})
RETURN r.name as name
LIMIT $number_of_recipes
Parameters:
{'number_of_recipes': 5} # This parameter was identified by the LLM
# The Text2Cypher Task
How many ingredients do you know about?
MATCH (i:Ingredient)
RETURN count(i) AS numberOfIngredients
Parameters: # No parameters are needed here
None
요약
함수 호출은 LLM의 기능을 크게 확장할 수 있는 강력한 도구에요. 이 글에서는 Neo4j에서 데이터를 검색하기 위해 두 가지 유형의 도구를 사용하는 방법을 설명했지만, 도구는 광범위한 기능을 다룰 수 있어요. 예를 들어, 다양한 데이터베이스에 대한 액세스를 허용하거나 API 요청을 할 수도 있죠. Pydantic 모델은 이러한 도구를 정의하고 출력을 검증하여 보다 예측 가능한 응답을 가능하게 해준답니다.
이 글은 LangChain과 Pydantic을 사용하여 도구 호출을 처리하는 데 초점을 맞추고 있지만, 이를 달성하는 다양한 방법이 있고 LLM 공급자의 API를 사용하여 간단하게 수행할 수도 있다는 점 참고해주세요.
자원
- GitHub 레포
- 랭체인:Graph Database를 통해 질문 응답 애플리케이션 구축
- 인류:효과적인 에이전트 구축
- 랭체인:하위 그래프를 사용하는 방법
- Neo4j: Text2CypherRetriever를 사용한 간편한 RAG
- 랭체인:채팅 모델을 사용하여 도구를 호출하는 방법
- 랭체인:병렬 실행을 위해 Map-Reduce 분기를 만드는 방법
- 오픈AI:OpenAI Chat Completions API 문서
- 랭체인:랭그래프 문서
- Agents
- Cypher
- Function
- Langchain
에이치시스템즈의 LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.
'Agent AI' 카테고리의 다른 글
| Building an AI Agent with Memory: Microsoft Agent Framework + Neo4j (1) | 2026.04.10 |
|---|---|
| GenAI 스택 완전 해부: Docker 환경에서 Neo4j, LangChain, Ollama의 숨겨진 이야기 (3) | 2026.04.09 |
| MCP Agentic 시스템에서 Graph 검색 성능 평가하기 (0) | 2026.04.09 |
| Google 어시스턴트 eBay 앱: Graph로 구동되는 대화형 커머스 (1) | 2026.04.08 |
| Doctor.ai: Neo4j와 AWS로 탄생한 헬스케어 음성 챗봇 (0) | 2026.04.08 |
