연결된 도구가 늘면서 사용자 요청보다 도구 설명이 길어졌습니다. 검색으로 바꾸고 탐색 범위에 허용 목록을 걸었더니, 사용자가 캔버스에서 직접 연결한 도구까지 함께 빠졌습니다. 화면 목록에는 이름이 보이는데 실제 호출 경로에는 없는 상태였습니다. "보인다"와 "부를 수 있다"가 왜 다른 문제인지 정리합니다.
도구를 쓸 수 있게 만드는 것과 전부 펼쳐 보여주는 것은 다른 일이었습니다
도구를 몇 개 연결했을 때는 전체 JSON 스키마를 한꺼번에 프롬프트에 넣어도 괜찮습니다. 저희도 그렇게 시작했습니다.
연결 노드와 플랫폼 카탈로그가 늘자 사용자 요청보다 도구 설명이 더 길어졌습니다. 실제로 쓰지 않을 정의가 컨텍스트의 대부분을 차지했습니다.
여기서 "도구를 보여줄 것인가"라는 질문 자체가 잘못돼 있었습니다. 그 질문에는 답이 두 개뿐입니다. 전부 보여주거나 감추거나입니다.
실제로 나눠야 할 것은 정보의 수준이었습니다.
존재를 안다
→ 이름과 설명으로 후보를 찾는다
→ 필요한 스키마를 승격한다
→ 권한과 입력을 확인한다
→ 실제 도구를 호출한다
현재 실행에 늘 필요한 소수의 내장 도구는 이름과 설명, 입력 스키마를 처음부터 줍니다. 사용자가 캔버스에서 직접 연결한 도구는 선택했다는 사실 자체가 중요하므로 이름과 짧은 설명을 먼저 보여줍니다. 모델이 필요하다고 판단하면 검색 도구가 상세 스키마를 현재 실행 도구로 승격합니다. 플랫폼의 대규모 카탈로그는 색인에서 후보를 찾은 뒤 고른 정의만 불러옵니다.
이것은 도구를 감추는 방식이 아닙니다. 목록은 그대로 주고 상세 정의만 사용할 때 펼치는 방식입니다. 모델이 읽는 토큰을 줄이면서 사용자가 연결한 의도는 유지합니다.
검색도 이름에 단어가 들어 있는지 보는 방식으로 만들지 않았습니다. 도구 이름과 설명을 정규화하고 단어 빈도와 희소성으로 후보를 정렬해 자연어 요청과 가까운 도구를 찾습니다.
컨텍스트가 커진다는 신고를 받으면 대체로 무엇을 뺄지 먼저 봅니다. 그 전에 볼 것은 지금 한 덩어리로 다루는 정보가 사실 몇 층인지입니다. 층이 나뉘면 뺄 필요 없이 미룰 수 있습니다.
검색 범위를 좁혔더니 연결한 도구까지 사라졌습니다
검색 경로가 생기자 어떤 소스를 탐색해도 되는지 제한할 필요가 생겼습니다. 실행마다 플랫폼 카탈로그에서 자동 검색할 수 있는 소스를 허용 목록으로 정했습니다.
7월 초 기본 경로를 연결된 소스 중심으로 좁혔습니다. 불필요한 플랫폼 도구가 후보에 섞이지 않게 하려는 것이었습니다.
여기서 회귀가 났습니다. 허용 목록 필터가 수집 단계 앞쪽에 놓이면서 연결된 소스까지 함께 제외됐습니다.
증상이 까다로웠습니다. 화면의 짧은 목록과 설명에는 도구가 그대로 보입니다. 그런데 모델이 그 이름으로 검색하면 결과가 없고 호출도 안 됩니다.
원인은 저희가 두 가지를 한 목록으로 합쳐 다룬 데 있었습니다.
플랫폼 카탈로그 시스템이 검색 후보로 제안하는 자원 → 허용 목록으로 제한
연결 도구 사용자가 이번 실행에 명시적으로 포함한 자원 → 제한 대상이 아님
'사용자가 연결함'과 '플랫폼 전체를 탐색해도 됨'은 다른 권한입니다. 한 목록으로 합치면 도구를 보여주려고 검색 범위를 과하게 열거나, 검색을 제한하려다 연결 도구까지 잃습니다. 저희는 후자를 했습니다.
빈 허용 목록의 의미도 이때 분명해졌습니다. 허용 목록이 비었다는 것은 플랫폼 카탈로그에서 허용된 소스가 없다는 뜻이지, 사용자가 캔버스에 연결한 도구도 없다는 뜻이 아닙니다. 선택 주체가 다릅니다.
7월 초 수집 순서를 고쳐 명시적으로 연결된 소스는 플랫폼 카탈로그 필터와 독립적으로 유지하게 했습니다. 도구 이름을 하나씩 예외 처리하는 대신 출처를 나눈 이유가 여기에 있습니다.
권한 목록을 설계할 때 한 목록에 서로 다른 출처를 담으면 이런 회귀가 반복됩니다. 목록에 들어오는 경로가 둘이면 나가는 규칙도 둘이어야 합니다.
출력 노드를 도구로 보여줬더니 모델이 전달을 결정하게 됐습니다
도구 공개와 같은 시기에 출력 노드의 책임도 정리했습니다.
이메일이나 메시지 노드를 일반 도구처럼 모델에게 보여주면 모델이 전송 여부를 결정합니다. 부르지 않으면 전달이 빠지고, 캔버스 실행기가 후속 노드를 다시 실행하면 같은 결과가 두 번 나갑니다.
전달이라는 부수효과를 두 실행 경로가 함께 소유하고 있었습니다.
먼저 종단 실행을 하네스 내부의 메타 도구로 모았습니다. 최종 결과가 확정된 뒤 한 실행 소유자가 하네스 결과에서 이어진 종단 체인을 순서대로 처리합니다. 일반 캔버스 경로에서는 이 노드들을 제외해 한 실행 경로 안에서 중복 실행되지 않게 했습니다.
이것을 분산 시스템의 정확히 한 번 전달 보장이라고 부르지는 않았습니다. 프로세스 안의 두 실행 경로가 같은 부수효과를 함께 소유하지 않게 한 계약입니다.
그런데 내부에서 도구로 실행한다고 해서 모델에게도 호출 도구로 보여줄 필요는 없었습니다.
모델에 주는 정보는 출력 채널이라는 자원 정보로 바꿨습니다. 모델은 결과가 이메일인지 메시지인지, 어떤 형식을 기대하는지 알고 최종 답을 구성합니다. 실제 전송 여부와 시점은 하네스가 정합니다.
모델에게 보이는 것 출력 채널의 이름·용도·기대 형식
하네스가 소유한 것 최종 결과 확정 · 종단 실행 · 중복 방지
여기서 확인한 것은 내부 실행 모델과 모델 컨텍스트가 같은 표현일 필요가 없다는 점입니다. 시스템 안에서 도구인 것이 모델에게도 도구여야 한다고 생각했던 것이 처음의 전제였고, 그 전제를 놓자 문제가 풀렸습니다.
채널 메타데이터에는 답을 만드는 데 필요한 값만 넣습니다. 파라미터 이름이 일반적인 비밀 패턴과 맞는 값은 제외합니다. 이 필터가 모든 제품의 민감 필드를 자동으로 아는 것은 아니므로 새 채널을 연결할 때 제품 정책과 회귀 테스트로 보강해야 합니다.
이름이 보인다고 부를 수 있는 것은 아니었습니다
앞의 회귀가 알려준 것을 규칙으로 굳혔습니다. 도구의 공개는 한 번에 일어나는 사건이 아니라 세 시점입니다.
이름 공개 연결된 도구의 존재와 짧은 설명을 준다
스키마 공개 모델이 선택하면 상세 정의를 승격한다
실행 권한 호출 직전에 입력 자료형과 사용자 권한, 실행 정책을 다시 본다
셋을 하나로 묶으면 앞의 두 가지 실패 중 하나가 납니다. 다 열면 컨텍스트가 커지고 권한이 넓어지며, 다 닫으면 사용자가 연결한 것도 못 씁니다.
검증도 목록 한 화면에서 끝내지 않았습니다. 연결 도구가 짧은 목록에 들어오는지, 모델이 정확한 이름을 검색해내는지, 그 스키마가 다음 호출 컨텍스트에 합류하는지, 실행 요청이 원래 도구 소스까지 도달하는지를 하나의 경로로 이어서 확인했습니다.
빈 허용 목록 조건도 회귀 테스트로 남겼습니다. 연결 도구는 남되 허용하지 않은 플랫폼 도구가 섞이지 않아야 합니다.
출력 채널도 자원 목록에 나타나는지만 보지 않았습니다. 확정된 최종 답이 연결된 종단에 전달되는지, 한 실행 경로에서 중복 처리되지 않는지, 일반 캔버스 경로와 하네스 경로가 같은 실행 소유권을 지키는지, 비밀값이 메타데이터에서 빠지는지를 확인했습니다.
기능이 화면에 보이는지로 검증을 끝내면 이 편의 회귀 같은 것을 놓칩니다. 목록에 나타나는 것과 그것을 실제로 쓸 수 있는 것 사이에 몇 단계가 있는지 세어보면, 검증해야 할 지점도 그만큼입니다.
컨텍스트는 읽히는 문서가 아니라 인터페이스였습니다
이 편에서 다룬 문제들은 표면적으로 서로 달라 보입니다. 도구 설명이 너무 길고, 허용 목록이 연결 도구를 지우고, 출력 노드를 누가 실행할지 겹칩니다.
전부 같은 자리에서 나왔습니다. 저희가 컨텍스트를 모델에게 읽히는 문서로 다루고 있었다는 것입니다. 문서라면 필요한 것을 다 넣어두면 되고, 목록은 하나면 되고, 시스템 안의 도구는 모델에게도 도구입니다.
컨텍스트가 모델과 실행 환경 사이의 인터페이스라면 이야기가 달라집니다. 인터페이스에는 무엇을 언제 노출하고 어느 시점에 권한을 확인하는지가 들어갑니다.
도구는 존재를 먼저 알리고 스키마는 필요할 때 펼치며 권한은 실행 직전에 다시 판단합니다. 출력 채널은 목적지를 알려주되 전달 책임은 하네스가 갖습니다.
마지막 편에서는 이 실행 경로에 교훈을 다시 넣습니다. 넣어보니 저장과 회상이 다 동작하는데도 결과가 같은 자리를 맴돌았고, 이유는 점수가 엉뚱한 후보에 붙어 있었기 때문이었습니다.

