استضافة وكيل مراجعة PR بالذكاء الاصطناعي على VPS
شغّل مراجع PR على VPS تملكه مع مشغّل GitHub Actions مستضاف ذاتياً، وأرسل الفرق فقط لتعليقات مضمّنة، مع فلاتر المسار والحجم وحساب التكلفة لكل PR.
ما الذي يفعله وكيل مراجعة PR مستضاف ذاتياً
وكيل مراجعة PR مستضاف ذاتياً هو برنامج صغير يعمل على خادم تملكه. يقرأ الفرق الخاص بطلب السحب (PR)، ويرسل الأسطر التي تغيّرت فقط إلى نموذج. ثم ينشر الناتج على شكل تعليقات مضمنة في المراجعة. لا يسحب فرعك إلى نسخة عمل، ولا يقرأ أي ملف لم يلمسه طلب السحب. بيانات الاعتماد الوحيدة التي يحتفظ بها هي مفتاح API واحد للنموذج، ورمز مميز واحد يتيح له إضافة التعليقات ولا يتيح له تنفيذ أي إجراء آخر. هذا تعريف محدود للوكيل، وهو أقرب إلى prompt تسبقه عوامل تصفية منه إلى أي شيء مستقل التشغيل. وإذا أردت فهم الصورة الكاملة قبل إنشاء هذا الوكيل، فتغطي المسار المتدرج من المفاهيم إلى حلقة تكتبها بنفسك الأساسيات اللازمة.
يمكن للنموذج قراءة الفرق. هذا الجزء محلول. المهم هو المكان الذي يذهب إليه الفرق والجهة التي تحتفظ بالمفتاح. يعني روبوت المراجعة المستضاف أن كل فرق من كل مستودع خاص يغادر شبكتك، ويصل إلى سجلات جهة خارجية، ويخضع لسياسة الاحتفاظ الخاصة بها. على VPS تملكه، ينتقل الفرق من GitHub إلى خادمك ثم إلى API النموذج، ويمكنك قراءة أسطر التعليمات البرمجية الأربعين التي تحدد ما يُرسل.
ما تحتاج إليه قبل البدء
- VPS يعمل بنظام Ubuntu 24.04، ومسجّل فيه مشغّل GitHub Actions مستضاف ذاتياً في المستودع. أضف إليه التصنيف
pr-reviewعند تسجيله، لأن سير العمل أدناه يحدّد المشغّل وفق هذا التصنيف. - مفتاح API من Anthropic صادر عن Claude Console.
- مستودع تتحكم في الأشخاص الذين يمكنهم فتح طلبات سحب فيه. المستودع الخاص هو الحالة الأسهل. يغطّي القسم التالي حالة المستودع العام، لكن الإجابة هناك أقل اطمئناناً.
ثبّت أداة المراجعة على VPS
تعمل خدمة runner باستخدام الحساب غير المميّز الذي أنشأته عند تشغيل ./svc.sh install. ثبّت أداة المراجعة باستخدام الحساب نفسه، حتى تتمكن المهمة من تشغيلها دون sudo. استبدل runner أدناه باسم حسابك.
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 --versionيطبع gh --version القيمة gh version 2.45.0 على Ubuntu 24.04 اعتباراً من August 2026. يدعم أي إصدار يبدأ من 2.20 العلامة --input المستخدمة أدناه. تعني رسالة Command 'gh' not found أن مكوّن universe غير مفعّل، لذا شغّل sudo add-apt-repository universe ثم حاول مرة أخرى.
مكان تخزين المفتاح والرمز المميز
سِرّان، ولكل منهما مدة صلاحية مختلفة. لا تضع أياً منهما في المستودع.
ANTHROPIC_API_KEY هو سر خاص بالمستودع، وتضبطه من خلال Settings، ثم Secrets and variables، ثم Actions. يشفّره GitHub ويحقنه في بيئة الخطوة وقت التشغيل. لا يكون هذا السر ملفاً على القرص، ولا يظهر في سجل git.
يعمل GITHUB_TOKEN بطريقة مختلفة. ينشئ Actions رمزاً مميزاً جديداً لكل مهمة، ويتلفه عند انتهاء المهمة. تحدد كتلة permissions: في سير العمل ما يمكن لهذا الرمز فعله، وهنا تُطبَّق فعلياً قاعدة أقل الصلاحيات:
permissions:
contents: read
pull-requests: writeيمكن لهذا الرمز نشر مراجعة. لكنه لا يستطيع دفع commit، أو دمج branch، أو تعديل ملف سير عمل، أو الوصول إلى مستودع آخر. الوكيل الذي يستطيع إضافة تعليق هو مراجع. أما الوكيل الذي يستطيع الدفع فهو committer، ولم يوافق أحد على منحه هذه الصلاحية. تعامل مع مفتاح النموذج بالعناية نفسها، لأنه ينفق أموالاً من حسابك. ستجد مزيداً من المعلومات عن هذا النوع من المشكلات في إبعاد الأسرار عن متناول وكيل الذكاء الاصطناعي.
يستبدل Actions سلسلة السر المطابقة تماماً بـ *** في سجلات المهمة. وتتم المطابقة مع السلسلة الأصلية كاملة فقط، لذلك قد يظهر المفتاح بوضوح إذا شفّرته باستخدام base64، أو قسمته على سطرين، أو طبعته حرفاً واحداً في كل مرة. لا تضف خطوة تصحيح تعرض محتويات البيئة بالكامل.
لماذا لا يرى pull request من fork مفتاح API الخاص بك
قاعدة GitHub واضحة: باستثناء GITHUB_TOKEN، لا تُمرَّر الأسرار إلى runner عندما يُشغَّل workflow من مستودع fork. لذلك يبدأ تشغيل pull_request من fork النص البرمجي لديك من دون ANTHROPIC_API_KEY، ويفشل أول استدعاء لـAPI مع invalid x-api-key.
الحل المغري هو تغيير المشغّل إلى pull_request_target، إذ يُشغَّل هذا المشغّل في سياق المستودع الأساسي ويحصل فعلاً على الأسرار. لا تفعل ذلك هنا. تنص إرشادات الأمان الخاصة بـGitHub على أن هذه workflows «تتمتع بصلاحيات، ما يعني أنها تشترك في ذاكرة التخزين المؤقت للفرع الرئيسي مع مشغّلات workflow أخرى ذات صلاحيات، وقد تملك صلاحية الكتابة إلى المستودع والوصول إلى الأسرار المشار إليها»، وأن النتيجة «يمكن استغلالها للاستيلاء على مستودع».
وتتحدث الإرشادات نفسها بوضوح عن runner: «ينبغي عدم استخدام self-hosted runners تقريباً أبداً مع المستودعات العامة على GitHub، لأن أي مستخدم يمكنه فتح pull requests ضد المستودع واختراق البيئة».
يفرض ذلك خيارين في التصميم. تتضمن المهمة guard بحيث لا تُشغَّل إلا على الفروع التي دُفعت إلى مستودعك الخاص. كما أن workflow لا يتضمن خطوة actions/checkout إطلاقاً. لا يملك agent الفرع على القرص، لذلك لا يكون pull request الضار سوى نص يُرسل إلى نموذج. ولا يمكنه تشغيل build script على VPS لديك، لأن لا شيء على VPS لديك يشغّله أصلاً. لكن النص ليس مرادفاً للمدخل الآمن: فالـdiff الذي يكتبه شخص مجهول هو input غير موثوق يصل إلى نموذج، ضمن حد الثقة نفسه الذي تواجهه عندما تمنح agent إمكانية البحث على الويب، والشيء الوحيد الذي يحصره هنا هو أن هذا agent لا يستطيع سوى نشر تعليق.
اجلب الفرق، لا المستودع
يعيد طلب واحد كامل الفرق كنص عادي.
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"إن نوع الوسائط Accept: application/vnd.github.diff هو الذي يحوّل الاستجابة من كائن JSON يصف طلب السحب إلى الفرق الموحّد نفسه، بينما يطبع gh api ذلك المحتوى دون تغيير. يجب أن يبدأ السطر الأول الذي تراه بـ diff --git a/. يعني gh: Not Found (HTTP 404) أن الرمز لا يستطيع رؤية المستودع، وهذا يعني غالباً في حالة الرمز الشخصي ذي الصلاحيات الدقيقة أن إذن Pull requests لم يُفعّل.
رشِّح قبل أن تنفق رمزاً واحداً
هذا القسم هو ما يحدد الفرق بين روبوت يقرأ الناس مخرجاته وروبوت يكتمونه. يعمل كل مرشح أدناه قبل أن يرى النموذج بايتاً واحداً.
- مرشحات المسارات. استبعد ملفات القفل، والأدلة المضمّنة، والحزم المصغّرة، والشيفرة المُولّدة. تعليق النموذج على
package-lock.jsonلا يضيف أي قيمة، وغالباً ما تحتوي هذه الملفات على معظم البايتات في الفرق. - حدّ للحجم. إذا تجاوز التغيير هذا الحد، فتجاوز المراجعة واخرج بنجاح. بدلاً من ستين تخميناً، يعرض تغيير إعادة هيكلة من 4,000 سطر رسالة صريحة واحدة تفيد بأنه كبير جداً للمراجعة التلقائية.
- حدّ للخطورة وحدّ للتعليقات. أبلغ عن النتائج عالية ومتوسطة الخطورة، وبحد أقصى عشرة منها، بدءاً بالأعلى خطورة. لا أحد يقرأ التعليق الحادي عشر.
البرنامج النصي
احفظ هذا في /opt/pr-review/review.py. يقرأ إعداداته من البيئة، لذلك يمكن لسير العمل تغيير النماذج من دون تعديل في الشيفرة.
#!/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,
).stdoutتقسيم الفرق حسب الملف هو ما يتيح تصفية المسارات. وترقيم كل سطر هو ما يجعل تعليقات المراجعة تظهر في مواضعها. لا تقبل GitHub التعليق المضمّن إلا على سطر يشكل جزءاً من الفرق، لذلك يجب أن يذكر النموذج رقم سطر حقيقياً. تزويده بالأرقام يعني أنه يستطيع نسخ أحدها بدلاً من اختراع رقم.
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)يحمل عنوان المقطع أرقام الأسطر. يوضح @@ -12,7 +12,9 @@ أن مقطع الملف الجديد يبدأ عند السطر 12، لذلك يبدأ العداد من هناك ويتقدم مع الأسطر المضافة وغير المتغيرة فقط. تمر الأسطر المحذوفة من دون ترقيم، لأنها غير موجودة في الملف الجديد. يتجاوز الشرط الخاص بالأسطر التي تبدأ بشرطة مائلة عكسية العلامة التي يكتبها git عند نهاية الملف للإشارة إلى عدم وجود سطر جديد؛ إذ كانت هذه العلامة ستزيح كل رقم لاحق بمقدار واحد.
يستخدم كلا الخروجين الحالة 0، وليس 1. يجب أن يظهر طلب السحب الذي تمت تصفيته أو الذي يتجاوز الحجم المسموح به كفحص ناجح. أما الفحص الأحمر الذي لا يستطيع الإنسان اتخاذ إجراء بناءً عليه فسيُتجاهل، وبمجرد تجاهل فحص واحد، تُتجاهل جميع الفحوص.
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,
)توجد ثلاثة تفاصيل أساسية في هذه الكتلة. تُقتطع JSON بين أول { وآخر }، لأن النموذج قد يضع إجابته أحياناً داخل كتلة تعليمات برمجية، ولأن json.loads يفشل عند وجود هذه الكتلة. ويُجرَّد path من b/ البادئة، لأن هذه البادئة تأتي من عنوان الفرق، بينما تريد GitHub مساراً نسبياً إلى المستودع. ويرسل --input - المراجعة كاملة في استدعاء API واحد، لذلك تصل عشر نتائج في إشعار واحد بدلاً من عشرة إشعارات.
عندما لا يوجد ما يستدعي الإبلاغ، لا ينشر البرنامج النصي شيئاً. فالروبوت الذي يكتب «لم يتم العثور على مشكلات» في كل طلب سحب يعلّم الناس تجاوز هذه الرسالة، ثم يتجاوزون الرسالة التي كانت مهمة.
اربطه بسير العمل
احفظ هذا باسم .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.pyلا يظهر GITHUB_REPOSITORY داخل كتلة env: تلك، لأن Actions يعيّنه لكل مهمة بالفعل. تهم مجموعة concurrency بالنسبة إلى التكلفة: من دونها، يؤدي دفع ثلاثة إصلاحات سريعة إلى فرع إلى تشغيل ثلاث مراجعات كاملة، وتدفع تكلفتها كلها. أما معها، فلا تبقى سوى المراجعة الأخيرة.
ينفّذ سطر if: مهمتين. يتجاوز النصف الأول طلبات السحب القادمة من المستودعات المتشعبة، لأنها ستفشل على أي حال من دون مفتاح. أما النصف الثاني، فيمنح فريقك مفتاح إيقاف: أضف التصنيف no-ai-review إلى طلب سحب، ولن تُشغَّل المهمة.
افتح طلب سحب وراقب ما يحدث:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsيعمل التشغيل بشكل صحيح إذا اكتمل خلال بضع ثوانٍ وظهر nothing above the severity threshold; posting no comment في السجل. هذه هي النتيجة المتوقعة في طلب سحب صغير ونظيف.
ما تكلفة مراجعة طلب سحب مؤتمتة؟
يمثل diff الجزء الأكبر من الإدخال تقريباً، لذلك يحدد حجم diff السعر. فيما يلي diff مقاسه 500 سطر، بالإضافة إلى system prompt، وقد حُسب باستخدام نقطة نهاية عدّ الرموز بدلاً من التقدير.
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"
}
]استهلك diff هذا 8,000 من رموز الإدخال على Haiku 4.5، و10,400 على Sonnet 5. النص نفسه، لكن العدد مختلف. تستخدم نماذج Claude بدءاً من 4.7 أداة ترميز أحدث تنتج رموزاً أكثر بنحو 30% للإدخال نفسه، كما توضح Anthropic في صفحة الأسعار الخاصة بها. ضع ذلك في الحسبان عند مقارنة نموذج أحدث بآخر أقدم اعتماداً على سعر كل مليون رمز فقط.
الأسعار المعلنة اعتباراً من August 2026: يبلغ سعر Haiku 4.5 مقدار $1 لكل مليون رمز إدخال و$5 لكل مليون رمز إخراج. ويبلغ سعر Sonnet 5 مقدار $2 و$10 ضمن السعر التمهيدي الساري حتى 31 August 2026، ثم يصبح $3 و$15. أما Opus 5 فسعره $5 و$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"
}
]تبلغ التكلفة 1.4 سنتاً لكل طلب سحب على Haiku 4.5، و9.1 سنتاً على Opus 5. يدفع فريق يدمج 200 طلب سحب شهرياً نحو $2.80 على Haiku 4.5، أو $7.28 على Sonnet 5، أو $18.20 على Opus 5. اعتباراً من 1 September 2026، اضرب قيمة صف Sonnet 5 في 1.5.
هناك عاملان يرفعان الفاتورة الفعلية فوق هذا التقدير. يراجع مشغّل synchronize كل push، لذلك تكلف branch نشطة تحتوي على ثمانية pushes ثماني مراجعات، ولا تفيد قاعدة التزامن إلا عندما تصل pushes بفواصل زمنية قصيرة. تفترض الأرقام أيضاً أن path filters تعمل كما ينبغي؛ إذ يمكن لملف lock واحد غير مفلتر أن يضاعف الإدخال بمفرده.
لا يفيد prompt caching هنا. يجب أن تكون البادئة المخزنة مؤقتاً مطابقة للبايتات بين الاستدعاءات، بينما يختلف diff في كل مرة. وsystem prompt هو الجزء المستقر الوحيد، لكنه أقصر بكثير من الحد الأدنى القابل للتخزين المؤقت. للاطلاع على القاعدة العامة، راجع متى يصبح prompt caching مجدياً، ولاختيار النموذج المناسب لكل مهمة من النماذج الثلاثة أعلاه، راجع أي نموذج Claude تستخدم لكل مهمة.
قِس diff الخاص بك قبل تفعيلها
أضف هذا السطر بعد إنشاء payload، ثم شغّل script يدوياً على عدد من طلبات السحب من الشهر الماضي:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)لا تشغّل نقطة النهاية الخاصة بالعد النموذج، لذلك لا تستهلك رموز إدخال أو إخراج، كما أنها تستخدم أداة الترميز الخاصة بالنموذج الذي تحدده. شغّلها على عشرة طلبات سحب فعلية من مستودعك، واستخدم الوسيط بدلاً من المتوسط حتى لا تؤثر عملية ترحيل ضخمة واحدة في التقدير.
لماذا يتم كتم روبوتات المراجعة وكيف تتجنب ذلك
هناك سلوكان يدمران الثقة بهذه الروبوتات، ولكل منهما إصلاح في الشيفرة أعلاه.
مراجعة كل شيء دفعة واحدة. الروبوت الذي يترك أربعين تعليقاً لن يقرأ أحد تعليقاته. عتبة الخطورة والحد الأقصى البالغ عشرة تعليقات ليسا مسألة مجاملة، بل هما ما يُبقي النتائج الفعلية ظاهرة. ويؤدي الفرز حسب الخطورة قبل الاقتطاع إلى حذف النتائج الأقل أهمية بدلاً من حذف عشرة نتائج عشوائية.
التعليق بثقة على شيء لا يستطيع التحقق منه. هذا هو السلوك الذي يدفع المهندسين إلى إيقاف الروبوت نهائياً. إذا عُرض على نموذج 200 سطر من قاعدة شيفرة تتكون من 40,000 سطر، فسيظل يكتب: "هذا يعطّل إبطال ذاكرة التخزين المؤقت في redis_client.py" عن ملف لم يره قط. وتدفعه مطالبة النظام، بلغة واضحة، إلى الإبلاغ فقط عن العيوب الظاهرة في الأسطر المعروضة، وإغفال أي شيء لا يثق به. إن تسمية الفشل مباشرة أكثر فاعلية من طلب الدقة عموماً، وإخبار النموذج بأن النتيجة الفارغة طبيعية هو ما يمنعه من اختلاق شيء يقوله عن تغيير مكوّن من سطرين. أما العيوب التي لا تظهر إلا عبر المستودع بأكمله فهي مهمة مختلفة، ويُعد الاستضافة الذاتية لـ open-kritt لفحص الأمان إحدى طرق تغطية ذلك النطاق دون توسيع ما يُسمح لهذا المراجع برؤيته.
انشر المراجعة بصفتها COMMENT، وليس بصفتها REQUEST_CHANGES. يجب ألا يكون رأي النموذج قادراً على منع الدمج، لأنه بمجرد أن يصبح قادراً على ذلك، سيزيل شخص لديه موعد نهائي سير العمل بأكمله بدلاً من الجدال معه. وعندما تريد فعلاً أن يكون لحكم الوكيل وزن حقيقي، فالحل هو أن تجعله ينتج شيئاً يمكنك التحقق منه بدلاً من شيء يتعين عليك تصديقه. وهذه هي الفكرة وراء أن يسلّمك الوكيل تقرير أدلة يمكنك إعادة تشغيله بنفسك.
أوضاع الفشل، مع النصوص التي ستظهر لك
HTTP 422 عند نشر المراجعة. تطبع gh القيمة gh: Unprocessable Entity (HTTP 422)، ويذكر نص الاستجابة الحقل: Pull request review thread line must be part of the diff. لا يستطيع GitHub ربط ذلك التعليق بموضع في diff. الأسباب المعتادة هي رقم سطر اخترعه النموذج، أو قيمة path لا تزال تحمل البادئة b/، أو تعليق على سطر محذوف، ويتطلب ذلك ضبط side على LEFT بدلاً من RIGHT. اطبع JSON الخاص بالمراجعة قبل نشره، وتحقق يدوياً من تعليق واحد بمقارنته مع diff.
invalid x-api-key من واجهة model API. تفشل الخطوة عند أول استدعاء messages.create. إما أن السر ANTHROPIC_API_KEY غير مضبوط في المستودع، أو أن pull request أُنشئ من fork، ولذلك لم تمرر Actions أي أسرار. كان ينبغي أن يتجاوزه شرط fork في السطر if:، لذا تحقق من ذلك السطر أولاً.
gh: Resource not accessible by integration (HTTP 403). لا يستطيع رمز job الكتابة إلى pull requests. أضف pull-requests: write إلى كتلة permissions:. إذا كان موجوداً بالفعل، فانتقل إلى Settings، ثم Actions، ثم General، حيث يمكن لسياسة المؤسسة تقييد ما يُسمح لأي رمز workflow بطلبه.
json.decoder.JSONDecodeError. لم يُرجع النموذج JSON قابلاً للتحليل. السبب الشائع هو أن الاستجابة بلغت حد الرموز وتوقفت في منتصف الكائن. يطبع سطر السجل stop_reason لهذا الغرض تحديداً: تعني قيمة max_tokens رفع max_tokens أو خفض MAX_COMMENTS.
لا يعمل workflow مطلقاً. لا يعرض gh run list أي شيء لـ pull request. تحقق من أن paths-ignore لم يستبعد كل ملف تم تغييره، ثم تحقق من شرط fork وشرط label، وبعد ذلك تحقق من أن runner يعمل باستخدام sudo systemctl status 'actions.runner.*' على VPS. يترك runner غير المتصل job في قائمة الانتظار من دون أي رسالة خطأ في pull request.
تعود كل مراجعة فارغة. اضبط MIN_SEVERITY على low لتشغيل واحد. إذا ظهرت النتائج، فهذا يعني أن threshold يعمل كما ينبغي. وإذا لم تظهر أي نتائج، فاطبع payload وتأكد من أن الفلاتر لم تستبعد diff بالكامل.
تشغيله إلى جانب الوكلاء الآخرين
المراجع صغير، لذلك قد تميل إلى وضعه على الخادم الذي يشغّل كل شيء آخر بالفعل. أبقه منفصلاً إذا كان المستودع مهماً. تحتفظ هذه العملية برمز مميز يمكنه التعليق على تعليماتك البرمجية، وبمفتاح ينفق أموالك، كما أن العداء المستضاف ذاتياً هو، بحكم تصميمه، مكان تُنفَّذ فيه تعليمات سير العمل البرمجية. حساب مخصص غير مميّز بلا صلاحيات sudo، على مضيف لا يشغّل أي شيء آخر، هو خط الأساس. إذا كنت تشغّل أيضاً وكلاء تفاعليين يسحبون التعليمات البرمجية، فإن آلة افتراضية مؤقتة لكل وكيل هي النمط العملي، بينما يشرح تشغيل وكيل برمجي على VPS الإعداد العام. إذا كانت Anthropic API جديدة عليك، فإن أول تطبيق Claude API على VPS يمثل نقطة بداية أصغر من هذا الدليل.
FAQ
هل يحتاج وكيل مراجعة PR يعمل بالذكاء الاصطناعي إلى صلاحية الكتابة في مستودعي؟
لا. يحتاج إلى pull-requests: write لنشر مراجعة، وإلى contents: read لجلب الفرق. هذه هي القائمة كاملة، وتضبطها في كتلة permissions: ضمن سير العمل، وهي التي تحدد ما يمكن لـGITHUB_TOKEN لكل مهمة تنفيذه. باستخدام هذين السطرين، يستطيع الوكيل التعليق على طلب سحب، لكنه لا يستطيع دفع commit أو دمج فرع. انشر المراجعات باستخدام event: COMMENT بدلاً من REQUEST_CHANGES، حتى لا يتمكن أيضاً من منع الدمج.
لماذا يفشل تعليقي في المراجعة مع HTTP 422؟
يقبل GitHub تعليق المراجعة المضمّن على سطر موجود ضمن فرق طلب السحب فقط، ويعيد Pull request review thread line must be part of the diff عندما لا يكون السطر كذلك. تحقق من أن path نسبي إلى المستودع ولا يتضمن البادئة b/ من ترويسة الفرق، وأن رقم السطر يقع داخل hunk الخاص بهذا الملف. يجب أن تكون side بقيمة RIGHT للسطر المضاف أو غير المتغير، وبقيمة LEFT للسطر المحذوف. إن إضافة رقم سطر الملف الجديد إلى بداية كل سطر في الفرق قبل إرساله إلى النموذج تمنع النموذج من اختلاق الأرقام من الأساس.
هل يمكنني تشغيل هذا على مستودع عام يحتوي على طلبات سحب من forks؟
ليس بهذا التصميم. لا يمرر GitHub الأسرار إلى سير عمل يتم تشغيله من fork، ولذلك يكون مفتاح النموذج مفقوداً ويفشل التشغيل. كما يذكر GitHub أن self-hosted runners «ينبغي ألا تُستخدم تقريباً أبداً مع المستودعات العامة»، لأن أي شخص يستطيع فتح طلب سحب يتسبب في تشغيل تعليمات برمجية على جهازك. بالنسبة إلى مشروع عام، قيّد المراجع إلى الفروع التي دُفعت إلى المستودع نفسه، وهو ما يفعله الحارس if:، أو انقل خطوة المراجعة إلى runner مستضاف من GitHub وتقبّل أن يغادر الفرق بنيتك التحتية.
أي نموذج ينبغي أن أستخدم لمراجعة طلبات السحب؟
ابدأ باستخدام Haiku 4.5. إن قراءة فرق محدود مقابل قائمة ثابتة من أنواع العيوب ليست مسألة تتطلب استدلالاً معقداً، كما أن النموذج الأرخص يحافظ على الفاتورة الشهرية عند رقم لا يثير اعتراض أحد. انتقل إلى Sonnet 5 إذا وجدت أنه يفوّت أخطاء حقيقية في لغتك أو إطار العمل الذي تستخدمه، وقِس ذلك بدلاً من افتراضه. يُعد Opus 5 الأغلى بين النماذج الثلاثة بفارق كبير لكل طلب سحب، ولذلك يسهل تبرير استخدامه في فرع إصدار أكثر من استخدامه مع كل دفع إلى كل فرع ميزة.