0Pricing
AI Agents · Lekcja

Budowanie agenta wzbogaconego o wiedzę

Od początku do końca: łączenie encji → zapytanie do grafu → synteza odpowiedzi.

Budowanie agenta wzbogaconego o wiedzę to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 4 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.

Czym jest agent wzbogacony o wiedzę?

Agent wzbogacony o wiedzę wykorzystuje bazę wiedzy do wzbogacania swoich odpowiedzi. Gdy pojawia się pytanie, agent wyodrębnia encje, wyszukuje je w grafie wiedzy, znajduje odpowiednie dokumenty za pomocą wyszukiwania wektorowego i przekazuje cały kontekst do LLM, aby uzyskać bogatą odpowiedź opartą na wiarygodnych danych.

Kompletny pipeline wyszukiwania

Pipeline agenta: 1 odbierz pytanie → 2 wyodrębnij encje → 3 wyszukaj kontekst encji w grafie → 4 wyszukaj odpowiednie dokumenty wektorowo → 5 połącz cały kontekst → 6 LLM wygeneruje odpowiedź.

from dataclasses import dataclass, field
from typing import List, Dict, Any

@dataclass
class RetrievalContext:
    question: str
    entities: List[str] = field(default_factory=list)
    graph_context: Dict[str, Any] = field(default_factory=dict)
    vector_documents: List[Dict] = field(default_factory=list)
    combined_context: str = ''
    answer: str = ''
    sources_used: List[str] = field(default_factory=list)

# The agent will populate this object as it works through the pipeline
ctx = RetrievalContext(question='What AI projects is Sam Altman known for?')
print('RetrievalContext created:', ctx.question)

Krok 1: Ekstrakcja encji

Wyodrębnij nazwane encje z pytania. Encje te staną się punktami odniesienia podczas wyszukiwania w grafie. Używaj spaCy ze względu na szybkość, a LLM w trudniejszych przypadkach lub w przypadku encji specyficznych dla domeny.

import spacy

nlp = spacy.load('en_core_web_sm')

def extract_question_entities(question: str) -> List[str]:
    doc = nlp(question)
    entities = list({
        ent.text for ent in doc.ents
        if ent.label_ in ['PERSON', 'ORG', 'GPE', 'PRODUCT', 'WORK_OF_ART']
    })
    return entities

def extract_entities_with_llm_fallback(question: str, client) -> List[str]:
    spacy_entities = extract_question_entities(question)
    
    if spacy_entities:
        return spacy_entities
    
    # Fallback to LLM for questions where spaCy finds nothing
    import json
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{
            'role': 'user',
            'content': f'Extract named entities (people, companies, technologies) from: "{question}". Return JSON: {{"entities": ["name1", "name2"]}}'
        }],
        response_format={'type': 'json_object'}
    )
    result = json.loads(response.choices[0].message.content)
    return result.get('entities', [])

question = 'What AI projects is Sam Altman known for?'
entities = extract_question_entities(question)
print('Extracted entities:', entities)

Krok 2: Wyszukiwanie w grafie

Dla każdej wyodrębnionej encji odpytaj graf wiedzy, aby uzyskać jej właściwości i relacje. Dostarcza to LLM faktów kontekstowych, których nie może on wiarygodnie wygenerować.

from neo4j import GraphDatabase

driver = GraphDatabase.driver('bolt://localhost:7687', auth=('neo4j', 'password'))

def get_rich_entity_context(entity_name: str) -> dict:
    with driver.session() as session:
        # Get entity + its relationships
        result = session.run(
            'MATCH (n {name: $name}) '
            'OPTIONAL MATCH (n)-[r]->(target) '
            'RETURN n, labels(n) AS labels, '
            'COLLECT({rel: type(r), target_name: target.name, target_label: labels(target)}) AS outgoing '
            'LIMIT 1',
            name=entity_name
        )
        record = result.single()
        if not record:
            return {'found': False, 'name': entity_name}
        
        return {
            'found': True,
            'name': entity_name,
            'labels': record['labels'],
            'properties': dict(record['n']),
            'connections': [
                c for c in record['outgoing'] if c.get('target_name')
            ][:10]
        }

def format_entity_context_for_llm(entity_ctx: dict) -> str:
    if not entity_ctx.get('found'):
        return f'No knowledge graph data found for "{entity_ctx["name"]}"'
    
    props = entity_ctx.get('properties', {})
    connections = entity_ctx.get('connections', [])
    conn_strs = [f"{c['rel']} -> {c['target_name']}" for c in connections[:5]]
    
    return (
        f"Entity: {entity_ctx['name']} ({', '.join(entity_ctx['labels'])})\n"
        f"Properties: {props}\n"
        f"Relationships: {'; '.join(conn_strs)}"
    )

Krok 3: Wyszukiwanie wektorowe

Uruchom wyszukiwanie wektorowe z użyciem oryginalnego pytania, aby znaleźć najbardziej istotne semantycznie dokumenty w bazie wiedzy. Dokumenty te dostarczają dowodów wspierających odpowiedź.

import chromadb
import openai

client = openai.OpenAI(api_key='sk-...')
chroma_client = chromadb.Client()
collection = chroma_client.get_or_create_collection('knowledge_base')

def vector_search(query: str, top_k: int = 5) -> list:
    response = client.embeddings.create(
        model='text-embedding-3-small',
        input=query
    )
    query_embedding = response.data[0].embedding
    
    results = collection.query(
        query_embeddings=[query_embedding],
        n_results=top_k,
        include=['documents', 'metadatas', 'distances']
    )
    
    documents = []
    for i in range(len(results['ids'][0])):
        documents.append({
            'text': results['documents'][0][i],
            'metadata': results['metadatas'][0][i],
            'distance': results['distances'][0][i],
            'relevance': 1 - results['distances'][0][i]  # Convert distance to similarity
        })
    
    return documents

print('Vector search function defined')

Krok 4: Łączenie kontekstu

Połącz kontekst grafu i dokumenty wektorowe w jeden dobrze ustrukturyzowany ciąg znaków kontekstu. Kolejność ma znaczenie: najpierw fakty z grafu (wysoka precyzja), a następnie dokumenty wektorowe (szeroki zakres).

def combine_context(question: str, graph_contexts: dict, vector_docs: list) -> str:
    sections = []
    
    # Graph facts section
    if graph_contexts:
        graph_parts = ['### Knowledge Graph Facts']
        for entity_name, ctx in graph_contexts.items():
            graph_parts.append(format_entity_context_for_llm(ctx))
        sections.append('\n'.join(graph_parts))
    
    # Vector documents section
    if vector_docs:
        doc_parts = ['### Relevant Documents']
        for i, doc in enumerate(vector_docs[:4]):
            title = doc.get('metadata', {}).get('title', f'Document {i+1}')
            text = doc['text'][:800]  # Limit per document
            relevance = doc.get('relevance', 0)
            doc_parts.append(f'**{title}** (relevance: {relevance:.2f})\n{text}')
        sections.append('\n'.join(doc_parts))
    
    context = '\n\n'.join(sections)
    # Total context budget: ~8000 tokens ~ 32000 chars
    if len(context) > 32000:
        context = context[:32000]
    
    return context

Krok 5: Generowanie odpowiedzi przez LLM

Przekaż połączony kontekst do LLM jako komunikat systemowy lub kontekst użytkownika. Poleć mu korzystać z dostarczonych informacji i wskazywać źródło każdego faktu.

import openai

client = openai.OpenAI(api_key='sk-...')

def generate_answer(question: str, combined_context: str) -> str:
    system_prompt = (
        'You are a knowledgeable assistant. Answer the question using ONLY the provided context. '
        'Cite your sources by mentioning whether a fact came from the knowledge graph or a specific document. '
        'If the context does not contain enough information, say so clearly.'
    )
    
    user_message = (
        f'Context:\n{combined_context}\n\n'
        f'Question: {question}\n\n'
        'Please answer based on the context above.'
    )
    
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[
            {'role': 'system', 'content': system_prompt},
            {'role': 'user', 'content': user_message}
        ],
        temperature=0.1  # Low temperature for factual answers
    )
    return response.choices[0].message.content

Pełny orkiestrator agenta

Funkcja orkiestratora spina wszystkie kroki. Przyjmuje pytanie, uruchamia pełny pipeline i zwraca ustrukturyzowany wynik zawierający odpowiedź oraz wykorzystany kontekst.

async def knowledge_augmented_agent(question: str) -> RetrievalContext:
    ctx = RetrievalContext(question=question)
    
    # Step 1: Extract entities
    ctx.entities = extract_question_entities(question)
    print(f'Entities: {ctx.entities}')
    
    # Steps 2 & 3: Graph + Vector in parallel
    import asyncio
    from concurrent.futures import ThreadPoolExecutor
    
    executor = ThreadPoolExecutor(max_workers=4)
    loop = asyncio.get_event_loop()
    
    async def graph_step():
        contexts = {}
        for entity in ctx.entities:
            context = await loop.run_in_executor(executor, get_rich_entity_context, entity)
            if context.get('found'):
                contexts[entity] = context
        return contexts
    
    async def vector_step():
        return await loop.run_in_executor(executor, vector_search, question, 5)
    
    ctx.graph_context, ctx.vector_documents = await asyncio.gather(
        graph_step(), vector_step()
    )
    
    # Step 4: Combine
    ctx.combined_context = combine_context(
        question, ctx.graph_context, ctx.vector_documents
    )
    
    # Step 5: Generate answer
    ctx.answer = generate_answer(question, ctx.combined_context)
    
    return ctx

Obsługa pustych wyników wyszukiwania

Gdy baza wiedzy nie zawiera istotnych informacji, agent powinien jasno o tym powiedzieć, zamiast halucynować. Przed wywołaniem LLM sprawdź, czy wyszukiwanie zwróciło użyteczne wyniki.

def check_retrieval_quality(graph_contexts: dict, vector_docs: list, threshold: float = 0.7) -> dict:
    has_graph = len(graph_contexts) > 0
    
    # Filter vector docs below relevance threshold
    high_quality_docs = [d for d in vector_docs if d.get('relevance', 0) >= threshold]
    
    return {
        'has_graph_context': has_graph,
        'graph_entity_count': len(graph_contexts),
        'vector_doc_count': len(high_quality_docs),
        'retrieval_quality': 'high' if (has_graph or len(high_quality_docs) >= 2) else 'low',
        'usable_docs': high_quality_docs
    }

def answer_with_fallback(question: str, graph_contexts: dict, vector_docs: list) -> str:
    quality = check_retrieval_quality(graph_contexts, vector_docs)
    
    if quality['retrieval_quality'] == 'low':
        return (
            f'I don\'t have enough information in my knowledge base to answer '
            f'"{question}" confidently. '
            'Please ensure relevant documents are indexed or the knowledge graph '
            'contains the required entities.'
        )
    
    context = combine_context(question, graph_contexts, quality['usable_docs'])
    return generate_answer(question, context)

Buforowanie wyników wyszukiwania

Buforuj wyszukiwania encji i wyniki wyszukiwania wektorowego, aby unikać powtarzających się kosztownych wywołań API dla podobnych pytań. Używaj TTL, aby okresowo odświeżać nieaktualne dane.

import hashlib
import json
from datetime import datetime, timedelta

class RetrievalCache:
    def __init__(self, ttl_minutes: int = 60):
        self.cache = {}
        self.ttl = timedelta(minutes=ttl_minutes)
    
    def _key(self, namespace: str, value: str) -> str:
        return hashlib.md5(f'{namespace}:{value}'.encode()).hexdigest()
    
    def get(self, namespace: str, value: str):
        key = self._key(namespace, value)
        entry = self.cache.get(key)
        if entry and datetime.now() - entry['ts'] < self.ttl:
            return entry['data']
        return None
    
    def set(self, namespace: str, value: str, data):
        key = self._key(namespace, value)
        self.cache[key] = {'data': data, 'ts': datetime.now()}

cache = RetrievalCache(ttl_minutes=30)

def cached_graph_lookup(entity: str) -> dict:
    cached = cache.get('graph', entity)
    if cached:
        print(f'Cache hit for entity: {entity}')
        return cached
    result = get_rich_entity_context(entity)
    cache.set('graph', entity, result)
    return result

print('Retrieval cache initialized')

Logowanie i obserwowalność

Loguj każdy etap wyszukiwania, aby móc zdiagnozować, dlaczego odpowiedź była dobra lub zła. Zapisuj, które encje znaleziono, ile dokumentów pobrano, ich wyniki trafności oraz końcową odpowiedź.

import logging
import json
from datetime import datetime

logger = logging.getLogger('ka_agent')

def log_agent_run(ctx: 'RetrievalContext', duration_ms: float):
    logger.info(json.dumps({
        'timestamp': datetime.utcnow().isoformat(),
        'question': ctx.question,
        'entities_found': ctx.entities,
        'graph_entities_resolved': list(ctx.graph_context.keys()),
        'vector_docs_retrieved': len(ctx.vector_documents),
        'vector_doc_relevances': [
            round(d.get('relevance', 0), 3)
            for d in ctx.vector_documents
        ],
        'context_length_chars': len(ctx.combined_context),
        'answer_length_chars': len(ctx.answer),
        'duration_ms': round(duration_ms, 1)
    }))

print('Observability logging configured')

Sprawdzenie wiedzy: Agent wzbogacony wiedzą

Proszę sprawdzić, czy rozumie Pan/Pani, jak budować agentów wzbogaconych wiedzą.

Podsumowanie agenta wzbogaconego wiedzą

Agent wzbogacony wiedzą łączy ekstrakcję encji, przechodzenie po grafie i wyszukiwanie wektorowe w potok, który dostarcza LLM bogatego kontekstu opartego na danych. Rezultatem są dokładniejsze odpowiedzi, w których rzadziej występują halucynacje, a które są poparte rzeczywistymi danymi z bazy wiedzy. Najważniejsze dodatki to buforowanie, obsługa pustych wyników wyszukiwania oraz logowanie strukturalne zapewniające obserwowalność.

Często zadawane pytania

Czy lekcja „Budowanie agenta wzbogaconego o wiedzę” jest bezpłatna?

Tak — pełny tekst „Budowanie agenta wzbogaconego o wiedzę” 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 „Budowanie agenta wzbogaconego o wiedzę”?

Od początku do końca: łączenie encji → zapytanie do grafu → synteza odpowiedzi. Ć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 4 z 4.

Ile czasu zajmuje lekcja „Budowanie agenta wzbogaconego o wiedzę”?

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. Ekstrakcja encji na potrzeby grafów wiedzy
  2. Zapytania Neo4j z narzędzi agenta
  3. Łączenie wyszukiwania wektorowego i grafowego
  4. Budowanie agenta wzbogaconego o wiedzę
← Powrót do AI Agents