
한 답에 웹 페이지, 사내 문서, DB에서 조회한 레코드, 커넥터(MCP)가 돌려준 티켓이 함께 실리면 출처를 어떻게 달지가 문제가 됩니다. 흔한 방법은 도구 결과마다 근거 번호를 매기고 모델이 본문에 [1] 같은 표지를 달게 하는 것입니다. 그런데 DB 조회 한 번으로 장애 티켓 14건을 표로 보여 준 답이라면, 14건이 번호 하나를 나눠 씁니다. 사용자가 그 번호를 눌러도 어느 티켓으로 가야 할지 정할 수 없습니다.
OwlAI에서 모델이 아는 출처 규칙은 하나입니다. 근거로 쓴 자료의 URL을 본문에 링크로 씁니다. 출처 목록을 짓고 남기면 안 되는 링크를 푸는 일은 서버가 정해진 규칙으로 합니다. 이 글은 출처를 URL 하나로 모은 이유, 서버가 목록을 짓는 순서, 그리고 모델이 쓴 URL이 진짜인지를 답 본문이 아니라 바깥으로 나가는 요청에서 확인하는 이유를 설명합니다.
근거 번호는 도구 결과를 가리키고, 사용자는 페이지를 연다
근거 번호 방식에도 장점이 있습니다. 번호가 도구 결과 하나에 묶여 있으니, 어느 문장이 어느 도구 결과에 기대는지 서버가 따라갈 수 있습니다. 검색형 AI 서비스에서 익숙한 모양이기도 합니다.
문제는 번호가 가리키는 대상이 도구 결과라는 점입니다. 웹 검색 결과 하나는 페이지 하나라서 번호와 페이지가 맞아떨어집니다. DB 조회 결과는 다릅니다. 행이 여러 개이고, 결과 자체에는 열어 볼 주소가 없습니다. 레코드마다 따로 표지([R1])를 두면 번호 체계가 둘이 됩니다. 모델은 두 체계를 함께 지켜야 하고, 서버는 표지마다 실제 도구 결과와 맞는지 대조하는 규칙을 따로 둬야 합니다. 화면에서는 표지 종류마다 눌렀을 때 가는 곳이 달라집니다.
사용자가 출처로 하는 일은 하나입니다. 누르고, 원문을 엽니다. 열어 볼 곳이 없는 표지는 눌러도 아무 일이 일어나지 않습니다. 그래서 출처의 단위를 도구 결과가 아니라 열어 볼 수 있는 주소로 정했습니다.
누를 수 있는 출처는 모두 주소다
답에 실리는 출처는 네 종류이고, 모두 주소로 바꿀 수 있습니다.
- 웹 페이지와 커넥터의 결과에는 원래 URL이 있습니다.
- DB 레코드는 관리자가 바로가기 링크 화면에 등록한 규칙으로 사내 업무 시스템의 상세 페이지 주소가 됩니다. 규칙은 상세 페이지 주소의 틀과 언제 쓸지 적은 설명입니다. 모델이 틀의 값 자리에 주문 번호나 티켓 키를 넣어 주소를 만듭니다.
- 사내 문서는 문서 검색 결과에 앱 안 문서 주소(
/documents/<문서 ID>)가 함께 실려 옵니다. 호스트가 없는 주소라 웹에서 열든 데스크톱 앱에서 열든 같은 문서가 앱 안 미리보기로 열립니다.
모델에게는 넷 모두 그냥 URL입니다. 근거 번호도, 레코드 표지도, 출처 종류도 모델이 알 필요가 없습니다. 장애 티켓 14건을 표로 보여 준 답에서는 티켓 키 14개가 각자 자기 상세 페이지로 링크됩니다.
링크 문법 두 가지가 화면 모양을 정한다
모델이 쓰는 링크 문법은 두 가지이고, 화면은 문법만 보고 모양을 정합니다.
| 문법 | 화면에 보이는 모양 | 쓰는 곳 |
|---|---|---|
[글](주소) |
그 글자에 걸린 밑줄 링크 | 주문 번호, 티켓 키, 문서 이름처럼 값 자체를 누르게 할 때 |
<주소> 또는 맨 주소 |
문장 끝의 출처 칩(사이트 아이콘과 페이지 제목이 붙은 작은 단추) | 그 문장을 뒷받침하는 웹 페이지 |
표 안의 링크는 주소만 썼어도 밑줄 링크로 그립니다. 답이 다 나온 뒤 표 위젯으로 바뀌는 표의 칸과 모양을 맞추기 위해서입니다.
링크가 문장 어디에 있는지, 어느 도구에서 왔는지를 보고 모양을 고르는 규칙 대신 문법을 기준으로 골랐습니다. 「문장 끝에 있으면 칩」 같은 추정 규칙은 문장 중간이나 표 바로 아래에 출처를 단 답처럼 경계 사례가 나올 때마다 다시 고쳐야 합니다. 문법은 모델이 쓴 그대로라 화면과 모델이 같은 규칙을 봅니다.
프롬프트는 도구 결과의 URL을 고치지 않고 그대로 옮기라고 지시하고, 실제 URL이 없으면 링크를 만들지 말고 없다고 말하라고 지시합니다.
서버는 최종 답에서 출처 목록을 짓는다
답이 확정되면 서버의 후처리가 「참고한 정보」 목록을 짓습니다. 모델에게 답을 다시 쓰게 하지 않고, 같은 답에는 늘 같은 목록이 나옵니다. 채팅, 딥 리서치, 플레이북 실행이 모두 같은 후처리를 거칩니다. 순서는 다음과 같습니다.
- 본문의 링크와 표 위젯의 링크 칸을 첫 등장 순으로 모읍니다. 코드 블록 안의 주소는 링크로 보지 않습니다.
- 주소가 같은 링크는 하나로 합칩니다. 목록은 100개까지이고, 넘는 링크는 본문에만 남습니다.
- 항목마다 제목을 고릅니다. 링크 글을 먼저 쓰고, 그게 비었으면 이번 턴(질문 하나를 받아 답을 내기까지의 한 번) 도구 결과에 있던 페이지 제목이나 문서 이름을, 그것도 없으면 호스트 이름을 씁니다.
제목 고르기에는 예외가 하나 있습니다. 같은 주소가 한 번이라도 출처 칩으로 나왔으면 링크 글을 제목으로 쓰지 않습니다. [ORD-1234](주소) 처럼 값에 건 링크와 같은 주소의 칩이 함께 있으면, 칩 제목이 「ORD-1234」가 되기 때문입니다. 그 경우 페이지 제목이나 호스트 이름으로 넘어갑니다.
남기면 안 되는 링크는 풀어서 글자만 남깁니다. 하나는 웹 주소도 앱 안 문서 주소도 아닌 링크입니다. 다른 하나는 가려 둔 전송 주소의 참조입니다. 커넥터가 돌려주는 사전 인증 다운로드 주소에는 접근 토큰이 들어 있어서, 서버가 이 주소를 짧은 참조로 바꿔 모델에게 보여 줍니다. 이 참조를 답 본문의 링크로 살리려면 원래 주소로 되돌려야 하고, 그러면 토큰이 답 본문과 DB에 남습니다. 그래서 링크로 만들지 않습니다.
답 본문의 URL은 글자 대조로 거르지 않는다
모델이 쓴 URL이 도구 결과에 실제로 있었는지 글자를 대조해 거르면 지어낸 링크를 막을 수 있을 것처럼 보입니다. 실운영 환경에서 949턴의 답 본문에 나온 URL 5,400개가 어디서 왔는지 따져 보면 이 필터의 대가가 보입니다.
91%는 도구 결과에 있던 글자 그대로였습니다. 정상적으로 조립한 주소가 330개였는데, 이슈 트래커의 「주소/browse/키」처럼 서비스 관례를 따라 모델이 만든 주소가 249개, 바로가기 링크 규칙의 주소 틀로 만든 주소가 81개였습니다. 깨졌거나 지어낸 주소는 약 50개, 0.9%였습니다.
글자 대조 필터를 넣으면 0.9%를 잡으려고 정상적으로 조립한 330개를 함께 지웁니다. 특히 바로가기 링크는 도구 결과에 없는 주소를 만드는 기능이라 대조 필터와 정면으로 부딪힙니다. 호스트 이름을 맞춰 주는 정규화도 두지 않았습니다. 설치마다 공개 주소가 다르고, 어느 호스트가 맞는지 정할 설정이 없습니다.
진위 확인은 바깥으로 나가는 요청에서 한다
답 본문의 링크와 도구가 여는 URL은 위험이 다릅니다. 본문의 링크는 사용자가 보고 나서 누릅니다. 도구가 여는 URL은 사용자 확인 없이 바로 바깥으로 요청이 나갑니다. 문서에 숨겨 둔 지시(간접 프롬프트 인젝션)가 모델에게 대화 내용을 실은 주소를 만들게 하면, 그 요청만으로 대화 내용이 유출됩니다.
그래서 URL을 확인하는 일은 요청이 나가는 쪽에 둡니다. 웹 페이지를 읽거나 URL에서 파일을 받는 도구는 대화에 이미 나온 주소만 열고, 모델이 그 주소에 값을 덧붙이지 못하게 합니다(자세한 규칙은 「도구 결과에 나온 주소도 모델이 먼저 썼다면 열지 않는다」).
출처 칩의 사이트 아이콘도 같은 이유로 제한합니다. 화면이 사이트마다 아이콘을 직접 받으면 사용자 IP와 본 페이지의 도메인이 그 사이트로 갑니다. 모델이 쓴 아무 주소의 아이콘을 받으면, 인젝션이 호스트 이름에 대화 내용을 실어 내보낼 수도 있습니다. 그래서 화면은 외부 그림을 받지 않고 OwlAI 서버에 아이콘을 묻습니다. 서버는 이번 턴의 웹 검색·웹 추출 결과에 나온 주소에만 아이콘을 붙이고, 정해진 아이콘 서비스 한 곳에서 도메인 단위로 받아 캐시합니다. 그 밖의 링크에는 기본 지구본 아이콘이 붙습니다.
설계에서 고른 것
답 본문의 링크에는 글자 대조 필터를 걸지 않았습니다. 대가로 깨진 링크가 드물게 본문에 남습니다. 사용자가 보고 누르는 링크라 이 대가를 받아들였고, 사용자 확인 없이 나가는 요청에는 주소 확인을 걸었습니다.
출처는 「본문에 링크로 쓴 것」으로 정했습니다. 답이 실제로 기대는 자료만 목록에 오릅니다. 조회는 했지만 링크로 쓰지 않은 자료는 목록에 없고, 무엇을 조회했는지는 사고 과정의 도구 단계에서 따로 봅니다.
이 정의에는 약점이 있습니다. 모델이 링크를 빠뜨리면 출처도 함께 빠집니다. 딥 리서치에서는 조사원이 보고서에 정확한 링크를 적어도 총괄이 최종 보고서로 옮기지 않으면 목록에 오르지 않습니다. 서버가 빠진 링크를 채워 넣지는 않습니다. 총괄이 일부러 뺀 발견이라면 그 링크도 빠지는 것이 맞기 때문입니다. 대신 레코드를 여는 값은 표 안에서도 링크로 옮기라고 프롬프트로 지시하고, 최종 보고서가 옮기지 않은 조사원 링크 수를 턴마다 로그로 남겨 그 지시가 얼마나 지켜지는지 봅니다.
도구 결과마다 출처의 생김새가 다른 에이전트라면 이 방식을 그대로 옮길 수 있습니다. 출처마다 열어 볼 주소를 만들 방법을 먼저 찾고, 모델에게는 링크를 쓰라는 규칙 하나만 주고, 목록과 제목은 서버가 짓습니다. 주소가 진짜인지는 그 주소로 요청이 나가는 곳에서 확인합니다.
어떤 문제를 해결하나요?
메모리와 검색의 다른 글

엔지니어링 노트 · 메모리와 검색
개인 메모리는 매 턴 싣고, 고칠 때는 통째로 다시 쓴다
사용자가 「이슈 트래커에서 제 계정은 이 ID예요. 앞으로 내 이슈는 이걸로 찾아 주세요」라고 하면 모델은 「기억하겠습니다」라고 답합니다.

엔지니어링 노트 · 메모리와 검색
대화에서 바로잡은 컬럼 뜻, 테이블 메모로 다음 사람에게 싣기
고객 테이블의 상태 컬럼 STATCD 에는 1, 2, 9가 들어 있습니다. 스키마만 봐서는 9가 해지라는 것을 알 수 없어서, 「활성 고객 수」를 물으면 모델은 해지 고객까지 세어 보고서에 올립니다.

엔지니어링 노트 · 메모리와 검색
조사 붙은 검색어는 전문 검색에 안 걸린다, 낱말을 OR로 잇는다
테이블 메모에 「반품 주문은 ORDRTN 테이블에 따로 쌓인다」라고 적혀 있고, 사용자가 「지난달 반품 주문 건수를 알려 줘」라고 묻습니다.
