Cursus
GLM-5-Turbo is speciaal ontworpen voor agent-achtige workflows, waarbij het model meerstapsuitvoering coördineert. Het is geoptimaliseerd voor het begrijpen van complexe instructies, het betrouwbaar aanroepen van tools en het produceren van gestructureerde outputs die door systemen kunnen worden gebruikt.
In deze tutorial bouwen we een realtime browseragent die actief het web doorzoekt, relevante informatie extraheert en een heldere samenvatting voor de gebruiker genereert. Aan het eind van deze tutorial heb je een herbruikbare architectuur die je kunt uitbreiden van vluchten naar elke echte zoek- en extractietaak.
Bekijk ons GLM-5 Turbo-project in actie in de onderstaande video:
Wat is GLM-5-Turbo?
GLM-5-Turbo is ontworpen om agent-workflows in de echte wereld te ondersteunen. Het biedt meerdere denkmodi, zodat gebruikers afhankelijk van de taak kunnen balanceren tussen snellere reacties en diepere redenering.
Voor interactieve toepassingen ondersteunt het ook streaming output, waardoor reacties token-voor-token kunnen worden geleverd voor een responsievere gebruikerservaring.
Een van zijn kernsterktes is function calling, waarmee het model externe tools betrouwbaar kan aanroepen. Het model ondersteunt ook gestructureerde outputs zoals JSON, wat de integratie met downstreamsystemen en UI-lagen vereenvoudigt.
Voor langere workflows bevat GLM-5-Turbo contextcaching, wat helpt de prestaties te behouden bij uitgebreide interacties zonder dezelfde informatie herhaaldelijk te verwerken. Het ondersteunt ook integratie met externe systemen via MCP (Model Context Protocol), waardoor het model verbinding kan maken met extra tools en databronnen.
Gebouwd voor agent-workflows
GLM-5-Turbo is diep geoptimaliseerd voor agent-achtige omgevingen, met name scenario's zoals OpenClaw, waar taken meerdere stappen, tools en lange uitvoeringsketens omvatten.
Deze optimalisatie verbetert verschillende kritieke gebieden:
- Betrouwbaarheid van toolaanroepen: Dit zorgt ervoor dat het model consequent de juiste functies kan aanroepen.
- Instructiedecompositie: Decompositie stelt het model in staat complexe taken op te delen in kleinere stappen.
- Stabiliteit bij lange ketens: Essentieel voor meerstapsagents.
In de praktijk is GLM-5-Turbo snel genoeg voor interactieve systemen en toch betrouwbaar in meerstaps- en tool-calling-scenario's, precies wat we nodig hebben voor het bouwen van browseragents.
GLM-5-Turbo voorbeeldproject: bouw een realtime browseragent
In deze tutorial maken we een Streamlit-applicatie die zich gedraagt als een lichte reisassistent. De gebruiker geeft een vraag op zoals “Vind de goedkoopste vluchten van SFO naar JFK van volgende vrijdag tot maandag.” Het systeem zal vervolgens:
- Het web doorzoeken naar relevante vluchtpagina's
- Die pagina's openen en inspecteren
- Handige informatie extraheren zoals prijs, duur en tussenstops
- De beste opties samenvatten in een gebruiksvriendelijk formaat
Op hoofdlijnen bestaat het systeem uit vier componenten die samenwerken.
- De eerste is de LLM-laag (
llm_client.py), die chat-completionverzoeken verstuurt naar Z.AI of OpenRouter. - De tweede is de browsertoollaag (
browser_tools.py), die acties aanbiedt zoals zoeken, pagina's openen en content extraheren. - De derde is de agentloop (
agent.py), die het model en de tools verbindt tot een meerstapsworkflow. - Tot slot zorgt de Streamlit-UI (
app.py) voor een interface voor interactie en visualisatie.
Volledige broncode: https://github.com/AashiDutt/GLM5_TURBO
Stap 1: Vereisten
Voordat je de applicatie bouwt, zorg dat je het volgende hebt:
- Python 3.9+
- Een GLM-5-Turbo API-sleutel (Z.AI of OpenRouter)
- Playwright geïnstalleerd voor browserautomatisering
Installeer de benodigde libraries:
pip install streamlit pandas python-dotenv requests playwright
playwright install chromium
Het bestand app.py gebruikt Streamlit, pandas, dotenv en custom modules. Terwijl browser_tools.py gebruikmaakt van requests en Playwright. API-calls worden gedaan in llm_client.py met requests.
Voer nu het volgende commando uit om een .env-bestand te maken waarin onze API-sleutel(s) worden opgeslagen:
cp .env.example .env
Kies je voorkeurs-API-endpoint en voeg de API-sleutel toe:
.env
PROVIDER=zai
# Direct Z.AI API
ZAI_API_KEY=Your Z.AI API key
ZAI_BASE_URL=https://api.z.ai/api/paas/v4/chat/completions
ZAI_MODEL=glm-5-turbo
# OpenRouter
OPENROUTER_API_KEY=Your OpenRouter API key
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_MODEL=z-ai/glm-5-turbo
# Browser
HEADLESS=true
MAX_STEPS=10
Op vergelijkbare wijze wordt de Playwright-browser gestart vanuit deze constructor in browser_tools.py:
class BrowserTools:
def __init__(self, headless: bool = True) -> None:
self.playwright = sync_playwright().start()
self.browser = self.playwright.chromium.launch(headless=headless)
self.context = self.browser.new_context(viewport={"width": 1440, "height": 1100})
self.page = self.context.new_page()
Dit blok initialiseert een volledig gecontroleerde browseromgeving met Playwright, die de agent gebruikt om met het web te interageren.
Het start de Playwright-engine, lanceert een Chromium-browser, creëert een geïsoleerde browsersessie en opent een nieuwe pagina waar alle acties plaatsvinden.
Met deze setup kan de agent echte websites browsen, live content observeren en beslissingen nemen op basis van wat hij ziet.
Stap 2: De LLM-client instellen
Vervolgens richten we onze LLMClient in, die zowel directe Z.AI-gebruik als OpenRouter ondersteunt met dezelfde interface. Het providerspecifieke endpoint wordt in de constructor geselecteerd. Aangezien de prijsstelling vergelijkbaar is, kun je voor deze demo elk van beide API's gebruiken, naar voorkeur.
Hier is de kernsetup:
class LLMClient:
def __init__(
self,
provider: str,
api_key: str,
model: str,
app_name: str = "GLM-5 Browser Agent Demo",
app_url: str = "http://localhost:8501",
) -> None:
self.provider = provider.lower().strip()
self.api_key = api_key
self.model = model
if self.provider == "zai":
self.url = os.getenv("ZAI_BASE_URL", "https://api.z.ai/api/paas/v4/chat/completions")
elif self.provider == "openrouter":
self.url = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions")
Deze initializer configureert de LLM-client door het juiste API-endpoint (Z.AI of OpenRouter) te selecteren, terwijl de interface consistent blijft.
Hij slaat de API-sleutel, het model en de endpoint-URL op, zodat de rest van het systeem het model kan aanroepen zonder zich zorgen te maken over providerspecifieke verschillen.
Vervolgens bouwt create_chat_completion() de payload, voegt tools toe wanneer nodig en parseert eventuele geretourneerde toolaanroepen.
payload: Dict[str, Any] = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": False,
}
if tools:
payload["tools"] = tools
payload["tool_choice"] = "auto"
De bovenstaande payload definieert het verzoek dat naar het model wordt gestuurd, inclusief de conversatiecontext en generatieparameters. Door de tools bij te voegen en tool_choice="auto" in te schakelen, kan het model beslissen wanneer externe functies moeten worden aangeroepen.
Stap 3: Browsertools definiëren
In agent.py definiëren we een toolschema dat het model kan aanroepen. Deze omvatten search_web, open_url, open_search_result, extract_page, extract_flight_cards, click, type_text en wait_for_text.
Hier is één voorbeeld van een tooldefinitie:
{
"type": "function",
"function": {
"name": "search_web",
"description": "Search the public web for flight result pages or travel aggregators.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 8},
},
"required": ["query"],
},
},
}
Dit schema vertelt het model wat de tool doet, welke inputs verwacht worden en hoe deze aan te roepen. Door tools op deze manier te definiëren, kan het model dynamisch beslissen wanneer het web te doorzoeken, in plaats van de flow hard te coderen.
Aan de uitvoeringskant worden die tools geïmplementeerd in browser_tools.py. Voor deze demo gebruikten we de HTML-endpoint van DuckDuckGo om de huidige resultaatslinks op te slaan voor vervolgnavigatie.
def search_web(self, query: str, max_results: int = 8) -> Dict[str, Any]:
url = f"https://html.duckduckgo.com/html/?q={quote_plus(query)}"
headers = {"User-Agent": "Mozilla/5.0"}
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
html = resp.text
De bovenstaande functie search_web() voert een lichte webzoekopdracht uit door ruwe HTML-resultaten op te halen en te parsen, zonder een betaalde zoek-API nodig te hebben. Ook slaat hij de resultaten intern op, zodat de agent ze in volgende stappen kan navigeren.
Nu we hebben gedefinieerd hoe de agent met het web kan interageren, is de volgende stap om te zien hoe hij deze tools daadwerkelijk gebruikt om vluchtinformatie via webzoekopdrachten te extraheren.
Stap 4: Vluchtinformatie extraheren
Zodra het model een veelbelovende bron vindt, moet het die inspecteren. We kunnen de browsertools (uit stap 3) gebruiken om ruwe paginasnapshots te extraheren.
Stap 4.1: Pagina's openen en extraheren
We gebruiken de tool open_url() om een webpagina te laden en een snapshot met titel, URL en zichtbare tekst te retourneren.
def open_url(self, url: str) -> Dict[str, Any]:
self.page.goto(url, wait_until="domcontentloaded", timeout=45000)
self.page.wait_for_timeout(1200)
return self._page_snapshot()
Voor bredere extractie gebruiken we de volgende tool extract_page() die het paginasnapshot en een verzameling links van de huidige pagina teruggeeft.
def extract_page(self, max_chars: int = 7000, link_limit: int = 20) -> Dict[str, Any]:
snapshot = self._page_snapshot(max_chars=max_chars)
snapshot["links"] = self._collect_links(limit=link_limit)
self.current_links = snapshot["links"]
return snapshot
Dit is nuttig omdat het model niet altijd blind hoeft te klikken. Soms kan het eerst de huidige paginatekst en beschikbare links inspecteren en daarna de volgende stap bepalen.
Stap 4.2: Heuristische vluchtextractie
Een sleutelonderdeel van de demo is de functie extract_flight_cards() die de zichtbare paginatekst gebruikt en met regex-patronen prijzen, tijden, duur en tussenstops detecteert.
Hier is de kern van die logica:
money_pattern = re.compile(r"(?:\$|USD\s?)(\d{2,5})")
time_pattern = re.compile(r"\b\d{1,2}:\d{2}\s?(?:AM|PM|am|pm)?\b")
duration_pattern = re.compile(r"\b\d{1,2}\s?h\s?\d{0,2}\s?m?\b|\b\d{1,2}\s?hr\s?\d{0,2}\s?min?\b", re.I)
stop_pattern = re.compile(r"nonstop|1 stop|2 stops|3 stops", re.I)
Deze regex-patronen werken als detectoren voor veelvoorkomende vluchtsignalen. Het money_pattern vangt prijzen in formaten zoals $299 of USD 299, terwijl het time_pattern vertrek- en aankomsttijden identificeert.
Evenzo extraheert het duration_pattern de reisduur, en detecteert het stop_pattern of een vlucht non-stop is of tussenstops heeft. Je kunt vergelijkbare patronen bouwen voor elke usecase.
Zodra deze signalen zijn gedefinieerd, scant de functie kleine tekstfragmenten en zoekt naar betekenisvolle combinaties. Wanneer een match wordt gevonden, construeert hij een gestructureerde vluchtkaart:
card = {
"snippet": joined[:350],
"price_usd": int(price_match.group(1)),
"times": times[:2],
"duration": duration.group(0) if duration else None,
"stops": stops.group(0) if stops else None,
}
Het bovenstaande codeblok zet ruwe tekst om in een gestructureerde representatie van een vluchtoptie. Het extraheert de prijs, beperkt tijdsvermeldingen tot de meest relevante en gaat zorgvuldig om met ontbrekende velden. Dit behoudt de oorspronkelijke context, wat handig is voor debugging.
Met de extractie op zijn plek heeft de agent nu een manier om ongestructureerde paginacontent om te zetten in bruikbare data. Vervolgens bekijken we hoe deze data door de agentloop stroomt om meerstapsbeslissingen aan te sturen.
Stap 5: Agentloop
Het daadwerkelijke agentgedrag zit in de klasse BrowserAgent (in agent.py). Deze klasse stuurt de prompt en het toolschema naar het model, controleert of het model een tool wil aanroepen, voert die tool uit, voegt het toolresultaat terug toe aan het gesprek en gaat door tot er een definitief antwoord is geproduceerd.
Hier is de hoofdloop:
for step in range(1, self.max_steps + 1):
assistant = self.llm.create_chat_completion(
messages=messages,
tools=TOOLS,
temperature=0.2,
max_tokens=2200,
enable_thinking=False,
)
if assistant.tool_calls:
...
result = self.tool_runner(tc.name, tc.arguments)
...
continue
final_content = assistant.content.strip()
if final_content:
yield {"type": "final", "step": step, "content": final_content}
return
Deze loop is het hart van de demo.
Bij elke stap ontvangt het model de huidige conversatiestatus en beslist wat het vervolgens doet. Als het toolaanroepen retourneert, worden die uitgevoerd met tool_runner en de resultaten worden terug de loop in gevoerd voor de volgende iteratie.
Dit proces gaat door totdat het model een definitieve reactie produceert in plaats van een tool aan te roepen.
De loop dwingt ook een maximumaantal stappen af om oneindige uitvoering te voorkomen en houdt het proces beheersbaar en efficiënt.
Stap 6: Tools koppelen aan de app
In app.py mappen we de door het model gevraagde toolnamen naar de daadwerkelijke browsermethoden. Die mapping is geïmplementeerd met de functie make_tool_runner():
def make_tool_runner(browser: BrowserTools):
def run(name: str, args: Dict[str, Any]):
if name == "search_web":
return browser.search_web(**args)
if name == "open_url":
return browser.open_url(**args)
if name == "open_search_result":
return browser.open_search_result(**args)
if name == "extract_page":
return browser.extract_page(**args)
if name == "extract_flight_cards":
return browser.extract_flight_cards(**args)
Deze functie fungeert als dispatcher tussen het model en de browser. Het model retourneert alleen de toolnaam en argumenten, en run() vertaalt dat naar een functieaanroep op de BrowserTools-instantie.
Nu de agent met succes tools kan aanroepen en uitvoeren, is de volgende stap het bouwen van de gebruikersinterface die alles bij elkaar brengt.
Stap 7: Streamlit-UI bouwen
De Streamlit-UI verandert de browseragent in een interactieve applicatie. Hij verzamelt de reisdgegevens van de gebruiker, start de agent, streamt tussenstappen en toont tot slot een samenvatting samen met een gestructureerde tabel met topopties voor vluchten. Deze volledige UI-flow is geïmplementeerd in app.py.
st.set_page_config(page_title="GLM-5 Browser Agent", page_icon="", layout="wide")
st.title("GLM-5-Turbo Real-Time Browser Agent")
st.subheader("Trip Request")
origin = st.text_input("Origin", value="SFO")
destination = st.text_input("Destination", value="JFK")
depart_date = st.date_input("Depart date")
return_date = st.date_input("Return date")
adults = st.number_input("Adults", min_value=1, max_value=9, value=1)
natural_request = st.text_area(
"Optional natural language request",
value="Find the cheapest reasonable flight options and summarize the best ones.",
height=120,
)
user_query = (
f"Find flight options from {origin} to {destination} for {adults} adult(s). "
f"Departure date: {depart_date}. Return date: {return_date}. "
f"User preference: {natural_request}"
)
run = st.button("Run browser agent", type="primary", use_container_width=True)
if run:
if not api_key:
st.error("Please enter an API key in the sidebar.")
st.stop()
llm = LLMClient(provider=provider, api_key=api_key, model=model)
event_box = st.container(border=True)
result_box = st.container(border=True)
all_events = []
with managed_browser(headless=headless) as browser:
agent = BrowserAgent(llm=llm, tool_runner=make_tool_runner(browser), max_steps=max_steps)
for event in agent.stream(user_query):
all_events.append(event)
with event_box:
if event["type"] == "status":
st.write(f"**Step {event['step']}** — {event['content']}")
elif event["type"] == "tool_call":
with st.expander(f"Step {event['step']} • Tool call: {event['tool']}", expanded=False):
st.json(event["arguments"])
elif event["type"] == "tool_result":
with st.expander(f"Step {event['step']} • Tool result: {event['tool']}", expanded=False):
st.json(event["result"])
elif event["type"] == "final":
st.success("Agent finished.")
final_event = next((e for e in reversed(all_events) if e["type"] == "final"), None)
final_text = final_event["content"] if final_event else ""
with result_box:
st.subheader("Final Answer")
parsed = parse_final_json(final_text)
if parsed:
st.markdown(render_final_markdown(parsed))
options_df = build_options_dataframe(parsed)
if not options_df.empty:
st.subheader("Top options table")
st.dataframe(
options_df,
use_container_width=True,
column_config={
"Link": st.column_config.LinkColumn("Link", display_text="Open source"),
},
)
else:
st.markdown(final_text)
Zo brengt de Streamlit-UI alle componenten samen:
- De aanroepen
st.set_page_config()enst.title()bepalen de algemene lay-out en kop van de app, terwijl invoerwidgets zoalsst.text_input(),st.date_input(),st.number_input()enst.text_area()de reisgegevens verzamelen. - Wanneer de gebruiker op de knop
Run browser agentklikt, controleert de app eerst of er een API-sleutel beschikbaar is en initialiseert dan deLLMClient. Vervolgens maakt hij aparte containers voor tussentijdse agentlogs en eindresultaten, en opent een beheerde Playwright-browsersessie. Binnen die sessie begint deBrowserAgentevents te streamen en de UI toont elke statusupdate, toolaanroep en toolresultaat. - Zodra de agent klaar is, haalt de app het laatste event op, parseert de geretourneerde JSON en rendert die. De samenvatting wordt getoond via
render_final_markdown(), terwijlbuild_options_dataframe()de gestructureerde vluchtopties omzet in een tabel met aanklikbare bronlinks.
Met deze stap voltooid kun je alles opslaan als app.py en de app starten met:
streamlit run app.py

Dit project werkt goed als een echte demo omdat het gebruikmaakt van daadwerkelijke browserzoekresultaten en toolaanroepen. Tegelijkertijd is het nog steeds een prototype. Vluchtwebsites zijn dynamisch, pagina's kunnen ruis bevatten en sommige platforms gebruiken anti-botbescherming.
De huidige extractielogica is bewust heuristisch, wat het systeem eenvoudig en begrijpelijk houdt.
Belangrijker nog: deze setup is niet beperkt tot vluchten. Met enkele aanpassingen, vooral aan de prompts en extractielogica, kun je dezelfde architectuur hergebruiken om agents te bouwen voor shoppen, onderzoek, nieuwsaggregatie of elke taak die browsen en het samenvatten van live informatie vereist.
Conclusie
In deze tutorial hebben we een realtime browseragent gebouwd met GLM-5-Turbo, Playwright en Streamlit. Het model fungeert als planner, de browsertools als de uitvoeringslaag en de UI zet gestructureerde resultaten om in een gebruiksvriendelijk formaat.
De belangrijkste les is dat de waarde niet alleen uit het model komt, maar uit de combinatie van het model met een gecontroleerde toollaag en een agentloop.
Er zijn echter verschillende uitbreidingen mogelijk. Je kunt de extractie verbeteren met sitespecifieke parsers, het systeem uitbreiden naar hotels of reisplanningen, of gebruikersvoorkeuren integreren zoals non-stop vluchten of voorkeursluchtvaartmaatschappijen. Daarmee kun je deze demo uitbouwen tot een volwaardige browserextensie.
GLM-5-Turbo FAQ's
Heb ik een GPU nodig om dit GLM-5-Turbo-project te draaien?
Nee. Voor dit project heb je geen GPU nodig omdat het model via een API (Z.AI of OpenRouter) wordt benaderd. Alle zware berekeningen gebeuren op de infrastructuur van de provider, terwijl je lokale machine alleen de UI en browserautomatisering afhandelt.
Waarom een browseragent gebruiken in plaats van een vlucht-API?
API's zijn betrouwbaarder en gestructureerder, maar ze zijn beperkt tot vooraf gedefinieerde data en vereisen vaak betaalde toegang. Een browseragent kan daarentegen:
- meerdere bronnen dynamisch verkennen
- zich aanpassen aan verschillende vragen
- werken zonder vooraf geïntegreerde API's
Voor productiesystemen is een hybride aanpak (agent en API's) meestal de beste keuze.
Waarom faalt de agent soms op bepaalde websites?
Veel reiswebsites gebruiken:
- dynamische JavaScript-rendering
- anti-botbescherming
- vaak veranderende lay-outs
Aangezien deze demo vertrouwt op generieke browserautomatisering en heuristische extractie, werkt hij mogelijk niet consistent op alle sites.
Waarom hebben we vermeden om JSON als eindoutput te retourneren?
JSON is intern handig voor debugging, gestructureerde verwerking, rangschikking en filtering. Eindgebruikers geven echter de voorkeur aan leesbare samenvattingen. Daarom zet het systeem gestructureerde outputs om naar een gebruiksvriendelijk formaat.
Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.

