So fügen Sie einem Agenten mit CapSkip ein LangChain-Captcha-Tool hinzu

Um ein LangChain-Captcha zu lösen, geben Sie dem Agenten ein Tool, das CapSkip aufruft, und halten Sie den Token aus der Konversation heraus. Das naheliegende Tool gibt den Token an das Modell zurück, wodurch etwa 1.700 Zeichen in den nächsten Modellaufruf wandern und eine zweite Kopie in jeden Aufruf danach. Ein besseres Tool gibt ein kurzes Handle zurück, hält den Token auf Ihrer Seite und verbraucht ihn in einem zweiten Tool. Das Tool zum Lösen sollte die Exceptions von CapSkip abfangen, denn eine Exception, die aus einem Tool entweicht, beendet den gesamten Lauf. CapSkip läuft auf Ihrem eigenen Windows-PC, daher hat ein Agent, der Versuche wiederholt, keine Gebühr pro Lösung. Dieser Leitfaden baut dieses Tool-Paar mit LangChain 1.4 und führt es ganz ohne API-Key aus.
Was Sie brauchen
- CapSkip läuft auf einem Windows-PC, mit eingeschaltetem reCAPTCHA-Löser. Die Einrichtungsanleitung führt durch den ersten Start.
- Python 3.10 oder neuer. LangChain 1.4 und das Paket capskip 1.3.0 setzen es beide voraus. Alles unten lief unter Windows 11 mit Python 3.14.0, langchain 1.4.6, langchain-core 1.6.12, langgraph 1.2.16, pydantic 2.14.1 und requests 2.34.2. Die Blöcke bestanden außerdem unter Python 3.11.7. Python 3.10, 3.12 und 3.13 wurden nicht ausgeführt.
- Eine PowerShell-Eingabeaufforderung. Die Befehle verwenden PowerShell-Syntax.
- Eine Registrierungsseite, die Ihnen gehört, mit einer reCAPTCHA-v2-Checkbox. Das Beispielformular hat ein verstecktes Feld namens _csrf und ein Feld namens email. Das Tool zum Absenden sendet diese beiden plus den Token, passen Sie die Namen also an Ihr Formular an.
- Eine Adresse für den Löser. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass ein Agent auf einem anderen PC, einem VPS oder einer gehosteten Plattform es über die API aufrufen kann. Eingestellt wird beides unter Verbindungseinstellungen.
Beim Schreiben dieses Leitfadens wurde kein Sprachmodell aufgerufen. Die Agent-Läufe verwenden ein skriptgesteuertes Modell, das die Tool-Aufrufe in fester Reihenfolge macht, die Verdrahtung läuft also wirklich, sagt aber nichts darüber, wie sich ein echtes Modell verhält. Die Messungen stammen von einem Mock-CapSkip und einer Mock-Registrierungsseite (nachgebaute Testattrappen) auf demselben PC, verwendet mit dem echten LangChain und dem echten SDK. Dieser Leitfaden nennt sie die Mock-Läufe und die Mock-Seite. Die Regeln der Mock-Seite zum Annehmen und Ablehnen, die Art, wie der Mock Lösungen einreiht, und alle Zeiten sind die des Mocks, auf diesem PC. Ein Lösevorgang gegen ein laufendes CapSkip mit dem öffentlichen Test-sitekey von Google und den Standardeinstellungen dauerte 12,9 Sekunden und lieferte einen Token mit 1.700 Zeichen. Die Lösezeiten schwanken von Lauf zu Lauf.
Warum ein Tool mehr Sorgfalt braucht als ein Skript
In einem Skript wählen Sie die Argumente und lesen das Ergebnis. In einem Agenten wählt das Modell die Argumente, das Modell liest das Ergebnis, und eine Seite, die der Agent gelesen hat, kann versuchen, das Modell zu lenken. Das ändert vier Dinge. Das Ergebnis landet in der Konversation und wird bei jedem späteren Aufruf erneut gesendet. Eine Exception im Tool wird nicht für Sie behandelt. Das Modell kann beliebig oft um eine weitere Lösung bitten. Und ein sitekey und eine Adresse, die vom Modell stammen, müssen geprüft werden, bevor sie CapSkip erreichen. Ein Agent, der selbst einen Browser steuert, hat dieselben Probleme, und die Anleitung zu Browser Use behandelt diesen Weg. Dieser Leitfaden baut eine einfache Funktion, die das Modell aufrufen kann.
Schritt 1: Das Projekt anlegen
Verwenden Sie eine virtuelle Umgebung in einem neuen Ordner. Die Befehle rufen das Python der Umgebung über dessen Pfad auf, Sie müssen sie also nicht aktivieren. Das Paket langchain zieht langgraph, langchain-core und pydantic nach, insgesamt etwa 40 Pakete. Die Modellanbieter sind eigene Pakete, die Sie nur hinzufügen, wenn Sie ein echtes Modell verwenden.
# 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__)"
Verwenden Sie für den Agenten create_agent aus langchain.agents. Die älteren Helfer gibt es nicht mehr. AgentExecutor, initialize_agent und create_react_agent lösen ImportError aus, wenn sie aus langchain.agents importiert werden. Das create_react_agent in langgraph.prebuilt läuft noch und gibt eine Warnung LangGraphDeprecatedSinceV10 aus, die Ihnen sagt, create_agent aus langchain.agents zu importieren. Ein Tutorial, das eines davon verwendet, ist veraltet.
Schritt 2: das erste Tool, und warum es die falsche Form hat
Speichern Sie dies als naive_tool.py. Es ist das kürzeste brauchbare Tool: das Captcha lösen und den Token zurückgeben. Das SDK liest keine Umgebungsvariablen, daher liest die Datei Host, Port und Schlüssel selbst. Der Schlüssel spielt nur eine Rolle, wenn Sie in CapSkip die API-Key-Validierung eingeschaltet haben. Jede Datei liest die Seitenadresse aus PAGE_URL und den sitekey aus SITEKEY, setzen Sie also beide einmal in Ihrem PowerShell-Fenster, von Ihrer eigenen Registrierungsseite. CapSkip antwortet mit einem Fehler, wenn eine Seite kein reCAPTCHA-v2-Widget trägt.
# 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
Führen Sie dann naive_tool.py mit .\.venv\Scripts\python naive_tool.py aus.
# 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")Es funktioniert und gibt die Länge des Tokens aus. Diese Länge ist das Problem. Ein reCAPTCHA-Token, das gegen den öffentlichen Test-sitekey von Google gelöst wurde, kam auf etwa 1.700 Zeichen. Wenn das Tool ihn zurückgibt, steht der Token einmal im Tool-Ergebnis. Das Modell muss ihn dann als Argument an das nächste Tool weitergeben, sodass er ein zweites Mal im eigenen Tool-Aufruf des Modells steht. Jeder Aufruf danach trägt beide Kopien. Ein Mock-Lauf zählte die Zeichen, die bei jedem von drei Aufrufen an das Modell gingen, als Nachrichtentext plus Argumente des Tool-Aufrufs. Er nutzte ein skriptgesteuertes Modell, einen System-Prompt mit einem Zeichen, einen Mock-Token mit 2.000 Zeichen und, in der ersten Zeile, ein zweites Tool, das den Token als Argument nimmt. Ein echter System-Prompt und die Tool-Schemas fügen beiden Zeilen dieselbe Menge hinzu:
| Variante | Aufruf 1 | Aufruf 2 | Aufruf 3 | Gesamt |
|---|---|---|---|---|
| Token zurückgegeben und weitergereicht | 11 | 2,096 | 4,146 | 6,253 |
| Kurzes Handle zurückgegeben | 11 | 152 | 253 | 416 |
Zum Vergleich: Eine zufällige Zeichenfolge mit 2.000 Zeichen ergab mit einem OpenAI-Tokenizer etwa 1.400 Tokens. Andere Modelle zählen anders. Das ist ein Argument über Kosten und Größe, keine Aussage darüber, wie oft ein Modell eine lange Zeichenfolge falsch abschreibt. Es ist außerdem eine Stelle mehr, an der ein Kopierfehler einen Token zerstören könnte, der nur einmal funktioniert. Die Lösung ist, den Token auf Ihrer Seite zu halten und dem Modell ein Handle zu geben.
Schritt 3: Einstellungen für die echten Tools
Die echten Tools liegen in einer Datei, captcha_tools.py, die Sie aus vier eingefügten Teilen bauen. Speichern Sie diesen ersten Teil. Er liest die Einstellungen, baut den Client und deklariert den kleinen Zustand, den die Tools teilen. ALLOWED_HOSTS stammt aus Ihrer eigenen PAGE_URL, nie vom Modell.
# 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()Schritt 4: zuerst den Host prüfen
Das Modell wählt die Seitenadresse, die an CapSkip geht, und Text auf einer Seite, die der Agent gelesen hat, kann es zu einer anderen drängen. Erzwingen Sie die Adresse im Tool, denn eine Regel im Prompt lässt sich umgehen. Hängen Sie dies an captcha_tools.py an.
# 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] = 0Die Prüfung berührt den Port absichtlich. Eine Prüfung, die nur den Hostnamen vergleicht, würde eine Adresse wie http://127.0.0.1:18512.evil.example/page durchlassen, weil der Parser 127.0.0.1 weiterhin als Host meldet, wenn der Port fehlerhaft ist. Das Lesen des Ports löst in diesem Fall ValueError aus, und die Funktion weist die Adresse dann ab. Sie weist außerdem Adressen mit file:, javascript: und ftp:, leere Zeichenfolgen und Adressen mit einem Benutzernamen darin ab. Sie vergleicht nur den Host, nicht den Port. Das Tool zum Absenden in Schritt 6 führt dieselbe Prüfung für die action-Adresse des Formulars aus.
Schritt 5: das Tool zum Lösen
Hängen Sie dies an captcha_tools.py an. Drei Entscheidungen darin sind wichtig. Das Tool fängt jede CapSkip-Exception ab und gibt ein kleines Dictionary zurück, denn eine Exception, die aus dem Tool entweicht, beendet den gesamten Agent-Lauf. Es zählt jeden CapSkip-Aufruf, den es macht, auch fehlgeschlagene, sodass ein Modell, das immer weiter fragt, CapSkip nicht dauerhaft beschäftigen kann. Und es speichert den Token in einem Dictionary und gibt nur ein Handle zurück.
# 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}Der Docstring ist das, was das Modell liest, beschreiben Sie dort also, was jedes Argument ist und dass der Token aus der Konversation herausbleibt. Ein Tool ohne Docstring scheitert bei der Definition mit ValueError: Function must have a docstring if description not provided. Der Typ Literal wird im Schema zu einem Enum. Ein Wert außerhalb davon, etwa v4, erreicht die Funktion nie, und CapSkip wird nicht aufgerufen. Das Modell sieht eine Meldung, die mit Error invoking tool ‘solve_recaptcha’ beginnt, die gesendeten Argumente wiederholt, kind: Input should be ‘v2’ or ‘v3’ nennt und mit Please fix the error and try again endet.
Was passiert, wenn ein Tool eine Exception auslöst
Deshalb gibt das Tool zum Lösen Dictionaries zurück. In den Mock-Läufen machte create_agent nur Fehler bei fehlerhaften Argumenten zu einer Nachricht für das Modell. Jede andere Exception verließ agent.invoke unverändert und beendete den Lauf. Jedes dieser Ergebnisse kam aus dem Agenten, wobei der Mock CapSkip ersetzte:
| Fall | Was aus agent.invoke entwich |
|---|---|
| Die Lösung ist nicht möglich | ApiException: ERROR_CAPTCHA_UNSOLVABLE |
| recaptchaTimeout von 2 Sekunden | TimeoutException: timeout 2.0 exceeded, nach 3,8 Sekunden |
| CapSkip läuft nicht | NetworkException: Max retries exceeded, WinError 10061, nach 2,0 Sekunden |
| Ein Aufruf, der version v3 und invisible zusammen setzt | ValidationException: invisible is only supported for reCAPTCHA v2. |
Wenn Sie handle_tool_error an einem Tool auf True setzen, hilft das hier nicht. Es deckt die eigene ToolException von LangChain ab, und keine der Exceptions von CapSkip ist eine. Das Timeout löste spät aus, weil das SDK sein Limit nur zwischen den Abfragen prüft. Das Tool oben gibt stattdessen ein kleines Dictionary zurück, mit ok auf false, einem Fehlernamen wie unsolvable und retryable auf true, womit das Modell etwas anfangen kann. Nur eine Fehlermeldung, die UNSOLVABLE enthält, gilt als retryable. Jeder andere API-Fehler, etwa ein falscher API-Key, kommt als capskip_rejected zurück und gilt nicht als wiederholbar. Details wie der Text des Verbindungsfehlers gehen ins Log, nicht an das Modell. Wenn Sie die Tools lieber roh lassen, hat die Agent-Bibliothek zwei Hooks. ToolErrorMiddleware nimmt eine Funktion on_error entgegen, die Text für das Modell zurückgibt, und wenn diese Funktion nichts zurückgibt, beendet die Exception den Lauf trotzdem. Mit dem Decorator wrap_tool_call schreiben Sie try und except selbst. Beide hielten den Lauf in den Mock-Läufen am Leben.
Schritt 6: das Tool zum Absenden
Hängen Sie den letzten Teil von captcha_tools.py an. Das Tool zum Absenden verbraucht ein Handle. Es nimmt den Token aus dem Dictionary heraus, sodass ein Handle nur einmal funktioniert, und weist ein Handle ab, das älter als TOKEN_TTL ist. Der Token läuft ab, daher erlaubt die Datei 100 Sekunden, unter den zwei Minuten, die Google dokumentiert. Das lässt etwa 20 Sekunden für den Abruf der Seite und das Absenden. Das Tool zum Absenden hat kein Adressargument, weil die Seitenadresse gespeichert wurde, als der Token gelöst wurde. Auch die action-Adresse des Formulars ist Seitentext, deshalb führt das Tool dieselbe Host-Prüfung dafür aus und folgt keinen Weiterleitungen. Das Modell kann nicht wählen, wohin der Token gesendet wird.
# 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 sendet keinen Referer-Header, und eine Website, die ihn prüft, antwortet mit 403. Das Tool setzt einen. Die Feldnamen im Code oben gehören zum Beispielformular, und ein Formular, das nicht passt, liefert form_not_recognised. Ein zweites Absenden mit demselben Handle liefert unknown_or_used_handle, und die Mock-Website sah genau einen Post. Das Tool gibt die ersten 80 Zeichen der Antwort der Seite an das Modell zurück. Text von einer Seite ist ein Weg, auf dem fremde Worte das Modell erreichen, geben Sie also nur den Status zurück, wenn Sie das nicht möchten. Den rohen Token zweimal zu senden, ließ den Mock mit 403 und token already used antworten, was die Regel des Mocks ist. Siehe Ablauf des reCAPTCHA-Tokens zur Gültigkeitsdauer eines echten Tokens.
Schritt 7: ein skriptgesteuertes Modell, kein API-Key
Speichern Sie dies als fake_model.py. Es ist ein kleines Chat-Modell, das die Tool-Aufrufe zurückgibt, die Sie vorgeben, der Reihe nach. Die Agent-Bibliothek ruft bind_tools an einem Modell auf, und dieses nimmt den Aufruf an und ignoriert die Schemas. Es beweist, dass Ihre Tools, ihr Zustand und die Agent-Schleife funktionieren. Es kann nicht zeigen, wie ein echtes Modell Argumente wählt oder einen Fehler liest.
# 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)])Schritt 8: den Agenten bauen und ausführen
Speichern Sie dies als run_agent.py. Ohne CAPTCHA_AGENT_MODEL nutzt es das skriptgesteuerte Modell. Ist die Variable auf einen Anbieter und einen Modellnamen gesetzt, baut es über init_chat_model ein echtes Modell. Der System-Prompt weist das Modell an, aufzuhören, wenn ein Tool meldet, dass ein Fehler nicht retryable ist. Führen Sie es mit .\.venv\Scripts\python run_agent.py aus.
# 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])Ein Lauf gibt die Konversation aus: die Nutzernachricht, einen Aufruf zum Lösen, das Tool-Ergebnis, das nur ein Handle enthält, einen Aufruf zum Absenden, die Antwort der Seite und Done. Auf der Mock-Seite enthielt das Tool-Ergebnis nur ein Handle und seine Lebensdauer, und die Seite akzeptierte einen Post.
Ein echtes Modell verwenden
Installieren Sie das Anbieter-Paket für Ihr Modell und setzen Sie dessen Key und den Modellnamen. An den Tools ändert sich nichts. Dies wurde nur gegen einen lokalen Stellvertreter ausgeführt, der die Anfrageformate von OpenAI und Anthropic spricht, mit Dummy-Keys, mit langchain-openai 1.7.2 und langchain-anthropic 1.7.6. Es wurde kein echter Anbieter aufgerufen.
# 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
Der Stellvertreter zeigte, was jeder Wrapper sendet. ChatOpenAI sendet das Tool als Funktion mit name, description und parameters an /v1/chat/completions. ChatAnthropic sendet es mit name, description und input_schema an /v1/messages. Keiner von beiden sendete standardmäßig tool_choice, strict oder parallel_tool_calls. Das Schema hatte sitekey und page_url als required und kind als Enum mit einem Standardwert. Bei eingeschaltetem strict schreibt LangChain das Schema so um, dass jede Property required ist, und ob ein Anbieter das akzeptiert, wurde nicht getestet.
Parallele Aufrufe und asynchrone Tools
Ein Modell kann in einer Runde mehrere Tools anfordern, und LangChain führt sie gleichzeitig aus. Eine Lösung von 2 Sekunden dauert in den Mock-Läufen 3,8 Sekunden, weil das SDK zwischen den Abfragen 0,25; 0,5; 1 und 2 Sekunden wartet. Drei davon in einer Runde waren ebenfalls in 3,8 Sekunden fertig, wenn der Mock Platz für alle drei hatte. Bei einem Mock, der auf einen Thread begrenzt war, reihte der Mock die übrigen ein, und dieselbe Runde dauerte 7,8 Sekunden. Die Semaphore namens _slots in captcha_tools.py begrenzt, wie viele Jobs dieser Prozess gleichzeitig offen hat. In den Mock-Läufen sparte sie keine Zeit, und eine Obergrenze unter der Thread-Zahl des Mocks kostete nur Zeit: Bei einem Mock mit 2 Threads brauchte eine Obergrenze von 1 insgesamt 11,3 Sekunden, eine Obergrenze von 2 dagegen 7,5 Sekunden. Ihr Zweck ist, zu verhindern, dass ein Schwall paralleler Aufrufe mehr Jobs einreiht als die Einstellung Max. Threads von CapSkip, die für reCAPTCHA standardmäßig 10 beträgt, denn ein Job, der länger als das Wait Timeout von 250 Sekunden wartet, schlägt fehl. Dieser Teil wurde nicht getestet. Setzen Sie die Obergrenze auf 2 oder höher, bis hin zu Max. Threads.
Ein einfaches def-Tool ist der sichere Standard. Unter agent.ainvoke führt LangChain es in einem Worker-Thread aus, sodass der Event-Loop frei bleibt: Auf diesem PC stand der Loop während einer Lösung von 2 Sekunden nie länger als 0,02 Sekunden still. Ein async-def-Tool, das darin den synchronen CapSkip-Client aufruft, bewirkt das Gegenteil. Es blockierte den Loop die gesamten 3,7 Sekunden. Wenn Sie async wollen, nehmen Sie AsyncCapSkip, das den Loop bis zu 0,3 Sekunden am Stück anhielt, weil es für jede Anfrage einen neuen HTTP-Client baut, mit je etwa 0,15 Sekunden. Speichern Sie dies als async_tool.py. Es braucht keine andere Datei. Ein Tool kann beide Wege bedienen:
# 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)))Ein Tool, das nur async ist und mit agent.invoke aufgerufen wird, scheitert mit NotImplementedError: StructuredTool does not support sync invocation. Die zwei Funktionen oben vermeiden das.
Ein Modell stoppen, das immer weiter fragt
Der Zähler für Lösungen hält CapSkip bei drei Jobs, hindert das Modell aber nicht am Nachfragen. Sonst stoppt es nichts so bald. Bei create_agent liegt das Standard-Rekursionslimit bei 9.999 Schritten. Ein skriptgesteuertes Modell, das nie aufhörte, machte auf diesem PC in unter einer Minute etwa 5.000 Modellaufrufe, bevor GraphRecursionError: Recursion limit of 9999 reached without hitting a stop condition kam. Übergeben Sie recursion_limit mit dem Wert 7 im Argument config von agent.invoke, um ein eigenes zu setzen. Ein Limit am Tool selbst ist sauberer, und die Agent-Bibliothek hat eines. Speichern Sie dies als limited_agent.py neben captcha_tools.py und fake_model.py, die es importiert, und führen Sie es mit .\.venv\Scripts\python limited_agent.py aus. Sein skriptgesteuertes Modell hört nie auf, um eine Lösung zu bitten.
# 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)Mit exit_behavior auf end stoppte der Lauf bei der vierten Anfrage, und CapSkip hatte drei Jobs erhalten. Die letzte Nachricht sagte, das Limit für Aufrufe des Tools solve_recaptcha sei erreicht, mit 4 von 3 Aufrufen. Die Option error löst stattdessen ToolCallLimitExceededError aus. Der Standard, continue, blockiert das Tool und weist das Modell an, es nicht erneut aufzurufen, und ein Modell, das das ignoriert, macht weiter, bis das Rekursionslimit greift, setzen Sie also eine der beiden anderen.
Zwei Läufe in einem Prozess
Der Tresor in captcha_tools.py (das Dictionary, in dem die Tokens liegen) ist eine Modulvariable, daher teilen sich zwei Agent-Läufe in einem Prozess ihn, und reset_run leert ihn für beide. Für einen Lauf nach dem anderen ist das in Ordnung. Halten Sie den Tresor bei gleichzeitigen Läufen pro Lauf getrennt. Ein Tool-Parameter vom Typ ToolRuntime wird von LangChain gefüllt und erscheint nicht im Schema, das das Modell sieht. Übergeben Sie jedem invoke-Aufruf ein Kontextobjekt, dann bekommt jeder Lauf seinen eigenen Tresor. Speichern Sie dies als runtime_tools.py neben fake_model.py, das es importiert, und führen Sie es mit .\.venv\Scripts\python runtime_tools.py aus:
# 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)Die erste Zeile, die es ausgibt, listet die beiden Eigenschaften, die das Modell sieht, sitekey und page_url, ohne runtime. Nach dem Lauf ist der Tresor leer. Der Zähler für Lösungen in captcha_tools.py ist ebenfalls eine Modulvariable, und runtime_tools.py lässt die Host-Prüfung, das Budget, das Zeitlimit und die Fehlerbehandlung weg, um kurz zu bleiben, übernehmen Sie diese also, wenn Sie es kopieren. Ein auf diese Weise gebautes Tool funktioniert nur innerhalb eines Agenten. Das Paar aus captcha_tools.py lief außerdem unverändert in einem von Hand gebauten LangGraph-Graphen mit ToolNode und tools_condition, ohne Warnungen mit langgraph 1.2.16. Die ToolRuntime-Version braucht context_schema=RunState am Graphen und ein Argument context an invoke.
Den Agenten gegen CapSkip auf einem anderen Rechner ausführen
Der Agent und der Löser müssen sich keinen PC teilen. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP. Für stabilen Fernzugriff wird eine statische öffentliche IP empfohlen. Setzen Sie dann die drei Variablen, die die Datei bereits liest. Der Löser bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.
# 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
Beachten Sie drei Dinge. Das SDK spricht mit CapSkip über reines HTTP, daher wandert der API-Schlüssel im Klartext. Halten Sie diese Verbindung in einem privaten Netzwerk oder einem VPN, und aktivieren Sie die API-Key-Validierung. CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse, daher grenzen Sie ihn mit einer Blockierregel der Windows Firewall für die Adressen ein, die Sie nicht erwarten. Tragen Sie in CAPSKIP_HOST nur die Adresse ein und verwenden Sie eine IP-Adresse: Eine verweigerte Verbindung dauerte auf diesem PC etwa 2 Sekunden zu 127.0.0.1 und etwa 4 zu localhost. Ein Wert, der mit http:// beginnt, scheitert mit einer NetworkException, die meldet, den Host http nicht auflösen zu können. Ein Wert mit dem Port am Ende scheitert mit einer Meldung, dass Host oder Port ungültig ist. Das Remote-Setup wurde für diesen Leitfaden nicht ausgeführt, es wurden nur die Variablen gelesen.
Häufige Fehler bei einem LangChain-Captcha-Tool
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| ImportError: cannot import name ‘AgentExecutor’ from ‘langchain.agents’ | Ein altes Tutorial | create_agent verwenden |
| Warnung LangGraphDeprecatedSinceV10 | create_react_agent, importiert aus langgraph.prebuilt | Importieren Sie create_agent aus langchain.agents |
| ValueError: Function must have a docstring | Das Tool hat weder Docstring noch Beschreibung | Fügen Sie einen Docstring hinzu |
| Error invoking tool, danach: Input should be ‘v2’ or ‘v3’ | Das Modell hat einen Wert außerhalb des Literal gesendet | Nichts zu ändern. Ein echtes Modell sollte es lesen und den Wert korrigieren, was nicht getestet wurde |
| NotImplementedError: StructuredTool does not support sync invocation | Ein Tool, das nur async ist, wurde mit invoke aufgerufen | Geben Sie dem Tool auch eine synchrone Funktion |
| agent.invoke endet mit einer CapSkip-Exception | Das Tool hat sie entweichen lassen | Fangen Sie sie im Tool ab und geben Sie ein Dictionary zurück |
| capskip_unreachable | CapSkip läuft nicht, oder der Port ist falsch. Ein verweigerter lokaler Port scheitert nach etwa 2 Sekunden, und ein ausgeschalteter Remote-PC brauchte etwa 21 Sekunden, weil der synchrone Client kein eigenes HTTP-Timeout setzt | Starten Sie CapSkip und prüfen Sie CAPSKIP_HOST und CAPSKIP_PORT |
| capskip_rejected | CapSkip hat den Job aus einem anderen Grund als einem unlösbaren Captcha abgelehnt, etwa wegen eines falschen API-Keys | Lesen Sie die Logzeile und korrigieren Sie den Key oder die Argumente |
| host_not_allowed | Die Seitenadresse gehört nicht zum Host von PAGE_URL | Setzen Sie PAGE_URL auf Ihre Seite |
| solve_budget_used_up | MAX_SOLVES in diesem Lauf erreicht | Erhöhen Sie den Wert oder beheben Sie, warum die Lösungen scheitern |
| unknown_or_used_handle oder handle_expired | Das Handle wurde verbraucht oder hat TOKEN_TTL überschritten | Erneut lösen |
| form_not_recognised | Die Seite hat kein Feld namens _csrf oder keine Formular-action | Ändern Sie die Feldnamen in submit_form |
| GraphRecursionError | Das Modell kam nie zu einer endgültigen Antwort | Setzen Sie recursion_limit und ein Limit für Tool-Aufrufe |
| Eine 403, die den Referer erwähnt | Die Website prüft ihn, und requests hat keinen gesendet | Behalten Sie den Referer-Header in submit_form bei |
FAQ
Brauche ich einen API-Key für ein Sprachmodell, um das auszuprobieren?
Nicht, um die Tools auszuführen. Das skriptgesteuerte Modell macht die Aufrufe, und CapSkip braucht keinen Key, es sei denn, Sie haben die API-Key-Validierung eingeschaltet. Um ein echtes Modell die Tools nutzen zu sehen, setzen Sie einen Anbieter-Key und CAPTCHA_AGENT_MODEL. Wie sich ein echtes Modell mit diesen Tools verhält, wurde für diesen Leitfaden nicht getestet.
Sollte der Agent stattdessen einen Browser steuern?
Das hängt von der Seite ab. Dieses Tool passt zu einem einfachen Formular, an das Sie senden können. Wenn das Captcha nur innerhalb einer echten Browser-Sitzung funktioniert, steuern Sie den Browser aus Ihrem eigenen Code und übergeben Sie den Token dort an die Seite, nicht über das Modell. Die Anleitung zu Browser Use zeigt einen Agenten, der den Browser selbst steuert.
Und wenn mein Agent Claude Code oder ein anderer MCP-Client ist?
Dann schreiben Sie das Tool nicht selbst. Dafür hat CapSkip einen MCP-Server, und die MCP-Captcha-Solver-Seite erklärt ihn.
Kann der Agent auf einer gehosteten Plattform laufen?
Eine gehostete Plattform, die Ihr Python ausführt, kann ein CapSkip aufrufen, das Sie auf Ihrem eigenen Windows-PC betreiben, und der Löser zieht nie auf die Plattform um. Schalten Sie CapSkip in den Server-Modus, geben Sie dem Rechner eine statische öffentliche IP und setzen Sie CAPSKIP_HOST. Der Schlüssel und die Seitenadresse wandern dann im Klartext über reines HTTP, bevorzugen Sie daher ein VPN oder ein privates Netzwerk und fügen Sie in der Windows Firewall eine Blockierregel für andere Adressen hinzu. Für diesen Leitfaden wurde keine gehostete Plattform getestet.
Die Kurzfassung
Ein LangChain-Captcha-Tool sollte ein Handle statt des Tokens zurückgeben, jede CapSkip-Exception abfangen und ein kleines Dictionary zurückgeben, die Seitenadresse innerhalb der Funktion prüfen, Lösungen zählen und hinter einem Limit für Tool-Aufrufe stehen. Halten Sie den Token in einem Dictionary auf Ihrer Seite und verbrauchen Sie ihn in einem zweiten Tool. Verwenden Sie ein einfaches def-Tool, außer Sie brauchen async.
- Alle Captcha-Typen, die das Python-Paket verarbeitet, finden Sie auf der Python-Captcha-Solver-Seite.
- Wie der Checkbox-Solver funktioniert: die reCAPTCHA-v2-Solver-Seite.
- Ein Agent, der seinen eigenen Browser steuert: Captchas in Browser Use lösen.
- Für Claude Code und andere MCP-Clients: MCP-Captcha-Solver.
Ein Agent, der es so lange versucht, bis es klappt, ist nur bezahlbar, wenn jeder Versuch kostenlos ist, und ein Captcha-Löser auf Ihrem eigenen Windows-Rechner sorgt dafür, dass jeder Versuch nur wenige Sekunden kostet und sonst nichts.
