מאייג׳נט נאיבי ל-LangChain: אותו רעיון, הרבה פחות כאב ראש

הרצה של אייג׳נט - כלומר איטרציה והפעלת כלים, אבל הפעם עם פריימוורק

בפוסט הקודם, למדנו על איך אייג׳נט עובד והשתמשתי בדוגמה מאד נאיבית כדי להבין את הקונספט הבסיסי של אייג׳נט, איטרציה ושימוש בכלים. סיימתי את הפוסט בהסבר על כך שמדובר בדוגמה נאיבית ושהיום אנחנו משתמשים בתשתיות אחרות לגמרי. אחת מהתשתיות הפופולריות ביותר היא LangChain ובפוסט הזה אני מסביר איך להשתמש בה.

בימים אלו הקוד הרבה פחות חשוב אלא מה שחשובה היא ההבנה. כיוון שאני מתכנת, אני מבין דברים דרך הקוד. אני משתמש פה בפייתון שזו שפה פשוטה ביותר שכל אחד יכול להבין, גם דוברי שפות אחרות. אבל מה שתמיד חשובה היא ההבנה. במקרה הזה: למרות שאייג׳נט אפשר לממש באופן נאיבי, כדאי לא לעשות את זה כיוון שהתשתיות מכסות דברים שאנחנו לא חושבים עליהם ודרך העבודה איתן תפתור לנו מראש בעיות כמו בעיות observibility, בעיות ביצועים, אחסון מידע ועוד דברים שבהתחלה לא מודעים אליהן.

הנה סרטון המלווה את הפוסט

הפעם, ניקח משהו קצת יותר מלהיב מבחינת אייג׳נט. אייג׳נט שמנהל בית חכם. הרבה אנשים חושבים שבית חכם זה בית שבו אני צועק לחלל האוויר ״הפעל מזגן״ והבית החכם מפעיל את המזגן. אבל לא, בית חכם אמיתי זה בית שיודע מראש שחם לי ואז פותח את המזגן או לחלופין, מבין שנעים מספיק בחוץ ופותח את התריסים וסוגר את המזגן או לחלופין מבין שזה לילה וסוגר את התריסים. בשביל זה צריך או אוטומציה מורכבת או אייג׳נט.

האייג׳נט עובד גם באיטרציה, מה האיטרציה? האיטרציה, שמופעלת פעם בשבוע

  1. קורא חיישנים: טמפרטורה, גלאי נוכחות, תאריך ושעה
  2. בודק אם היום חופשה / שבת לפי לוח עברי ישראלי
  3. על פי הנתונים האלו. מחליט: לפתוח או לסגור וילונות, להפעיל או לכבות מזגן
  4. מדפיס לקונסול כל צעד — כדי שנוכל לראות את האיטרציה בעיניים, בחיים האמיתיים זה עוזר לנו לגרום לאייג׳נט לעבוד טוב יותר (בלשון העם: לאפטם).

לשם הדוגמ החיישנים והמפעילים הם מדומים (בית בזיכרון). כלי החופשה לא: הוא משתמש ב־pyluach מול לוח החגים בישראל. אם מוצא חן בעיניכם כל הסיפור – אז ESP32 או רספברי פיי יעזרו לכם לבנות את החיישנים באפס זמן ומאמץ. אבל כרגע זה פונקציות מוק. אם יהיה ביקוש אני אבנה את זה.

למה לא לעשות את זה רק בפייתון? מה רע בנאיבי?

בפוסט הקודם עשינו ידנית כמעט הכל:

  • סכמת JSON ארוכה לכל כלי (tools_definition)
  • מילון tools_map שמחבר שם־כלי לפונקציה
  • לולאת for שמנהלת היסטוריית הודעות
  • פרסור של tool_calls, הרצה מקומית, והחזרת תוצאה בפורמט שה־API דורש
  • עצירה כשאין יותר קריאות לכלים

הכל יעבוד באופן נאיבי, אבל כשיוצאים למערכת אמיתית וקצת מורכבת, אז לתחזק ולהבין את זה זה בעייתי. חלקכם יגידו, ״טוב, מה אכפת לי?!? קלוד יודע הכל!״ אבל זה לא עובד ככה, כי קוד נקי מונע ממכם להעמיס על הקונטקסט ועל המודל שאתם משתמשים בו. קוד קריא וברור לבני אדם === קוד שמודל השפה הגדול יבין אותו יותר טוב.

יש כמה פריימוורקים שמסייעים לנו לבנות סוכנים גם בלי הקוד הנאיבי שהראינו בפוסט הקודם. הכי ידוע מביניהם הוא LangChain. לא מדובר בקסם אפל או בוודו.אותו דבר כמו במימוש הנאיבי אבלLangChain פשוט לוקח את החלקים החוזרים ומארגן אותם.

אז איך זה בנוי?

כאן יש הסבר עם הקוד, למי שרוצה לדלג, עברו לכותרת דברים בולטים שכדאי לשים אליהם לב. מי שרוצה לבנות קוד זהה, יכול להשתמש בפרומפט:


Build a Python blog-demo project named `demo-agent`: an hourly IoT home-comfort agent using Grok (xAI) + LangChain, with console-only observability (no LangSmith).

## Goals
- Each tick the agent reads sensors, decides policy, then actuates curtains + AC.
- Show a clear LangChain tool-calling iteration loop in the console.
- Use `uv` only (no pip). Python >= 3.13.
- Keep the project minimal and blog-friendly.

## Stack / deps (via uv)
- langchain, langchain-core, langchain-xai
- pyluach (Hebrew/Israeli calendar)
- python-dotenv
- Optional dev: ruff
- Entry points:
  - `uv run python -m demo_agent once|loop ...`
  - console script `demo-agent`

## Package layout (strict)
Only `agent.py` is a real app module at package root. CLI/tracing live under services; IoT demo code under tools.

```text
demo-agent/
  pyproject.toml
  README.md
  .env.example          # XAI_API_KEY=
  .gitignore
  src/demo_agent/
    agent.py            # ONLY main app module at root
    __init__.py         # expose main()
    __main__.py         # thin: call services.cli.main
    tools/
      __init__.py       # export TOOLS, HOUSE, SCENARIOS, apply_scenario
      house.py          # in-memory mock house state
      holidays.py       # real vacation check via pyluach
      iot.py            # LangChain @tool wrappers + TOOLS list
      scenarios.py      # reproducible presets
    services/
      __init__.py       # export ConsoleTrace
      console_trace.py  # BaseCallbackHandler printing LLM/tool steps
      cli.py            # argparse once|loop
```

## Domain behavior
Mock house state (`HouseState`) in Asia/Jerusalem:
- sensors: temperature_c, someone_home, now() (optional now_override)
- actuators: curtains ("open"|"closed"), ac_on, ac_target_c
- keep actuator history log

LangChain tools in `tools/iot.py`:
1. get_temperature
2. get_occupancy
3. get_datetime
4. is_vacation  → REAL tool using pyluach Israel schedule
5. set_curtains(state)
6. set_air_conditioner(on, target_c=None)

`is_vacation` rules:
- True on Shabbat
- True on Israel festivals where melacha is prohibited (`festival(..., israel=True, include_working_days=False)`)
- Chol HaMoed / work-permitted days are NOT vacation
- Return JSON with is_vacation, reasons, gregorian_date, hebrew_date, calendar_holiday

Agent policy (system prompt in agent.py):
- Hot >= 26°C; comfortable < 26°C
- Daytime 07:00–19:59; otherwise night
- If vacation OR nobody home: close curtains
- If nobody home: AC off (even if hot)
- If someone home AND hot: close curtains + AC on (~23–24°C)
- If someone home AND comfortable AND daytime AND not vacation: open curtains; AC off
- If someone home AND comfortable AND (night OR vacation): close curtains; AC off
- Must read ALL sensors first, then actuators, then one-sentence summary
- Never invent sensor values

LLM:
- ChatXAI model `grok-4-1-fast-non-reasoning`, temperature=0
- create_agent(model, tools=TOOLS, system_prompt=...)
- Disable LangSmith/tracing env vars (LANGSMITH_TRACING=false, LANGCHAIN_TRACING_V2=false)
- Load `.env` for XAI_API_KEY; exit clearly if missing

## Console observability
Implement `ConsoleTrace(BaseCallbackHandler)` that prints:
- `→ LLM think #N`
- `← tool_calls: ...` / final LLM text
- `… tool(args)` and `✓ tool → result`
No LangSmith.

`run_tick()` should print state before/after and actuator log.

## CLI (`services/cli.py`)
Commands:
- `once` — single tick
- `loop` — repeat every `--interval-seconds` (default 3600)

Flags:
- `--scenario` with presets:
  - live
  - hot_occupied (29°C, home, weekday afternoon)
  - cool_daytime
  - empty_home
  - pesach (Pesach 2026-04-02 Israel)
  - shabbat_hot (2026-08-01 afternoon, hot)

## Docs
Write a short README with setup (`uv sync`, `.env`) and run examples.

## Done when
1. `uv sync` works
2. `uv run python -m demo_agent once --scenario hot_occupied` runs end-to-end with XAI_API_KEY
3. Console shows multi-step LLM/tool iteration
4. Layout matches the structure above

src/demo_agent/agent.py – האייג׳נט עצמו

זה הקובץ המרכזי והכי מעניין. מה שרלוונטי פה הוא build_agent שהוא לב יצירת הסוכן ואתייחס לזה בהמשך. הפונקציה run_tick היא ממש פונקצית דמה שטוענת ״סטייט״ של בית בשביל שלכלים יהיה מידע מזויף. למשל טמפרטורה וחיישן זיהוי אנשים וכמובן זמן. כאמור לצרכי דמו. אין מה להתעמק ב-run_tick

"""Grok-powered LangChain IoT comfort agent."""

from __future__ import annotations

import os
from pathlib import Path

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_xai import ChatXAI

from demo_agent.console_trace import ConsoleTrace
from demo_agent.house import HOUSE
from demo_agent.tools import TOOLS


def _load_env() -> None:
    for candidate in (
        Path.cwd() / ".env",
        Path(__file__).resolve().parents[2] / ".env",
    ):
        if candidate.is_file():
            load_dotenv(candidate)
            return
    load_dotenv()


SYSTEM_PROMPT = """\
You are a home IoT comfort agent for an apartment in Israel (Asia/Jerusalem).

On each hourly tick you MUST:
1. Read ALL sensors: get_temperature, get_occupancy, get_datetime, is_vacation
2. Decide curtain and AC actions from the policy below
3. Call the actuator tools as needed
4. Finish with one short sentence summarizing what you did and why

Policy:
- Hot means indoor temperature >= 26°C. Comfortable means < 26°C.
- Daytime is 07:00–19:59 local time; night is otherwise.
- If is_vacation is true OR nobody is home: close curtains.
- If nobody is home: turn AC off (energy saving), even if hot.
- If someone is home AND hot: close curtains and turn AC on (target ~23–24°C).
- If someone is home AND comfortable AND daytime AND not vacation: open curtains; AC off.
- If someone is home AND comfortable AND (night OR vacation): close curtains; AC off.
- Prefer using tools over guessing. Never invent sensor values.
"""

HOURLY_PROMPT = (
    "Hourly comfort check. Read all sensors, apply the house policy, "
    "set curtains and air conditioner, then summarize."
)


def build_agent():
    _load_env()
    if not os.getenv("XAI_API_KEY"):
        raise SystemExit(
            "Missing XAI_API_KEY. Copy .env.example to .env and set your key."
        )

    # Keep tracing local — console callback only
    os.environ.setdefault("LANGSMITH_TRACING", "false")
    os.environ.setdefault("LANGCHAIN_TRACING_V2", "false")

    llm = ChatXAI(model="grok-4-1-fast-non-reasoning", temperature=0)
    return create_agent(
        model=llm,
        tools=TOOLS,
        system_prompt=SYSTEM_PROMPT,
    )


def run_tick(agent=None) -> dict:
    agent = agent or build_agent()
    before = HOUSE.snapshot()
    print("=" * 60)
    print(f"IoT tick @ {before['local_time']}")
    print(f"State before: {before}")
    print("=" * 60)

    result = agent.invoke(
        {"messages": [{"role": "user", "content": HOURLY_PROMPT}]},
        config={"callbacks": [ConsoleTrace()]},
    )

    after = HOUSE.snapshot()
    print("-" * 60)
    print(f"State after:  {after}")
    if HOUSE.history:
        print("Actuator log:", "; ".join(HOUSE.history))
    print("=" * 60)
    return result


בתיקית tools יש כמה כלים

  • iot.py — הכלים עצמם (@tool): חיישנים + וילונות/מזגן, ורשימת TOOLS. מה שמעניין הוא הדקורטור @tool ואתייחס אליו בהמשך.
"""LangChain tools wrapping mock IoT sensors/actuators + Hebrew vacation check."""

from __future__ import annotations

import json
from typing import Literal

from langchain_core.tools import tool

from demo_agent.tools.holidays import vacation_status
from demo_agent.tools.house import HOUSE


@tool
def get_temperature() -> str:
    """Read the indoor temperature sensor in Celsius."""
    return json.dumps({"temperature_c": HOUSE.temperature_c})


@tool
def get_occupancy() -> str:
    """Read the people detector: whether someone is currently home."""
    return json.dumps({"someone_home": HOUSE.someone_home})


@tool
def get_datetime() -> str:
    """Read the local date and time in Asia/Jerusalem."""
    now = HOUSE.now()
    return json.dumps(
        {
            "iso": now.isoformat(timespec="seconds"),
            "weekday": now.strftime("%A"),
            "hour": now.hour,
            "date": now.date().isoformat(),
        }
    )


@tool
def is_vacation() -> str:
    """Check if today is a vacation day using the Hebrew/Israeli holiday calendar.

    True on Shabbat and Israel-schedule festivals where work (melacha) is prohibited.
    Uses pyluach; does not count Chol HaMoed as vacation.
    """
    return json.dumps(vacation_status(HOUSE.now()), ensure_ascii=False)


@tool
def set_curtains(state: Literal["open", "closed"]) -> str:
    """Open or close the curtains. state must be 'open' or 'closed'."""
    if state not in ("open", "closed"):
        return json.dumps({"ok": False, "error": "state must be 'open' or 'closed'"})
    HOUSE.curtains = state
    HOUSE.log(f"curtains → {state}")
    return json.dumps({"ok": True, "curtains": HOUSE.curtains})


@tool
def set_air_conditioner(on: bool, target_c: float | None = None) -> str:
    """Turn the air conditioner on/off. When on, optionally set target Celsius."""
    HOUSE.ac_on = on
    if on:
        HOUSE.ac_target_c = 24.0 if target_c is None else float(target_c)
    else:
        HOUSE.ac_target_c = None
    HOUSE.log(
        f"ac → {'on' if on else 'off'}"
        + (f" @ {HOUSE.ac_target_c}°C" if HOUSE.ac_on else "")
    )
    return json.dumps(
        {
            "ok": True,
            "ac_on": HOUSE.ac_on,
            "ac_target_c": HOUSE.ac_target_c,
        }
    )


TOOLS = [
    get_temperature,
    get_occupancy,
    get_datetime,
    is_vacation,
    set_curtains,
    set_air_conditioner,
]
  • house.py — מצב הבית המדומה בזיכרון (טמפ׳, נוכחות, וילונות, מזגן). פחות מעניין.
  • holidays.py — בודק אם היום חופשה/שבת לפי לוח עברי ישראלי (pyluach). זו פונקצית פייתון פשוטה ודטרמניסטית שמשתמשת בחבילה כדי לבדוק אם זה חג היום.

services/

  • console_trace.py — callback שמדפיס לקונסול כל סיבוב LLM וכל קריאה לכלי

דברים בולטים שכדאי לשים אליהם לב

בגדול מה שמעניין אותנו הם רק כמה דברים, יצירת הכלים והאייג׳נט. שהם הרבה יותר פשוטים.

1. כלי = פונקציה עם docstring

במקום לכתוב סכמת JSON ידנית לכל כלי, יש לנו דקורטור נהדר בשם @tool שלוקח את החתימה והתיאור ובונה את מה שהמודל צריך. הוספת כלי חדש היא בעיקר: לכתוב פונקציה.

@tool
def get_temperature() -> str:
    """Read the indoor temperature sensor in Celsius."""
    return json.dumps({"temperature_c": HOUSE.temperature_c})

2. הלולאה כבר בנויה מראש

במקום for iteration in range(1, 6) עם messages.append ופרסור ידני, אנחנו יוצרים את האייג׳נט ואז עושים לו invoke, מאד מאד קל לראות את זה. יש ממשק פשוט של קינפוג וגם לוגינג.

agent = create_agent(
    model=llm,
    tools=TOOLS,
    system_prompt=SYSTEM_PROMPT,
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": HOURLY_PROMPT}]},
    config={"callbacks": [ConsoleTrace()]},
)

זה אותו מחזור: מודל כלים מודל תשובה. רק שהכל מנוהל ופשוט יותר.

הקינפוג, שפה אנו רואים אותו עם ה-callbacks מאפשר לי למשל להכניס לוגים בקלות. להרוויח את אותה שקיפות שהייתה לנו בקוד הנאיבי בלי לזהם את הקוד ב־print בכל מקום.

3. החיבור ל־API של התשתיות הוא שורה

from langchain_xai import ChatXAI

llm = ChatXAI(model="grok-4-1-fast-non-reasoning", temperature=0)

בלי לנהל בעצמכם base_url, מבנה הודעות tool, ופורמט תשובה — לפחות לא בשכבת האפליקציה.

למה זה בכלל "מסודר" יותר

במימוש הנאיבי הכל ישב בקובץ אחד ארוך: DB, כלים, סכמות, לולאה, לוגים. זה בסדר לדמו לימודי. אבל בעולם האמיתי זה לא. הנקודה החשובה: LangChain לא משנה את איך שהאייג׳נט מתנהל, אבל הוא מציג את זה יותר בקלות. המודל רק מפרק את זה לצעדים וקורא לכלים. בדיוק כמו בפוסט הקודם — רק בלי שאתם תבנו את מנוע האיטרציה.

ההרצה החגיגית

כדי להריץ, אחרי שדאגתי שיהיה מפתח של גרוק בקובץ הסביבה, אני יכול לבחור סנריו ולהפעיל. למשל:

uv run python -m demo_agent once --scenario hot_occupied

התוצאה תהיה:

============================================================
IoT tick @ 2026-07-14T14:00:00+03:00
State before: {'temperature_c': 29.0, 'someone_home': True, 'curtains': 'open', 'ac_on': False, 'ac_target_c': None, 'local_time': '2026-07-14T14:00:00+03:00'}
============================================================
→ LLM think #1
← tool_calls: get_temperature({}), get_occupancy({}), get_datetime({}), is_vacation({})
  … get_temperature({})
  ✓ get_temperature → {"temperature_c": 29.0}
  … get_datetime({})
  ✓ get_datetime → {"iso": "2026-07-14T14:00:00+03:00", "weekday": "Tuesday", "hour": 14, "date": "2026-07-14"}
  … get_occupancy({})
  ✓ get_occupancy → {"someone_home": true}
  … is_vacation({})
  ✓ is_vacation → {"is_vacation": false, "reasons": [], "gregorian_date": "2026-07-14", "hebrew_date": "כ״ט תמוז ה׳תשפ״ו", "calendar_holiday": null, "israel_schedule": true}
→ LLM think #2
← tool_calls: set_curtains({'state': 'closed'}), set_air_conditioner({'on': True, 'target_c': 24.0})
  … set_curtains({'state': 'closed'})
  ✓ set_curtains → {"ok": true, "curtains": "closed"}
  … set_air_conditioner({'on': True, 'target_c': 24.0})
  ✓ set_air_conditioner → {"ok": true, "ac_on": true, "ac_target_c": 24.0}
→ LLM think #3
← LLM: Closed curtains and turned AC on to 24°C because it’s 29°C, someone is home, and it’s daytime.
------------------------------------------------------------
State after:  {'temperature_c': 29.0, 'someone_home': True, 'curtains': 'closed', 'ac_on': True, 'ac_target_c': 24.0, 'local_time': '2026-07-14T14:00:00+03:00'}
Actuator log: curtains → closed; ac → on @ 24.0°C
============================================================

ואפשר לראות בדיוק את האיטרציה, איזה יופי זה!

ראשית, החשיבה., אחרי כן הקריאה לכלים וקבלת התוצאות ואז הסיבוב השני, חשיבה נוספת וקריאה נוספת לכלים, אחרי זה החשיבה הסופית וההגעה למסקנות. תם הטקס.

מה שהכי חשוב: להבין שזו איטרציה.

פוסטים נוספים שכדאי לקרוא

פתרונות ומאמרים על פיתוח אינטרנט

גישת Least Privilege

גישה לכתיבת קוד מאובטח שכדאי מאד להכיר – במיוחד בעידן הבינה המלאכותית

יסודות בתכנות

Decoupling ו-Coupling בהנדסת תוכנה

הסבר על מושג מרכזי בהנדסת תוכנה ובכתיבת קוד שכדאי להכיר במיוחד כשמנחים LLM בכתיבת קוד.

יסודות בתכנות

איך TCP עובד? מבט מעמיק

הסבר מעמיק מתחת למנוע על איך תקשורת TCP עובדת כולל ניתוח פקטות.

פתרונות ומאמרים על פיתוח אינטרנט

העולם המדהים של Chrome debugging

איך תוכנות שונות מפעילות את כרום כרצונן? איך דיבאגר בפרונט עובד? צלילה לעומק לתוך העולם המופלא של CDP

יסודות בתכנות

מבוא לאבטחת מידע: גוגל דורקינג

מאמר מבוא המספר בקצרה ובלשון קלה על גוגל דורקינג – טכניקה לביצוע האקינג גם ללא ידע טכני כלל.

גלילה לראש העמוד