📚 Agentic AI - 기업용 자율 에이전트 개발 4장 · 에이전트 ② 웹검색 Agentic AI란

따라하기 — for 루프로 에이전트 직접 만들기

한 줄 요약

run_agent() 함수를 한 줄씩 뜯어봅니다. 이 30줄이 5장에서 create_agent 한 줄로 바뀝니다. 지금 내부 동작을 확실히 이해해 두면, 앞으로 라이브러리가 무엇을 대신하는지 알고 쓰게 됩니다.


1. 전체 모습

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

def run_agent(question: str, verbose: bool = True) -> str:
    """ReAct 루프를 직접 돌린다: 모델이 도구를 그만 부를 때까지 반복."""
    config = types.GenerateContentConfig(
        tools=list(TOOLS.values()),
        system_instruction=SYSTEM,
        temperature=0,
        automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
    )
    history = [types.Content(role="user", parts=[types.Part(text=question)])]

    for step in range(1, MAX_STEPS + 1):
        resp = client.models.generate_content(
            model=GEMINI_MODEL, contents=history, config=config)

        if not resp.function_calls:
            if verbose:
                print(f"  [{step}단계] 도구 호출 없음 → 최종 답변")
            return resp.text.strip()

        history.append(resp.candidates[0].content)

        for fc in resp.function_calls:
            result = TOOLS[fc.name](**dict(fc.args))
            if verbose:
                print(f"  [{step}단계] {fc.name}({dict(fc.args)})")
                print(f"           └ {result.splitlines()[0][:70]} ...")
            history.append(types.Content(role="user", parts=[
                types.Part.from_function_response(
                    name=fc.name, response={"result": result})]))

    return "(최대 단계에 도달해 중단했습니다)"

핵심은 15줄쯤입니다. 나머지는 화면에 찍는 코드입니다.


2. 한 줄씩

① 대화의 시작점

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

history = [types.Content(role="user", parts=[types.Part(text=question)])]

사용자 질문 하나로 시작합니다. 2장에서 history = [] 로 시작했던 것과 같은 자리입니다.

② 반복 — 상한을 걸고 돈다

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

for step in range(1, MAX_STEPS + 1):

while True: 가 아니라 for + 상한입니다. 여기서 상한(上限)반복 횟수의 최대값MAX_STEPS 번을 넘겨서는 돌지 않는다는 뜻입니다. 이유는 다음 절에서 자세히 다룹니다. 한 문장으로 말하면 — 무한 반복과 요금 폭탄을 막기 위해서입니다.

③ 생각 — 모델에게 판단을 맡긴다

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

resp = client.models.generate_content(
    model=GEMINI_MODEL, contents=history, config=config)

contents=history 를 눈여겨보세요. 매번 지금까지 쌓인 것 전체를 보냅니다. 2장의 chat_multi 와 똑같습니다.

단계가 늘수록 보내는 양이 늘어납니다. 2장에서 본 비용 이야기가 여기서도 그대로입니다.

④ 종료 판정

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

if not resp.function_calls:
    return resp.text.strip()

부를 도구가 없다 = 이제 답할 수 있다. 3장 6절에서 미리 봐 둔 그 판정입니다.

이 한 줄이 루프의 정상 종료입니다. 우리가 "몇 번 돌아라"라고 정하지 않습니다. 모델이 충분하다고 판단할 때까지 돕니다.

⑤ 모델의 결정을 기록

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

history.append(resp.candidates[0].content)

여기가 빠뜨리기 쉬운 곳입니다.

resp.candidates[0].content모델이 방금 만든 발언 전체입니다 — "get_product_info 를 이 값으로 불러 줘"라는 요청서가 담겨 있습니다.

이걸 기록하지 않으면 어떻게 될까요? 다음 호출 때 모델은 자기가 방금 도구를 요청했다는 사실을 모릅니다. 그런데 실행 결과는 들어와 있죠. 앞뒤가 안 맞아 같은 도구를 또 부르거나 혼란스러워합니다.

2장에서 "③(답변 기록)을 빠뜨리면 모델은 자기가 한 말을 모른다" 고 했던 것과 정확히 같은 문제입니다. 양쪽 다 기록해야 합니다.

⑥ 행동 — 실행은 우리가

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

for fc in resp.function_calls:
    result = TOOLS[fc.name](**dict(fc.args))

3장 2절의 그 경계선입니다. 모델은 요청서만, 실행은 우리 코드.

**dict(fc.args){'query': '스마트워치 시장 가격대'}search_web(query='스마트워치 시장 가격대') 로 펼쳐집니다.

⑦ 관찰을 기록

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

history.append(types.Content(role="user", parts=[
    types.Part.from_function_response(
        name=fc.name, response={"result": result})]))

실행 결과를 대화에 넣습니다. 이게 없으면 모델은 도구를 부른 것까지만 알고 결과는 모릅니다.

name=fc.name 으로 어느 도구의 결과인지 밝혀 줍니다. 한 단계에서 도구를 여러 개 불렀다면 이게 있어야 짝이 맞습니다.

⑧ 비정상 종료

아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.

return "(최대 단계에 도달해 중단했습니다)"

for 문을 다 돌았는데도 모델이 계속 도구를 부르고 있으면 여기로 옵니다. 다음 절의 주제입니다.


3. 흐름 정리

history = [질문]1단계모델 호출 → 도구 요청 있음history += 모델의 결정도구 실행history += 실행 결과2단계모델 호출 → 도구 요청 있음history += 모델의 결정도구 실행history += 실행 결과3단계모델 호출 → 도구 요청 없음답변 반환 ✓도구를 호출할 때마다 '어떤 도구를 호출했는지'와 '결과가 무엇인지'가 history에 쌓입니다. 이 기록을 바탕으로 다음 행동을 결정합니다.
단계마다 모델의 결정과 도구 실행 결과가 history 에 쌓인다

history 가 계속 자라는 것이 이 구조의 전부입니다.


4. 5장에서 이 30줄이 이렇게 됩니다

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

from langchain.agents import create_agent

llm = get_chat(temperature=0)          # common.py 가 만들어 주는 모델
agent = create_agent(llm, tools=[run_pandas], system_prompt=SYSTEM)

한 줄입니다. 그리고 history 관리, 종료 판정, 도구 실행, 결과 기록을 전부 라이브러리가 합니다.

편합니다. 그런데 감춰진 것을 알고 쓰는 것과 모르고 쓰는 것은 다릅니다.

여기서 손으로 한 것 5장에서 라이브러리가
history 만들고 append 자동
if not resp.function_calls 자동
TOOLS[fc.name](...) 실행 자동
Part.from_function_response 자동
MAX_STEPS 상한 설정으로 지정
도구를 무엇으로 구성할지 여전히 사람의 일
각 도구의 독스트링 여전히 사람의 일

아래 셋은 라이브러리가 대신해 주지 않습니다. 그게 이 과정에서 계속 강조하는 "사람이 판단하는 자리"입니다.


스스로 해 보기

  1. ⑤번 줄(history.append(resp.candidates[0].content))을 지우고 실행해 보세요. 어떻게 되나요? 몇 단계까지 가나요? 왜 그런지 설명해 보세요.
  2. ⑦번 줄(관찰 기록)을 지우고 실행해 보세요. 이번엔 어떻게 되나요?
  3. history 의 길이를 매 단계 찍어 보세요. 단계당 몇 개씩 늘어나나요?
    python print(f" [{step}단계] history 길이: {len(history)}")
  4. MAX_STEPS1 로 줄이고 실행해 보세요. 무엇이 출력되나요?

1번과 2번은 일부러 고장 내 보는 연습입니다. 정상 동작만 보면 각 줄이 왜 있는지 체감되지 않습니다. 지웠을 때 무엇이 무너지는지를 봐야 그 줄의 역할이 이해됩니다.


핵심 정리

  • ReAct 루프의 실체는 for 문 하나입니다. 핵심은 15줄쯤입니다.
  • contents=history — 매번 쌓인 것 전체를 보냅니다. 2장 chat_multi 와 같습니다.
  • 종료 조건은 if not resp.function_calls — 부를 도구가 없으면 답할 때입니다.
  • 모델의 결정(⑤)과 실행 결과(⑦)를 둘 다 기록해야 합니다. 하나라도 빠지면 앞뒤가 안 맞습니다.
  • 실행은 TOOLS[fc.name](**dict(fc.args)) — 여전히 우리 코드입니다.
  • 5장에서 이 30줄이 create_agent 한 줄이 됩니다. 감춰지는 것이 정확히 무엇인지 지금 봐 두세요.
  • 도구 구성과 독스트링은 라이브러리가 대신해 주지 않습니다.
← 이전 절06. 따라하기 두 도구를 함께 쥐여 주기다음 절 →08. 종료 조건 무한 반복과 비용 폭탄 막기