Cara Menambahkan Tool CAPTCHA LangChain ke Agent dengan CapSkip

langchain captcha - How to Add a LangChain CAPTCHA Tool to an Agent With CapSkip

Untuk memecahkan CAPTCHA di LangChain, beri agent sebuah tool yang memanggil CapSkip, dan jauhkan token dari percakapan. Tool yang paling jelas mengembalikan token ke model, yang menaruh sekitar 1,700 karakter ke panggilan model berikutnya dan satu salinan lagi di setiap panggilan sesudahnya. Tool yang lebih baik mengembalikan handle pendek, menyimpan token di sisi Anda, dan memakainya di tool kedua. Tool pemecah harus menangkap exception CapSkip, karena exception yang lolos dari sebuah tool mengakhiri seluruh run. CapSkip berjalan di PC Windows Anda sendiri, jadi agent yang mencoba ulang tidak memiliki tagihan per pemecahan. Panduan ini membangun pasangan tool itu dengan LangChain 1.4 dan menjalankannya tanpa API key.

Apa yang Anda butuhkan

  • CapSkip yang berjalan di PC Windows, dengan pemecah reCAPTCHA diaktifkan. Lihat panduan penyiapan yang menjelaskan cara menjalankan CapSkip untuk pertama kali.
  • Python 3.10 atau lebih baru. LangChain 1.4 dan paket capskip 1.3.0 sama-sama membutuhkannya. Semua yang ada di bawah dijalankan di Windows 11 dengan Python 3.14.0, langchain 1.4.6, langchain-core 1.6.12, langgraph 1.2.16, pydantic 2.14.1, dan requests 2.34.2. Blok-bloknya juga lulus di Python 3.11.7. Python 3.10, 3.12, dan 3.13 tidak dijalankan.
  • Prompt PowerShell. Perintahnya memakai sintaks PowerShell.
  • Halaman pendaftaran milik Anda sendiri dengan kotak centang reCAPTCHA v2. Formulir contohnya memiliki field tersembunyi bernama _csrf dan field bernama email. Tool submit mengirim kedua field itu ditambah token, jadi ubah namanya agar sesuai dengan formulir Anda.
  • Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga agent di PC lain, VPS, atau platform hosted dapat memanggilnya lewat API. Keduanya diatur di pengaturan koneksi.

Tidak ada language model yang dipanggil saat panduan ini ditulis. Run agent memakai model berskrip yang membuat panggilan tool dalam urutan tetap, jadi wiring-nya benar-benar berjalan dan tidak menyatakan apa pun tentang perilaku model yang sesungguhnya. Pengukuran berasal dari mock CapSkip (pengganti CapSkip yang dibuat khusus untuk pengujian) dan halaman pendaftaran mock di PC yang sama, dipakai bersama LangChain yang asli dan SDK yang asli. Panduan ini menyebutnya run mock dan halaman mock. Aturan menerima dan menolak pada halaman mock, cara mock mengantrekan pemecahan, dan semua pengukuran waktu adalah milik mock itu sendiri, di PC ini. Satu pemecahan terhadap CapSkip yang sedang berjalan, dengan sitekey uji publik dari Google dan pengaturan default, memakan 12.9 detik dan mengembalikan token sepanjang 1,700 karakter. Waktu pemecahan bervariasi dari satu run ke run berikutnya.

Mengapa tool butuh lebih banyak perhatian daripada skrip

Dalam skrip, Anda yang memilih argumen dan Anda yang membaca hasilnya. Dalam agent, model yang memilih argumen, model yang membaca hasilnya, dan halaman yang telah dibaca agent bisa mencoba mengarahkan model. Itu mengubah empat hal. Hasilnya masuk ke percakapan dan dikirim lagi pada setiap panggilan berikutnya. Exception di dalam tool tidak ditangani untuk Anda. Model bisa meminta pemecahan lagi sesering yang ia mau. Dan sitekey serta alamat yang berasal dari model perlu diperiksa sebelum sampai ke CapSkip. Agent yang mengendalikan browser sendiri memiliki masalah yang sama, dan panduan Browser Use membahas jalur itu. Yang ini hanyalah fungsi biasa yang bisa dipanggil model.

Langkah 1: buat proyek

Gunakan virtual environment di folder baru. Perintah di bawah menjalankan Python milik environment itu lewat path-nya, jadi Anda tidak perlu mengaktifkannya. Paket langchain menarik langgraph, langchain-core, dan pydantic, sekitar 40 paket secara keseluruhan. Penyedia model adalah paket terpisah yang Anda tambahkan hanya saat memakai model sungguhan.

# pip install langchain capskip
mkdir langchain-captcha
cd langchain-captcha
python -m venv .venv
.\.venv\Scripts\python -m pip install langchain capskip
.\.venv\Scripts\python -c "import capskip, langchain; print(langchain.__version__)"

Gunakan create_agent dari langchain.agents untuk agent. Helper yang lebih lama sudah dihapus. AgentExecutor, initialize_agent, dan create_react_agent memunculkan ImportError saat diimpor dari langchain.agents. create_react_agent di langgraph.prebuilt masih berjalan, dan mencetak peringatan LangGraphDeprecatedSinceV10 yang menyuruh Anda mengimpor create_agent dari langchain.agents. Tutorial yang memakai salah satunya sudah usang.

Langkah 2: tool pertama, dan mengapa bentuknya salah

Simpan ini sebagai naive_tool.py. Ini adalah tool berguna yang paling singkat: pecahkan CAPTCHA dan kembalikan token. SDK tidak membaca variabel lingkungan apa pun, jadi file ini membaca host, port, dan key sendiri. Key hanya berpengaruh jika Anda mengaktifkan API key validation di CapSkip. Setiap file membaca alamat halaman dari PAGE_URL dan sitekey dari SITEKEY, jadi setel keduanya sekali di jendela PowerShell Anda, dari halaman pendaftaran Anda sendiri. CapSkip menjawab dengan error untuk halaman yang tidak memuat widget reCAPTCHA v2.

# PowerShell: set these once per window, before you run any file
$env:PAGE_URL = "https://your-site.example/signup"   # your own page
$env:SITEKEY = "YOUR_SITEKEY"                        # its data-sitekey

Lalu jalankan naive_tool.py dengan .\.venv\Scripts\python naive_tool.py.

# naive_tool.py: the first version, token straight back to the model
import os
from capskip import CapSkip
from langchain.tools import tool

solver = CapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
)


@tool
def solve_recaptcha(sitekey: str, page_url: str) -> str:
    """Solve a reCAPTCHA v2 checkbox and return the token."""
    return solver.recaptcha(sitekey, page_url)["code"]


if __name__ == "__main__":
    args = {
        "sitekey": os.getenv("SITEKEY", "YOUR_SITEKEY"),
        "page_url": os.getenv(
            "PAGE_URL", "https://example.com/page-with-recaptcha"),
    }
    token = solve_recaptcha.invoke(args)
    print(len(token), "characters")

Ini berfungsi, dan mencetak panjang token. Panjang itulah masalahnya. Token reCAPTCHA yang dipecahkan terhadap sitekey uji publik dari Google berukuran sekitar 1,700 karakter. Saat tool mengembalikannya, token disimpan sekali di hasil tool. Model lalu harus meneruskannya ke tool berikutnya sebagai argumen, sehingga token disimpan untuk kedua kalinya di panggilan tool milik model sendiri. Setiap panggilan sesudahnya membawa kedua salinan. Sebuah run mock menghitung karakter yang dikirim ke model pada masing-masing dari tiga panggilan, sebagai teks pesan ditambah argumen panggilan tool. Run itu memakai model berskrip, system prompt satu karakter, token mock 2,000 karakter, dan, untuk baris pertama, tool kedua yang menerima token sebagai argumen. System prompt sungguhan dan skema tool menambahkan jumlah yang sama ke kedua baris:

DesainPanggilan 1Panggilan 2Panggilan 3Total
Token dikembalikan dan diteruskan kembali112,0964,1466,253
Handle pendek dikembalikan11152253416

Sebagai gambaran, string acak sepanjang 2,000 karakter terhitung sekitar 1,400 token dengan tokenizer OpenAI. Model lain menghitung secara berbeda. Ini argumen soal biaya dan ukuran, bukan klaim tentang seberapa sering model salah menyalin string panjang. Ini juga satu tempat lagi tempat kesalahan salin bisa merusak token yang hanya berfungsi sekali. Solusinya adalah menyimpan token di sisi Anda dan memberi model sebuah handle.

Langkah 3: pengaturan untuk tool yang sesungguhnya

Tool yang sesungguhnya berada dalam satu file, captcha_tools.py, yang Anda bangun dari empat bagian yang ditempel. Simpan bagian pertama ini. Bagian ini membaca pengaturan, membangun klien, dan mendeklarasikan sedikit state yang dipakai bersama oleh tool. ALLOWED_HOSTS berasal dari PAGE_URL milik Anda sendiri, tidak pernah dari model.

# captcha_tools.py, part 1: settings
import logging
import os
import re
import secrets
import threading
import time
from typing import Literal
from urllib.parse import urljoin, urlparse

import requests
from capskip import ApiException, CapSkip, CapSkipError
from capskip import NetworkException, TimeoutException
from langchain.tools import tool

log = logging.getLogger("captcha_tools")
solver = CapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
PAGE_URL = os.getenv("PAGE_URL", "https://example.com/page-with-recaptcha")
SITEKEY = os.getenv("SITEKEY", "YOUR_SITEKEY")
ALLOWED_HOSTS = {urlparse(PAGE_URL).hostname}  # set by you, not the model
MAX_SOLVES = 3        # CapSkip calls allowed per run
TOKEN_TTL = 100       # seconds a stored token may wait for its submit
_slots = threading.Semaphore(2)  # at or below Max. Threads in CapSkip
_vault = {}           # handle -> (token, page_url, born)
_used = [0]
_lock = threading.Lock()

Langkah 4: periksa host sebelum apa pun

Model yang memilih alamat halaman yang dikirim ke CapSkip, dan teks di halaman yang dibaca agent bisa mendorongnya ke alamat lain. Tegakkan aturan alamat di dalam tool, karena aturan di prompt bisa disiasati. Tambahkan ini ke captcha_tools.py.

# captcha_tools.py, part 2: guards (appended to part 1)
def host_ok(url):
    try:
        u = urlparse(url)
        u.port  # raises ValueError on a malformed port
    except ValueError:
        return False
    return (u.scheme in ("http", "https") and u.username is None
            and u.hostname in ALLOWED_HOSTS)


def reset_run():
    _vault.clear()
    _used[0] = 0

Pemeriksaan ini sengaja menyentuh port. Pemeriksaan yang hanya membandingkan nama host akan meloloskan alamat seperti http://127.0.0.1:18512.evil.example/page, karena parser tetap melaporkan 127.0.0.1 sebagai host saat port-nya rusak. Membaca port memunculkan ValueError untuk kasus itu, dan fungsi lalu menolak alamat tersebut. Fungsi ini juga menolak alamat file:, javascript:, dan ftp:, string kosong, dan alamat yang mengandung nama pengguna. Fungsi ini hanya membandingkan host, bukan port. Tool submit di Langkah 6 menjalankan pemeriksaan yang sama pada alamat action formulir.

Langkah 5: tool pemecah

Tambahkan ini ke captcha_tools.py. Tiga keputusan di dalamnya penting. Tool menangkap setiap exception CapSkip dan mengembalikan dictionary kecil, karena exception yang lolos dari tool mengakhiri seluruh run agent. Tool menghitung setiap panggilan CapSkip yang dibuatnya, termasuk yang gagal, sehingga model yang terus meminta tidak bisa terus membuat CapSkip sibuk. Dan tool menyimpan token di dictionary lalu hanya mengembalikan sebuah handle.

# captcha_tools.py, part 3: the solve tool (appended to part 2)
def fail(error, retryable=False):
    return {"ok": False, "error": error, "retryable": retryable}


@tool
def solve_recaptcha(
    sitekey: str, page_url: str, kind: Literal["v2", "v3"] = "v2"
) -> dict:
    """Solve a reCAPTCHA on a page and return a handle for submit_form.

    sitekey is the data-sitekey value on the page. page_url is the
    full URL of the page that shows the CAPTCHA. The token itself is
    kept out of the conversation; pass the returned handle on.
    """
    if not host_ok(page_url):
        return fail("host_not_allowed")
    with _lock:
        if _used[0] >= MAX_SOLVES:
            return fail("solve_budget_used_up")
        _used[0] += 1
    try:
        with _slots:
            result = solver.recaptcha(sitekey, page_url, version=kind)
    except TimeoutException:
        return fail("timeout", retryable=True)
    except NetworkException as exc:
        log.warning("CapSkip unreachable: %s", exc)
        return fail("capskip_unreachable")
    except ApiException as exc:
        log.warning("CapSkip refused the job: %s", exc)
        unsolvable = "UNSOLVABLE" in str(exc)
        return fail("unsolvable" if unsolvable else "capskip_rejected",
                    retryable=unsolvable)
    except CapSkipError as exc:
        return fail("bad_arguments: %s" % exc)
    handle = "cap_" + secrets.token_hex(3)
    _vault[handle] = (result["code"], page_url, time.monotonic())
    return {"ok": True, "handle": handle, "valid_for_s": TOKEN_TTL}

Docstring adalah yang dibaca model, jadi jelaskan di sana apa itu setiap argumen dan bahwa token tidak masuk ke percakapan. Tool tanpa docstring gagal saat definisi dengan ValueError: Function must have a docstring if description not provided. Tipe Literal menjadi enum di skema. Nilai di luarnya, seperti v4, tidak pernah sampai ke fungsi, dan CapSkip tidak dipanggil. Model melihat pesan yang diawali Error invoking tool ‘solve_recaptcha’, mengulang argumen yang dikirimnya, menyebut kind: Input should be ‘v2’ or ‘v3’, dan diakhiri dengan Please fix the error and try again.

Apa yang terjadi saat sebuah tool memunculkan error

Inilah sebabnya tool pemecah mengembalikan dictionary. Dalam run mock, create_agent hanya mengubah error argumen yang buruk menjadi pesan untuk model. Setiap exception lain keluar dari agent.invoke tanpa berubah dan mengakhiri run. Masing-masing berikut keluar dari agent, dengan mock sebagai pengganti CapSkip:

KasusYang keluar dari agent.invoke
Pemecahan tidak dapat dilakukanApiException: ERROR_CAPTCHA_UNSOLVABLE
recaptchaTimeout 2 detikTimeoutException: timeout 2.0 exceeded, setelah 3.8 detik
CapSkip tidak sedang berjalanNetworkException: Max retries exceeded, WinError 10061, setelah 2.0 detik
Panggilan yang menyetel version v3 dan invisible bersamaanValidationException: invisible is only supported for reCAPTCHA v2.

Menyetel handle_tool_error ke True pada sebuah tool tidak membantu di sini. Pengaturan itu hanya mencakup ToolException milik LangChain sendiri, dan tidak ada exception CapSkip yang termasuk jenis itu. Timeout terpicu terlambat karena SDK memeriksa batasnya hanya di antara polling. Tool di atas mengembalikan dictionary kecil sebagai gantinya, dengan ok disetel ke false, nama error seperti unsolvable, dan retryable disetel ke true, yang bisa ditindaklanjuti model. Hanya pesan error yang mengandung UNSOLVABLE yang dianggap retryable. Error API lain, seperti API key yang salah, kembali sebagai capskip_rejected dan tidak retryable. Detail seperti teks error koneksi masuk ke log, bukan ke model. Jika Anda lebih suka membiarkan tool apa adanya, library agent punya dua hook. ToolErrorMiddleware menerima fungsi on_error yang mengembalikan teks untuk model, dan jika fungsi itu tidak mengembalikan apa-apa, exception tetap mengakhiri run. Dekorator wrap_tool_call memungkinkan Anda menulis try dan except sendiri. Keduanya membuat run tetap hidup dalam run mock.

Langkah 6: tool submit

Tambahkan bagian terakhir captcha_tools.py. Tool submit memakai sebuah handle. Ia mengeluarkan token dari dictionary, sehingga handle hanya berfungsi sekali, dan menolak handle yang lebih tua dari TOKEN_TTL. Token kedaluwarsa, jadi file ini mengizinkan 100 detik, di bawah dua menit yang didokumentasikan Google. Itu menyisakan sekitar 20 detik untuk mengambil halaman dan mengirim post. Tool submit tidak punya argumen alamat, karena alamat halaman disimpan saat token dipecahkan. Alamat action formulir itu sendiri juga merupakan teks halaman, jadi tool menjalankan pemeriksaan host yang sama padanya dan tidak mengikuti redirect. Model tidak bisa memilih ke mana token dikirim.

# captcha_tools.py, part 4: the submit tool (appended to part 3)
@tool
def submit_form(handle: str, email: str) -> dict:
    """Submit the sign-up form with a handle from solve_recaptcha.

    A handle works once. After a failed submit, solve again.
    """
    entry = _vault.pop(handle, None)  # pop: a handle is single use
    if entry is None:
        return fail("unknown_or_used_handle")
    token, page_url, born = entry
    if time.monotonic() - born > TOKEN_TTL:
        return fail("handle_expired", retryable=True)
    try:
        session = requests.Session()
        page = session.get(page_url, timeout=15, allow_redirects=False)
        html = page.text
        csrf = re.search(r'name="_csrf" value="([^"]+)"', html)
        action = re.search(r'<form[^>]*action="([^"]*)"', html)
        if not (csrf and action):
            return fail("form_not_recognised")
        target = urljoin(page_url, action.group(1))
        if not host_ok(target):  # the form action is page text too
            return fail("host_not_allowed")
        reply = session.post(
            target,
            data={"_csrf": csrf.group(1), "email": email,
                  "g-recaptcha-response": token},
            headers={"Referer": page_url},  # requests sends none itself
            timeout=15,
        )
    except requests.RequestException:
        return fail("site_unreachable")
    said = re.sub(r"<[^>]+>", " ", reply.text).strip()[:80]
    return {"ok": reply.status_code == 200, "status": reply.status_code,
            "page_says": said}

Requests tidak mengirim header Referer, dan situs yang memeriksanya menjawab 403. Tool menyetelnya. Nama field di kode di atas adalah milik formulir contoh, dan formulir yang tidak cocok mengembalikan form_not_recognised. Pengiriman kedua dengan handle yang sama mengembalikan unknown_or_used_handle, dan situs mock hanya melihat satu post. Tool mengembalikan 80 karakter pertama dari balasan halaman ke model. Teks dari halaman adalah salah satu jalan bagi masukan dari pihak luar untuk sampai ke model, jadi kembalikan hanya statusnya jika Anda tidak menginginkannya. Mengirim token mentah dua kali membuat mock menjawab 403 dengan token already used, yang merupakan aturan mock. Lihat kedaluwarsa token reCAPTCHA untuk mengetahui berapa lama token asli bertahan.

Langkah 7: model berskrip, tanpa API key

Simpan ini sebagai fake_model.py. Ini adalah model chat kecil yang mengembalikan panggilan tool yang Anda skrip, secara berurutan. Library agent memanggil bind_tools pada sebuah model, dan model ini menerima panggilan itu lalu mengabaikan skemanya. Ini membuktikan bahwa tool Anda, state-nya, dan loop agent berfungsi. Ini tidak bisa menunjukkan bagaimana model sungguhan memilih argumen atau membaca error.

# fake_model.py: a scripted chat model, no API key needed
# It calls the tools exactly as scripted, so the wiring runs for
# real. It says nothing about how a real model behaves.
from typing import Any, List

from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration, ChatResult


def calls(*items) -> AIMessage:
    """One AI turn asking for tool calls: (name, args, id) each."""
    return AIMessage(content="", tool_calls=[
        {"name": n, "args": a, "id": i, "type": "tool_call"}
        for n, a, i in items
    ])


def call(name: str, args: dict, cid: str) -> AIMessage:
    return calls((name, args, cid))


class ScriptedChat(BaseChatModel):
    script: List[Any]  # AIMessages, or functions(messages) -> AIMessage
    seen: List[Any] = []  # what each model call was sent
    i: int = 0

    @property
    def _llm_type(self) -> str:
        return "scripted"

    def bind_tools(self, tools, **kwargs):
        return self  # the agent calls this; we ignore the schemas

    def _generate(self, messages, stop=None, run_manager=None, **kw):
        self.seen.append(list(messages))
        item = self.script[self.i]
        self.i += 1
        msg = item(messages) if callable(item) else item
        return ChatResult(generations=[ChatGeneration(message=msg)])

Langkah 8: bangun agent dan jalankan

Simpan ini sebagai run_agent.py. Tanpa CAPTCHA_AGENT_MODEL, skrip memakai model berskrip. Dengan variabel disetel ke nama penyedia dan model, skrip membangun model sungguhan lewat init_chat_model. System prompt menyuruh model berhenti ketika sebuah tool mengatakan error tidak retryable. Jalankan dengan .\.venv\Scripts\python run_agent.py.

# run_agent.py: wire the tools into an agent and run it
import json
import os

from langchain.agents import create_agent
from langchain.messages import AIMessage
from captcha_tools import PAGE_URL, SITEKEY, reset_run
from captcha_tools import solve_recaptcha, submit_form

SYSTEM = (
    "Sign the user up on the page. Call solve_recaptcha, then "
    "submit_form with the handle it returns. Never invent a handle. "
    "If a tool says retryable is false, stop and report the error."
)


def build_model():
    name = os.getenv("CAPTCHA_AGENT_MODEL")  # "provider:model-id"
    if name:
        from langchain.chat_models import init_chat_model
        return init_chat_model(name)
    # No API key: a scripted model that calls the tools in order.
    from fake_model import ScriptedChat, call
    first = call("solve_recaptcha",
                 {"sitekey": SITEKEY, "page_url": PAGE_URL}, "c1")

    def second(msgs):  # read the handle out of the first tool result
        handle = json.loads(msgs[-1].content)["handle"]
        args = {"handle": handle, "email": "[email protected]"}
        return call("submit_form", args, "c2")

    return ScriptedChat(script=[first, second, AIMessage(content="Done.")])


agent = create_agent(build_model(), [solve_recaptcha, submit_form],
                     system_prompt=SYSTEM)

if __name__ == "__main__":
    reset_run()
    task = "Sign up [email protected] at %s. Sitekey: %s"
    out = agent.invoke({"messages": [("user", task % (PAGE_URL, SITEKEY))]})
    for m in out["messages"]:
        print(m.type, "|", str(m.content or m.tool_calls)[:100])

Sebuah run mencetak percakapan: pesan pengguna, panggilan solve, hasil tool yang hanya berisi handle, panggilan submit, jawaban halaman, dan Done. Pada halaman mock, hasil tool hanya berisi handle dan masa berlakunya, dan halaman menerima satu post.

Pakai model sungguhan

Instal paket penyedia untuk model Anda dan setel key serta nama modelnya. Tidak ada yang berubah pada tool. Ini hanya dijalankan terhadap pengganti lokal yang memakai format request OpenAI dan Anthropic, dengan key palsu, memakai langchain-openai 1.7.2 dan langchain-anthropic 1.7.6. Tidak ada penyedia sungguhan yang dipanggil.

# pip install langchain-anthropic   (or langchain-openai for OpenAI)
.\.venv\Scripts\python -m pip install langchain-anthropic
$env:PAGE_URL = "https://your-site.example/signup"   # your own page
$env:SITEKEY = "YOUR_SITEKEY"
$env:ANTHROPIC_API_KEY = "YOUR_PROVIDER_KEY"
$env:CAPTCHA_AGENT_MODEL = "anthropic:YOUR_MODEL_ID"
.\.venv\Scripts\python run_agent.py

Pengganti itu menunjukkan apa yang dikirim setiap wrapper. ChatOpenAI mengirim tool sebagai function dengan name, description, dan parameters, ke /v1/chat/completions. ChatAnthropic mengirimnya dengan name, description, dan input_schema, ke /v1/messages. Tidak satu pun mengirim tool_choice, strict, atau parallel_tool_calls secara default. Skemanya mewajibkan sitekey dan page_url, dan kind sebagai enum dengan nilai default. Dengan strict diaktifkan, LangChain menulis ulang skema agar setiap properti wajib, dan apakah penyedia menerimanya tidak diuji.

Panggilan paralel dan tool async

Model bisa meminta beberapa tool dalam satu giliran, dan LangChain menjalankannya pada saat yang sama. Satu pemecahan 2 detik memakan 3.8 detik dalam run mock, karena SDK menunggu 0.25, 0.5, 1, dan 2 detik di antara polling. Tiga pemecahan dalam satu giliran juga selesai dalam 3.8 detik ketika mock punya ruang untuk ketiganya. Dengan mock dibatasi satu thread, mock mengantrekan sisanya dan giliran yang sama memakan 7.8 detik. Semaphore bernama _slots di captcha_tools.py membatasi berapa banyak job yang terbuka sekaligus di proses ini. Semaphore itu tidak menghemat waktu dalam run mock, dan batas di bawah jumlah thread mock hanya menambah waktu: dengan mock 2 thread, batas 1 memakan 11.3 detik dibandingkan 7.5 detik dengan batas 2. Tujuannya adalah mencegah rentetan panggilan paralel mengantrekan lebih banyak job daripada Max. Threads milik CapSkip, yang secara default 10 untuk reCAPTCHA, karena job yang menunggu melewati Wait Timeout 250 detik akan gagal. Bagian itu tidak diuji. Setel batas ke 2 atau lebih tinggi, sampai Max. Threads.

Tool def biasa adalah default yang aman. Di bawah agent.ainvoke, LangChain menjalankannya di thread pekerja, jadi event loop tetap bebas: di PC ini loop tidak pernah macet lebih dari 0.02 detik selama pemecahan 2 detik. Tool async def yang memanggil klien CapSkip sinkron di dalamnya melakukan kebalikannya. Tool itu memblokir loop selama seluruh 3.7 detik. Jika Anda ingin async, pakai AsyncCapSkip, yang membuat loop macet hingga 0.3 detik dalam satu waktu, karena ia membangun klien HTTP baru untuk setiap request, sekitar 0.15 detik untuk masing-masing. Simpan ini sebagai async_tool.py. File ini tidak membutuhkan file lain. Satu tool bisa melayani kedua jalur:

# async_tool.py: one tool that works under .invoke and .ainvoke
import asyncio
import os

from capskip import AsyncCapSkip, CapSkip
from langchain_core.tools import StructuredTool

KEY = os.getenv("CAPSKIP_API_KEY", "capskip")
HOST = os.getenv("CAPSKIP_HOST", "127.0.0.1")
PORT = int(os.getenv("CAPSKIP_PORT", "8080"))
PAGE_URL = os.getenv("PAGE_URL", "https://example.com/page-with-recaptcha")


def solve_sync(sitekey: str, page_url: str) -> dict:
    token = CapSkip(apiKey=KEY, host=HOST, port=PORT).recaptcha(
        sitekey, page_url)["code"]
    return {"ok": True, "token_chars": len(token)}


async def solve_async(sitekey: str, page_url: str) -> dict:
    client = AsyncCapSkip(apiKey=KEY, host=HOST, port=PORT)
    result = await client.recaptcha(sitekey, page_url)
    return {"ok": True, "token_chars": len(result["code"])}


solve = StructuredTool.from_function(
    func=solve_sync,
    coroutine=solve_async,
    name="solve_recaptcha",
    description="Solve a reCAPTCHA v2 on a page; report the token size.",
)

if __name__ == "__main__":
    args = {"sitekey": os.getenv("SITEKEY", "YOUR_SITEKEY"),
            "page_url": PAGE_URL}
    print(solve.invoke(args))
    print(asyncio.run(solve.ainvoke(args)))

Tool yang hanya async dan dipanggil dengan agent.invoke gagal dengan NotImplementedError: StructuredTool does not support sync invocation. Kedua fungsi di atas menghindarinya.

Hentikan model yang terus meminta

Penghitung pemecahan menjaga CapSkip pada tiga job, tetapi tidak menghentikan model untuk meminta. Tidak ada yang lain yang menghentikannya dalam waktu dekat. Dengan create_agent, batas rekursi default adalah 9,999 langkah. Model berskrip yang tidak pernah berhenti membuat sekitar 5,000 panggilan model dalam waktu kurang dari semenit di PC ini sebelum GraphRecursionError: Recursion limit of 9999 reached without hitting a stop condition. Berikan recursion_limit dengan nilai 7 pada argumen config agent.invoke untuk menetapkan batas Anda sendiri. Batas pada tool itu sendiri lebih bersih, dan library agent punya satu. Simpan ini sebagai limited_agent.py di sebelah captcha_tools.py dan fake_model.py, yang diimpornya, lalu jalankan dengan .\.venv\Scripts\python limited_agent.py. Model berskripnya tidak pernah berhenti meminta pemecahan.

# limited_agent.py: stop a model that keeps asking for solves
from langchain.agents import create_agent
from langchain.agents.middleware import ToolCallLimitMiddleware
from captcha_tools import PAGE_URL, SITEKEY, reset_run
from captcha_tools import solve_recaptcha, submit_form
from fake_model import ScriptedChat, call

# A scripted model that never stops asking for a solve.
args = {"sitekey": SITEKEY, "page_url": PAGE_URL}
model = ScriptedChat(script=[
    (lambda msgs: call("solve_recaptcha", args, "c%d" % len(msgs)))
] * 50)

agent = create_agent(
    model,
    [solve_recaptcha, submit_form],
    middleware=[ToolCallLimitMiddleware(
        tool_name="solve_recaptcha", run_limit=3, exit_behavior="end")],
)

if __name__ == "__main__":
    reset_run()
    out = agent.invoke({"messages": [("user", "go")]})
    print(len(model.seen), "model calls")
    print(out["messages"][-1].content)

Dengan exit_behavior disetel ke end, run berhenti pada permintaan keempat dan CapSkip telah menerima tiga job. Pesan terakhir mengatakan bahwa batas panggilan tool solve_recaptcha tercapai, dengan 4 dari 3 panggilan. Opsi error memunculkan ToolCallLimitExceededError sebagai gantinya. Default-nya, continue, memblokir tool dan menyuruh model agar tidak memanggilnya lagi, dan model yang mengabaikan itu terus berjalan sampai batas rekursi, jadi setel salah satu dari dua opsi lainnya.

Dua run dalam satu proses

Vault di captcha_tools.py adalah variabel modul, jadi dua run agent dalam satu proses berbagi vault itu, dan reset_run mengosongkannya untuk keduanya. Untuk satu run pada satu waktu itu tidak masalah. Untuk run bersamaan, simpan vault per run. Parameter tool bertipe ToolRuntime diisi oleh LangChain dan tidak muncul di skema yang dilihat model. Berikan objek context ke setiap panggilan invoke, dan setiap run mendapat vault-nya sendiri. Simpan ini sebagai runtime_tools.py di sebelah fake_model.py, yang diimpornya, lalu jalankan dengan .\.venv\Scripts\python runtime_tools.py:

# runtime_tools.py: a per-run vault through ToolRuntime (hidden arg)
import json
import os
from dataclasses import dataclass, field

from capskip import CapSkip
from langchain.agents import create_agent
from langchain.messages import AIMessage
from langchain.tools import ToolRuntime, tool
from langchain_core.utils.function_calling import convert_to_openai_tool

from fake_model import ScriptedChat, call

solver = CapSkip(
    apiKey=os.getenv("CAPSKIP_API_KEY", "capskip"),
    host=os.getenv("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.getenv("CAPSKIP_PORT", "8080")),
)
PAGE_URL = os.getenv("PAGE_URL", "https://example.com/page-with-recaptcha")


@dataclass
class RunState:  # one instance per agent.invoke(..., context=...)
    vault: dict = field(default_factory=dict)  # handle -> token


@tool
def solve_recaptcha(
    sitekey: str, page_url: str, runtime: ToolRuntime[RunState]
) -> dict:
    """Solve a reCAPTCHA and return a handle for use_token."""
    token = solver.recaptcha(sitekey, page_url)["code"]
    handle = "cap_%d" % (len(runtime.context.vault) + 1)
    runtime.context.vault[handle] = token
    return {"ok": True, "handle": handle}


@tool
def use_token(handle: str, runtime: ToolRuntime[RunState]) -> dict:
    """Spend a handle from solve_recaptcha. It works once."""
    token = runtime.context.vault.pop(handle, None)
    if token is None:
        return {"ok": False, "error": "unknown_or_used_handle"}
    return {"ok": True, "token_chars": len(token)}


if __name__ == "__main__":
    # the model-facing schema has no "runtime" property
    schema = convert_to_openai_tool(solve_recaptcha)
    print(list(schema["function"]["parameters"]["properties"]))
    args = {"sitekey": os.getenv("SITEKEY", "YOUR_SITEKEY"),
            "page_url": PAGE_URL}
    def second(msgs):  # read the handle out of the first tool result
        handle = json.loads(msgs[-1].content)["handle"]
        return call("use_token", {"handle": handle}, "c2")

    model = ScriptedChat(script=[
        call("solve_recaptcha", args, "c1"), second, AIMessage(content="Done."),
    ])
    agent = create_agent(model, [solve_recaptcha, use_token],
                         context_schema=RunState)
    run = RunState()
    out = agent.invoke({"messages": [("user", "go")]}, context=run)
    print(out["messages"][-2].content, "| vault left:", run.vault)

Baris pertama yang dicetaknya mencantumkan dua properti yang dilihat model, sitekey dan page_url, tanpa runtime. Setelah run, vault kosong. Penghitung pemecahan di captcha_tools.py juga variabel modul, dan runtime_tools.py tidak menyertakan pemeriksaan host, anggaran, batas waktu, dan penanganan error agar tetap singkat, jadi bawa semuanya jika Anda menyalinnya. Tool yang dibangun seperti ini hanya berfungsi di dalam agent. Pasangan captcha_tools.py juga berjalan tanpa perubahan di graph LangGraph buatan tangan dengan ToolNode dan tools_condition, tanpa peringatan di langgraph 1.2.16. Versi ToolRuntime membutuhkan context_schema=RunState pada graph dan argumen context pada invoke.

Menjalankan agent terhadap CapSkip di mesin lain

Agent dan pemecah CAPTCHA tidak harus berada di PC yang sama. Alihkan CapSkip ke mode Server di pengaturan koneksi, dan ia akan mendengarkan di alamat jaringan atau IP publik Anda. IP publik statis direkomendasikan untuk akses jarak jauh yang stabil. Lalu setel tiga variabel yang sudah dibaca file itu. Pemecah CAPTCHA tetap berupa mesin Windows milik Anda sendiri, dan pemecahannya tetap tanpa kuota.

# CapSkip on another Windows PC, in Server mode: set these first
$env:PAGE_URL = "https://your-site.example/signup"   # your own page
$env:SITEKEY = "YOUR_SITEKEY"
$env:CAPSKIP_HOST = "192.168.1.50"
$env:CAPSKIP_PORT = "8080"
$env:CAPSKIP_API_KEY = "YOUR_API_KEY"   # only if API key validation is on
.\.venv\Scripts\python run_agent.py

Perhatikan tiga hal. SDK berbicara dengan CapSkip lewat HTTP biasa, sehingga API key dikirim sebagai teks polos. Jaga agar tautan itu berada di jaringan pribadi atau VPN, dan aktifkan API key validation. CapSkip membuka port-nya untuk semua alamat saat mode Server dimulai, jadi aturan blokir Windows Firewall untuk alamat yang tidak Anda harapkan adalah cara untuk mempersempitnya. Isi CAPSKIP_HOST hanya dengan alamatnya, dan pakai alamat IP: koneksi yang ditolak memakan sekitar 2 detik ke 127.0.0.1 dan sekitar 4 ke localhost di PC ini. Nilai yang diawali http:// gagal dengan NetworkException yang menyebut tidak dapat menyelesaikan host http. Nilai dengan port di ujungnya gagal dengan pesan bahwa host atau port tidak valid. Penyiapan jarak jauh tidak dijalankan untuk panduan ini, hanya variabelnya yang dibaca.

Kesalahan umum pada tool CAPTCHA LangChain

Apa yang Anda lihatPenyebabPerbaiki
ImportError: cannot import name ‘AgentExecutor’ from ‘langchain.agents’Tutorial lamaGunakan create_agent
Peringatan LangGraphDeprecatedSinceV10create_react_agent diimpor dari langgraph.prebuiltImpor create_agent dari langchain.agents
ValueError: Function must have a docstringTool tidak punya docstring dan tidak punya deskripsiTambahkan docstring
Error invoking tool, lalu: Input should be ‘v2’ or ‘v3’Model mengirim nilai di luar LiteralTidak perlu mengubah apa pun. Model sungguhan seharusnya membacanya dan memperbaiki nilainya, hal yang tidak diuji
NotImplementedError: StructuredTool does not support sync invocationTool yang hanya async dipanggil dengan invokeBeri tool fungsi sinkron juga
agent.invoke berakhir dengan exception CapSkipTool membiarkannya lolosTangkap di tool dan kembalikan dictionary
capskip_unreachableCapSkip tidak berjalan, atau port-nya salah. Port lokal yang ditolak gagal dalam sekitar 2 detik, dan PC jarak jauh yang dimatikan memakan sekitar 21 detik, karena klien sinkron tidak menetapkan timeout HTTP sendiriJalankan CapSkip dan periksa CAPSKIP_HOST dan CAPSKIP_PORT
capskip_rejectedCapSkip menolak job karena alasan selain CAPTCHA yang tidak dapat dipecahkan, seperti API key yang salahBaca baris log dan perbaiki key atau argumennya
host_not_allowedAlamat halaman tidak berada di host PAGE_URLSetel PAGE_URL ke halaman Anda
solve_budget_used_upMAX_SOLVES tercapai dalam run iniNaikkan nilainya, atau perbaiki penyebab pemecahan gagal
unknown_or_used_handle atau handle_expiredHandle sudah dipakai, atau menunggu melewati TOKEN_TTLPecahkan lagi
form_not_recognisedHalaman tidak punya field bernama _csrf, atau tidak punya action formulirUbah nama field di submit_form
GraphRecursionErrorModel tidak pernah sampai ke jawaban akhirSetel recursion_limit dan batas panggilan tool
403 yang menyebut RefererSitus memeriksanya dan requests tidak mengirim apa punPertahankan header Referer di submit_form

FAQ

Apakah saya butuh API key untuk language model agar bisa mencoba ini?

Tidak untuk menjalankan tool. Model berskrip yang membuat panggilannya, dan CapSkip tidak butuh key kecuali Anda mengaktifkan API key validation. Untuk melihat model sungguhan memakai tool, setel key penyedia dan CAPTCHA_AGENT_MODEL. Bagaimana model sungguhan berperilaku dengan tool ini tidak diuji untuk panduan ini.

Haruskah agent mengendalikan browser sebagai gantinya?

Itu tergantung halamannya. Tool ini cocok untuk formulir biasa yang bisa Anda kirimi post. Jika CAPTCHA hanya berfungsi di dalam sesi browser sungguhan, kendalikan browser dari kode Anda sendiri dan serahkan token ke halaman di sana, bukan lewat model. Contoh di panduan Browser Use menunjukkan agent yang menjalankan browser sendiri.

Bagaimana jika agent saya adalah Claude Code atau klien MCP lain?

Maka Anda tidak perlu menulis tool. CapSkip punya server MCP untuk itu, dan halaman pemecah CAPTCHA MCP menjelaskannya.

Bisakah agent berjalan di platform hosted?

Platform hosted yang menjalankan Python Anda bisa memanggil CapSkip yang Anda jalankan di PC Windows Anda sendiri, dan pemecah CAPTCHA tidak pernah berpindah ke platform itu. Jalankan CapSkip dalam mode Server, beri mesin itu IP publik statis, dan setel CAPSKIP_HOST. Key dan alamat halaman lalu dikirim sebagai teks polos lewat HTTP biasa, jadi pilih VPN atau jaringan pribadi, dan tambahkan aturan blokir Windows Firewall untuk alamat lain. Tidak ada platform hosted yang diuji untuk panduan ini.

Versi singkatnya

Tool CAPTCHA LangChain sebaiknya mengembalikan handle dan bukan token, menangkap setiap exception CapSkip dan mengembalikan dictionary kecil, memeriksa alamat halaman di dalam fungsi, menghitung pemecahan, dan berada di balik batas panggilan tool. Simpan token di dictionary di sisi Anda dan pakai di tool kedua. Gunakan tool def biasa kecuali Anda butuh async.

Agent yang mencoba ulang sampai berhasil hanya terjangkau jika setiap percobaan ulang gratis, dan dengan sebuah pemecah captcha di mesin Windows Anda sendiri, setiap percobaan ulang hanya memakan beberapa detik dan tidak ada biaya lain.