AI Agents · Lekcja

Ranking i filtrowanie wyników wyszukiwania

Ocenianie trafności, usuwanie duplikatów i wybieranie najlepszych wyników do kontekstu.

Lekcja 2 z 413 kroki

Ranking i filtrowanie wyników wyszukiwania to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.

Dlaczego ranking i filtrowanie mają znaczenie

Interfejs API wyszukiwania zwraca od 5 do 10 wyników, ale nie wszystkie są równie trafne, wiarygodne lub przydatne dla zadania agenta. Przekazywanie surowych wyników bezpośrednio do LLM marnuje tokeny kontekstu i może wprowadzać szum lub dezinformację.

Ranking i filtrowanie poprawiają stosunek sygnału do szumu, zanim wyniki trafią do LLM.

Ocena trafności za pomocą BM25

BM25 (Best Match 25) to klasyczny algorytm rankingowania tekstu, który ocenia dokumenty na podstawie pokrycia słów kluczowych z zapytaniem. Sprawdza się dobrze w dopasowaniu leksykalnym — gdy zapytanie i dokument zawierają te same słowa.

Należy zainstalować za pomocą pip install rank-bm25.

from rank_bm25 import BM25Okapi

def rank_with_bm25(query, results):
    # Tokenize: lowercase and split into words
    tokenized_results = [
        r['content'].lower().split()
        for r in results
    ]
    bm25 = BM25Okapi(tokenized_results)

    query_tokens = query.lower().split()
    scores = bm25.get_scores(query_tokens)

    # Sort results by score descending
    ranked = sorted(
        zip(scores, results),
        key=lambda x: x[0],
        reverse=True
    )
    return [(score, result) for score, result in ranked]

Ocena trafności za pomocą embeddingów

BM25 dopasowuje tylko dokładne słowa. Podobieństwo embeddingów uwzględnia znaczenie semantyczne — dlatego „Python web development” i „building websites with Django” mogą uzyskać wysokie podobieństwo mimo użycia różnych słów.

Należy użyć podobieństwa cosinusowego między embeddingiem zapytania a embeddingami wyników.

import numpy as np
import openai
import os

client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

def embed(text):
    resp = client.embeddings.create(
        model='text-embedding-3-small',
        input=text[:8000]
    )
    return np.array(resp.data[0].embedding)

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def rank_by_embedding(query, results):
    q_emb = embed(query)
    scored = []
    for r in results:
        r_emb = embed(r['content'][:1000])
        score = cosine_similarity(q_emb, r_emb)
        scored.append((score, r))
    return sorted(scored, key=lambda x: x[0], reverse=True)

Ranking hybrydowy: BM25 + embeddingi

BM25 i ocena na podstawie embeddingów uwzględniają różne aspekty trafności. Ranking hybrydowy łączy obie oceny za pomocą średniej ważonej, wykorzystując zalety zarówno dopasowania leksykalnego, jak i semantycznego.

def hybrid_rank(query, results, bm25_weight=0.4, embed_weight=0.6):
    # Get BM25 scores (normalized 0-1)
    bm25_scored = rank_with_bm25(query, results)
    max_bm25 = max(s for s, _ in bm25_scored) or 1
    bm25_norm = {r['url']: s / max_bm25 for s, r in bm25_scored}

    # Get embedding scores
    embed_scored = rank_by_embedding(query, results)
    embed_norm = {r['url']: s for s, r in embed_scored}

    # Combine
    combined = []
    for r in results:
        url = r['url']
        score = (bm25_weight * bm25_norm.get(url, 0) +
                 embed_weight * embed_norm.get(url, 0))
        combined.append((score, r))

    return sorted(combined, key=lambda x: x[0], reverse=True)

Deduplikacja według adresu URL

Wyniki wyszukiwania często zawierają niemal identyczne elementy: ten sam artykuł z wielu serwisów przedrukowujących treści albo tę samą stronę z różnymi parametrami URL. Deduplikacja usuwa je przed przekazaniem wyników do LLM.

from urllib.parse import urlparse, urlunparse

def normalize_url(url):
    parsed = urlparse(url)
    # Remove query params and fragment (tracking params, etc.)
    clean = parsed._replace(query='', fragment='')
    return urlunparse(clean).rstrip('/')

def deduplicate_results(results):
    seen_urls = set()
    unique = []
    for r in results:
        url = normalize_url(r.get('url', ''))
        if url not in seen_urls:
            seen_urls.add(url)
            unique.append(r)
    return unique

# Also deduplicate by content similarity (near-duplicate detection)
def deduplicate_by_content(results, min_unique_ratio=0.7):
    unique = [results[0]] if results else []
    for candidate in results[1:]:
        cand_words = set(candidate['content'].lower().split())
        is_duplicate = False
        for kept in unique:
            kept_words = set(kept['content'].lower().split())
            overlap = len(cand_words & kept_words) / max(len(cand_words), 1)
            if overlap > (1 - min_unique_ratio):
                is_duplicate = True
                break
        if not is_duplicate:
            unique.append(candidate)
    return unique

if __name__ == '__main__':
    demo_results = [
        {'url': 'https://example.com/a?utm_source=x'},
        {'url': 'https://example.com/a'},
        {'url': 'https://example.com/b'},
    ]
    unique = deduplicate_results(demo_results)
    print(f'{len(demo_results)} results -> {len(unique)} unique')
    for r in unique:
        print(' -', r['url'])

Ocena jakości domeny

Wynik z docs.python.org jest bardziej wiarygodny niż wynik z przypadkowego bloga. Należy przypisać mnożniki jakości poszczególnym poziomom domen i uwzględnić je w końcowym rankingu.

DOMAIN_QUALITY = {
    # Tier 1 — authoritative (1.3x boost)
    'docs.python.org': 1.3,
    'developer.mozilla.org': 1.3,
    'arxiv.org': 1.3,
    'github.com': 1.2,
    'stackoverflow.com': 1.2,
    # Tier 2 — good (1.0x, no change)
    # Tier 3 — low quality (penalty)
    'pinterest.com': 0.3,
    'quora.com': 0.5,
    'wikihow.com': 0.6
}

def get_domain_multiplier(url):
    from urllib.parse import urlparse
    domain = urlparse(url).netloc.lower().replace('www.', '')
    return DOMAIN_QUALITY.get(domain, 1.0)  # default: no change

def apply_domain_boost(scored_results):
    boosted = []
    for score, r in scored_results:
        multiplier = get_domain_multiplier(r.get('url', ''))
        boosted.append((score * multiplier, r))
    return sorted(boosted, key=lambda x: x[0], reverse=True)

if __name__ == '__main__':
    scored = [(1.0, {'url': 'https://pinterest.com/x'}), (1.0, {'url': 'https://docs.python.org/x'})]
    for score, r in apply_domain_boost(scored):
        print(f"{r['url']}: boosted score {score:.2f}")

Filtrowanie wyników niskiej jakości

Niektóre wyniki mają niską jakość ze względów strukturalnych, niezależnie od domeny: są zbyt krótkie, aby były użyteczne, zawierają głównie tekst nawigacyjny albo pochodzą ze stron wymagających logowania. Należy odfiltrować je przed rankingowaniem.

MIN_CONTENT_LENGTH = 200  # characters

LOW_QUALITY_SIGNALS = [
    'sign in to view',
    'please log in',
    'subscribe to read',
    '404 not found',
    'access denied',
    'this content is for members only'
]

def is_quality_result(result):
    content = result.get('content', '')

    # Too short
    if len(content) < MIN_CONTENT_LENGTH:
        return False

    # Paywall / access barrier detected
    content_lower = content.lower()
    for signal in LOW_QUALITY_SIGNALS:
        if signal in content_lower:
            return False

    return True

def filter_results(results):
    return [r for r in results if is_quality_result(r)]

if __name__ == '__main__':
    demo_results = [
        {'content': 'Please log in to view this article which has plenty of extra padding text here.'},
        {'content': 'A' * 250},
    ]
    kept = filter_results(demo_results)
    print(f'{len(demo_results)} results -> {len(kept)} passed quality filter')

Skracanie wyników w ramach budżetu kontekstu

Nawet po odfiltrowaniu może pozostać 5 wysokiej jakości wyników po 600 znaków każdy — łącznie daje to 3000 znaków. Należy ustalić, ile wyników mieści się w budżecie kontekstu LLM-a, i odpowiednio je skrócić.

MAX_CONTEXT_CHARS = 4000
MAX_SNIPPET_CHARS = 600

def truncate_for_context(ranked_results, budget=MAX_CONTEXT_CHARS):
    selected = []
    used_chars = 0

    for score, result in ranked_results:
        content = result.get('content', '')[:MAX_SNIPPET_CHARS]
        entry = f"Source: {result['title']}\nURL: {result['url']}\nContent: {content}"
        entry_len = len(entry)

        if used_chars + entry_len > budget:
            break

        selected.append(result)
        used_chars += entry_len

    return selected

ranked_results = [
    (0.9, {'title': 'Doc A', 'url': 'http://a', 'content': 'x' * 800}),
    (0.7, {'title': 'Doc B', 'url': 'http://b', 'content': 'y' * 800}),
]
budget = 1000
selected = truncate_for_context(ranked_results, budget=budget)
print(f'Selected {len(selected)} results within {budget}-char budget')

Formatowanie wyników w prompcie LLM-a

Po ustaleniu rankingu, usunięciu duplikatów i skróceniu wyników należy sformatować je jako numerowaną listę w prompcie LLM-a. Numerowane źródła ułatwiają modelowi cytowanie ich w odpowiedzi.

def format_results_for_prompt(results):
    lines = ['Here are relevant search results:\n']
    for i, r in enumerate(results, 1):
        lines.append(f'[{i}] {r["title"]}')
        lines.append(f'    URL: {r["url"]}')
        lines.append(f'    {r.get("content", "")[:400]}')
        lines.append('')
    lines.append('Use these sources to answer the question. Cite sources as [1], [2], etc.')
    return '\n'.join(lines)

# Usage in agent prompt
formatted = format_results_for_prompt(selected_results)
response = llm_call(
    system='You are a research assistant.',
    user=f'{formatted}\n\nQuestion: {user_question}'
)

Buforowanie wyników wyszukiwania

To samo zapytanie może być powtarzane w różnych sesjach lub pętlach agenta. Należy buforować wyniki wyszukiwania z krótkim TTL-em (np. 1 godzinę), aby ograniczyć koszty API i przyspieszyć odpowiedzi na powtarzające się zapytania.

import hashlib
import time

search_cache = {}  # In production: use Redis or disk cache
CACHE_TTL = 3600  # 1 hour

def cached_search(query, **kwargs):
    cache_key = hashlib.md5(query.encode()).hexdigest()
    entry = search_cache.get(cache_key)

    if entry and (time.time() - entry['ts']) < CACHE_TTL:
        print('Search cache hit')
        return entry['results']

    results = client.search(query=query, **kwargs)
    search_cache[cache_key] = {
        'results': results,
        'ts': time.time()
    }
    return results

Pełny potok filtrowania i rankingowania

Wszystkie kroki należy połączyć w jednej funkcji potoku: pobieranie → filtrowanie wyników niskiej jakości → deduplikacja → ranking → zastosowanie wzmocnienia domeny → skrócenie w ramach kontekstu → formatowanie na potrzeby promptu.

def search_and_rank(query, max_context_chars=4000):
    # 1. Fetch
    raw = cached_search(query, max_results=8)
    results = raw.get('results', [])

    # 2. Filter quality
    results = filter_results(results)

    # 3. Deduplicate
    results = deduplicate_results(results)

    # 4. Rank (BM25 fast path — save embedding costs)
    scored = rank_with_bm25(query, results)

    # 5. Domain boost
    scored = apply_domain_boost(scored)

    # 6. Truncate to context budget
    selected = truncate_for_context(scored, budget=max_context_chars)

    # 7. Format
    return format_results_for_prompt(selected)

Sprawdzenie wiedzy

Jaka jest główna zaleta rankingu hybrydowego (BM25 + embeddingi) w porównaniu z użyciem samego BM25?

Podsumowanie: ranking i filtrowanie wyników wyszukiwania

Surowe wyniki wyszukiwania wymagają przetworzenia, zanim trafią do LLM-a. Potok obejmuje: filtrowanie wyników niskiej jakości (zbyt krótkich lub objętych płatnym dostępem) → deduplikację według adresu URL i treści → ranking według BM25 i/lub podobieństwa embeddingów → zastosowanie wzmocnień jakości domen → skrócenie do budżetu kontekstu → sformatowanie jako numerowanych źródeł.

Buforowanie wyników wyszukiwania ogranicza koszty API w przypadku powtarzających się zapytań. Numerowane cytowania w prompcie umożliwiają LLM-owi przypisywanie twierdzeń do konkretnych źródeł.

Bezpłatny start

Ucz się AI Agents dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
60
Lekcje
239

Często zadawane pytania

Czy lekcja „Ranking i filtrowanie wyników wyszukiwania” jest bezpłatna?

Tak — pełny tekst „Ranking i filtrowanie wyników wyszukiwania” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.

Co nauczysz się w „Ranking i filtrowanie wyników wyszukiwania”?

Ocenianie trafności, usuwanie duplikatów i wybieranie najlepszych wyników do kontekstu. Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Agents?

Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „Ranking i filtrowanie wyników wyszukiwania”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?

Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Tavily i SerpAPI do wyszukiwania przez agenta
  2. Ranking i filtrowanie wyników wyszukiwania
  3. Wzorzec pętli pogłębionego researchu
  4. Łączenie wyszukiwania internetowego z RAG
← Powrót do AI Agents