에이전트 검색을 AI Gateway 한 곳으로 모으는 Cloudflare Web Search API
Cloudflare Web Search API 베타로 코딩 에이전트에 최신 문서 검색을 붙이는 법. 토큰·ai 바인딩 설정, AI Gateway를 지나는 순서, 세 제공자의 값과 ZDR, 도구 루프 코드를 다룹니다.
curl 한 번으로 시작하는 검색
Next.js 16에서 middleware.ts는 proxy.ts로 이름이 바뀌었습니다. 그 전에 학습을 마친 모델은 이 변화를 모르고, 검색이 없는 에이전트는 문서 URL을 짐작해 가져오다 404를 받고 다시 시도합니다. Web Search API는 질의 하나에 최신 결과 목록을 돌려주고, 이 호출은 AI Gateway(모델·검색 호출이 지나가는 Cloudflare의 중계 지점)를 지납니다.
REST로 부를 토큰은 대시보드 My Profile > API Tokens에서 Create Token으로 사용자 지정 토큰을 만들고, 권한에 Account > Workers AI > Read와 Account > AI Gateway > Read를 넣습니다. 게이트웨이는 계정마다 있는 default를 씁니다.
curl https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/ai/websearch/ \ --request POST \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ --header "Content-Type: application/json" \ --data '{"query": "Next.js 16 middleware proxy.ts", "provider": "ceramic", "limit": 5, "options": {"gateway": {"id": "default"}}}'값은 AI Gateway 크레딧에서 제공자 정가 그대로 빠지고 Cloudflare 몫은 붙지 않으니, 크레딧을 먼저 충전해 둡니다. 이미 제공자 계정이 있으면 키를 게이트웨이에 저장해 두고(BYOK) 제공자에게 청구받을 수도 있습니다.
요청 하나가 지나는 순서
Worker에서는 Wrangler 설정에 ai 바인딩(코드에서 env.AI로 Cloudflare 기능을 부르게 해 주는 설정)을 두고 env.AI.websearch()를 부릅니다.
{ "name": "web-search-worker", "main": "src/index.ts", "compatibility_date": "2026-10-11", "ai": { "binding": "AI" }}REST든 Worker든 요청은 같은 길을 지납니다.
에이전트가 게이트웨이 ID와 질의 Next.js 16 middleware proxy.ts를 보냅니다.
게이트웨이에 저장된 제공자 키를 찾고, 없으면 AI Gateway 크레딧으로 냅니다.
고른 제공자에게 넘깁니다. provider를 비우면 Ceramic.ai입니다.
결과를 같은 모양으로 맞춰 돌려주고, 요청은 게이트웨이 로그에 남습니다.
키를 고르는 둘째 단계에서 실수가 나기 쉽습니다. byokAlias를 적었는데 그 이름의 키가 게이트웨이에 없으면 크레딧으로 넘어가지 않고 400으로 실패합니다. 적지 않으면 default 이름의 키를 먼저 찾고, 그것도 없을 때만 크레딧을 씁니다.
제공자 API를 바로 쓰던 때와 달라지는 것
모델 호출을 이미 AI Gateway로 보내고 있다면, 검색도 그 게이트웨이의 키·크레딧·로그를 함께 씁니다.
- API 키앱 환경 변수게이트웨이에 저장
- 청구제공자마다 따로AI Gateway 크레딧(정가)
- 로그앱에서 따로모델 호출과 같은 로그
- 제공자 바꾸기호출·파서 수정provider 값 하나
- 응답 모양제공자마다 다름url·title·description
저장한 키는 요청에 실리지 않고 Secrets Store로 암호화됩니다. 대신 결과 모양을 맞추는 만큼 제공자 고유 옵션은 줄어, Exa는 auto 검색, Linkup은 fast 깊이로만 불립니다.
세 제공자 중 무엇을 고르나
1,000회 값으로 보면 Exa가 Ceramic.ai의 28배($7 ÷ $0.25)입니다.
| Ceramic.ai | Exa | Linkup | |
|---|---|---|---|
| provider 값 | ceramic(기본) | exa | linkup |
| 1,000회당 | $0.25 | $7 | $5 |
| ZDR | 지원 | 미지원 | 지원 |
| 결과에 붙는 것 | 긴 설명 최대 8,000자 | 관련 하이라이트 | 출처 스니펫 |
ZDR(Zero Data Retention)은 제공자가 요청 내용을 저장하지 않는다는 약속입니다. 코딩 에이전트의 질의에는 사내 저장소나 내부 API 이름이 섞이기 쉬우니, 그럴 때는 Exa를 빼고 Ceramic.ai나 Linkup을 씁니다. Exa는 질의에 맞는 발췌를 설명 자리에 넣어 주므로 짧은 근거를 모델 컨텍스트에 바로 넣을 때 맞습니다. 응답 모양이 같아서 provider 값만 바꿔 같은 질의를 세 번 보내면 바로 견줄 수 있습니다. 결과를 저장하거나 화면에 다시 보여 줄 때는 제공자마다 따로 있는 약관을 따릅니다.
검색 결과를 모델에 다시 넘기는 도구 루프
함수 도구는 모델이 "이 함수를 이 인자로 불러 달라"고 요청하게 하는 방식입니다. 모델에 web_search 도구를 주고, 모델이 질의를 담아 부르면 env.AI.websearch()를 돌려 결과를 role: "tool" 메시지로 붙인 뒤 모델을 다시 부릅니다. 모델이 더 검색하지 않고 답을 내면 끝나고, 검색이 끝없이 이어지지 않게 세 번에서 끊습니다.
export interface Env { AI: Ai;}const MODEL = "@cf/google/gemma-4-26b-a4b-it";const webSearch = { type: "function", function: { name: "web_search", description: "Search the web for current information.", parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"], }, },} as const;export default { async fetch(request, env): Promise<Response> { const messages: ChatCompletionMessageParam[] = [ { role: "user", content: "Next.js 16에서 middleware.ts는 어떻게 바꾸나?" }, ]; for (let turn = 0; turn < 3; turn++) { const res = await env.AI.run( MODEL, { messages, tools: [webSearch] }, { gateway: { id: "default" } }, ); const msg = res.choices[0].message; if (!msg.tool_calls?.length) return Response.json(msg.content); messages.push({ role: "assistant", content: msg.content, tool_calls: msg.tool_calls }); for (const call of msg.tool_calls) { if (call.type !== "function") continue; const { query } = JSON.parse(call.function.arguments); const search = await env.AI.websearch({ gatewayId: "default", query, limit: 5 }); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(await search.json()), }); } } return new Response("검색을 세 번 넘게 요청했습니다", { status: 500 }); },} satisfies ExportedHandler<Env>;Gemma 4는 도구 호출을 choices[0].message.tool_calls에 담고 인자를 JSON 문자열로 주므로 JSON.parse로 질의를 꺼냅니다. limit은 10까지 받습니다. 모델이 tool 메시지로 받아 읽는 응답은 아래 필드로 이뤄집니다.
Web Search API를 쓸 때와 제공자 API를 바로 쓸 때
이미 Workers AI나 AI Gateway로 모델을 부르고 있거나, 한 에이전트에서 제공자를 바꿔 가며 견줄 생각이면 Web Search API를 씁니다. 키·청구·로그가 모델 호출과 같은 자리에 남고, 제공자를 바꿀 때는 provider 값만 고칩니다. 반대로 Exa의 auto 말고 다른 검색 유형이나 Linkup의 fast 말고 다른 깊이처럼 제공자 고유 옵션이 필요하면, 그 제공자의 API를 바로 부릅니다.