一、ReAct 模式核心概念
ReAct = Reason + Act,核心思想是让 LLM 在"思考"和"行动"之间交替循环,直到完成任务。
Thought: 我需要查一下北京的天气
Action: get_weather(city="北京")
Observation: {"temperature": 22, "condition": "晴"}
Thought: 天气不错,但我还需要知道明天的气温
Action: get_forecast(city="北京", days=2)
Observation: {"tomorrow": {"high": 25, "low": 18}}
Thought: 信息足够了,我来整理回答
Final Answer: 北京今天22°C晴,明天18~25°C...
三个关键要素:
| 要素 | 含义 | 谁来做 |
|---|---|---|
| Thought | 当前的思考/推理步骤 | LLM |
| Action | 要调用的工具及其参数 | LLM 决定,系统执行 |
| Observation | 工具返回的结果 | 系统填入 |
二、Go 实现:Agent 内核
2.1 数据结构定义
// agent/types.go
package agent
import (
"encoding/json"
"time"
)
// Step 单步执行记录
type Step struct {
Index int `json:"index"`
Thought string `json:"thought"`
Action string `json:"action"`
ActionInput json.RawMessage `json:"action_input"`
Observation string `json:"observation"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Error string `json:"error,omitempty"`
}
// AgentState Agent 运行时状态
type AgentState struct {
Task string `json:"task"`
Steps []Step `json:"steps"`
MaxSteps int `json:"max_steps"`
CurrentStep int `json:"current_step"`
Finished bool `json:"finished"`
FinalAnswer string `json:"final_answer,omitempty"`
Memory []string `json:"memory"` // 短期记忆:历史步骤摘要
}
// ToolFunc 工具函数签名
type ToolFunc func(input json.RawMessage) (string, error)
// ToolDefinition 工具定义
type ToolDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters json.RawMessage `json:"parameters"` // JSON Schema
Execute ToolFunc
}
2.2 Agent 核心引擎
// agent/engine.go
package agent
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
)
// Agent ReAct Agent 核心
type Agent struct {
LLM LLMClient
Tools map[string]ToolDefinition
MaxSteps int
MaxRetries int
}
type LLMClient interface {
Chat(ctx context.Context, messages []Message) (string, error)
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// NewAgent 创建 Agent
func NewAgent(llm LLMClient, tools []ToolDefinition) *Agent {
toolMap := make(map[string]ToolDefinition)
for _, t := range tools {
toolMap[t.Name] = t
}
return &Agent{
LLM: llm,
Tools: toolMap,
MaxSteps: 10,
MaxRetries: 2,
}
}
// Run 执行任务
func (a *Agent) Run(ctx context.Context, task string) (*AgentState, error) {
state := &AgentState{
Task: task,
Steps: make([]Step, 0),
MaxSteps: a.MaxSteps,
CurrentStep: 0,
Memory: make([]string, 0),
}
systemPrompt := a.buildSystemPrompt()
for state.CurrentStep < state.MaxSteps {
select {
case <-ctx.Done():
return state, ctx.Err()
default:
}
// 1. 构建当前轮次的 Prompt
messages := a.buildMessages(systemPrompt, state)
// 2. 调用 LLM
response, err := a.LLM.Chat(ctx, messages)
if err != nil {
return state, fmt.Errorf("LLM call failed at step %d: %w", state.CurrentStep, err)
}
// 3. 解析响应
parsed, err := a.parseResponse(response)
if err != nil {
// 解析失败,重试
if state.CurrentStep > 0 && state.CurrentStep%a.MaxRetries == 0 {
return state, fmt.Errorf("parse failed after retries: %w", err)
}
continue
}
// 4. 检查是否是最终答案
if parsed.IsFinal {
state.Finished = true
state.FinalAnswer = parsed.FinalAnswer
return state, nil
}
// 5. 执行工具调用
step := Step{
Index: state.CurrentStep,
Thought: parsed.Thought,
Action: parsed.Action,
ActionInput: parsed.ActionInput,
StartTime: time.Now(),
}
observation, execErr := a.executeTool(parsed.Action, parsed.ActionInput)
step.EndTime = time.Now()
if execErr != nil {
step.Error = execErr.Error()
observation = fmt.Sprintf("Error: %s", execErr.Error())
}
step.Observation = observation
state.Steps = append(state.Steps, step)
state.Memory = append(state.Memory, a.summarizeStep(step))
state.CurrentStep++
}
// 达到最大步数仍未完成
return state, fmt.Errorf("max steps (%d) reached without completion", a.MaxSteps)
}
2.3 Prompt 构建
// agent/prompt.go
package agent
import (
"encoding/json"
"fmt"
"strings"
)
func (a *Agent) buildSystemPrompt() string {
var toolDescs []string
for _, tool := range a.Tools {
paramsStr := string(tool.Parameters)
desc := fmt.Sprintf(`- %s: %s
参数: %s`, tool.Name, tool.Description, paramsStr)
toolDescs = append(toolDescs, desc)
}
return fmt.Sprintf(`你是一个 AI 助手,可以通过思考和调用工具来完成任务。
## 可用工具
%s
## 工作流程
每次回复必须遵循以下格式:
Thought: 你的思考过程(分析当前情况,决定下一步做什么)
Action: 工具名称(如果要调用工具)
ActionInput: {"param1": "value1"}(工具的 JSON 参数)
或者,如果任务已完成:
Final Answer: 你的最终回答
## 规则
1. 每次只能调用一个工具
2. 仔细观察工具返回的结果
3. 如果工具出错,尝试其他方法
4. 最多 %d 步内完成任务
5. 不要编造工具返回值`,
strings.Join(toolDescs, "\n"),
a.MaxSteps)
}
func (a *Agent) buildMessages(systemPrompt string, state *AgentState) []Message {
messages := []Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: fmt.Sprintf("任务:%s", state.Task)},
}
// 添加历史步骤
for _, step := range state.Steps {
messages = append(messages, Message{Role: "assistant", Content: fmt.Sprintf(
"Thought: %s\nAction: %s\nActionInput: %s",
step.Thought, step.Action, string(step.ActionInput),
)})
messages = append(messages, Message{Role: "user", Content: fmt.Sprintf(
"Observation: %s", step.Observation,
)})
}
return messages
}
2.4 响应解析
// agent/parser.go
package agent
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
// ParsedResponse 解析后的 LLM 响应
type ParsedResponse struct {
IsFinal bool
FinalAnswer string
Thought string
Action string
ActionInput json.RawMessage
}
func (a *Agent) parseResponse(response string) (*ParsedResponse, error) {
// 1. 检查是否是最终答案
finalRe := regexp.MustCompile(`(?s)Final Answer:\s*(.*)`)
if matches := finalRe.FindStringSubmatch(response); len(matches) > 1 {
return &ParsedResponse{
IsFinal: true,
FinalAnswer: strings.TrimSpace(matches[1]),
}, nil
}
// 2. 提取 Thought
thoughtRe := regexp.MustCompile(`(?s)Thought:\s*(.*?)(?=Action:|$)`)
thought := ""
if matches := thoughtRe.FindStringSubmatch(response); len(matches) > 1 {
thought = strings.TrimSpace(matches[1])
}
// 3. 提取 Action
actionRe := regexp.MustCompile(`Action:\s*(\w+)`)
action := ""
if matches := actionRe.FindStringSubmatch(response); len(matches) > 1 {
action = strings.TrimSpace(matches[1])
}
// 4. 提取 ActionInput (JSON)
inputRe := regexp.MustCompile(`(?s)ActionInput:\s*(\{.*?\}|\[.*?\])`)
var input json.RawMessage
if matches := inputRe.FindStringSubmatch(response); len(matches) > 1 {
input = json.RawMessage(matches[1])
}
if action == "" {
return nil, fmt.Errorf("no action found in response: %s", response)
}
return &ParsedResponse{
IsFinal: false,
Thought: thought,
Action: action,
ActionInput: input,
}, nil
}
2.5 工具执行
// agent/tool_executor.go
package agent
import (
"encoding/json"
"fmt"
)
func (a *Agent) executeTool(name string, input json.RawMessage) (string, error) {
tool, ok := a.Tools[name]
if !ok {
return "", fmt.Errorf("unknown tool: %s", name)
}
result, err := tool.Execute(input)
if err != nil {
return "", fmt.Errorf("tool %s execution failed: %w", name, err)
}
return result, nil
}
func (a *Agent) summarizeStep(step Step) string {
return fmt.Sprintf("Step %d: %s → %s(%s) → %s",
step.Index,
truncate(step.Thought, 50),
step.Action,
truncate(string(step.ActionInput), 40),
truncate(step.Observation, 60),
)
}
func truncate(s string, maxLen int) string {
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "..."
}
三、完整使用示例
// examples/basic_agent/main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"your-project/agent"
)
func main() {
// 1. 定义工具
tools := []agent.ToolDefinition{
{
Name: "get_weather",
Description: "获取指定城市的当前天气",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}`),
Execute: func(input json.RawMessage) (string, error) {
var params struct {
City string `json:"city"`
}
if err := json.Unmarshal(input, ¶ms); err != nil {
return "", err
}
// 模拟天气查询
weatherData := map[string]string{
"北京": `{"temperature": 22, "condition": "晴", "humidity": 45}`,
"上海": `{"temperature": 27, "condition": "多云", "humidity": 65}`,
"广州": `{"temperature": 31, "condition": "阵雨", "humidity": 78}`,
}
if data, ok := weatherData[params.City]; ok {
return data, nil
}
return `{"error": "city not found"}`, nil
},
},
{
Name: "calculate",
Description: "执行数学计算",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式,如 2+2"}
},
"required": ["expression"]
}`),
Execute: func(input json.RawMessage) (string, error) {
var params struct {
Expression string `json:"expression"`
}
if err := json.Unmarshal(input, ¶ms); err != nil {
return "", err
}
// 这里应该用安全的表达式求值库
return fmt.Sprintf("计算结果: %s = ?", params.Expression), nil
},
},
}
// 2. 创建 LLM Client(这里用模拟实现)
llm := &mockLLM{}
// 3. 创建 Agent
ag := agent.NewAgent(llm, tools)
// 4. 执行任务
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
state, err := ag.Run(ctx, "北京今天天气怎么样?适合出门吗?")
if err != nil {
log.Printf("Agent finished with error: %v", err)
}
// 5. 输出结果
if state.Finished {
fmt.Printf("\n=== 最终答案 ===\n%s\n", state.FinalAnswer)
}
fmt.Printf("\n=== 执行步骤 (%d 步) ===\n", len(state.Steps))
for _, step := range state.Steps {
fmt.Printf("\nStep %d:\n", step.Index)
fmt.Printf(" Thought: %s\n", step.Thought)
fmt.Printf(" Action: %s(%s)\n", step.Action, string(step.ActionInput))
fmt.Printf(" Observation: %s\n", step.Observation)
if step.Error != "" {
fmt.Printf(" Error: %s\n", step.Error)
}
}
}
// mockLLM 模拟 LLM 用于演示
type mockLLM struct{}
func (m *mockLLM) Chat(ctx context.Context, messages []agent.Message) (string, error) {
// 在实际项目中,这里调用 OpenAI / DeepSeek 等 API
// 这里返回预设的响应来演示流程
return `Thought: 我需要先查看北京的天气情况,然后判断是否适合出门。
Action: get_weather
ActionInput: {"city": "北京"}`, nil
}
四、关键设计决策
4.1 为什么不用框架?
| 方面 | 自己实现 | 用框架(LangChain等) |
|---|---|---|
| 代码量 | 300 行 | 引入数万行依赖 |
| 可控性 | 每一行都理解 | 黑盒行为难以调试 |
| 定制化 | 随意改 | 受限于框架抽象 |
| 生产稳定性 | 自己把控 | 依赖第三方维护 |
| 学习成本 | 一次学会本质 | 学的是框架 API |
原则:先用原生实现理解本质,再用框架提升效率。
4.2 核心设计模式
Agent 核心循环:
1. 构建 Prompt(系统指令 + 历史步骤 + 当前状态)
2. 调用 LLM
3. 解析响应(提取 Thought/Action/Observation)
4. 如果是 Final Answer → 结束
5. 执行工具 → 得到 Observation
6. 回到步骤 1
4.3 边界情况处理
| 场景 | 处理方式 |
|---|---|
| LLM 返回格式错误 | 重试(最多 N 次) |
| 工具不存在 | 返回错误 Observation,让 LLM 自己纠正 |
| 工具执行超时 | 设置上下文超时,返回 Timeout 错误 |
| 无限循环 | 设置最大步数限制 |
| 上下文过长 | 历史步骤摘要压缩 |
| 并发请求 | 每个 Agent 实例独立状态,互不干扰 |
五、本讲总结
┌─────────────────────────────────────────────────────────────┐
│ 第2讲 核心收获 │
│ │
│ ✅ 理解了 ReAct 的本质:Thought → Action → Observation 循环 │
│ │
│ ✅ 实现了 Agent 核心内核(约 300 行 Go): │
│ - 数据结构:Step / AgentState / ToolDefinition │
│ - 引擎循环:buildPrompt → callLLM → parse → execute │
│ - 工具注册与动态调用 │
│ - 错误处理与重试 │
│ │
│ ✅ 掌握了 Prompt 设计的关键: │
│ - 清晰的格式指令(Thought/Action/ActionInput/Final Answer)│
│ - 完整的工具描述(名称+描述+参数 Schema) │
│ - 历史步骤回放(让 LLM 知道已经做了什么) │
│ │
│ ✅ 理解了"先理解本质,再用框架"的原则 │
│ │
└─────────────────────────────────────────────────────────────┘
六、下一讲预告
第3讲:《工具系统设计------注册、校验、沙箱、限流》
你将学到:
- 工具注册中心的设计(名称路由、版本管理)
- 参数校验(JSON Schema 校验器)
- 安全沙箱(代码执行隔离、文件系统限制)
- 限流与配额(每分钟/每天调用次数限制)
- 工具调用追踪(对接第9讲的 OpenTelemetry)
🧰 开发之余的小工具推荐
调试 Agent 时经常需要构造 JSON 参数、解析 LLM 返回的 JSON 片段。我用 zz365.top 的 JSON 格式化器和 Base64 工具来处理这些琐事,纯前端本地运行,不用担心数据泄漏。免费无广告,适合放在浏览器收藏夹。