155 lines
7.0 KiB
Markdown
155 lines
7.0 KiB
Markdown
# 14 KI: Schlüssel und Modelle
|
|
|
|
Das CRM ruft OpenAI an drei Stellen auf. Schlüssel und Modell pflegen
|
|
Administratoren unter **Einstellungen → KI** - ohne SSH, ohne Neustart.
|
|
|
|

|
|
|
|
## Wo die KI arbeitet
|
|
|
|
| Einsatzzweck | Code | Schlüssel in `crm_settings` | Vorgabe |
|
|
| --- | --- | --- | --- |
|
|
| Scanner: Bewertung Lebenslauf gegen Projekt | `scanner/services/matchingEngine.js` | `ai_model_matching` | `gpt-4o-mini` |
|
|
| Scanner: Abgleich „bereits beworben" | `scanner/services/appliedCheck.js` | `ai_model_applied` | `gpt-5.6-luna` |
|
|
| Erweiterte Suche | `routes/search.js` | `ai_model_search` | `gpt-4o-mini` (bzw. `OPENAI_MODEL`) |
|
|
|
|
Die Vorgaben sind genau die Modelle, die vor der Einstellung fest im Code
|
|
standen. Wer nichts umstellt, merkt also nichts. Steht in `crm_settings` kein
|
|
oder ein ungültiger Eintrag, gilt die Vorgabe.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
UI["Einstellungen → KI"] -->|"PUT /api/ai/key"| V{"GET /v1/models<br/>mit neuem Schlüssel"}
|
|
V -->|"abgelehnt"| X["Fehlermeldung,<br/>alter Schlüssel bleibt"]
|
|
V -->|"ok"| K[("perinco_scanner.settings<br/>openai_api_key")]
|
|
UI -->|"PUT /api/ai/models"| M[("perinco_CRM.crm_settings<br/>ai_model_*")]
|
|
K --> AI["server/src/ai.js<br/>aiKey() / modelFor()"]
|
|
E["OPENAI_API_KEY<br/>server/.env"] -.->|"nur wenn DB leer"| AI
|
|
M --> AI
|
|
AI --> S1["Scanner-Bewertung"]
|
|
AI --> S2["Abgleich bereits beworben"]
|
|
AI --> S3["Erweiterte Suche"]
|
|
```
|
|
|
|
## Schlüssel
|
|
|
|
Es gibt **genau einen** OpenAI-Schlüssel für Scanner und CRM. Er liegt
|
|
unverändert als `openai_api_key` in der Scanner-Tabelle `settings` - dort, wo er
|
|
schon immer lag. Nichts musste umziehen. Ersatzweise gilt `OPENAI_API_KEY` aus
|
|
`server/.env`.
|
|
|
|
* **Anzeige nur mit Ende.** Die Oberfläche zeigt `sk-…` und die letzten vier
|
|
Zeichen, dazu die Quelle (*hier hinterlegt* oder *server/.env*). Mehr gibt der
|
|
Server nie heraus, auch nicht an Administratoren. Bei Schlüsseln unter zwölf
|
|
Zeichen steht nur *ja*.
|
|
* **Vor dem Speichern geprüft.** Ein neuer Schlüssel wird erst gegen
|
|
`GET https://api.openai.com/v1/models` ausprobiert. Lehnt OpenAI ab, bleibt der
|
|
alte gültig, und die Meldung von OpenAI steht im Fehlertext. Der Abruf kostet
|
|
nichts.
|
|
* **Verwerfen** leert den hinterlegten Wert. Danach gilt wieder die `.env` -
|
|
oder, wenn dort nichts steht, gibt es keinen Schlüssel mehr.
|
|
* Der Schlüssel liegt **unverschlüsselt** in der Scanner-Datenbank, wie bisher.
|
|
Ein Dump von `perinco_scanner` enthält ihn also.
|
|
|
|
Im Reiter *Scanner* gibt es kein Schlüsselfeld mehr. `POST /api/scanner/settings`
|
|
nimmt `openai_api_key` aus Kompatibilität weiter an.
|
|
|
|
## Modelle
|
|
|
|
Das Dropdown füllt sich **live von OpenAI**: alle Modelle, die der Schlüssel
|
|
nutzen darf, gefiltert auf Textmodelle (`gpt-*`, `o1`/`o3`/`o4…`, `chatgpt-*`,
|
|
ohne Audio, Bild, Embedding, Realtime, TTS, Suche, Moderation). Die aktuell
|
|
eingestellten Modelle stehen immer mit drin, die Vorgabe ist markiert.
|
|
|
|
Ist die Liste nicht abrufbar (kein Schlüssel, OpenAI nicht erreichbar), zeigt
|
|
das Dropdown eine feste Auswahl, und die Seite sagt, warum.
|
|
|
|
Die Wahl gilt ab dem nächsten Aufruf - es gibt keinen Zwischenspeicher.
|
|
|
|
### Reasoning- und klassische Modelle
|
|
|
|
Die beiden Modellfamilien nehmen unterschiedliche Parameter. Damit ein Wechsel
|
|
im Dropdown keinen `400` von OpenAI auslöst, entscheidet `isReasoningModel()`
|
|
(Namen `o1…`, `o3…`, `gpt-5…`, `gpt-6…` und jede spätere Generation) je Aufruf:
|
|
|
|
| | Reasoning (`o*`, `gpt-5*`, `gpt-6*` …) | klassisch (`gpt-4o*`, `gpt-4.1*`) |
|
|
| --- | --- | --- |
|
|
| Scanner-Bewertung | `reasoning_effort: low` | `temperature: 0.1` |
|
|
| Abgleich „bereits beworben" | `reasoning_effort: low` | `temperature: 0` |
|
|
| Erweiterte Suche | `max_completion_tokens` | `max_completion_tokens` |
|
|
|
|
`max_completion_tokens` verstehen beide Familien, das alte `max_tokens` die
|
|
Reasoning-Modelle nicht.
|
|
|
|
Bis zum 24.09.2026 erkannte das Muster nur `gpt-5…`. `gpt-6-luna` galt damit als
|
|
klassisch, bekam `temperature: 0.1` und OpenAI lehnte jeden Aufruf ab
|
|
(„Only the default (1) value is supported"). Deshalb zählt jetzt jede Generation
|
|
ab 5 als Reasoning-Modell.
|
|
|
|
**Vergleich Scanner-Bewertung, 24.09.2026** (33 echte Projekte aus Lisa, Ariane
|
|
und Tobias, neuer Bewertungsprompt, siehe [03 Scanner](03-scanner.md#bewertungsprompt)):
|
|
|
|
| | `gpt-4o-mini` | `gpt-6-luna` (`reasoning_effort: low`) |
|
|
| --- | --- | --- |
|
|
| Fehler | 0 | 0 |
|
|
| Ø Antwortzeit | 1,3 s | 2,8 s |
|
|
| Ø Token rein / raus | 5337 / 108 | 5336 / 172 (davon 35 Reasoning) |
|
|
| passende Rollen | 85-95 | 82-98 |
|
|
| fachfremde Rollen | meist 0 oder 30 | 0-15, feiner abgestuft |
|
|
| Preis je 1 Mio. Token rein / raus (Standard, Stand 24.09.2026) | 0,15 $ / 0,60 $ | 0,10 $ / 0,50 $ |
|
|
| Kosten je Bewertung (ohne Cache) | 0,087 ct $ | 0,062 ct $ |
|
|
| hochgerechnet auf ~9 500 Bewertungen je Woche | ~8,20 $ | ~5,90 $ |
|
|
|
|
Luna ist also rund 28 % günstiger, obwohl es mehr Token ausgibt: die Eingabe
|
|
(Prompt plus Lebenslauf, rund 5 300 Token) macht über 90 % der Kosten aus, und
|
|
die ist bei Luna billiger. Gecachte Eingabe kostet bei Luna 0,01 $ statt
|
|
0,075 $ je 1 Mio. Token - Systemprompt und Lebenslauf stehen am Anfang jeder
|
|
Anfrage eines Agenten, das Einsparpotenzial ist dort also noch größer.
|
|
|
|
Luna begründet genauer und bleibt nah am Lebenslauf. Grenzfälle
|
|
(übertragbare Erfahrung) bewertet es spürbar großzügiger als `gpt-4o-mini`.
|
|
|
|
### Produktiv auf `gpt-6-luna` - seit 24.09.2026
|
|
|
|
Nach dem Vergleich hat Sascha alle drei Zwecke unter *Einstellungen → KI* auf
|
|
`gpt-6-luna` gestellt (`ai_model_matching`, `ai_model_applied`,
|
|
`ai_model_search` in `crm_settings`). Die Vorgaben im Code bleiben unverändert.
|
|
Sie greifen nur, wenn nichts eingestellt ist.
|
|
|
|
Erster Live-Lauf, 24.09.2026 09:52-10:05:
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| Projekte abgerufen / neu gespeichert | 236 / 24 |
|
|
| Bewertungen | 126, keine Fehler |
|
|
| Abgleich „bereits beworben" | 21 Projekte geprüft, keine Fehler |
|
|
| höchste Werte | Dana 94 (SAP-Berater), Niklas 89 / Tobias 83 (Systemadministrator) |
|
|
| Con „Deutsch B2" | 0 |
|
|
|
|
Lisa und Ariane lagen in diesem Lauf bei höchstens 38 bzw. 35. Unter den
|
|
24 neuen Projekten war keins aus Marketing, Kommunikation oder PMO, der Wert
|
|
ist also plausibel und kein Fehler.
|
|
|
|
Zurück auf das alte Modell: in *Einstellungen → KI* wieder `gpt-4o-mini`
|
|
wählen. Dank `isReasoningModel()` ist kein Deployment nötig.
|
|
|
|
## Endpunkte
|
|
|
|
Alle unter `/api/ai`, nur für Administratoren.
|
|
|
|
| Methode | Pfad | Zweck |
|
|
| --- | --- | --- |
|
|
| GET | `/settings` | Schlüsselzustand, Modell je Zweck, Auswahlliste |
|
|
| PUT | `/key` | `{ key }` - prüfen, dann speichern |
|
|
| DELETE | `/key` | hinterlegten Schlüssel verwerfen |
|
|
| PUT | `/models` | `{ matching?, applied?, search? }` - Namen geprüft gegen `^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$` |
|
|
|
|
## Dateien
|
|
|
|
| Datei | Aufgabe |
|
|
| --- | --- |
|
|
| `server/src/ai.js` | Schlüssel, Modellwahl, Modellliste, Aufruf der Suche |
|
|
| `server/src/routes/ai.js` | die vier Endpunkte |
|
|
| `src/components/settings/AiSettings.tsx` | Reiter *KI* |
|
|
| `server/test/ai.test.mjs` | Modellnamen und Reasoning-Erkennung |
|