LLM API 를 에이전트 관점에서 다시 보기
에이전트를 짜기 전에 LLM API를 한 번 더 봐야 하는 이유가 있습니다. 챗봇을 만들 때는 대충 넘어가도 되던 세부사항이, 에이전트에서는 루프가 스무 번 도는 동안 매번 비용과 정확도에 영향을 줍니다.
메시지는 배열이지 대화가 아니다
API 입장에서 "대화"라는 건 없습니다. 있는 건 messages라는 배열뿐이고, 모델은 매 호출마다 그 배열 전체를 처음부터 다시 읽습니다.
messages = [
{"role": "user", "content": "서울 날씨 알려줘"},
{"role": "assistant", "content": "..."},
{"role": "user", "content": "내일은?"},
]
response = model.run(messages)두 번째 호출에서 모델은 "내일은?"이라는 한 문장만 보는 게 아니라 배열 전체를 다시 봅니다. 즉 대화가 10턴 이어지면 10번째 호출은 1턴짜리 호출보다 훨씬 많은 토큰을 씁니다. 챗봇에서는 이게 그냥 비용 문제지만, 에이전트에서는 다릅니다 — 도구 호출 결과가 메시지 배열에 계속 쌓이기 때문에, 루프가 열 번만 돌아도 배열이 수천 토큰으로 불어납니다. 7장에서 이 문제를 다시 다룹니다.
role 세 개, 그리고 에이전트가 추가하는 것
기본적인 API는 user와 assistant 두 role로 충분합니다. 에이전트 루프에서는 도구 실행 결과를 모델에게 돌려줘야 하는데, 이게 tool(또는 API에 따라 tool_result 콘텐츠 블록) 형태로 들어갑니다.
messages.append({"role": "assistant", "content": response.content}) # 도구 호출 요청
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": tool_id, "content": result}]
})주의할 점 하나. Anthropic API는 도구 결과를 role: "user" 메시지 안의 tool_result 블록으로 넣습니다. OpenAI 계열 API는 별도의 role: "tool"을 씁니다. 이름만 다른 게 아니라 메시지 구조 자체가 달라서, 프레임워크 없이 직접 짤 때는 사용하는 SDK의 문서를 반드시 확인해야 합니다. 이 책의 예제는 Anthropic Messages API 형식을 기준으로 합니다.
컨텍스트 윈도우는 예산이지 한계가 아니다
"컨텍스트 윈도우가 20만 토큰이니 넉넉하다"고 생각하면 에이전트에서 곧 문제가 생깁니다. 도구가 반환하는 값 — 예를 들어 검색 결과 전문이나 API 응답 JSON 전체 — 을 그대로 메시지에 넣으면 몇 번의 도구 호출만으로 윈도우 절반이 날아갑니다. 그리고 컨텍스트가 길어질수록 모델이 앞부분 지시사항을 놓치는 경향도 실제로 있습니다. "쓸 수 있는 최대치"와 "써도 되는 적정치"는 다른 숫자입니다.
실무에서 쓰는 원칙은 단순합니다. 도구 결과는 모델이 다음 판단에 필요한 만큼만 잘라서 넣습니다. 검색 API가 문서 20개를 반환해도, 제목과 요약만 넘기고 본문은 모델이 필요하다고 판단할 때만 별도 도구로 가져오게 합니다.
스트리밍과 에이전트는 궁합이 애매하다
챗봇 UI에서는 스트리밍이 사실상 필수입니다. 사용자가 답변이 한 글자씩 나오는 걸 봐야 기다림이 덜 지루하니까요. 그런데 에이전트 루프에서는 스트리밍의 이점이 줄어듭니다. 모델이 도구를 호출할지 말지는 응답이 끝나야 알 수 있는 경우가 많고, 중간 결과를 사용자에게 보여줄 이유도 없을 때가 많습니다. 저는 에이전트 루프의 내부 단계에서는 스트리밍을 꺼 두고, 최종 답변을 사용자에게 보여주는 마지막 단계에서만 켜는 편입니다. 코드가 단순해지고, 도구 호출 파싱 중 스트림 청크가 잘리는 골치 아픈 버그를 피할 수 있습니다.
다음 장에서는 이 메시지 배열에 tools 파라미터를 얹었을 때 실제로 무슨 일이 벌어지는지 — Tool Use의 정체를 봅니다.