# Функции и где их задавать

> [HTML-версия](https://learnvibecoding.ru/publiclessons/funktsii-i-gde-ikh-zadavat) · [Индекс для LLM](https://learnvibecoding.ru/llms.txt) · [Политика использования материалов](https://learnvibecoding.ru/politika-materialov)
> Материалы защищены. Обучение LLM без согласия запрещено. При разрешённом использовании — обязательна прямая ссылка на страницу-источник.
> Соберём ключевые функции и типы запросов, которые можно использовать при создании агента.

**Курс:** Создание своего агента

### **Что за «GET»: два смысла и как их использовать**

## A) GET как HTTP-метод (для Tool’ов)

Когда агент вызывает внешние источники (погода, поиск, базы), чаще всего это **HTTP GET**:

- **идемпотентно** (безопасно переисполнять),
- запрос с параметрами в URL,
- удобно кэшировать и ставить строгие таймауты.

Мини-паттерн для инструмента на GET:

```python
import requests

def get_weather(location: str, timeout=8) -> dict:
    """
    Get current weather by city name.
    Returns: {"temp_c": float, "desc": str, ...}
    """
    url = "https://api.example.com/weather"
    params = {"q": location, "units": "metric"}
    r = requests.get(url, params=params, timeout=timeout)
    r.raise_for_status()
    data = r.json()
    # нормализуем поля, чтобы Observation был компактным и предсказуемым
    return {"temp_c": data["temp"], "desc": data["description"]}

```

Рекомендации:

- Ставь `timeout`, оборачивай ошибки и **не** прокидывай «сырые» респонсы в Observation (делай нормализацию).
- Кэшируй (ETag/If-None-Match) для частых запросов.
- Никогда не вставляй ключи API/PII в Observation — маскируй.

## B) `get_*` как вспомогательные функции агента

Часто удобно сделать «геттеры» для логики цикла — чтобы код был читабельнее и управляемее:

```python
def get_thought(messages, cfg) -> str:
    """Запрашивает у LLM только Thought + черновик Action (Stop & Parse)."""
    prompt = build_thought_prompt(messages)
    return llm.generate(
        prompt,
        temperature=cfg.temperature_reasoning,     # низкая вариативность
        max_tokens=cfg.max_tokens_thought,
        stop=["Observation:"],
    )

def get_action_json(thought_text, schema) -> dict:
    """Парсит JSON действия из текста и валидирует по схеме."""
    action = extract_json_block(thought_text)
    validate_json(action, schema)
    return action

def get_observation_msg(tool_result: dict) -> dict:
    """Формирует ассистент-сообщение Observation для истории."""
    text = normalize(tool_result)     # укоротить, отредактировать PII
    return {"role":"assistant","content": f"Observation:\n{text}"}

def get_final_answer(messages, cfg) -> str:
    """Просим LLM красиво и кратко сформулировать итоговый ответ."""
    prompt = build_final_prompt(messages)
    return llm.generate(
        prompt,
        temperature=cfg.temperature_final,        # здесь можно другую!
        max_tokens=cfg.max_tokens_final,
        stop=None,
    )

```

Зачем так:

- чётко разделяет этапы, удобно логировать/тестировать,
- позволяет **разные параметры** (включая температуру) для мышления и для финальной речи.

## 2) `think()`: внутреннее рассуждение (Reasoning)

**Цель.** Превратить текущий контекст задачи в *план следующего шага*: какой инструмент звать, с какими параметрами, что проверить после.

**Где задаётся.**

— В системном промпте (инструкции «думай пошагово», «оцени альтернативы»).

— Параметры генерации (`temperature`, `max_tokens`, `stop`).

— Возможен явный обёртчик `think()` для логирования и контроля длины «мышления».

**Входы.**

- `history`: вся история сообщений (system + user + assistant).
- `goals/state`: цель, ограничения, дедлайны, бюджет токенов.
- (опционально) `memory`: пользовательские предпочтения, долгосрочные заметки.

**Выходы.**

- Строка «мысли» (обоснование) **и** структурированное намерение действия (напр., JSON-черновик).

**Шаблон-промпт.**

```
You are an agent. Think step-by-step before acting.
1) Restate the goal.
2) List candidate actions with pros/cons.
3) Choose exactly ONE next action and draft its JSON {action, action_input}.
Return only the Thought and the Action draft. Do NOT invent Observation.

```

**Псевдокод.**

```python
def think(history, goals, cfg):
    prompt = build_thought_prompt(history, goals)
    return llm.generate(
        prompt,
        temperature=cfg.temperature_reasoning,   # обычно 0.2–0.4
        max_tokens=cfg.max_tokens_thought,
        stop=["Observation:"],                  # Stop & Parse
    )

```

**Точки отказа & защита.**

- *Галлюцинация Observation.* Решение: `stop=["Observation:"]`.
- *Длинное «мышление».* Ограничить `max_tokens` и внедрить «TL;DR first».
- *Неправильный выбор инструмента.* Добавить «tool-router» (см. раздел Action).

**Тесты.**

- Unit: сравнить выбранный `action` с эталоном на фиксированных промптах.
- Property: мысль не должна содержать сырые персональные данные.

## 3) `act()`: оформление и исполнение действия

**Цель.** Превратить намерение в корректный **вызов инструмента**: строгое формирование JSON / кода, валидация, исполнение.

**Где задаётся.**

— Формат действий и список инструментов — в **System Prompt** (инструкции + спецификация).

— Валидатор JSON/схемы — в коде агента.

— Исполнители (tool runners) — в коде, вне LLM.

**Формат действия (JSON-схема).**

```json
{
  "type": "object",
  "required": ["action", "action_input"],
  "properties": {
    "action": {"type": "string", "enum": ["get_weather", "search", "calc"]},
    "action_input": {"type": "object"}
  },
  "additionalProperties": false}

```

**Псевдокод.**

```python
def act(thought_text, tools_registry, schema):
    action_json = extract_json_block(thought_text)          # парсинг Action
    validate_json(action_json, schema)                      # строгая валидация
    tool = tools_registry.get(action_json["action"])        # выбор инструмента
    result = tool(**action_json["action_input"])            # безопасное исполнение
    return result

```

**Куда девать температуру?**

В `act()` — нигде. Действие исполняет код, не модель. Температура влияет на **формирование** действия в `think()`.

**Частые ошибки & защита.**

- *Неверный JSON / несовпадающие типы.* Схема + подробные сообщения об ошибке.
- *Вызов несуществующего инструмента.* Строгий `enum` + fallback-подсказка для LLM.
- *Безопасность.* Песочница для кода, таймауты, ограничения сети/файлов (особенно для Code Agent).

**Тесты.**

- Unit на парсинг/валидацию JSON.
- Интеграционные — мок инструментов (без реальных API).

---

## 3) `observe()`: фиксация результата и обновление контекста

**Цель.** Превратить результат действия в **Observation** — единственный источник истины, который добавляется в историю и возвращается в цикл мышления.

**Где задаётся.**

— В коде: формирование Observation-сообщения, пост-обработка (`truncate`, redaction).

— В промпте: правило «Observation — краткий, точный, без лишних данных».

**Входы.** Результат инструмента (успех/ошибка), метаданные (время, источник), лог.

**Выходы.** Текст Observation + обновлённый `messages`.

**Псевдокод.**

```python
def observe(messages, tool_result, redact=True):
    text = normalize(tool_result)               # форматирование, усечение
    if redact:
        text = scrub_pii(text)                  # удалить PII/секреты
    obs_msg = {"role": "assistant", "content": f"Observation:\n{text}"}
    return messages + [obs_msg]

```

**Точки отказа & защита.**

- *Слишком длинный Observation → переполнение контекста.* Суммарные лимиты + конденсация.
- *Утечка данных.* Маскирование PII/секретов перед добавлением.
- *Шумные логи.* Фильтрация, хранить «сырые» логи отдельно от сообщений LLM.

**Тесты.**

- Проверка, что Observation не превышает лимитов, не содержит запрещённых токенов.
- Snapshot-тесты на формат («Observation:\n…»).

---

## 4) `reflect()`: проверка прогресса и принятие решения «заканчиваем или продолжаем»

**Цель.** Дать агента «мета-уровень»: достигнута ли цель? Нужны ли ещё шаги? Есть ли ошибки стратегии?

**Где задаётся.**

— В промпте: критерии готовности («Если выполнены X/Y/Z — завершай»).

— В коде: твёрдые стоп-условия (`max_cycles`, `time_budget`, `cost_budget`).

**Входы.** История (goal, Thought/Action/Observation), текущий результат.

**Выходы.** Решение: `finish` (с формулировкой `Final Answer`) или `continue`.

**Шаблон-промпт.**

```
Review the goal and the last Observation.
If acceptance criteria are met, output:

Thought: I now know the final answer
Final Answer: <concise answer>

Else, explain what single next step is required.

```

**Псевдокод цикла.**

```python
for step in range(cfg.max_cycles):
    thought = think(messages, goals, cfg)
    result = act(thought, tools, ACTION_SCHEMA)
    messages = observe(messages, result)
    if should_finish(messages, goals):          # простое правило в коде
        return final_answer_from(messages)
# fallback
return graceful_exit_summary(messages)

```

**Точки отказа & защита.**

- *Зацикливание.* Всегда иметь `max_cycles` + явные «сигналы стагнации».
- *Преждевременное завершение.* Чёткие acceptance-критерии в System Prompt.

**Тесты.**

- E2E на сценариях «нужно 1 действие», «нужно 3 действия», «ошибка инструмента».
- Проверка корректного формата `Final Answer`.

---

## 5) `tool()`/`@tool`: объявление и описание инструментов

**Цель.** Единообразно описывать инструменты (имя, назначение, аргументы, типы, выход), генерировать строку-описание для System Prompt.

**Пример.**

```python
def calculator(a: int, b: int) -> int:
    """Multiply two integers."""
    return a * b

tool = Tool(
  name="calculator",
  description="Multiply two integers.",
  arguments=[("a", "int"), ("b", "int")],
  outputs="int",
  func=calculator
)

tools_registry = {"calculator": tool}

```

**Автогенерация описания (to_string).** Удобно добавлять в System Prompt:

`Tool Name: calculator, Description: Multiply two integers., Arguments: a: int, b: int, Outputs: int`

**Безопасность.** Таймауты, лимиты, песочницы, whitelists доменов/методов.

---

## 6) Где задавать температуру и другие параметры

- `temperature` влияет **только** на генерацию LLM (в `think()`/`reflect()`), а не на исполнение действия.
    - Аналитика/инструменты: `0.0–0.3`
    - Ассистент/диалог: `0.3–0.5`
    - Креатив: `0.7–1.0`
- `stop`: всегда включайте `["Observation:"]` при ReAct-шаблоне.
- `max_tokens_thought`, `max_cycles`, `time_budget`, `cost_budget` — в конфиге агента.

**Конфиг.**

```python
class AgentConfig:
    model = "gpt-4o"                 # пример
    temperature_reasoning = 0.2
    max_tokens_thought = 256
    max_cycles = 3
    stop_tokens = ["Observation:"]

```

---

# Температура в финальном ответе: зачем и как настраивать

## Зачем разделять температуру

- Для **reasoning/Action** лучше низкая `temperature` (`0.1–0.3`) — меньше фантазий, стабильный JSON.
- Для **Final Answer** можно слегка повысить (`0.3–0.6`), чтобы ответ звучал живее, читабельнее, но без «креативных вольностей».
- Для **креативных выводов** (маркетинг/сторителлинг) — выше (`0.7–0.9`).

## Как это задать на практике

В конфиге держим **две** температуры:

```python
class AgentConfig:
    model = "gpt-4o"
    # рассуждение и действия
    temperature_reasoning = 0.2
    max_tokens_thought = 256
    stop_tokens = ["Observation:"]
    # финальный ответ
    temperature_final = 0.4
    max_tokens_final = 300
    max_cycles = 3

```

В коде цикла — две разные генерации:

```python
# 1) Думаем и выбираем действие (низкая температура)
thought = get_thought(messages, cfg)
action = get_action_json(thought, ACTION_SCHEMA)
tool_result = tools[action["action"]](**action["action_input"])
messages.append(get_observation_msg(tool_result))

# ... цикл повторяется ...

# 2) Когда готовы — формируем финальный ответ (чуть выше температура)
final_text = get_final_answer(messages, cfg)
messages.append({"role":"assistant","content": final_text})
return final_text

```

## Полезные «ручки» помимо температуры

- `top_p` (nucleus sampling): для финального ответа можно `0.9` (более плавная вариативность).
- `presence_penalty` / `frequency_penalty`: уменьшить повторы в длинных ответах.
- `max_tokens_final`: чтобы не «разливалось».

---

# 3) Где указывать «температуру в финальном ответе» в ReAct-шаблоне

Если у тебя ReAct (Thought/Action/Observation), то:

1. **Мысль/Действие** — вызов LLM c `stop=["Observation:"]` и `temperature_reasoning`.
2. **Финал** — **отдельный** вызов LLM с `temperature_final`, без стоп-токена, по промпту вида:

```
SYSTEM: You are a precise assistant. Summarize results clearly,
cite data only from Observations, avoid speculation.

USER: Based on the conversation and Observations above,
provide a concise, helpful final answer (3–5 sentences).

```

Так мы гарантированно получаем:

- строгие действия,
- и приятную для чтения финальную подачу.

<aside>
💡

> **Не обязательно** руками дробить всё на этапы, если ты используешь OpenAI (или похожие) ассистенты. Но **понимать и управлять** этапами («Думай → Действуй → Наблюдай → Финал») всё равно полезно — это даёт надёжность, контроль стоимости и качества. Ниже — когда можно «не заморачиваться», а когда этапы лучше задать явно, и как это сделать на практике в Assistants API.
> 
</aside>

---

## Когда этапы НЕ **нужны**

Используй «прямой» режим (один run → один ответ), если:

- задача простая: Q&A, краткая справка, перефразирование;
- нет вызовов инструментов/внешних API;
- не критичны жёсткие проверки и трассировка.

В Assistants API тогда достаточно:

- системных инструкций (role: system),
- одного user-сообщения,
- одного `run` (модель сама «подумает» и вернёт финал).

---

## Когда этапы **нужны/полезны**

Задай явный цикл, если:

- есть **инструменты/функции** (weather, search, БД, калькулятор, код);
- важны **детерминизм** и предсказуемость (никаких выдуманных «Observation»);
- требуется **контроль стоимости** (лимиты шагов, токенов) и **логируемость**;
- есть **риски** (PII, внешние побочные эффекты) → нужен строгий парсинг/валидация.

В Assistants API это делается без «ручной склейки» промптов: платформа хранит тред, сама вызывает tool-calls, а ты исполняешь их и возвращаешь результаты. Концептуально этапы те же, просто инфраструктура часть работы берёт на себя.

**Мини-шаблон: ассистент с инструментами (Python-псевдокод)**

```jsx

from openai import OpenAI
client = OpenAI()

assistant = client.beta.assistants.create(
    model="gpt-4.1",                             # или reasoning-модель
    name="Agent",
    instructions=("""
Ты работаешь по циклу: Thought -> Action -> Observation -> Final Answer.
- Не выдумывай Observation — проси tool call.
- Ровно один tool-call за шаг.
- Итог давай только когда критерии выполнены.
"""),
    tools=[
        {"type": "function",
         "function": {
            "name": "get_weather",
            "description": "Get current weather for a city.",
            "parameters": { "type": "object",
                            "properties": { "location": {"type": "string"} },
                            "required": ["location"],
                            "additionalProperties": False }}
        }
    ],
    # по умолчанию параллельные вызовы могут быть включены; можно отключить:
    tool_resources={"parallel_tool_calls": False}
)

thread = client.beta.threads.create()
client.beta.threads.messages.create(thread_id=thread.id, role="user",
                                   content="What's the weather in London?")

run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id,
                                      # «низкая» температура для шага рассуждения/действий
                                      temperature=0.2, max_output_tokens=300)

while True:
    run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)
    if run.status == "requires_action":
        # 1) Модель выбрала инструмент → достаём tool_calls
        tool_calls = run.required_action.submit_tool_outputs.tool_calls
        outputs = []
        for call in tool_calls:
            if call.function.name == "get_weather":
                city = json.loads(call.function.arguments)["location"]
                # 2) ВЫПОЛНЯЕМ ФУНКЦИЮ САМИ (HTTP GET, таймауты, нормализация)
                data = {"desc": "sunny", "temp_c": 12}  # тут реальный API
                outputs.append({"tool_call_id": call.id,
                                "output": json.dumps(data)})
        # 3) Возвращаем Observation в ран
        run = client.beta.threads.runs.submit_tool_outputs(
            thread_id=thread.id,
            run_id=run.id,
            tool_outputs=outputs
        )
    elif run.status in ["completed", "failed", "cancelled", "expired"]:
        break
    else:
        time.sleep(0.5)

messages = client.beta.threads.messages.list(thread_id=thread.id)
final = next(m for m in messages.data if m.role == "assistant")
print(final.content[0].text.value)

**Что здесь важно
• Этап «Действие»: ассистент присылает *tool_call*, а не придуманную «Observation». Ты сам вызываешь API/функцию и возвращаешь результат через submit_tool_outputs.
• Отдельные параметры генерации:
    ◦ для шага рассуждения/действий — temperature=0.1–0.3 (меньше фантазий, стабильнее JSON/функции);
    ◦ для финального ответа можно сделать отдельный run с temperature=0.4–0.6, чтобы ответ звучал приятнее:**

client.beta.threads.runs.create(
    thread_id=thread.id, assistant_id=assistant.id,
    temperature=0.5, instructions="Сформулируй финальный ответ кратко и ясно."
)

**• Можно жёстче управлять: задать tool_choice={"type": "function", "function": {"name": "get_weather"}} или tool_choice="required", чтобы модель обязательно вызывала инструмент.
• Для строгого формата ответа пригодится response_format={"type":"json_schema", "json_schema":{...}}.**
```

**«Нужно ли разбивать на Thought/Action/Observation прямо в тексте?»**
Не обязательно. В Assistants API **ReAct-логика может быть имплицитной**:
• «Thought» остаётся внутри модели (скрыто);
• «Action» проявляется как `tool_calls`;
• «Observation» — это твой `submit_tool_outputs`;
• «Final Answer» — текст ассистента после успешной обработки.
Если хочешь явную трассировку (для аудита, обучения команды, отладки) — добавь инструкции «думай шагами» и проси кратко фиксировать «Thought: …» (без лишних токенов). Но для продакшена чаще достаточно **инструментального цикла** без явного прогона мыслей в текст.

**Когда точно делай «двухфазно»**
• Несколько инструментов и возможные ветвления («сначала поиск → затем парсинг → затем расчёт»).
• Суровые требования к **надёжности и комплаенсу** (нельзя «выдумывать» факты).
• **Оптимизация стоимости**: лимиты на шаги/токены, ранний выход при готовности.
• Нужна **разная температура** для логики и финальной подачи.

**Важные настройки & практики**
• **Отключи параллельные tool-calls**, если нужен ровно один вызов за шаг.
• Валидация аргументов функций (JSON-схема, типы, `additionalProperties:false`).
• Таймауты, ретраи, маскирование секретов в логах/наблюдениях.
• `max_output_tokens` для каждого шага + `max_cycles` на весь «квест».
• Для кода-агентов (если используешь Code Interpreter) — песочница и ограничения.

**Вывод**
• **Нет**, ты не обязана вручную расписывать все этапы, если используешь OpenAI Assistants — платформа уже поддерживает цикл действий через tool-calls.
• **Да**, иметь в голове (и частично в настройках) модель **Thought → Action → Observation → Final Answer** стоит: это позволит точнее управлять температурой, качеством, безопасностью и стоимостью.

## Что важно запомнить

- **Температура** = управляет стилем мышления (точность vs креатив).
- **Функции get / act / observe** = фазы взаимодействия с миром.
- **System Prompt** = сценарий, где прописано, что агент “умеет”.
- **Stop-токен** = тормоз от галлюцинаций.

[Все уроки](https://learnvibecoding.ru/publiclessons.md) · HTML: https://learnvibecoding.ru/publiclessons