- BrainTools - https://www.braintools.ru -
От автора: Эта статья родилась из желания разобраться в том, что осталось за кадром отличного доклада.
Всё началось с доклада Антона Юрченко «Улучшаем качество отчётов нагрузочного тестирования с помощью Go, LangChain и GigaChat» [1].
Доклад мне понравился: чёткая постановка проблемы, грамотный подход к автоматизации, отличная идея с использованием LLM для генерации человекопонятных отчётов. Но после просмотра осталась одна проблема — код интеграции так и не показали.
Было сказано лишь, что нужно «реализовать интерфейс» для подключения GigaChat к LangChain. Звучит просто, но когда ты открываешь документацию LangChainGo, которая к слову еще написана только наполовину и пытаешься понять, с чего начать — возник вопрос: Какой именно интерфейс реализовывать? Далее по изучению документации возникли и другие:
Что такое функции в LLM и как их реализовывать?
Как связать это всё с цепочками (chains) и зачем они вообще нужны?
Что такое шаблон запроса и нужно ли мне им пользоваться?
Так появился этот pet-проект. Я решил сам разобраться и создать рабочий пример, который можно потрогать, запустить и модифицировать.
Показать рабочий код интеграции GigaChat с LangChainGo на Go. В нём я хочу реализовать приложение которое указано в примерах как библиотеки, так и документации GigaChat, а именно сервис по определению погоды, но сделать не просто запрос к модели, которая отдаст мне возможно галлюцинации, а возможно и правильные данные. Создать и использовать агента, который будет из запроса пользователя получать город и количество дней для прогноза, передавать получать реальный прогноз погоды с помощью mcp-сервера ,а затем уже имея все необходимые данные формировать прогноз и давать совет по одежде, которую одеть на улицу.
Для создания агента мы используем библиотеку github.com/tmc/langchaingo [2], ещё в работе агента нам понадобится:
mcp сервер погоды
Библиотека для его подключения github.com/modelcontextprotocol/go-sdk/mcp [3]
GigaChat – непосредственно сама модель
Node.js – для работы mcp сервера погоды.
Для доступа к GigaChat API нам нужно создать аккаунт в Sber Developers и создать там свой проект.
Шаг 1:
Шаг 2:
Шаг 3:
Шаг 4:
Шаг 5:
Для начала инициализируем проект weather-agent
go mod init weather-agent
Cтруктура нашего проекта:
Немного разобравшись в документации langchaingo я узнал, что в основном любой агент состоит из нескольких компонентов:
Модель, которая будет основой агента (Model)
Функции которые она умеет выполнять (Tools)
Память [4] модели (Brain)
Цепочки которые используются для определения последовательности действий агента (Chain)
Это стандартные компоненты агента и их можно комбинировать, а чтобы агент был более детерминирован, мы как и в обыкновенной программе описываем ему последовательность действий, которая называется цепочка. Составляя эти цепочки мы определяем порядок работы агента, а также можем делать возврат к необходимой нам части цепочки при сбое, или повторению [5] отдельного ее элемента. Я решил не добавлять память в своего агента, так как это не особо требуется при получении прогноза погоды, но при желании можно просто добавить элемент памяти если вам это будет необходимо.
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
}
После прохождения всей цепочки модель возвращает свой ответ:
### Прогноз погоды на неделю в Новосибирске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}
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 %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)
}
}
MCP — это протокол для подключения внешних инструментов к LLM-приложениям.
Представьте, что ваша LLM — это мозг [6]. 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 [7] (open-source альтернатива Postman).
Шаг 2:
Шаг 3:
Шаг 4:
{"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}
Реализована полная интеграция GigaChat + LangChainGo
Функция получения модели для интерфейса llms.Model через openai.LLM
Работа с цепочками (chains)
Вызов функции агента
Создан рабочий HTTP-сервер
Добавлена поддержка MCP
Подключение внешних инструментов
Добавить другие инструменты
Реализовать кэширование ответов
Поддержать streaming ответов
Добавить память и историю чатов
Нужно сказать, что согласно GigaChat API также возможна реализация этого функционала по протоколу GRPC.
Доклад Антона Юрченко дал отличную идею, но не хватало практического кода. Этот проект — попытка восполнить этот пробел.
Главный вывод: интеграция GigaChat с LangChainGo — это не так страшно, как кажется. Этот код — готовая основа для pet-проектов. Берите, модифицируйте, добавляйте свои инструменты. Буду рад обсудить ваш опыт [8] с созданием агентов.
LangChainGo Docs: https://github.com/tmc/langchaingo [2]
GigaChat API: https://developers.sber.ru/docs/ru/gigachat [9]
MCP Protocol: https://modelcontextprotocol.io/ [10]
Fiber v3: https://docs.gofiber.io/ [11]
Bruno: https://www.usebruno.com/ [7]
Репозиторий: https://github.com/art9276/weather-agent-mcp [12]
Автор: artemkaVlg
Источник [13]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/30075
URLs in this post:
[1] «Улучшаем качество отчётов нагрузочного тестирования с помощью Go, LangChain и GigaChat»: https://golangmeetup2025.dialog.sberbank.ru/translation
[2] github.com/tmc/langchaingo: https://github.com/tmc/langchaingo
[3] github.com/modelcontextprotocol/go-sdk/mcp: https://github.com/modelcontextprotocol/go-sdk/mcp
[4] Память: http://www.braintools.ru/article/4140
[5] повторению: http://www.braintools.ru/article/4012
[6] мозг: http://www.braintools.ru/parts-of-the-brain
[7] Bruno: https://www.usebruno.com/
[8] опыт: http://www.braintools.ru/article/6952
[9] https://developers.sber.ru/docs/ru/gigachat: https://developers.sber.ru/docs/ru/gigachat
[10] https://modelcontextprotocol.io/: https://modelcontextprotocol.io/
[11] https://docs.gofiber.io/: https://docs.gofiber.io/
[12] https://github.com/art9276/weather-agent-mcp: https://github.com/art9276/weather-agent-mcp
[13] Источник: https://habr.com/ru/articles/1033718/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1033718
Нажмите здесь для печати.