AI-PR-Review-Agent auf einem VPS selbst hosten
Betreiben Sie einen AI-PR-Reviewer auf Ihrem VPS: mit selbst gehostetem Runner, Diff-Prompts, Pfad- und Größenfiltern, Inline-Kommentaren und Kosten pro PR.
Was ein selbst gehosteter PR-Review-Agent macht
Ein selbst gehosteter PR-Review-Agent ist ein kleines Programm auf einem Server, den Sie selbst verwalten. Er liest den Diff eines Pull Requests (PR) und sendet nur die geänderten Zeilen an ein Modell. Die Antwort wird als Inline-Review-Kommentar veröffentlicht. Der Agent checkt Ihren Branch nie aus und liest niemals eine Datei, die vom Pull Request nicht geändert wurde. Die einzigen Zugangsdaten, die er besitzt, sind ein API-Schlüssel für ein Modell und ein Token, mit dem er kommentieren kann, aber keine anderen Aktionen ausführen darf. Das ist eine eng gefasste Definition eines Agenten. Sie beschreibt eher einen Prompt mit vorgeschalteten Filtern als etwas Autonomes. Wenn Sie vor dem Aufbau dieses Systems zunächst das vollständige Bild benötigen, behandelt der schrittweise Weg von den Konzepten bis zu einer selbst geschriebenen Schleife die zugrunde liegenden Themen.
Ein Modell kann ein Diff lesen. Dieser Teil ist gelöst. Entscheidend ist, wohin das Diff gesendet wird und wer den Schlüssel besitzt. Bei einem gehosteten Review-Bot verlässt jedes Diff aus jedem privaten Repository Ihr Netzwerk, wird in den Logs eines Drittanbieters gespeichert und unterliegt dessen Aufbewahrungsrichtlinie. Auf einem VPS (Virtual Private Server), den Sie selbst besitzen, geht das Diff von GitHub zu Ihrem Server und anschließend an die Model-API. Außerdem können Sie die vierzig Codezeilen einsehen, die festlegen, welche Daten gesendet werden.
Was Sie vor dem Start benötigen
- Einen VPS mit Ubuntu 24.04 und einem bereits für das Repository registrierten, selbst gehosteten GitHub-Actions-Runner. Weisen Sie ihm bei der Registrierung zusätzlich das Label
pr-reviewzu, da der folgende Workflow anhand dieses Labels ausgewählt wird. - Einen Anthropic-API-Schlüssel aus der Claude Console.
- Ein Repository, in dem Sie kontrollieren, wer einen Pull Request eröffnen darf. Ein privates Repository ist der einfache Fall. Der folgende Abschnitt zu Forks behandelt den öffentlichen Fall. Die dortige Antwort ist weniger komfortabel.
Installieren Sie den Reviewer auf dem VPS
Der Runner-Dienst läuft unter dem unprivilegierten Konto, das Sie bei der Ausführung von ./svc.sh install erstellt haben. Installieren Sie den Reviewer unter demselben Konto, damit der Job ihn ohne sudo ausführen kann. Ersetzen Sie runner unten durch den Namen Ihres Kontos.
sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --versiongh --version gibt unter Ubuntu 24.04 mit Stand August 2026 gh version 2.45.0 aus. Jede Version ab 2.20 unterstützt das unten verwendete Flag --input. Eine Command 'gh' not found-Meldung bedeutet, dass die Komponente universe nicht aktiviert ist. Führen Sie daher sudo add-apt-repository universe aus und versuchen Sie es erneut.
Wo der Schlüssel und das Token liegen
Zwei Secrets mit zwei unterschiedlichen Gültigkeitsdauern. Keines davon gehört in das Repository.
ANTHROPIC_API_KEY ist ein Repository-Secret. Sie legen es unter Settings, dann Secrets and variables und anschließend Actions fest. GitHub verschlüsselt es und injiziert es zur Laufzeit in die Umgebung des Steps. Es ist niemals eine Datei auf der Festplatte und befindet sich niemals in der git-Historie.
GITHUB_TOKEN funktioniert anders. Actions erstellt für jeden Job ein neues Token und löscht es, wenn der Job endet. Welche Aktionen dieses Token ausführen darf, wird durch den Block permissions: im Workflow festgelegt. Hier wird das Prinzip der geringsten Berechtigungen tatsächlich umgesetzt:
permissions:
contents: read
pull-requests: writeDieses Token kann ein Review veröffentlichen. Es kann keinen Commit pushen, keinen Branch mergen, keine Workflow-Datei bearbeiten und auf kein anderes Repository zugreifen. Ein Agent, der kommentieren kann, ist ein Reviewer. Ein Agent, der pushen kann, ist ein Committer. Dem hat niemand zugestimmt. Behandeln Sie den Model-Key ebenso sorgfältig, weil er Kosten auf Ihrem Konto verursacht. Mehr zu diesem Problem finden Sie unter Secrets außerhalb der Reichweite eines AI-Agenten halten.
Actions ersetzt die exakte Secret-Zeichenfolge in Job-Logs durch ***. Es wird nur nach der exakten Zeichenfolge gesucht. Wenn Sie einen Key mit base64 kodieren, auf zwei Zeilen aufteilen oder Zeichen einzeln ausgeben, erscheint er im Klartext. Fügen Sie keinen Debug-Schritt hinzu, der die Umgebung ausgibt.
Warum ein Pull Request aus einem Fork niemals Ihren API-Schlüssel sieht
Die GitHub-Regel ist kurz: Mit Ausnahme von GITHUB_TOKEN werden Secrets nicht an den Runner übergeben, wenn ein Workflow aus einem geforkten Repository ausgelöst wird. Ein pull_request-Lauf aus einem Fork startet Ihr Skript daher ohne ANTHROPIC_API_KEY, und der erste API-Aufruf schlägt mit invalid x-api-key fehl.
Die naheliegende Lösung besteht darin, den Trigger auf pull_request_target umzustellen. Dieser läuft im Kontext des Basis-Repositorys und erhält die Secrets. Tun Sie das hier nicht. GitHub weist in den eigenen Sicherheitshinweisen darauf hin, dass diese Workflows „privilegiert sind. Das bedeutet, dass sie denselben Cache des Main-Branches wie andere privilegierte Workflow-Trigger verwenden und möglicherweise Schreibzugriff auf das Repository sowie Zugriff auf referenzierte Secrets haben.“ Das Ergebnis „kann ausgenutzt werden, um ein Repository zu übernehmen“.
Dieselben Hinweise formulieren auch eine klare Warnung zum Runner: „Self-hosted Runner sollten für öffentliche Repositorys auf GitHub fast nie verwendet werden, weil jeder Benutzer Pull Requests gegen das Repository eröffnen und die Umgebung kompromittieren kann.“
Daraus ergeben sich zwei Designentscheidungen. Der Job enthält eine Abfrage, sodass er nur auf Branches ausgeführt wird, die in Ihr eigenes Repository gepusht wurden. Außerdem enthält der Workflow überhaupt keinen actions/checkout-Schritt. Der Agent hat den Branch niemals auf der Festplatte. Ein schädlicher Pull Request ist daher nur Text, der an ein Modell gesendet wird. Der Agent kann auf Ihrem VPS kein Build-Skript ausführen, weil auf Ihrem VPS nichts davon ausgeführt wird. Text ist deshalb jedoch nicht automatisch harmlos: Ein von einer fremden Person erstellter Diff ist nicht vertrauenswürdige Eingabe, die bei einem Modell ankommt. Das ist dieselbe Vertrauensgrenze wie bei der Übergabe einer Websuche an einen Agenten. Der einzige begrenzende Faktor ist hier, dass dieser Agent ausschließlich einen Kommentar veröffentlichen kann.
Den Diff abrufen, nicht das Repository
Eine Anfrage ruft den gesamten Diff als Klartext ab.
export GH_TOKEN=your_token # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"Der Medientyp Accept: application/vnd.github.diff wandelt die Antwort von einem JSON-Objekt mit der Beschreibung des Pull Requests in den eigentlichen Unified Diff um. gh api gibt diesen Inhalt unverändert aus. Die erste angezeigte Zeile sollte mit diff --git a/ beginnen. Ein gh: Not Found (HTTP 404) bedeutet, dass das Token keinen Zugriff auf das Repository hat. Bei einem fein abgestuften persönlichen Token bedeutet das fast immer, dass die Berechtigung Pull requests nicht aktiviert wurde.
Filtern, bevor Sie ein Token verbrauchen
Dieser Abschnitt entscheidet darüber, ob Menschen einen Bot lesen oder stummschalten. Jeder der folgenden Filter wird ausgeführt, bevor das Modell auch nur ein Byte sieht.
- Pfadfilter. Sperren Sie Lock-Dateien, vendored Verzeichnisse, minimierte Bundles und generierten Code. Ein Modellkommentar zu
package-lock.jsonist reines Rauschen. Außerdem enthalten diese Dateien häufig den größten Teil der Bytes eines Diffs. - Eine Größenobergrenze. Überschreitet der Diff die Obergrenze, überspringen Sie das Review und beenden Sie den Lauf erfolgreich. Bei einem Refactoring mit 4,000 Zeilen genügt eine klare Zeile, dass es für eine automatische Prüfung zu groß war. Das ist besser als sechzig Vermutungen.
- Ein Schweregradschwellenwert und eine Kommentarobergrenze. Melden Sie Befunde mit hohem und mittlerem Schweregrad, höchstens zehn davon und nach absteigendem Schweregrad sortiert. Den Kommentar Nummer elf liest niemand.
Das Skript
Speichern Sie dies als /opt/pr-review/review.py. Das Skript liest seine Konfiguration aus der Umgebung. Dadurch kann der Workflow die Modelle ohne Codeänderung wechseln.
#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys
import anthropic
REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")
raw_diff = subprocess.run(
["gh", "api", f"/repos/{REPO}/pulls/{PR}",
"-H", "Accept: application/vnd.github.diff"],
check=True, capture_output=True, text=True,
).stdoutDie Aufteilung des Diffs nach Datei ermöglicht die Pfadfilterung. Die Nummerierung jeder Zeile sorgt dafür, dass die Review-Kommentare an der richtigen Stelle landen. GitHub akzeptiert einen Inline-Kommentar nur für eine Zeile, die Bestandteil des Diffs ist. Das Modell muss daher eine tatsächliche Zeilennummer angeben. Wenn die Nummern mitgeliefert werden, kann es eine davon übernehmen, statt eine zu erfinden.
def per_file(diff_text):
"""Split a unified diff into one string per file."""
sections, current = [], []
for line in diff_text.splitlines():
if line.startswith("diff --git ") and current:
sections.append("\n".join(current))
current = []
current.append(line)
if current:
sections.append("\n".join(current))
return sections
def annotate(section):
"""Prefix every line that exists in the new file with its line number."""
out, n, in_hunk = [], 0, False
for line in section.splitlines():
if line.startswith("@@"):
n = int(line.split("+")[1].split(",")[0].split(" ")[0])
in_hunk = True
out.append(line)
elif not in_hunk or line.startswith(("-", "\\")):
out.append(line)
else:
out.append(f"{n}\t{line}")
n += 1
return "\n".join(out)
kept = [s for s in per_file(raw_diff)
if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)
if not payload.strip():
print("every changed file was filtered out")
raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
raise SystemExit(0)Der Hunk-Header enthält die Nummerierung. @@ -12,7 +12,9 @@ gibt an, dass der Hunk der neuen Datei bei Zeile 12 beginnt. Der Zähler startet daher dort und wird nur für hinzugefügte und unveränderte Zeilen weitergezählt. Entfernte Zeilen erhalten keine Nummer, weil sie in der neuen Datei nicht vorhanden sind. Die Abfrage für Zeilen, die mit einem umgekehrten Schrägstrich beginnen, überspringt die Markierung für das fehlende Zeilenende, die git am Ende einer Datei schreibt. Andernfalls würden alle folgenden Nummern um eins verschoben.
Beide Beendigungen verwenden den Status 0, nicht 1. Ein gefilterter oder zu großer Pull Request sollte einen grünen Status anzeigen. Ein roter Status, auf den Menschen nicht reagieren können, wird ignoriert. Sobald ein Status ignoriert wird, werden alle ignoriert.
SYSTEM = (
"You review one pull request diff. Every line that exists in the new file is "
"prefixed with its line number and a tab character. "
"Report only defects you can see in the lines shown: a crash, a resource leak, "
"a security mistake, a wrong boundary condition, a broken contract with code "
"that is visible in this diff. Do not comment on style, naming or formatting. "
"Do not guess about code you cannot see. Leave out anything you are not "
"certain about. An empty findings list is a normal and common answer. "
'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
'"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
"Every line number must be one you can see in the left column of that file."
)
client = anthropic.Anthropic()
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=SYSTEM,
messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
f"out={message.usage.output_tokens}", file=sys.stderr)
text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]
if not findings:
print("nothing above the severity threshold; posting no comment")
raise SystemExit(0)
review = {
"event": "COMMENT",
"body": f"Automated review of the changed lines. {len(findings)} finding(s).",
"comments": [
{"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
"body": f"**{f['severity']}** {f['comment']}"}
for f in findings
],
}
subprocess.run(
["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
input=json.dumps(review), text=True, check=True,
)Drei Details in diesem Block sind entscheidend. Das JSON wird zwischen dem ersten { und dem letzten } ausgeschnitten, weil ein Modell seine Antwort manchmal in einen Codeblock einschließt und json.loads an diesem Codeblock scheitert. Bei path wird ein führendes b/ entfernt, weil dieses Präfix aus dem Diff-Header stammt und GitHub einen relativ zum Repository angegebenen Pfad erwartet. --input - sendet das gesamte Review in einem API-Aufruf. Dadurch treffen zehn Befunde als eine Benachrichtigung statt als zehn ein.
Wenn es nichts zu melden gibt, veröffentlicht das Skript nichts. Ein Bot, der bei jedem Pull Request „keine Probleme gefunden“ schreibt, bringt Menschen dazu, solche Meldungen zu überfliegen. Dann übersehen sie auch die eine Meldung, die wichtig war.
In den Workflow integrieren
Speichern Sie dies als .github/workflows/pr-review.yml:
name: pr-review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
runs-on: [self-hosted, linux, pr-review]
steps:
- name: Review the changed lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEW_MODEL: claude-haiku-4-5-20251001
MIN_SEVERITY: medium
run: /opt/pr-review/venv/bin/python /opt/pr-review/review.pyGITHUB_REPOSITORY steht nicht in diesem env:-Block, weil Actions es bereits für jeden Job setzt. Die Gruppe concurrency ist für die Abrechnung relevant: Ohne sie führen drei schnelle Korrekturen, die Sie in einen Branch pushen, zu drei vollständigen Reviews, die alle berechnet werden. Mit dieser Gruppe bleibt nur der letzte Lauf bestehen.
Die Zeile if: erfüllt zwei Aufgaben. Der erste Teil überspringt Pull Requests aus Forks, die ohne Schlüssel ohnehin fehlschlagen würden. Der zweite Teil gibt Ihrem Team eine Möglichkeit, den Job zu deaktivieren: Fügen Sie einem Pull Request das Label no-ai-review hinzu, wird der Job nicht ausgeführt.
Öffnen Sie einen Pull Request und beobachten Sie das Ergebnis:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsEin Lauf, der innerhalb weniger Sekunden mit nothing above the severity threshold; posting no comment im Log beendet wird, funktioniert korrekt. Bei einem kleinen, sauberen Pull Request ist das das erwartete Ergebnis.
Was kostet eine automatisierte Pull-Request-Prüfung?
Der Diff macht fast die gesamte Eingabe aus. Daher bestimmt seine Größe den Preis. Unten sehen Sie einen gemessenen Diff mit 500 Zeilen einschließlich des System-Prompts. Die Token wurden mit dem Token-Zählungsendpunkt ermittelt und nicht geschätzt.
The data behind this chart
[
{
"label": "Haiku 4.5",
"input_tokens": "8,000",
"output_tokens": "1,200"
},
{
"label": "Sonnet 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
},
{
"label": "Opus 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
}
]Dieser Diff umfasste 8,000 Eingabetoken auf Haiku 4.5 und 10,400 auf Sonnet 5. Der Text war identisch, die Zählung jedoch unterschiedlich. Claude-Modelle ab 4.7 verwenden einen neueren Tokenizer, der für dieselbe Eingabe ungefähr 30% mehr Token erzeugt. Anthropic dokumentiert dies auf der Preisseite. Berücksichtigen Sie das, wenn Sie ein neueres Modell allein anhand des Preises pro einer Million Token mit einem älteren vergleichen.
Listenpreise im August 2026: Haiku 4.5 kostet 1 $ pro einer Million Eingabetoken und 5 $ pro einer Million Ausgabetoken. Sonnet 5 kostet im Rahmen der bis zum 31. August 2026 geltenden Einführungspreise 2 $ beziehungsweise 10 $, danach 3 $ beziehungsweise 15 $. Opus 5 kostet 5 $ beziehungsweise 25 $.
The data behind this chart
[
{
"label": "Haiku 4.5",
"cost_per_pr_cents": 1.4,
"cost_200_prs_usd": "2.80"
},
{
"label": "Sonnet 5",
"cost_per_pr_cents": 3.64,
"cost_200_prs_usd": "7.28"
},
{
"label": "Opus 5",
"cost_per_pr_cents": 9.1,
"cost_200_prs_usd": "18.20"
}
]Das sind 1.4 Cent pro Pull Request auf Haiku 4.5 und 9.1 Cent auf Opus 5. Ein Team, das monatlich 200 Pull Requests zusammenführt, zahlt auf Haiku 4.5 ungefähr $2.80, auf Sonnet 5 $7.28 oder auf Opus 5 $18.20. Ab dem 1. September 2026 multiplizieren Sie den Betrag für Sonnet 5 mit 1.5.
Zwei Faktoren führen dazu, dass die tatsächlichen Kosten höher ausfallen können. Der synchronize-Trigger prüft jeden Push. Ein aktiver Branch mit acht Pushes verursacht daher acht Prüfungen. Die Parallelitätsregel hilft nur, wenn die Pushes in kurzem Abstand eintreffen. Die Zahlen setzen außerdem voraus, dass die Pfadfilter funktionieren. Eine einzige nicht gefilterte Lock-Datei kann die Eingabemenge bereits verdoppeln.
Prompt-Caching hilft hier nicht. Der gecachte Präfix muss zwischen den Aufrufen byte-identisch sein. Der Diff ist jedoch bei jedem Aufruf anders. Der System-Prompt ist der einzige stabile Teil und liegt deutlich unter der Mindestlänge für das Caching. Die allgemeine Regel finden Sie unter wann sich Prompt-Caching lohnt. Informationen zur Auswahl zwischen den drei genannten Modellen finden Sie unter welches Claude-Modell für welche Aufgabe verwendet werden sollte.
Messen Sie Ihre eigenen Diffs, bevor Sie die Funktion aktivieren
Fügen Sie diese Zeile ein, nachdem payload erstellt wurde. Führen Sie das Skript anschließend manuell für einige Pull Requests des vergangenen Monats aus:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)Der Zählungsendpunkt führt das Modell nicht aus. Daher verbraucht er keine Eingabe- oder Ausgabetoken. Er verwendet den Tokenizer des von Ihnen angegebenen Modells. Führen Sie ihn für zehn echte Pull Requests aus Ihrem eigenen Repository aus. Verwenden Sie den Median statt des Mittelwerts, damit eine besonders große Migration die Schätzung nicht verfälscht.
Warum Review-Bots stummgeschaltet werden und wie Sie das vermeiden
Zwei Verhaltensweisen zerstören das Vertrauen in diese Bots. Für beide gibt es im obigen Code eine Lösung.
Alles auf einmal prüfen. Ein Bot, der vierzig Kommentare hinterlässt, wird davon keinen einzigen gelesen. Die Schwelle für den Schweregrad und die Begrenzung auf zehn Kommentare sind keine Frage der Höflichkeit. Sie sorgen dafür, dass die tatsächlichen Befunde sichtbar bleiben. Wenn vor dem Kürzen nach Schweregrad sortiert wird, entfernt die Begrenzung die unwichtigsten Befunde und nicht zufällig zehn davon.
Mit Gewissheit etwas kommentieren, das der Bot nicht prüfen kann. Das ist der Punkt, an dem Entwickler den Bot endgültig deaktivieren. Ein Modell, dem 200 Zeilen aus einer Codebasis mit 40,000 Zeilen gezeigt werden, schreibt trotzdem: „Das bricht die Cache-Invalidierung in redis_client.py“, obwohl es die betreffende Datei nie gesehen hat. Der System-Prompt wirkt dem mit einer klaren Vorgabe entgegen: Melden Sie nur Fehler, die in den gezeigten Zeilen sichtbar sind, und lassen Sie alles weg, bei dem Sie nicht sicher sind. Den Fehler direkt zu benennen funktioniert besser, als allgemein um Genauigkeit zu bitten. Der Hinweis, dass ein leeres Ergebnis normal ist, verhindert außerdem, dass das Modell zu einer Änderung von zwei Zeilen etwas erfindet. Fehler, die erst im gesamten Repository sichtbar werden, erfordern eine andere Aufgabe. open-kritt für Security-Scans selbst zu hosten ist eine Möglichkeit, diesen Bereich abzudecken, ohne den für diesen Reviewer sichtbaren Umfang zu erweitern.
Veröffentlichen Sie das Review als COMMENT, niemals als REQUEST_CHANGES. Die Einschätzung eines Modells darf einen Merge nicht blockieren. Sobald sie das kann, wird jemand unter Zeitdruck den gesamten Workflow entfernen, statt darüber zu diskutieren. Wenn die Bewertung eines Agents wirklich maßgeblich sein soll, sollte er etwas erzeugen, das Sie prüfen können, statt etwas, dem Sie glauben müssen. Darauf basiert die Idee, einen Agent einen Evidence-Report erstellen zu lassen, den Sie selbst erneut ausführen können.
Fehlerbilder und die angezeigten Zeichenfolgen
HTTP 422 beim Posten des Reviews. gh gibt gh: Unprocessable Entity (HTTP 422) aus, und der Antworttext nennt das Feld: Pull request review thread line must be part of the diff. GitHub kann diesen Kommentar nicht verankern. Häufige Ursachen sind eine vom Modell erfundene Zeilennummer, ein path, das weiterhin das Präfix b/ enthält, oder ein Kommentar zu einer entfernten Zeile. In diesem Fall muss side auf LEFT statt auf RIGHT gesetzt werden. Geben Sie das Review-JSON vor dem Posten aus und prüfen Sie einen Kommentar manuell anhand des Diffs.
invalid x-api-key von der Modell-API. Der Schritt schlägt beim ersten messages.create-Aufruf fehl. Entweder ist das Geheimnis ANTHROPIC_API_KEY im Repository nicht gesetzt, oder der Pull Request stammt aus einem Fork, sodass Actions überhaupt keine Geheimnisse übergeben hat. Die Fork-Prüfung in der Zeile if: hätte den Schritt überspringen müssen. Prüfen Sie daher zuerst diese Zeile.
gh: Resource not accessible by integration (HTTP 403). Das Job-Token kann nicht in Pull Requests schreiben. Fügen Sie pull-requests: write in den Block permissions: ein. Wenn es bereits vorhanden ist, prüfen Sie Settings, dann Actions und anschließend General. Dort kann eine Organisationsrichtlinie begrenzen, welche Berechtigungen ein Workflow-Token anfordern darf.
json.decoder.JSONDecodeError. Das Modell hat kein parsbares JSON zurückgegeben. Die häufigste Ursache ist eine Antwort, die das Token-Limit erreicht und mitten in einem Objekt abgebrochen wurde. Die Logzeile gibt dafür stop_reason aus: Ein Wert von max_tokens bedeutet, dass Sie max_tokens erhöhen oder MAX_COMMENTS verringern müssen.
Der Workflow wird nie ausgeführt. gh run list zeigt für den Pull Request nichts an. Prüfen Sie, ob paths-ignore nicht alle geänderten Dateien herausgefiltert hat. Prüfen Sie danach die Fork-Prüfung und die Label-Prüfung. Kontrollieren Sie anschließend mit sudo systemctl status 'actions.runner.*' auf dem VPS, ob der Runner aktiv ist. Ein nicht erreichbarer Runner lässt den Job ohne Fehlermeldung im Pull Request in der Warteschlange.
Jedes Review bleibt leer. Setzen Sie MIN_SEVERITY für einen Durchlauf auf low. Wenn Findings erscheinen, arbeitet der Schwellenwert wie vorgesehen. Wenn nichts erscheint, geben Sie payload aus und prüfen Sie, ob die Filter nicht den gesamten Diff entfernt haben.
Zusammenbetrieb mit Ihren anderen Agents
Der Reviewer ist klein. Daher liegt es nahe, ihn auf dem Host zu installieren, auf dem bereits alle anderen Dienste laufen. Wenn das Repository wichtig ist, halten Sie ihn getrennt. Dieser Prozess besitzt ein Token, mit dem er Ihren Code kommentieren kann, sowie einen Schlüssel, der Ausgaben auslöst. Ein selbst gehosteter Runner ist außerdem ausdrücklich ein Ort, an dem Workflow-Code ausgeführt wird. Ein dediziertes Konto ohne privilegierte Rechte und ohne sudo-Berechtigungen auf einem Host, auf dem nichts anderes läuft, bildet die Mindestanforderung. Wenn Sie zusätzlich interaktive Agents ausführen, die Code auschecken, ist eine verworfene VM pro Agent das belastbare Muster. Einen Coding-Agent auf einem VPS auszuführen behandelt die allgemeine Einrichtung. Wenn die Anthropic API für Sie neu ist, bietet eine erste Claude-API-Anwendung auf einem VPS einen kleineren Einstieg als dieses Setup.
FAQ
Benötigt ein KI-Agent für PR-Reviews Schreibzugriff auf mein Repository?
Nein. Er benötigt pull-requests: write, um ein Review zu veröffentlichen, und contents: read, um das Diff abzurufen. Das ist die vollständige Liste. Sie legen diese Berechtigungen im permissions:-Block des Workflows fest. Dieser begrenzt, was der auftragsbezogene GITHUB_TOKEN-Block ausführen darf. Mit diesen beiden Zeilen kann der Agent einen Pull Request kommentieren, aber keinen Commit pushen und keinen Branch mergen. Veröffentlichen Sie Reviews mit event: COMMENT statt mit REQUEST_CHANGES. Dadurch kann der Agent einen Merge ebenfalls nicht blockieren.
Warum schlägt mein Review-Kommentar mit HTTP 422 fehl?
GitHub akzeptiert einen Inline-Review-Kommentar nur für eine Zeile, die Bestandteil des Pull-Request-Diffs ist. Andernfalls gibt GitHub Pull request review thread line must be part of the diff zurück. Prüfen Sie, dass path relativ zum Repository angegeben ist und keinen b/-Präfix aus dem Diff-Header enthält. Außerdem muss die Zeilennummer innerhalb eines Hunk dieser Datei liegen. side muss für eine hinzugefügte oder unveränderte Zeile RIGHT und für eine entfernte Zeile LEFT sein. Wenn Sie jeder Diff-Zeile vor der Übergabe an das Modell die Zeilennummer der neuen Datei voranstellen, kann das Modell von Anfang an keine falschen Nummern erfinden.
Kann ich dies für ein öffentliches Repository mit Pull Requests aus Forks ausführen?
Nicht mit diesem Design. GitHub übergibt keine Secrets an einen Workflow, der aus einem Fork ausgelöst wird. Daher fehlt der Modellschlüssel, und der Lauf schlägt fehl. GitHub weist außerdem darauf hin, dass selbst gehostete Runner „fast nie für öffentliche Repositories verwendet werden sollten“, weil jeder einen Pull Request öffnen kann, der Code auf Ihrem Rechner ausführt. Beschränken Sie den Reviewer bei einem öffentlichen Projekt entweder auf Branches, die direkt in das Repository gepusht wurden. Genau das bewirkt die if:-Bedingung. Oder verlagern Sie den Review-Schritt auf einen von GitHub gehosteten Runner und akzeptieren Sie, dass das Diff Ihre eigene Infrastruktur verlässt.
Welches Modell sollte ich für Pull-Request-Reviews verwenden?
Beginnen Sie mit Haiku 4.5. Ein begrenztes Diff anhand einer festen Liste von Fehlertypen zu prüfen, ist kein schwieriges Reasoning-Problem. Das günstigste Modell hält die monatlichen Kosten auf einem Betrag, über den niemand diskutiert. Wechseln Sie zu Sonnet 5, wenn das Modell echte Fehler in Ihrer Sprache oder Ihrem Framework übersieht. Messen Sie dies, statt es nur anzunehmen. Opus 5 ist bei den Kosten pro Pull Request mit deutlichem Abstand das teuerste der drei Modelle. Das lässt sich für einen Release-Branch leichter rechtfertigen als für jeden Push in jeden Feature-Branch.