Cara Menambahkan Tool CAPTCHA LangChain ke Agent dengan 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:
| Desain | Panggilan 1 | Panggilan 2 | Panggilan 3 | Total |
|---|---|---|---|---|
| Token dikembalikan dan diteruskan kembali | 11 | 2,096 | 4,146 | 6,253 |
| Handle pendek dikembalikan | 11 | 152 | 253 | 416 |
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] = 0Pemeriksaan 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:
| Kasus | Yang keluar dari agent.invoke |
|---|---|
| Pemecahan tidak dapat dilakukan | ApiException: ERROR_CAPTCHA_UNSOLVABLE |
| recaptchaTimeout 2 detik | TimeoutException: timeout 2.0 exceeded, setelah 3.8 detik |
| CapSkip tidak sedang berjalan | NetworkException: Max retries exceeded, WinError 10061, setelah 2.0 detik |
| Panggilan yang menyetel version v3 dan invisible bersamaan | ValidationException: 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 lihat | Penyebab | Perbaiki |
|---|---|---|
| ImportError: cannot import name ‘AgentExecutor’ from ‘langchain.agents’ | Tutorial lama | Gunakan create_agent |
| Peringatan LangGraphDeprecatedSinceV10 | create_react_agent diimpor dari langgraph.prebuilt | Impor create_agent dari langchain.agents |
| ValueError: Function must have a docstring | Tool tidak punya docstring dan tidak punya deskripsi | Tambahkan docstring |
| Error invoking tool, lalu: Input should be ‘v2’ or ‘v3’ | Model mengirim nilai di luar Literal | Tidak perlu mengubah apa pun. Model sungguhan seharusnya membacanya dan memperbaiki nilainya, hal yang tidak diuji |
| NotImplementedError: StructuredTool does not support sync invocation | Tool yang hanya async dipanggil dengan invoke | Beri tool fungsi sinkron juga |
| agent.invoke berakhir dengan exception CapSkip | Tool membiarkannya lolos | Tangkap di tool dan kembalikan dictionary |
| capskip_unreachable | CapSkip 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 sendiri | Jalankan CapSkip dan periksa CAPSKIP_HOST dan CAPSKIP_PORT |
| capskip_rejected | CapSkip menolak job karena alasan selain CAPTCHA yang tidak dapat dipecahkan, seperti API key yang salah | Baca baris log dan perbaiki key atau argumennya |
| host_not_allowed | Alamat halaman tidak berada di host PAGE_URL | Setel PAGE_URL ke halaman Anda |
| solve_budget_used_up | MAX_SOLVES tercapai dalam run ini | Naikkan nilainya, atau perbaiki penyebab pemecahan gagal |
| unknown_or_used_handle atau handle_expired | Handle sudah dipakai, atau menunggu melewati TOKEN_TTL | Pecahkan lagi |
| form_not_recognised | Halaman tidak punya field bernama _csrf, atau tidak punya action formulir | Ubah nama field di submit_form |
| GraphRecursionError | Model tidak pernah sampai ke jawaban akhir | Setel recursion_limit dan batas panggilan tool |
| 403 yang menyebut Referer | Situs memeriksanya dan requests tidak mengirim apa pun | Pertahankan 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.
- Setiap tipe CAPTCHA yang ditangani paket Python: halaman pemecah CAPTCHA Python.
- Cara kerja pemecah kotak centang: halaman pemecah reCAPTCHA v2.
- Agent yang menjalankan browser sendiri: memecahkan CAPTCHA di Browser Use.
- Untuk Claude Code dan klien MCP lain: pemecah CAPTCHA MCP.
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.
