Bezpłatna rozmowa

Agent AI kwalifikujący przetargi z Bazy Konkurencyjności

Baza Konkurencyjności publikuje setki zapytań ofertowych tygodniowo, a prawie żadne nie pasuje do konkretnego dostawcy. Ten workflow odsiewa oczywisty szum lokalnie, a resztę oddaje agentowi, który zwraca decyzję GO albo NO_GO wraz z pytaniami, jakie warto zadać zamawiającemu.

Autor

Opublikowano:
Ostatnia weryfikacja:
Wersja n8n:
2.8.4

W skrócie

Co robi

  • Pobiera ogłoszenia z Bazy Konkurencyjności dwa razy dziennie
  • Odsiewa nietrafione lokalnie, zanim zapłacisz za tokeny
  • Kwalifikuje pozostałe agentem AI na GO, GO_IF albo NO_GO
  • Zwraca wagę kryterium ceny, ryzyka i pytania do zamawiającego
  • Wysyła raport na Telegram, pomijając odrzucone

Czego nie robi

  • Nie mierzy własnej skuteczności — nie ma zestawu ewaluacyjnego
  • Nie przycina listy widzianych ogłoszeń, która rośnie bez ograniczeń
  • Nie obsługuje rejestrów innych niż Baza Konkurencyjności
  • Nie podejmuje decyzji za człowieka — kwalifikuje, nie startuje

Czego wymaga

  • n8n z node'ami LangChain
  • Klucz API do modelu językowego
  • Bot Telegrama i identyfikator czatu

Agent AI kwalifikujący przetargi z Bazy Konkurencyjności

Baza Konkurencyjności to publiczny rejestr, w którym beneficjenci dotacji unijnych mają ustawowy obowiązek publikować zapytania ofertowe. Zasada konkurencyjności, bez tego nie rozliczą wydatku. Efekt: publicznie dostępna lista firm, które właśnie opisały, co kupują, za ile i do kiedy, razem z osobą kontaktową.

Problem nie polega na dostępie do danych. Polega na tym, że ogłoszeń jest za dużo, a większość nie ma z twoją firmą nic wspólnego. Ten workflow czyta listę dwa razy dziennie, odsiewa oczywiste pudła lokalnie i dopiero resztę oddaje modelowi, który zwraca decyzję w ustalonej strukturze.

Poprawiony canvas workflow, w którym każda ramka sekcji jest dość szeroka, by objąć własne node'y
Ten sam workflow po przeliczeniu geometrii. Akapit mieści się w trzech liniach, a ramka obejmuje sekcję, którą opisuje.

Dlaczego naiwna wersja się wysypuje

Odruch jest taki: pobrać listę, wrzucić wszystko do modelu, przeczytać odpowiedź. Trzy rzeczy pękają.

Koszt

Jedno ogłoszenie z pełnym kontekstem to około 12 tysięcy tokenów. Sto ogłoszeń na przebieg, dwa przebiegi dziennie, i płacisz za analizowanie zapytań o roboty budowlane i catering. Model musi widzieć wyłącznie to, co ma szansę mieć znaczenie.

Odpowiedź nadaje się do czytania, nie do przetwarzania

Model, który odpisuje prozą, wymaga parsowania tekstu. Parsowanie tekstu z LLM to najkrótsza droga do cichej awarii, bo sformułowanie zmieni się przy następnej wersji modelu i nikt tego nie zauważy.

Skrót ogłoszenia nie zawiera tego, co decyduje

Ta trzecia rzecz jest zarazem odpowiedzią na pytanie, po co tu w ogóle agent zamiast zwykłego promptu. Decyzja zależy dokładnie od pól, których w liście nie ma. Agent sam ocenia, kiedy warto dociągnąć pełny rekord, zamiast palić zapytanie na każde ogłoszenie z osobna. Prompt bez narzędzia musiałby albo zgadywać, albo pobierać wszystko.

Konfiguracja w jednym miejscu

Wszystkie endpointy, słowa kluczowe i kryteria siedzą w jednym node'cie Set. Nic nie jest zapisane na sztywno w kodzie ani w node'ach HTTP.

Endpoint listy ogłoszeń
curl "https://bazakonkurencyjnosci.funduszeeuropejskie.gov.pl/api/announcements/search?page=1&limit=100&sort=default&status%5B0%5D=PUBLISHED"

Dwa pola z tego node'a trafiają wprost do promptu agenta i to one decydują o jakości wyniku.

companyProfile
Two-person software house. We build custom web applications, internal
business systems, production and booking software. Stack: Next.js, React,
TypeScript, Python/FastAPI, PostgreSQL, Docker. We do NOT do: embedded
firmware, hardware, construction work, hardware resale, off-the-shelf
ERP licences.
dealBreakers
Price carries more than 80% of the award criteria. The subject is
off-the-shelf software or licences rather than a custom build. The scope
is mostly outside our stack and partial offers are not allowed. Required
references or track record we do not have. Deposit or security above what
a small company can post.

Profil jest parametrem, nie treścią promptu. Zmiana stacku albo apetytu na ryzyko to edycja jednego pola, nie przepisywanie instrukcji.

Filtr lokalny przed modelem

To najważniejszy element całości i zarazem najtańszy.

pre-filter.js
const strip = s => String(s ?? '').toLowerCase()
  .replace(/ł/g,'l').replace(/ą/g,'a').replace(/ę/g,'e').replace(/ś/g,'s')
  .replace(/ć/g,'c').replace(/ż/g,'z').replace(/ź/g,'z')
  .replace(/ó/g,'o').replace(/ń/g,'n');

const KEEP = String(cfg.keywords || '').split(',').map(s => strip(s.trim()));
const DROP = String(cfg.excludeKeywords || '').split(',').map(s => strip(s.trim()));

// Wykluczenia mają pierwszeństwo — sprawdzamy je PRZED słowami kluczowymi.
if (DROP.some(w => text.includes(w))) continue;
if (KEEP.length && !KEEP.some(w => text.includes(w))) continue;

Trzy decyzje warte omówienia.

Zdejmowanie polskich znaków przed porównaniem. Ogłoszenia bywają pisane bez diakrytyków, wielkimi literami, z literówkami. Po normalizacji wszystkie warianty to jedno słowo.

Fragmenty słów, nie całe słowa. Lista trzyma oprogramowan, nie oprogramowanie. Polska odmiana zjada końcówki, a dopasowanie po rdzeniu łapie wszystkie przypadki bez wypisywania form.

Wykluczenia mają pierwszeństwo przed słowami kluczowymi. To jest reguła, która robi największą różnicę.

Co przepuszczać

Frazy konkretne, opisujące usługę, a nie przedmiot:

oprogramowanie na zamowienie aplikacja webowa wdrozenie ERP system informatyczny

Co odcinać

Sprzęt i licencje sprzedawane pod nazwami brzmiącymi jak software:

system podawania palet system stabilizacji gruntu licencje na oprogramowanie dostawa sprzetu

:::

Jedno ogólne słowo w rodzaju system przeciągnie te wszystkie ogłoszenia z prawej kolumny przed agenta, a każde kosztuje około 12 tysięcy tokenów, żeby usłyszeć „nie".

Do tego dwa progi:

ParametrWartośćDlaczego
minDaysToDeadline5przygotowanie oferty w trzy dni to strata czasu
maxPerRun5twardy sufit kosztu jednego przebiegu

Deduplikacja i jej wada

dedupe.js
const store = $getWorkflowStaticData('global');
store.seen = store.seen || [];

if (!id || store.seen.includes(id)) continue;
store.seen.push(id);

Kluczem jest numeryczne id ogłoszenia. Ponowna publikacja tego samego postępowania dostaje nowe id, więc przejdzie jako nowość. To jest zamierzone — ponowione postępowanie zwykle znaczy, że pierwsze się nie rozstrzygnęło, a to jest sygnał, a nie szum.

Agent i wymuszona struktura

Sekcja agenta w kadrze: node agenta z podpiętym modelem, narzędziem tender_details i parserem strukturalnym, wszystkie wewnątrz ramki sekcji
Trzy sub-node'y podpięte do agenta: model, narzędzie HTTP do szczegółów ogłoszenia i parser wymuszający strukturę odpowiedzi.

Agent ma maxIterations: 4, czyli twardy limit pętli narzędziowych. Narzędzie tender_details to toolHttpRequest z ograniczonym wyjściem:

UstawienieWartość
dataFielddata.advertisement
fieldsToIncludeselected
fieldstitle, submission_deadline, partial_offer_allowed, orders, contact_persons

Opis narzędzia zawiera instrukcję kosztową, nie tylko funkcjonalną:

Call it at most once per announcement, and only when the decision genuinely depends on the award criteria or the real scope.

Agent czyta opis narzędzia jako część kontekstu. Wpisanie tam warunku użycia działa lepiej niż powtarzanie go w prompcie systemowym.

Schemat wyjścia

output-schema.json
{
  "type": "object",
  "properties": {
    "verdict": { "type": "string", "enum": ["GO", "NO_GO", "GO_IF"] },
    "confidence": { "type": "number" },
    "one_line_reason": { "type": "string" },
    "price_weight_percent": { "type": ["number", "null"] },
    "estimated_value": { "type": ["string", "null"] },
    "contact": { "type": ["string", "null"] },
    "condition_to_verify": { "type": ["string", "null"] },
    "scope_in_our_wheelhouse": { "type": "string" },
    "scope_outside_our_wheelhouse": { "type": "string" },
    "open_questions": { "type": "array", "items": { "type": "string" } },
    "risks": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["verdict", "one_line_reason",
               "scope_in_our_wheelhouse", "open_questions"]
}

Trzy rzeczy w tym schemacie są celowe.

Trzecia wartość GO_IF. Binarne GO/NO_GO wymusza na modelu udawanie pewności, której nie ma. GO_IF razem z polem condition_to_verify pozwala oddać stan „warto, o ile potwierdzi się X", czyli najczęstszą realną sytuację.

Typy nullowalne przy price_weight_percent, estimated_value i contact. Te dane bywają nieobecne w ogłoszeniu. Bez null w schemacie model wypełni je zmyśloną wartością, bo schemat wymaga stringa.

open_questions w polach wymaganych. To jest zawór bezpieczeństwa. Prompt mówi wprost:

Do not invent facts. If something is genuinely unknown after using the tool, say so in open_questions rather than guessing.

Wymagane pole na niewiedzę daje modelowi miejsce, w którym może przyznać się do braku danych, zamiast zgadywać w polu decyzyjnym.

Ostatnie zdanie promptu jest instrukcją o asymetrii kosztu, nie o dokładności:

Be decisive. A borderline GO that wastes two days of work is worse than an honest NO_GO.

Raport

report.js
const worth = rows.filter(r => r && r.verdict && r.verdict !== 'NO_GO');
if (!worth.length) return [];

NO_GO zostają w wyjściu workflow, ale nie idą na Telegram. To nie jest oszczędność miejsca ani ukrywanie porażek.

Rzecz w tym, że werdykt odmowny i werdykt pozytywny mają inny cykl życia. GO trzeba zobaczyć od razu, bo termin składania ofert biegnie. NO_GO czyta się raz w tygodniu, przy strojeniu listy wykluczeń, i wtedy interesuje cię nie pojedyncze ogłoszenie, tylko powtarzający się wzorzec.

Gdyby wszystko szło na czat, po tygodniu nikt by tam nie zaglądał. Kanał, na którym dziewięć wiadomości na dziesięć nie wymaga reakcji, przestaje być kanałem powiadomień.

Odrzucone werdykty nie są zresztą puste. Typowy NO_GO wygląda tak:

W naszym zakresie

Projekt i dostawa komponentów programowych: algorytmy wizyjne, interfejs, backend, integracja z API urządzenia, testy modułów.

Poza naszym zakresem

Montaż mechaniczny i elektryczny, firmware, walidacja sprzętu na miejscu, gwarancje i części zamienne, roboty budowlane.

:::

Do tego condition_to_verify mówiące, przy jakich warunkach ten sam przetarg byłby GO_IF, oraz lista pytań do zamawiającego. To jest materiał do decyzji, tylko nie na dziś.

Treść jest cięta do 3900 znaków, bo Telegram odrzuca dłuższe wiadomości.

Raport na Telegramie z jednym ogłoszeniem zakwalifikowanym jako GO, wagą kryterium ceny i listą pytań do zamawiającego
Werdykty NO_GO nie trafiają na czat. Zostają w wyjściu workflow jako materiał do strojenia listy wykluczeń.

Ograniczenia

Brak pomiaru skuteczności. Nie prowadziliśmy zestawu ewaluacyjnego z etykietami nadanymi przez człowieka, więc nie wiemy, ile było fałszywych GO, a ile przegapionych okazji. Każdy, kto to wdroży, powinien przez pierwszy miesiąc ręcznie oceniać werdykty równolegle. Bez tego nie da się odróżnić dobrego profilu firmy od modelu, który przytakuje.

Brak pomiaru kosztu. Szacunek 12 tysięcy tokenów na ogłoszenie pochodzi z etapu budowy, nie z rachunku.

Nieprzycinana lista widzianych ogłoszeń, opisana wyżej.

Filtr lokalny może odciąć trafne ogłoszenia. Wykluczenia mają pierwszeństwo, więc ogłoszenie na system produkcyjny, którego zakres wspomina o dostawie sprzętu, wypadnie przed dotarciem do modelu. To świadomy kompromis na rzecz kosztu, ale trzeba go znać.

Workflow kwalifikuje, nie decyduje. Fałszywe GO kosztuje dzień pracy. Fałszywe NO_GO kosztuje zlecenie. Werdykt jest wejściem do rozmowy, nie jej zakończeniem.

Kiedy to nie ma sensu

Jeśli startujesz w kilku przetargach rocznie i tak czytasz każdy w całości, ten workflow niczego nie doda. Ma sens od momentu, w którym samo przeglądanie listy zaczyna zajmować godzinę tygodniowo, a odrzucasz ponad 90% ogłoszeń już po tytule.

Weź to do siebie

Jak to zacytować

Igor Panek (2026). Agent AI kwalifikujący przetargi z Bazy Konkurencyjności. SEVENEDGE. https://sevenedge.pl/pl/workflows/przetargi-baza-konkurencyjnosci-agent-ai (dostęp: 29 sierpnia 2026)

Źródła

  1. Baza Konkurencyjnościdokumentacja API
  2. n8n docs — AI Agentdokumentacja n8n
  3. n8n docs — Structured Output Parserdokumentacja n8n

Kto za tym stoi

Igor Panek

Współzałożyciel SEVENEDGE

Współzałożyciel SEVENEDGE. Odpowiada za automatyzacje procesów: buduje i utrzymuje instancję n8n obsługującą wdrożenia klienckie, publikuje workflow w bibliotece n8n. Poza automatyzacjami pracuje na tym samym stacku co drugi współzałożyciel — Next.js, FastAPI, PostgreSQL.

Powiązane dokumenty

Inne przepływy, które opisaliśmy tak samo dokładnie.

7SVENZwykle odpowiada od ręki
Cześć! Jestem SVEN z SEVENEDGE. Robimy apki, SaaS i automatyzacje AI. W czym pomóc?
SVEN, maskotka SEVENEDGE, analizuje zgłoszenieSVEN, maskotka SEVENEDGE, zastanawia się przy kawieSVEN, maskotka SEVENEDGE, pracuje przy laptopie
Masz pomysł na apkę?