SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-26

Tự host AI review PR trên VPS: chi phí và setup

Chạy bot review pull request trên VPS của bạn với runner tự host, prompt chỉ theo diff, lọc path và kích thước, comment inline, cùng chi phí mỗi PR.

Agent review PR tự host làm gì

Một PR review agent tự host là một chương trình nhỏ chạy trên server do bạn sở hữu. Nó đọc diff của pull request (PR) và chỉ gửi các dòng đã thay đổi đến model. Phản hồi nhận được sẽ được đăng thành các comment inline trong review. Nó không bao giờ checkout branch của bạn và cũng không đọc file mà pull request không thay đổi. Credentials duy nhất nó giữ là một model API (application programming interface) key và một token chỉ có quyền comment, không có quyền nào khác. Đây là định nghĩa rất giới hạn về một agent, gần với một prompt có các bộ lọc đứng trước hơn là một thứ có tính tự động. Nếu muốn hiểu đầy đủ hơn trước khi tự xây dựng agent này, lộ trình từng bước từ các khái niệm đến một loop tự viết trình bày các nền tảng cần thiết.

Model có thể đọc diff. Phần đó đã được giải quyết. Điều quan trọng là diff đi đâu và ai giữ key. Với một bot review dạng hosted, mọi diff từ mọi repository private đều rời khỏi network của bạn, được ghi vào log của bên thứ ba và chịu chính sách lưu giữ dữ liệu của họ. Trên một VPS (virtual private server) do bạn sở hữu, diff đi từ GitHub đến máy của bạn rồi đến model API. Bạn cũng có thể đọc 40 dòng code quyết định nội dung nào được gửi đi.

Những thứ cần chuẩn bị

  • Một VPS chạy Ubuntu 24.04, đã đăng ký self-hosted GitHub Actions runner với repository. Gán thêm label pr-review khi đăng ký runner, vì workflow bên dưới sẽ chọn runner theo label đó.
  • Một Anthropic API key lấy từ Claude Console.
  • Một repository mà bạn kiểm soát được ai có thể mở pull request. Repository private là trường hợp đơn giản nhất. Phần fork bên dưới đề cập đến trường hợp repository public, nhưng cách xử lý trong đó có rủi ro hơn.

Cài reviewer trên VPS

runner service chạy bằng account không có quyền đặc biệt mà bạn đã tạo khi chạy ./svc.sh install. Cài reviewer dưới cùng account đó để job có thể thực thi reviewer mà không cần sudo. Thay runner bên dưới bằng tên account của bạn.

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 in gh version 2.45.0 trên Ubuntu 24.04 tính đến tháng 8 năm 2026. Mọi release từ 2.20 trở lên đều có flag --input được dùng bên dưới. Thông báo Command 'gh' not found có nghĩa là component universe chưa được bật, vì vậy hãy chạy sudo add-apt-repository universe rồi thử lại.

Vị trí lưu key và token

Hai secret, hai vòng đời khác nhau. Không secret nào được đặt trong repository.

ANTHROPIC_API_KEY là repository secret, được đặt trong Settings, rồi Secrets and variables, rồi Actions. GitHub mã hóa secret này và inject nó vào environment của step khi chạy. Secret này không bao giờ là file trên disk và không bao giờ xuất hiện trong git history.

GITHUB_TOKEN hoạt động khác. Actions tạo một token mới cho mỗi job và hủy token đó khi job kết thúc. Quyền của token được xác định trong block permissions: của workflow. Đây là nơi áp dụng nguyên tắc đặc quyền tối thiểu:

permissions:
  contents: read
  pull-requests: write

Token đó có thể đăng review. Nó không thể push commit, merge branch, chỉnh sửa workflow file hoặc truy cập repository khác. Agent có quyền comment là reviewer. Agent có quyền push là committer, nhưng không ai đồng ý cấp quyền đó. Hãy bảo vệ model key cẩn thận như vậy, vì key này có thể phát sinh chi phí trên account của bạn. Nội dung giữ secret ngoài tầm với của AI agent trình bày thêm về dạng vấn đề này.

Actions thay chuỗi secret chính xác bằng *** trong job log. Actions chỉ khớp chuỗi chính xác. Vì vậy, key được encode bằng base64, tách thành hai dòng hoặc in từng ký tự sẽ xuất hiện rõ trong log. Không thêm debug step để dump environment.

Tại sao pull request từ fork không bao giờ nhận được API key của bạn

Quy tắc của GitHub rất rõ: ngoại trừ GITHUB_TOKEN, secrets không được truyền cho runner khi workflow được kích hoạt từ một repository fork. Vì vậy, một lần chạy pull_request từ fork sẽ khởi chạy script mà không có ANTHROPIC_API_KEY, và lần gọi API đầu tiên sẽ thất bại với invalid x-api-key.

Cách sửa dễ nghĩ đến là đổi trigger sang pull_request_target. Trigger này chạy trong context của repository gốc và nhận được secrets. Không dùng cách đó ở đây. Hướng dẫn bảo mật của chính GitHub nêu rằng các workflow này “có quyền cao, nghĩa là chúng dùng chung cache của branch chính với các trigger workflow có quyền cao khác, đồng thời có thể có quyền ghi vào repository và quyền truy cập các secrets được tham chiếu”, và kết quả “có thể bị khai thác để chiếm quyền kiểm soát repository”.

Hướng dẫn này cũng cảnh báo rõ về runner: “Gần như không bao giờ nên dùng self-hosted runner cho các repository public trên GitHub, vì bất kỳ người dùng nào cũng có thể mở pull request vào repository và compromise môi trường.”

Điều đó dẫn đến 2 lựa chọn thiết kế. Job có một guard để chỉ chạy trên các branch được push vào repository của bạn. Workflow cũng hoàn toàn không có bước actions/checkout. Agent không bao giờ có branch trên disk, nên một pull request độc hại chỉ là text được gửi đến model. Nó không thể chạy build script trên VPS của bạn, vì không có gì trên VPS của bạn chạy script đó. Tuy nhiên, text không đồng nghĩa với an toàn: diff do người lạ viết là untrusted input gửi đến model, cùng trust boundary bạn gặp khi cho agent dùng web search, và yếu tố duy nhất giới hạn rủi ro ở đây là agent này không thể làm gì ngoài việc đăng comment.

Tải diff, không tải cả repository

Một request sẽ lấy toàn bộ diff dưới dạng văn bản thuần.

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"

Media type Accept: application/vnd.github.diff chuyển response từ một JSON object mô tả pull request thành chính unified diff, còn gh api in nguyên phần body đó. Dòng đầu tiên bạn thấy phải bắt đầu bằng diff --git a/. gh: Not Found (HTTP 404) cho biết token không thể truy cập repository. Với fine-grained personal token, nguyên nhân gần như luôn là chưa bật quyền Pull requests.

Lọc trước khi tốn token

Đây là điểm khác biệt giữa một bot được mọi người đọc và một bot bị mọi người tắt thông báo. Mỗi bộ lọc dưới đây chạy trước khi model nhìn thấy một byte nào.

  • Bộ lọc path. Bỏ qua lock file, thư mục vendored, bundle đã minify và code được generate. Comment của model về package-lock.json hoàn toàn là nhiễu, và những file này thường chiếm phần lớn số byte trong diff.
  • Giới hạn kích thước. Nếu vượt quá giới hạn, bỏ qua review và thoát với trạng thái thành công. Một lần refactor 4,000 dòng chỉ cần một dòng trung thực cho biết thay đổi quá lớn để review tự động, thay vì sáu mươi phỏng đoán.
  • Ngưỡng severity và giới hạn số comment. Báo cáo các finding mức high và medium, tối đa 10 finding, sắp xếp theo severity từ cao xuống thấp. Không ai đọc comment thứ 11.

Script

Lưu nội dung này thành /opt/pr-review/review.py. Script đọc cấu hình từ environment, nên workflow có thể đổi model mà không cần sửa code.

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

Tách diff theo từng file là điều cho phép lọc theo path. Đánh số từng dòng là điều giúp comment review trỏ đúng vị trí. GitHub chỉ chấp nhận inline comment trên dòng thuộc diff, nên model phải trích dẫn một số dòng có thật. Cung cấp sẵn các số dòng giúp model sao chép số phù hợp thay vì tự bịa.

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 header chứa thông tin đánh số. @@ -12,7 +12,9 @@ cho biết hunk của file mới bắt đầu ở dòng 12, nên bộ đếm bắt đầu từ đó và chỉ tăng trên các dòng được thêm hoặc không thay đổi. Các dòng bị xóa đi qua mà không được đánh số vì chúng không tồn tại trong file mới. Điều kiện kiểm tra các dòng bắt đầu bằng dấu gạch chéo ngược sẽ bỏ qua marker không có newline mà git ghi ở cuối file. Nếu không bỏ qua, marker này sẽ làm mọi số dòng sau đó lệch 1.

Cả hai nhánh thoát đều dùng status 0, không phải 1. Pull request bị lọc hoặc quá lớn vẫn phải hiển thị check màu xanh. Một check màu đỏ mà con người không thể xử lý sẽ bị bỏ qua. Khi một check bị bỏ qua, các check khác cũng sẽ bị bỏ qua.

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,
)

Có 3 chi tiết trong block đó rất quan trọng. JSON được cắt từ { đầu tiên đến } cuối cùng vì đôi khi model bọc câu trả lời trong code fence, còn json.loads sẽ lỗi khi gặp code fence. path bị loại bỏ b/ ở đầu vì prefix đó đến từ diff header, trong khi GitHub cần path tương đối với repository. --input - gửi toàn bộ review trong một API call, nên 10 phát hiện chỉ tạo 1 notification thay vì 10.

Khi không có gì cần báo cáo, script không gửi bài đăng nào. Một bot luôn ghi “không tìm thấy vấn đề” trên mọi pull request sẽ khiến mọi người quen với việc lướt qua các comment đó. Sau đó họ có thể lướt qua luôn comment thực sự quan trọng.

Kết nối vào workflow

Lưu nội dung này thành .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 không nằm trong block env: đó vì Actions đã đặt biến này cho mọi job. Nhóm concurrency ảnh hưởng đến chi phí: nếu không có nó, việc push 3 bản sửa nhanh lên một branch sẽ chạy 3 lần review đầy đủ và bạn phải trả phí cho cả 3 lần. Có nhóm này thì chỉ lần chạy cuối cùng được giữ lại.

Dòng if: thực hiện 2 việc. Nửa đầu bỏ qua các pull request từ fork, vì chúng sẽ fail nếu không có key. Nửa sau cung cấp một công tắc tắt cho team: thêm label no-ai-review vào pull request thì job sẽ không chạy.

Mở một pull request và theo dõi kết quả:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

Một run hoàn tất trong vài giây và có nothing above the severity threshold; posting no comment trong log là đang hoạt động đúng. Với một pull request nhỏ, không có thay đổi phức tạp, đây là kết quả dự kiến.

Chi phí review pull request tự động là bao nhiêu?

Diff gần như là toàn bộ input, nên kích thước diff quyết định chi phí. Dưới đây là kết quả đo một diff 500 dòng kèm system prompt, được đếm bằng token counting endpoint thay vì ước tính.

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 đó có 8,000 input token trên Haiku 4.5 và 10,400 trên Sonnet 5. Cùng một nội dung nhưng số token khác nhau. Từ phiên bản 4.7 trở đi, các model Claude dùng tokenizer mới và tạo ra nhiều hơn khoảng 30% token cho cùng một input. Anthropic có ghi rõ điều này trên trang giá. Hãy tính đến chênh lệch này khi chỉ so sánh giá trên mỗi triệu token giữa model mới và model cũ.

Giá niêm yết tính đến tháng 8 năm 2026: Haiku 4.5 có giá $1 cho mỗi triệu input token và $5 cho mỗi triệu output token. Sonnet 5 có giá lần lượt là $2 và $10 theo mức giá giới thiệu áp dụng đến ngày 31 tháng 8 năm 2026, sau đó tăng lên $3 và $15. Opus 5 có giá lần lượt là $5 và $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"
  }
]

Chi phí là 1.4 cent cho mỗi pull request trên Haiku 4.5 và 9.1 cent trên Opus 5. Một team merge 200 pull request mỗi tháng sẽ trả khoảng $2.80 trên Haiku 4.5, $7.28 trên Sonnet 5 hoặc $18.20 trên Opus 5. Từ ngày 1 tháng 9 năm 2026, nhân chi phí của Sonnet 5 với 1.5.

Có 2 yếu tố khiến hóa đơn thực tế cao hơn ước tính trên. Trigger synchronize thực hiện review sau mỗi lần push, nên một branch hoạt động với 8 lần push sẽ tốn chi phí cho 8 lần review. Quy tắc concurrency chỉ có tác dụng khi các lần push xảy ra gần nhau. Các con số trên cũng giả định path filter hoạt động đúng. Chỉ một lock file không được lọc cũng có thể tự làm input tăng gấp đôi.

Prompt caching không giúp được trong trường hợp này. Prefix được cache phải giống nhau từng byte giữa các lần gọi, còn diff thì thay đổi mỗi lần. System prompt là phần duy nhất ổn định, nhưng nó ngắn hơn nhiều so với độ dài tối thiểu để cache. Xem quy tắc chung tại khi nào prompt caching bắt đầu có lợi, và xem cách chọn giữa 3 model trên tại nên dùng model Claude nào cho công việc nào.

Đo diff của chính bạn trước khi bật tính năng

Thêm dòng này sau khi payload được tạo, rồi chạy script thủ công với một vài pull request của tháng trước:

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

Count endpoint không chạy model, nên không tiêu thụ input hoặc output token. Endpoint này dùng tokenizer tương ứng với model bạn chỉ định. Hãy chạy nó với 10 pull request thực tế từ repository của bạn và lấy median thay vì mean, để một migration rất lớn không làm sai lệch ước tính.

Vì sao bot review bị tắt, và cách tránh tình trạng này

Có 2 hành vi làm mất niềm tin vào các bot này. Cả 2 đều đã có cách xử lý trong đoạn code trên.

Review mọi thứ cùng lúc. Một bot để lại 40 comment thì sẽ không có comment nào được đọc. Ngưỡng mức độ nghiêm trọng và giới hạn 10 comment không phải để lịch sự. Chúng giúp các phát hiện thực sự quan trọng vẫn được nhìn thấy. Sắp xếp theo mức độ nghiêm trọng trước khi cắt bớt sẽ loại các phát hiện ít quan trọng nhất, thay vì loại ngẫu nhiên 10 phát hiện.

Tự tin comment về thứ mà bot không thể kiểm tra. Đây là nguyên nhân khiến engineer tắt bot vĩnh viễn. Một model chỉ được cung cấp 200 dòng trong codebase có 40.000 dòng vẫn sẽ viết rằng “điều này làm hỏng việc invalidation của cache trong redis_client.py” về một file mà nó chưa từng thấy. System prompt phản biện rõ ràng: chỉ báo cáo các lỗi nhìn thấy trong những dòng được cung cấp, và bỏ qua mọi điều không chắc chắn. Nêu thẳng điều kiện thất bại hiệu quả hơn việc chỉ yêu cầu độ chính xác một cách chung chung. Cho model biết rằng kết quả rỗng là bình thường cũng ngăn nó bịa ra một vấn đề để nói về một thay đổi chỉ có 2 dòng. Các lỗi chỉ xuất hiện khi xét toàn bộ repository là một công việc khác. Tự host open-kritt để quét bảo mật là một cách xử lý phạm vi đó mà không mở rộng những gì reviewer này được phép xem.

Đăng review dưới dạng COMMENT, không dùng REQUEST_CHANGES. Ý kiến của model không được phép chặn việc merge. Khi có thể làm vậy, người đang bị deadline sẽ xóa toàn bộ workflow thay vì tranh luận với nó. Nếu muốn phán đoán của agent có trọng lượng thực sự, hãy yêu cầu agent tạo ra thứ bạn có thể kiểm tra thay vì thứ bạn phải tin. Đây là ý tưởng đằng sau việc yêu cầu agent cung cấp báo cáo bằng chứng để bạn tự chạy lại.

Các trường hợp lỗi và chuỗi bạn sẽ thấy

HTTP 422 khi gửi review. gh in gh: Unprocessable Entity (HTTP 422) và response body ghi rõ field: Pull request review thread line must be part of the diff. GitHub không thể đặt comment vào vị trí đó. Nguyên nhân thường là số dòng do model tự tạo, một path vẫn còn tiền tố b/, hoặc comment trên một dòng đã bị xóa; trường hợp này cần đặt side thành LEFT thay vì RIGHT. In review JSON trước khi gửi và kiểm tra thủ công một comment với diff.

invalid x-api-key từ model API. Step fail ngay ở lần gọi messages.create đầu tiên. Hoặc secret ANTHROPIC_API_KEY chưa được set trong repository, hoặc pull request đến từ fork nên Actions không nhận được secret nào. Fork guard trong dòng if: đáng lẽ phải bỏ qua step này, vì vậy hãy kiểm tra dòng đó trước.

gh: Resource not accessible by integration (HTTP 403). Job token không thể ghi vào pull request. Thêm pull-requests: write vào block permissions:. Nếu đã có, vào Settings, rồi Actions, rồi General. Tại đây, policy của organisation có thể giới hạn những gì workflow token được phép request.

json.decoder.JSONDecodeError. Model không trả về JSON có thể parse. Nguyên nhân phổ biến là response chạm giới hạn token và dừng giữa chừng trong một object. Log line in stop_reason chính xác cho trường hợp này: giá trị max_tokens nghĩa là tăng max_tokens hoặc giảm MAX_COMMENTS.

Workflow không bao giờ chạy. gh run list không hiển thị gì cho pull request. Kiểm tra xem paths-ignore có lọc hết mọi file đã thay đổi không, sau đó kiểm tra fork guard và label guard, rồi kiểm tra runner còn hoạt động bằng sudo systemctl status 'actions.runner.*' trên VPS. Runner offline khiến job nằm ở trạng thái queued mà không có thông báo lỗi nào trong pull request.

Mọi review đều trả về rỗng. Đặt MIN_SEVERITY thành low cho một lần chạy. Nếu có finding, threshold đang hoạt động đúng. Nếu vẫn không có gì, in payload và xác nhận các filter không xóa toàn bộ diff.

Chạy song song với các agent khác

Reviewer có kích thước nhỏ, nên bạn có thể muốn đặt nó trên máy đang chạy mọi thứ khác. Tuy nhiên, hãy tách riêng nếu repository quan trọng. Process này giữ một token có thể comment vào code của bạn và một key có thể chi tiền của bạn. Self-hosted runner về bản chất là nơi workflow code được thực thi. Một account không có quyền root chuyên dụng không có quyền sudo, chạy trên một host không chạy dịch vụ nào khác, là cấu hình cơ bản. Nếu bạn cũng chạy các agent tương tác có checkout code, một VM tạm thời cho mỗi agent là mô hình phù hợp để duy trì an toàn, còn chạy coding agent trên VPS trình bày cách thiết lập tổng quát. Nếu Anthropic API còn mới với bạn, ứng dụng Claude API đầu tiên trên VPS là điểm bắt đầu đơn giản hơn nội dung này.

FAQ

AI PR review agent có cần quyền ghi vào repository của tôi không?

Không. Agent chỉ cần pull-requests: write để đăng review và contents: read để lấy diff. Đây là toàn bộ danh sách quyền cần thiết. Bạn đặt các quyền này trong block permissions: của workflow, block này giới hạn những gì GITHUB_TOKEN theo từng job có thể thực hiện. Với 2 dòng đó, agent có thể bình luận trên pull request nhưng không thể push commit hoặc merge branch. Hãy đăng review bằng event: COMMENT thay vì REQUEST_CHANGES để agent cũng không thể chặn việc merge.

Tại sao comment review của tôi bị lỗi HTTP 422?

GitHub chỉ chấp nhận inline review comment trên một dòng thuộc diff của pull request. Nếu không, GitHub trả về Pull request review thread line must be part of the diff. Kiểm tra để path là đường dẫn tương đối với repository và không có tiền tố b/ từ diff header. Số dòng cũng phải nằm trong một hunk của file đó. side phải là RIGHT đối với dòng được thêm hoặc không thay đổi, và LEFT đối với dòng bị xóa. Gắn số dòng của file mới vào đầu mỗi dòng trong diff trước khi gửi cho model sẽ ngăn model tự tạo số dòng ngay từ đầu.

Tôi có thể chạy workflow này trên repository public có pull request từ fork không?

Không với thiết kế này. GitHub không truyền secrets cho workflow được kích hoạt từ fork, nên model key bị thiếu và run sẽ fail. GitHub cũng nêu rõ rằng self-hosted runner “hầu như không bao giờ nên được dùng cho repository public”, vì bất kỳ ai cũng có thể mở pull request khiến code chạy trên máy của bạn. Với project public, bạn có thể chỉ cho phép reviewer chạy trên các branch được push trực tiếp vào repository. Đây là chức năng của guard if:. Hoặc chuyển bước review sang GitHub-hosted runner và chấp nhận rằng diff sẽ rời khỏi hạ tầng của bạn.

Tôi nên dùng model nào để review pull request?

Hãy bắt đầu với Haiku 4.5. Đọc một diff có giới hạn và đối chiếu với danh sách cố định các loại defect không phải bài toán cần reasoning phức tạp. Model rẻ nhất giúp giữ chi phí hằng tháng ở mức dễ chấp nhận. Chuyển lên Sonnet 5 nếu bạn phát hiện model bỏ sót bug thực tế trong ngôn ngữ hoặc framework của mình, và hãy đo lường kết quả thay vì tự giả định. Opus 5 đắt hơn nhiều so với 2 model còn lại trên mỗi pull request. Vì vậy, model này phù hợp để dùng trên release branch hơn là chạy ở mỗi lần push vào mọi feature branch.