过去使用大语言模型时,我们通常输入一个问题,等待它返回一段文字。模型再强,这个过程也只是“一问一答”:它能告诉你天气查询接口怎么调用,却不能真的替你查询;能分析项目问题,却看不到本地代码和测试结果。
Agent(智能体)解决的核心问题,是让模型从“生成答案”走向“根据结果继续行动”。
这一章先不引入复杂框架,也不讨论多 Agent 协作。我们只建立一个足够小、但结构完整的 Agent。后续无论接入 OpenAI、Claude,还是本地模型,都可以在这个骨架上继续扩展。
Chatbot 和 Agent 的区别
普通 Chatbot 的数据流是单向的:
用户问题 → LLM → 文本答案
Agent 多了工具执行和结果反馈:
用户目标
↓
LLM 判断下一步
↓
调用工具 → 得到真实结果
↑ ↓
└──── LLM 继续判断
↓
最终答案
关键差异不在于 Prompt 写得更长,而在于程序提供了一个可重复运行的控制循环。模型负责判断,代码负责执行和约束。
例如用户说:“帮我看看项目测试为什么失败。”一个 Agent 可能依次完成:
- 查看项目文件。
- 运行目标测试。
- 根据错误日志定位代码。
- 修改代码。
- 再次运行测试验证。
- 汇总改动与风险。
模型并不是一次就规划好全部步骤。它会根据每次工具返回的真实结果决定下一步,这也是 Agent 能处理开放任务的原因。
一个 Agent 的四个核心部分
1. 模型:负责决策
模型接收用户目标、历史消息和可用工具说明,输出以下两种结果之一:
- 调用某个工具,并给出参数。
- 任务已经完成,返回最终答案。
模型不应该直接执行命令、访问数据库或修改文件。它只产生结构化的“行动意图”,真正的操作由宿主程序完成。
2. 工具:连接真实世界
工具是由程序提供给模型的受控能力,例如:
- 搜索文件;
- 查询数据库;
- 调用 HTTP API;
- 获取天气;
- 发送消息;
- 运行测试。
一个工具至少需要名称、用途说明、参数结构和执行函数。工具描述要明确,因为模型主要靠描述判断什么时候应该使用它。
工具的设计原则是:输入边界清楚,输出保留真实错误,权限尽量小。
不要直接给 Agent 一个无限制 Shell,再期待 Prompt 能保证安全。更可靠的方式是把高风险能力拆成用途明确的工具,并在代码层校验参数、限制目录和设置超时。
3. 状态:保存过程
Agent 每执行一步,都要把结果追加到消息历史中。否则模型看不到刚才发生了什么,只能重复猜测。
最小状态通常包括:
- 用户最初的目标;
- 模型发起的工具调用;
- 工具执行结果;
- 模型最终回答。
复杂系统还会加入任务进度、短期记忆、长期记忆和检查点。但“保存所有内容”不等于记忆做得好:上下文越长,成本和干扰越大。实际项目需要对历史进行裁剪、摘要或按需检索。
4. 循环:控制何时继续、何时停止
Agent 的运行循环可以概括为:
思考 → 行动 → 观察 → 再思考
其中“思考”是模型推理,“行动”是工具调用,“观察”是工具返回。宿主程序必须掌握循环控制权,并设置最大步数、超时和取消机制,避免模型陷入重复调用。
用 Go 写一个最小 Agent 骨架
下面的代码刻意不绑定任何模型厂商。Model 和 Tool 都是接口,之后只需分别实现模型适配器和业务工具。
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 的最小闭环:
- 把当前消息交给模型。
- 没有工具调用时,返回最终答案。
- 有工具调用时,找到并执行对应工具。
- 把真实结果写回消息历史。
- 让模型基于新结果继续判断。
这里对未知工具、执行失败和超过最大步数都直接返回错误。开发阶段让问题清楚暴露,比悄悄把错误拼成普通文本更容易定位根因。生产环境可以对“业务可恢复错误”建立明确协议,但不应吞掉系统错误并伪装成功。
为什么不建议一开始就用复杂框架
Agent 框架通常提供工具注册、工作流、记忆、追踪和多 Agent 编排,能减少不少样板代码。但如果没有先理解运行循环,很容易遇到三个问题:
- 不知道一次请求为什么触发了多次模型调用。
- 工具失败后,只看到框架包装过的模糊错误。
- 成本、重试和停止条件散落在默认配置里。
先实现一次最小闭环,再选择框架,会更容易判断框架解决了什么问题,也知道出错时该查看哪一层。
从 Demo 到生产还缺什么
上面的骨架能帮助理解原理,但还不能直接作为生产系统。至少还需要补齐:
- 参数校验:每个工具使用明确的参数结构,拒绝未知字段和非法值。
- 权限控制:限制文件目录、数据库操作、网络目标和可执行命令。
- 超时与取消:模型和每个工具都设置合理的截止时间。
- 幂等性:发送消息、创建订单等写操作要防止重复执行。
- 可观测性:记录步骤耗时、模型用量、工具参数摘要和错误链路。
- 停止策略:除了最大步数,还要识别重复调用和无进展状态。
- 人工确认:付款、删除、发布等不可逆操作在执行前必须确认。
这些能力大多应该由确定性的程序实现,而不是只写在 System Prompt 中。Prompt 是行为指导,代码才是安全边界。
本章小结
Agent 不是一个更会聊天的模型,而是一个由程序控制的运行系统。它最核心的结构只有四部分:
模型负责决策
工具负责执行
状态负责记录
循环负责推进
理解这个闭环后,很多看起来复杂的概念——记忆、规划、反思、多 Agent——都只是围绕它增加新的状态或控制策略。
下一章将从工具调用开始:实现一个带 JSON Schema 参数校验的 Go 工具注册器,并处理超时、错误分类与危险操作确认。