构建你的第一个 Agent
从理论到实践
前面几章我们理解了 Agent 的核心概念——推理循环、工具调用、架构模式、记忆管理。现在把这些拼在一起,从零构建一个能用的 Agent。
我们要构建的是一个文件分析 Agent:给它一个目录路径,它会自主地浏览文件、分析代码结构,回答你关于这个项目的问题。
Agent 循环的最小实现
Agent 的核心是一个 while 循环。以下是用 Python + Anthropic SDK 的最小实现:
import anthropic
import json
import os
client = anthropic.Anthropic()
# 定义工具
tools = [
{
"name": "list_directory",
"description": "列出目录中的文件和子目录",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "目录路径"}
},
"required": ["path"]
}
},
{
"name": "read_file",
"description": "读取文件内容。适用于文本文件,如代码、配置文件、文档等",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "文件路径"},
"limit": {"type": "integer", "description": "最多读取的行数,默认 100"}
},
"required": ["path"]
}
},
{
"name": "search_files",
"description": "在目录中搜索包含指定文本的文件",
"input_schema": {
"type": "object",
"properties": {
"directory": {"type": "string", "description": "搜索目录"},
"pattern": {"type": "string", "description": "搜索的文本模式"}
},
"required": ["directory", "pattern"]
}
}
]
# 工具执行
def execute_tool(name, params):
if name == "list_directory":
path = params["path"]
try:
entries = os.listdir(path)
return json.dumps(entries[:50]) # 限制数量
except Exception as e:
return json.dumps({"error": str(e)})
elif name == "read_file":
try:
with open(params["path"], "r") as f:
lines = f.readlines()[:params.get("limit", 100)]
return "".join(lines)
except Exception as e:
return json.dumps({"error": str(e)})
elif name == "search_files":
results = []
for root, dirs, files in os.walk(params["directory"]):
for file in files:
filepath = os.path.join(root, file)
try:
with open(filepath, "r") as f:
for i, line in enumerate(f, 1):
if params["pattern"] in line:
results.append(f"{filepath}:{i}: {line.strip()}")
except:
continue
return json.dumps(results[:20]) # 限制结果数量
# Agent 主循环
def run_agent(user_message):
messages = [{"role": "user", "content": user_message}]
system = "你是一个文件分析助手。使用提供的工具来浏览和分析项目文件,回答用户的问题。"
max_iterations = 20
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=system,
tools=tools,
messages=messages
)
# 把模型的回复加入消息历史
messages.append({"role": "assistant", "content": response.content})
# 如果模型不再调用工具,任务完成
if response.stop_reason == "end_turn":
# 提取最终文本回复
for block in response.content:
if hasattr(block, "text"):
return block.text
return "任务完成。"
# 处理工具调用
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
return "达到最大迭代次数,任务未完成。"
# 使用
answer = run_agent("分析 ./my-project 的项目结构,告诉我这是什么类型的项目")
print(answer)
这 100 行代码就是一个完整的 Agent。核心就是 while 循环——不断让模型思考和行动,直到它决定给出最终回答,或者达到迭代上限。
关键设计决策
工具粒度
上面的例子有三个工具:列目录、读文件、搜索。这个粒度是有意为之的:
- 太少(只有一个 "analyze_project" 工具):模型失去了灵活性,不能按需探索
- 太多(20 个细粒度工具):模型容易选错工具,上下文被工具定义占满
- 刚好(3-8 个):模型能清晰区分每个工具的用途
一个实用的经验:先从 3-5 个核心工具开始,根据模型实际表现再调整。
工具描述
对比这两个描述:
❌ "读取文件"
✅ "读取文件内容。适用于文本文件,如代码、配置文件、文档等"
好的描述告诉模型三件事:做什么、什么时候用、什么时候不用。 在面向 Agent 的工具描述中要比面向人的更具体。
错误处理
注意上面代码中每个工具执行都包裹了 try-catch,返回清晰的错误信息。这是因为:
- Agent 执行过程中一定会遇到错误(路径不存在、权限不足、文件太大)
- 清晰的错误信息让模型能自我纠正
- 如果工具直接抛异常导致程序崩溃,Agent 循环就中断了
安全考量
让 AI 自主执行操作,安全是最大的顾虑。
最小权限原则
只给 Agent 它需要的能力,不多给。上面的例子只能读文件和搜索,不能写文件、不能执行命令、不能访问网络。
# 危险:给 Agent 执行任意命令的能力
def execute_command(cmd):
return os.popen(cmd).read() # 永远不要这样做
# 安全:限定在特定目录的只读操作
def read_file(path):
# 验证路径在允许的目录内
allowed_dir = "/home/user/projects"
real_path = os.path.realpath(path)
if not real_path.startswith(allowed_dir):
return {"error": "路径不在允许范围内"}
# ... 读取文件
路径验证
Agent 可能构造出意想不到的路径——比如 ../../etc/passwd。永远验证路径是否在允许的范围内。
操作确认
对于有副作用的操作(写文件、发请求、删除数据),加一层人类确认:
def execute_tool_with_confirmation(name, params):
if name in ["write_file", "delete_file", "send_request"]:
print(f"Agent 想要执行: {name}({params})")
confirm = input("是否允许?(y/n): ")
if confirm != "y":
return {"error": "用户拒绝了此操作"}
return execute_tool(name, params)
资源限制
防止 Agent 失控:
- 最大迭代次数:前面代码中的
max_iterations = 20 - token 预算:限制单次任务的最大 token 消耗
- 超时:长时间未完成的任务应该被终止
- 文件大小限制:避免读取巨大文件撑爆上下文
调试与评估
Agent 比普通 LLM 调用更难调试——因为行为不确定,同样的输入可能走完全不同的路径。
日志是最重要的工具
记录 Agent 的每一步:思考了什么、调用了什么工具、得到了什么结果。
# 在 Agent 循环中添加日志
for block in response.content:
if hasattr(block, "text"):
print(f"[思考] {block.text}")
if block.type == "tool_use":
print(f"[工具] {block.name}({json.dumps(block.input)})")
评估标准
怎么判断 Agent 是否在正确工作?
- 任务完成率:给一组测试任务,统计成功完成的比例
- 步骤效率:完成任务用了多少步?有没有冗余步骤?
- 错误恢复:遇到错误时是否成功恢复?
- 成本:每个任务消耗多少 token?
不要追求 100% 的成功率。Agent 的价值在于它能自主处理大部分情况,剩下的交给人类兜底。
主流 Agent 框架
我们从零构建 Agent 是为了理解原理。在实际项目中,你可能会用现成的框架:
Anthropic Agent SDK:Anthropic 官方出品,轻量级,提供 Agent 循环、工具注册、多 Agent 编排(handoff)等基础能力。适合直接使用 Claude 模型的场景。
LangGraph:LangChain 生态的一部分,用图(Graph)来定义 Agent 的工作流。适合需要复杂流程控制的场景。灵活但学习曲线较陡。
CrewAI:专注于多 Agent 协作,用"角色"和"任务"来组织 Agent 团队。适合模拟团队协作的场景。
Mastra:TypeScript 生态的 Agent 框架,内置工作流引擎、RAG、评估等功能。适合 Node.js 技术栈的项目。
选框架的原则和选模型一样:先理解你的需求,再选工具。 如果你的场景用 100 行代码就能实现,不需要引入框架。
要点总结
- Agent 的核心就是一个 while 循环——调用模型、执行工具、把结果喂回去,重复直到完成。100 行代码就能实现一个可用的 Agent。
- 工具设计决定 Agent 的上限:3-8 个核心工具、清晰的描述、有意义的错误信息。
- 安全是第一优先级:最小权限、路径验证、操作确认、资源限制。让 AI 自主执行 ≠ 让 AI 无限制执行。
- 日志和评估不可或缺:记录每一步,用任务完成率和步骤效率来衡量 Agent 质量。
- 框架是可选的——先理解原理再选框架,简单场景不需要框架。