一、背景:为什么我们需要“可推理”的 Agent

在很多 AI 应用的早期阶段,我们通常采用一种非常直接的方式:

用户问题 → LLM → 返回答案

比如ollama模型调用,或者直接调用模型生成接口。但只要进入稍微复杂一点的业务场景,这种方式很快就会遇到瓶颈,例如:

  • 用户问题信息不完整(没给城市,却问天气)
  • 答案需要依赖外部系统(天气 API、数据库、内部服务)
  • 需要多步决策(先判断,再查询,再组合)

此时,我们真正需要的并不是一个“聊天机器人”,而是一个 具备推理与行动能力的 AI Agent

本文将基于 CloudWeGo Eino 框架,使用 ReAct Agent 模式,实现一个:

能够自动判断是否需要工具、并通过多步推理完成任务的天气查询 Agent


二、什么是 ReAct Agent

ReAct(Reason + Act)是一种经典的 Agent 设计模式,其核心思想是:

推理(Reason)和行动(Act)交替进行

一个典型的 ReAct Agent 执行流程如下:

ReAct Agent 执行流程

这与传统的「一次 LLM 调用」有本质区别。


三、为什么选择 Eino

Eino 是 CloudWeGo 生态下的 AI 应用框架,面向 工程化 AI Agent 开发,具备以下特点:

  • 原生支持 Tool Calling
  • 提供 ReAct Agent Loop
  • Tool 使用 强类型 Schema
  • 对 Go 开发者非常友好
  • 易于和现有后端系统(IM、RPC、HTTP)集成

在 Go 生态中,这是一个非常适合做 生产级 Agent 的框架。


四、Agent 目标设计

本文实现的 Weather Agent 目标非常明确:

用户输入示例

我这边今天天气怎么样?有什么穿衣建议吗?

Agent 行为逻辑

  1. 判断用户是否明确给出了城市
  2. 如果没有 → 调用定位工具
  3. 查询指定城市天气
  4. 根据天气给出穿衣 / 出行建议
  5. 输出自然语言回答

这是一个典型的顺序依赖型 Agent 场景

答案示例

答案


五、整体架构设计

整体结构可以抽象为:

整体架构

关键点在于:
LLM 不只是生成文本,而是 Agent 的“决策中枢”


六、三种 Tool 封装方式(工程实战重点)

在这个 Demo 中,我们刻意使用了 三种不同的 Tool 封装方式,这是 Eino 非常有价值的一点。

为了方便Demo的实现,Tool中的功能我们不去真正的实现,而是模拟一个返回^ - ^


1. 使用 utils.NewTool:定位城市

return utils.NewTool(info, func(ctx context.Context, _ *LocateCityParams) (string, error) {
	return `{"city":"北京"}`, nil
})

特点:

  • 手动定义 Tool Schema
  • 参数、返回值完全可控
  • 适合核心基础能力(定位、鉴权、用户信息)

2. 使用 utils.InferTool:查询天气

queryWeatherTool, _ := utils.InferTool(
	"query_weather",
	"Query weather by city name",
	QueryWeatherFunc,
)

特点:

  • 直接基于 Go 函数
  • 自动推断参数 Schema
  • 业务代码最简洁

非常适合已有服务函数快速接入 Agent


3. 实现 InvokableTool 接口:天气建议

type WeatherAdviceTool struct{}

特点:

  • Tool 内部可维护复杂逻辑
  • 可接数据库、配置中心、缓存
  • 企业级项目中最常见

小结

Eino 允许在同一个 Agent 中混用多种 Tool 形式,这是非常工程化的设计。


七、为什么必须使用 ReAct Agent,而不是普通 Chain

在第一次使用 Eino 时最容易踩的坑,直接使用官方教程的Chain方法,导致出现的结果是不正常的。

普通 Chain 的执行模式

LLM → Tool → 结束

这种模式下:

  • Tool 只会执行一次
  • Tool 结果不会触发新的推理
  • 不适合顺序依赖场景

ReAct Agent 的执行模式

LLM → Tool → LLM → Tool → LLM → Answer

这个天气场景:

定位 → 查天气 → 给建议

本质上就必须是 ReAct Agent。


八、System Prompt:Agent 的“行为协议”

在 ReAct Agent 中,System Prompt 的作用远大于普通对话场景。

示例:

你是一个天气助手。
当用户询问天气或穿衣建议时:
1. 如果不知道城市,先调用 locate_city
2. 拿到城市后,调用 query_weather
3. 拿到天气后,调用 weather_advice
4. 最后用自然语言回复用户,不要再调用工具

这段 Prompt 实际上是:

Agent 的执行契约(Execution Contract)

没有它,Agent 只是一个“随便试试工具的 LLM”。


九、Agent 执行效果说明

当用户输入:

今天天气怎么样?有什么穿衣建议吗?

Agent 的行为大致为:

  1. 调用locate_city → 根据Ip获取定位
  2. 调用 query_weather(city=北京)
  3. 调用 weather_advice
  4. 汇总信息
  5. 输出最终回答

整个过程无需手写任何循环逻辑。


十、总结与思考

通过这个 Demo,我们可以得出几个非常重要的结论:

  1. Agent ≠ Chatbot
  2. Chain ≠ Agent Loop
  3. Tool 是 Agent 的“能力扩展”,不是普通函数
  4. System Prompt 是 Agent 行为的核心控制器

Eino 提供的 ReAct Agent 能力,使得在 Go 生态中构建 工程级 AI Agent 成为一件现实且可控的事情。

代码附录

附Demo代码参考

package main

import (
	"context"
	"github.com/cloudwego/eino-ext/components/model/openai"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/components/tool/utils"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/flow/agent/react"
	"github.com/cloudwego/eino/schema"
	"go.uber.org/zap"
)

var logger *zap.Logger

func init() {
	var err error
	logger, err = initLogger()
	if err != nil {
		panic(err)
	}
}

func main() {
	openAIAPIKey := os.Getenv("OPENAI_API_KEY")
	openAIModelName := os.Getenv("OPENAI_MODEL_NAME")
	openAIBaseURL := os.Getenv("OPENAI_BASE_URL")

	ctx := context.Background()

	queryWeatherTool, err := utils.InferTool(
		"query_weather",
		"Query weather by city name",
		QueryWeatherFunc,
	)
	if err != nil {
		logger.Error("InferTool failed!", zap.Error(err))
		return
	}

	// 初始化 tools
	baseTools := []tool.BaseTool{
		getLocateCityTool(),  // 使用 NewTool 方式
		queryWeatherTool,     // 使用 InferTool 方式
		&WeatherAdviceTool{}, // 使用结构体实现方式, 此处未实现底层逻辑
	}
	weatherTools := compose.ToolsNodeConfig{
		Tools: baseTools,
	}

	temp := float32(0.7)
	// 创建并配置 ChatModel
	chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
		BaseURL:     openAIBaseURL,
		Model:       openAIModelName,
		APIKey:      openAIAPIKey,
		Temperature: &temp,
	})
	if err != nil {
		logger.Error("NewChatModel failed!", zap.Error(err))
		return
	}

	// 构建完整的处理链
	agent, err := react.NewAgent(ctx, &react.AgentConfig{
		ToolCallingModel: chatModel,
		ToolsConfig:      weatherTools,
		MaxStep:          10,
	})
	if err != nil {
		logger.Error("NewAgent failed!", zap.Error(err))

	}

	systemMsg := &schema.Message{
		Role: schema.System,
		Content: `
你是一个天气助手。
当用户询问天气或穿衣建议时:
1. 如果不知道城市,先调用 locate_city
2. 拿到城市后,调用 query_weather
3. 拿到天气后,调用 weather_advice
4. 最后用自然语言回复用户,不要再调用工具
`,
	}

	// 运行示例
	resp, err := agent.Generate(ctx, []*schema.Message{
		systemMsg,
		{
			Role:    schema.User,
			Content: "今天天气怎么样?有什么穿衣建议吗?",
		},
	})
	if err != nil {
		logger.Error("agent.Invoke failed", zap.Error(err))
		return
	}

	// 输出结果

	logger.Sugar().Infof("message: %s: %s", resp.Role, resp.Content)

}

// getLocateCityTool  获取当前用户的位置
func getLocateCityTool() tool.InvokableTool {
	info := &schema.ToolInfo{
		Name: "locate_city",
		Desc: "Locate user's current city",
		ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"ip": {
				Desc: "user ip address",
				Type: schema.String,
			},
		}),
	}

	return utils.NewTool(info, func(ctx context.Context, _ *LocateCityParams) (string, error) {
		logger.Info("invoke locate city")
		// Mock 实现
		return `{"city":"北京"}`, nil
	})
}

// WeatherAdviceTool
// 获取天气的建议
// 自行实现 InvokableTool 接口
type WeatherAdviceTool struct{}

func (w *WeatherAdviceTool) Info(_ context.Context) (*schema.ToolInfo, error) {
	return &schema.ToolInfo{
		Name: "weather_advice",
		Desc: "Give clothing and travel advice based on weather",
		ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
			"weather": {
				Type:     schema.String,
				Required: true,
			},
			"temp": {
				Type:     schema.String,
				Required: true,
			},
		}),
	}, nil
}

func (w *WeatherAdviceTool) InvokableRun(
	_ context.Context,
	args string,
	_ ...tool.Option,
) (string, error) {

	logger.Sugar().Infof("invoke weather_advice: %s", args)

	return `{"advice":"天气晴朗,气温适中,适合外出,建议穿短袖"}`, nil
}

func QueryWeatherFunc(_ context.Context, params *WeatherQueryParams) (string, error) {
	logger.Info("invoke QueryWeatherFunc")
	if params.City == "北京" {
		return `{"city":"北京","weather":"晴","temp":"26℃","wind":"微风"}`, nil
	}
	return `{"city":"` + params.City + `","weather":"未知"}`, nil
}

// -------- Tool Params --------

type WeatherQueryParams struct {
	City string `json:"city" jsonschema_description:"city name"`
}

type LocateCityParams struct {
	IP *string `json:"ip,omitempty" jsonschema_description:"user ip address"`
}

func initLogger() (*zap.Logger, error) {
	return zap.NewDevelopment()
}


参考文章

Agent-让大模型拥有双手
什么是AI Agent?AI Agent综述,看这一篇就够了!
带你用Eino实现Agent与RAG
深入解析CloudWeGo Eino项目中React Agent的并发工具调用问题

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐