웹 검색 및 웹페이지 가져오기
웹 검색은 지식 베이스 밖의 정보를 보충하는 데 사용합니다. 에이전트는 web_search로 결과를 찾고 web_fetch로 웹페이지 본문을 읽습니다. 검색 서비스를 연동하거나 SearXNG를 배포할 수 있습니다.
'설정 → 웹 검색'에서 제공자를 선택하고 자격 증명을 입력해 연결을 테스트한 뒤 에이전트에서 해당 검색 설정을 선택합니다. 검색 결과 수는 에이전트의 최대 결과 수 설정으로 제한됩니다.
지원 검색 엔진
엔진은 internal/container/container.go에서 등록합니다.
registry.Register("duckduckgo", infra_web_search.NewDuckDuckGoProvider)
registry.Register("google", infra_web_search.NewGoogleProvider)
registry.Register("bing", infra_web_search.NewBingProvider)
registry.Register("tavily", infra_web_search.NewTavilyProvider)
registry.Register("ollama", infra_web_search.NewOllamaProvider)
registry.Register("baidu", infra_web_search.NewBaiduProvider)
registry.Register("searxng", infra_web_search.NewSearxngProvider)
registry.Register("keenable", infra_web_search.NewKeenableProvider)
registry.Register("zhipu", infra_web_search.NewZhipuProvider)
registry.Register("metaso", infra_web_search.NewMetasoProvider)
registry.Register("exa", infra_web_search.NewExaProvider)
registry.Register("bocha", infra_web_search.NewBochaProvider)
registry.Register("brave", infra_web_search.NewBraveProvider)| 엔진 | 소스 파일 | API Key 필요 여부 | 엔드포인트 | 비고 |
|---|---|---|---|---|
| DuckDuckGo | duckduckgo.go | 아니요 | HTML 가져오기 우선, API로 대체 | 무료; proxy_url 설정 가능 |
google.go | 예(engine_id도 필요) | Google Custom Search API(공식 SDK customsearch/v1) | ||
| Bing | bing.go | 예 | https://api.bing.microsoft.com/v7.0/search(하드코딩) | |
| Tavily | tavily.go | 예 | https://api.tavily.com/search(하드코딩) | |
| Ollama Web Search | ollama.go | 예 | https://ollama.com/api/web_search(하드코딩) | 최대 10개 결과 |
| Baidu Qianfan AI 검색 | baidu.go | 예 | https://qianfan.baidubce.com/v2/ai_search/web_search(하드코딩) | |
| SearXNG | searxng.go | 아니요 | 테넌트가 입력하는 base_url(자체 호스팅 인스턴스) | 주소를 직접 설정할 수 있는 유일한 엔진, SSRF 검증 필요 |
| Keenable | keenable.go | 선택 사항 | https://api.keenable.ai(하드코딩) | Key 없으면 공개 속도 제한 엔드포인트 사용, Key 있으면 제한 해제 |
| Zhipu 검색 | zhipu.go | 예 | https://open.bigmodel.cn/api/paas/v4/web_search(하드코딩), 기본 엔진 search_std | |
| Metaso | metaso.go | 예 | https://metaso.cn/api/v1/search | extra_config.scope로 리소스 범위 선택, 기본값 webpage |
| Exa | exa.go | 예 | https://api.exa.ai/search | 기본값 highlights, extra_config.include_text로 본문 가져오기 가능 |
| Bocha | bocha.go | 예 | https://api.bochaai.com/v1/web-search | extra_config.freshness, summary |
| Brave Search | brave.go | 예 | https://api.search.brave.com/res/v1/web/search | 호출별 country/freshness 전달 지원 |
'설정 → 웹 검색'에서 제공자를 선택하고 API Key를 입력해 테스트한 뒤 에이전트에서 해당 설정을 선택합니다. 현재 13개 엔진이 등록되어 있으며 실제 결과 수는 여전히 에이전트의 최대 결과 수로 제한됩니다.
| 제공자 추가 설정 | 값 |
|---|---|
| Metaso scope | webpage(기본값), document, scholar, podcast, video, image |
| Exa include_text | 문자열 불리언(예: "true"); 기본적으로 본문을 가져오지 않음 |
| Bocha freshness | noLimit(기본값), oneDay, oneWeek, oneMonth, oneYear |
| Bocha summary | 요약 요청 여부를 결정하는 문자열 불리언 |
| Brave 호출별 필터 | country/freshness는 web_search 도구 매개변수이며 아래 참고; Bocha 고정 설정 필드와 값이 다름 |
SearXNG를 제외한 모든 엔진의 엔드포인트는 하드코딩되어 테넌트가 설정할 수 없습니다. 이는 SSRF 방어의 첫 번째 조치입니다(소스 주석: Not configurable by tenants — prevents SSRF).
검색 엔진 설정(Provider 엔티티)
각 작업 공간은 여러 검색 엔진 설정 인스턴스(예: "프로덕션 Bing", "테스트 Google")를 만들 수 있습니다. 이는 web_search_providers 테이블의 WebSearchProviderEntity(internal/types/web_search_provider.go)로 저장되고 Agent가 ID로 참조합니다. 매개변수 구조 WebSearchProviderParameters:
| 이름 | 타입 | 기본값 | 설명 |
|---|---|---|---|
api_key | string | 비어 있음 | 검색 서비스 키, AES-GCM 암호화 저장; /credentials 하위 리소스로만 수정하며 응답에는 반환하지 않음 |
engine_id | string | 비어 있음 | Google Custom Search에만 필요 |
base_url | string | 비어 있음 | SearXNG 전용: 자체 호스팅 인스턴스 주소; utils.ValidateURLForSSRF로 검증하며 내부망 주소는 SSRF_WHITELIST에 추가해야 함 |
proxy_url | string | 비어 있음 | 선택적 아웃바운드 HTTP/HTTPS 프록시(트래픽 터널링만 수행하고 API 엔드포인트는 바꾸지 않음), 동일하게 SSRF 검증 적용 |
extra_config | map[string]string | nil | 제공자별 매개변수(예: Metaso scope, Exa include_text, Bocha freshness/summary) |
CRUD 라우트(RegisterWebSearchProviderRoutes, internal/router/router.go): /web-search-providers 아래 생성/조회/수정/삭제, POST /test(기존 자격 증명으로 외부 서비스 확인, Admin 권한), POST /:id/test, PUT /:id/credentials. 별도로 GET /web-search/providers가 사용 가능한 엔진 유형 목록을 반환합니다.
Agent 검색 및 페이지 읽기
web_search는 출처를 찾고 web_fetch는 선택한 페이지를 읽습니다. 사용자가 웹페이지를 지정하면 바로 읽을 수 있고, 외부 정보나 실시간 정보를 요청하면 바로 검색할 수 있습니다. 지식 베이스 검색 여부는 작업 관련성과 현재 사용 가능한 도구에 따라 결정하며, 더 이상 grep_chunks와 knowledge_search를 먼저 호출하도록 강제하지 않습니다.
web_search
호출 예시:
{"query":"Python release notes","count":5}
{"query":"Rust release notes","country":"DE","freshness":"pw","content":true}count는 결과 수를 지정하며, 범위는 1부터 현재 Agent 설정의 최대 결과 수(최대 20)까지입니다. 생략하면 기존 Agent 기본값을 사용합니다.country/freshness는 새로 추가된 Brave 제공자에서 적용됩니다. 지역은 두 글자 코드 또는ALL, 최신성은pd/pw/pm/py또는YYYY-MM-DDtoYYYY-MM-DD를 받습니다.country를 생략하면 Brave에 해당 매개변수를 전달하지 않습니다(Brave 자체 기본값은 US). 명시적인ALL은 전 세계 결과를 뜻합니다. 다른 제공자는 아직 이 필터를 지원하지 않으므로 명시적으로 전달하면 조용히 무시하지 않고 오류를 반환합니다. 값은 Brave 공식 API 문서를 참고하세요.웹 검색 설정에서 Brave Search 설정을 새로 만들고 API Key를 입력하면 기존 프록시 설정을 사용할 수 있습니다. API Key에는 기존 암호화 저장 및 독립 자격 증명 API를 적용합니다.
content는 기본적으로 꺼져 있습니다.true이면 상위 3개 결과의 본문을 병렬로 가져옵니다(배치 전체 15초 예산, 페이지마다 최대 5,000자 발췌). 나머지 결과는 검색 요약을 유지하며web_fetch로 이어 읽어야 합니다. 가져오기에 실패해도 요약은 유지하고, 전체 본문 주소는full_output_path로 반환합니다. 검색과 독립web_fetch는 이번 실행의 스냅샷을 공유하며, 짧은 시간 초과가 진행 중인 공유 가져오기를 취소하지는 않습니다.Brave의 상대적인
age는 그대로 유지하여 “2 days ago”를 정확한 게시 날짜인 것처럼 바꾸지 않습니다.기존 다중 검색 엔진, 테넌트 설정, 프록시, 차단 목록, 날짜 기능을 유지하며 provider는 여전히 Agent 실행 설정에서 결정합니다.
빈 질의, 유효하지 않은 URL, 중복 결과를 제거합니다. 최대 결과 수는 Agent 설정에서 가져오며 상한은 20입니다.
Agent 검색은 더 이상
CompressWithRAG를 호출하지 않으며, 임시 지식 베이스를 만들거나 임베딩/리랭킹 모델 또는 Redis 임시 상태에 의존하지 않습니다. 채팅 빠른 답변 파이프라인의 RAG 압축 설정은 기존 파이프라인이 계속 처리합니다.모델 출력에는 제목, 도메인, 확인 가능한 날짜, wN 페이지 ID가 포함됩니다. 요약과 provider content는 페이지 검증을 거치지 않은 검색 근거로 표시합니다. 각 구간은 최대 1,500자이며 전체 근거 예산은 16,000자입니다.
web_fetch
호출 예시:
{"items":[{"url":"w1"},{"url":"https://example.com/guide","limit":4000}]}- 알려진 wN 페이지 ID와 사용자가 제공하거나 페이지에서 발견한 HTTP(S) URL을 모두 받습니다. 짧은 ID는 모델 컨텍스트 경계에서 복원하고 UI와 저장 결과에는 실제 URL을 유지합니다.
prompt매개변수는 제거되었으며 도구 schema에는url,offset,limit만 노출됩니다. 요약을 위해 두 번째 모델을 호출하지 않고 주 Agent가 웹페이지 본문을 직접 분석합니다.- HTML은 먼저 Readability로 본문을 추출합니다. 성공하면 추출 결과 전체를 바로 변환하고, 실패한 경우에만 main/article/body로 대체하여 내부
.content노드를 다시 선택하다 인접 문단을 잃지 않도록 합니다. Markdown으로 변환한 뒤 제목, 문단, 링크, 표, 코드를 유지합니다. 상대 링크는 최종 HTTP URL을 기준으로 해석하며 임베드 리소스는 자동 다운로드하지 않습니다. - 일반 텍스트, Markdown, JSON/XML은 직접 읽어
<...>를 HTML로 간주해 버리지 않도록 합니다. 바이너리 형식은unsupported_content를 명확히 보고합니다. - HTTP를 우선 사용하고 기존 Chromium 동적 페이지 대체 경로를 유지합니다. 네트워크 요청은 공유 SSRF 검증, 안전 클라이언트, DNS pinning을 계속 거칩니다.
- 배치당 최대 8개 항목이며 정규화된 URL, offset, limit가 같으면 중복 제거합니다. 각 항목은 독립적으로
success/failed/skipped를 반환하고 일부 실패해도 성공한 본문은 유지합니다. offset은 0부터 시작하는 Unicode 문자 오프셋이며limit의 기본값과 상한은 모두 8,000입니다. 배치는 출력 예산에 따라 본문 공간을 배분하고offset,returned_chars,content_length,truncated를 반환합니다. 남은 콘텐츠가 있으면next_offset을 반환합니다.- 같은 URL과
offset=next_offset으로 이어 읽습니다. 메모리 캐시는 최대 8개 페이지 스냅샷을 보관하며 이번 실행의 문자 단위 이어 읽기에만 사용합니다. 스냅샷이 제거되면 반환된full_output_path로 같은 전체 본문을 계속 읽을 수 있어 웹페이지를 다시 가져올 필요가 없습니다. 기존 문자 단위 이어 읽기는 캐시가 만료되면 재시도 가능한snapshot_expired를 반환합니다(offset 0부터 다시 가져오거나read_file사용). 서로 다른 버전의 페이지가 이어 붙는 것을 방지합니다. 같은 배치의 이어 읽기는 최초 가져오기가 완료될 때까지 기다립니다. - 가져온 전체 Markdown은 세션이 속한 테넌트의 파일 저장소에 저장하고
web://...형식의full_output_path를 반환합니다.read_file은 샌드박스 활성화 없이 여러 턴에 걸쳐 이 파일을 읽을 수 있습니다. 본문은 이를 생성한 assistant 메시지에 바인딩되며, 읽을 때 테넌트, 세션 소유자, 세션, 메시지, 웹페이지 전용 바인딩을 확인합니다. 일반 첨부 파일을 웹페이지로 읽을 수는 없습니다. 메시지나 세션을 삭제하면 접근할 수 없으며, 저장 보존 정책은 기존 소프트 삭제 메시지 첨부 파일과 같습니다. - 저장에 실패해도 이미 가져온 본문은 버리지 않습니다. 결과에
storage_error가 포함되며 이때 이어 읽기는 이번 실행의 메모리 캐시로 제한됩니다. 저장하는 Markdown 하나의 상한은 8 MiB입니다. read_file의offset은 1부터 시작하는 행 번호이며limit는 최대 2,000행, 웹페이지 읽기는 최대 50 KiB입니다. Agent 출력 예산도 계속 적용됩니다. 매우 긴 단일 행을 만나면next_offset과next_line_offset을 반환하며,offset과line_offset으로 같은 행을 이어 읽습니다. 따라서 샌드박스가 없는 Agent도 shell을 실행할 필요가 없습니다.- Agent의 단일 페이지 다운로드 상한은 2 MiB입니다. 초과하면
body_too_large를 보고하며 조용히 잘린 HTML을 완전한 페이지로 취급하지 않습니다. 요청 시간 제한은 여전히 60초이고 Agent 가져오기는 HTTP 2xx 응답을 허용합니다. - 실패 시 안정적인 오류 코드와 재시도 가능 여부를 계속 반환합니다. 일시적 실패는 합리적으로 재시도하고, 영구적 실패는 다른 관련 출처를 선택할 수 있으며 근거가 부족하면 부족한 부분을 설명합니다. 한 번의 전체 배치 실패가 조사를 강제 종료하지는 않으며 검증 성공으로 간주해서도 안 됩니다.
- 웹 연결을 끄면 이전
allowed_tools에 두 도구가 나열되어 있더라도 런타임에 등록하지 않습니다. 실패한 웹페이지를 성공한 웹페이지 인용으로 표시하지 않습니다.
공유 가져오기 구성 요소의 빠른 답변 경로는 계속 NewPipelineFetcher를 사용합니다. 시간 제한 15초, 다운로드 상한 100 KiB, HTTP-only 및 기존 일반 텍스트 추출을 유지합니다.
동작 변경 및 회귀 검증
| 동작 | 변경 전 WeKnora Agent | 변경 후 |
|---|---|---|
| 검색 선행 조건 | 등록되지 않아도 두 KB 도구를 강제 호출 | 작업과 사용 가능한 출처에 따라 선택 |
| 검색 부가 처리 | 임시 KB에 자동 수집하여 RAG 수행 가능 | 검색 근거를 직접 반환하고 필요할 때 페이지 읽기 |
| 페이지 읽기 매개변수 | url + prompt 필수 | url, 필요에 따라 offset/limit |
| 본문 분석 | 페이지마다 모델을 다시 호출해 요약 | 주 Agent가 Markdown 직접 읽기 |
| 잘림 | 페이지별/배치 전체 한도, 뒤쪽 페이지가 비어 있을 수 있으며 이어 읽기 없음 | 페이지별 할당량 유지, 전체 본문 저장, 턴을 넘겨 행 단위 이어 읽기 |
| 실패 | 전체 배치 실패 시 검색 강제 중단 | 기존 근거 유지, 합리적 재시도 또는 출처 변경 |
다중 검색 엔진, wN 인용, 배치 호출, 테넌트 스위치, SSRF 방어를 유지합니다. Brave 어댑터로 country/freshness를 지원하며 필터 미지원 제공자는 명확한 오류를 반환합니다. 명시적인 content=true와 독립 web_fetch 모두 페이지를 읽을 수 있습니다.
본문 추출은 Go Readability / Markdown 라이브러리를 사용합니다. 로컬 HTML 회귀 예제는 전체 글의 인접 문단, 구조화된 콘텐츠, 링크 목차, 코드 들여쓰기를 포함하고 본문 구조와 링크 보존 여부를 확인하여 2차 잘라내기로 문단이 사라지는 것을 방지합니다. go test ./internal/infrastructure/web_fetch -run TestMarkdownExtractionFixtures로 검증할 수 있습니다.
docker/searxng의 역할
SearXNG는 자체 호스팅 메타 검색 엔진(여러 상위 엔진 결과 집계)입니다. WeKnora는 이를 API Key가 필요 없는 기본 선택형 검색 백엔드로 docker-compose.yml의 searxng / full profile에 포함합니다.
docker/searxng/settings.yml: 주요 사용자 정의는search.formats에서json활성화(WeKnora 백엔드는/search?format=json사용),server.limiter: false(IP 요청 제한 비활성화, 그렇지 않으면 백엔드가 제한됨; 공개 배포 시 다시 활성화하고 허용 목록 설정 필요), 진입 스크립트가secret_key를SEARXNG_SECRET환경 변수로 교체하는 것입니다.searxng-init보조 컨테이너가 템플릿을 먼저 별도 volume에 복사하여 SearXNG 진입 스크립트의 제자리 sed 수정으로 해석된 키가 저장소 작업 공간에 기록되지 않도록 합니다.- 애플리케이션 컨테이너는 기본적으로
searxng호스트 이름을 SSRF 허용 목록에 넣습니다.SSRF_WHITELIST_EXTRA=searxng,qdrant,...이므로 테넌트가base_url: http://searxng:8080을 설정하면 바로 사용할 수 있습니다. - 클라이언트 시간 제한은 12s(
defaultSearxngTimeout)로, SearXNG의outgoing.max_request_timeout: 10.0보다 조금 깁니다. 따라서 느린 상위 엔진은 클라이언트 취소가 아닌 SearXNG 측 오류로 나타납니다.ValidateSearxngBaseURL은 "저장"과 "사용" 양쪽에서 공유하여 설정 검증의 일관성을 보장합니다.
구현 및 확장 참고
아웃바운드 요청 SSRF 방어
internal/infrastructure/web_search/proxy.go의 NewSearchHTTPClient는 모든 엔진에 통일된 안전 HTTP 클라이언트를 구성합니다.
DialContext는utils.SSRFSafeDialContext를 사용합니다(연결 시 대상 IP 검증, DNS rebinding 방지).- 리디렉션은 홉마다
ssrfSafeRedirect를 거쳐ValidateURLForSSRF로 재검증하며 최대 홉 수를 넘으면 즉시 실패합니다. - 명시적인
proxy_url은 SSRF 검증을 통과해야 하며, 미설정 시ProxyFromEnvironment를 사용합니다.
인터페이스 추상화
검색 기능은 두 계층의 인터페이스로 정의합니다(internal/types/interfaces/web_search.go).
// WebSearchProvider는 웹 검색 제공자의 인터페이스를 정의합니다.
type WebSearchProvider interface {
Name() string
Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error)
}
// WebSearchService는 웹 검색 서비스의 인터페이스를 정의합니다.
type WebSearchService interface {
Search(ctx context.Context, providerID string, config *types.WebSearchConfig, query string) ([]*types.WebSearchResult, error)
CompressWithRAG(ctx context.Context, sessionID string, tempKBID string, questions []string, ...) (...)
}internal/infrastructure/web_search/registry.go는 provider 유형 -> 팩토리 함수 레지스트리를 유지하며, 인스턴스는 호출 시 테넌트 매개변수로 생성합니다.
type ProviderFactory func(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)
func (r *Registry) Register(id string, factory ProviderFactory)
func (r *Registry) CreateProvider(providerType string, params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)검색 엔진 추가 방법
internal/infrastructure/web_search/에<engine>.go를 만들고interfaces.WebSearchProvider(Name()+Search())를 구현한 뒤 팩토리 함수func New<Engine>Provider(params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error)를 제공합니다. 공식 엔드포인트는 상수로 하드코딩하고 HTTP 클라이언트는NewSearchHTTPClient(timeout, params.ProxyURL)로 구성해야 합니다.internal/types/web_search_provider.go에WebSearchProviderType상수를 추가합니다.internal/container/container.go의 등록 위치에registry.Register("<engine>", infra_web_search.New<Engine>Provider)를 추가합니다.- 키/추가 매개변수 검증이 필요하면 web search provider service의 매개변수 검증 분기에 추가하고(
ValidateSearxngBaseURL의 공유 검증 패턴 참고), 프런트엔드GET /web-search/providers목록의 표시 정보도 보충합니다. searxng_test.go/zhipu_test.go를 참고하여httptest로 상위 서비스를 모의하는 단위 테스트를 작성합니다.