에이전트 루프에서의 GLM 5.2 도구 호출 (Tool Calls): 'OpenAI 호환'이 숨기고 있는 것
요약
GLM 5.2 모델이 OpenAI 호환 API를 사용함에도 불구하고, 도구 호출(Tool Calls) 시 텍스트를 동시에 반환하는 독특한 동작 방식을 취함을 분석합니다. 이는 기존 OpenAI 스타일로 설계된 에이전트 루프에서 예기치 않은 오류를 발생시킬 수 있습니다.
핵심 포인트
- GLM 5.2는 도구 호출과 텍스트 응답을 동일한 턴에서 동시에 제공함
- OpenAI API 규약(content가 null이어야 함)과 차이가 있어 에이전트 루프가 깨질 수 있음
- GLM 5.2는 API 인터페이스는 OpenAI를 따르지만 동작 방식은 Anthropic과 유사한 측면이 있음
- 에이전트 개발 시 모델별 finish_reason과 content 존재 여부를 정밀하게 검증해야 함
기존의 OpenAI 스타일 에이전트 루프를 GLM 5.2로 지정하면 대부분은 그대로 작동합니다. tools를 보내고, tool_calls를 돌려받고, 이를 실행한 뒤 결과를 다시 보내는 방식입니다. 하지만 그 후, SDK 예제에서는 전혀 보여주지 않는 동작을 수행합니다. 어시스턴트(Assistant)가 도구 호출(tool calls)과
동일한 턴(turn)에서 텍스트 한 줄을 반환하는 것입니다:
{
"choices": [{
"finish_reason": "tool_calls",
...
두 가지 관습이 지배적이며, 두 가지를 모두 염두에 두는 것이 도움이 됩니다. OpenAI의 방식에서는 함수 스키마(function schemas)를 보내고, tool_calls를 돌려받은 뒤, 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)
...
Anthropic의 방식은 다르게 형성되어 있습니다. 도구(tools)는 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})
...
GLM 5.2는 OpenAI 방언을 사용합니다.
OpenAI의 규약(contract)에서는 finish_reason이 tool_calls일 때 message.content는 null입니다. 수많은 에이전트 루프가 이 점에 의존합니다. 즉, "내용(content) 또는 도구 호출(tool calls)"에 따라 분기하고, content를 최종 답변으로 기록하거나, 그것이 비어 있다고 단정합니다. 하지만 GLM은 이 두 가지를 동시에 제공하며, 바로 그 가정이 가장 먼저 깨지게 됩니다.
이 동작은 glm-5.2에 대한 실시간 도구 호출 요청에서 포착되었으며, 참조점으로 gpt-5.5와 claude-opus-4-8을 동일한 작업에 실행했습니다. 요약하자면 다음과 같습니다: GLM 5.2는 OpenAI API 표면(surface)을 사용하지만, 몇 가지 축에서는 GPT보다는 Claude처럼 동작하며, OpenAI로 학습된 루프가 발을 헛디디게 됩니다.
동일한 턴, 세 가지 방식
동일한 프롬프트, 동일한 두 개의 도구, 세 가지 모델:
GLM (glm-5.2) | OpenAI (gpt-5.5) | Anthropic (claude-opus-4-8) | |
|---|---|---|---|
| API surface | OpenAI chat-completions | OpenAI chat-completions | Anthropic messages |
| ... |
루프가 깨지는 지점은 두 행입니다: 도구 호출 (tool-call) 턴에서의 텍스트, 그리고 해당 턴에서 나타나는 추론 (reasoning)입니다. 나머지는 안심할 수 있을 만큼 평범합니다.
텍스트가 도구 호출과 함께 전달됨
GLM 5.2는 finish_reason: "tool_calls"와 함께 tool_calls 옆에 짧은 어시스턴트 content 서문을 일상적으로 출력합니다. 이는 오류도 아니고 일시적인 현상도 아닙니다.
다음은 차이가 나는 부분만 남기고 다듬은 세 모델의 동일한 턴입니다:
// OpenAI gpt-5.5: 도구 호출 턴에서 content는 null임
"message": { "content": null,
"tool_calls": [ {/* get_weather */}, {/* get_time */} ] }
...
OpenAI는 content를 null로 남겨두고, GLM은 이를 채우며, Anthropic은 항상 그곳에 text 블록을 넣어왔습니다. 따라서 GLM은 OpenAI의 와이어 포맷 (wire format)을 따르면서도, 행동하기 전에 설명을 덧붙이는 Anthropic의 습관을 동시에 취하고 있습니다. 결과적으로 OpenAI를 기준으로 작성된 루프가 허를 찔리게 됩니다. 해결책은 간단하지만 의도적으로 조치해야 합니다. 도구 호출 턴을 내용이 없는 (content-free) 상태로 취급하지 마세요:
resp = client.chat.completions.create(model="glm-5.2", messages=msgs, tools=tools)
msg = resp.choices[0].message
...
만약 당신의 루프가 어시스턴트의 답변으로서 content를 사용자에게 렌더링한다면, 이제 모든 도구 호출 전에 "확인해 보겠습니다"와 같은 문구가 표시될 것입니다. 이를 원하는지 여부를 결정하십시오. 핵심은 그 결정이 당신의 몫이지, 모델의 침묵이 대신 결정해 주는 것이 아니라는 점입니다.
생각하며 말하기 (It thinks out loud)
GLM 5.2는 추론 모델 (reasoning model)이며, 도구 사용을 위해 추론을 멈추지 않습니다. 도구 호출 턴은 추론을 함께 수반하며, GLM 5.2는 이를 텍스트로 노출합니다. 비스트리밍 (non-streaming) 응답에서는 토큰 계산을 통해 이를 명확히 알 수 있습니다:
"usage": {
"prompt_tokens": 224,
"completion_tokens": 68,
...
가시적인 출력값이 두 개의 짧은 함수 호출(function calls)뿐인 요청임에도 불구하고, 생성된 결과(completion)의 거의 절반이 추론(reasoning)이었습니다. 이 지점이 세 모델이 모두 갈라지는 부분입니다. GLM 5.2는 추론 내용을 reasoning_content와 함께 토큰 수로 제공합니다. OpenAI는 usage에서 reasoning_tokens를 청구하지만 텍스트는 절대 보여주지 않습니다. Anthropic은 확장된 사고(extended thinking) 기능을 켰을 때만 thinking 블록으로 보여줍니다. 기본 설정 상태에서 GLM 5.2가 세 모델 중 가장 투명하게 공개되어 있습니다.
두 가지 결과가 따릅니다. 첫째, 비용입니다. 도구 호출(tool-call) 턴에서 이러한 추론 토큰에 대한 비용을 지불해야 하며, 에이전트 루프(agent loop)는 수많은 턴으로 구성됩니다. 추론 노력(reasoning effort)은 그 수치를 움직이는 다이얼이며, 이에 대해서는 GLM 5.2: 추론 노력은 비용 레버이다 (GLM 5.2: Reasoning Effort Is the Cost Lever)에서 다루었습니다. 최종 답변뿐만 아니라 모든 턴에서 추론 토큰을 계산하십시오.
둘째, 스트리밍 순서입니다. 요청을 스트리밍할 때, GLM은 추론을 먼저 보내고, 그다음 서문(preamble) 텍스트를, 그다음 도구 호출을 보냅니다:
reasoning_content (많은 델타(deltas))
content (몇 개의 델타(deltas))
tool_calls (id + name, 그 다음 arguments)
순수 OpenAI 채팅 완성(chat completions)을 기준으로 작성된 파서(parser)는 reasoning_content 필드를 알지 못하므로, 처음에 쏟아지는 이 데이터를 조용히 무시할 것입니다. 보통은 괜찮습니다. 하지만 만약 사용자의 UI가 첫 번째 콘텐츠 델타(content delta)를 기준으로 "생각 중...(thinking…)" 상태를 표시하도록 설계되어 있다면 문제가 됩니다. 네트워크를 통해 가장 먼저 전달되는 것은 콘텐츠가 아니라 추론이기 때문에, 상태 표시기가 결코 전환되지 않기 때문입니다.
GLM 5.2 도구 호출 턴의 비용
동작 방식이 이야기의 절반이라면, 청구서는 나머지 절반입니다. 그리고 에이전트 루프는 동일한 턴을 여러 번 반복합니다. 고정된 접두사(약 2,000토큰의 시스템 프롬프트와 도구 정의)를 사용하고 호출마다 사용자 메시지를 변경하며, 10번의 워밍업 턴(warm turns) 동안 측정한 결과는 다음과 같습니다:
| 워밍업 도구 호출 턴당 (per warm tool-call turn) | GLM glm-5.2 | OpenAI gpt-5.5 | Anthropic claude-opus-4-8 |
|---|---|---|---|
| 비용 (Cost) | $0.0009 | $0.0042 | $0.0051 |
| ... |
GLM 5.2는 가장 저렴합니다. 워밍업 턴당 GPT-5.5보다 약 4.5배, Opus보다 약 5.4배 저렴합니다. 하지만 또한 가장 느린 모델이기도 한데, 다른 두 모델이 이 작업에서 추론 토큰 (reasoning tokens)을 전혀 사용하지 않은 반면, GLM은 매 턴마다 추론 토큰을 사용하기 때문에 지연 시간 (latency)이 2배에서 3.5배 더 길었습니다. 이것이 바로 트레이드오프 (trade-off)입니다. GLM은 지연 시간을 대가로 비용을 절감하며, 추론 노력 (reasoning effort)이 이를 조절하는 다이얼 역할을 합니다.
캐싱 (Caching)은 루프 내에서 이 모델들을 감당 가능한 수준으로 만드는 핵심 요소입니다. 시스템 프롬프트 (system prompt)와 도구 정의 (tool definitions)는 모든 프롬프트의 대부분을 차지하며 매 턴 동일하므로, 접두사 (prefix)가 캐싱되면 턴당 비용이 2.8배에서 4.9배까지 저렴해집니다. 캐싱 적용 여부는 두 가지 요소에 의해 결정됩니다. GLM과 OpenAI는 접두사를 자동으로 캐싱하지만, Anthropic은 cache_control로 표시한 부분만 캐싱합니다. 또한 GLM의 캐시는 한 박자 늦게 예열(warm up)되기 때문에, 3단계 작업은 전체 비용을 지불해야 할 수도 있는 반면, 30단계 작업은 캐싱된 상태로 실행될 수 있습니다. 상세한 메커니즘은 Open-Weight LLM Caching에서 확인할 수 있습니다.
언제 GLM 5.2를 선택해야 하며, 어떻게 잘 실행할 것인가
지금까지의 내용을 종합해 보겠습니다. GLM 5.2는 위 표에서 가장 저렴하지만 느린 모델이며, 매 턴 추론을 수행합니다. 이러한 특성은 이 모델이 제 역할을 다할 수 있는 위치를 가리킵니다.
적합한 용도: 비용이 지배적이고 턴당 몇 초의 시간이 허용되는 긴 다단계 에이전트 루프 (long, multi-step agent loops). 백그라운드 코딩 에이전트, CI 및 배치 자동화 (batch automation), 무인으로 실행되는 작업 등이 해당됩니다. 모델을 느리게 만드는 그 추론 능력이 바로 단순한 라우팅 (routing)을 넘어 실제 코딩과 계획 수립에서 성능을 유지하는 이유이기도 합니다. 캐싱은 예열 단계를 지나면 그 이점을 더욱 증폭시킵니다. 30단계 작업은 접두사 비용을 분할 상환(amortize)하여 저렴하게 실행되지만, 3단계 작업은 전체 비용을 지불하면서 지연 시간만 낭비할 수 있습니다. 따라서 긴 작업에는 GLM 5.2를 선택하고, 턴당 6초의 지연이 체감되는 대화형 단발성 호출 (single-shot calls)에는 더 빠른 모델을 유지하십시오.
GLM 5.2를 잘 실행하는 방법. OpenAI API 인터페이스를 벗어나지 않고도 루프(loop)를 GLM에 최적화할 수 있는 다섯 가지 습관은 다음과 같습니다:
- 도구 호출 (tool-call) 턴이
content를 포함할 수 있다고 간주하십시오.content가 비어 있다고 단정하지 마십시오. - 네트워크 전송 시
reasoning_content가 전달되고usage에reasoning_tokens가 포함될 것을 예상하십시오. 두 가지 모두를 위한 예산을 책정하고, 추론 노력 (reasoning-effort) 조절 다이얼을 사용하여 품질과 비용을 교환하십시오. - 스트리밍 (streaming) 시, 추론 (reasoning)이 먼저 도착하므로 첫 번째 콘텐츠 델타 (content delta)에 UI 상태를 고정하지 마십시오.
tool_call_id를 있는 그대로 (verbatim) 에코(echo) 하십시오. 이를 불투명한 (opaque) 값으로 취급하고, 절대 파싱하거나 재생성하지 마십시오.- 호출이 종료될 때까지
index별로 스트리밍되는arguments를 누적하십시오. 청크 (chunk) 개수를 가정하지 마십시오.
방어할 필요가 없는 두 가지 사항은 다음과 같습니다: GLM은 다른 모델들과 마찬가지로 index와 함께 병렬 도구 호출 (parallel tool calls)을 생성하며, 왕복 (round-trip) 과정이 정상적으로 종료된다는 점입니다. 어시스턴트 (assistant) 턴을 추가하고, 각 호출에 대해 결과가 포함된 하나의 tool 메시지를 추가하면 finish_reason: "stop"으로 종료됩니다. 이 과정에서 캐시 가능한 접두사 (cacheable prefix)를 턴 전반에 걸쳐 바이트 단위로 안정적으로 (byte-stable) 유지하십시오. 시스템 프롬프트 (system prompt)와 도구 정의 (tool definitions)가 모든 프롬프트의 대부분을 차지하며, 안정적인 접두사는 GLM의 캐시가 예열(warm)된 후 비용을 절감할 수 있게 해주는 핵심입니다.
이 중 어느 것도 생소한 것이 아닙니다. 이는 "요청이 성공한다"와 "에이전트 루프 (agent loop)가 올바르게 작동한다" 사이의 간극이며, GLM에서 그 간극은 주로 두 가지 가정의 너비만큼 존재합니다: 도구 호출 턴은 무음(silent)일 것이라는 가정과, 모델이 생각(thinking)하지 않을 것이라는 가정입니다. 이 두 가지 가정을 버리고 접두사를 안정적으로 유지하면, 지연 시간 (latency)을 최적화해야 하는 상황이 아닌 곳에서는 GLM이 비용을 아주 적게 쓰면서도 GLM, GPT, Claude 모두를 동일하게 처리하는 하나의 루프를 구현할 수 있습니다.
면책 조항 (Disclaimer)
위의 비용, 지연 시간 및 캐시 수치는 2026-06-30에 glm-5.2, gpt-5.5, claude-opus-4-8 모델에 대해 모델당 10회의 예열된 도구 호출 턴을 통해 측정되었습니다. 비용은 보고된 사용량을 기준으로 하며, 지연 시간은 실제 경과 시간 (wall-clock) 중앙값이며 부하 및 추론 노력에 따라 변동됩니다. 모델의 동작과 가격은 변동될 수 있으므로, 위 수치를 지표로만 간주하고 실제 의존하기 전에 귀하의 트래픽에 맞춰 다시 측정하십시오.
출처 (Sources)
출처 (Sources)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기