【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
【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 步循环:
对初学者来说,实现 ReAct 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 main
import "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 main
import (
"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 适配版)
https://smith.langchain.com/hub

Go SRE 新手可以自己编写提示词,也可以参考其他开发者贡献到社区的提示词。目前最大的提示词开源平台是 LangChain Hub(https://smith.langchain.com/hub)
LangChain Hub 提供了官方的 ReAct 提示词模板,其核心是 “明确思考规则 + 强制工具调用格式”。我们先基于这个模板,设计适合 Golang 解析的提示词,解决 “LLM 输出不规范” 的问题。
3.1、LangChain Hub ReAct 模板核心逻辑
官方模板的核心要素:
-
-
- 告知 LLM 角色(工具调用专家);
- 明确 ReAct 循环规则(思考→行动→观察→反馈);
- 定义工具调用的结构化格式(避免自然语言混淆);
- 提供工具列表和示例,降低 LLM 理解成本。
-
3.2、SRE/AIOps 场景下优化后的 ReAct 提示词模板(Golang 实现)
针对 SRE/AIOps 运维场景的特殊性(故障排查、指标监控、日志分析、自动化操作等),我们重新设计了更贴合运维场景的 ReAct 提示词模板,重点解决 LLM 幻觉、工具调用不精准、参数格式错误等核心问题。以下是完整的 Golang 实现代码:
package main
import (
"fmt"
"strings"
)
// Tool 定义SRE/AIOps场景的工具结构体(强化运维属性)
type Tool struct {
Name string // 工具名称(如 query_prom_cpu、search_es_logs)
Description string // 工具描述(精准描述运维场景能力)
Params string // 参数说明(严格定义运维参数格式/取值范围)
Example string // 参数示例(降低LLM参数拼接错误概率)
Scope string // 适用范围(如"K8s集群/物理机/云服务器")
}
// BuildSREReActPrompt 构建SRE/AIOps场景的ReAct提示词模板
// 参数:
// userInput: 用户运维需求(如"排查192.168.1.100服务器CPU高的原因")
// tools: 运维工具列表
// context: 历史上下文(故障排查记录/工具调用结果)
// cluster: 集群环境(可选,限定工具适用范围)
func BuildSREReActPrompt(userInput string, tools []Tool, context, cluster string) string {
// 1. 过滤适配当前集群的工具(SRE场景核心:工具与环境匹配)
var filteredTools []Tool
for _, tool := range tools {
if tool.Scope == cluster || tool.Scope == "通用" {
filteredTools = append(filteredTools, tool)
}
}
// 2. 拼接标准化工具列表(结构化展示,降低LLM理解成本)
var toolListBuilder strings.Builder
toolListBuilder.WriteString("### 可用运维工具列表(仅调用以下工具,禁止虚构)\n")
for i, tool := range filteredTools {
toolItem := fmt.Sprintf(`工具%d:
- 名称:%s
- 能力:%s
- 参数要求(必填+格式):%s
- 正确示例:%s
- 适用范围:%s
`, i+1, tool.Name, tool.Description, tool.Params, tool.Example, tool.Scope)
toolListBuilder.WriteString(toolItem)
}
// 3. SRE/AIOps场景专属ReAct提示词模板(核心优化版)
promptTemplate := `# 角色定义
你是一名资深SRE工程师,专注于AIOps领域的故障排查、指标监控、日志分析与自动化操作,严格遵循ReAct思考-行动闭环处理运维需求,**禁止虚构工具/参数/执行结果**。
# ReAct核心流程(SRE场景强化版)
## 1. 思考阶段(必须输出思考过程)
- 分析目标:明确用户的运维需求类型(故障排查/指标查询/日志分析/自动化操作);
- 环境校验:确认当前集群环境为【%s】,仅调用适配该环境的工具;
- 工具选择:仅从下方"可用运维工具列表"中选择工具,禁止使用列表外工具;
- 参数校验:检查工具参数是否符合格式要求(如IP格式、时间范围、集群名称),缺失参数需明确标注;
- 必要性判断:若无需工具即可回答(如基础运维知识),直接输出自然语言结论;若需工具,必须生成JSON调用指令。
## 2. 行动阶段(严格遵循格式)
工具调用指令必须为纯JSON格式(无任何前置/后置文字),格式如下:
{
"reason": "详细思考过程(说明:1.需求分析;2.选择该工具的原因;3.参数合理性校验)",
"tool": "工具名称(必须与列表完全一致)",
"params": {
"参数名1": "参数值(严格匹配格式要求)",
"参数名2": "参数值(严格匹配格式要求)"
},
"risk": "操作风险评估(如:无风险/需确认权限/可能影响业务)"
}
## 3. 观察阶段
接收工具执行结果后,需验证结果有效性:
- 若结果为错误(如参数错误/工具执行失败),分析失败原因并调整工具/参数;
- 若结果为有效数据,结合运维知识分析数据(如CPU高是否由进程占用导致);
- 若结果不完整,判断是否需要调用其他工具补充(如先查CPU指标,再查对应进程日志)。
## 4. 反馈阶段
- 若已获取足够信息,输出结构化运维结论(包含:问题根因/指标数值/解决方案);
- 若信息不足,继续生成工具调用指令,直至完成排查;
- 单次对话最多调用3个工具,避免无效循环。
# 可用运维工具列表
%s
# 历史排查上下文
%s
# 运维禁忌(严格遵守)
1. 禁止调用未列出的工具(如虚构"restart_service"工具);
2. 禁止伪造参数值(如IP必须符合xxx.xxx.xxx.xxx格式,时间必须为YYYY-MM-DD HH:MM:SS);
3. 禁止编造工具执行结果(如未调用工具时,不得输出"CPU使用率90%");
4. 禁止执行高危操作(如重启集群、删除日志),需先标注风险并提示确认。
# 用户当前运维需求
%s`
// 4. 注入变量生成最终提示词(填充集群/工具/上下文/用户需求)
prompt := fmt.Sprintf(
promptTemplate,
cluster,
toolListBuilder.String(),
context,
userInput,
)
return prompt
}
// 示例:初始化SRE工具列表
func initSRETools() []Tool {
return []Tool{
{
Name: "query_prom_cpu",
Description: "查询指定服务器/容器的CPU使用率(百分比),支持近1小时/6小时/24小时的指标聚合",
Params: "host: 服务器IP(必填,格式xxx.xxx.xxx.xxx);time_range: 时间范围(必填,取值:1h/6h/24h);interval: 采样间隔(可选,默认10s,取值1s/5s/10s)",
Example: `{"host":"192.168.1.100","time_range":"1h","interval":"5s"}`,
Scope: "物理机",
},
{
Name: "search_es_logs",
Description: "检索Elasticsearch中的运维日志,支持按IP/时间/日志级别过滤",
Params: "host: 服务器IP(必填);start_time: 开始时间(必填,格式YYYY-MM-DD HH:MM:SS);end_time: 结束时间(必填);log_level: 日志级别(可选,取值:INFO/WARN/ERROR/FATAL)",
Example: `{"host":"192.168.1.100","start_time":"2024-10-01 09:00:00","end_time":"2024-10-01 10:00:00","log_level":"ERROR"}`,
Scope: "通用",
},
{
Name: "check_k8s_pod",
Description: "检查K8s集群中Pod的运行状态(重启次数/资源占用/事件)",
Params: "pod_name: Pod名称(必填);namespace: 命名空间(必填,取值:default/prod/test);cluster: 集群名称(必填,取值:prod-cluster/dev-cluster)",
Example: `{"pod_name":"nginx-78f9d79876-2x7zl","namespace":"prod","cluster":"prod-cluster"}`,
Scope: "K8s集群",
},
}
}
// 测试函数:生成SRE场景的ReAct提示词
func main() {
// 初始化运维工具列表
sreTools := initSRETools()
// 用户运维需求
userInput := "排查prod集群中192.168.1.100物理机CPU使用率持续高于80%的原因"
// 历史上下文(空)
context := ""
// 当前集群环境
cluster := "物理机"
// 生成优化后的提示词
prompt := BuildSREReActPrompt(userInput, sreTools, context, cluster)
fmt.Println("===== SRE/AIOps场景ReAct提示词 =====")
fmt.Println(prompt)
}
帮助 Go SRE 新手关键解读:
-
-
-
- Tool 结构体是“工具的说明书”,LLM 只能通过 Name/Description/Params 理解工具,所以描述必须清晰(比如 “query_cpu_usage:查询服务器 CPU 使用率,参数需包含日期和服务器 IP”);
- 提示词中强制要求 LLM 输出 JSON 格式的工具调用指令,且无额外文字:这是后续 Golang 解析的关键,避免 LLM 输出“我觉得应该调用 XX 工具,参数是 XXX” 这种自然语言;
- context 参数用于存储多轮对话的历史(比如上一轮调用工具的结果),让 Agent 能“记住”之前的操作。
-
-
3.3、SRE/AIOps 场景 ReAct 提示词模板设计原理
3.3.1、核心目标
| 运维场景痛点 | 提示词优化策略 |
|---|---|
| LLM 虚构运维工具 / 参数 | 严格限定工具列表,增加「工具适用范围」过滤,禁止调用列表外工具 |
| 参数格式错误(如时间 / IP) | 新增「参数示例」字段,强制 LLM 参考示例拼接参数,明确参数取值范围 |
| 幻觉输出(编造监控数据) | 增加「运维禁忌」模块,明确禁止伪造执行结果,要求工具调用结果必须可验证 |
| 工具与环境不匹配(如 K8s 工具用在物理机) | 增加集群环境过滤逻辑,仅展示适配当前环境的工具 |
| 高危操作风险 | 新增「操作风险评估」字段,要求 LLM 先评估风险再调用工具 |
3.3.2、模板结构拆解(分模块解析)
(1)角色定义模块(解决「身份认知」问题)
-
-
-
- 设计原理:明确 LLM 的 SRE 工程师身份,锚定其运维领域的专业边界,避免跨领域幻觉;
- 核心优化:强调「禁止虚构工具 / 参数 / 执行结果」,直击 SRE 场景中 LLM 最易出现的幻觉问题;
- 实现细节:在 Golang 代码中通过硬编码角色描述,确保每次生成提示词都包含该核心约束。
-
-
(2)ReAct 流程强化模块(适配运维场景)
-
-
-
- 思考阶段:新增「环境校验」「参数校验」步骤,解决 SRE 场景中工具与环境不匹配、参数格式错误的核心问题;
- 行动阶段:
- 严格限定 JSON 格式,且要求reason字段包含 "需求分析 + 工具选择原因 + 参数校验" 三层内容,强制 LLM 理性思考而非随机调用工具;
- 新增risk字段,适配 SRE 场景的操作风险管控(如避免误执行重启集群等高危操作);
- 观察阶段:强调 "结果有效性验证",要求 LLM 判断工具返回结果是否可用,而非直接使用错误数据;
- 反馈阶段:限定单次对话工具调用次数(3 次),避免故障排查时无限循环调用工具。
-
-
(3)工具列表模块(结构化展示)
-
-
-
- 设计原理:运维工具参数复杂(如时间范围、命名空间、集群名称),结构化展示可降低 LLM 理解成本;
- 实现细节:
- 新增Example字段:提供可直接参考的参数示例,解决 LLM 参数拼接错误问题;
- 新增Scope字段:结合 Golang 的工具过滤逻辑,确保仅展示适配当前集群环境的工具;
- 用「工具编号 + 分段展示」替代纯 JSON 列表,提升 LLM 对工具的识别精度。
-
-
(4)运维禁忌模块(底线约束)
-
-
-
- 设计原理:SRE 操作直接影响业务稳定性,必须明确禁止高危 / 违规行为;
- 核心内容:
- 禁止调用未列出工具(如虚构restart_service工具);
- 禁止伪造参数值(如 IP 格式错误、时间范围无效);
- 禁止编造执行结果(如未调用 Prometheus 却输出 CPU 使用率);
- 禁止执行高危操作(需先评估风险)。
-
-
3.4、提示词设计的关键优化
-
-
- 格式强制约束:明确写“必须输出严格 JSON,无额外文字”,避免 LLM 加解释性语句(比如“以下是工具调用指令:{...}”);
- 工具描述具体化:不要写“查询 CPU”,要写“查询指定服务器指定日期的 CPU 使用率,返回百分比数值”,LLM 能更精准判断;
- 参数格式明确:指定参数类型和格式(如日期 YYYY-MM-DD),减少参数错误。
-
四、步骤 2:Agent 工具实现逻辑(标准化封装)
工具是 ReAct Agent 的“手脚”,我们需要封装工具的执行逻辑,并设计标准化的调用接口,解决“Agent 如何与外部工具交互”的问题。
4.1、工具实现原则
4.2、Golang 实现工具封装
创建 tools.go,实现两个示例工具(CPU 使用率查询、内存使用率查询),并封装工具调用入口:
package main
import (
"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 main
import (
"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 ToolCall
err := 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 main
import "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、运行步骤
- 替换 config.go 中的 OpenAIAPIKey 为自己的密钥;
- 执行 go mod tidy 安装依赖;
- 执行 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 真正适配你的业务需求。

浙公网安备 33010602011771号