Architektur + Code-Audit a-icking
1. Inventur
Repo ist ~12k LOC Python, nicht 2.8k — der Pipeline-Kern ist klein (~1.5k), aber Fine-Tuning, Evaluation und Daten-Tools sind dick.
- search_pipeline/ (~1500 LOC, der produktive Kern):
cli.py(691, SearchPipeline-Klasse mitrun,run_batch, async-Rerank),pg_client.py(488,postgres_search+postgres_hybrid_search),lambda_handler.py(255, SQS-Entry, ThreadPool),lambda_embedding.py/lambda_rerank.py(~430 zusammen, boto3-Wrapper auf Container-Lambdas),pg_jobs.py(140, sauber),audit_logger.py(161, DSGVO-Log),query_preprocessor.py(260, Maß-Extraktion DN/mm/m²). - lambdas/ (~320 LOC, drei dünne Handler):
api_handler(SQS-Enqueue),auth_handler(HMAC-API-Key gegen Secrets Manager),status_handler(DB-Read, eigene psycopg2-Connection, eigener Health-Check). - models/embedding/ + models/reranking/ (~150 LOC + Dockerfiles, Container-Lambdas mit BGE-M3 / BGE-Reranker — Standard SentenceTransformer-Pattern, in Ordnung).
- fine-tuning/ (~2700 LOC): SageMaker-Launcher, lokale Training-Skripte,
gaeb_extractor.py(607),labeling_cli.py(512). Eigener Mini-Stack neben der Pipeline. - Eval-Scripts im Root (~2500 LOC,
evaluate_baseline.py,evaluate_pipeline_local.py,generate_training_data.py,benchmark_rerankers.py,reembed_catalog.py) — Datascience-Scratchpad, kein App-Code. - terraform/ mit dev1/prod/shared/global + ~11 Modulen — Struktur ist OK, nicht inspiziert.
- Tot/quasi-tot:
run_batchund der ganze Hybrid-Pfad (postgres_hybrid_search,SearchMode.HYBRID,query_preprocessor.build_keyword_boost_query) werden vom Production-Handler nicht aufgerufen — nur von Benchmarks undlabeling_cli. Das sind ~350 LOC Code, die niemand schaltet.
2. Architektur — was rauswürfe
- SQS für synchronen Use-Case. Client postet, bekommt
job_id, pollt/status/{id}(README empfiehlt 2 s Interval, Job-Lebensdauer 5–25 s). Das ist ein Sync-Request mit drei zusätzlichen Hops, drei Audit-Log-Stellen und einer Jobs-Tabelle, die niemand braucht. Direkt-Invoke der Pipeline-Lambda mit synchronem Response (oder Function URL) spart ~5–6 s Polling-Overhead pro Call. - Drei Lambdas wo eine reicht. Embedding-Lambda und Rerank-Lambda sind beide Container, die warm gehalten werden müssen. Embedding + Rerank gemeinsam in einer Container-Lambda (oder direkt im Search-Pipeline-Container) eliminiert zwei boto3-
invoke-Hops und macht Trace+Debug trivial. - Pipeline-Lambda nutzt
runstattrun_batch. Ironie:cli.pyhat eine fertige optimierte Batch-Methode mitget_embedding_batch(1 Lambda-Call statt N) undasyncio.gather-Rerank. Der Production-Handler ignoriert sie und feuert für jeden Query separatSearchPipeline().run()im ThreadPool, der intern wieder einzelne sync-boto3.invoke-Calls macht. Toter Optimierungs-Pfad neben der echten Code-Bahn. - asyncio + aioboto3 in
cli.run_batchparallel zu ThreadPool inlambda_handler. Zwei Concurrency-Modelle, die einander widersprechen. Einer fliegt raus.
3. Code-Qualität (Findings)
pg_client.py:124,138,143,158,312,349,364,372,407— SQL via f-string. Vektor,top_k,score_threshold, Schema-/Tabellenname, sogarquery_text(mit halbgaremreplace("'", "''")) werden in den Query-String formatiert. Blocker. Über die API kommt zwar nur ein Vektor (numerisch validiert) und kein roher Text inpostgres_search— aberpostgres_hybrid_searchundlabeling_clinehmen User-Text. Sobald jemand Hybrid scharfschaltet: Injection-Vektor.pg_jobs.pymacht es nebenan korrekt mitpsycopg.sql— das Muster existiert im Repo, wurde nur nicht angewendet.lambda_handler.py:171— ThreadPool um synchrones boto3. ThreadPoolExecutor umSearchPipeline().run(), das intern blockierendeboto3.invokemacht. Funktioniert, ist aber teurer als ein einziger Batch-Embedding-Call + async Rerank, der schon im Code liegt (run_batch). Major.cli.py:11–20— try/except-Import. Zwei Import-Pfade weil der Lambda-Zip die Files flach legt, das Package aber alssearch_pipeline.cliläuft. Funktioniert, riecht. Minor.- DB-Connection-Management dreigleisig.
pg_client.pypsycopg3 +ConnectionPool(autocommit) + lru_cache-Config;pg_jobs.pygreift auf denselben Pool zu, ruft aberconn.commit()trotz autocommit (No-Op, harmlos);lambdas/status_handler/handler.pybenutzt psycopg2 (nicht 3), globale Single-Connection mit_conn.closed == 0-Check, kein Pool, kein SSL-Erzwingen. Major — inkonsistent, jeder Lambda kennt nur einen Teil des Wahrheitsbilds. status_handler/handler.py:30keinsslmode=require, der direkte Fallback inpg_jobs._get_db_conn:24setzt es — Inkonsistenz. Major im Kontext einer DSGVO-Pipeline mit Audit-Logging.pg_client.py:46Poolmax_size=10global auf Modul-Ebene, aberlambda_handler.py:171startet bis zu 5 Threads, jeder ruftinit_job/update_partial_result/complete_jobplus einen Search — bei concurrent Lambdas summiert sich das schnell auf die RDS-Verbindungsgrenze. Major.- Hardcoded/streng env-getrieben:
MAX_TOP_K,MAX_QUERY_LENGTH,MAX_TEXT_LENGTH,MAX_PARALLEL_WORKERS,MAX_CONCURRENT_RERANKS,SEARCH_TOP_K,SEARCH_LIMIT,AWS_REGION-Defaulteu-central-1an drei Stellen kopiert. Jede neue Env-Var ist eine neue Stelle die schiefgehen kann. Minor. audit_logger.pyschreibt strukturierte Logs, aberapi_key_hash=Nonewird vom Handler immer übergeben — die ganze Pseudonymisierungs-Logik ist ungenutzt. Minor.- Test-Coverage realistisch: 5 Unit-Files, ~1100 LOC Tests, davon mocken nur
test_cli.py+test_hybrid_search.py+test_query_preprocessor.pyecht.test_api.pyundtest_workflow.pysind Integration gegen die echte API mittime.sleep(5)zwischen jedem Test. Heisst: Tests laufen nur wenn die Stage läuft. Major — du kannst nichts lokal regressionssicher refactoren.
4. Was fehlt fürs “5-Minuten-Debug-Versprechen”
- Kein
.github/-Verzeichnis, also keine CI, kein automatisches Test-Run, kein Lint, kein Deploy-Trigger. Terraform muss manuell laufen. - Keine Tracing-Spans (X-Ray würde mit 3 Zeilen
aws-xray-sdkgreifen, gerade weil drei Lambdas hintereinander hängen — genau dafür gebaut). Logs sind Plain-Text, kein structured-logging, keine Correlation-IDs (job_idist vorhanden, wird aber nicht konsistent durch alle Lambdas geloggt). - Health-Endpoint ist halbgar:
status_handlerantwortet auf/health, prüft DB. Prüft aber nicht Embedding-/Rerank-Lambda und nicht SQS. Wenn die Pipeline-Lambda hängt sagt der Health-Check trotzdemhealthy. - Keine Cloudwatch-Alarms im Repo definiert.
- Schema-Migrations laufen manuell über
apply_migrations.py— nicht im Deploy-Pipeline. - Keine ONBOARDING.md, kein ARCHITECTURE.md.
5. Kill-or-Keep
Keep (in einen Neubau mitnehmen):
- Terraform-Module-Struktur (envs/modules/stacks), das ist solide aufgesetzt.
models/embedding/app.py+models/reranking/app.py+ die zwei Dockerfiles.db/migrations/00*.sql— pgvector + HNSW + jobs-Tabelle.query_preprocessor.pyMaß-Extraktion (DN/mm/m²) — Fachlogik die Wert hat.auth_handler/handler.py— 79 LOC, HMAC-Vergleich, sauber.- Eval-Scripts als Referenz für Quality-Gates (nicht als Production-Code).
- Den gesamten Fine-Tuning-Tract als separates Repo/Sub-Projekt — der hat mit der Online-Pipeline nichts zu tun.
Kill (im Neubau nicht reproduzieren):
- SQS + Jobs-Tabelle + Polling-Pattern.
- Getrennte Embedding-Lambda und Rerank-Lambda. Eine Container-Lambda mit beiden Modellen.
pg_client.pyals f-string-Builder — neu mitpsycopg.sql+ Parameter-Binding.cli.py-Doppel-Code-Bahn (runundrun_batchund Hybrid).- Drei DB-Connect-Patterns.
audit_loggerPseudonymisierungs-Logik die nie scharfgeschaltet wird.
Fazit
Die Behauptung “Qualität passt nicht” stimmt — aber der technische Schaden ist überschaubar. Der produktive Kern ist ~1500 LOC, davon 300–400 tot, 200 SQL-Injection-Reparatur, der Rest funktioniert.
Was wirklich nicht stimmt sind die Architektur-Entscheidungen (SQS, getrennte Lambdas, Polling-Pattern, kein CI) — und die kosten Latenz, Lambda-Minuten und Debug-Zeit. Empfehlung: Rewrite wäre machbar in 2-3 Tagen, aber Refactor genauso schnell und mit weniger Risiko. Schema + Container-Modelle + Terraform-Module + auth_handler + query_preprocessor übernehmen, Rest neu strukturieren.