AI Agent 第一章:从对话到行动,理解智能体的最小闭环

最后更新:2026-07-29
所属系列:AI Agent 实战 · 第 1 / 1 篇
文章目录 10 个章节

过去使用大语言模型时,我们通常输入一个问题,等待它返回一段文字。模型再强,这个过程也只是“一问一答”:它能告诉你天气查询接口怎么调用,却不能真的替你查询;能分析项目问题,却看不到本地代码和测试结果。

Agent(智能体)解决的核心问题,是让模型从“生成答案”走向“根据结果继续行动”。

这一章先不引入复杂框架,也不讨论多 Agent 协作。我们只建立一个足够小、但结构完整的 Agent。后续无论接入 OpenAI、Claude,还是本地模型,都可以在这个骨架上继续扩展。

Chatbot 和 Agent 的区别

普通 Chatbot 的数据流是单向的:

用户问题 → LLM → 文本答案

Agent 多了工具执行和结果反馈:

用户目标
LLM 判断下一步
调用工具 → 得到真实结果
   ↑              ↓
   └──── LLM 继续判断
               最终答案

关键差异不在于 Prompt 写得更长,而在于程序提供了一个可重复运行的控制循环。模型负责判断,代码负责执行和约束。

例如用户说:“帮我看看项目测试为什么失败。”一个 Agent 可能依次完成:

  1. 查看项目文件。
  2. 运行目标测试。
  3. 根据错误日志定位代码。
  4. 修改代码。
  5. 再次运行测试验证。
  6. 汇总改动与风险。

模型并不是一次就规划好全部步骤。它会根据每次工具返回的真实结果决定下一步,这也是 Agent 能处理开放任务的原因。

一个 Agent 的四个核心部分

1. 模型:负责决策

模型接收用户目标、历史消息和可用工具说明,输出以下两种结果之一:

  • 调用某个工具,并给出参数。
  • 任务已经完成,返回最终答案。

模型不应该直接执行命令、访问数据库或修改文件。它只产生结构化的“行动意图”,真正的操作由宿主程序完成。

2. 工具:连接真实世界

工具是由程序提供给模型的受控能力,例如:

  • 搜索文件;
  • 查询数据库;
  • 调用 HTTP API;
  • 获取天气;
  • 发送消息;
  • 运行测试。

一个工具至少需要名称、用途说明、参数结构和执行函数。工具描述要明确,因为模型主要靠描述判断什么时候应该使用它。

工具的设计原则是:输入边界清楚,输出保留真实错误,权限尽量小。

不要直接给 Agent 一个无限制 Shell,再期待 Prompt 能保证安全。更可靠的方式是把高风险能力拆成用途明确的工具,并在代码层校验参数、限制目录和设置超时。

3. 状态:保存过程

Agent 每执行一步,都要把结果追加到消息历史中。否则模型看不到刚才发生了什么,只能重复猜测。

最小状态通常包括:

  • 用户最初的目标;
  • 模型发起的工具调用;
  • 工具执行结果;
  • 模型最终回答。

复杂系统还会加入任务进度、短期记忆、长期记忆和检查点。但“保存所有内容”不等于记忆做得好:上下文越长,成本和干扰越大。实际项目需要对历史进行裁剪、摘要或按需检索。

4. 循环:控制何时继续、何时停止

Agent 的运行循环可以概括为:

思考 → 行动 → 观察 → 再思考

其中“思考”是模型推理,“行动”是工具调用,“观察”是工具返回。宿主程序必须掌握循环控制权,并设置最大步数、超时和取消机制,避免模型陷入重复调用。

用 Go 写一个最小 Agent 骨架

下面的代码刻意不绑定任何模型厂商。ModelTool 都是接口,之后只需分别实现模型适配器和业务工具。

package agent

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
)

type Message struct {
	Role       string
	Content    string
	ToolCallID string
}

type ToolCall struct {
	ID        string
	Name      string
	Arguments json.RawMessage
}

type ModelReply struct {
	Content   string
	ToolCalls []ToolCall
}

type ToolSpec struct {
	Name        string
	Description string
}

type Model interface {
	Complete(ctx context.Context, messages []Message, tools []ToolSpec) (ModelReply, error)
}

type Tool interface {
	Spec() ToolSpec
	Execute(ctx context.Context, arguments json.RawMessage) (string, error)
}

type Agent struct {
	model    Model
	tools    map[string]Tool
	maxSteps int
}

func New(model Model, tools []Tool, maxSteps int) (*Agent, error) {
	if model == nil {
		return nil, errors.New("model is required")
	}
	if maxSteps < 1 {
		return nil, errors.New("maxSteps must be greater than zero")
	}

	toolMap := make(map[string]Tool, len(tools))
	for _, tool := range tools {
		name := tool.Spec().Name
		if name == "" {
			return nil, errors.New("tool name is required")
		}
		if _, exists := toolMap[name]; exists {
			return nil, fmt.Errorf("duplicate tool: %s", name)
		}
		toolMap[name] = tool
	}

	return &Agent{
		model:    model,
		tools:    toolMap,
		maxSteps: maxSteps,
	}, nil
}

这里先建立几个重要约束:

  • 模型和最大步数必须显式提供,不使用隐藏默认值。
  • 工具按名称注册,重名直接报错。
  • 工具参数保留为 JSON,具体校验由各工具在执行边界完成。
  • context.Context 贯穿模型和工具调用,用于超时与主动取消。

接下来实现运行循环:

func (a *Agent) Run(ctx context.Context, prompt string) (string, error) {
	if prompt == "" {
		return "", errors.New("prompt is required")
	}

	messages := []Message{
		{Role: "user", Content: prompt},
	}
	specs := make([]ToolSpec, 0, len(a.tools))
	for _, tool := range a.tools {
		specs = append(specs, tool.Spec())
	}

	for step := 1; step <= a.maxSteps; step++ {
		reply, err := a.model.Complete(ctx, messages, specs)
		if err != nil {
			return "", fmt.Errorf("complete at step %d: %w", step, err)
		}

		if len(reply.ToolCalls) == 0 {
			if reply.Content == "" {
				return "", fmt.Errorf("empty model reply at step %d", step)
			}
			return reply.Content, nil
		}

		messages = append(messages, Message{
			Role:    "assistant",
			Content: reply.Content,
		})

		for _, call := range reply.ToolCalls {
			tool, ok := a.tools[call.Name]
			if !ok {
				return "", fmt.Errorf("unknown tool %q at step %d", call.Name, step)
			}

			result, err := tool.Execute(ctx, call.Arguments)
			if err != nil {
				return "", fmt.Errorf("execute tool %q: %w", call.Name, err)
			}

			messages = append(messages, Message{
				Role:       "tool",
				Content:    result,
				ToolCallID: call.ID,
			})
		}
	}

	return "", fmt.Errorf("agent exceeded maximum steps: %d", a.maxSteps)
}

这个循环完成了 Agent 的最小闭环:

  1. 把当前消息交给模型。
  2. 没有工具调用时,返回最终答案。
  3. 有工具调用时,找到并执行对应工具。
  4. 把真实结果写回消息历史。
  5. 让模型基于新结果继续判断。

这里对未知工具、执行失败和超过最大步数都直接返回错误。开发阶段让问题清楚暴露,比悄悄把错误拼成普通文本更容易定位根因。生产环境可以对“业务可恢复错误”建立明确协议,但不应吞掉系统错误并伪装成功。

为什么不建议一开始就用复杂框架

Agent 框架通常提供工具注册、工作流、记忆、追踪和多 Agent 编排,能减少不少样板代码。但如果没有先理解运行循环,很容易遇到三个问题:

  • 不知道一次请求为什么触发了多次模型调用。
  • 工具失败后,只看到框架包装过的模糊错误。
  • 成本、重试和停止条件散落在默认配置里。

先实现一次最小闭环,再选择框架,会更容易判断框架解决了什么问题,也知道出错时该查看哪一层。

从 Demo 到生产还缺什么

上面的骨架能帮助理解原理,但还不能直接作为生产系统。至少还需要补齐:

  • 参数校验:每个工具使用明确的参数结构,拒绝未知字段和非法值。
  • 权限控制:限制文件目录、数据库操作、网络目标和可执行命令。
  • 超时与取消:模型和每个工具都设置合理的截止时间。
  • 幂等性:发送消息、创建订单等写操作要防止重复执行。
  • 可观测性:记录步骤耗时、模型用量、工具参数摘要和错误链路。
  • 停止策略:除了最大步数,还要识别重复调用和无进展状态。
  • 人工确认:付款、删除、发布等不可逆操作在执行前必须确认。

这些能力大多应该由确定性的程序实现,而不是只写在 System Prompt 中。Prompt 是行为指导,代码才是安全边界。

本章小结

Agent 不是一个更会聊天的模型,而是一个由程序控制的运行系统。它最核心的结构只有四部分:

模型负责决策
工具负责执行
状态负责记录
循环负责推进

理解这个闭环后,很多看起来复杂的概念——记忆、规划、反思、多 Agent——都只是围绕它增加新的状态或控制策略。

下一章将从工具调用开始:实现一个带 JSON Schema 参数校验的 Go 工具注册器,并处理超时、错误分类与危险操作确认。