SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor · อัปเดตเมื่อ 2026-08-13

วิธีติดตั้ง AI PR Review Agent บน VPS ของคุณเอง

เรียนรู้วิธีติดตั้ง AI ช่วยตรวจ Pull Request บน VPS เพื่อความเป็นส่วนตัวของข้อมูล โดยใช้ diff-scoped prompts และตัวกรองขนาดไฟล์ พร้อมคำนวณต้นทุนต่อ PR อย่างแม่นยำ

หน้าที่ของ PR review agent แบบ self-hosted

PR review agent แบบ self-hosted คือโปรแกรมขนาดเล็กที่ทำงานบนเซิร์ฟเวอร์ที่คุณเป็นเจ้าของ โปรแกรมนี้จะอ่านค่า diff ของ pull request (PR) และส่งเฉพาะบรรทัดที่มีการเปลี่ยนแปลงไปยังโมเดล ผลลัพธ์ที่ได้จะถูกโพสต์เป็นความคิดเห็นแบบ inline review โปรแกรมนี้จะไม่ทำการ checkout branch ของคุณ และจะไม่เข้าถึงไฟล์ใดๆ ที่ไม่ได้ถูกแก้ไขใน pull request นั้น ข้อมูลรับรองเพียงอย่างเดียวที่โปรแกรมถือครองคือ API (application programming interface) key ของโมเดลหนึ่งชุด และ token ที่มีสิทธิ์เพียงแค่การแสดงความคิดเห็นเท่านั้น

โมเดลสามารถอ่านค่า diff ได้ ซึ่งส่วนนี้ถือว่าจัดการได้เรียบร้อยแล้ว สิ่งที่สำคัญคือ diff เหล่านั้นถูกส่งไปที่ใดและใครเป็นผู้ถือ key การใช้ review bot แบบ hosted หมายความว่า diff ทุกรายการจาก repository ส่วนตัวของคุณจะถูกส่งออกนอกเครือข่าย ไปเก็บไว้ใน log ของบุคคลที่สาม และอยู่ภายใต้เงื่อนไขการเก็บรักษาข้อมูลของพวกเขา แต่บน VPS (virtual private server) ที่คุณเป็นเจ้าของ diff จะถูกส่งจาก GitHub ไปยังเซิร์ฟเวอร์ของคุณและต่อไปยังโมเดล API โดยตรง อีกทั้งคุณยังสามารถตรวจสอบโค้ดประมาณ 40 บรรทัดที่ทำหน้าที่ตัดสินใจว่าข้อมูลใดบ้างที่จะถูกส่งออกไปได้ด้วยตนเอง

สิ่งที่ต้องเตรียมก่อนเริ่มต้น

  • VPS ที่รัน Ubuntu 24.04 พร้อมด้วย GitHub Actions runner แบบ self-hosted ที่ลงทะเบียนกับ repository เรียบร้อยแล้ว ให้กำหนด label เพิ่มเติมเป็น pr-review ในขั้นตอนการลงทะเบียน เนื่องจาก workflow ด้านล่างจะเลือกใช้งาน runner ผ่าน label นี้
  • Anthropic API key จาก Claude Console
  • Repository ที่คุณสามารถควบคุมสิทธิ์ผู้ที่สามารถเปิด pull request ได้ กรณีที่เป็น private repository จะดำเนินการได้ง่ายที่สุด ส่วนกรณีที่เป็น public repository จะมีการอธิบายไว้ในส่วนของ fork ด้านล่าง ซึ่งมีวิธีการจัดการที่ซับซ้อนกว่า

ติดตั้ง reviewer บน VPS

บริการ runner ทำงานภายใต้บัญชีผู้ใช้ที่ไม่มีสิทธิ์พิเศษที่คุณสร้างขึ้นเมื่อรัน ./svc.sh install ให้ติดตั้ง reviewer ภายใต้บัญชีผู้ใช้เดียวกันเพื่อให้งานสามารถเรียกใช้งานได้โดยไม่ต้องใช้ 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 ณ เดือนสิงหาคม 2026 ทุกรุ่นตั้งแต่ 2.20 เป็นต้นไปจะรองรับแฟล็ก --input ที่ใช้ด้านล่าง หากพบข้อความ Command 'gh' not found แสดงว่าคอมโพเนนต์ universe ยังไม่ได้เปิดใช้งาน ให้รัน sudo add-apt-repository universe แล้วลองใหม่อีกครั้ง

ตำแหน่งที่เก็บคีย์และโทเค็น

ความลับสองประเภทมีอายุการใช้งานที่แตกต่างกัน ทั้งสองอย่างต้องไม่ถูกเก็บไว้ใน repository

ANTHROPIC_API_KEY เป็นความลับของ repository ซึ่งตั้งค่าไว้ที่ Settings ตามด้วย Secrets and variables และ Actions โดย GitHub จะทำการเข้ารหัสและฉีดค่านี้เข้าไปใน environment ของขั้นตอนการทำงานขณะรันไทม์ ข้อมูลนี้จะไม่ถูกบันทึกเป็นไฟล์บนดิสก์และไม่ปรากฏในประวัติของ git

GITHUB_TOKEN มีการทำงานที่แตกต่างออกไป โดย Actions จะสร้างโทเค็นใหม่สำหรับแต่ละงานและทำลายทิ้งเมื่อจบงานนั้น ขอบเขตการทำงานของโทเค็นถูกกำหนดโดยบล็อก permissions: ใน workflow ซึ่งเป็นจุดที่หลักการให้สิทธิ์น้อยที่สุด (least privilege) ถูกนำมาใช้จริง:

permissions:
  contents: read
  pull-requests: write

โทเค็นดังกล่าวสามารถโพสต์รีวิวได้ แต่ไม่สามารถ push commit, merge branch, แก้ไขไฟล์ workflow หรือเข้าถึง repository อื่นได้ เอเจนต์ที่สามารถแสดงความคิดเห็นได้ถือเป็นผู้ตรวจสอบ (reviewer) ส่วนเอเจนต์ที่สามารถ push ได้ถือเป็นผู้ส่งมอบงาน (committer) ซึ่งไม่มีใครตกลงให้สิทธิ์ดังกล่าว โปรดดูแลคีย์ของโมเดลด้วยความระมัดระวังเช่นเดียวกัน เนื่องจากคีย์นี้มีผลต่อการใช้จ่ายเงินในบัญชีของคุณ สามารถอ่านรายละเอียดเพิ่มเติมเกี่ยวกับปัญหาในรูปแบบนี้ได้ที่ การเก็บความลับให้พ้นจากมือของเอเจนต์ AI

Actions จะแทนที่สตริงความลับด้วย *** ใน log ของงานโดยอัตโนมัติ การแทนที่นี้จะทำเฉพาะกับสตริงที่ตรงกันทุกประการเท่านั้น ดังนั้นหากคุณนำคีย์ไปเข้ารหัส base64, แบ่งบรรทัด หรือพิมพ์ออกมาทีละตัวอักษร ข้อมูลจะปรากฏเป็นข้อความปกติใน log ห้ามเพิ่มขั้นตอนการ debug ที่แสดงค่า environment ออกมาโดยเด็ดขาด

เหตุใด pull request จาก fork จึงไม่เห็น API key ของคุณ

กฎของ GitHub นั้นเรียบง่าย: ยกเว้นกรณี GITHUB_TOKEN แล้ว secrets จะไม่ถูกส่งไปยัง runner เมื่อ workflow ถูกทริกเกอร์จาก repository ที่ถูก fork มา ดังนั้นการรัน pull_request จาก fork จะเริ่มสคริปต์ของคุณโดยไม่มี ANTHROPIC_API_KEY และการเรียก API ครั้งแรกจะล้มเหลวด้วย invalid x-api-key

วิธีแก้ไขที่น่าดึงดูดใจคือการเปลี่ยนทริกเกอร์เป็น pull_request_target ซึ่งจะรันในบริบทของ repository หลักและได้รับ secrets เหล่านั้น แต่ห้ามทำเช่นนั้นในกรณีนี้ คำแนะนำด้านความปลอดภัยของ GitHub ระบุว่า workflow เหล่านั้น "มีสิทธิ์พิเศษ ซึ่งหมายความว่าพวกมันใช้ cache ชุดเดียวกับ branch หลักร่วมกับ workflow ทริกเกอร์ที่มีสิทธิ์พิเศษอื่นๆ และอาจมีสิทธิ์เขียนข้อมูลใน repository รวมถึงเข้าถึง secrets ที่อ้างอิงไว้ได้" และผลลัพธ์ที่ได้ "อาจถูกใช้ประโยชน์เพื่อยึดครอง repository"

คำแนะนำเดียวกันนี้ยังระบุไว้อย่างชัดเจนเกี่ยวกับ runner ว่า "ไม่ควรใช้ self-hosted runners กับ public repositories บน GitHub แทบทุกกรณี เพราะผู้ใช้ทุกคนสามารถเปิด pull request เข้ามายัง repository และทำให้สภาพแวดล้อมถูกบุกรุกได้"

สิ่งนี้ผลักดันให้เกิดการตัดสินใจในการออกแบบสองประการ ประการแรก งานนี้มีตัวป้องกันเพื่อให้รันเฉพาะบน branch ที่ถูก push มายัง repository ของคุณเองเท่านั้น ประการที่สอง workflow นี้ไม่มีขั้นตอน actions/checkout เลย ตัว agent จึงไม่มี branch นั้นอยู่บนดิสก์ ดังนั้น pull request ที่ไม่หวังดีจึงเป็นเพียงข้อความที่ถูกส่งไปยังโมเดลเท่านั้น มันไม่สามารถรัน build script บน VPS ของคุณได้ เพราะไม่มีสิ่งใดบน VPS ของคุณที่รันสคริปต์นั้น อย่างไรก็ตาม ข้อความไม่ได้หมายความว่าไม่มีอันตราย เพราะ diff ที่เขียนโดยคนแปลกหน้าถือเป็นข้อมูลที่ไม่น่าเชื่อถือซึ่งส่งเข้ามายังโมเดล ซึ่งเป็นขอบเขตความปลอดภัยเดียวกับที่คุณพบเมื่อ ให้ agent ค้นหาข้อมูลบนเว็บ และสิ่งเดียวที่ควบคุมมันไว้ในที่นี้คือ agent นี้ไม่สามารถทำอะไรได้นอกจากโพสต์ความคิดเห็นเท่านั้น

ดึงข้อมูลเฉพาะส่วนต่าง (diff) ไม่ใช่ทั้ง repository

คำขอเดียวสามารถดึงข้อมูลส่วนต่างทั้งหมดในรูปแบบข้อความธรรมดาได้

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 คือสิ่งที่เปลี่ยนการตอบกลับจากวัตถุ JSON ที่อธิบาย pull request ให้กลายเป็น unified diff โดยตรง และ gh api จะแสดงผลเนื้อหานั้นโดยไม่มีการเปลี่ยนแปลง บรรทัดแรกที่คุณเห็นควรขึ้นต้นด้วย diff --git a/ หากพบ gh: Not Found (HTTP 404) หมายความว่า token ไม่สามารถเข้าถึง repository ได้ ซึ่งในกรณีของ fine-grained personal token มักเกิดจากการไม่ได้ตั้งค่าสิทธิ์ Pull requests ไว้

กรองข้อมูลก่อนประมวลผล

ส่วนนี้คือสิ่งที่ตัดสินว่าบอทของคุณจะเป็นที่น่าสนใจหรือจะถูกปิดการแจ้งเตือน ตัวกรองแต่ละรายการด้านล่างจะทำงานก่อนที่โมเดลจะได้รับข้อมูลแม้แต่ไบต์เดียว

  • ตัวกรองเส้นทาง (Path filters): ล็อกไฟล์, ไดเรกทอรี vendored, บันเดิลที่ผ่านการ minified และโค้ดที่สร้างขึ้นอัตโนมัติ ความเห็นของโมเดลเกี่ยวกับ package-lock.json ถือเป็นสัญญาณรบกวน และไฟล์เหล่านี้มักเป็นส่วนใหญ่ของจำนวนไบต์ใน diff
  • การจำกัดขนาด (Size cap): หากเกินขีดจำกัด ให้ข้ามการตรวจสอบและจบการทำงานโดยไม่มีข้อผิดพลาด การรีแฟคเตอร์โค้ด 4,000 บรรทัดควรได้รับความเห็นเพียงบรรทัดเดียวว่ามีขนาดใหญ่เกินกว่าจะตรวจสอบอัตโนมัติ แทนที่จะให้โมเดลพยายามเดาถึงหกสิบครั้ง
  • เกณฑ์ความรุนแรงและขีดจำกัดจำนวนความเห็น: รายงานเฉพาะสิ่งที่พบในระดับความรุนแรงสูงและปานกลาง โดยจำกัดไว้ไม่เกินสิบรายการและเรียงตามระดับความรุนแรงสูงสุดก่อน ไม่มีใครอ่านความเห็นที่สิบเอ็ดหรอก

สคริปต์

บันทึกไฟล์นี้เป็น /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 ตามไฟล์เป็นสิ่งที่ทำให้การกรอง path ทำได้จริง การระบุหมายเลขกำกับทุกบรรทัดช่วยให้การเขียนคอมเมนต์รีวิวตรงจุด GitHub ยอมรับคอมเมนต์แบบ inline เฉพาะบรรทัดที่เป็นส่วนหนึ่งของ 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 ตัวนับจึงเริ่มจากจุดนั้นและเพิ่มขึ้นเฉพาะในบรรทัดที่เพิ่มเข้ามาหรือบรรทัดที่ไม่มีการเปลี่ยนแปลง ส่วนบรรทัดที่ถูกลบออกจะไม่ถูกนับเพราะไม่มีอยู่จริงในไฟล์ใหม่ ตัวตรวจสอบบรรทัดที่ขึ้นต้นด้วย backslash จะข้ามตัวระบุ 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 จะถูกตัดระหว่าง { ตัวแรกและ } ตัวสุดท้าย เนื่องจากบางครั้งโมเดลจะครอบคำตอบด้วย code fence ซึ่ง json.loads จะไม่สามารถประมวลผลได้ นอกจากนี้ path จะถูกตัด b/ ที่นำหน้าออก เนื่องจาก prefix ดังกล่าวมาจากส่วนหัวของ diff แต่ GitHub ต้องการ path ที่อ้างอิงจาก root ของ repository และ --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 ตั้งค่านี้ไว้ให้ทุกงานโดยอัตโนมัติ กลุ่ม concurrency มีความสำคัญต่อค่าใช้จ่าย: หากไม่มีกลุ่มนี้ การ push แก้ไขด่วน 3 ครั้งไปยัง branch จะทำให้เกิดการตรวจสอบเต็มรูปแบบ 3 ครั้งและคุณต้องจ่ายค่าบริการทั้งหมด แต่หากมีกลุ่มนี้ เฉพาะการทำงานล่าสุดเท่านั้นที่จะคงอยู่

บรรทัด if: ทำหน้าที่สองอย่าง ส่วนแรกคือการข้าม pull request ที่มาจาก fork ซึ่งจะล้มเหลวอยู่แล้วเนื่องจากไม่มี key ส่วนที่สองคือการสร้างสวิตช์ปิดการทำงานให้ทีมของคุณ: เพียงเพิ่ม label no-ai-review เข้าไปใน pull request แล้วงานดังกล่าวจะไม่ถูกเรียกใช้งาน

เปิด 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 ใน log แสดงว่าระบบทำงานถูกต้อง สำหรับ pull request ขนาดเล็กและสะอาด นี่คือผลลัพธ์ที่คาดหวังไว้

การรีวิว pull request แบบอัตโนมัติมีค่าใช้จ่ายเท่าไร

เนื่องจาก diff คือข้อมูลส่วนใหญ่ของ input ขนาดของ diff จึงเป็นตัวกำหนดราคา ด้านล่างนี้คือการวัด diff ขนาด 500 บรรทัดรวมกับ system prompt โดยใช้ endpoint สำหรับนับจำนวน token แทนการประมาณการ

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 ดังกล่าวมีจำนวน input tokens เท่ากับ 8,000 บน Haiku 4.5 และ 10,400 บน Sonnet 5 ซึ่งเป็นข้อความเดียวกันแต่ได้จำนวน token ต่างกัน โมเดล Claude ตั้งแต่เวอร์ชัน 4.7 เป็นต้นไปใช้ tokenizer รุ่นใหม่ที่สร้างจำนวน token มากขึ้นประมาณ 30% สำหรับ input เดียวกัน ตามที่ Anthropic ระบุไว้ในหน้าแสดงราคา โปรดคำนึงถึงจุดนี้เสมอเมื่อเปรียบเทียบราคาระหว่างโมเดลรุ่นใหม่กับรุ่นเก่าโดยดูจากราคาต่อล้าน token เพียงอย่างเดียว

ราคาตามรายการ ณ เดือนสิงหาคม 2026: Haiku 4.5 มีราคา $1 ต่อล้าน input tokens และ $5 ต่อล้าน output tokens ส่วน Sonnet 5 มีราคา $2 และ $10 ภายใต้ราคาช่วงแนะนำซึ่งมีผลถึงวันที่ 31 สิงหาคม 2026 จากนั้นจะปรับเป็น $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"
  }
]

นั่นหมายถึงค่าใช้จ่าย 1.4 เซนต์ต่อหนึ่ง pull request บน Haiku 4.5 และ 9.1 เซนต์บน Opus 5 ทีมที่รวม pull request 200 รายการต่อเดือนจะมีค่าใช้จ่ายประมาณ $2.80 บน Haiku 4.5, $7.28 บน Sonnet 5 หรือ $18.20 บน Opus 5 ตั้งแต่วันที่ 1 กันยายน 2026 เป็นต้นไป ให้คูณราคาของ Sonnet 5 ด้วย 1.5

มีสองปัจจัยที่ทำให้ค่าใช้จ่ายจริงสูงกว่าการประมาณการนี้ ประการแรก synchronize จะเรียกการรีวิวทุกครั้งที่มีการ push ดังนั้น branch ที่มีการเคลื่อนไหวและมีการ push 8 ครั้งจะมีค่าใช้จ่ายเท่ากับการรีวิว 8 ครั้ง และกฎ concurrency จะช่วยได้ก็ต่อเมื่อมีการ push เข้ามาใกล้เคียงกันเท่านั้น ประการที่สอง ตัวเลขเหล่านี้ตั้งอยู่บนสมมติฐานว่า path filters ทำงานได้อย่างถูกต้อง เพราะ lock file เพียงไฟล์เดียวที่ไม่ได้กรองอาจทำให้ขนาด input เพิ่มขึ้นเป็นสองเท่าได้ด้วยตัวมันเอง

Prompt caching ไม่ช่วยในกรณีนี้ เนื่องจาก prefix ที่แคชไว้จะต้องเหมือนกันทุกไบต์ในแต่ละการเรียก แต่ 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 สำหรับนับจำนวน token ไม่ได้รันโมเดลจริง จึงไม่ใช้ input หรือ output tokens และใช้ tokenizer ของโมเดลที่คุณระบุ ให้รันกับ pull request จริง 10 รายการจาก repository ของคุณเองและใช้ค่ามัธยฐานแทนค่าเฉลี่ย เพื่อป้องกันไม่ให้การ migration ขนาดใหญ่เพียงรายการเดียวทำให้การประมาณการคลาดเคลื่อน

เหตุผลที่บอทรีวิวถูกปิดการใช้งานและวิธีป้องกัน

พฤติกรรม 2 ประการที่ทำลายความน่าเชื่อถือของบอทเหล่านี้ ซึ่งทั้งสองประการมีวิธีแก้ไขในโค้ดด้านบน

การรีวิวทุกอย่างพร้อมกันในคราวเดียว บอทที่ทิ้งคอมเมนต์ไว้ถึง 40 รายการจะไม่มีใครอ่านเลยแม้แต่รายการเดียว เกณฑ์ความรุนแรง (severity threshold) และการจำกัดจำนวนคอมเมนต์ไว้ที่ 10 รายการไม่ใช่เรื่องของมารยาท แต่เป็นสิ่งที่ช่วยให้ข้อผิดพลาดที่สำคัญจริงๆ ยังคงมองเห็นได้ การเรียงลำดับตามความรุนแรงก่อนที่จะตัดทอนข้อมูล จะช่วยให้การจำกัดจำนวนนั้นคัดเอาเฉพาะสิ่งที่สำคัญน้อยที่สุดออกไป แทนที่จะเป็นการสุ่มเลือก 10 รายการ

การแสดงความเห็นอย่างมั่นใจในสิ่งที่ตรวจสอบไม่ได้ นี่คือสาเหตุที่ทำให้วิศวกรตัดสินใจปิดการใช้งานบอทอย่างถาวร โมเดลที่ได้รับข้อมูลเพียง 200 บรรทัดจากฐานโค้ดขนาด 40,000 บรรทัด อาจยังคงเขียนว่า "สิ่งนี้ทำให้การทำ cache invalidation ใน redis_client.py พัง" เกี่ยวกับไฟล์ที่มันไม่เคยเห็นมาก่อน ระบบ prompt จะคอยคัดค้านด้วยภาษาที่ชัดเจนว่า: ให้รายงานเฉพาะข้อบกพร่องที่ปรากฏในบรรทัดที่แสดงให้เห็นเท่านั้น และละเว้นสิ่งที่คุณไม่แน่ใจ การระบุถึงความล้มเหลวโดยตรงนั้นได้ผลดีกว่าการขอให้มีความแม่นยำโดยทั่วไป และการบอกโมเดลว่าผลลัพธ์ที่ว่างเปล่าเป็นเรื่องปกติ คือสิ่งที่หยุดไม่ให้มันพยายามแต่งเรื่องขึ้นมาพูดเกี่ยวกับการเปลี่ยนแปลงเพียงสองบรรทัด

ให้โพสต์รีวิวในรูปแบบ COMMENT เท่านั้น ห้ามใช้ REQUEST_CHANGES ความเห็นของโมเดลไม่ควรมีอำนาจในการบล็อกการ merge และทันทีที่มันทำได้ ผู้ที่กำลังเร่งรีบตามกำหนดการจะลบ workflow ทั้งหมดทิ้งแทนที่จะต้องมาโต้เถียงกับมัน

รูปแบบความล้มเหลวและข้อความที่คุณจะพบ

HTTP 422 เมื่อส่งรีวิว gh จะพิมพ์ gh: Unprocessable Entity (HTTP 422) ออกมา และเนื้อหาใน response body จะระบุชื่อฟิลด์ไว้ว่า Pull request review thread line must be part of the diff ซึ่ง GitHub ไม่สามารถปักหมุดคอมเมนต์นั้นได้ สาเหตุทั่วไปคือหมายเลขบรรทัดที่โมเดลสร้างขึ้นเอง, path ที่ยังมี prefix เป็น b/ ติดอยู่ หรือการคอมเมนต์ในบรรทัดที่ถูกลบไปแล้ว ซึ่งจำเป็นต้องตั้งค่า side ให้เป็น LEFT แทนที่จะเป็น RIGHT ให้ลองพิมพ์รีวิวในรูปแบบ JSON ออกมาก่อนทำการส่ง และตรวจสอบคอมเมนต์หนึ่งรายการด้วยตนเองเทียบกับ diff

invalid x-api-key จาก API ของโมเดล ขั้นตอนนี้จะล้มเหลวในการเรียกใช้ messages.create ครั้งแรก สาเหตุอาจเกิดจากไม่ได้ตั้งค่า secret ANTHROPIC_API_KEY ไว้ใน repository หรือ pull request มาจาก fork ทำให้ Actions ไม่ส่ง secret ใดๆ มาให้เลย ตัวป้องกัน fork ในบรรทัด if: ควรจะข้ามการทำงานนี้ไปแล้ว ดังนั้นให้ตรวจสอบบรรทัดนั้นก่อนเป็นอันดับแรก

gh: Resource not accessible by integration (HTTP 403) job token ไม่สามารถเขียนข้อมูลลงใน pull request ได้ ให้เพิ่ม pull-requests: write เข้าไปในบล็อก permissions: หากมีอยู่แล้ว ให้ไปที่ Settings จากนั้นไปที่ Actions และ General ซึ่งนโยบายขององค์กรอาจจำกัดสิทธิ์ที่ workflow token สามารถร้องขอได้

json.decoder.JSONDecodeError โมเดลไม่ได้ส่งคืนข้อมูลในรูปแบบ JSON ที่สามารถ parse ได้ สาเหตุทั่วไปคือ response มีความยาวเกินขีดจำกัดของ token และหยุดทำงานกลางคันขณะสร้าง object บรรทัด log จะพิมพ์ 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 ออฟไลน์ งานจะค้างอยู่ในสถานะรอคิวโดยไม่มีข้อความแสดงข้อผิดพลาดใดๆ ปรากฏใน pull request

รีวิวทุกรายการส่งกลับมาว่างเปล่า ให้ตั้งค่า MIN_SEVERITY เป็น low สำหรับการรันหนึ่งครั้ง หากมีผลลัพธ์ปรากฏขึ้น แสดงว่าเกณฑ์การกรองทำงานได้ตามปกติ แต่ถ้าไม่มีอะไรปรากฏเลย ให้พิมพ์ payload ออกมาและยืนยันว่าตัวกรองไม่ได้ลบเนื้อหาใน diff ออกไปทั้งหมด

การใช้งานร่วมกับเอเจนต์อื่นของคุณ

เนื่องจากตัว reviewer มีขนาดเล็ก จึงอาจดูน่าสนใจที่จะติดตั้งลงบนเครื่องที่รันงานอื่นอยู่แล้ว อย่างไรก็ตาม หาก repository ของคุณมีความสำคัญ ควรแยกเครื่องใช้งานให้ชัดเจน กระบวนการนี้ถือครอง token ที่สามารถแสดงความคิดเห็นในโค้ดของคุณและมี key ที่ใช้จ่ายเงินได้ อีกทั้งตัว self-hosted runner ถูกออกแบบมาให้เป็นพื้นที่สำหรับประมวลผลโค้ดใน workflow โดยเฉพาะ การใช้ บัญชีผู้ใช้ที่ไม่มีสิทธิ์พิเศษ ซึ่งไม่มีสิทธิ์ sudo บนโฮสต์ที่ไม่ได้รันงานอื่นใดเลยถือเป็นมาตรฐานขั้นต่ำ หากคุณมีการรันเอเจนต์แบบโต้ตอบที่ต้องมีการ checkout โค้ดด้วย การใช้ VM แบบใช้แล้วทิ้งสำหรับแต่ละเอเจนต์ คือรูปแบบที่ปลอดภัยที่สุด และ การรันเอเจนต์เขียนโค้ดบน VPS จะครอบคลุมการตั้งค่าทั่วไป หากคุณยังใหม่กับ Anthropic API การสร้างแอป Claude API แรกบน VPS เป็นจุดเริ่มต้นที่เหมาะสมกว่าการเริ่มจากวิธีนี้

FAQ

AI PR review agent จำเป็นต้องมีสิทธิ์เขียน (write access) ใน repository ของฉันหรือไม่

ไม่จำเป็น โดยต้องใช้เพียง pull-requests: write เพื่อส่งรีวิวและ contents: read เพื่อดึงข้อมูล diff เท่านั้น นี่คือรายการสิทธิ์ทั้งหมดที่คุณต้องกำหนดในบล็อก permissions: ของ workflow ซึ่งจะจำกัดขอบเขตการทำงานของ GITHUB_TOKEN ในแต่ละงาน ด้วยสิทธิ์เพียงสองรายการนี้ ตัว agent จะสามารถแสดงความคิดเห็นใน pull request ได้ แต่ไม่สามารถ push commit หรือ merge branch ได้ ควรส่งรีวิวด้วย event: COMMENT แทน REQUEST_CHANGES เพื่อป้องกันไม่ให้ agent ขัดขวางการ merge

ทำไมความคิดเห็นรีวิวของฉันถึงล้มเหลวด้วย HTTP 422

GitHub จะยอมรับความคิดเห็นรีวิวแบบ inline เฉพาะในบรรทัดที่เป็นส่วนหนึ่งของ diff ใน pull request เท่านั้น และจะส่งคืน Pull request review thread line must be part of the diff หากไม่เป็นไปตามเงื่อนไข ตรวจสอบให้แน่ใจว่า path เป็น path ที่อ้างอิงจาก root ของ repository โดยไม่มี prefix b/ จากส่วนหัวของ diff และหมายเลขบรรทัดต้องปรากฏอยู่ภายใน hunk ของไฟล์นั้น side ต้องเป็น RIGHT สำหรับบรรทัดที่เพิ่มเข้ามาหรือไม่มีการเปลี่ยนแปลง และเป็น LEFT สำหรับบรรทัดที่ถูกลบ การใส่หมายเลขบรรทัดของไฟล์ใหม่ไว้หน้าทุกบรรทัดของ diff ก่อนส่งไปยังโมเดล คือวิธีป้องกันไม่ให้โมเดลสร้างหมายเลขบรรทัดขึ้นมาเอง

ฉันสามารถรันสิ่งนี้บน public repository ที่มี pull request จาก fork ได้หรือไม่

ไม่ได้ด้วยการออกแบบนี้ GitHub จะไม่ส่ง secrets ไปยัง workflow ที่ถูกกระตุ้นจาก fork ทำให้ key ของโมเดลหายไปและส่งผลให้การรันล้มเหลว นอกจากนี้ GitHub ยังระบุว่า self-hosted runner "ไม่ควรใช้กับ public repository โดยเด็ดขาด" เนื่องจากใครก็ตามสามารถเปิด pull request เพื่อสั่งให้โค้ดรันบนเครื่องของคุณได้ สำหรับโปรเจกต์สาธารณะ ให้จำกัดการรีวิวเฉพาะ branch ที่ push เข้ามาใน repository โดยตรง ซึ่งเป็นสิ่งที่ตัวป้องกัน if: ทำ หรือย้ายขั้นตอนการรีวิวไปรันบน GitHub-hosted runner และยอมรับว่าข้อมูล diff จะถูกส่งออกไปนอกโครงสร้างพื้นฐานของคุณ

ฉันควรใช้โมเดลใดในการรีวิว pull request

เริ่มต้นด้วย Haiku 4.5 การอ่าน diff ที่มีขอบเขตจำกัดเทียบกับรายการประเภทข้อผิดพลาดที่กำหนดไว้ไม่ใช่ปัญหาการใช้เหตุผลที่ซับซ้อน และโมเดลที่ประหยัดที่สุดจะช่วยให้ค่าใช้จ่ายรายเดือนอยู่ในระดับที่ไม่มีใครโต้แย้งได้ ให้ขยับไปใช้ Sonnet 5 หากพบว่าโมเดลพลาดการตรวจพบข้อผิดพลาดจริงในภาษาหรือ framework ของคุณ และควรวัดผลจากประสิทธิภาพจริงแทนการคาดเดา ส่วน Opus 5 นั้นมีราคาสูงที่สุดในบรรดาสามรุ่นอย่างเห็นได้ชัดต่อหนึ่ง pull request ซึ่งการใช้ใน release branch จะมีความคุ้มค่ากว่าการใช้ในทุกการ push ของทุก feature branch