台中市网站建设_网站建设公司_Node.js_seo优化
2026/1/2 17:06:08 网站建设 项目流程

【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)

引言

专题背景:ReAct Agent 作为 AI Agent 落地的核心模式,是打通“思考 - 行动”闭环的关键,但主流框架(LangChain/LangGraph)的高度封装让初学者难以理解底层逻辑。

设计模式的核心定位:ReAct 模式的本质是“思考→行动→观察→反馈”的循环,脱离框架从零实现,是理解其核心原理的最佳路径。

技术选型思考:选择 Golang 作为实现语言,兼顾高性能、并发安全与工程化落地特性,贴合 AIOPS 等运维场景的生产需求

本文价值:针对初学者,拆解 ReAct Agent 实现的 3 大核心步骤,用 Golang 逐行实现,讲清 “为什么这么写”,“怎么解决问题”,让新手彻底掌握 ReAct 底层逻辑。

        在 AI Agent 开发中,LangChain、LangGraph 等框架确实能快速实现 ReAct 模式,但就像“只会用封装好的函数却不懂底层算法”一样,过度依赖框架会让开发者失去对 Agent 核心逻辑的掌控。本文将完全脱离第三方 AI 开发框架,用纯 Golang 实现一个可运行的 ReAct Agent,从提示词设计、工具封装到多轮对话闭环,每一步都讲透细节,让初学者不仅“会写”,更“懂原理”。

一、ReAct Agent 核心原理(Go SRE友好)

在动手编码前,先花 5 分钟理清 ReAct Agent 的核心逻辑,避免“写代码却不知为何而写”:

ReAct Agent 的核心是 4 步循环:

    1. 思考(Reason):Agent 接收用户需求,分析“是否需要调用工具?调用哪个工具?需要什么参数?”;
    2. 行动(Act):执行工具调用,获取外部数据 / 执行操作;
    3. 观察(Observe):解析工具返回的结果(成功 / 失败、数据 / 错误);
    4. 反馈(Feedback):根据观察结果修正思考,决定“继续调用工具 / 更换工具 / 直接回答用户”。

对初学者来说,实现 ReAct Agent 的核心难点在于:

    • 如何让 LLM 稳定输出 “思考 + 工具调用” 的结构化内容,而非杂乱的自然语言;
    • 如何封装工具,让 Agent 能标准化调用;
    • 如何维护多轮对话的上下文,实现循环闭环。

本文将围绕这 3 个难点,用 Golang 逐一解决。

二、环境准备

2.1、基础依赖

我们需要用到 Golang 的基础库和一个 LLM 调用的基础客户端(以 OpenAI API 为例,新手可直接复用):

// 初始化项目
mkdir react-agent-demo && cd react-agent-demo
go mod init react-agent-demo// 安装必要依赖(仅基础 HTTP 客户端,无 AI 框架)
go get github.com/go-resty/resty/v2 // 简化 HTTP 请求(工具调用/LLM 调用)
go get github.com/tidwall/gjson     // 简化 JSON 解析(LLM 输出/工具结果解析)

2.2、配置 LLM 密钥

创建 config.go,存放 LLM 调用的基础配置(以 OpenAI GPT-3.5 Turbo 为例,新手可替换为国内模型如通义千问): 

2.2.1、用 resty 实现

在这个 React Agent Demo 中选择 go-resty/resty/v2 而非 Go 标准库 net/http,核心原因是简化 HTTP 调用的开发成本,尤其适配 LLM / 工具调用场景的高频需求。

package mainimport "github.com/go-resty/resty/v2"// 全局配置
var (OpenAIAPIKey = "你的 OpenAI API Key" // 替换为自己的密钥OpenAIAPIURL = "https://api.openai.com/v1/chat/completions"Client       = resty.New() // 全局 HTTP 客户端
)// 调用 LLM 的基础函数(新手重点理解:这是 Agent 与“大脑”的通信通道)
func callLLM(prompt string) (string, error) {// 构造 OpenAI API 请求体reqBody := map[string]interface{}{"model": "gpt-3.5-turbo","messages": []map[string]string{{"role": "user", "content": prompt},},"temperature": 0.1, // 低温度保证输出稳定(工具调用需结构化,避免随机性)}// 发送请求resp, err := Client.R().SetAuthToken(OpenAIAPIKey).SetHeader("Content-Type", "application/json").SetBody(reqBody).Post(OpenAIAPIURL)if err != nil {return "", err}// 解析响应(用 gjson 简化 JSON 提取)content := gjson.Get(resp.String(), "choices.0.message.content").String()return content, nil
}

帮助 Go SRE 新手关键解读:  

        • callLLM 是整个 Agent 的“核心通信函数”,负责把我们设计的提示词传给 LLM,再拿回 LLM 的输出;
        • temperature=0.1 是关键:ReAct 需要 LLM 输出结构化的工具调用指令,低温度能减少“幻觉” 和随机输出,避免 LLM 瞎写;
        • 国内用户可替换为通义千问 API,只需修改 OpenAIAPIURL 和请求体格式,核心逻辑不变

2.2.2、再给个用 net/http 写法

net/http 是 Go 内置的基础 HTTP 库,提供了最底层的 HTTP 协议实现;而 resty 是基于 net/http 封装的高阶 HTTP 客户端,本质上是对 net/http 的 “易用性升级”,核心优势是减少样板代码。

package mainimport ("bytes""encoding/json""net/http""io/ioutil"
)func callLLMWithNetHTTP(prompt string) (string, error) {// 1. 构造请求体(手动序列化 JSON)reqBody := map[string]interface{}{"model": "gpt-3.5-turbo","messages": []map[string]string{{"role": "user", "content": prompt}},"temperature": 0.1,}jsonBody, err := json.Marshal(reqBody)if err != nil {return "", err}// 2. 创建请求(手动设置 Header、Auth)req, err := http.NewRequest("POST", OpenAIAPIURL, bytes.NewBuffer(jsonBody))if err != nil {return "", err}req.Header.Set("Content-Type", "application/json")req.Header.Set("Authorization", "Bearer "+OpenAIAPIKey)// 3. 发送请求(需手动创建 Client)client := &http.Client{}resp, err := client.Do(req)if err != nil {return "", err}defer resp.Body.Close()// 4. 解析响应(手动读取 Body、反序列化)body, err := ioutil.ReadAll(resp.Body)if err != nil {return "", err}// 还要手动解析 JSON(比如用 encoding/json)var result map[string]interface{}if err := json.Unmarshal(body, &result); err != nil {return "", err}content := result["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"].(string)return content, nil
}

2.2.3、resty 适配 LLM/Agent 场景的核心优势

在 Agent 开发中,高频需要调用 HTTP 接口(LLM API、工具 API 如天气 / 地图),resty 针对这些场景做了针对性优化:

特性resty 优势net/http 不足
链式调用 一行完成「设置 Header/Auth/Body + 发送请求」,代码简洁易读 需分步创建请求、设置参数、发送请求,样板代码多
自动 JSON 序列化 SetBody 自动将 map / 结构体转为 JSON,无需手动 json.Marshal 需手动序列化 JSON,处理字节缓冲区
简化认证 SetAuthToken 一键设置 Bearer Token,还支持 Basic Auth、API Key 等 需手动拼接 Authorization Header
内置重试 / 超时 支持 SetRetryCount/SetTimeout 一键配置(Agent 调用 LLM 需容错) 需手动实现重试、超时逻辑
响应处理 内置 resp.StatusCode()/resp.String() 等便捷方法,无需手动读取 Body 需手动关闭 Body、读取字节流
兼容性 完全兼容 net/http(底层复用 http.Client),可自定义 Transport 等 无上层封装,所有逻辑需手动实现

2.2.4、什么时候该用 net/http?

resty 不是 “银弹”,以下场景优先用原生 net/http:

    • 极致性能 / 资源敏感场景:resty 是封装层,有微小的性能开销(大部分场景可忽略),如果是高性能网关 / 核心服务,可考虑原生;
    • 极简依赖:如果项目要求“零第三方依赖”,避免引入 resty;
    • 复杂定制化:比如需要深度定制 HTTP Transport(如自定义连接池、代理、TLS 配置),原生 net/http 更灵活(但 resty 也支持自定义 http.Client)。

三、步骤 1:构建高质量的 ReAct 提示词模板(LangChain Hub 适配版)

LangChain Hub 提供了官方的 ReAct 提示词模板,其核心是 “明确思考规则 + 强制工具调用格式”。我们先基于这个模板,设计适合 Golang 解析的提示词,解决 “LLM 输出不规范” 的问题。

3.1、LangChain Hub ReAct 模板核心逻辑

官方模板的核心要素:

      • 告知 LLM 角色(工具调用专家);
      • 明确 ReAct 循环规则(思考→行动→观察→反馈);
      • 定义工具调用的结构化格式(避免自然语言混淆);
      • 提供工具列表和示例,降低 LLM 理解成本。

3.2、Golang 实现提示词模板封装

创建 prompt.go,封装 ReAct 提示词模板,并支持动态注入工具列表和上下文:

package mainimport "fmt"// Tool 定义工具结构体(新手重点:工具的“身份证”,LLM 靠这个识别工具)
type Tool struct {Name        string // 工具名称(如 query_cpu_usage)Description string // 工具描述(LLM 据此判断是否调用)Params      string // 参数说明(如 "date: 日期,格式 YYYY-MM-DD;host: 服务器IP")
}// BuildReActPrompt 构建 ReAct 提示词模板
// 参数:
//   userInput: 用户输入
//   tools: 可用工具列表
//   context: 历史上下文(多轮对话用)
func BuildReActPrompt(userInput string, tools []Tool, context string) string {// 1. 拼接工具列表字符串(让 LLM 知道有哪些工具可用)toolList := "["for i, tool := range tools {toolItem := fmt.Sprintf(`{"name":"%s","description":"%s","params":"%s"}`,tool.Name, tool.Description, tool.Params)if i < len(tools)-1 {toolItem += ","}toolList += toolItem}toolList += "]"// 2. 核心提示词模板(LangChain Hub 适配版,强制 JSON 格式输出)promptTemplate := `你是一个专业的 ReAct Agent,严格按照“思考→行动→观察→反馈”的流程处理任务。
## 规则:
1. 思考阶段:分析用户需求和上下文,判断是否需要调用工具。- 若不需要调用工具:直接用自然语言回答用户问题;- 若需要调用工具:必须输出严格的 JSON 格式(无任何额外文字),格式如下:{"reason":"思考过程(说明为什么调用这个工具)","tool":"工具名称","params":{"参数名":"参数值"}}
2. 行动阶段:仅执行思考阶段指定的工具,参数必须符合工具要求;
3. 观察阶段:接收工具返回结果,作为下一步思考的依据;
4. 反馈阶段:根据工具结果决定是否继续调用工具,或直接回答用户。## 可用工具列表:
%s## 历史上下文(多轮对话):
%s## 用户当前需求:
%s`// 3. 注入变量生成最终提示词prompt := fmt.Sprintf(promptTemplate, toolList, context, userInput)return prompt
}

帮助 Go SRE 新手关键解读:

        • Tool 结构体是“工具的说明书”,LLM 只能通过 Name/Description/Params 理解工具,所以描述必须清晰(比如 “query_cpu_usage:查询服务器 CPU 使用率,参数需包含日期和服务器 IP”);
        • 提示词中强制要求 LLM 输出 JSON 格式的工具调用指令,且无额外文字:这是后续 Golang 解析的关键,避免 LLM 输出“我觉得应该调用 XX 工具,参数是 XXX” 这种自然语言;
        • context 参数用于存储多轮对话的历史(比如上一轮调用工具的结果),让 Agent 能“记住”之前的操作。

3.3、提示词设计的关键优化

      • 格式强制约束:明确写“必须输出严格 JSON,无额外文字”,避免 LLM 加解释性语句(比如“以下是工具调用指令:{...}”);
      • 工具描述具体化:不要写“查询 CPU”,要写“查询指定服务器指定日期的 CPU 使用率,返回百分比数值”,LLM 能更精准判断;
      • 参数格式明确:指定参数类型和格式(如日期 YYYY-MM-DD),减少参数错误。

四、步骤 2:Agent 工具实现逻辑(标准化封装)

工具是 ReAct Agent 的“手脚”,我们需要封装工具的执行逻辑,并设计标准化的调用接口,解决“Agent 如何与外部工具交互”的问题。

4.1、工具实现原则

    1. 单一职责:一个工具只做一件事(如 query_cpu_usage 只查 CPU 使用率);
    2. 标准化输入输出:所有工具接收 map[string]string 类型的参数,返回 string 类型的结果(便于统一处理);
    3. 错误处理:工具内部捕获错误,返回友好的错误信息(如“查询失败:服务器 IP 不存在”)。

4.2、Golang 实现工具封装

创建 tools.go,实现两个示例工具(CPU 使用率查询、内存使用率查询),并封装工具调用入口:

package mainimport ("errors""fmt""time"
)// ToolExecutor 工具执行函数类型(所有工具需遵循此签名)
type ToolExecutor func(params map[string]string) (string, error)// 全局工具注册表:映射工具名称到执行函数(Go SRE 新手重点:Agent 查这个表找到工具执行逻辑)
var ToolRegistry = map[string]ToolExecutor{"query_cpu_usage": QueryCPUUsage,  // CPU 使用率查询工具"query_mem_usage": QueryMemUsage, // 内存使用率查询工具
}// QueryCPUUsage 模拟查询CPU使用率(实际场景可替换为真实的监控API调用)
func QueryCPUUsage(params map[string]string) (string, error) {// 1. 参数校验(新手避坑:工具必须先校验参数,避免无效调用)date, ok := params["date"]if !ok {return "", errors.New("参数缺失:date(格式 YYYY-MM-DD)")}host, ok := params["host"]if !ok {return "", errors.New("参数缺失:host(服务器IP)")}// 2. 模拟执行查询(实际场景替换为调用监控系统API)// 这里用随机数模拟,真实场景可调用 Prometheus/Elasticsearch 等cpuUsage := fmt.Sprintf("%.2f%%", 30.0 + float64(time.Now().Second())%20)result := fmt.Sprintf("服务器 %s %s 的CPU使用率为:%s", host, date, cpuUsage)return result, nil
}// QueryMemUsage 模拟查询内存使用率
func QueryMemUsage(params map[string]string) (string, error) {// 参数校验date, ok := params["date"]if !ok {return "", errors.New("参数缺失:date(格式 YYYY-MM-DD)")}host, ok := params["host"]if !ok {return "", errors.New("参数缺失:host(服务器IP)")}// 模拟执行memUsage := fmt.Sprintf("%.2f%%", 60.0 + float64(time.Now().Second())%15)result := fmt.Sprintf("服务器 %s %s 的内存使用率为:%s", host, date, memUsage)return result, nil
}// ExecuteTool 执行工具(统一入口)
func ExecuteTool(toolName string, params map[string]string) (string, error) {// 1. 检查工具是否存在executor, ok := ToolRegistry[toolName]if !ok {return "", fmt.Errorf("工具不存在:%s", toolName)}// 2. 执行工具并返回结果return executor(params)
}

帮助 Go SRE 新手关键解读:

        • ToolExecutor 是工具执行函数的“统一接口”:所有工具都遵循这个签名,Agent 调用工具时无需关心内部实现,只需传参数即可;
        • ToolRegistry 是“工具注册表”:把工具名称和执行函数绑定,比如 query_cpu_usage 对应 QueryCPUUsage 函数,相当于“工具字典”;
        • 参数校验是必做步骤:LLM 可能会漏传参数(比如忘记传 host),工具内部先校验,避免调用外部 API 时出错;
        • 示例工具是模拟实现,真实场景中可替换为:
          • 调用 Prometheus API 查询服务器指标;
          • 调用 Elasticsearch 查询日志;
          • 调用企业微信 API 发送告警。

五、步骤 3:Agent 多轮对话核心逻辑(ReAct 闭环)

这是 ReAct Agent 的核心,我们需要实现“提示词生成→LLM 调用→工具调用→上下文更新→循环执行”的闭环,解决“多轮工具调用”的问题

5.1、核心逻辑拆解

      • 解析 LLM 输出:判断是直接回答还是工具调用;
      • 工具调用处理:调用工具并获取结果;
      • 上下文更新:把工具调用结果存入上下文,供下一轮思考使用;
      • 循环控制:设置最大循环次数,避免死循环。

5.2、Golang 实现多轮对话闭环

创建 agent.go,实现 ReAct Agent 的核心循环:

package mainimport ("encoding/json""fmt""strings"
)// ToolCall 解析后的工具调用指令(对应 LLM 输出的 JSON)
type ToolCall struct {Reason string            `json:"reason"` // 思考过程Tool   string            `json:"tool"`   // 工具名称Params map[string]string `json:"params"` // 工具参数
}// ParseLLMResponse 解析 LLM 输出(Go SRE 新手重点:把 LLM 的输出转为可执行的结构体)
func ParseLLMResponse(llmOutput string) (string, *ToolCall, error) {// 1. 先判断是否是工具调用(是否包含 JSON 格式)llmOutput = strings.TrimSpace(llmOutput)if strings.HasPrefix(llmOutput, "{") && strings.HasSuffix(llmOutput, "}") {// 2. 解析为 ToolCall 结构体var toolCall ToolCallerr := json.Unmarshal([]byte(llmOutput), &toolCall)if err != nil {return "", nil, fmt.Errorf("解析工具调用指令失败:%v,原始输出:%s", err, llmOutput)}return "", &toolCall, nil}// 3. 不是工具调用,直接返回回答return llmOutput, nil, nil
}// ReActLoop ReAct Agent 核心循环
// 参数:
//   userInput: 用户输入
//   tools: 可用工具列表
//   maxIter: 最大循环次数(避免死循环)
func ReActLoop(userInput string, tools []Tool, maxIter int) (string, error) {// 初始化上下文(存储历史操作和结果)var context string// 核心循环for i := 0; i < maxIter; i++ {fmt.Printf("\n===== 第 %d 轮思考 =====\n", i+1)// 步骤 1:构建 ReAct 提示词prompt := BuildReActPrompt(userInput, tools, context)fmt.Printf("提示词:%s\n", prompt)// 步骤 2:调用 LLM 获取输出llmOutput, err := callLLM(prompt)if err != nil {return "", fmt.Errorf("调用 LLM 失败:%v", err)}fmt.Printf("LLM 输出:%s\n", llmOutput)// 步骤 3:解析 LLM 输出directAnswer, toolCall, err := ParseLLMResponse(llmOutput)if err != nil {return "", err}// 步骤 4:判断是否直接回答if directAnswer != "" {return directAnswer, nil}// 步骤 5:执行工具调用fmt.Printf("执行工具:%s,参数:%v\n", toolCall.Tool, toolCall.Params)toolResult, err := ExecuteTool(toolCall.Tool, toolCall.Params)if err != nil {toolResult = fmt.Sprintf("工具调用失败:%v", err)fmt.Printf("工具调用失败:%v\n", err)} else {fmt.Printf("工具调用结果:%s\n", toolResult)}// 步骤 6:更新上下文(关键:让下一轮思考能用到本轮工具结果)context += fmt.Sprintf("\n第 %d 轮操作:\n- 思考过程:%s\n- 调用工具:%s\n- 工具参数:%v\n- 工具结果:%s",i+1, toolCall.Reason, toolCall.Tool, toolCall.Params, toolResult,)}// 循环次数耗尽return "", fmt.Errorf("达到最大循环次数(%d次),任务未完成", maxIter)
}  

5.3、主函数

创建 main.go,编写代码,让 SRE 新手可以直接运行:

package mainimport "fmt"func main() {// 1. 定义可用工具列表(新手可根据需求扩展)tools := []Tool{{Name:        "query_cpu_usage",Description: "查询指定服务器指定日期的CPU使用率,返回百分比数值",Params:      "date: 日期,格式 YYYY-MM-DD;host: 服务器IP地址",},{Name:        "query_mem_usage",Description: "查询指定服务器指定日期的内存使用率,返回百分比数值",Params:      "date: 日期,格式 YYYY-MM-DD;host: 服务器IP地址",},}// 2. 用户输入(模拟 AIOPS 运维场景)userInput := "查询192.168.1.100服务器2024-10-01的CPU和内存使用率"// 3. 启动 ReAct Agent 循环(最大循环次数 3 次)result, err := ReActLoop(userInput, tools, 3)if err != nil {fmt.Printf("Agent 执行失败:%v\n", err)return}// 4. 输出最终结果fmt.Println("\n===== 最终结果 =====")fmt.Println(result)
}

六、运行与调试

6.1、运行步骤

    1. 替换 config.go 中的 OpenAIAPIKey 为自己的密钥;
    2. 执行 go mod tidy 安装依赖;
    3. 执行 go run .,查看输出
      ===== 第 1 轮思考 =====
      提示词:你是一个专业的 ReAct Agent...
      LLM 输出:{"reason":"用户需要查询192.168.1.100服务器2024-10-01的CPU使用率,需要调用query_cpu_usage工具","tool":"query_cpu_usage","params":{"date":"2024-10-01","host":"192.168.1.100"}}
      执行工具:query_cpu_usage,参数:map[date:2024-10-01 host:192.168.1.100]
      工具调用结果:服务器 192.168.1.100 2024-10-01 的CPU使用率为:35.42%===== 第 2 轮思考 =====
      提示词:你是一个专业的 ReAct Agent...
      LLM 输出:{"reason":"用户还需要查询该服务器的内存使用率,需要调用query_mem_usage工具","tool":"query_mem_usage","params":{"date":"2024-10-01","host":"192.168.1.100"}}
      执行工具:query_mem_usage,参数:map[date:2024-10-01 host:192.168.1.100]
      工具调用结果:服务器 192.168.1.100 2024-10-01 的内存使用率为:68.75%===== 第 3 轮思考 =====
      提示词:你是一个专业的 ReAct Agent...
      LLM 输出:192.168.1.100服务器2024-10-01的CPU使用率为35.42%,内存使用率为68.75%。===== 最终结果 =====
      192.168.1.100服务器2024-10-01的CPU使用率为35.42%,内存使用率为68.75%。

6.2、新手常见问题与解决办法

6.2.1、LLM 输出不规范(不是纯 JSON)

解决:在提示词中强化 “必须输出严格 JSON,无额外文字”,并降低 temperature(如 0.1);
兜底:在 ParseLLMResponse 中增加容错逻辑(如去除多余文字)。

6.2.2、工具调用参数错误

解决:在工具执行函数中增加参数格式校验(如日期格式);
优化:在提示词中增加参数示例(如 "date 示例:2024-10-01")。

6.2.3、循环死循环

解决:设置 maxIter 最大循环次数(建议 3-5 次);
优化:在上下文更新时增加“任务完成判断”(如工具结果满足用户需求则终止循环)。

        脱离框架实现 ReAct Agent,不是为了“重复造轮子”,而是为了理解“轮子的构造”。当你掌握了底层逻辑后,再使用 LangChain/LangGraph 时,就能清楚知道“框架封装了什么”、“如何定制框架的行为”,真正做到“知其然,更知其所以然”。

        未来,ReAct Agent 可结合 Reflexion 模式实现“反思优化”,或结合 ReWOO 模式实现“并行工具调用”,而这些扩展的基础,正是我们今天掌握的 ReAct 核心逻辑。

        无论是 AIOPS 运维、企业服务还是个人助手场景,掌握底层实现,才能让 Agent 真正适配你的业务需求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询