从零设计一个 AI Agent:架构、循环、工具、记忆与工程实践
从零设计一个 AI Agent:架构、循环、工具、记忆与工程实践
这篇文章整理一套面向工程实现的 AI Agent 设计方法。
如果你已经理解 LLM、Tool Calling、Function Calling、API 调用,也能写服务端程序,那么可以把 Agent 理解成一个更高层的运行时系统:它不是简单地调用一次大模型,而是围绕一个目标,反复让模型决策、调用工具、观察结果、更新状态,直到完成任务或触发停止条件。
一句话概括:
LLM 是大脑,Tool 是手脚,Agent Runtime 是神经系统和执行调度器。Agent 是一个带目标、能规划、能调用工具、能根据反馈迭代行动的 AI 系统。
1. 什么是 Agent?
传统 LLM 应用通常是这样的:
User Input -> LLM -> Response
用户问一个问题,模型给一个回答。这是单轮或多轮对话,本质上仍然是“语言生成”。
而 Agent 更像这样:
User Goal
-> Agent 理解目标
-> Agent 规划下一步
-> Agent 调用工具
-> 工具返回结果
-> Agent 观察结果
-> Agent 调整计划
-> Agent 继续调用工具
-> ...
-> 完成目标 / 失败退出 / 请求用户确认
例如用户说:
帮我检查这个 Go 项目里的登录接口为什么变慢,并给出修复建议。
普通 LLM 可能会根据经验回答:
可能是数据库查询慢、缓存失效、锁竞争、外部服务延迟……
但一个 Agent 应该可以:
1. 搜索项目里的登录接口代码
2. 阅读 handler/service/repository 逻辑
3. 找到相关 SQL
4. 检查慢查询、索引、缓存逻辑
5. 运行测试或 benchmark
6. 总结问题原因
7. 给出修改建议,甚至直接生成 patch
这就是 Agent 与普通 Chatbot 的核心区别:
Chatbot 主要负责回答,Agent 负责行动。
2. Agent 的核心能力
一个基础 Agent 通常具备以下能力。
2.1 目标理解
Agent 接收的不是单纯问题,而是一个待完成的目标。
例如:
修复这个 bug
生成一份日报
帮我分析这个仓库
根据日志定位线上故障
查询订单状态并生成客服回复
目标可能很模糊,所以 Agent 需要从用户输入中提取:
- 用户真正想完成什么
- 当前有哪些已知条件
- 需要哪些外部信息
- 是否需要进一步澄清
- 是否涉及高风险操作
2.2 任务规划
对于复杂目标,Agent 需要拆解步骤。
例如:
目标:修复 API 超时问题
计划:
1. 定位 API 对应的 handler
2. 查看业务逻辑
3. 查找数据库访问代码
4. 分析可能的慢查询
5. 检查是否有缓存
6. 尝试修改代码
7. 运行测试
8. 输出修复说明
规划不一定一次完成。更好的方式是动态规划:
先制定一个粗计划
执行一步
观察结果
根据结果更新计划
继续执行
因为 Agent 经常面对不确定环境,提前规划太细反而容易偏离现实。
2.3 工具调用
Agent 的关键能力是使用工具。
工具可以是:
- 文件读取
- 文件写入
- 搜索代码
- 执行 shell 命令
- 查询数据库
- 调用 HTTP API
- 操作浏览器
- 发送消息
- 创建工单
- 查询监控
- 运行测试
- 调用内部服务
没有工具的 Agent,本质上还是 Chatbot。
工具让 Agent 能够触达真实世界。
2.4 观察与反馈
Tool 执行后会返回结果,Agent 需要根据结果决定下一步。
例如:
Agent 调用 search_code("LoginHandler")
Tool 返回:找到 internal/api/login.go
Agent 继续调用 read_file("internal/api/login.go")
Tool 返回:文件内容
Agent 发现里面调用了 userService.Login
Agent 继续搜索 userService.Login
这个过程就是典型的:
Action -> Observation -> Next Action
2.5 状态维护
Agent 不能每一步都失忆。
它至少需要知道:
- 用户目标是什么
- 已经做了哪些步骤
- 调用了哪些工具
- 工具返回了什么
- 当前计划是什么
- 哪些问题已经解决
- 哪些问题还阻塞
- 最终答案应该包含什么
因此 Agent 必须有 State。
2.6 停止条件
Agent 不能无限循环。
必须定义清楚什么时候停止:
- 任务完成
- 达到最大步骤数
- 达到最大执行时间
- 达到最大 token/cost
- 连续工具失败
- 需要用户确认
- 权限不足
- 信息不足
- 进入无效循环
没有停止条件的 Agent 是非常危险的。
3. Agent 的基础架构
一个实用 Agent 可以拆成以下模块:
Agent
├── Runtime / Orchestrator
├── LLM Client
├── Tool Registry
├── State / Memory
├── Planner
├── Executor
├── Guardrails
├── Evaluator
└── Tracing / Logs
可以画成下面的流程:
┌──────────────────┐
│ User Goal │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Agent Runtime │
└────────┬─────────┘
│
┌──────────┴──────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ LLM │ │ State │
└──────┬───────┘ └──────────────┘
│
▼
┌──────────────┐
│ Decision │
└──────┬───────┘
│
▼
┌──────────────┐
│ Guardrails │
└──────┬───────┘
│
▼
┌──────────────┐
│ Tool │
└──────┬───────┘
│
▼
┌──────────────┐
│ Observation │
└──────┬───────┘
│
▼
┌──────────────┐
│ Update State │
└──────────────┘
4. Agent Runtime:最核心的调度器
Agent Runtime,也可以叫 Orchestrator,是 Agent 的核心。
它负责把 LLM、Tool、State、Guardrails 串起来。
主循环大致如下:
1. 接收用户目标
2. 初始化状态
3. 构造上下文
4. 请求 LLM 决策下一步
5. 解析 LLM 输出
6. 如果是 final,则返回结果
7. 如果是 tool call,则校验工具调用
8. 执行工具
9. 记录 observation
10. 更新 state
11. 判断是否停止
12. 继续下一轮
伪代码:
func (a *Agent) Run(ctx context.Context, input string) (*Result, error) {
state := NewState(input)
for step := 0; step < a.MaxSteps; step++ {
messages := a.BuildMessages(state)
decision, err := a.LLM.Decide(ctx, messages, a.Tools.Schema())
if err != nil {
return nil, err
}
switch decision.Type {
case "final":
return &Result{
Answer: decision.Content,
Trace: state.Trace,
}, nil
case "tool_call":
tool, ok := a.Tools.Get(decision.ToolName)
if !ok {
state.AddObservation("unknown tool: " + decision.ToolName)
continue
}
if err := a.Guardrails.ValidateToolCall(decision); err != nil {
state.AddObservation("tool call rejected: " + err.Error())
continue
}
obs, err := tool.Execute(ctx, decision.Args)
if err != nil {
state.AddObservation("tool error: " + err.Error())
continue
}
state.AddToolResult(decision, obs)
default:
state.AddObservation("invalid decision")
}
}
return nil, ErrMaxStepsExceeded
}
这个循环就是 Agent 的基本骨架。
所有复杂 Agent 最终都绕不开这个闭环:
Think / Decide -> Act -> Observe -> Update -> Repeat
5. LLM Client 设计
LLM 是 Agent 的决策核心。
但是工程上不建议在业务代码里直接绑定某个具体模型供应商。应该抽象一层接口。
例如:
type LLM interface {
Generate(ctx context.Context, req GenerateRequest) (*GenerateResponse, error)
}
请求结构:
type GenerateRequest struct {
Messages []Message
Tools []ToolSpec
}
type Message struct {
Role string
Content string
}
响应结构:
type GenerateResponse struct {
Decision Decision
Usage Usage
}
type Usage struct {
PromptTokens int
CompletionTokens int
TotalTokens int
}
Agent 不应该关心底层调用的是 OpenAI、Claude、Gemini、Qwen、DeepSeek,还是本地模型。
只要实现这个接口,就可以切换模型。
6. Decision:模型输出应该结构化
Agent 的每一步都需要 LLM 做决策。
决策可以分为两类:
1. 调用工具
2. 给出最终答案
因此可以定义:
type DecisionType string
const (
DecisionToolCall DecisionType = "tool_call"
DecisionFinal DecisionType = "final"
)
type Decision struct {
Type DecisionType `json:"type"`
Content string `json:"content,omitempty"`
ToolName string `json:"tool_name,omitempty"`
Args map[string]any `json:"args,omitempty"`
}
工具调用示例:
{
"type": "tool_call",
"tool_name": "search_code",
"args": {
"query": "LoginHandler"
}
}
最终回答示例:
{
"type": "final",
"content": "我已经检查完成,登录接口慢的主要原因是..."
}
生产环境里建议优先使用模型厂商提供的 Tool Calling / Function Calling / Structured Output,而不是完全靠 prompt 让模型输出 JSON。
因为模型自由输出 JSON 常见问题包括:
- 多输出了 Markdown
- JSON 格式错误
- 字段缺失
- 参数类型错误
- 工具名称幻觉
- 内容里夹杂解释文字
使用 Tool Calling 可以显著降低解析失败率。
7. Tool:Agent 的行动能力
Tool 是 Agent 能够执行动作的接口。
一个 Tool 至少需要具备:
- 名称
- 描述
- 参数 Schema
- 执行函数
- 返回结果
Go 里可以这样定义:
type Tool interface {
Name() string
Description() string
InputSchema() map[string]any
Execute(ctx context.Context, args map[string]any) (*ToolResult, error)
}
工具返回值不要只用字符串,建议结构化:
type ToolResult struct {
Summary string
Content any
Artifacts []Artifact
Metadata map[string]any
}
Artifact 可以表示文件、截图、日志、报告等较大的产物:
type Artifact struct {
ID string
Type string
URI string
Summary string
Metadata map[string]any
}
为什么不要只返回字符串?
因为 Tool 的结果可能很复杂,例如:
{
"summary": "Found 3 matches in 2 files",
"content": [
{
"file": "internal/api/login.go",
"line": 28,
"snippet": "func LoginHandler(c *gin.Context) {"
},
{
"file": "internal/service/user.go",
"line": 102,
"snippet": "func (s *UserService) Login(ctx context.Context, req LoginReq)"
}
],
"metadata": {
"elapsed_ms": 12
}
}
Agent 可以把 summary 放进上下文,把完整内容放入 artifact 或 state store。
这样可以减少 token 浪费。
8. Tool Registry:工具注册中心
Agent 需要知道有哪些工具可以用。
因此需要一个 Tool Registry:
type ToolRegistry struct {
tools map[string]Tool
}
func NewToolRegistry() *ToolRegistry {
return &ToolRegistry{
tools: make(map[string]Tool),
}
}
func (r *ToolRegistry) Register(t Tool) error {
name := t.Name()
if name == "" {
return fmt.Errorf("tool name is empty")
}
if _, exists := r.tools[name]; exists {
return fmt.Errorf("tool already registered: %s", name)
}
r.tools[name] = t
return nil
}
func (r *ToolRegistry) Get(name string) (Tool, bool) {
t, ok := r.tools[name]
return t, ok
}
func (r *ToolRegistry) Specs() []ToolSpec {
specs := make([]ToolSpec, 0, len(r.tools))
for _, t := range r.tools {
specs = append(specs, ToolSpec{
Name: t.Name(),
Description: t.Description(),
InputSchema: t.InputSchema(),
})
}
return specs
}
ToolSpec 可以这样定义:
type ToolSpec struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema map[string]any `json:"input_schema"`
}
Tool 名称应该稳定、短、语义明确。
推荐命名:
read_file
write_file
list_files
search_code
run_shell
http_request
query_database
create_ticket
send_message
不推荐:
doSomething
helper
tool1
apiCall
functionA
Tool 的描述非常重要,因为 LLM 会根据工具描述判断什么时候使用它。
一个好的工具描述应该说明:
- 这个工具做什么
- 什么时候应该用
- 什么时候不应该用
- 参数含义
- 返回结果是什么
- 有什么限制
例如:
search_code:
Search for code snippets in the current repository.
Use this when you need to locate functions, types, variables, routes, or references.
Input query should be a symbol name, keyword, or regex-like string.
Returns matched file paths, line numbers, and snippets.
9. Tool 参数 Schema
Tool 的参数必须有 Schema。
例如 read_file:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Relative file path to read"
}
},
"required": ["path"]
}
search_code:
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Keyword, symbol, or pattern to search"
},
"limit": {
"type": "integer",
"description": "Maximum number of results",
"default": 20
}
},
"required": ["query"]
}
run_shell:
{
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
},
"working_dir": {
"type": "string",
"description": "Working directory"
}
},
"required": ["command"]
}
注意,run_shell 是高风险工具,必须有额外限制,不能只靠 Schema。
10. State:Agent 的工作记忆
State 是 Agent 当前任务的运行状态。
最小设计:
type State struct {
Goal string
Messages []Message
Steps []Step
Artifacts map[string]Artifact
Variables map[string]any
StartedAt time.Time
}
Step 记录每一步发生了什么:
type Step struct {
Index int
Decision Decision
Observation string
Error string
StartedAt time.Time
FinishedAt time.Time
}
每一轮循环后都应该记录:
- 第几步
- 模型决策
- 调用了哪个工具
- 参数是什么
- 工具返回了什么
- 是否出错
- 耗时多久
- token/cost 使用情况
这样做有几个好处:
- 方便调试
- 方便审计
- 方便恢复
- 方便评估 Agent 能力
- 方便生成最终报告
一个 Agent 如果没有 trace,几乎不可维护。
11. Memory:短期记忆与长期记忆
Agent 的 Memory 可以分为两类:
短期记忆:当前任务内的上下文和步骤记录
长期记忆:跨任务保存的用户偏好、知识、经验
11.1 短期记忆
短期记忆就是当前 State。
例如:
用户目标
当前计划
已完成步骤
最近工具结果
当前阻塞点
这些信息会参与每次 LLM 决策。
11.2 长期记忆
长期记忆可以存储:
- 用户偏好
- 项目背景
- 常用 API
- 历史任务总结
- 代码库结构摘要
- 已验证过的结论
但长期记忆要谨慎,不要什么都存。
尤其不能保存:
- 密码
- API Key
- Token
- 验证码
- 支付信息
- 敏感身份信息
- 业务高敏数据
长期记忆最好有明确策略:
什么能存
什么不能存
存多久
谁可以读取
如何删除
如何更新
12. 上下文构建:不要把所有东西都塞给模型
Agent 每一轮都要构造 prompt/context。
一个常见错误是把所有历史记录、所有工具结果、所有文件内容都塞给模型。
这样会导致:
- token 暴涨
- 成本升高
- 延迟变大
- 模型注意力分散
- 旧信息干扰新决策
更合理的上下文结构:
System prompt
+ Developer instructions
+ User goal
+ Current state summary
+ Current plan
+ Recent steps
+ Relevant observations
+ Available tools
+ Important artifacts references
可以设计成:
func BuildMessages(state *State) []Message {
return []Message{
{
Role: "system",
Content: buildSystemPrompt(),
},
{
Role: "user",
Content: buildUserGoal(state),
},
{
Role: "assistant",
Content: buildStateSummary(state),
},
{
Role: "assistant",
Content: buildRecentSteps(state),
},
}
}
对于长上下文,需要做压缩:
最近 N 步:完整保留
更早步骤:摘要保留
大文件:只保留相关片段
大日志:只保留错误附近上下文
大网页:只保留正文摘要
数据库结果:只保留统计和样例
一个实用策略:
1. 最近 5 步完整加入上下文
2. 之前步骤滚动摘要
3. Tool Result 超过限制则截断
4. 大内容存 Artifact,只在上下文放引用
5. 需要时让 Agent 再调用工具读取 Artifact
13. Planner:什么时候需要显式计划?
简单 Agent 可以没有独立 Planner。
例如:
用户:查一下北京今天温度
Agent:调用天气 API
Agent:返回答案
这种任务不需要计划。
但对于复杂任务,显式计划会很有用:
用户:帮我分析这个项目的鉴权模块,并给出重构建议
这类任务可能涉及:
- 搜索路由
- 阅读中间件
- 阅读 JWT 逻辑
- 阅读权限模型
- 查找调用点
- 对比最佳实践
- 输出重构方案
此时可以引入 Plan:
type Plan struct {
Goal string
Items []PlanItem
}
type PlanItem struct {
ID string
Description string
Status PlanItemStatus
}
type PlanItemStatus string
const (
PlanItemPending PlanItemStatus = "pending"
PlanItemInProgress PlanItemStatus = "in_progress"
PlanItemDone PlanItemStatus = "done"
PlanItemBlocked PlanItemStatus = "blocked"
)
示例计划:
{
"goal": "分析项目鉴权模块并给出重构建议",
"items": [
{
"id": "1",
"description": "定位鉴权相关中间件和路由",
"status": "done"
},
{
"id": "2",
"description": "分析 JWT 生成与校验逻辑",
"status": "in_progress"
},
{
"id": "3",
"description": "检查权限模型和角色控制",
"status": "pending"
}
]
}
Planner 可以有两种实现方式。
13.1 单模型规划执行
一个 LLM 同时负责规划和执行。
流程:
LLM 生成计划
LLM 执行下一步
Tool 返回结果
LLM 更新计划
继续执行
优点:
- 简单
- 成本低
- 实现快
缺点:
- 复杂任务下计划容易漂移
- 状态管理容易混在 prompt 里
13.2 Planner / Executor 分离
Planner 负责制定和更新计划。
Executor 负责执行当前步骤。
Evaluator 负责判断结果是否满足要求。
流程:
Planner -> Plan
Executor -> Execute Step
Tool -> Observation
Evaluator -> Done / Continue / Replan
Planner -> Update Plan
优点:
- 模块清晰
- 适合复杂任务
- 更容易调试
缺点:
- 多次 LLM 调用
- 成本更高
- 延迟更大
- 工程复杂度上升
建议:
第一版不要急着做 Planner / Executor / Evaluator 三层拆分。先把基本 Agent Loop 做稳定,再根据任务复杂度演进。
14. Executor:工具执行层
Executor 负责真正执行工具调用。
它不能只是简单调用函数,还应该处理:
- 参数校验
- 权限判断
- 超时控制
- 重试
- 限流
- 错误包装
- 输出截断
- 审计日志
例如:
func (e *Executor) Execute(ctx context.Context, tool Tool, args map[string]any) (*ToolResult, error) {
if err := e.Validator.Validate(tool.InputSchema(), args); err != nil {
return nil, fmt.Errorf("invalid tool args: %w", err)
}
if err := e.Guardrails.Validate(ctx, tool, args); err != nil {
return nil, fmt.Errorf("tool call rejected: %w", err)
}
toolCtx, cancel := context.WithTimeout(ctx, e.TimeoutFor(tool))
defer cancel()
result, err := tool.Execute(toolCtx, args)
if err != nil {
return nil, fmt.Errorf("tool execution failed: %w", err)
}
result = e.TruncateResultIfNeeded(result)
return result, nil
}
15. Guardrails:生产级 Agent 的关键
Agent 的最大风险不是模型会说错话,而是模型可以驱动工具行动。
如果一个 LLM 可以调用:
run_shell
delete_file
query_database
send_email
deploy_service
transfer_money
那就必须有强约束。
Guardrails 至少包括:
权限控制
参数校验
工具分级
沙箱隔离
危险操作确认
输出过滤
速率限制
审计日志
15.1 Tool 权限分级
可以把工具分成几类:
type PermissionLevel int
const (
PermissionRead PermissionLevel = iota
PermissionWrite
PermissionDangerous
)
示例:
Read:
- read_file
- list_files
- search_code
- query_metrics
Write:
- write_file
- create_ticket
- update_document
Dangerous:
- run_shell
- delete_file
- execute_sql
- send_email
- deploy_service
不同级别采用不同策略:
Read:默认允许
Write:限定范围内允许
Dangerous:默认需要用户确认
15.2 参数校验
即使模型使用了 Tool Calling,也不能完全信任参数。
必须做 Schema Validate。
例如:
if err := ValidateJSONSchema(tool.InputSchema(), decision.Args); err != nil {
return fmt.Errorf("invalid tool args: %w", err)
}
还要做业务校验:
文件路径是否越界
SQL 是否只读
URL 是否在 allowlist
命令是否包含危险操作
参数长度是否超限
15.3 文件系统沙箱
如果 Agent 可以读写文件,必须限制工作目录。
例如:
允许:
/workspace/project/**
禁止:
/etc/passwd
~/.ssh/id_rsa
.env
credentials.json
路径要做规范化,防止:
../../../../etc/passwd
Go 示例:
func IsPathAllowed(baseDir, targetPath string) bool {
absBase, err := filepath.Abs(baseDir)
if err != nil {
return false
}
absTarget, err := filepath.Abs(filepath.Join(baseDir, targetPath))
if err != nil {
return false
}
rel, err := filepath.Rel(absBase, absTarget)
if err != nil {
return false
}
return !strings.HasPrefix(rel, "..") && rel != "."
}
15.4 Shell 沙箱
run_shell 是最危险的工具之一。
至少要限制:
- 工作目录
- 命令白名单/黑名单
- 超时
- 输出大小
- 环境变量
- 网络访问
- 文件系统访问
- 是否允许交互式命令
危险命令示例:
rm -rf
shutdown
reboot
mkfs
dd
curl | sh
wget | sh
chmod 777
sudo
ssh
scp
实际生产中,最好用容器或专门沙箱执行命令。
15.5 HTTP 工具限制
如果 Agent 可以请求 HTTP,需要防止 SSRF。
限制项:
只允许访问 allowlist domain
禁止访问内网 IP
禁止访问 metadata service
限制 method
限制 body 大小
限制响应大小
限制重定向
危险地址示例:
http://127.0.0.1
http://localhost
http://169.254.169.254
http://10.0.0.1
http://192.168.1.1
15.6 数据库工具限制
如果 Agent 可以查询数据库,建议:
默认只读
禁止 DELETE/UPDATE/INSERT/DROP/ALTER
限制查询超时
限制返回行数
限制扫描行数
使用只读账号
敏感字段脱敏
不要让 Agent 直接拿生产库写权限。
15.7 Human-in-the-loop
高风险动作前应该请求用户确认。
例如:
Agent 想执行:
delete_file("internal/auth/legacy.go")
需要向用户确认:
我准备删除 internal/auth/legacy.go,原因是它已经没有引用。
是否继续?
确认机制可以是:
type ConfirmationRequest struct {
ToolName string
Args map[string]any
Reason string
Risk string
}
Agent Loop 遇到需要确认时可以暂停:
status = waiting_for_confirmation
用户确认后继续执行。
16. Evaluator:如何判断任务完成?
Agent 需要知道什么时候可以结束。
简单任务可以让 LLM 自己决定是否 final。
但复杂任务建议加入 Evaluator。
Evaluator 可以是规则,也可以是 LLM。
16.1 规则型 Evaluator
例如:
如果测试通过,则任务完成
如果查到订单状态,则任务完成
如果生成文件成功,则任务完成
规则型可靠、便宜、可控。
16.2 LLM 型 Evaluator
对于开放任务,可以让模型判断:
当前结果是否满足用户目标?
是否还有明显遗漏?
是否需要继续调用工具?
但是不能完全依赖 LLM 判断,最好配合硬性停止条件。
16.3 Stop Conditions
必须有硬限制:
type StopConfig struct {
MaxSteps int
MaxWallTime time.Duration
MaxToolErrors int
MaxTokens int
MaxCostUSD float64
}
每轮循环都检查:
func (a *Agent) ShouldStop(state *State) bool {
if len(state.Steps) >= a.StopConfig.MaxSteps {
return true
}
if time.Since(state.StartedAt) > a.StopConfig.MaxWallTime {
return true
}
if state.ToolErrorCount >= a.StopConfig.MaxToolErrors {
return true
}
return false
}
17. Prompt 设计
Agent 的 System Prompt 要明确告诉模型:
- 它是什么角色
- 它的目标是什么
- 它可以使用哪些工具
- 什么时候调用工具
- 什么时候停止
- 哪些操作禁止
- 输出格式是什么
示例:
You are an autonomous software engineering agent.
Your job is to complete the user's goal by reasoning step by step and using available tools.
At each step, you must either:
1. Call exactly one tool, or
2. Return a final answer.
Use tools when you need:
- file contents
- code search
- execution results
- external facts
- current system state
Do not guess tool results.
Stop when:
- the user's goal is completed
- the task is blocked
- more information is required from the user
- an action requires user confirmation
Constraints:
- Do not call dangerous tools unless explicitly allowed.
- Keep tool arguments minimal and valid.
- If a tool fails, inspect the error and choose a recovery step.
- Do not repeat the same failed action without changing strategy.
Final answer should include:
- what was done
- key findings
- verification performed
- remaining risks or limitations
如果不使用模型厂商的 Tool Calling,而是要求模型输出 JSON,可以这样写:
Return exactly one JSON object.
For a tool call:
{
"type": "tool_call",
"tool_name": "name_of_tool",
"args": {}
}
For final answer:
{
"type": "final",
"content": "..."
}
Do not output Markdown.
Do not output explanations outside JSON.
但实际工程中,优先使用原生 Tool Calling。
18. 一个最小 Go Agent 项目结构
可以按下面的目录组织:
agent/
├── agent.go
├── llm.go
├── decision.go
├── state.go
├── tool.go
├── registry.go
├── executor.go
├── guardrails.go
├── prompt.go
├── errors.go
└── tools/
├── read_file.go
├── write_file.go
├── search_code.go
├── run_shell.go
└── http_request.go
核心类型:
type Agent struct {
LLM LLM
Tools *ToolRegistry
Executor *Executor
Guardrails Guardrails
MaxSteps int
}
运行结果:
type Result struct {
Final string
Steps []Step
Usage Usage
}
主循环:
func (a *Agent) Run(ctx context.Context, goal string) (*Result, error) {
state := NewState(goal)
for i := 0; i < a.MaxSteps; i++ {
messages := BuildMessages(state)
resp, err := a.LLM.Generate(ctx, GenerateRequest{
Messages: messages,
Tools: a.Tools.Specs(),
})
if err != nil {
return nil, err
}
decision := resp.Decision
if decision.Type == DecisionFinal {
return &Result{
Final: decision.Content,
Steps: state.Steps,
Usage: state.Usage,
}, nil
}
if decision.Type != DecisionToolCall {
state.AddObservation("invalid decision type")
continue
}
tool, ok := a.Tools.Get(decision.ToolName)
if !ok {
state.AddObservation("unknown tool: " + decision.ToolName)
continue
}
startedAt := time.Now()
obs, err := a.Executor.Execute(ctx, tool, decision.Args)
step := Step{
Index: i + 1,
Decision: decision,
StartedAt: startedAt,
FinishedAt: time.Now(),
}
if err != nil {
step.Error = err.Error()
state.AddStep(step)
continue
}
step.Observation = obs.Summary
state.AddStep(step)
for _, artifact := range obs.Artifacts {
state.Artifacts[artifact.ID] = artifact
}
}
return nil, ErrMaxStepsExceeded
}
19. 示例:代码分析 Agent
假设要实现一个代码分析 Agent,它需要这些工具:
list_files
read_file
search_code
run_tests
用户输入:
帮我分析这个 Go 项目的用户登录流程,并指出可能的问题。
Agent 的执行过程可能是:
Step 1:
tool_call: list_files
observation: found cmd/, internal/, pkg/, go.mod
Step 2:
tool_call: search_code(query="login")
observation: found internal/api/login.go, internal/service/auth.go
Step 3:
tool_call: read_file(path="internal/api/login.go")
observation: LoginHandler parses username/password and calls AuthService.Login
Step 4:
tool_call: read_file(path="internal/service/auth.go")
observation: AuthService.Login verifies password and generates JWT
Step 5:
tool_call: search_code(query="GenerateToken")
observation: found internal/auth/jwt.go
Step 6:
tool_call: read_file(path="internal/auth/jwt.go")
observation: token expiration is 30 days, secret is read from config
Step 7:
final:
总结登录流程和风险点
最终回答可以包括:
登录流程:
1. POST /login 进入 LoginHandler
2. 参数绑定 username/password
3. 调用 AuthService.Login
4. 查询用户
5. 校验密码
6. 生成 JWT
7. 返回 token
发现的问题:
1. 登录失败错误信息可能泄露用户是否存在
2. 未发现登录频率限制
3. JWT 有效期较长
4. 未发现 refresh token 机制
5. 密码校验逻辑需要确认是否使用安全哈希算法
建议:
1. 增加 rate limit
2. 统一错误提示
3. 缩短 access token 过期时间
4. 增加 refresh token
5. 增加登录审计日志
20. Tracing 与可观测性
Agent 一定要有 Trace。
每次运行应该记录:
run_id
user_goal
model_name
start_time
end_time
step_count
每一步的 decision
每一步的 tool call
每一步的 tool args
每一步的 observation
每一步的 error
每一步耗时
token usage
cost
最终结果
Trace 示例:
{
"run_id": "run_123",
"goal": "分析登录流程",
"steps": [
{
"index": 1,
"type": "tool_call",
"tool_name": "search_code",
"args": {
"query": "login"
},
"observation": "Found 2 matches",
"elapsed_ms": 31
}
],
"final": "登录流程如下..."
}
没有 trace,就很难回答这些问题:
为什么 Agent 调用了这个工具?
为什么它漏掉了某个文件?
为什么它陷入循环?
为什么它给了错误结论?
哪个工具最慢?
哪一步 token 消耗最大?
21. 错误处理策略
Agent 会频繁遇到错误:
- LLM 请求失败
- LLM 输出格式错误
- 工具不存在
- 工具参数错误
- 工具执行超时
- 文件不存在
- API 返回 500
- 数据库查询失败
- 权限不足
不要一出错就直接终止,也不要无限重试。
可以分类处理。
21.1 可恢复错误
例如:
文件不存在
搜索无结果
HTTP 404
命令执行失败
把错误作为 observation 交给 LLM,让它调整策略:
Tool read_file failed: file not found: internal/auth.go
模型可能下一步改用:
list_files
search_code
21.2 不可恢复错误
例如:
权限不足
用户未授权
达到最大预算
危险操作被拒绝
上下文严重超限
这类应该终止或请求用户干预。
21.3 重试策略
对网络错误可以重试:
type RetryConfig struct {
MaxRetries int
BaseDelay time.Duration
MaxDelay time.Duration
}
但对逻辑错误不要盲目重试。
例如:
JSON schema validation failed
file path not allowed
dangerous command rejected
这些错误重复执行也不会成功。
22. 多 Agent 要不要做?
很多人一开始就想做多 Agent:
Planner Agent
Coder Agent
Reviewer Agent
Tester Agent
Manager Agent
听起来很高级,但工程上复杂度会迅速增加:
- 多个 Agent 之间如何通信
- 状态如何共享
- 谁有最终决策权
- 出现冲突怎么办
- token 成本明显上升
- trace 更复杂
- debug 更困难
大多数场景,一开始不需要多 Agent。
建议路线:
单 Agent + 多工具
-> 单 Agent + Planner
-> 单 Agent + Evaluator
-> 必要时再拆多 Agent
什么时候适合多 Agent?
任务天然有多个角色
不同角色需要不同上下文
不同角色需要不同工具权限
需要交叉审查
任务非常复杂且可并行
例如:
软件开发 Agent:
- Planner 负责需求拆解
- Coder 负责改代码
- Reviewer 负责审查 patch
- Tester 负责运行测试
但即使这样,也可以先在一个 Agent 内用不同 prompt 阶段模拟,而不是上来就多 Agent 通信框架。
23. Agent 的几种常见模式
23.1 Tool-Using Agent
最基础模式:
LLM -> Tool Call -> Observation -> LLM -> Final
适合:
查询类任务
简单 API 操作
客服辅助
数据检索
23.2 ReAct Agent
经典模式:
Reasoning + Acting
Thought -> Action -> Observation -> Thought -> Action -> Observation -> Final
工程上不一定要暴露完整 Thought,可以只保留结构化 decision。
23.3 Plan-and-Execute Agent
先规划,再执行。
Plan -> Execute Step 1 -> Execute Step 2 -> ... -> Final
适合复杂任务。
23.4 Reflection Agent
执行后自我检查:
Draft -> Critique -> Revise
适合:
写作
代码生成
方案设计
总结报告
23.5 Workflow Agent
把 Agent 限制在固定流程内:
Step A -> Step B -> Step C
LLM 只在部分节点决策。
这种模式生产最稳。
例如客服退款流程:
1. 查询订单
2. 判断是否符合退款规则
3. 生成处理意见
4. 高风险情况转人工
很多业务系统不应该追求完全自主 Agent,而应该做:
LLM + Workflow + Tools
这比完全开放式 Agent 更可控。
24. 生产环境建议
如果要把 Agent 用在真实业务中,建议至少满足这些条件:
1. 所有工具都有 Schema
2. 所有工具调用都有权限控制
3. 危险操作必须确认
4. Agent 有最大步数和超时
5. Tool Result 有长度限制
6. 所有运行都有 Trace
7. 错误可以被观测和复盘
8. 成本可以统计
9. Prompt 有版本管理
10. 模型输出有结构化约束
额外建议:
使用只读工具起步
先做内部辅助,不直接自动执行高风险动作
对每个 Agent Run 生成审计日志
对关键任务做离线评测
建立失败样例集
持续优化工具描述和 prompt
25. 开发路线图
推荐按下面顺序实现。
第一阶段:单轮 Tool Calling
目标:
用户输入
-> LLM 选择工具
-> 执行工具
-> 返回结果
实现:
LLM interface
Tool interface
Tool registry
Tool calling parsing
第二阶段:Agent Loop
目标:
支持多步调用工具,直到 final
实现:
Agent Run loop
State
Step trace
MaxSteps
Tool result 回填
第三阶段:Guardrails
目标:
让 Agent 可控
实现:
JSON Schema validation
Tool permission
Timeout
Path sandbox
HTTP allowlist
Dangerous tool confirmation
第四阶段:上下文管理
目标:
控制 token 和信息质量
实现:
Recent steps
State summary
Result truncation
Artifact store
Memory compression
第五阶段:Planner / Evaluator
目标:
提升复杂任务完成率
实现:
Plan generation
Plan update
Completion check
Failure detection
第六阶段:评测与优化
目标:
让 Agent 变得稳定
实现:
Eval dataset
Trace replay
Regression tests
Tool usage metrics
Prompt versioning
Cost tracking
26. 常见坑
26.1 过早做复杂框架
很多 Agent 项目失败,是因为一开始就做:
多 Agent
长期记忆
复杂规划器
自主反思
任务队列
插件市场
但基础 Loop 都没跑稳。
建议先做小而稳:
LLM + Tools + State + Guardrails + Trace
26.2 Tool 太少或太弱
Agent 的能力上限由工具决定。
如果工具只能查一两个 API,Agent 再聪明也做不了复杂任务。
Tool 设计比 Prompt 更重要。
26.3 Tool 描述模糊
模型会根据描述选择工具。
描述不清会导致:
该调用时不调用
不该调用时乱调用
参数填错
重复调用
26.4 没有权限模型
这是最大风险之一。
不要让 LLM 直接控制高权限工具。
26.5 Observation 太长
日志、文件、网页、数据库结果很容易撑爆上下文。
一定要:
摘要
截断
分页
Artifact 化
按需读取
26.6 没有评测
Agent 看起来能跑,不代表可靠。
应该准备典型任务集:
简单任务
复杂任务
异常任务
工具失败任务
权限拒绝任务
边界任务
每次改 Prompt、换模型、改工具后跑回归测试。
27. 一个基础可用 Agent 的判断标准
当你的系统满足下面这些条件,就可以称为一个基础可用的 Agent:
1. 它能接收一个目标,而不是只回答一句问题
2. 它能自主选择工具
3. 工具结果会进入下一步决策
4. 它能多步推进任务
5. 它有状态管理
6. 它有停止条件
7. 它有错误处理
8. 它有权限控制
9. 它有执行 trace
10. 它能输出最终结果
更进一步的生产标准:
1. 工具参数有 Schema 校验
2. 高风险动作需要确认
3. 文件、网络、数据库访问有沙箱
4. 上下文有压缩和裁剪
5. 成本和 token 可统计
6. Prompt 和工具版本可追踪
7. 任务结果可评测
8. 失败案例可复盘
28. 总结
Agent 并不是一个神秘概念。
从工程角度看,Agent 本质上是一个围绕目标运行的闭环系统:
Goal
-> Context
-> LLM Decision
-> Tool Call
-> Observation
-> State Update
-> Next Decision
-> Final
最小可行架构是:
LLM interface
+ Tool interface
+ Tool registry
+ Agent loop
+ State trace
+ Guardrails
+ Stop conditions
不要一开始就陷入多 Agent、复杂记忆、复杂规划框架。
正确的开发顺序应该是:
先让 Agent 能稳定调用工具
再让它能多步执行
再让它可控、安全、可观测
再优化规划和记忆
最后再考虑多 Agent 协作
最后,用一句话收尾:
一个好的 Agent,不是让 LLM 自由发挥,而是给 LLM 一个清晰目标、一组可靠工具、一套严格边界,以及一个可观测、可恢复、可迭代的运行时。

浙公网安备 33010602011771号