У Гоголя помещик Собакевич так расхваливает умерших работников, будто завтра они выйдут на смену, подлатают крышу и попросят прибавки. Чичиков, которому для своей затеи нужны их имена в документах, всё-таки просит список. Хозяин подходит к бюро и начинает писать. Из потока похвал наконец появляется бумага: её можно развернуть и прочитать.
Допустим, перед поездкой на дачу у нас на столе лежат катушка шпагата и два пакетика семян. Рядом телефон: «Сколько всё вместе?» Разговорчивый помощник уже готов основать агрохолдинг, назначить пугало директором и предъявить урожай следующего века. Придержим его за пиджак: нам пока нужны цены. Дадим ему два небольших дела — поиск и подсчёт, — а себе оставим возможность заглянуть в журнал.
Каталог крошечный: A — шпагат за 120 рублей, B — семена за 80. Покупка должна дать 280. Цены для упражнения придуманы; путь числа мы проследим целиком.
Кто сходит к бюро за ценой
Модель читает вопрос и выбирает действие. Программа проверяет её просьбу и запускает функцию — небольшое действие с понятным результатом. Передали код A, получили цену 120 из каталога.
У нас две функции: lookup_price ищет цену по коду, total считает сумму по товарам и количествам. В запросе список доступных инструментов называется tools.

Схема вызова инструмента: между просьбой модели и каталогом работает ваша программа.
В примере Tool Calls модель возвращает название функции и аргументы — данные для вызова. Выполняет её внешний исполнитель. Здесь это программа на Go в одном файле.
За фразой «сейчас посмотрю» появится обращение к строке с ценой. Программа ничего не заказывает, покупка пока лежит на столе.
Собственные функции подключим через API — адрес для обращения программы к модели. Нужен свой доступ к DeepSeek API с действующим балансом или лимитом; обращения оплачиваются по условиям аккаунта. Доступ к веб-чату оформлен отдельно.
Сначала получим 280 без подключения к модели
Нужен установленный Go: вот инструкция установки. На Windows команды вводят в PowerShell, на macOS — в «Терминале». В этом окне вы пишете поручение компьютеру и видите его ответ.
Если застряли, откройте DeepSeek и передайте запрос вместе с кодом из конца статьи:
Хочу запустить пример агента из статьи. Помоги по одному действию.
Сначала узнай мою операционную систему и установлен ли Go.
Потом объясни: что открыть, куда вставить команду и какой ответ ждать.
Начнём с локальной проверки -check, без обращения к API.
Используй готовый код статьи, не переписывай его с нуля.
Жди моего результата после каждого шага. Если пришлю ошибку,
объясни ближайшее исправление. Ключ доступа у меня не запрашивай.
Создайте папку first-agent, сохраните в ней код как обычный текстовый файл agent.go. Если редактор добавляет расширение .txt, уточните у помощника, как его убрать. Откройте терминал в этой папке и введите:
go version
Ждём ответ, начинающийся с go version. Команда не найдена — проверьте установку и заново откройте терминал. Затем выполните:
go run agent.go -check
Проверка запускает функции без обращения к DeepSeek. Ждём три строки:
OK {"total":280}
OK {"error":"unknown","sku":"C"}
OK {"error":"invalid_quantity"}
Первая подтверждает сумму, вторая — отсутствие товара C, третья — отказ считать отрицательное количество. При ошибке дайте помощнику её текст и название команды. Исполнитель уже умеет считать; подключим собеседника.
Дадим модели ключ от одного ящика
Программа прочитает ключ из переменной окружения DEEPSEEK_API_KEY — значения для текущего окна терминала. В переписку с помощником ключ не вставляйте.
На macOS с оболочкой zsh введите следующие две строки. После первой вставьте ключ локально, в скрытый ввод, и нажмите Enter:
read -s 'DEEPSEEK_API_KEY?Ключ DeepSeek: '
export DEEPSEEK_API_KEY
В PowerShell используйте скрытый ввод и перенесите значение в окружение текущего процесса:
$agentKey = Read-Host 'Ключ DeepSeek' -AsSecureString
$env:DEEPSEEK_API_KEY = [System.Net.NetworkCredential]::new('', $agentKey).Password
Remove-Variable agentKey
Теперь запустите в той же папке:
go run agent.go
Программа спросит «Сколько стоит A и две B?», затем — «А если взять две A?». Ждём итоги 280 и 240. Между вопросом и ответом появится журнал: lookup_price означает поиск цены, total — расчёт. Модель может объединить несколько просьб, поэтому число строк различается.
В журнале первого вопроса ищем цены A и B и подсчёт с количествами один и два. Если сумма появилась без действий, задайте программе вопрос явно: «Узнай цены A и B через lookup_price, затем посчитай одну A и две B через total».
HTTP-запрос идёт на https://api.deepseek.com/chat/completions. Модель deepseek-flash и обдумывание thinking уже указаны в коде по контракту Chat Completions.
Почему помощник носит с собой всю папку
На втором вопросе проявляется особенность DeepSeek. В API уходит накопленная переписка: вопросы, сообщения модели, вызовы и результаты функций.
В ответе есть поле обдумывания reasoning_content. При запросах с tools DeepSeek требует возвращать его из всех прошлых ходов, включая сообщения без вызова функции: см. Thinking Mode.
Представьте папку с документами: вопрос, поручение, результат и ответ. Следующий вопрос кладём поверх. Секретарь вытаскивает предыдущие листы: «Я вам самое главное оставил!» Потом ещё лист, ещё. В конце торжественно несёт директору обложку. Программа обойдётся без такого усердия: все сообщения остаются на месте.

Схема истории: вместе со следующим вопросом программа передаёт все прежние сообщения, включая окончательный ответ модели.
Код добавляет сообщение assistant целиком. Затем прикладывает сообщение tool: результат и номер tool_call_id, связывающий его с просьбой модели. В журнал выводятся действия, аргументы и результаты; обдумывание остаётся внутри переписки с API.
Два вопроса подряд проверяют продолжение разговора: катушка знакомая, состав покупки другой. На один вопрос даётся до пяти обращений к модели, затем программа остановится. Бесконечно обсуждать судьбу шпагата помощнику не поручено.
Неизвестный товар полезнее второй удачной покупки
Передадим программе отдельный вопрос в кавычках:
go run agent.go 'Узнай цену товара C'
Товара C нет: ждём unknown и сообщение об отсутствии цены. Затем проверим отрицательное количество:
go run agent.go 'Посчитай минус одну A через total'
Модель может сама указать на неверное количество либо вызвать функцию и получить invalid_quantity. Прямой отказ функции уже проверен через -check.

Контрольные случаи: сумма получается из каталога, отсутствующий товар и неверное количество сохраняют свои отдельные результаты.
| Что проверяем | Что ищем в журнале | Что означает результат |
|---|---|---|
| Одна A и две B | Цены 120 и 80, затем total = 280 | Поиск и расчёт дошли до ответа |
| Товар C | error = unknown | В каталоге отсутствует такой код |
| Минус одна A | invalid_quantity при вызове total | Исполнитель отклонил количество |
| Следом две A | Новый расчёт 240 после первого ответа | Диалог продолжился с прежней историей |
| Остановка с HTTP 400 | Код статуса, сохранённые сообщения в программе | Нужно проверить формат запроса и полноту истории |
Когда оба вопроса работают, устройте каталогу маленькую перестановку. В файле найдите строку var prices, поменяйте цену A со 120 на 130, сохраните файл и повторите обычный запуск. Теперь первая покупка должна стоить 290, вторая — 260. Верните исходную цену после опыта. На такой правке особенно хорошо видно происхождение ответа: изменился источник — через всю программу прошли новые числа.
В журнале 290, в ответе 280 — разбираем передачу результата в ответ. Уже в журнале 280 — проверяем сохранение файла и папку запуска. Одна правка ценника помогает найти, где застряло старое число.
При HTTP 401 проверяйте ключ и доступ, при 402 — баланс аккаунта, при 429 — ограничения частоты запросов. Код ошибки указывает, куда смотреть дальше: запишите его и откройте официальную таблицу ошибок DeepSeek. Помощнику можно дать этот код, команду и описание предыдущего результата без ключа.
Готовый файл agent.go
Ниже полный agent.go. Две функции работают с каталогом, отдельная часть отправляет запрос и возвращает результаты модели. Дополнительные библиотеки не нужны.
Показать код agent.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
type Obj = map[string]any
var prices = map[string]int{"A": 120, "B": 80}
func decode(raw string, dst any) error {
d := json.NewDecoder(strings.NewReader(raw))
d.DisallowUnknownFields()
if err := d.Decode(dst); err != nil {
return err
}
if err := d.Decode(new(any)); err != io.EOF {
return fmt.Errorf("лишние данные")
}
return nil
}
func tool(name, raw string) Obj {
switch name {
case "lookup_price":
var a struct {
SKU string `json:"sku"`
}
if decode(raw, &a) != nil || a.SKU == "" {
return Obj{"error": "invalid_arguments"}
}
p, ok := prices[a.SKU]
if !ok {
return Obj{"error": "unknown", "sku": a.SKU}
}
return Obj{"sku": a.SKU, "price": p}
case "total":
var a struct {
Lines []struct {
SKU string `json:"sku"`
Qty *int `json:"qty"`
} `json:"lines"`
}
if decode(raw, &a) != nil || len(a.Lines) == 0 || len(a.Lines) > 20 {
return Obj{"error": "invalid_arguments"}
}
sum := 0
for _, line := range a.Lines {
if line.Qty == nil || *line.Qty < 1 || *line.Qty > 100 {
return Obj{"error": "invalid_quantity"}
}
p, ok := prices[line.SKU]
if !ok {
return Obj{"error": "unknown", "sku": line.SKU}
}
sum += p * *line.Qty
}
return Obj{"total": sum}
}
return Obj{"error": "unknown_tool"}
}
func jsonText(v any) string { b, _ := json.Marshal(v); return string(b) }
var tools = json.RawMessage(`[
{"type":"function","function":{"name":"lookup_price",
"description":"Узнать цену по коду товара",
"parameters":{"type":"object","properties":{"sku":{"type":"string"}},
"required":["sku"],"additionalProperties":false}}},
{"type":"function","function":{"name":"total",
"description":"Посчитать сумму по кодам и количествам, цены берёт программа",
"parameters":{"type":"object","properties":{"lines":{"type":"array",
"items":{"type":"object","properties":{"sku":{"type":"string"},
"qty":{"type":"integer","minimum":1,"maximum":100}},
"required":["sku","qty"],"additionalProperties":false}}},
"required":["lines"],"additionalProperties":false}}}
]`)
func turn(messages *[]Obj, key, question string) error {
*messages = append(*messages, Obj{"role": "user", "content": question})
client := http.Client{Timeout: 120 * time.Second}
for step := 1; step <= 5; step++ {
body := Obj{"model": "deepseek-flash", "messages": *messages,
"tools": tools, "thinking": Obj{"type": "enabled"}, "stream": false}
req, err := http.NewRequest("POST", "https://api.deepseek.com/chat/completions",
bytes.NewBufferString(jsonText(body)))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return err
}
data, err := io.ReadAll(io.LimitReader(res.Body, 8<<20))
res.Body.Close()
if err != nil {
return err
}
if res.StatusCode != 200 {
return fmt.Errorf("HTTP %d: проверьте справку и аккаунт", res.StatusCode)
}
var reply struct {
Choices []struct {
Message Obj `json:"message"`
Finish string `json:"finish_reason"`
} `json:"choices"`
}
if json.Unmarshal(data, &reply) != nil || len(reply.Choices) == 0 {
return fmt.Errorf("непонятный ответ API")
}
choice := reply.Choices[0]
if choice.Finish != "stop" && choice.Finish != "tool_calls" {
return fmt.Errorf("ответ не завершён: %s", choice.Finish)
}
msg := choice.Message
if msg["role"] != "assistant" {
return fmt.Errorf("нет сообщения assistant")
}
*messages = append(*messages, msg) // Сохраняем сообщение целиком.
var calls []struct {
ID string `json:"id"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
if err := json.Unmarshal([]byte(jsonText(msg["tool_calls"])), &calls); err != nil {
return err
}
if len(calls) == 0 {
answer, ok := msg["content"].(string)
if !ok || strings.TrimSpace(answer) == "" {
return fmt.Errorf("пустой ответ")
}
fmt.Println("Ответ:", answer)
return nil
}
if len(calls) > 8 {
return fmt.Errorf("слишком много вызовов")
}
for _, call := range calls {
if call.ID == "" {
return fmt.Errorf("нет ID вызова")
}
result := tool(call.Function.Name, call.Function.Arguments)
fmt.Printf("Шаг %d: %s %s → %s\n", step, call.Function.Name, call.Function.Arguments, jsonText(result))
*messages = append(*messages, Obj{"role": "tool", "tool_call_id": call.ID, "content": jsonText(result)})
}
}
return fmt.Errorf("достигнут предел пяти обращений")
}
func main() {
if len(os.Args) > 1 && os.Args[1] == "-check" {
cases := []struct{ Name, Args, Want string }{
{"total", `{"lines":[{"sku":"A","qty":1},{"sku":"B","qty":2}]}`, `{"total":280}`},
{"lookup_price", `{"sku":"C"}`, `{"error":"unknown","sku":"C"}`},
{"total", `{"lines":[{"sku":"A","qty":-1}]}`, `{"error":"invalid_quantity"}`},
}
for _, c := range cases {
got := jsonText(tool(c.Name, c.Args))
if got != c.Want {
fmt.Fprintln(os.Stderr, "FAIL", got)
os.Exit(1)
}
fmt.Println("OK", got)
}
return
}
key := os.Getenv("DEEPSEEK_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "Задайте DEEPSEEK_API_KEY")
os.Exit(1)
}
messages := []Obj{{"role": "system", "content": "Ты считаешь покупки из каталога. Сначала узнавай цены каждого нужного кода через lookup_price, затем вызывай total. Количество бери из вопроса. При ошибке объясни её, сумму не выдумывай. В ответе используй результат total. Каталог: A — шпагат, B — семена. Цены в рублях."}}
questions := []string{"Сколько стоит A и две B?", "А если взять две A?"}
if len(os.Args) > 1 {
questions = []string{strings.Join(os.Args[1:], " ")}
}
for _, q := range questions {
fmt.Println("Вопрос:", q)
if err := turn(&messages, key, q); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
Исполнитель проверяет аргументы и имя функции; цену берёт из каталога.
Частые вопросы
Можно создать ИИ-агента в DeepSeek одной инструкцией в чате?
Чтобы создать ИИ-агента в DeepSeek с собственными функциями, нужны исполнитель и API. В чате можно обсудить замысел, задать роль и порядок беседы; функции подключает программа.
Почему бы сразу не попросить сложить цены?
Для разового подсчёта можно. Здесь осваиваем получение сведений из отдельного источника. Позже вместо двух строк подключим поиск по своим данным.
Что делать, если пять обращений закончились без ответа?
Посмотрите в журнале, какая просьба повторяется. При ошибке аргументов исправляйте описание инструмента или вопрос: станет понятно, почему помощник ходит к ящику кругами.
Можно собрать похожую схему без кода?
Да, если удобнее соединять готовые блоки, посмотрите пример ИИ-агента в n8n. Там источник ищет время встречи. Переход полезен после понимания цикла: вопрос, вызов, результат, ответ.
С какой собственной задачей продолжить?
Выберите повторяющийся вопрос с небольшим источником: наличие материала, стоимость набора, время события. В статье о выборе первого сценария бота есть способ сузить замысел до одного проверяемого ответа. Тогда новому инструменту будет понятно, за чем идти.
Сохраните файл и журнал удачного запуска рядом. Теперь у фразы «я всё посчитал» есть продолжение: вот полученные цены, вот количества, вот сумма. Катушку можно убрать в дачную сумку. Пугало пусть пока побудет пугалом. У помощника уже получилось дело поскромнее — он открыл нужный ящик и вернулся с ответом, который можно проверить.