11장. 프레임워크 기반 에이전트 구성
직접 만든 루프와 create_agent — 무엇이 무엇이 되는가
한 줄 요약
9~10장에서 만든 부품은 거의 전부 LangChain에 짝이 있습니다. 사라지는 것은 루프 코드뿐이고, 도구와 시스템 프롬프트는 모양만 바꿔 그대로 가져갑니다. 그리고 권한 경계와 비용 상한은 프레임워크가 대신해 주지 않습니다.
1. 부품 대응표
| 9~10장 (직접 구현) | 11장 (LangChain) |
|---|---|
get_client() + MODEL |
ChatGoogleGenerativeAI(model=MODEL, ...) |
| 도구 함수 (독스트링 포함) | tool(함수)로 감싼 도구 |
SYSTEM 문자열 → system_instruction= |
SYSTEM 문자열 → system_prompt= |
react_loop() 함수 |
create_agent(...)가 돌려준 agent |
contents 목록 |
messages 목록 |
response.function_calls |
AIMessage의 tool_calls |
직접 넣은 print 로그 |
agent.stream(...)으로 단계마다 받기 |
2. 도구 — 함수는 그대로, 한 번 감싼다
8장의 make_tools(session)이 돌려준 함수들을 tool()로 감쌉니다.
session = CustomerSession("C003")
raw = make_tools(session)
get_my_orders = tool(raw["get_my_orders"])
get_order_status = tool(raw["get_order_status"])
tool()은 함수의 이름, 타입 힌트, 독스트링을 읽어 도구 선언을 만듭니다. 6장에서 google-genai의 자동 함수 호출이 하던 일과 같은 원리입니다. 8장에서 독스트링을 공들여 써 둔 것이 여기서도 그대로 쓰입니다.
감싸도 안전장치는 남습니다. raw의 함수들은 만들어질 때 이미 session을 품고 있습니다. get_order_status는 여전히 C003 본인의 주문만 보여 줍니다. 프레임워크가 바뀌어도 권한 경계는 도구 안에 있습니다.
3. 루프 — 함수 하나가 호출 한 번으로
9장의 react_loop는 서른 줄쯤이었습니다. 호출하고, function_calls를 확인하고, 도구를 실행하고, 결과를 contents에 붙이고, 다시 호출했습니다.
agent = create_agent(
model=llm,
tools=TOOLS,
system_prompt=SYSTEM,
)
create_agent는 이 세 가지를 받아 같은 일을 하는 에이전트를 만들어 줍니다. 모델을 부르고, 도구 요청이 있으면 실행해 결과를 돌려주고, 도구 요청이 없으면 끝냅니다.
실행은 이렇게 합니다.
result = agent.invoke(
{"messages": [{"role": "user", "content": "제 최근 주문 상태 알려주세요."}]})
| 인자 | 9장에서는 |
|---|---|
model=llm |
client.models.generate_content(model=MODEL, ...) |
tools=TOOLS |
config의 tools=list(tools.values()) |
system_prompt=SYSTEM |
config의 system_instruction=SYSTEM |
4. 메시지 — contents의 새 이름
9장의 contents에는 역할이 붙은 Content가 차례로 쌓였습니다. LangChain에서는 메시지(message) 가 쌓입니다. 종류는 클래스 이름으로 구분합니다.
| LangChain 메시지 | 담는 것 | 9장의 contents에서는 |
|---|---|---|
HumanMessage |
고객의 질문 | role="user"인 질문 |
AIMessage |
모델의 응답. 도구 요청이면 tool_calls에 담긴다 |
response.candidates[0].content |
ToolMessage |
도구 실행 결과 | 함수 결과를 담아 role="user"로 넣은 Content |
구조는 같습니다. 질문 → 모델의 도구 요청 → 도구 결과 → 모델의 도구 요청 → 도구 결과 → 최종 답변.
한 가지 편해진 점이 있습니다. 9장에서는 도구 결과를 어떤 역할로 돌려줄지를 우리가 정해 적었습니다. LangChain에서는 ToolMessage를 Gemini가 받는 모양으로 바꾸는 일을 어댑터가 합니다.
5. 프레임워크가 해 주지 않는 것
여기가 이번 장에서 가장 중요한 부분입니다.
| 일 | 누가 하나 |
|---|---|
| 모델 호출과 도구 실행을 반복한다 | 프레임워크 |
| 메시지를 차례로 쌓는다 | 프레임워크 |
| 어떤 도구를 부를지 고른다 | 모델 |
| 도구가 무엇을 할 수 있고 없는지 (8장) | 우리 |
| 시스템 프롬프트 (3장, 10장) | 우리 |
| 한 문의에 얼마까지 쓸지 (10장) | 우리 |
| 한도에 닿았을 때 고객에게 무엇을 할지 (10장) | 우리 |
create_agent(model, tools, system_prompt) 세 줄에는 토큰 상한도, 입력 가드도 없습니다. 10장에서 만든 가드레일은 옮겨 오지 않으면 없는 것입니다.
LangChain에는 이런 장치를 끼워 넣는 자리가 있습니다. 미들웨어(middleware) 라고 하며, 에이전트의 각 단계 앞뒤에 끼어드는 부품입니다. 예를 들어 ModelCallLimitMiddleware는 10장의 max_steps처럼 모델 호출 횟수를 제한합니다.
from langchain.agents.middleware import ModelCallLimitMiddleware
agent = create_agent(
model=llm, tools=TOOLS, system_prompt=SYSTEM,
middleware=[ModelCallLimitMiddleware(run_limit=6, exit_behavior="end")],
)
다만 끼워 넣을지, 숫자를 얼마로 할지, 한도에 닿았을 때 무엇을 할지는 여전히 우리가 정합니다. 10장의 입력 가드와 금지 조항을 create_agent 위로 옮겨 오는 일은 실습문제 2에서 직접 해 봅니다.
프레임워크는 루프를 대신 돌려 줍니다. 무엇을 허용하고 어디서 멈출지는 대신 정해 주지 않습니다.
핵심 정리
- 9~10장의 부품은 거의 전부 LangChain에 짝이 있습니다.
- 도구는
tool()로 한 번 감싸면 됩니다. 독스트링과 세션에 묶인 권한 경계는 그대로 남습니다. - 루프는
create_agent(model, tools, system_prompt)호출 한 번이 됩니다. contents는messages가 되고, 종류는HumanMessage,AIMessage,ToolMessage입니다.- 권한 경계, 비용 상한, 한도에 닿았을 때의 대응은 프레임워크로 옮겨도 우리 일입니다.