AI 대화의 도구 호출과 결과는 어떻게 짝을 찾을까?

AI가 도구를 두 번 불렀는데, 결과도 두 개 돌아왔습니다. 그런데 결과가 호출 바로 다음 줄에 붙어 있지 않다면 어떨까요? 대화 기록을 읽는 쪽에서는 갑자기 작은 추리극이 시작됩니다. 이 결과는 어느 호출의 답일까? 그냥 가까이 있는 것끼리 붙이면 되는 걸까?
도구를 쓰는 AI 대화는 보통 도구 호출과 도구 결과를 별도 블록으로 남깁니다. 호출은 AI가 어떤 도구에 무엇을 요청했는지 나타내고, 결과는 실행 뒤에 돌아온 내용을 담습니다. 둘을 다시 연결하는 일을 여기서는 **짝짓기(pairing)**라고 부르겠습니다. 대화 화면에서는 한 덩어리처럼 보여도, 구조화된 기록에서는 둘이 떨어져 있을 수 있기 때문입니다.
가까운 것끼리 붙이면 되지 않을까?
가장 먼저 떠오르는 생각은 단순합니다. “위에서부터 읽다가 다음 결과를 만나면, 가장 가까운 호출에 붙이면 되겠네.” 한 번에 하나만 실행한다면 그럴듯합니다. 하지만 AI가 여러 도구를 병렬로 요청하면 호출과 결과는 서로 끼어들 수 있습니다. 먼저 요청한 일이 먼저 끝난다는 보장도 없습니다. 줄의 위치만 보고 가족관계를 정하는 셈이죠.
식당 주방을 떠올려 보세요. 주문서가 두 장 들어왔고, 음식은 조리 순서에 따라 나옵니다. 가장 최근 주문서와 가장 최근 접시를 무조건 붙이면, 비빔밥 주문서에 국수가 달려갈 수도 있습니다. 주문 번호가 있다면 이야기가 달라집니다. AI 대화의 도구 기록도 같은 원리입니다.
이 오해가 위험한 이유는 “대화가 대체로 순서대로 보인다”는 인상 때문입니다. 보기 좋은 순서와 검증 가능한 연결은 다른 문제입니다. 내보낸 JSON처럼 나중에 다시 읽을 데이터라면, 그 한 번의 잘못된 연결이 이후 분석까지 흐리게 만들 수 있습니다. 도구 결과도 가끔 줄 서기를 거부합니다. 바쁜 도구들이 단체로 “먼저 끝난 사람부터 나갑니다”를 외치는 셈이니까요.
첫 번째 원칙: ID를 먼저 찾습니다
가장 안전한 기준은 **식별자(ID)**입니다.
식별자는 각 도구 호출에 붙은 고유한 이름표입니다.
호출 블록의 id와 결과 블록의 tool_use_id가 같다면, 두 블록은 문서에서 얼마나 떨어져 있든 직접 연결할 수 있습니다.
구조는 복잡하지 않습니다. 호출을 읽을 때는 ID와 그 호출이 저장된 위치를 **맵(Map)**에 기록합니다. 맵은 “이 이름표면 이 위치”를 빠르게 찾는 표라고 생각하면 됩니다. 결과를 읽을 때는 결과의 ID로 맵을 조회합니다. 같은 이름표를 찾았다면 그 호출과 결과를 짝으로 확정합니다.
호출을 만남 → 호출 ID를 맵에 저장
결과를 만남 → 결과의 호출 ID로 맵 조회
같은 ID가 있음 → 해당 호출과 결과를 연결
이 방식의 장점은 순서와 무관한 연결입니다. 둘 사이에 다른 텍스트, 다른 도구 호출, 다른 결과가 있어도 이름표가 같으면 됩니다. 병렬 실행과 섞인 기록에서 특히 중요한 이유가 여기에 있습니다. “앞뒤가 맞아 보인다”가 아니라 “같은 ID다”라고 말할 수 있으니, 연결의 근거가 분명해집니다.
ID가 없을 때는 FIFO로, 단 규칙임을 잊지 말기
모든 기록에 ID가 있는 것은 아닙니다. 명시적인 식별자가 빠진 경우나 이전 형식의 호환 처리에서는 다른 기준이 필요합니다. 이때 쓰는 것이 FIFO(First-In, First-Out) 큐입니다. 먼저 들어온 것을 먼저 꺼내는 대기열 규칙이죠.
아직 결과가 없는 호출을 만날 때마다 큐에 넣습니다. ID로 연결하지 못한 결과가 나오면, 큐에서 가장 오래 기다린 호출을 꺼내 연결합니다. 카페에서 진동벨 번호가 없다면 주문받은 순서대로 음료를 건네는 방식과 비슷합니다. 완벽한 신원 확인은 아니지만, 기록의 문서 순서를 존중하는 일관된 대안입니다.
짝이 없는 호출을 만남 → FIFO 큐 뒤에 넣기
ID로 못 찾은 결과를 만남 → FIFO 큐 앞에서 하나 꺼내기
꺼낸 호출과 결과를 연결
여기서 중요한 태도는 보조 규칙을 증거로 착각하지 않는 것입니다. ID 연결은 같은 식별자를 확인한 직접 연결입니다. FIFO는 식별자가 없을 때 정한 복원 규칙입니다. 둘을 같은 신뢰도로 다루면, 나중에 기록을 검토할 사람이 왜 이 둘이 붙었는지 알기 어려워집니다. 그래서 구현에서도 ID 기반 연결을 먼저 시도하고, 실패했을 때만 FIFO를 사용합니다.
짝이 없는 결과도 버리지 않습니다
더 까다로운 경우가 하나 남습니다. 결과를 만났는데 앞선 호출을 찾을 수 없을 수 있습니다. 이런 **고아 결과(orphan result)**는 짝을 만들지 못한 결과 블록입니다. 없애 버리면 기록은 깔끔해 보일지 몰라도, 실제로는 데이터가 사라집니다. 깔끔함을 위해 증거를 버리는 것은 청소가 아니라 분실물 처리입니다.
따라서 연결할 호출이 없으면 결과를 독립 기록으로 보존하는 편이 낫습니다. 이 선택은 “무조건 성공한 짝만 남긴다”보다 “관찰한 블록을 잃지 않는다”를 우선합니다. 나중에 원본이 보완되거나 수집 순서를 조사해야 할 때도, 남아 있는 고아 결과가 단서가 될 수 있습니다. (기록은 조용히 사라질 때 가장 골치 아픕니다.)
내보내기 코드를 볼 때 확인할 세 가지
도구 대화 기록을 JSON이나 다른 구조화된 형식으로 옮긴다면, 아래 세 가지만 먼저 확인해 보세요.
- ID 우선 연결: 호출과 결과에 공통 ID가 있으면, 위치가 아니라 ID로 찾는가?
- FIFO 보조 연결: ID가 없을 때만 미완료 호출을 오래된 순서로 꺼내는가?
- 고아 결과 보존: 연결하지 못한 결과도 독립 항목으로 남기는가?
이 세 가지는 대단한 마법이 아니라, 불완전한 순서를 다루는 기본 자세입니다. ID는 확실한 이름표를 따르고, FIFO는 이름표가 없을 때의 질서가 되며, 고아 결과 보존은 모르는 것을 억지로 아는 척하지 않게 합니다.
다음에 AI 대화의 도구 블록이 어지럽게 섞여 보여도 너무 당황하지 마세요. 가까운 문장이 아니라 연결의 근거를 찾으면 됩니다. 이름표가 있으면 이름표를, 없으면 정한 순서를, 어느 쪽도 없으면 남겨 둔 흔적을 보세요. 그렇게 만든 기록은 단순히 보기 좋은 로그가 아니라, 다시 믿고 읽을 수 있는 대화가 됩니다.
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.