SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

راه اندازی عامل بررسی هوشمند PR روی VPS شخصی

با میزبانی یک عامل بررسی کد روی VPS، امنیت کدهای خصوصی خود را حفظ کنید. این راهنما نحوه پیکربندی فیلترهای مسیر، محدودیت حجم diff و ثبت نظرات inline را به شما آموزش می‌دهد.

یک عامل بررسی PR خود-میزبانی‌شده چه کاری انجام می‌دهد

یک عامل بررسی PR خود-میزبانی‌شده، برنامه کوچکی است که روی سروری که مالک آن هستید اجرا می‌شود. این برنامه تفاوت‌های (diff) یک Pull Request (PR) را می‌خواند و فقط خطوط تغییریافته را به یک مدل ارسال می‌کند. آنچه بازگردانده می‌شود، به عنوان نظرات بررسی درون‌خطی (inline review comments) ثبت می‌گردد. این عامل هرگز شاخه (branch) شما را checkout نمی‌کند و هیچ فایلی را که در Pull Request تغییری نکرده است، نمی‌خواند. تنها اعتبارنامه‌هایی که در اختیار دارد، یک کلید API (رابط برنامه‌نویسی کاربردی) مدل و یک توکن است که فقط اجازه ثبت نظر دارد و هیچ کار دیگری نمی‌تواند انجام دهد.

یک مدل می‌تواند یک diff را بخواند. این بخش حل شده است. آنچه اهمیت دارد این است که diff به کجا می‌رود و چه کسی کلید را در اختیار دارد. یک ربات بررسی میزبانی‌شده (hosted) به این معنی است که هر diff از هر مخزن خصوصی، شبکه شما را ترک می‌کند، در لاگ‌های شخص ثالث قرار می‌گیرد و تحت سیاست نگهداری داده‌های آن‌ها باقی می‌ماند. روی یک VPS (سرور خصوصی مجازی) که مالک آن هستید، diff از GitHub به سرور شما و سپس به API مدل می‌رود و شما می‌توانید آن چهل خط کدی را که تصمیم می‌گیرد چه چیزی ارسال شود، مطالعه کنید.

پیش‌نیازهای شروع کار

  • یک سرور مجازی (VPS) با سیستم‌عامل Ubuntu 24.04 که یک GitHub Actions runner خودمیزبان روی آن ثبت شده باشد. هنگام ثبت runner، برچسب اضافی pr-review را به آن اختصاص دهید، زیرا گردش‌کار (workflow) زیر بر اساس همین برچسب انتخاب می‌شود.
  • یک کلید API از Anthropic که از طریق Claude Console دریافت شده باشد.
  • مخزنی (repository) که در آن کنترل دسترسی برای ایجاد pull request در اختیار شما باشد. مخازن خصوصی ساده‌ترین حالت هستند. بخش مربوط به fork در ادامه، حالت عمومی را پوشش می‌دهد، هرچند راهکار آن محدودیت‌های بیشتری دارد.

نصب reviewer روی VPS

سرویس runner با همان کاربری بدون دسترسی ریشه (unprivileged) که هنگام اجرای ./svc.sh install ایجاد کردید، اجرا می‌شود. reviewer را تحت همان حساب کاربری نصب کنید تا job بتواند بدون نیاز به 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 تا اوت 2026 در Ubuntu 24.04 خروجی gh version 2.45.0 را نمایش می‌دهد. هر نسخه‌ای از 2.20 به بعد، دارای فلگ --input است که در ادامه استفاده شده است. پیام Command 'gh' not found به این معناست که مؤلفه universe فعال نیست؛ بنابراین sudo add-apt-repository universe را اجرا کرده و دوباره تلاش کنید.

محل نگهداری کلید و توکن

دو راز، دو طول عمر متفاوت. هیچ‌کدام نباید در مخزن (repository) قرار بگیرند.

ANTHROPIC_API_KEY یک راز مخزن است که در بخش Settings و سپس Secrets and variables و در نهایت Actions تنظیم می‌شود. GitHub آن را رمزنگاری کرده و در زمان اجرا به محیط (environment) آن مرحله تزریق می‌کند. این راز هرگز به صورت یک فایل روی دیسک ذخیره نمی‌شود و در تاریخچه git نیز وجود ندارد.

GITHUB_TOKEN عملکرد متفاوتی دارد. Actions برای هر job یک توکن جدید صادر می‌کند و پس از پایان job آن را از بین می‌برد. کارهایی که این توکن مجاز به انجام آن‌هاست توسط بلوک permissions: در workflow تعیین می‌شود؛ بنابراین اصل «حداقل دسترسی» (least privilege) در اینجا اعمال می‌گردد:

permissions:
  contents: read
  pull-requests: write

این توکن می‌تواند یک review ثبت کند. این توکن قادر نیست commit ارسال کند، branch ادغام کند، فایل workflow را ویرایش کند یا به مخزن دیگری دسترسی داشته باشد. عاملی (agent) که می‌تواند نظر بدهد، یک بازبین (reviewer) است. عاملی که می‌تواند push انجام دهد، یک committer است و هیچ‌کس با چنین دسترسی موافقی ندارد. با کلید مدل نیز با همان دقت رفتار کنید، زیرا این کلید باعث کسر هزینه از حساب شما می‌شود. اطلاعات بیشتر درباره این نوع چالش در دور نگه داشتن رازها از دسترس عامل هوش مصنوعی موجود است.

Actions رشته دقیق راز را در لاگ‌های job با *** جایگزین می‌کند. این جایگزینی فقط برای رشته دقیق انجام می‌شود؛ بنابراین کلیدی که آن را base64 encode کرده‌اید، در دو خط تقسیم کرده‌اید یا کاراکتر به کاراکتر چاپ می‌کنید، به صورت متن آشکار (clear text) ظاهر خواهد شد. هرگز یک مرحله debug که محیط (environment) را dump می‌کند، اضافه نکنید.

چرا یک pull request از یک fork هرگز کلید API شما را نمی‌بیند

قانون GitHub کوتاه است: به استثنای GITHUB_TOKEN، هنگامی که یک workflow از یک مخزن fork شده اجرا می‌شود، secrets به runner ارسال نمی‌شوند. بنابراین، یک pull_request که از یک fork اجرا می‌شود، اسکریپت شما را بدون ANTHROPIC_API_KEY شروع می‌کند و اولین فراخوانی API با invalid x-api-key شکست می‌خورد.

راه‌حل وسوسه‌انگیز، تغییر trigger به pull_request_target است که در context مخزن اصلی اجرا می‌شود و به secrets دسترسی دارد. این کار را در اینجا انجام ندهید. راهنمای امنیتی خود GitHub می‌گوید که این workflowها «دارای امتیاز هستند، به این معنی که از cache یکسانی با branch اصلی در سایر workflowهای دارای امتیاز استفاده می‌کنند و ممکن است به نوشتن در مخزن و secrets ارجاع‌شده دسترسی داشته باشند» و نتیجه آن «می‌تواند برای تصاحب مخزن مورد سوءاستفاده قرار گیرد».

همین راهنما در مورد runner صریح است: «Self-hosted runnerها تقریباً هرگز نباید برای مخازن عمومی در GitHub استفاده شوند، زیرا هر کاربری می‌تواند pull requestهایی علیه مخزن باز کند و محیط را به خطر بیندازد.»

این موضوع دو انتخاب طراحی را دیکته می‌کند. این job دارای یک محافظ است تا فقط روی branchهایی که به مخزن خودتان push شده‌اند، اجرا شود. و این workflow اصلاً مرحله actions/checkout ندارد. agent هرگز branch را روی دیسک ندارد، بنابراین یک pull request خصمانه فقط متنی است که به یک مدل ارسال می‌شود. این متن نمی‌تواند یک اسکریپت build را روی VPS شما اجرا کند، زیرا هیچ چیزی روی VPS شما آن را اجرا نمی‌کند. البته متن به معنای بی‌خطر بودن نیست: یک diff که توسط یک غریبه نوشته شده، ورودی غیرقابل‌اعتمادی است که به یک مدل می‌رسد؛ همان مرز اعتمادی که هنگام دادن قابلیت جستجوی وب به یک agent با آن مواجه می‌شوید، و تنها چیزی که آن را در اینجا محدود می‌کند این است که این agent کاری جز ارسال یک comment نمی‌تواند انجام دهد.

دریافت diff به جای مخزن

یک درخواست، کل diff را به صورت متن ساده دریافت می‌کند.

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 که pull request را توصیف می‌کند، به خود unified diff تبدیل می‌کند و gh api آن بدنه را بدون تغییر چاپ می‌کند. اولین خطی که مشاهده می‌کنید باید با diff --git a/ شروع شود. یک gh: Not Found (HTTP 404) به این معنی است که توکن نمی‌تواند مخزن را ببیند، که در مورد یک توکن شخصی با دسترسی دقیق (fine-grained)، تقریباً همیشه به این معنی است که مجوز Pull requests فعال نشده است.

پیش از صرف توکن، فیلتر کنید

این بخش تفاوت میان باتی که کاربران آن را می‌خوانند و باتی که کاربران آن را نادیده می‌گیرند (mute) مشخص می‌کند. هر یک از فیلترهای زیر پیش از آنکه مدل حتی یک بایت از داده را ببیند، اجرا می‌شوند.

  • فیلترهای مسیر (Path filters). فایل‌های lock، دایرکتوری‌های vendored، بسته‌های minified و کدهای تولیدشده را مسدود کنید. نظر مدل روی package-lock.json صرفاً نویز است و این فایل‌ها اغلب بیشترین حجم بایت‌ها را در یک diff تشکیل می‌دهند.
  • سقف اندازه (Size cap). اگر حجم از حد مجاز فراتر رفت، بررسی را رد کنید و با وضعیت موفق (green) خارج شوید. برای یک refactor با 4000 خط کد، به جای شصت حدس اشتباه، تنها یک جمله صادقانه بنویسید که حجم کد برای بررسی خودکار بیش از حد زیاد بوده است.
  • آستانه شدت و سقف تعداد نظرات. یافته‌های با شدت بالا و متوسط را گزارش کنید؛ حداکثر تا 10 مورد و به ترتیب اولویت از بالاترین شدت. هیچ‌کس نظر یازدهم را نمی‌خواند.

اسکریپت

این فایل را با نام /opt/pr-review/review.py ذخیره کنید. اسکریپت پیکربندی خود را از محیط (environment) می‌خواند، بنابراین گردش کار می‌تواند بدون تغییر در کد، مدل‌ها را تغییر دهد.

#!/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

تفکیک تفاوت‌ها (diff) به ازای هر فایل، امکان فیلتر کردن مسیرها را فراهم می‌کند. شماره‌گذاری هر خط باعث می‌شود نظرات بازبینی در جای درست قرار بگیرند. GitHub فقط در صورتی نظر درون‌خطی (inline comment) را می‌پذیرد که آن خط بخشی از diff باشد، بنابراین مدل باید به یک شماره خط واقعی ارجاع دهد. ارائه شماره‌ها به مدل باعث می‌شود به‌جای اختراع شماره، یکی از آن‌ها را کپی کند.

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)

سرآیند hunk حاوی شماره‌گذاری است. @@ -12,7 +12,9 @@ می‌گوید hunk فایل جدید از خط 12 شروع می‌شود، بنابراین شمارنده از آنجا شروع شده و فقط برای خطوط اضافه شده و بدون تغییر افزایش می‌یابد. خطوط حذف شده بدون شماره باقی می‌مانند، زیرا در فایل جدید وجود ندارند. محافظی که روی خطوط شروع شده با بک‌اسلش قرار دارد، از نشانگر no-newline که git در انتهای فایل می‌نویسد عبور می‌کند؛ در غیر این صورت، آن نشانگر باعث می‌شد هر شماره بعدی یک واحد جابه‌جا شود.

هر دو خروجی از وضعیت 0 استفاده می‌کنند، نه 1. یک pull request فیلتر شده یا بیش از حد بزرگ باید تیک سبز نشان دهد. تیک قرمزی که انسان نتواند برای آن کاری انجام دهد نادیده گرفته می‌شود و وقتی یک تیک نادیده گرفته شود، همه آن‌ها نادیده گرفته خواهند شد.

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/ ابتدایی پاکسازی می‌شود، زیرا این پیشوند از سرآیند diff می‌آید و GitHub مسیری نسبی به مخزن (repository-relative) می‌خواهد. و --input - کل بازبینی را در یک فراخوانی API ارسال می‌کند، بنابراین ده یافته به‌جای ده اعلان، در قالب یک اعلان واحد ارسال می‌شوند.

وقتی چیزی برای گزارش وجود ندارد، اسکریپت چیزی ارسال نمی‌کند. رباتی که در هر pull request می‌نویسد "هیچ مشکلی یافت نشد"، به افراد یاد می‌دهد که از آن عبور کنند و در نتیجه، از موردی که واقعاً اهمیت دارد نیز عبور خواهند کرد.

ادغام در گردش‌کار

این فایل را با نام .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 آن را به‌صورت پیش‌فرض برای هر job تنظیم می‌کند. گروه concurrency برای هزینه‌ها اهمیت دارد: بدون آن، ارسال سه اصلاحیه سریع به یک branch باعث اجرای سه بررسی کامل می‌شود و شما هزینه هر سه را می‌پردازید، اما با وجود آن، فقط آخرین مورد باقی می‌ماند.

خط if: دو وظیفه دارد. بخش اول، pull requestهای ارسال‌شده از forkها را نادیده می‌گیرد که در هر صورت به دلیل نداشتن کلید با شکست مواجه می‌شدند. بخش دوم، یک کلید قطع اضطراری در اختیار تیم شما قرار می‌دهد: با افزودن برچسب no-ai-review به یک pull request، آن job اجرا نخواهد شد.

یک pull request باز کنید و نتیجه را مشاهده کنید:

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 در لاگ به پایان برسد، به‌درستی کار می‌کند. در یک pull request کوچک و تمیز، این نتیجه مورد انتظار است.

هزینه بررسی خودکار pull request چقدر است؟

تغییرات (diff) تقریباً تمام ورودی را تشکیل می‌دهد، بنابراین اندازه diff تعیین‌کننده قیمت است. در ادامه، یک diff با 500 خط به همراه system prompt آورده شده است که به جای تخمین، با استفاده از endpoint شمارش توکن محاسبه شده است.

ChartTokens for one 500 line pull request diff, measured August 2026
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 در مدل Haiku 4.5 معادل 8,000 توکن ورودی و در مدل Sonnet 5 معادل 10,400 توکن بوده است. متن یکسان است اما تعداد توکن‌ها متفاوت است. مدل‌های Claude از نسخه 4.7 به بعد از tokenizer جدیدتری استفاده می‌کنند که برای ورودی یکسان، حدود 30 درصد توکن بیشتری تولید می‌کند؛ موضوعی که Anthropic در صفحه قیمت‌گذاری خود به آن اشاره کرده است. هنگام مقایسه مدل جدیدتر با قدیمی‌تر صرفاً بر اساس قیمت به ازای هر میلیون توکن، این نکته را در نظر داشته باشید.

قیمت‌های لیست‌شده تا اوت 2026: مدل Haiku 4.5 برابر با 1 دلار به ازای هر میلیون توکن ورودی و 5 دلار به ازای هر میلیون توکن خروجی است. مدل Sonnet 5 تحت قیمت‌گذاری مقدماتی که تا 31 اوت 2026 معتبر است، 2 دلار و 10 دلار، و پس از آن 3 دلار و 15 دلار است. مدل Opus 5 نیز 5 دلار و 25 دلار است.

ChartCost of that one review at list prices, August 2026
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"
  }
]

این هزینه برای هر pull request در مدل Haiku 4.5 برابر با 1.4 سنت و در مدل Opus 5 برابر با 9.1 سنت است. تیمی که ماهانه 200 pull request را merge می‌کند، حدود $2.80 در مدل Haiku 4.5، $7.28 در مدل Sonnet 5، یا $18.20 در مدل Opus 5 پرداخت می‌کند. از 1 سپتامبر 2026، ردیف مربوط به Sonnet 5 را در 1.5 ضرب کنید.

دو عامل باعث افزایش صورت‌حساب واقعی نسبت به این تخمین می‌شوند. synchronize در هر push بررسی را فعال می‌کند، بنابراین یک branch فعال با هشت push، هزینه هشت بررسی را خواهد داشت و قانون concurrency تنها زمانی کمک می‌کند که pushها در فواصل زمانی بسیار نزدیک انجام شوند. این ارقام همچنین فرض می‌کنند که فیلترهای مسیر (path filters) به‌درستی کار می‌کنند: یک فایل lock فیلترنشده می‌تواند به تنهایی ورودی را دو برابر کند.

استفاده از prompt caching در اینجا کمکی نمی‌کند. پیشوند کش‌شده باید بین فراخوانی‌ها از نظر بایت یکسان باشد، در حالی که diff هر بار تغییر می‌کند. system prompt تنها بخش ثابت است و طول آن بسیار کمتر از حداقل طول قابل کش‌شدن است. برای قاعده کلی، به زمانی که prompt caching صرفه اقتصادی دارد مراجعه کنید و برای انتخاب بین سه مدل فوق، کدام مدل Claude برای چه کاری مناسب است را ببینید.

پیش از فعال‌سازی، diffهای خود را اندازه‌گیری کنید

این خط را پس از ساخته‌شدن payload اضافه کنید، سپس اسکریپت را به‌صورت دستی روی چند مورد از pull requestهای ماه گذشته اجرا کنید:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

endpoint شمارش، مدل را اجرا نمی‌کند، بنابراین توکن‌های ورودی یا خروجی مصرف نمی‌شود و از tokenizer متعلق به مدلی که نام می‌برید استفاده می‌کند. آن را روی ده pull request واقعی از مخزن خود اجرا کنید و به جای میانگین، میانه (median) را در نظر بگیرید تا یک migration عظیم، تخمین شما را منحرف نکند.

چرا ربات‌های بازبینی مسدود (mute) می‌شوند و چگونه از آن جلوگیری کنیم

دو رفتار باعث از بین رفتن اعتماد به این ربات‌ها می‌شود و برای هر دو در کد بالا راهکاری وجود دارد.

بازبینی همه موارد به‌صورت یک‌جا. رباتی که چهل نظر ثبت می‌کند، هیچ‌کدام از آن‌ها خوانده نمی‌شود. آستانه شدت (severity threshold) و محدودیت ده نظر، از روی ادب نیست؛ بلکه این موارد باعث می‌شوند یافته‌های واقعی قابل‌مشاهده باقی بمانند. مرتب‌سازی بر اساس شدت پیش از اعمال محدودیت باعث می‌شود که یافته‌های کم‌اهمیت‌تر حذف شوند، نه ده مورد تصادفی.

اظهارنظر قطعی درباره چیزی که ربات نمی‌تواند بررسی کند. این همان رفتاری است که باعث می‌شود مهندسان ربات را برای همیشه غیرفعال کنند. مدلی که 200 خط از یک کدبیس 40,000 خطی به آن نشان داده شده، همچنان ممکن است بنویسد «این مورد باعث خرابی ابطال کش در redis_client.py می‌شود»؛ در حالی که هرگز آن فایل را ندیده است. پرامپت سیستم با زبانی ساده این موضوع را کنترل می‌کند: فقط نقص‌های قابل‌مشاهده در خطوط ارائه‌شده را گزارش کنید و هر موردی که از آن اطمینان ندارید را نادیده بگیرید. نام بردن مستقیم از خطا، بهتر از درخواست کلی برای دقت عمل می‌کند؛ همچنین به مدل بگویید که نتیجه خالی (empty result) یک وضعیت عادی است؛ این کار مانع از آن می‌شود که مدل برای یک تغییر دو خطی، به‌زور چیزی از خود بسازد.

بازبینی را به‌عنوان COMMENT ارسال کنید، هرگز به‌عنوان REQUEST_CHANGES ارسال نکنید. نظر یک مدل نباید قادر به مسدود کردن یک merge باشد؛ به محض اینکه چنین اتفاقی بیفتد، شخصی که ددلاین دارد، به‌جای بحث کردن با ربات، کل workflow را حذف خواهد کرد.

حالت‌های شکست و پیام‌های مربوطه

خطای HTTP 422 هنگام ارسال بررسی. gh مقدار gh: Unprocessable Entity (HTTP 422) را چاپ می‌کند و بدنه پاسخ، نام فیلد را مشخص می‌کند: Pull request review thread line must be part of the diff. گیت‌هاب نمی‌تواند آن کامنت را لنگر (anchor) کند. دلایل معمول عبارتند از: شماره خطی که مدل ابداع کرده است، یک path که هنوز پیشوند b/ را دارد، یا کامنتی روی یک خط حذف‌شده که نیاز دارد side به جای RIGHT روی LEFT تنظیم شود. پیش از ارسال، JSON بررسی را چاپ کنید و یک کامنت را به‌صورت دستی با diff تطبیق دهید.

خطای invalid x-api-key از API مدل. این مرحله در اولین فراخوانی messages.create شکست می‌خورد. یا secret مربوط به ANTHROPIC_API_KEY در مخزن تنظیم نشده است، یا pull request از یک fork ارسال شده که باعث می‌شود Actions هیچ secretای را منتقل نکند. محافظ fork در خط if: باید آن را نادیده می‌گرفت، پس ابتدا آن خط را بررسی کنید.

خطای gh: Resource not accessible by integration (HTTP 403). توکن job اجازه نوشتن روی pull requestها را ندارد. مقدار 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 را چک کنید، و در نهایت با دستور sudo systemctl status 'actions.runner.*' روی VPS بررسی کنید که runner فعال است یا خیر. یک runner آفلاین باعث می‌شود job در صف باقی بماند بدون اینکه هیچ پیام خطایی در pull request ظاهر شود.

تمام بررسی‌ها خالی برمی‌گردند. برای یک بار اجرا، MIN_SEVERITY را روی low تنظیم کنید. اگر یافته‌ها ظاهر شدند، یعنی آستانه (threshold) به‌درستی کار می‌کند. اگر چیزی ظاهر نشد، payload را چاپ کنید و تأیید کنید که فیلترها کل diff را حذف نکرده باشند.

اجرای آن در کنار سایر ایجنت‌های شما

از آنجایی که reviewer کوچک است، وسوسه‌انگیز است که آن را روی سروری که هم‌اکنون همه چیز روی آن اجرا می‌شود، قرار دهید. اگر مخزن کد اهمیت دارد، آن را جدا نگه دارید. این فرآیند دارای توکنی است که می‌تواند روی کد شما کامنت بگذارد و کلیدی که می‌تواند هزینه ایجاد کند؛ همچنین یک runner که به‌صورت self-hosted اجرا می‌شود، ذاتاً مکانی است که کد workflow در آن اجرا می‌گردد. استفاده از یک حساب کاربری اختصاصی بدون دسترسی‌های ویژه که فاقد حقوق sudo است و روی میزبانی اجرا می‌شود که هیچ چیز دیگری روی آن نیست، حداقل استاندارد امنیتی است. اگر ایجنت‌های تعاملی دیگری نیز اجرا می‌کنید که کدها را checkout می‌کنند، استفاده از یک ماشین مجازی یک‌بارمصرف برای هر ایجنت الگوی مناسبی است و اجرای یک ایجنت برنامه‌نویسی روی یک VPS تنظیمات کلی را پوشش می‌دهد. اگر API شرکت Anthropic برای شما جدید است، ساخت اولین برنامه با Claude API روی یک VPS نقطه شروع کوچک‌تری نسبت به این مورد است.

FAQ

آیا یک عامل بررسی PR مبتنی بر هوش مصنوعی به دسترسی نوشتن در مخزن من نیاز دارد؟

خیر. این عامل تنها به pull-requests: write برای ارسال بررسی و contents: read برای دریافت diff نیاز دارد. این تمام لیست دسترسی‌هاست و شما آن را در بلوک permissions: در workflow تنظیم می‌کنید که محدودیت‌های GITHUB_TOKEN را برای هر job تعیین می‌کند. با این دو خط، عامل می‌تواند روی یک pull request نظر بدهد اما نمی‌تواند commit ارسال کند یا branch را merge کند. بررسی‌ها را با event: COMMENT ارسال کنید، نه REQUEST_CHANGES، تا عامل نتواند فرآیند merge را مسدود کند.

چرا نظر بررسی من با خطای HTTP 422 مواجه می‌شود؟

GitHub نظر بررسی inline را تنها در صورتی می‌پذیرد که آن خط بخشی از diff مربوط به pull request باشد؛ در غیر این صورت خطای Pull request review thread line must be part of the diff را برمی‌گرداند. بررسی کنید که path نسبت به مخزن باشد و پیشوند b/ از هدر diff را نداشته باشد، و شماره خط در یکی از hunkهای آن فایل قرار بگیرد. مقدار side باید برای یک خط اضافه شده یا بدون تغییر برابر RIGHT و برای یک خط حذف شده برابر LEFT باشد. پیش‌وند کردن هر خط از diff با شماره خط فایل جدید پیش از ارسال به مدل، همان چیزی است که از ابداع شماره‌های اشتباه توسط مدل جلوگیری می‌کند.

آیا می‌توانم این ابزار را روی یک مخزن عمومی با pull requestهایی از forkها اجرا کنم؟

با این طراحی خیر. GitHub اسرار (secrets) را به workflowهایی که از یک fork شروع شده‌اند ارسال نمی‌کند، بنابراین کلید مدل در دسترس نیست و اجرا با شکست مواجه می‌شود. GitHub همچنین اعلام کرده است که runnerهای self-hosted "تقریباً هرگز نباید برای مخازن عمومی استفاده شوند"، زیرا هر کسی می‌تواند یک pull request باز کند که باعث اجرای کد روی ماشین شما شود. برای یک پروژه عمومی، یا بررسی‌کننده را به branchهایی که مستقیماً به خود مخزن push شده‌اند محدود کنید (که این همان کاری است که محافظ if: انجام می‌دهد)، یا مرحله بررسی را به یک runner میزبانی‌شده توسط GitHub منتقل کنید و بپذیرید که diff از زیرساخت شما خارج می‌شود.

برای بررسی pull request از کدام مدل استفاده کنم؟

با Haiku 4.5 شروع کنید. خواندن یک diff محدود در برابر لیستی مشخص از انواع نقص‌ها، یک مسئله استدلالی دشوار نیست و ارزان‌ترین مدل باعث می‌شود هزینه ماهانه در عددی باقی بماند که کسی به آن اعتراض نکند. اگر متوجه شدید که مدل در زبان یا فریم‌ورک شما باگ‌های واقعی را نادیده می‌گیرد، به Sonnet 5 ارتقا دهید و این موضوع را به جای حدس و گمان، اندازه‌گیری کنید. Opus 5 در مقایسه با دو مدل دیگر، به ازای هر pull request بسیار گران‌تر است و توجیه آن برای branchهای release آسان‌تر از هر push به هر branch ویژگی (feature branch) است.