От LLM к агенту: Как заставить Go приложение думать и действовать
- вторник, 12 мая 2026 г. в 00:00:17
От автора: Эта статья родилась из желания разобраться в том, что осталось за кадром отличного доклада.
Всё началось с доклада Антона Юрченко «Улучшаем качество отчётов нагрузочного тестирования с помощью Go, LangChain и GigaChat».
Доклад мне понравился: чёткая постановка проблемы, грамотный подход к автоматизации, отличная идея с использованием LLM для генерации человекопонятных отчётов. Но после просмотра осталась одна проблема — код интеграции так и не показали.
Было сказано лишь, что нужно «реализовать интерфейс» для подключения GigaChat к LangChain. Звучит просто, но когда ты открываешь документацию LangChainGo, которая к слову еще написана только наполовину и пытаешься понять, с чего начать — возник вопрос: Какой именно интерфейс реализовывать? Далее по изучению документации возникли и другие:
Что такое функции в LLM и как их реализовывать?
Как связать это всё с цепочками (chains) и зачем они вообще нужны?
Что такое шаблон запроса и нужно ли мне им пользоваться?
Так появился этот pet-проект. Я решил сам разобраться и создать рабочий пример, который можно потрогать, запустить и модифицировать.
Показать рабочий код интеграции GigaChat с LangChainGo на Go. В нём я хочу реализовать приложение которое указано в примерах как библиотеки, так и документации GigaChat, а именно сервис по определению погоды, но сделать не просто запрос к модели, которая отдаст мне возможно галлюцинации, а возможно и правильные данные. Создать и использовать агента, который будет из запроса пользователя получать город и количество дней для прогноза, передавать получать реальный прогноз погоды с помощью mcp-сервера ,а затем уже имея все необходимые данные формировать прогноз и давать совет по одежде, которую одеть на улицу.
Для создания агента мы используем библиотеку github.com/tmc/langchaingo, ещё в работе агента нам понадобится:
mcp сервер погоды
Библиотека для его подключения github.com/modelcontextprotocol/go-sdk/mcp
GigaChat - непосредственно сама модель
Node.js - для работы mcp сервера погоды.
Для доступа к GigaChat API нам нужно создать аккаунт в Sber Developers и создать там свой проект.
Шаг 1:

Шаг 2:

Шаг 3:

Шаг 4:

Шаг 5:

Для начала инициализируем проект weather-agent
go mod init weather-agent
Cтруктура нашего проекта:

Немного разобравшись в документации langchaingo я узнал, что в основном любой агент состоит из нескольких компонентов:
Модель, которая будет основой агента (Model)
Функции которые она умеет выполнять (Tools)
Память модели (Brain)
Цепочки которые используются для определения последовательности действий агента (Chain)
Это стандартные компоненты агента и их можно комбинировать, а чтобы агент был более детерминирован, мы как и в обыкновенной программе описываем ему последовательность действий, которая называется цепочка. Составляя эти цепочки мы определяем порядок работы агента, а также можем делать возврат к необходимой нам части цепочки при сбое, или повторению отдельного ее элемента. Я решил не добавлять память в своего агента, так как это не особо требуется при получении прогноза погоды, но при желании можно просто добавить элемент памяти если вам это будет необходимо.

GigaChatLLM - Реализация интерфейса llms.Model для GigaChat, Agent - Координатор инструментов и исполнитель цепочек (WeatherAgent), Chains - Цепочка запросов, Tools - Внешние функции (weather через MCP), MCP Client - Клиент для подключения к weather-серверу
Разберём полный путь запроса на примере: «Какая погода в Москве на 3 дня?»
POST http://localhost:8080/api/weather/process Content-Type: application/json { "prompt": "Какая погода в Новосибирске на 7 дней?" }
Файл: 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": "Запрос успешно обработан", }) }
Файл: 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 }
Файл: 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 }
Задача: Извлечь город и количество дней из текстового запроса.
Здесь и далее мы будем использовать шаблоны, на основании которых и создаются наши промпты к модели добавляя в них необходимые нам данные
Файл: 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}
Задача: Распарсить 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
Задача: Вызвать 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) }
Задача: Сформировать человекочитаемый ответ с рекомендациями.
// createFinalResponseChain создаёт цепочку для генерации финального ответа. func createFinalResponseChain(llm llms.Model) *chains.LLMChain { // Создаём LLM-цепочку с шаблоном FinalWeatherPromptTemplate chain := chains.NewLLMChain( llm, FinalWeatherPromptTemplate, ) // Устанавливаем имя выходного ключа chain.OutputKey = "text" return chain }
После прохождения всей цепочки модель возвращает свой ответ:
### Прогноз погоды на неделю в Новосибирске\n\n**Город:** Новосибирск \n\n**Понедельник, 29 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +3°C \nСкорость ветра: до 26 м/с \nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.\n\n**Вторник, 30 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +4°C \nСкорость ветра: до 18 м/с \nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.\n\n**Среда, 1 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +7°C \nСкорость ветра: до 21 м/с \nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.\n\n**Четверг, 2 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.\n\n**Пятница, 3 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +9°C \nСкорость ветра: до 24 м/с \nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.\n\n**Суббота, 4 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.
Далее идет возврат ответа клиенту в виде JSON
{"message":"Запрос успешно обработан","response":"### Прогноз погоды на неделю в Новосибирске\n\n**Город:** Новосибирск \n\n**Понедельник, 29 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +3°C \nСкорость ветра: до 26 м/с \nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.\n\n**Вторник, 30 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +4°C \nСкорость ветра: до 18 м/с \nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.\n\n**Среда, 1 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +7°C \nСкорость ветра: до 21 м/с \nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.\n\n**Четверг, 2 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.\n\n**Пятница, 3 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +9°C \nСкорость ветра: до 24 м/с \nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.\n\n**Суббота, 4 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.","success":true}
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.
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 }
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"` }
Собираем и запускаем наш сервис
Файл: 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 %s\n", 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) } }
MCP — это протокол для подключения внешних инструментов к LLM-приложениям.
Представьте, что ваша LLM — это мозг. MCP — это руки, которые могут:
Делать HTTP-запросы к API
Читать файлы
Выполнять код
Работать с базами данных
В нашем случае MCP подключается к weather-серверу, который возвращает прогноз погоды.
Файл: 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) }
Базовый запрос:
curl -X POST http://localhost:8080/api/weather/process \ -H "Content-Type: application/json" \ -d '{"prompt": "Какая погода в Москве на 3 дня?"}'
Шаг 1: Устанавливаем Bruno (open-source альтернатива Postman).
Шаг 2:

Шаг 3:

Шаг 4:

{"message":"Запрос успешно обработан","response":"### Прогноз погоды на неделю в Новосибирске\n\n**Город:** Новосибирск \n\n**Понедельник, 29 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +3°C \nСкорость ветра: до 26 м/с \nРекомендации: Возьмите теплый свитер или куртку, ветер сильный, возможен дискомфорт от холода.\n\n**Вторник, 30 апреля 2026 года:** \nПогода: облачная \nТемпература воздуха: около +4°C \nСкорость ветра: до 18 м/с \nРекомендации: Легкая куртка будет достаточно, ветрено, лучше одеться теплее.\n\n**Среда, 1 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +7°C \nСкорость ветра: до 21 м/с \nРекомендации: Теплая одежда обязательна, ветер ощутимый, одевайтесь тепло.\n\n**Четверг, 2 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Наденьте легкую куртку или ветровку, температура комфортная, но ветер сильный.\n\n**Пятница, 3 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +9°C \nСкорость ветра: до 24 м/с \nРекомендации: Одевайтесь легко, ветер умеренный, легкая верхняя одежда подойдет.\n\n**Суббота, 4 мая 2026 года:** \nПогода: облачная \nТемпература воздуха: около +8°C \nСкорость ветра: до 26 м/с \nРекомендации: Берите теплую одежду, погода прохладная, ветер порывистый.","success":true}
Реализована полная интеграция GigaChat + LangChainGo
Функция получения модели для интерфейса llms.Model через openai.LLM
Работа с цепочками (chains)
Вызов функции агента
Создан рабочий HTTP-сервер
Добавлена поддержка MCP
Подключение внешних инструментов
Добавить другие инструменты
Реализовать кэширование ответов
Поддержать streaming ответов
Добавить память и историю чатов
Нужно сказать, что согласно GigaChat API также возможна реализация этого функционала по протоколу GRPC.
Доклад Антона Юрченко дал отличную идею, но не хватало практического кода. Этот проект — попытка восполнить этот пробел.
Главный вывод: интеграция GigaChat с LangChainGo — это не так страшно, как кажется. Этот код — готовая основа для pet-проектов. Берите, модифицируйте, добавляйте свои инструменты. Буду рад обсудить ваш опыт с созданием агентов.
LangChainGo Docs: https://github.com/tmc/langchaingo
GigaChat API: https://developers.sber.ru/docs/ru/gigachat
MCP Protocol: https://modelcontextprotocol.io/
Fiber v3: https://docs.gofiber.io/
Bruno: https://www.usebruno.com/
Репозиторий: https://github.com/art9276/weather-agent-mcp