🎁 신규 무료 가입, 10회 호출 제공. 최대 $1, 카드 불필요.
에이전트 루프에서의 GLM 5.2 Tool Call: 'OpenAI 호환'이 숨기는 것

에이전트 루프에서의 GLM 5.2 Tool Call: 'OpenAI 호환'이 숨기는 것

목차
  1. 같은 턴, 세 가지 방식
  2. 텍스트가 tool call 에 함께 실린다
  3. 생각을 소리 내어 한다
  4. GLM 5.2 의 tool 호출 턴 비용
  5. GLM 5.2를 언제 쓰고, 어떻게 잘 돌릴까
  6. 면책 조항
  7. 출처

기존 OpenAI 스타일 에이전트 루프를 GLM 5.2 로 돌려보면 대부분 그대로 동작한다. tools 를 보내면 tool_calls 가 돌아오고, 그걸 실행한 뒤 결과를 다시 보내면 된다. 그런데 SDK 예제에서는 본 적 없는 동작이 하나 나온다. 어시스턴트가 tool call 과 같은 턴에 텍스트 한 줄을 함께 반환한다.

{
  "choices": [{
    "finish_reason": "tool_calls",
    "message": {
      "role": "assistant",
      "content": "I'll look up both pieces of information for you at the same time!",
      "tool_calls": [
        {"id": "call_…", "type": "function",
         "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}},
        {"id": "call_…", "type": "function",
         "function": {"name": "get_time", "arguments": "{\"city\":\"Tokyo\"}"}}
      ]
    }
  }]
}

널리 쓰이는 컨벤션은 두 가지이고, 둘 다 같이 보는 게 좋다. OpenAI 방식에서는 function 스키마를 보내면 tool_calls 가 돌아오고, call 마다 tool_call_id 로 묶인 tool 메시지로 응답한다.

resp = openai.chat.completions.create(model="…", tools=tools, tool_choice="auto", messages=messages)
# assistant.tool_calls → [{"id": "call_…", "function": {"name": "get_weather", "arguments": "{\"city\":\"Paris\"}"}}]
messages.append(resp.choices[0].message)
messages.append({"role": "tool", "tool_call_id": "call_…", "content": "18C, clear"})

Anthropic 방식은 구조가 다르다. tool 에 input_schema 가 붙고, 모델은 tool_use 블록을 내보내며, tool_result 블록으로 응답한다.

resp = anthropic.messages.create(model="…", tools=tools, messages=messages)
# resp.content → [{"type": "tool_use", "id": "toolu_…", "name": "get_weather", "input": {"city": "Paris"}}]
messages.append({"role": "assistant", "content": resp.content})
messages.append({"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": "toolu_…", "content": "18C, clear"}]})

GLM 5.2 는 OpenAI 방언을 쓴다.

OpenAI 규약에서는 finish_reasontool_calls 일 때 message.contentnull 이다. 많은 에이전트 루프가 이 점에 의존한다. “content 냐 tool call 이냐”로 분기하거나, content 를 최종 답변으로 로깅하거나, content 가 비어 있다고 단언하는 식이다. GLM 은 둘을 한꺼번에 넘겨주므로, 이 가정이 가장 먼저 깨진다.

여기서 다루는 동작은 glm-5.2 에 실제로 보낸 tool-calling 요청에서 관찰한 것이고, 같은 작업을 gpt-5.5claude-opus-4-8 로도 돌려 비교 기준으로 삼았다. 요약하면 이렇다. GLM 5.2 는 OpenAI API 표면을 쓰지만, 몇 가지 축에서는 GPT 보다 Claude 에 가깝게 동작한다. 그래서 발이 걸리는 건 OpenAI 기준으로 짜인 루프 쪽이다.

같은 턴, 세 가지 방식

같은 프롬프트, 같은 두 개의 tool, 세 개의 모델:

GLM (glm-5.2)OpenAI (gpt-5.5)Anthropic (claude-opus-4-8)
API surfaceOpenAI chat-completionsOpenAI chat-completionsAnthropic messages
tool-call 턴의 텍스트content 프리앰블 (non-null)contentnulltool_use 앞의 text 블록
해당 턴의 reasoning노출됨: reasoning_content + reasoning_tokens숨김; usagereasoning_tokens활성화한 경우에만 thinking 블록으로
병렬 tool call지원, index 포함지원지원, 여러 개의 tool_use 블록
완료 신호finish_reason: "tool_calls"finish_reason: "tool_calls"stop_reason: "tool_use"
tool-call id 접두사call_…call_…toolu_…

루프가 깨지는 지점은 두 행이다. tool-call 턴에 텍스트가 실리는 경우, 그리고 그 턴에 reasoning 이 등장하는 경우. 나머지는 다행히도 별다를 게 없다.

텍스트가 tool call 에 함께 실린다

GLM 5.2 는 tool_calls 와 함께 짧은 assistant content 프리앰블을 흔히 내보낸다. finish_reason"tool_calls" 다. 오류도 아니고 가끔 일어나는 일도 아니다.

세 모델에서 같은 턴을 가져와, 차이가 나는 부분만 잘라낸 것이다:

// OpenAI gpt-5.5: content is null on a tool-call turn
"message": { "content": null,
             "tool_calls": [ {/* get_weather */}, {/* get_time */} ] }

// GLM glm-5.2: content carries a preamble
"message": { "content": "I'll look up both pieces of information for you at the same time!",
             "tool_calls": [ {/* get_weather */}, {/* get_time */} ] }

// Anthropic claude-opus-4-8: a text block sits before the tool_use blocks
"content": [ { "type": "text", "text": "I'll get both pieces of information for you." },
             { "type": "tool_use", /* get_weather */ },
             { "type": "tool_use", /* get_time */ } ]

OpenAI 는 content 를 null 로 둔다. GLM 은 채운다. Anthropic 은 원래부터 거기에 text 블록을 넣어왔다. 결국 GLM 은 OpenAI 의 wire format 을 쓰면서 행동 전에 설명부터 하는 Anthropic 의 습관을 가져온 셈이고, OpenAI 기준으로 짠 루프가 허를 찔린다. 고치는 건 간단하지만 의식적으로 해야 한다. tool-call 턴에 content 가 없다고 가정하지 마라:

resp = client.chat.completions.create(model="glm-5.2", messages=msgs, tools=tools)
msg = resp.choices[0].message

# GLM may return assistant text in the same turn as the tool calls.
if msg.content:
    log.debug("preamble: %s", msg.content)   # keep or drop, but don't assume it's empty

msgs.append(msg)
for call in msg.tool_calls:
    result = dispatch(call.function.name, json.loads(call.function.arguments))
    msgs.append({"role": "tool", "tool_call_id": call.id, "content": result})

루프가 content 를 assistant 의 응답으로 사용자에게 그대로 보여준다면, 이제 모든 tool call 앞에 “확인해 볼게요” 같은 문구가 표시된다. 이걸 원하는지 결정하라. 핵심은 그 결정을 당신이 한다는 것이다. 모델의 침묵이 대신 정해주는 게 아니다.

생각을 소리 내어 한다

GLM 5.2 는 추론 모델이고, tool 호출 때도 추론을 멈추지 않는다. tool 호출 턴에도 추론이 함께 따라오는데, GLM 5.2 는 이걸 텍스트로 드러낸다. non-streaming 응답에서는 token 집계에 그대로 나타난다。

"usage": {
  "prompt_tokens": 224,
  "completion_tokens": 68,
  "completion_tokens_details": { "reasoning_tokens": 30 },
  "total_tokens": 292
}

눈에 보이는 출력은 짧은 함수 호출 두 개뿐인데, completion 의 거의 절반이 추론이다. 세 모델이 갈리는 지점이 바로 여기다. GLM 5.2 는 추론을 reasoning_content 로 주고 token 수까지 알려준다. OpenAI 는 usagereasoning_tokens 로 과금하지만 텍스트는 절대 보여주지 않는다. Anthropic 은 extended thinking 을 켰을 때만 thinking 블록으로 보여준다. 셋 중 기본값에서 가장 많이 드러내는 게 GLM 5.2 다。

결과는 두 가지다. 첫째는 비용이다. tool 호출 턴마다 이 추론 token 값을 내야 하고, agent 루프는 턴이 많다. 이 숫자를 움직이는 다이얼이 reasoning effort 인데, GLM 5.2: Reasoning Effort Is the Cost Lever 에서 다뤘다. 추론 token 은 마지막 답변뿐 아니라 매 턴마다 세야 한다。

둘째는 streaming 순서다. 요청을 stream 하면 GLM 은 추론을 먼저 보내고, 그다음에 preamble 텍스트, 마지막에 tool 호출을 보낸다。

reasoning_content  (many deltas)
content            (a few deltas)
tool_calls         (id + name, then arguments)

기본 OpenAI chat completions 기준으로 작성한 파서는 reasoning_content 필드를 모르기 때문에, 맨 앞에 쏟아지는 이 추론 덩어리를 조용히 무시한다. 대개는 문제없다. 하지만 첫 content 델타를 기준으로 “thinking…” 상태를 띄우는 UI 라면 얘기가 다르다. wire 에 맨 처음 오는 건 content 가 아니라 추론이라서, 표시기가 끝내 바뀌지 않는다。

GLM 5.2 의 tool 호출 턴 비용

동작이 절반이라면, 비용이 나머지 절반이다. agent 루프는 같은 턴을 여러 번 반복해서 돌린다. 고정 prefix(약 2,000 token 의 system prompt 와 tool 정의)를 두고 user 메시지만 호출마다 바꿔서, warm 상태 열 턴을 측정한 결과다。

warm tool 호출 턴당GLM glm-5.2OpenAI gpt-5.5Anthropic claude-opus-4-8
비용$0.0009$0.0042$0.0051
지연 시간 (중앙값)6.6s1.9s3.1s
Prompt 캐시≈96%≈81%≈97%
추론 token≈2700
Cold → warm 비용3.4×2.8×4.9×

GLM 5.2 가 싸다. warm 턴당 GPT-5.5 보다 약 4.5배, Opus 보다 약 5.4배 저렴하다. 동시에 가장 느리기도 하다. 매 턴마다 추론 token 을 쓰는데 다른 둘은 이 작업에서 추론을 전혀 쓰지 않았기 때문에, 지연 시간이 두 배에서 세 배 반에 이른다. 결국 거래다. GLM 은 지연 시간을 내주고 비용을 얻으며, 그 균형을 움직이는 다이얼이 reasoning effort 다。

루프에서 이걸 감당 가능하게 만드는 건 캐싱이다. system prompt 와 tool 정의가 매 prompt 의 대부분을 차지하고 턴마다 동일하니, prefix 가 한번 캐시되면 턴 비용이 2.8배에서 4.9배 싸진다. 이게 보이느냐는 두 가지가 결정한다. GLM 과 OpenAI 는 prefix 를 자동으로 캐시하지만, Anthropic 은 cache_control 로 표시한 것만 캐시한다. 그리고 GLM 의 캐시는 한 박자 늦게 데워져서, 3단계 작업은 full price 를 다 내는데 30단계 작업은 캐시된 채로 돌아간다. 동작 방식은 Open-Weight LLM Caching 에 정리했다。

GLM 5.2를 언제 쓰고, 어떻게 잘 돌릴까

앞의 내용을 종합해 보자. GLM 5.2는 그 표에서 가장 싸고 가장 느린 모델이고, 매 턴마다 추론을 한다. 이 특성이 곧 이 모델이 빛을 발하는 지점을 말해 준다.

어디에 맞는가: 비용이 지배적이고 턴당 몇 초 정도는 괜찮은, 길고 여러 단계로 이어지는 에이전트 루프다. 백그라운드 코딩 에이전트, CI와 배치 자동화, 사람 손을 타지 않고 도는 작업이다. 모델을 느리게 만드는 그 추론이, 단순 라우팅이 아니라 실제 코딩과 계획에서 버텨 주는 이유이기도 하다. 워밍업을 지나면 캐싱이 이 판단을 더 굳혀 준다. 서른 단계짜리 작업은 prefix 비용을 분산시켜 싸게 돌아가지만, 세 단계짜리는 비용을 전액 다 내면서 지연만 떠안고 얻는 게 없다. 그러니 긴 작업에는 GLM 5.2를 꺼내 쓰고, 턴당 6초가 체감되는 인터랙티브한 단발성 호출에는 더 빠른 모델을 따로 두자.

GLM 5.2를 잘 돌리는 법. 다섯 가지 습관이면 OpenAI API 표면을 벗어나지 않고도 루프를 GLM에 맞게 준비할 수 있다.

  • tool-call 턴이 content를 담고 있을 수 있다고 보라. 비어 있다고 단정하지 마라.
  • 와이어로는 reasoning_content, usage에는 reasoning_tokens가 온다고 예상하라. 둘 다 예산을 잡아 두고, reasoning-effort 다이얼로 품질과 비용을 맞바꿔라.
  • 추론이 먼저 오므로, streaming에서 첫 content 델타를 기준으로 UI 상태를 잡지 마라.
  • tool_call_id는 그대로 되돌려보내라. 불투명한 값으로 취급하고, 절대 파싱하거나 새로 만들지 마라.
  • 호출이 끝날 때까지 streaming argumentsindex별로 모아라. 청크 개수를 가정하지 마라.

방어하지 않아도 되는 것이 두 가지 있다. GLM은 다른 모델들처럼 index를 붙여 병렬 tool call을 내보내고, 왕복은 정상적으로 닫힌다. assistant 턴을 붙이고, 각 호출마다 결과를 담은 tool 메시지를 하나씩 붙이면 finish_reason: "stop"으로 끝난다. 이왕 하는 김에 캐시 가능한 prefix를 턴 사이에서 바이트 단위로 안정적으로 유지하라. system prompt와 tool 정의가 거의 모든 prompt의 대부분을 차지하고, prefix가 안정적이어야 GLM의 캐시가 워밍된 뒤로 비용을 떠안아 준다.

특별할 것은 하나도 없다. “요청이 성공한다”와 “에이전트 루프가 올바르다” 사이의 간극이고, GLM에서 그 간극은 대체로 두 가정의 폭이다. tool-call 턴은 조용하다는 가정과, 모델이 생각하지 않는다는 가정. 이 둘을 버리고 prefix를 안정적으로 유지하면, 하나의 루프로 GLM, GPT, Claude를 똑같이 돌릴 수 있다. 게다가 지연이 최적화 대상이 아닌 곳이라면 GLM이 그 일을 푼돈으로 해낸다.

면책 조항

위의 비용, 지연, 캐시 수치는 2026-06-30에 glm-5.2, gpt-5.5, claude-opus-4-8로 모델당 워밍된 tool-call 턴 10회를 측정한 값이다. 비용은 보고된 usage에서 가져왔고, 지연은 wall-clock 중앙값으로 부하와 reasoning effort에 따라 달라진다. 모델 동작과 가격은 변하므로, 이 수치는 참고용으로만 보고 의존하기 전에 직접 트래픽으로 다시 측정하라.

출처

← 블로그로 돌아가기