O mneSkúsenostiBlogKontakt
LinkedInGitHubGitLabWhatsApp
Všetky články

4 min čítania

Privátne vyhľadávanie v kóde: parameter, ktorý nikto nenapísal

repowiselitellmollamaembeddingsself-hosted

Sémantické vyhľadávanie vo firemnom kóde, pri ktorom nič neopustí firemnú sieť a odpovede píše 120B model na firemnom hardvéri. Štyri nástroje, dva modely, dve pasce a jedno pole v každej požiadavke, ktoré na mojej strane nikto nenapísal.

Zostava

Repowise indexuje repozitár: parsuje súbory, stavia graf symbolov, reže chunky a výsledok vystavuje cez MCP, takže každý agent môže zavolať search_codebase a get_answer namiesto čítania polovice repozitára. LanceDB, ktorá je v ňom pribalená, ukladá vektory na disk priamo v repozitári. Ollama spúšťa otvorené váhy, a to na dvoch miestach. Na notebooku nomic-embed-text mení každý chunk na 768-rozmerný vektor, dosť malý na to, aby bežal na notebooku. Na firemnom serveri gpt-oss:120b píše wiki stránky a z nájdených chunkov skladá odpovede. LiteLLM stojí pred serverom ako tímová smerovacia proxy a hovorí OpenAI-kompatibilným API: jeden endpoint, jeden kľúč, jeden zoznam modelov, a výmena backendu za názvom modelu je zmena v konfigurácii, ktorú na strane klienta nikto nepostrehne.

Toto je zostava, pri ktorej som skončil. Nie je to tá, s ktorou som začal.

Repozitár
ParsovanieRepowise na notebooku: súbory, symboly, graf závislostí
Písanie wiki stránokgpt-oss:120b cez LiteLLM na firemnom serveri
Embeddingnomic-embed-text v lokálnej Ollame: z každej stránky a chunku vznikne 768-rozmerný vektor
UloženieLanceDB na disku, priamo v repozitári
Agent položí otázkudovtedy sa nič nedeje
VyhľadanieRepowise na notebooku, dve vyhľadávania zlúčené do jedného poradia
fulltextvektory
Odpoveďgpt-oss:120b cez LiteLLM ju napíše z nájdených chunkov

Prvé štyri kroky bežia raz a potom prírastkovo po každom commite. Posledné dva bežia zakaždým, keď sa agent opýta. Generovanie wiki išlo cez LiteLLM od prvého dňa a nikdy nerobilo problémy. Nič z tohto toku neopustí firemnú sieť a krok, ktorý beží najčastejšie, embedding, neopustí ani notebook.

Najprv: dvere nemajú meno

Kým sa čokoľvek z toho mohlo rozbehnúť, hostname proxy sa nedal preložiť. Notebook bol na VPN do prostredia v Azure, nie do firemnej siete, a tá VPN zachytáva každý DNS dopyt a posiela ho resolveru v Azure, ktorý o interných firemných menách nikdy nepočul. Windows vie jednu konkrétnu príponu nasmerovať na vlastný resolver: pravidlom v Name Resolution Policy Table. Jeden riadok v administrátorskom PowerShelli, bez reštartu, prežije aj opätovné pripojenie VPN:

Add-DnsClientNrptRule -Namespace ".internal.example" -NameServers "10.0.0.2","10.0.0.3"
nslookup litellm.internal.example

Pasca 1: parameter, ktorý nikto nenapísal

Moja prvá verzia posielala cez LiteLLM aj embeddingy, na bge-m3 na serveri. Jedny dvere pre všetko, o jednu vec menej, ktorá by bežala na notebooku. Vyhľadávanie súborov a symbolov fungovalo. Prvá otázka, ktorá potrebovala význam, vrátila toto:

unsupported parameter
Indexed 0 items (N failed)

Iný klient proti rovnakému endpointu a rovnakému modelu fungoval. Porovnal som obe požiadavky a našiel jediný rozdiel: encoding_format.

Nikdy som ho nenastavil. Nastavuje ho za vás klient OpenAI. Ak v embeddings.create() nepoviete, ako chcete mať čísla zakódované, vyžiada si base64 a bajty si potom sám dekóduje späť na floaty. Menšia odpoveď, rozumný default proti vlastným serverom OpenAI.

# vnútri balíka openai, nie vo vašom kóde
if not is_given(encoding_format):
    params["encoding_format"] = "base64"

LiteLLM prekladá každé pole požiadavky do API daného backendu. Embed endpoint Ollamy také pole nemá, takže pre LiteLLM je každá hodnota encoding_format neznáma a neznáma štandardne znamená odmietnutá. Explicitné "float" mení hodnotu, nie prítomnosť poľa, a kontroluje sa práve prítomnosť. Prepínač „nič neposielať“ neexistuje.

Klient má dve vrstvy: pohodlné metódy ako create(), ktoré skladajú telo požiadavky a dopĺňajú defaulty, a pod nimi holé post(), ktoré pozná base URL, autentifikáciu, opakovania a timeouty, ale do tela, ktoré mu podáte, nepridá nič. Nežiaduce pole žije v hornej vrstve, tak som upravil embedder v Repowise, aby to pole vynechal.

# embedder v Repowise po úprave: volá priamo HTTP vrstvu klienta
response = client.post(
    "/embeddings",
    body={"model": model, "input": chunks},
    cast_to=CreateEmbeddingResponse,
)
vectors = [item.embedding for item in response.data]

cast_to rozparsuje odpoveď do rovnakej typovanej štruktúry, akú by vrátilo create(), takže Repowise si nič nevšimne. Žiadne kódovanie si nevyžiadal, takže Ollama odpovedá obyčajnými floatmi. Model bol vymeniteľný. Tvar požiadavky nie.

Pasca 2: šírka

Požiadavky už prechádzali a LanceDB odmietla každý vektor ako nesprávny tvar. Repowise poznalo modely OpenAI a pre všetko ostatné predpokladalo 1536, lenže bge-m3 vracia 1024. REPOWISE_EMBEDDING_DIMS=1024 to rieši.

Výsledok: 1024-rozmerné vektory, zhruba 1,6 sekundy na položku cez LiteLLM, nula zlyhaní. Fungovalo to.

Oprava, ktorú som nezvolil, a tá, ktorú áno

LiteLLM má presne na toto nastavenie: drop_params: true zahodí každý parameter, ktorému backend nerozumie. Jeden riadok v konfigurácii proxy, žiadna úprava kódu, problém preč. Nepožiadal som oň. Proxy zdieľajú všetky tímy a proxy, ktorá prehĺta neznáme polia, by skryla moju chybu aj chyby všetkých ostatných: preklep v názve parametra alebo parameter, ktorý backend naozaj potrebuje.

Úprava kódu nechala cenu chyby tam, kde vznikla. Stále to však bola úprava, a to balíka, ktorý najbližší pip install -U potichu vráti do pôvodného stavu, takže z nej vyrástol skript s režimom --check, ktorý beží pri štarte shellu a ozve sa skôr, než príde ďalšie Indexed 0 items.

Potom som si položil otázku, ktorú som si mal položiť ako prvú: prečo vlastne chunky kódu putujú po sieti? Embedder so 768 rozmermi beží na notebooku bez problémov. 1433 stránok embedovaných lokálne, nula zlyhaní, žiadna proxy v ceste, žiadna cesta na server a späť pre každý chunk, žiadny parameter, o ktorý by sa bolo treba hádať, žiadny skript, ktorý treba udržiavať. Jediné, čo ešte potrebuje server, je 120B model, ktorý píše odpovede, a presne na to LiteLLM je.

Nastavenie, tvar, pri ktorom som skončil

pip install repowise
ollama pull nomic-embed-text

export OLLAMA_EMBEDDING_MODEL=nomic-embed-text
export LITELLM_BASE_URL=https://litellm.internal.example/v1
export LITELLM_API_KEY=$KEY
export REPOWISE_MODEL=gpt-oss:120b

cd repo && repowise init

Konfiguračné súbory, secrety a ďalšie citlivé cesty patria do ignore súboru skôr, než sa odreže prvý chunk.

Stačí to na seriózny kód?

Väčšinou áno. nomic-embed-text je všeobecný textový embedder, na kóde plnom identifikátorov slabší než bge-m3 alebo model ladený na kód. Repowise to kompenzuje zlučovaním fulltextových a vektorových výsledkov, takže presné názvy sa stále nájdu, a embedder je od výmeny vzdialený jeden ollama pull. Veľký monorepozitár notebook prvýkrát embeduje pomaly, potom už len prírastkovo. Časti, ktoré pôsobia skôr ako porozumenie než ako vyhľadávanie, get_overview, get_why, wiki, vznikajú v textovom prechode a práve tam si 120B model zaslúži svoje miesto.

Posielajte len to, čo naozaj chcete poslať. Nežiadajte server, aby vám odpustil, že ste to neurobili. Najlepšou opravou parametra, ktorý nikto nenapísal, bola požiadavka, ktorú nikto neposiela.