❝当 Agent 面对复杂任务时,它是如何「思考」的?从文本解析到结构化 API,ReAct 到 Function Calling,一文讲透 Agent 的规划与执行机制。❞
如果说 LLM 是 Agent 的「大脑」,那么 「Plan 机制」 就是这个大脑的「思维方式」。
一个天气查询 Agent 的执行流程看似简单:
用户: 北京今天天气怎么样?
Agent: [思考] → [调用天气工具] → [整理结果] → 北京今天晴,25°C
但背后涉及的技术演进,从 2022 年的 ReAct 论文到今天的 Function Calling,经历了多次范式转变。本文将以 trpc-agent-go 框架为例,深入剖析:
Plan 机制是 Agent 的「决策引擎」,负责将用户请求转化为可执行的步骤序列。
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Agent 执行流程 │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 用户输入 │
│ "帮我查下北京和上海的天气" │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Planner(规划器) │
│ │
│ 分析任务 → 生成执行计划 │
│ • 需要查询两个城市的天气 │
│ • 调用 weather_tool 两次 │
│ • 汇总结果返回用户 │
└─────────────────┬───────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ 执行循环 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Step 1 │ → │ Step 2 │ → │ Step 3 │ │
│ │ 查北京 │ │ 查上海 │ │ 汇总回复 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 最终响应 │
│ "北京 25°C 晴,上海 28°C 多云" │
└─────────────────────────────────────┘
职责 | 说明 | 示例 |
|---|---|---|
「任务分解」 | 将复杂任务拆分为原子操作 | "查两地天气" → 查北京 + 查上海 |
「工具选择」 | 决定使用哪些工具 | 选择 weather_tool 而非 search_tool |
「流程控制」 | 决定何时继续、何时终止 | 信息足够后停止循环 |
当前 Agent 框架主要有两种 Planner 实现方式:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Planner 实现方式对比 │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ ReAct Planner │ │ Builtin Planner │
│ (文本提示词方式) │ │ (结构化 API 方式) │
├─────────────────────────────┤ ├─────────────────────────────┤
│ │ │ │
│ Prompt 引导模型输出: │ │ 使用模型原生能力: │
│ • Thought: ... │ │ • Function Calling │
│ • Action: tool_name │ │ • Tool Use │
│ • Action Input: {...} │ │ • Thinking Mode │
│ │ │ │
│ 通过正则/解析器提取 │ │ 结构化 JSON 响应 │
│ │ │ │
│ ⚠️ 依赖模型遵循格式 │ │ ✅ API 保证格式正确 │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
│ │
│ 历史演进方向 │
└─────────────────►─────────────────────┘
「ReAct (Reasoning + Acting)」 由 Google 于 2022 年提出,核心思想是让 LLM 「交替进行推理和行动」。
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ReAct 核心思想 │
└─────────────────────────────────────────────────────────────────────────────────┘
传统方式: ReAct 方式:
┌────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ Input → LLM → Output │ │ Input │
│ │ │ ↓ │
│ 一步到位,无法使用工具 │ │ Thought (推理) │
│ │ │ ↓ │
└────────────────────────┘ │ Action (行动) │
│ ↓ │
│ Observation (观察) │
│ ↓ │
│ Thought → Action → ...│
│ ↓ │
│ Final Answer │
│ │
│ 交替进行,可调用工具 │
└────────────────────────┘
ReAct 通过精心设计的 「Prompt 模板」 引导模型按特定格式输出:
// trpc-agent-go 中的 ReAct Prompt 模板
const ReactPromptTemplate = `
You are an AI assistant that can use tools to help answer questions.
Available Tools:
{{range .Tools}}
- {{.Name}}: {{.Description}}
Parameters: {{.Parameters}}
{{end}}
IMPORTANT: You must use the EXACT format below. Any deviation will cause parsing errors.
Format:
/*PLANNING*/
[Your analysis of what needs to be done]
/*END_PLANNING*/
/*ACTION*/
{"tool": "tool_name", "parameters": {"param1": "value1"}}
/*END_ACTION*/
When you have the final answer:
/*FINAL_ANSWER*/
[Your complete answer to the user]
/*END_FINAL_ANSWER*/
User Question: {{.Question}}
`
/*PLANNING*/
用户询问北京天气,我需要调用天气工具获取信息。
/*END_PLANNING*/
/*ACTION*/
{"tool": "get_weather", "parameters": {"city": "北京"}}
/*END_ACTION*/
框架需要解析模型的文本输出,提取结构化信息:
// trpc-agent-go 中的解析逻辑
func (p *ReactPlanner) parseResponse(text string) (*PlanStep, error) {
// 使用正则表达式提取 ACTION 块
actionPattern := regexp.MustCompile(`/\*ACTION\*/\s*([\s\S]*?)\s*/\*END_ACTION\*/`)
matches := actionPattern.FindStringSubmatch(text)
iflen(matches) < 2 {
// 检查是否是最终答案
finalPattern := regexp.MustCompile(`/\*FINAL_ANSWER\*/\s*([\s\S]*?)\s*/\*END_FINAL_ANSWER\*/`)
if finalMatches := finalPattern.FindStringSubmatch(text); len(finalMatches) >= 2 {
return &PlanStep{Type: StepTypeFinal, Content: finalMatches[1]}, nil
}
returnnil, errors.New("failed to parse response")
}
// 解析 JSON
var action ActionPayload
if err := json.Unmarshal([]byte(matches[1]), &action); err != nil {
returnnil, fmt.Errorf("invalid action JSON: %w", err)
}
return &PlanStep{
Type: StepTypeAction,
ToolName: action.Tool,
Parameters: action.Parameters,
}, nil
}
尽管 ReAct 是里程碑式的工作,但在工程实践中存在明显痛点:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ReAct 文本解析的痛点 │
└─────────────────────────────────────────────────────────────────────────────────┘
问题 1:格式不一致
┌────────────────────────────────────────────────────────────────────────────────┐
│ 期望输出: │ 实际输出(变体): │
│ Action: get_weather │ action: get_weather ← 小写 │
│ Action Input: {"city": "北京"} │ Action:get_weather ← 中文冒号 │
│ │ Action: get_weather。 ← 多了句号 │
│ │ 动作: get_weather ← 中文关键字 │
└────────────────────────────────────────────────────────────────────────────────┘
问题 2:JSON 格式错误
┌────────────────────────────────────────────────────────────────────────────────┐
│ 期望:{"city": "北京"} │
│ 实际:{'city': '北京'} ← 单引号 │
│ 实际:{city: "北京"} ← 缺少键的引号 │
│ 实际:{"city": "北京",} ← 尾部逗号 │
└────────────────────────────────────────────────────────────────────────────────┘
问题 3:多余内容
┌────────────────────────────────────────────────────────────────────────────────┐
│ 期望: │
│ /*ACTION*/ │
│ {"tool": "get_weather", "parameters": {"city": "北京"}} │
│ /*END_ACTION*/ │
│ │
│ 实际: │
│ 好的,我来帮你查询天气。 ← 多余的开场白 │
│ /*ACTION*/ │
│ {"tool": "get_weather", "parameters": {"city": "北京"}} │
│ /*END_ACTION*/ │
│ 让我来调用这个工具... ← 多余的解释 │
└────────────────────────────────────────────────────────────────────────────────┘
这些问题导致:
2023 年 6 月,OpenAI 发布了 「Function Calling」 功能,彻底改变了 Agent 的实现范式。
核心思想:「不再依赖文本约定,而是通过 API 契约保证格式」。
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 从「文本约定」到「API 契约」 │
└─────────────────────────────────────────────────────────────────────────────────┘
文本约定(ReAct): API 契约(Function Calling):
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ │ │ │
│ "请按照这个格式输出: │ │ 请求中定义工具 Schema: │
│ Thought: ... │ │ { │
│ Action: tool_name │ │ "tools": [{ │
│ Action Input: {...}" │ │ "name": "get_weather", │
│ │ │ "parameters": {...} │
│ ⚠️ 模型可能不遵守 │ │ }] │
│ ⚠️ 格式可能出错 │ │ } │
│ │ │ │
└────────────────────────────────┘ │ 响应保证是结构化的: │
│ { │
│ "tool_calls": [{ │
│ "function": { │
│ "name": "get_weather", │
│ "arguments": "{...}" │
│ } │
│ }] │
│ } │
│ │
│ ✅ API 保证格式正确 │
└────────────────────────────────┘
// 请求:定义工具
req := openai.ChatCompletionRequest{
Model: "gpt-4-turbo",
Messages: messages,
Tools: []openai.Tool{
{
Type: "function",
Function: &openai.FunctionDefinition{
Name: "get_weather",
Description: "获取指定城市的天气",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "城市名称",
},
},
"required": []string{"city"},
},
},
},
},
}
// 响应:结构化的工具调用
response, _ := client.CreateChatCompletion(ctx, req)
iflen(response.Choices[0].Message.ToolCalls) > 0 {
toolCall := response.Choices[0].Message.ToolCalls[0]
// 直接访问结构化字段,无需正则解析
name := toolCall.Function.Name // "get_weather"
args := toolCall.Function.Arguments // `{"city": "北京"}`
}
// Claude 的工具定义
tools := []anthropic.Tool{
{
Name: "get_weather",
Description: "获取天气信息",
InputSchema: anthropic.InputSchema{
Type: "object",
Properties: map[string]anthropic.Property{
"city": {Type: "string", Description: "城市名称"},
},
Required: []string{"city"},
},
},
}
// Claude 响应解析
for _, content := range response.Content {
if content.Type == "tool_use" {
name := content.ToolUse.Name // "get_weather"
input := content.ToolUse.Input // map[string]interface{}{"city": "北京"}
}
}
框架定义了统一的结构化数据模型:
// model/request.go - 工具调用请求
type ToolCall struct {
Type string `json:"type"` // 类型:固定为 "function"
Function FunctionDefinitionParam `json:"function"`// 函数信息
ID string `json:"id"` // 调用 ID
Index *int `json:"index"` // 流式响应中的索引
}
type FunctionDefinitionParam struct {
Name string`json:"name"` // 函数名
Description string`json:"description"`// 描述
Arguments []byte`json:"arguments"` // JSON 格式的参数
}
// model/response.go - 响应中的辅助方法
func (rsp *Response) IsToolCallResponse() bool {
return rsp != nil &&
len(rsp.Choices) > 0 &&
(len(rsp.Choices[0].Message.ToolCalls) > 0 ||
len(rsp.Choices[0].Delta.ToolCalls) > 0)
}
func (rsp *Response) IsFinalResponse() bool {
if rsp == nil {
returntrue
}
if rsp.IsPartial || rsp.IsToolCallResponse() {
returnfalse
}
return rsp.Done && (len(rsp.Choices) > 0 || rsp.Error != nil)
}
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 结构化 API 优势详解 │
└─────────────────────────────────────────────────────────────────────────────────┘
1. 格式保证
┌────────────────────────────────────────────────────────────────────────────────┐
│ 文本方式: │
│ text := "Action: get_weather\nAction Input: {\"city\": \"北京\"}" │
│ // 可能变成: │
│ // "action: get_weather" / "Action:get_weather" / "动作: get_weather" │
│ │
│ 结构化 API: │
│ toolCalls[0].Function.Name // 永远是 "get_weather",不会有变体 │
└────────────────────────────────────────────────────────────────────────────────┘
2. 参数校验
┌────────────────────────────────────────────────────────────────────────────────┐
│ 文本方式:参数可能是任意格式 │
│ "Action Input: city=北京" ← 不是 JSON │
│ "Action Input: {'city': '北京'}" ← Python 风格 │
│ │
│ 结构化 API:参数必须符合 JSON Schema │
│ { │
│ "type": "object", │
│ "properties": {"city": {"type": "string"}}, │
│ "required": ["city"] │
│ } │
│ // 模型生成的参数必须符合这个 Schema │
└────────────────────────────────────────────────────────────────────────────────┘
3. 并行调用原生支持
┌────────────────────────────────────────────────────────────────────────────────┐
│ 用户:查询北京和上海的天气 │
│ │
│ 响应(一次返回多个工具调用): │
│ { │
│ "tool_calls": [ │
│ {"function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"}},│
│ {"function": {"name": "get_weather", "arguments": "{\"city\":\"上海\"}"}} │
│ ] │
│ } │
│ │
│ // 可以并行执行两个工具调用,提升效率 │
└────────────────────────────────────────────────────────────────────────────────┘
无论是 ReAct 还是 Builtin Planner,Agent 的核心都是一个 「执行循环」:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Agent 执行循环 │
└─────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┐
│ 开始执行 │
└────────┬─────────┘
│
▼
┌─────────────────────────┐
┌─────►│ 调用 LLM │
│ │ (带上工具定义/历史) │
│ └────────────┬────────────┘
│ │
│ ▼
│ ┌─────────────────────────┐
│ │ 检查响应类型 │
│ └────────────┬────────────┘
│ │
│ ┌────────┴────────┐
│ │ │
│ ▼ ▼
│ ┌──────────┐ ┌──────────┐
│ │工具调用? │ │最终响应? │
│ └────┬─────┘ └────┬─────┘
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │执行工具 │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │结果加入 │ │
│ │消息历史 │ │
│ └────┬─────┘ │
│ │ │
└────────┘ │
▼
┌──────────────────┐
│ 返回最终结果 │
└──────────────────┘
循环何时终止?两种方式的判断逻辑不同:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 循环终止条件对比 │
└─────────────────────────────────────────────────────────────────────────────────┘
ReAct(文本解析): Builtin(结构化 API):
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ │ │ │
│ for { │ │ for { │
│ response := callLLM() │ │ response := callLLM() │
│ │ │ │
│ // 解析文本,检测关键字 │ │ // 直接检查结构化字段 │
│ if contains(text, │ │ if len(toolCalls) == 0 { │
│ "FINAL_ANSWER") { │ │ break // 没有工具调用 │
│ break │ │ } │
│ } │ │ │
│ │ │ // 或者检查 finish_reason │
│ // 解析 Action │ │ if finishReason == "stop" │
│ match := regex.Find(text) │ │ break │
│ if match != nil { │ │ } │
│ execute(match) │ │ │
│ } │ │ for _, tc := range │
│ } │ │ toolCalls { │
│ │ │ execute(tc) │
│ ⚠️ 解析可能失败 │ │ } │
│ ⚠️ 需要复杂容错逻辑 │ │ } │
│ │ │ │
└────────────────────────────────┘ │ ✅ 永远不会解析失败 │
│ ✅ 代码简洁清晰 │
└────────────────────────────────┘
框架通过 Response 的辅助方法实现优雅的循环控制:
func (a *Agent) runLoop(ctx context.Context, request *Request) (*Response, error) {
for iteration := 0; iteration < a.maxIterations; iteration++ {
// 调用 LLM
response, err := a.model.GenerateContent(ctx, request)
if err != nil {
returnnil, fmt.Errorf("LLM call failed: %w", err)
}
// 使用结构化方法检查响应类型
if response.IsToolCallResponse() {
// 处理工具调用
for _, tc := range response.Choices[0].Message.ToolCalls {
result, err := a.executeTool(ctx, tc)
if err != nil {
// 错误处理...
}
// 将结果加入消息历史
request.Messages = append(request.Messages,
model.NewToolResultMessage(tc.ID, result))
}
continue
}
// 检查是否是最终响应
if response.IsFinalResponse() {
return response, nil
}
}
returnnil, errors.New("max iterations reached")
}
一些先进模型(如 Claude 3.5)支持 「显式思考过程」,在生成响应前先进行推理:
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Thinking Mode 流程 │
└─────────────────────────────────────────────────────────────────────────────────┘
普通模式: Thinking Mode:
┌────────────────────────────┐ ┌────────────────────────────┐
│ │ │ │
│ Input │ │ Input │
│ ↓ │ │ ↓ │
│ [模型内部处理] │ │ Thinking Block (可见) │
│ ↓ │ │ "让我分析这个问题... │
│ Output │ │ 首先需要查询天气... │
│ │ │ 然后整理信息..." │
│ ⚠️ 推理过程不可见 │ │ ↓ │
│ │ │ Output │
└────────────────────────────┘ │ │
│ ✅ 推理过程透明可解释 │
└────────────────────────────┘
框架支持通过配置启用 Thinking Mode:
// 启用 Thinking Mode
request := &model.Request{
Messages: messages,
GenerationConfig: &model.GenerationConfig{
ThinkingConfig: &model.ThinkingConfig{
ThinkingBudget: 1024, // 思考 token 预算
},
},
}
除了工具调用,结构化 API 还支持强制模型输出符合特定 JSON Schema 的响应:
// 强制模型输出结构化的天气信息
request := &model.Request{
Messages: messages,
StructuredOutput: &model.StructuredOutput{
Type: model.StructuredOutputJSONSchema,
JSONSchema: &model.JSONSchemaConfig{
Name: "weather_response",
Schema: map[string]any{
"type": "object",
"properties": map[string]any{
"temperature": map[string]any{"type": "number"},
"condition": map[string]any{"type": "string"},
"suggestion": map[string]any{"type": "string"},
},
"required": []string{"temperature", "condition", "suggestion"},
},
Strict: true,
},
},
}
// 响应保证是:
// {"temperature": 22, "condition": "晴朗", "suggestion": "适合户外活动"}
可以控制模型如何选择工具:
// 自动选择(默认)
request.ToolChoice = "auto"
// 强制使用特定工具
request.ToolChoice = &model.ToolChoice{
Type: "function",
Function: &model.ToolChoiceFunction{
Name: "get_weather", // 强制调用这个工具
},
}
// 禁止使用工具
request.ToolChoice = "none"
// 必须使用工具(但不指定哪个)
request.ToolChoice = "required"
维度 | ReAct (文本方式) | Builtin (结构化 API) |
|---|---|---|
「格式可靠性」 | ⚠️ 依赖模型遵循 Prompt | ✅ API 保证格式正确 |
「解析复杂度」 | 🔴 需要复杂正则/解析器 | 🟢 直接访问结构化字段 |
「模型兼容性」 | ✅ 所有模型都支持 | ⚠️ 需要模型支持 FC |
「并行调用」 | 🔴 难以实现 | ✅ 原生支持 |
「调试可读性」 | ✅ 文本输出易于阅读 | ⚠️ JSON 可读性稍差 |
「成本」 | ⚠️ Prompt 较长 | ✅ 工具定义紧凑 |
「灵活性」 | ✅ 可完全自定义格式 | ⚠️ 受限于 API 设计 |
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 选择决策树 │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────┐
│ 模型支持 Function │
│ Calling 吗? │
└────────────┬────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 是 │ │ 否 │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ 追求稳定性和 │ │ 使用 ReAct Planner │
│ 生产环境? │ │ (文本提示词方式) │
└─────────┬─────────┘ └───────────────────────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 是 │ │ 否 │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Builtin Planner │ │ 可根据需要选择 │
│ (结构化 API) │ │ 任一方式 │
└─────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────┐
│ trpc-agent-go 的混合策略 │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────┐
│ Planner 接口 │
└────────────┬────────────┘
│
┌──────────────────┴──────────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Builtin Planner │ │ React Planner │
│ │ │ │
│ 使用结构化 API: │ │ 使用文本提示词: │
│ • Function Calling │ │ • /*PLANNING*/ │
│ • Tool Use │ │ • /*ACTION*/ │
│ • Thinking Mode │ │ • /*FINAL_ANSWER*/ │
│ │ │ │
│ 适用于: │ │ 适用于: │
│ • OpenAI/Claude │ │ • 开源模型 │
│ • 生产环境 │ │ • 特殊定制场景 │
│ • 追求稳定性 │ │ • 需要更多控制 │
└─────────────────────┘ └─────────────────────┘
原因:
要点 | 说明 |
|---|---|
「Plan 机制是核心」 | 决定 Agent 如何「思考」和「行动」 |
「两种实现范式」 | ReAct (文本) vs Builtin (结构化 API) |
「结构化是趋势」 | Function Calling 正在成为主流 |
「循环控制关键」 | 优雅地判断何时继续、何时终止 |
「框架要兼容」 | 好的框架应同时支持两种方式 |
通过 trpc-agent-go 的 Planner 模块,你可以:
✅ 「灵活切换」 Builtin 和 ReAct 两种 Planner ✅ 「统一抽象」 屏蔽不同模型的 API 差异 ✅ 「可靠执行」 结构化 API 保证格式正确 ✅ 「优雅扩展」 自定义 Planner 实现特殊需求
❝💡 「思考题」:你的 Agent 应用使用的是哪种 Plan 机制?在实际项目中,你更倾向于文本方式还是结构化 API?欢迎在评论区分享你的实践经验。❞