
Function Calling의 최소 구현을 통해 본 AI 에이전트가 실패하는 경계
요약
Function Calling을 활용한 AI 에이전트 구현 시 발생하는 실질적인 실패 사례와 경계 설계 방법을 다룹니다. 단순 데모를 넘어 신뢰할 수 있는 도구로 작동하기 위한 인자 검증, 호출 판단, 에러 처리 전략을 제시합니다.
핵심 포인트
- JSON Schema의 description과 enum 최적화로 형식 에러 방지 가능
- 그럴듯하게 틀린 인자(argument)는 모델이 아닌 실행 측에서 검증해야 함
- 도구 호출 여부의 판단을 고정하기보다 모델의 판단력을 검증하는 설계 필요
- 에러 발생 시 예외를 raise 하지 말고 도구의 결과값으로 반환하여 모델이 인지하게 함
TL;DR
- function calling은 「데모가 작동하는 것」과 「도구로서 맡길 수 있는 것」 사이에 큰 격차가 있습니다. 실패하는 지점은 대략 4곳이었습니다.
- 모델에 전달하는 JSON Schema가 실질적인 프롬프트입니다. description과 enum을 조절하는 것만으로도, 구현 전에 실패의 절반 정도를 없앨 수 있습니다.
- 다만 schema로 방지할 수 있는 것은 「형식의 에러」뿐이며, 「그럴듯하게 틀린 인자(argument)」는 방지할 수 없습니다. 그 부분은 경계 설계(boundary design)의 영역입니다.
- 인간의 확인은 프롬프트에 「위험한 조작은 확인해줘」라고 쓰는 것이 아니라, 코드 측의 분기(branch)로 두는 것이 더 확실했습니다.
서론
function calling의 데모는 금방 작동합니다. 도구를 정의하고, 모델에 전달하고, 반환된 호출을 실행한다. 튜토리얼대로 작성하면 30분도 걸리지 않습니다.
문제는 그 이후입니다. 동일한 코드를 에이전트에組み込んで 맡기는 순간, 확인하지 않은 경로를 삭제하러 가거나, 에러 상태 그대로 침묵하거나, 같은 실패를 반복하기 시작합니다. 코드는 바꾸지 않았는데 말입니다.
이 기사에서는 그 「데모가 작동하는 것」과 「도구로서 맡길 수 있는 것」 사이에 있는 격차를 최소 구현을 통해 구분해 보겠습니다. 작동 방식에 대한 해설은 하지 않습니다. 대상은 도구 실행 루프를 직접 작성하고 있거나(또는 작성할 예정인) 분들입니다.
전제와 최소 구현
검증용 최소 구현은 이것뿐입니다. 도구는 읽기 전용인 list_files와 파괴적인 delete_file 두 가지입니다. 나중에 설명할 경계에 관한 이야기를 위해, 성질이 다른 것을 의도적으로 섞었습니다.
import json
from openai import OpenAI
client = OpenAI()
...
이것으로 「지시 → 도구 호출 → 결과를 반환 → 다음 판단」의 루프는 돌아갑니다. 데모로서는 충분합니다.
문제는 이것을 에이전트로서 맡기는 순간 시작됩니다.
실패 모드: 깨지는 지점은 대략 이 4곳
- 인자가 그럴듯하게 틀려 있는 경우
가장 많은 경우가 이것입니다. list_files의 결과에 app_2024.log가 있는데, 삭제 시에는 app-2024.log를 지정합니다. 혹은 대화에서 한 번도 나오지 않은 경로를 문맥상 「있을 법하다」는 이유로 구성해 옵니다.
schema 측면에서는 완전히 valid하므로, 형식 체크로는 걸러낼 수 없습니다. 존재하지 않는 경로라면 FileNotFoundError로 멈출 뿐이지만, 우연히 존재하는 다른 파일을 가리키고 있을 경우 파괴적인 조작이 그대로 통과됩니다. 여기가 첫 번째 경계입니다. 인자의 정확성은 모델이 아니라 실행 측에서 담보해야 합니다.
- 호출하지 않거나, 혹은 너무 많이 호출하는 경우
도구를 전달했음에도 모델이 「아마 logs에는 오래된 파일이 있을 것입니다」라며 실행하지 않고 추측으로 대답하는 케이스가 있습니다. 반대로, 대화만으로 답할 수 있는 질문에 대해 도구를 호출하러 가는 경우도 있습니다.
tool_choice를 required로 설정하면 반드시 호출하지만, 이번에는 「호출할 필요가 없는 상황에서도 무언가를 호출하는」 쪽으로 치우칩니다. 저는 첫 통신 확인 이외에 required를 사용하는 것은 그만두었습니다. 호출 여부의 판단 자체를 검증하고 싶은데, 그 판단을 고정해 버리면 검증이 되지 않기 때문입니다.
- 에러 반환 방식을 틀리면 루프 자체가 깨지는 경우
최소 구현의 run_tool에서 예외(exception)를 문자열로 만들어 반환하는 것은 장식이 아닙니다. 여기서 예외를 그대로 raise 하면, 모델에 「실패했다」는 정보가 전달되지 않은 채 처리가 중단됩니다.
에러를 tool의 결과로서 반환하면, 모델은 다음 턴에서 그것을 읽고 경로를 다시 확인하는 등의 수정 행동을 취할 수 있습니다. 즉 에러 메시지는 모델에 대한 피드백의 일부입니다. error: FileNotFoundError라고만 반환하는 것보다, 「존재하지 않습니다. list_files로 확인해 주세요」라고 반환하는 편이 다음 수의 질이 명확하게 달라집니다.
- 같은 실패를 반복하며 멈추지 않는 경우
수정 행동을 취할 수 있다고 말씀드렸지만, 항상 취할 수 있는 것은 아닙니다. 같은 인자로 같은 에러를 계속 일으키는, 이른바 폭주 루프에 빠질 수 있습니다. 위 코드에서 for _ in range(8)과 같이 상한을 정해둔 것은 그 때문입니다.
상한에 도달했을 때 「중간 과정을 요약하여 사용자에게 반환」하는 처리를 넣어두면, 실패하더라도 무엇이 일어났는지는 추적할 수 있습니다. 말없이 멈추는 것이 가장 곤란합니다.
schema가 실질적인 프롬프트가 된다
실패 모드 1과 2는 구현을 수정하기 전에 schema 측면에서 상당히 줄일 수 있습니다. 모델은 description을 읽고 호출 여부를 판단하므로, schema는 문서가 아니라 프롬프트의 일부로서 작성해야 합니다.
시도해 보았을 때 효과가 있었던 순서대로:
- description에 「언제 사용하는지」와 「사용 전 조건」을 작성한다.
delete_file에 「list_files로 존재를 확인한 대상만」이라고 적혀 있는 것이 바로 이 사례입니다. 작성 전후로 갑자기 삭제를 시도하는 빈도가 눈에 띄게 달라집니다. - 취할 수 있는 값이 유한하다면 enum으로 제한한다. 자유로운 string을 전달하면 모델은 마음대로 발명해냅니다.
additionalProperties: false와required를 반드시 작성한다. 불필요한 키의 발명과 필수 인수의 생략은 Structured Outputs의 strict 모드를 사용하면 형식(format) 레벨에서는 보장할 수 있습니다.
단, 한계도 명확합니다. strict 모드가 보장하는 것은 「schema에 부합하는 JSON이 반환된다」는 것까지이며, 실패 모드 1인 「유효(valid)하지만 의미적으로 틀린 인자」는 그대로 통과됩니다. 형식의 올바름과 의미의 올바름은 별개의 레이어 문제이며, 후자를 schema에 기대하면 배신당하게 됩니다.
인간의 확인을 어디에 둘 것인가
의미의 올바름을 완전히 검증할 수 없는 이상, 파괴적인 조작 전에는 인간의 확인이 필요합니다. 여기서 가장 흔히 저지르는 실수는 시스템 프롬프트에 「위험한 조작 전에는 반드시 확인해 주세요」라고 적는 것입니다.
저도 그렇게 적었습니다. 그리고 확인 없이 삭제되는 상황을 경험했습니다. 프롬프트의 지시는 통계적인 경향일 뿐, 보증이 아닙니다.
확인은 모델에게 부탁하는 것이 아니라 **코드의 분기(branch)**로서 배치해야 합니다.
DESTRUCTIVE = {"delete_file"}
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
...
사소한 부분이지만, 거절되었을 때 처리를 중단하는 것이 아니라 거절되었다는 사실을 tool의 결과로서 반환하고 있는 것이 포인트입니다. 이렇게 하면 모델은 「거절되었다」는 전제하에 대화를 이어갈 수 있고, 대안 제시로 넘어갈 수 있습니다. 처리를 중단해 버리면 사용자 입장에서는 「왜 갑자기 말이 없지?」라고 느껴지게 됩니다.
전체 흐름은 다음과 같습니다.

어디까지를 「파괴적」인 범위에 포함할지는 아직 스스로 선을 긋는 과정에 있습니다. 삭제나 네트워크를 통한 쓰기는 확실히 포함하되, 로컬 파일 덮어쓰기를 매번 확인하게 하면 이번에는 확인 피로(confirmation fatigue) 때문에 기계적으로 'y'를 누르게 됩니다. 그것은 확인이 없는 상태보다 더 나쁠 수도 있습니다. 이 부분은 운영하면서 조정해 나가는 단계입니다.
요약
최소 구현을 통해 보인 경계를 정리하자면, function calling을 「믿고 맡길 수 있는 도구」로 만드는 작업은 모델의 똑똑함을 끌어내는 작업이라기보다, 모델을 신뢰하지 않을 지점을 결정하는 작업이었습니다.
- 형식의 올바름: schema와 strict 모드에 맡긴다
- 에러로부터의 회복: 에러를 tool 결과로 반환하여 모델에게 맡긴다
- 의미의 올바름과 파괴적 조작: 모델에게 맡기지 않고, 코드의 분기와 인간의 확인으로 보호한다
다음에는 확인의 입도(조작 단위가 아니라 「계획 단위」로 승인을 받는 형태)를 시도해 볼 예정입니다. 비슷한 문제로 고민하고 계신 분들께 참고가 되길 바랍니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기