【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 步循环:

    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 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

image

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、工具实现原则

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

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、运行步骤

    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 真正适配你的业务需求。

posted @ 2026-01-02 17:05  左扬  阅读(0)  评论(0)    收藏  举报