본 콘텐츠는 사용자의 편의를 고려해 자동 기계 번역 서비스를 사용하였습니다. 영어 원문과 다른 오류, 누락 또는 해석상의 미묘한 차이가 포함될 수 있습니다. 필요하시다면 영어 원문을 참조하시기를 바랍니다.
모든 에이전트 에는 검색이 필요합니다. 코딩 에이전트는 저장소에서 수백만 개의 파일을 검색하거나 지원 에이전트는 고객 티켓 및 내부 문서를 검색합니다. 사용 사례는 다양하지만, 근본적인 문제는 적시에 모델에 올바른 정보를 제공하는 것입니다.
직접 검색 기능을 구축하는 경우, 벡터 인덱스, 문서를 구문 분석하고 분할하는 인덱싱 파이프라인 및 데이터가 변경될 때 인덱스를 최신 상태로 유지하는 것이 필요합니다. 키워드 검색도 필요한 경우 이는 별도의 색인과 통합 논리입니다. 각 에이전트가 자체적으로 검색 가능한 컨텍스트를 필요로 한다면, 에이전트별로 모든 컨텍스트를 설정해야 합니다.
AI Search (이전의 AutoRAG)는 여러분에게 필요한 플러그 앤 플레이 검색 기본 요소입니다. 인스턴스를 동적으로 생성하고, 인스턴스에 데이터를 제공하고, Worker, Agents SDK, Wrangler CLI에서 검색을 수행할 수 있습니다. 주요 배송 기능은 다음과 같습니다.
하이브리드 검색. 동일한 쿼리에서 시맨틱 및 키워드 매칭을 모두 활성화합니다. 벡터 검색과 BM25는 병렬로 실행되며 결과가 통합됩니다. (이제 저희 블로그 검색은 AI 검색을 통해 제공됩니다. (오른쪽 상단의 돋보기 아이콘을 사용해 보세요.)
내장 스토리지 및 인덱스. 새로운 인스턴스는 자체 스토리지와 벡터 인덱스를 갖추고 있습니다. API를 통해 인스턴스에 직접 파일을 업로드하면 색인이 생성됩니다. R2 버킷을 설정할 필요가 없고, 외부 데이터 소스를 먼저 연결할 필요가 없습니다. 새로운 ai_search_namespaces 바인딩을 사용하면 런타임에 Worker에서 인스턴스를 생성하고 삭제할 수 있으므로 재배포 없이 에이전트별, 고객별, 언어별로 하나씩 인스턴스를 스핀업할 수 있습니다.
또한 이제 문서에 메타데이터를 연결하여 쿼리 시 순위를 높이고 한 번의 호출로 여러 인스턴스에 걸쳐 쿼리할 수 있습니다.
이제 이것이 실제로 무엇을 의미하는지 살펴보겠습니다.
공유 제품 문서 및 과거 해결 방법과 같은 고객별 이력이라는 두 가지 종류의 지식을 검색하는 지원 에이전트에 대해 살펴보겠습니다. 제품 문서는 너무 방대해 컨텍스트 창에 표시되지 않고, 문제가 해결될 때마다 고객에 대한 과거력도 증가하기 때문에, 상담원은 관련성을 검색해야 합니다.
Here's what that looks like with AI Search and the Agents SDK. 프로젝트 스캐폴딩으로 시작:
npm create cloudflare@latest -- --template cloudflare/agents-starter
먼저 AI Search 네임스페이스를 Worker에 바인딩합니다.
// wrangler.jsonc
{
"ai_search_namespaces": [
{ "binding": "SUPPORT_KB", "namespace": "support" }
],
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [
{ "name": "SupportAgent", "class_name": "SupportAgent" }
]
}
}
공유 제품 문서가 product-doc이라는 R2 버킷에 있다고 가정해 보겠습니다. Cloudflare 대시보드에서 support 네임스페이스 내의 버킷으로 지원되는 일회용 AI Search 인스턴스(product-knowledge 라는 이름)를 만들 수 있습니다.
이는 모든 상담원이 참조할 수 있는 공유 기술 자료 문서입니다.
고객이 새로운 호를 가지고 돌아왔을 때 이미 사용된 제품을 알면 모두의 시간을 절약할 수 있습니다. 고객별로 AI Search 인스턴스를 생성하여 이를 추적할 수 있습니다. 각각의 문제가 해결되면, 에이전트는 무엇이 잘못되었는지, 어떻게 수정했는지에 대한 요약을 저장합니다. 시간이 지나면서, 검색 가능한 과거 결의안 로그가 작성됩니다. 네임스페이스 바인딩을 사용하여 인스턴스를 동적으로 생성할 수 있습니다.
// create a per-customer instance when they first show up
await env.SUPPORT_KB.create({
id: `customer-${customerId}`,
index_method:{ keyword: true, vector: true }
});
각 인스턴스는 R2 및 Vectorize로 구동되는 자체 내장 스토리지와 벡터 인덱스를 확보합니다. 인스턴스가 비어 있는 상태로 시작되다가 시간이 지남에 따라 컨텍스트가 누적됩니다. 고객이 다음에 다시 돌아올 때 모든 정보를 검색할 수 있습니다.
고객이 소수인 경우의 네임스페이스는 다음과 같습니다.
namespace: "support"
├── product-knowledge (R2 as source, shared across all agents)
├── customer-abc123 (managed storage, per-customer)
├── customer-def456 (managed storage, per-customer)
└── customer-ghi789 (managed storage, per-customer)
이제 에이전트입니다. 이는 Agents SDK에서 AIChatAgent 를 확장하고 두 가지 도구를 정의합니다. 현재 Workers AI를 통해 Kimi K2.5를 LLM으로 사용하고 있습니다. 모델은 대화를 기반으로 도구를 호출할 시기를 결정합니다.
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { routeAgentRequest } from "agents";
import { z } from "zod";
export class SupportAgent extends AIChatAgent<Env> {
async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
// the client passes customerId in the request body
// via the Agent SDK's sendMessage({ body: { customerId } })
const customerId = options?.body?.customerId;
// create a per-customer instance when they first show up.
// each instance gets its own storage and vector index.
if (customerId) {
try {
await this.env.SUPPORT_KB.create({
id: `customer-${customerId}`,
index_method: { keyword: true, vector: true }
});
} catch {
// instance already exists
}
}
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/moonshotai/kimi-k2.5"),
system: `You are a support agent. Use search_knowledge_base
to find relevant docs before answering. Search results
include both product docs and this customer's past
resolutions — use them to avoid repeating failed fixes
and to recognize recurring issues. When the issue is
resolved, call save_resolution before responding.`,
// this.messages is the full conversation history, automatically
// persisted by AIChatAgent across reconnects
messages: await convertToModelMessages(this.messages),
tools: {
// tool 1: search across shared product docs AND this
// customer's past resolutions in a single call
search_knowledge_base: tool({
description: "Search product docs and customer history",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
// always search product docs;
// include customer history if available
const instances = ["product-knowledge"];
if (customerId) {
instances.push(`customer-${customerId}`);
}
return await this.env.SUPPORT_KB.search({
query: query,
ai_search_options: {
// surface recent docs over older ones
boost_by: [
{ field: "timestamp", direction: "desc" }
],
// search across both instances at once
instance_ids: instances
}
});
}
}),
// tool 2: after resolving an issue, the agent saves a
// summary so future agents have full context
save_resolution: tool({
description:
"Save a resolution summary after solving a customer's issue",
inputSchema: z.object({
filename: z.string().describe(
"Short descriptive filename, e.g. 'billing-fix.md'"
),
content: z.string().describe(
"What the problem was, what caused it, and how it was resolved"
),
}),
execute: async ({ filename, content }) => {
if (!customerId) return { error: "No customer ID" };
const instance = this.env.SUPPORT_KB.get(
`customer-${customerId}`
);
// uploadAndPoll waits until indexing is complete,
// so the resolution is searchable before the next query
const item = await instance.items.uploadAndPoll(
filename, content
);
return { saved: true, filename, status: item.status };
}
}),
},
// cap agentic tool-use loops at 10 steps
stopWhen: stepCountIs(10),
abortSignal: options?.abortSignal,
});
return result.toUIMessageStreamResponse();
}
}
// route requests to the SupportAgent durable object
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;
이를 통해 모델은 검색 시간과 저장 시간을 결정합니다. 검색할 때 제품 지식 및 이 고객의 과거 해결 방법을 함께 쿼리합니다. 문제가 해결되면 향후 대화에서 즉시 검색할 수 있는 요약이 저장소에 저장됩니다.
How AI Search finds what you're looking for
AI 검색은 내부적으로 다단계 검색 파이프라인을 실행하며, 모든 단계를 구성할 수 있습니다.
하이브리드 검색: 의도를 이해하고 용어와 일치하는 검색
지금까지 AI 검색은 벡터 검색만 제공했습니다. 벡터 검색은 의도를 파악하는 데는 탁월하지만, 세부적인 내용을 잃을 수 있습니다. "ERR_CONNECTION_refused 제한 시간 초과" 쿼리에서 임베딩은 연결 실패의 광범위한 개념을 캡처합니다. 하지만 사용자는 일반적인 네트워킹 문서를 찾고 있지 않습니다. “ERR_CONNECTION_refuseD”가 언급된 특정 문서를 찾고 있습니다. 벡터 검색은 정확한 오류 문자열이 포함된 페이지를 표시하지 않고도 문제 해결에 대한 결과를 반환할 수 있습니다.
키워드 검색은 이러한 격차를 해소합니다. 이제 AI 검색은 가장 널리 사용되는 검색 점수 함수 중 하나인 BM25를 지원합니다. BM25는 쿼리 용어가 나타나는 빈도, 해당 용어가 전체 코퍼스에서 얼마나 희귀한지, 문서의 길이를 기준으로 문서의 점수를 매깁니다. 특정 용어로 일치하는 항목을 보상하고, 자주 사용되는 채우기 단어에 벌칙을 주며, 문서 길이를 정규화합니다. 'ERR_CONNECTION_REFUSED 제한 시간 초과'을 검색하면 BM25는 실제로 'ERR_CONNECTION_REFUSED'를 용어로 포함하는 문서를 찾습니다. 하지만 BM25는 같은 문제를 설명하고 있다고 해도 "네트워크 연결 문제 해결"에 대한 페이지를 놓치고 있을 수 있습니다. 벡터 검색이 빛을 발하는 곳이 바로 이 점이며, 두 가지 모두가 필요한 이유입니다.
하이브리드 검색을 활성화하면 벡터와 BM25가 동시에 실행되고 결과가 통합되며 선택적으로 순위가 재조정됩니다.
BM25의 새로운 구성과 이들이 결합하는 방법을 살펴보겠습니다.
Tokenizer는 색인 생성 시 문서가 일치 가능한 용어로 변환되는 방식을 제어합니다. Porter 형태소 분석기(옵션: porter)는 단어의 형태소를 생성하므로 "running"이 "run"과 일치합니다. Trigram(옵션: trigram)은 문자 하위 문자열과 일치하므로 "conf"는 "configuration"과 일치합니다. 문서와 같은 자연어 콘텐츠에는 포터를, 부분 일치가 중요한 코드에는 trigram을 사용할 수 있습니다.
키워드 일치 모드는 쿼리 시 어떤 문서가 BM25 점수에 포함될 후보인지 제어합니다. AND는 모든 쿼리 용어가 문서에 표시되어야 한다고 요구하거나 OR은 최소 하나 이상과 일치하는 항목을 포함합니다.
융합을 통해 쿼리 시 벡터 및 키워드 결과가 최종 결과 목록으로 결합되는 방식을 제어할 수 있습니다. 상호 랭크 융합(옵션: rrf)은 점수가 아닌 순위 위치를 기준으로 병합하므로 호환되지 않는 두 점수 척도를 비교하지 않는 반면, 최대 퓨전(옵션: max)은 더 높은 점수를 받습니다.
(선택 사항) 순위를 재지정 하면 쿼리와 문서를 한 쌍으로 함께 평가하여 결과를 다시 평가하는 교차 인코더 통과가 추가됩니다. 그러면 결과에 올바른 용어가 있지만 질문에 대한 답변이 아닌 사례를 포착하는 데 도움이 될 수 있습니다.
모든 옵션을 생략하면 정상적인 기본값이 설정됩니다. 고객은 새로운 인스턴스를 생성할 때마다 중요한 사항을 유연하게 구성할 수 있습니다.
const instance = await env.AI_SEARCH.create({
id: "my-instance",
index_method: { keyword: true, vector: true },
indexing_options: {
keyword_tokenizer: "porter"
},
retrieval_options: {
keyword_match_mode: "or"
},
fusion_method: "rrf",
reranking: true,
reranking_model: "@cf/baai/bge-reranker-base"
});
검색으로는 관련성 있는 결과를 얻을 수 있지만, 관련성만으로는 항상 충분하지 않습니다. 예를 들어, 뉴스 검색에서 지난주의 기사와 3년 전의 기사가 모두 "선거 결과"와 의미론적으로 관련이 있을 수 있지만, 대부분의 사용자는 가장 최근의 결과를 원할 것입니다. 부스팅을 이용하면 문서 메타데이터를 기반으로 순위를 이동시켜 검색 외에도 비즈니스 로직을 계층화할 수 있습니다.
타임스탬프(모든 항목에 기본 제공) 또는 정의한 사용자 지정 메타데이터 필드 를 개선할 수 있습니다.
// boost high priority docs
const results = await instance.search({
query: "deployment guide",
ai_search_options: {
boost_by: [
{ field: "timestamp", direction: "desc" }
]
}
});
지원 에이전트의 예에서 제품 문서와 고객 해결 이력은 설계상 별도의 인스턴스에 있습니다. 하지만 에이전트가 질문에 답변할 때는 한 번에 두 곳의 컨텍스트가 모두 필요합니다. 교차 인스턴스 검색이 없다면 두 번의 별도 호출을 수행하고 결과를 직접 병합하게 됩니다.
네임스페이스 바인딩은 이를 처리하는 search() 메서드를 노출합니다. 인스턴스 이름 배열을 전달하고 순위 목록 하나 반환:
const results = await env.SUPPORT_KB.search({
query: "billing error",
ai_search_options: {
instance_ids: ["product-knowledge", "customer-abc123"]
}
});
결과는 인스턴스 간에 병합되고 순위가 매겨집니다. 상담원은 공유 문서와 고객 해결 기록이 별도의 장소에 있다는 것을 알거나 신경 쓸 필요가 없습니다.
How AI Search instances work
지금까지 AI 검색으로 올바른 결과를 찾는 방법을 살펴보았습니다. 이제 검색 인스턴스를 만들고 관리하는 방법을 살펴보겠습니다.
이번 릴리스 이전에 AI Search를 사용한 경우에는 설정을 알고 있을 것입니다. R2 버킷을 생성하고, 이를 AI Search 인스턴스에 연결하고, AI Search가 서비스 API 토큰을 생성하고, 계정에 프로비저닝된 Vectorize를 관리합니다. 개체를 업로드하려면 R2에 작성한 다음 동기화 작업이 실행되어 개체를 인덱싱할 때까지 기다려야 합니다.
이제 생성된 새로운 인스턴스가 다르게 작동합니다. create()를 호출하면 인스턴스가 내장된 자체 저장소 및 벡터 인덱스와 함께 제공됩니다. 파일을 업로드하면 해당 파일이 즉시 인덱싱으로 전송되고 한 번의 uploadAndpoll() API로 인덱싱 상태를 폴링할 수 있습니다. 검색이 완료되면 인스턴스를 즉시 검색할 수 있으며, 함께 연결할 외부 종속성이 없습니다.
const instance = env.AI_SEARCH.get("my-instance");
// upload and wait for indexing to complete
const item = await instance.items.uploadAndPoll("faq.md", content, {
metadata: { category: "onboarding" }
});
console.log(item.status); // "completed"
// immediately search after indexing is completed
const results = await instance.search({
// alternative way to pass in users' query other than using parameter query
messages: [{ role: "user", content: "onboarding guide" }],
});
또한 각 인스턴스는 하나의 외부 데이터 소스(R2 버킷 또는 웹 사이트)에 연결하고 동기화 일정에 따라 실행할 수 있습니다. 이는 제공된 내장 스토리지와 함께 존재할 수 있습니다. 지원 에이전트의 예에서 제품 지식 은 공유 문서용으로 R2 버킷으로 지원되는 반면, 각 고객의 인스턴스는 컨텍스트를 즉시 업로드하기 위해 내장 스토리지를 사용합니다.
The ai_search_namespaces 는 런타임에 검색 인스턴스를 동적으로 생성하는 데 활용할 수 있는 새로운 바인딩입니다. 이는 이전의 env.AI.autorag() 를 대체합니다. AI 바인딩을 통해 AI 검색에 액세스하는 API. 이전 바인딩은 Workers 호환성 날짜를 사용하여 계속 작동합니다.
// wrangler.jsonc
{
"ai_search_namespaces": [
{ "binding": "AI_SEARCH", "namespace": "example" },
]
}
네임스페이스 바인딩은 네임스페이스 수준에서 create(), delete(), list(), search() 와 같은 API를 제공합니다. 인스턴스를 동적으로(예: 에이전트별, 고객별, 테넌트별) 생성하는 경우, 이 바인딩을 사용해야 합니다.
// create an instance
const instance = await env.AI_SEARCH.create({
id: "my-instance"
});
// delete an instance and all its indexed data
await env.AI_SEARCH.delete("old-instance");
오늘을 기준으로 생성된 새로운 인스턴스에는 내장 스토리지와 벡터 인덱스가 자동으로 주어집니다.
이러한 인스턴스는 AI 검색이 오픈 베타 버전으로 제공되는 동안 무료로 사용할 수 있으며, 다음과 같은 제한이 있습니다. 웹 사이트를 데이터 소스로 사용하는 경우 Browser Run(이전의 Browser Rendering)을 사용한 웹 사이트 크롤링은 이제 기본 서비스로 제공되므로 별도로 요금이 청구되지 않습니다. 베타 이후의 목표는 각 기본 구성 요소에 대해 별도로 청구하는 대신 AI 검색에 대해 단일 서비스로 통합된 가격을 제공하는 것입니다. Workers AI와 AI Gateway 사용량은 계속 별도로 청구됩니다.
요금 청구가 시작되기 최소 30일 전에 통지하고 자세한 가격 정보를 알려드립니다.
Limit | Workers Free | Workers Paid |
|---|
계정당 AI Search 인스턴스 | 100 | 5,000 |
인스턴스당 파일 수 | 100,000 | 1백만 또는 50만(하이브리드 검색의 경우) |
최대 파일 크기 | 4MB | 4MB |
월별 쿼리 | 20,000 | 무제한 |
하루 크롤링하는 최대 페이지 수 | 500 | 무제한 |
기존 인스턴스는 어떨까요?
이번 릴리스 이전에 생성한 인스턴스는 현재와 동일하게 계속 작동합니다. R2 버킷, Vectorize 인덱스, Browser Run 사용량은 계정에 유지되며 이전과 같이 청구됩니다. 기존 인스턴스에 대한 마이그레이션 세부 정보를 곧 공유해 드리겠습니다.
검색은 에이전트가 수행할 수 있는 가장 기본적인 작업 중 하나입니다. With AI Search, AI 검색을 구현하기 위해 인프라를 구축할 필요가 없습니다. 인스턴스를 생성하고, 사용자의 데이터를 제공하면, 에이전트가 인스턴스를 검색합니다.
이 명령으로 첫 번째 인스턴스를 생성하는 것으로 오늘 시작하세요.
npx wrangler ai-search create my-search
문서를 확인하고 Cloudflare 개발자 Discord에서 무엇을 구축하고 있는지 알려주세요.