От LLM к агенту: Как заставить Go приложение думать и действовать. ai.. ai. gigachat.. ai. gigachat. Go.. ai. gigachat. Go. golang.. ai. gigachat. Go. golang. langchain.. ai. gigachat. Go. golang. langchain. искусственный интеллект.. ai. gigachat. Go. golang. langchain. искусственный интеллект. исскуственный интеллект.

От автора: Эта статья родилась из желания разобраться в том, что осталось за кадром отличного доклада.

1. Введение

1.1. История создания проекта

Всё началось с доклада Антона Юрченко «Улучшаем качество отчётов нагрузочного тестирования с помощью Go, LangChain и GigaChat».

Доклад мне понравился: чёткая постановка проблемы, грамотный подход к автоматизации, отличная идея с использованием LLM для генерации человекопонятных отчётов. Но после просмотра осталась одна проблема — код интеграции так и не показали.

Было сказано лишь, что нужно «реализовать интерфейс» для подключения GigaChat к LangChain. Звучит просто, но когда ты открываешь документацию LangChainGo, которая к слову еще написана только наполовину и пытаешься понять, с чего начать — возник вопрос: Какой именно интерфейс реализовывать? Далее по изучению документации возникли и другие:

  • Что такое функции в LLM и как их реализовывать?

  • Как связать это всё с цепочками (chains) и зачем они вообще нужны?

  • Что такое шаблон запроса и нужно ли мне им пользоваться?

Так появился этот pet-проект. Я решил сам разобраться и создать рабочий пример, который можно потрогать, запустить и модифицировать.

1.2. Цель статьи

Показать рабочий код интеграции GigaChat с LangChainGo на Go. В нём я хочу реализовать приложение которое указано в примерах как библиотеки, так и документации GigaChat, а именно сервис по определению погоды, но сделать не просто запрос к модели, которая отдаст мне возможно галлюцинации, а возможно и правильные данные. Создать и использовать агента, который будет из запроса пользователя получать город и количество дней для прогноза, передавать получать реальный прогноз погоды с помощью mcp-сервера ,а затем уже имея все необходимые данные формировать прогноз и давать совет по одежде, которую одеть на улицу.

2. Подготовка: что понадобится

2.1. Инструменты и зависимости

Для создания агента мы используем библиотеку github.com/tmc/langchaingo, ещё в работе агента нам понадобится:

  1. mcp сервер погоды

  2. Библиотека для его подключения github.com/modelcontextprotocol/go-sdk/mcp

  3. GigaChat – непосредственно сама модель

  4. Node.js – для работы mcp сервера погоды.

2.2. Регистрация в Sber Developers

Для доступа к GigaChat API нам нужно создать аккаунт в Sber Developers и создать там свой проект.

Шаг 1:

Переходим на developers.sber.ru, регистрируемся и авторизуемся.

Переходим на developers.sber.ru, регистрируемся и авторизуемся.

Шаг 2:

Выбираем создать новый проект.

Выбираем создать новый проект.

Шаг 3:

Выбираем AI-модели -> GigaChat API

Выбираем AI-модели -> GigaChat API

Шаг 4:

Заходим в созданный проект

Заходим в созданный проект

Шаг 5:

Выбираем настроить API и нажимаем получить новый ключ

Выбираем настроить API и нажимаем получить новый ключ

2.3. Создание и структура проекта

Для начала инициализируем проект weather-agent

go mod init weather-agent

Cтруктура нашего проекта:

Структура проекта

Структура проекта

3. Архитектура приложения

3.1. Общая схема

Немного разобравшись в документации langchaingo я узнал, что в основном любой агент состоит из нескольких компонентов:

  • Модель, которая будет основой агента (Model)

  • Функции которые она умеет выполнять (Tools)

  • Память модели (Brain)

  • Цепочки которые используются для определения последовательности действий агента (Chain)

Это стандартные компоненты агента и их можно комбинировать, а чтобы агент был более детерминирован, мы как и в обыкновенной программе описываем ему последовательность действий, которая называется цепочка. Составляя эти цепочки мы определяем порядок работы агента, а также можем делать возврат к необходимой нам части цепочки при сбое, или повторению отдельного ее элемента. Я решил не добавлять память в своего агента, так как это не особо требуется при получении прогноза погоды, но при желании можно просто добавить элемент памяти если вам это будет необходимо.

Схема работы агента

Схема работы агента

3.2. Ключевые компоненты

GigaChatLLM – Реализация интерфейса llms.Model для GigaChat, Agent – Координатор инструментов и исполнитель цепочек (WeatherAgent), Chains – Цепочка запросов, Tools – Внешние функции (weather через MCP), MCP Client – Клиент для подключения к weather-серверу

4. Поток данных: от запроса до ответа

Разберём полный путь запроса на примере: «Какая погода в Москве на 3 дня?»

4.1. Шаг 1: HTTP-запрос пользователя

POST http://localhost:8080/api/weather/process
Content-Type: application/json

{
  "prompt": "Какая погода в Новосибирске на 7 дней?"
}

4.2. Шаг 2: Контроллер (controller.go)

Файл: controller.go

const (
	weatherProcessRoute = "/weather/process" 
)

// AgentRequest представляет запрос к агенту с произвольным промптом.
type AgentRequest struct {
	Prompt string `json:"prompt"` // Текстовый запрос пользователя
}

// llmRoutes хранит зависимости для обработки запросов к LLM.
type llmRoutes struct {
	l     *slog.Logger        // l — логгер для записи событий
	agent *agent.WeatherAgent // agent — агент для обработки запросов о погоде
}

// newLLMRoutes регистрирует все маршруты для работы с LLM.
func newLLMRoutes(api fiber.Router, l *slog.Logger, agent *agent.WeatherAgent) {
	// Инициализируем структуру с зависимостями
	lr := llmRoutes{l, agent}

	// Регистрируем обработчик
	api.Post(weatherProcessRoute, lr.ProcessWeather)
}

// ProcessWeather обрабатывает запрос о прогнозе погоды.
func (lr *llmRoutes) ProcessWeather(c fiber.Ctx) error {
	// Создаём контекст с таймаутом 60 секунд для получения прогноза
	ctx, cancel := context.WithTimeout(c.Context(), 60*time.Second)
	defer cancel()

	// Парсим JSON из тела запроса в структуру AgentRequest
	var req AgentRequest
	raw := c.BodyRaw()
	if err := json.Unmarshal(raw, &req); err != nil {
		// Возвращаем HTTP 400 при невалидном JSON
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"error":   "Невалидный JSON",
		})
	}

	// Проверяем, что поле prompt не пустое
	if req.Prompt == "" {
		return c.Status(http.StatusBadRequest).JSON(fiber.Map{
			"success": false,
			"error":   "Поле 'prompt' обязательно",
		})
	}

	// Логируем полученный запрос о погоде с длиной промпта
	lr.l.Info("[ProcessWeather] Processing weather request", "prompt_length", len(req.Prompt))

	// Передаём запрос агенту для обработки
	response, err := lr.agent.ProcessWeather(ctx, req.Prompt)
	if err != nil {
		// Логируем ошибку обработки
		lr.l.Error("[ProcessWeather] Agent execution failed", "error", err)

		// Возвращаем HTTP 500 с описанием ошибки
		return c.Status(http.StatusInternalServerError).JSON(fiber.Map{
			"success": false,
			"error":   "Ошибка обработки запроса: " + err.Error(),
		})
	}

	// Логируем успешную обработку с длиной ответа
	lr.l.Info("[ProcessWeather] Success", "response_length", len(response))

	// Отправляем ответ клиенту
	return c.Status(http.StatusOK).JSON(fiber.Map{
		"success":  true,
		"response": response,
		"message":  "Запрос успешно обработан",
	})
}

4.3. Шаг 3: Агент (agent/weather_agent.go)

Файл: agent/weather_agent.go

// интерфейс для логирования.
type Logger interface {
	Debug(msg string, args ...any)
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

// основная структура агента для обработки запросов о погоде.
type WeatherAgent struct {
	llm    llms.Model                   // языковая модель
	tool   *weather.WeatherForecastTool // инструмент для работы агента
	logger Logger                       // логгер
}

// NewWeatherAgent создаёт и инициализирует новый экземпляр WeatherAgent.
func NewWeatherAgent(llm llms.Model, l Logger) *WeatherAgent {
	l.Info("[WeatherAgent] Agent initialized")
	weatherTool := weather.NewWeatherForecastTool()
	return &WeatherAgent{
		llm:    llm,
		tool:   weatherTool,
		logger: l,
	}
}

// ProcessWeather обрабатывает запрос о прогнозе погоды.
func (wa *WeatherAgent) ProcessWeather(ctx context.Context, input string) (string, error) {
	wa.logger.Info("[WeatherAgent] Starting weather processing", "input_preview", input)
	// отдаем на обработку промта цепочкой агента
	result, err := weather.HandleWeatherRequest(ctx, wa.llm, wa.tool, wa.logger, input)
	if err != nil {
		wa.logger.Error("[WeatherAgent] Weather processing failed", "error", err)
		return "", err
	}

	wa.logger.Info("[WeatherAgent] Weather processing completed successfully")
	return result, nil
}

4.4. Шаг 4: Обработчик погоды (weather/handler.go)

Файл: weather/handler.go

func HandleWeatherRequest(ctx context.Context, llm llms.Model, tool *WeatherForecastTool, l Logger, userInput string) (string, error) {
	// Логируем начало обработки погодного запроса
	l.Info("[WeatherHandler] Starting weather forecast chain", "input", userInput)

	// Создаём цепочки для каждого этапа обработки
	// createExtractArgsChain — извлекает город и количество дней из запроса
	extractChain := createExtractArgsChain(llm, l)

	// createParseArgsChain — парсит JSON с аргументами в структуру
	parseArgsChain := createParseArgsChain(l)

	// createWeatherToolChain — вызывает инструмент погоды с полученными аргументами
	weatherToolChain := createWeatherToolChain(tool, l)

	// createFinalResponseChain — форматирует финальный ответ с рекомендациями
	finalResponseChain := createFinalResponseChain(llm)

	// Создаём последовательную цепочку из всех этапов
	accumulatingChain := NewAccumulatingSequentialChain([]chains.Chain{
		extractChain,
		parseArgsChain,
		weatherToolChain,
		finalResponseChain,
	})

	l.Info("[WeatherHandler] AccumulatingSequentialChain created, executing...")

	// Запускаем цепочку с пользовательским запросом
	result, err := accumulatingChain.Call(ctx, map[string]any{
		"userInput": userInput,
	})
	if err != nil {
		// Логируем ошибку выполнения цепочки
		l.Error("[WeatherHandler] AccumulatingSequentialChain execution failed", "error", err)
		return "", err
	}

	// Извлекаем текстовый результат из выходных данных
	finalOutput, ok := result["text"].(string)
	if !ok {
		// Логируем ошибку типа данных
		l.Error("[WeatherHandler] Invalid output type from AccumulatingSequentialChain")
		return "", fmt.Errorf("invalid output from AccumulatingSequentialChain")
	}

	// Логируем успешное завершение
	l.Info("[WeatherHandler] AccumulatingSequentialChain completed successfully")

	return finalOutput, nil
}

4.5. Шаг 5: Цепочка 1 — ExtractArgsChain

Задача: Извлечь город и количество дней из текстового запроса.

Здесь и далее мы будем использовать шаблоны, на основании которых и создаются наши промпты к модели добавляя в них необходимые нам данные

Файл: weather/template.go

var (
	// WeatherPromptTemplate — шаблон для извлечения аргументов из запроса пользователя.
	WeatherPromptTemplate prompts.PromptTemplate

	// FinalWeatherPromptTemplate — шаблон для генерации финального ответа с рекомендациями.
	FinalWeatherPromptTemplate prompts.PromptTemplate
)

// initTemplates инициализирует все шаблоны промптов.
// Каждый шаблон определяется с текстом промпта и списком требуемых переменных.
func InitTemplates() {
	// Инструктирует LLM определить город и количество дней для прогноза
	WeatherPromptTemplate = prompts.NewPromptTemplate(
		`Ты — помощник по погоде. Твоя задача — помочь пользователю с прогнозом погоды и дать рекомендации по одежде.

У тебя есть доступ к function weather_forecast, который возвращает прогноз погоды на указанное количество дней для заданного города.

Инструкции:
1. Проанализируй запрос пользователя и сгенерируй аргументы для weather_forecast. Если количество дней не указано, по умолчанию возьми 1.
Верни ответ в виде json
Запрос пользователя: {{.userInput}}`,
		[]string{"userInput"},
	)

	// Преобразует сырые данные прогноза в понятный текст с рекомендациями
	FinalWeatherPromptTemplate = prompts.NewPromptTemplate(
		`Ты — помощник по погоде. Получен прогноз погоды для города {{.city}} на {{.days}} дней:

{{.forecast}}

Проанализируй этот прогноз выведи название этого города и дай пользователю краткий сводный прогноз на каждый день, а также рекомендации по одежде (например, если ожидается дождь, возьми зонт; если холодно, надень теплую куртку). Выведи все в виде текста без специальных символов чтобы человек мог это понять.`,
		[]string{"city", "days", "forecast"},
	)
}

В процессе реализации цепочки выполнения я понял что в библиотеке нет такой цепочки которая сохраняла бы результаты выполнения предыдущих цепочек, а не просто передавала бы значения текущей в следующую цепочку, поэтому я решил реализовать свою цепочку которая сохраняла бы все значения полученные в цепочках и передавала бы их на следующие этапы, хотя можно было бы здесь и внедрить память агента.

Файл: weather/chains.go

// AccumulatingSequentialChain — кастомная цепочка для последовательного выполнения нескольких цепочек.
// В отличие от стандартной SequentialChain, накапливает ключи между шагами,
// передавая результаты каждого предыдущего этапа следующему
// здесь мы просто реализуем интерфейс Сhain
type AccumulatingSequentialChain struct {
	chains []chains.Chain
}

// Call выполняет все цепочки последовательно, передавая результаты от одной к другой.
func (c *AccumulatingSequentialChain) Call(ctx context.Context, inputs map[string]any, options ...chains.ChainCallOption) (map[string]any, error) {
	// Инициализируем результат входными данными
	result := inputs

	// Последовательно выполняем каждую цепочку
	for _, chain := range c.chains {
		// Вызываем текущую цепочку с накопленными результатами
		stepResult, err := chains.Call(ctx, chain, result, options...)
		if err != nil {
			// Возвращаем ошибку при неудаче любой цепочки
			return nil, err
		}

		// Объединяем результаты с предыдущими (накопление ключей)
		result = mergeMaps(result, stepResult)
	}

	return result, nil
}

Реализация самих цепочек:

// createExtractArgsChain создаёт цепочку для извлечения аргументов из запроса.
func createExtractArgsChain(llm llms.Model, l Logger) chains.Chain {
	return chains.NewTransform(
		func(ctx context.Context, input map[string]any, _ ...chains.ChainCallOption) (map[string]any, error) {
			// Извлекаем пользовательский запрос из входных данных
			userInput, ok := input["userInput"].(string)
			if !ok {
				return nil, fmt.Errorf("invalid userInput type")
			}

			// Логируем начало извлечения аргументов
			l.Info("[WeatherArgsExtract] Extracting city and days from user input", "input", userInput)

			// Формируем промпт для LLM используя шаблон
			// WeatherPromptTemplate.Format подставляет userInput в шаблон
			prompt, err := WeatherPromptTemplate.Format(map[string]any{"userInput": userInput})
			if err != nil {
				l.Error("[WeatherArgsExtract] Failed to format prompt", "error", err)
				return nil, err
			}

			// Создаём сообщение для LLM
			messages := []llms.MessageContent{
				llms.TextParts(llms.ChatMessageTypeHuman, prompt),
			}

			// Настраиваем ToolChoice для принудительного вызова weather_forecast
			toolChoice := llms.ToolChoice{
				Type:     "function",
				Function: &llms.FunctionReference{Name: "weather_forecast"},
			}

			l.Info("[WeatherArgsExtract] Calling LLM with ToolChoice", "function", "weather_forecast")

			// Вызываем LLM с инструментами и ToolChoice
			resp, err := llm.GenerateContent(ctx, messages,
				llms.WithTools(AvailableTools),
				llms.WithToolChoice(toolChoice),
			)
			if err != nil {
				l.Error("[WeatherArgsExtract] LLM.GenerateContent failed", "error", err)
				return nil, err
			}

			// Проверяем, что LLM вернула ответ
			if len(resp.Choices) == 0 {
				return nil, fmt.Errorf("no response from LLM")
			}

			// Получаем ответ от LLM
			llmResponse := resp.Choices[0].Content
			l.Info("[WeatherArgsExtract] LLM response received", "content_preview", llmResponse)

			// Извлекаем JSON из ответа LLM
			jsonStr := extractJSON(llmResponse)
			l.Debug("[WeatherArgsExtract] Extracted JSON", "json", jsonStr)

			// Возвращаем сырой JSON для следующего этапа парсинга
			return map[string]any{
				"raw_args": jsonStr,
			}, nil
		},
		[]string{"userInput"},
		[]string{"raw_args"},
	)
}

Пример работы: Вход: "Какая погода в Москве на 3 дня?" Выход: {"city": "Москва", "days": 3}

Вход: "Покажи погоду в Питере" Выход: {"city": "Санкт-Петербург", "days": 1}

4.6. Шаг 6: Цепочка 2 — ParseArgsChain

Задача: Распарсить JSON в структуру и установить значения по умолчанию.

Файл: weather/handler.go

// createParseArgsChain создаёт цепочку для парсинга JSON аргументов.
// Преобразует сырой JSON от LLM в структурированные данные (город и дни).
func createParseArgsChain(l Logger) chains.Chain {
	return chains.NewTransform(
		func(ctx context.Context, input map[string]any, _ ...chains.ChainCallOption) (map[string]any, error) {
			// Извлекаем сырой JSON из предыдущего этапа
			rawArgs, ok := input["raw_args"].(string)
			if !ok {
				return nil, fmt.Errorf("invalid raw_args type")
			}

			// Логируем сырой вывод LLM
			l.Debug("[ParseArgs] Raw LLM output", "raw", rawArgs, 200)

			// Извлекаем JSON из строки (на случай markdown-разметки)
			jsonStr := extractJSON(rawArgs)
			l.Debug("[ParseArgs] Extracted JSON", "json", jsonStr)

			// Парсим JSON в структуру weatherArgs
			var args weatherArgs
			if err := json.Unmarshal([]byte(jsonStr), &args); err != nil {
				// При ошибке парсинга используем значения по умолчанию
				l.Warn("[ParseArgs] Failed to parse JSON, using fallback", "error", err)
				args.City = "Волгоград"
				args.Days = 1
			}

			// Устанавливаем значения по умолчанию для пустых полей
			if args.City == "" {
				args.City = "Волгоград"
			}
			if args.Days < 1 || args.Days > 7 {
				args.Days = 1
			}

			// Логируем распарсенные аргументы
			l.Info("[ParseArgs] Parsed arguments", "city", args.City, "days", args.Days)

			// Возвращаем структурированные данные для следующего этапа
			return map[string]any{
				"city": args.City,
				"days": args.Days,
			}, nil
		},
		[]string{"raw_args"},
		[]string{"city", "days"},
	)
}

Валидация:

  • Пустой city → "Волгоград"

  • days < 1 или days > 7 → 1

4.7. Шаг 7: Цепочка 3 — WeatherToolChain

Задача: Вызвать MCP-инструмент weather_forecast и получить прогноз.

Инициализация MCP сервера: Файл: weather/mcp.go

// InitMCPSession инициализирует MCP сессию один раз при старте приложения
func InitMCPSession(ctx context.Context) error {
	var initErr error
	mcpSessionOnce.Do(func() {
		mcpSessionMu.Lock()
		defer mcpSessionMu.Unlock()

		// Создаём MCP клиента
		client := mcp.NewClient(
			&mcp.Implementation{Name: "weather-client", Version: "1.0.0"},
			nil,
		)

		// Подключаемся к MCP weather серверу через stdio
		// Сервер должен быть запущен отдельно: npx -y @dangahagan/weather-mcp@latest
		cmd := exec.Command("npx", "-y", "@dangahagan/weather-mcp@latest")
		transport := &mcp.CommandTransport{Command: cmd}

		var err error
		mcpSession, err = client.Connect(ctx, transport, nil)
		if err != nil {
			initErr = fmt.Errorf("failed to connect to MCP weather server: %w", err)
			return
		}
	})
	return initErr
}

Применение инструмента:

Файл: weather/tool.go

// createWeatherToolChain создаёт цепочку для вызова инструмента погоды.
// Получает прогноз погоды для указанного города на заданное количество дней.
func createWeatherToolChain(weatherTool *WeatherForecastTool, l Logger) chains.Chain {
	return chains.NewTransform(
		func(ctx context.Context, input map[string]any, _ ...chains.ChainCallOption) (map[string]any, error) {
			// Извлекаем город из предыдущего этапа
			city, ok := input["city"].(string)
			if !ok {
				return nil, fmt.Errorf("invalid city type")
			}

			// Преобразуем количество дней в int
			days := convertToInt(input["days"])
			if days <= 0 {
				days = 1
			}

			// Логируем вызов инструмента погоды
			l.Info("[WeatherTool] Calling weather_forecast via LLM", "city", city, "days", days)

			// Формируем JSON-аргументы для вызова инструмента
			argsJSON, _ := json.Marshal(map[string]any{
				"city": city,
				"days": days,
			})

			l.Info("[WeatherTool] Executing weather_forecast tool", "args", string(argsJSON))

			// Вызываем инструмент получения прогноза погоды
			toolResult, err := weatherTool.Call(ctx, string(argsJSON))
			if err != nil {
				l.Error("[WeatherTool] Tool call failed", "error", err)
				return nil, err
			}

			// Логируем результат работы инструмента
			l.Info("[WeatherTool] Tool completed", "result_preview", toolResult)

			// Возвращаем прогноз для следующего этапа
			return map[string]any{
				"forecast": toolResult,
			}, nil
		},
		[]string{"city", "days"},
		[]string{"forecast"},
	)
}

MCP вызов:

// weather/mcp.go
func fetchWeatherViaMCP(ctx context.Context, city string, days int) ([]DailyForecast, error) {
 session, err := getMCPSession()
 if err != nil {
  return nil, fmt.Errorf("failed to get MCP session: %w", err)
 }

 // Сначала ищем координаты города через search_location
 searchParams := &mcp.CallToolParams{
  Name: "search_location",
  Arguments: map[string]any{
   "query": city,
   "limit": 1,
  },
 }

 searchResult, err := session.CallTool(ctx, searchParams)
 if err != nil {
  return nil, fmt.Errorf("search_location failed: %w", err)
 }

 if searchResult.IsError {
  return nil, fmt.Errorf("search_location returned error: %v", searchResult)
 }

 // Парсим результат поиска из Markdown формата
 lat, lon := parseLocationFromMarkdown(searchResult)

 if lat == 0 && lon == 0 {
  return nil, fmt.Errorf("city '%s' not found", city)
 }

 // Получаем прогноз через get_forecast
 forecastParams := &mcp.CallToolParams{
  Name: "get_forecast",
  Arguments: map[string]any{
   "latitude":    lat,
   "longitude":   lon,
   "days":        days,
   "granularity": "daily",
  },
 }

 forecastResult, err := session.CallTool(ctx, forecastParams)
 if err != nil {
  return nil, fmt.Errorf("get_forecast failed: %w", err)
 }

 if forecastResult.IsError {
  return nil, fmt.Errorf("get_forecast returned error: %v", forecastResult)
 }

 // Парсим результат прогноза
 return parseForecastResult(forecastResult, days)
}

4.8. Шаг 8: Цепочка 4 — FinalResponseChain

Задача: Сформировать человекочитаемый ответ с рекомендациями.

// createFinalResponseChain создаёт цепочку для генерации финального ответа.
func createFinalResponseChain(llm llms.Model) *chains.LLMChain {
	// Создаём LLM-цепочку с шаблоном FinalWeatherPromptTemplate
	chain := chains.NewLLMChain(
		llm,
		FinalWeatherPromptTemplate,
	)
	// Устанавливаем имя выходного ключа
	chain.OutputKey = "text"
	return chain
}

После прохождения всей цепочки модель возвращает свой ответ:

### Прогноз погоды на неделю в Новосибирскеnn**Город:** Новосибирск  nn**Понедельник, 29 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +3°C  nСкорость ветра: до 26 м/с  nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.nn**Вторник, 30 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +4°C  nСкорость ветра: до 18 м/с  nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.nn**Среда, 1 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +7°C  nСкорость ветра: до 21 м/с  nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.nn**Четверг, 2 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.nn**Пятница, 3 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +9°C  nСкорость ветра: до 24 м/с  nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.nn**Суббота, 4 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.

Далее идет возврат ответа клиенту в виде JSON

{"message":"Запрос успешно обработан","response":"### Прогноз погоды на неделю в Новосибирскеnn**Город:** Новосибирск  nn**Понедельник, 29 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +3°C  nСкорость ветра: до 26 м/с  nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.nn**Вторник, 30 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +4°C  nСкорость ветра: до 18 м/с  nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.nn**Среда, 1 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +7°C  nСкорость ветра: до 21 м/с  nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.nn**Четверг, 2 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.nn**Пятница, 3 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +9°C  nСкорость ветра: до 24 м/с  nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.nn**Суббота, 4 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.","success":true}

5. Реализация ключевых интерфейсов

5.1. Интерфейс llms.Model для GigaChat

LangChainGo требует реализации интерфейса llms.Model, о чём собственно и говорилось в докладе:

type Model interface {
    GenerateContent(
        ctx context.Context, 
        messages []Message, 
        options ...CallOption,
    ) (*Response, error)
}

Я не стал реализовывать свою версию, а воспользовался уже готовой из библиотеки openai.LLM так как GigaChat совместим с запросами типа openai. Нужно сказать, что необязательно использовать именно GigaChat, можно брать и другие модели, которые будут совместима с запросами к openai,так как сейчас почти все современные ллм поддерживают или частично поддерживают его. Но даже если модель несовместима, то есть уже готовые реализации других моделей, такие как Ollama, Mistral и другие. Если же и реализации не подходят для вашей модели, то тогда необходимо реализовать самому интерфейс Model.

5.2. llm.go — создание адаптера GigaChat

Cоздаем метод в котором получаем подключение к необходимой нам модели.

Файл: llm.go

const (
 model = "GigaChat-2" //данная модель самая младшая в линейке, ее хватает чтобы решить данную задачу
 // если вы решаете что то более сложное целесообразно использовать более старшие модели
 url   = "https://gigachat.devices.sberbank.ru/api/v1"
)
// 
func getGigaChatLLM(token string) (*openai.LLM, error) {
    // подключение к модели по url, также есть возможность делать это через grpc
 llm, err := openai.New(openai.WithToken(token), openai.WithBaseURL(url), openai.WithModel(model))
 if err != nil {
  return nil, err

 }
 return llm, nil
}

5.3. token.go — OAuth 2.0 токенизация

GigaChat использует OAuth 2.0. В моем проекте я получаю токен 1 раз, но если вы хотите чтобы приложение работало постоянно необходимо предусмотреть обновление токена, согласно документации срок его жизни 30 минут.

Файл: token.go

// getToken получает acessToken на основе нашего ключа
func getToken(authKey string) (string, error) {
	//готовим данные для запроса
	url := tokenUrl
	method := tokenMethod
	payload := strings.NewReader(scope)
	client := &http.Client{}
	req, err := http.NewRequest(method, url, payload)
	if err != nil {

		return "", err
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("Accept", "application/json")
	req.Header.Add("RqUID", uuid.New().String())
	req.Header.Add("Authorization", "Basic "+authKey)
	// делаем сам запрос на сревера Сбербанка для получения токена
	res, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()

	body, err := io.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	//разбираем полученные данные
	var tok Tok
	err = json.Unmarshal(body, &tok)
	if err != nil {
		return "", err
	}
	return tok.AcessToken, nil
}

// описание структуры токена
type Tok struct {
	AcessToken string `json:"access_token" db:"access_token"`
	ExpiresAt  int64  `json:"expires_at" db:"expires_at"`
}

5.4. main.go — инициализация

Собираем и запускаем наш сервис

Файл: main.go

func main() {
	//инициализируем логгер
	l := newLogger(0, true)
	//получаем переменные для работы приложения
	token, ip, port, _, err := getConfig()
	if err != nil {
		l.Error("не удалось получить токен авторизации!", "code", 404)
		panic(err)
	}
	// получаем acessToken
	acessToken, err := getToken(token)
	if err != nil {
		l.Error("не удалось получить токен доступа!", "code", 404)
		//	panic(err)
	}
	// подключаемся к LLM
	llm, err := getGigaChatLLM(acessToken)
	if err != nil {
		l.Error("не удалось подключиться к ллм модели!", "code", 500)
	}

	// Инициализируем инструменты и шаблоны weather-пакета
	weather.InitTools()
	weather.InitTemplates()

	// Инициализируем MCP сессию для weather инструмента
	ctx := context.Background()
	if err := weather.InitMCPSession(ctx); err != nil {
		l.Warn("не удалось инициализировать MCP weather сессию", "error", err)
		l.Info("weather инструмент будет недоступен")
	} else {
		l.Info("MCP weather сессия успешно инициализирована")
		// Регистрируем закрытие MCP сессии при остановке
		defer func() {
			if err := weather.CloseMCPSession(); err != nil {
				l.Error("ошибка закрытия MCP сессии", "error", err)
			}
		}()
	}

	// Создаём агента
	agentInstance := agent.NewWeatherAgent(llm, l)
	app := fiber.New(fiber.Config{
		AppName:        "llm Service",
		ServerHeader:   "llm Service", // добавляем заголовок для идентификации сервера
		CaseSensitive:  true,          // включаем чувствительность к регистру в URL
		StrictRouting:  true,
		RequestMethods: []string{"POST"}, // включаем строгую маршрутизацию
	})
	api := app.Group("/api") // /api
	//не даем падать нашему сервису при панике
	api.Use(recover.New())
	api.Use(cors.New(cors.Config{
		AllowHeaders: []string{"Origin, Content-Type, Accept, Authorization"},
		AllowMethods: []string{"POST"},
	}))
	api.Use(compress.New(compress.Config{
		Level: compress.LevelBestSpeed, // 1
	}))
	// Передаём агент в роуты
	newLLMRoutes(api, l, agentInstance)
	routes := app.GetRoutes()
	for _, route := range routes {
		fmt.Printf("%s %sn", route.Method, route.Path)
	}
	t := 3 * time.Second

	err = serveServer(app, ip, port, t, l)
	if err != nil {
		l.Error("Server ListenAndServe error")
		panic(err)
	}
}

6. Настройка MCP для weather-инструмента

6.1. Что такое MCP (Model Context Protocol)

MCP — это протокол для подключения внешних инструментов к LLM-приложениям.

Представьте, что ваша LLM — это мозг. MCP — это руки, которые могут:

  • Делать HTTP-запросы к API

  • Читать файлы

  • Выполнять код

  • Работать с базами данных

В нашем случае MCP подключается к weather-серверу, который возвращает прогноз погоды.

6.2. weather/mcp.go — инициализация сессии

Файл: weather/mcp.go

Помимо функции инициализации, которую мы уже рассмотрели выше также используются функции получения и закрытия сессии с mcp сервером.

// getMCPSession возвращает существующую MCP сессию
func getMCPSession() (*mcp.ClientSession, error) {
	mcpSessionMu.Lock()
	defer mcpSessionMu.Unlock()

	if mcpSession == nil {
		return nil, fmt.Errorf("MCP session not initialized. Call InitMCPSession first")
	}

	return mcpSession, nil
}

// CloseMCPSession закрывает MCP сессию (вызывать при остановке приложения)
func CloseMCPSession() error {
	mcpSessionMu.Lock()
	defer mcpSessionMu.Unlock()

	if mcpSession != nil {
		mcpSession.Close()
		mcpSession = nil
	}
	return nil
}

Также у нас есть основная функция получения данных по mcp:

// fetchWeatherViaMCP получает прогноз погоды через MCP сервер
func fetchWeatherViaMCP(ctx context.Context, city string, days int) ([]DailyForecast, error) {
	session, err := getMCPSession()
	if err != nil {
		return nil, fmt.Errorf("failed to get MCP session: %w", err)
	}

	// Сначала ищем координаты города через search_location
	searchParams := &mcp.CallToolParams{
		Name: "search_location",
		Arguments: map[string]any{
			"query": city,
			"limit": 1,
		},
	}

	searchResult, err := session.CallTool(ctx, searchParams)
	if err != nil {
		return nil, fmt.Errorf("search_location failed: %w", err)
	}

	if searchResult.IsError {
		return nil, fmt.Errorf("search_location returned error: %v", searchResult)
	}

	// Парсим результат поиска из Markdown формата
	lat, lon := parseLocationFromMarkdown(searchResult)

	if lat == 0 && lon == 0 {
		return nil, fmt.Errorf("city '%s' not found", city)
	}

	// Получаем прогноз через get_forecast
	forecastParams := &mcp.CallToolParams{
		Name: "get_forecast",
		Arguments: map[string]any{
			"latitude":    lat,
			"longitude":   lon,
			"days":        days,
			"granularity": "daily",
		},
	}

	forecastResult, err := session.CallTool(ctx, forecastParams)
	if err != nil {
		return nil, fmt.Errorf("get_forecast failed: %w", err)
	}

	if forecastResult.IsError {
		return nil, fmt.Errorf("get_forecast returned error: %v", forecastResult)
	}

	// Парсим результат прогноза
	return parseForecastResult(forecastResult, days)
}

7. Примеры использования

7.1. Тестирование через curl

Базовый запрос:

curl -X POST http://localhost:8080/api/weather/process 
  -H "Content-Type: application/json" 
  -d '{"prompt": "Какая погода в Москве на 3 дня?"}'

7.2. Тестирование через Bruno

Шаг 1: Устанавливаем Bruno (open-source альтернатива Postman).

Шаг 2:

Создаём новую коллекцию GigaChat Weather API

Создаём новую коллекцию GigaChat Weather API

Шаг 3:

Добавляем запрос

Добавляем запрос

Шаг 4:

Отправляем запрос и видим ответ

Отправляем запрос и видим ответ

7.3. Ожидаемый ответ

{"message":"Запрос успешно обработан","response":"### Прогноз погоды на неделю в Новосибирскеnn**Город:** Новосибирск  nn**Понедельник, 29 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +3°C  nСкорость ветра: до 26 м/с  nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.nn**Вторник, 30 апреля 2026 года:**  nПогода: облачная  nТемпература воздуха: около +4°C  nСкорость ветра: до 18 м/с  nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.nn**Среда, 1 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +7°C  nСкорость ветра: до 21 м/с  nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.nn**Четверг, 2 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.nn**Пятница, 3 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +9°C  nСкорость ветра: до 24 м/с  nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.nn**Суббота, 4 мая 2026 года:**  nПогода: облачная  nТемпература воздуха: около +8°C  nСкорость ветра: до 26 м/с  nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.","success":true}

8. Итоги

8.1. Что было сделано

Реализована полная интеграция GigaChat + LangChainGo

  • Функция получения модели для интерфейса llms.Model через openai.LLM

  • Работа с цепочками (chains)

  • Вызов функции агента

Создан рабочий HTTP-сервер

Добавлена поддержка MCP

  • Подключение внешних инструментов

8.2. Что можно улучшить

  • Добавить другие инструменты

  • Реализовать кэширование ответов

  • Поддержать streaming ответов

  • Добавить память и историю чатов

Нужно сказать, что согласно GigaChat API также возможна реализация этого функционала по протоколу GRPC.

9. Заключение

Доклад Антона Юрченко дал отличную идею, но не хватало практического кода. Этот проект — попытка восполнить этот пробел.

Главный вывод: интеграция GigaChat с LangChainGo — это не так страшно, как кажется. Этот код — готовая основа для pet-проектов. Берите, модифицируйте, добавляйте свои инструменты. Буду рад обсудить ваш опыт с созданием агентов.

Приложения

А. Полезные ссылки

В. Исходный код

Репозиторий: https://github.com/art9276/weather-agent-mcp

Автор: artemkaVlg

Источник