How (and Why) to Build an AI Agent from Scratch in Python
TL;DR · AI 摘要
How and Why to Build an AI Agent from Scratch in Python By Bala Priya C on October 5, 2026 in Artificial Intelligence 0...
核心要点
- 主题聚焦:How and Why to Build an AI Agent from Scratch in
- 来源:Machine Learning Mastery,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
如何(以及为什么)从零开始用 Python 构建 AI 代理
By
Bala Priya C
on
2026 年 10 月 5 日
in
人工智能
0
分享
发布
在本文中,你将学习什么是 AI 代理,以及如何在不依赖任何编排框架的情况下,使用 Anthropic API 通过纯 Python 从零构建一个 AI 代理。
我们将涵盖的主题包括:
- 为什么需要工具调用以及如何定义模型可以使用的工具。
- 如何实现驱动代理行为的请求-执行-响应循环。
- 如何添加持久化内存,使代理在多轮对话中保持上下文。
介绍
当答案可以由模型已有的知识生成时,简单调用大型语言模型就足够了:解释政策、起草回复、总结文本。一旦答案依赖于训练集之外的数据,例如实时订单状态或数据库中的某一行,这种简单调用就不再足够;这种差距正是代理所要弥补的。
当模型能够调用其他内容(如函数、数据库查询、API)并在回答前使用结果时,就会产生 AI 代理。最终,使用代理编排框架来实现这一点是合理的。
通过纯 Python 和原始 API 调用从零构建一个代理,可以让底层架构更加清晰。本质上,它只是一个模型、一个管理交互的循环,以及一组定义良好的函数,这些函数为模型提供所需的能力。去除框架和抽象层后,更容易理解这些组件如何协同工作以及幕后实际发生了什么。
本文涵盖以下内容:
- 为什么需要工具调用以及需要安装什么才能尝试
- 如何描述工具,让模型知道何时以及如何调用它
- 请求-执行-响应循环在代码中的表现形式
- 如何添加内存,使代理在多轮对话中保持上下文
你可以在本 GitHub 仓库中找到本文的配套代码。
先决条件
你需要 Python 3.10 或更高版本、Anthropic API 密钥以及 Anthropic Python SDK:
pip install anthropic
1
pip
install
anthropic
将密钥设置为环境变量,以便客户端无需硬编码即可获取:
export ANTHROPIC_API_KEY="your-key-here"
export
ANTHROPIC_API_KEY
=
"your-key-here"
这些就是开始所需的全部内容。你可以在 agent.py 脚本中找到所有代码。
⚠️ 给读者的提示:Anthropic 将于 2026 年 11 月 30 日正式停用 Claude Sonnet 4.5。如果你在该日期之后遵循本教程,这些示例中使用的特定模型代码将不再有效。为确保你的项目顺利运行,请将 API 调用中的模型字符串替换为更新版本或最新可用模型。
设置模型调用
围绕 API 的最小封装是一个发送提示并返回文本的函数:
import anthropic client = anthropic.Anthropic() def ask(prompt): response = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, system="You are a helpful support assistant. Be direct and factual.", messages=[{"role": "user", "content": prompt}], ) return response.content[0].text
2
3
4
5
6
7
8
9
10
11
12
import
client
.
(
)
def
ask
prompt
:
response
messages
create
model
"claude-sonnet-4-5"
,
max_tokens
512
system
"You are a helpful support assistant. Be direct and factual."
[
{
"role"
"user"
"content"
}
]
return
content
text
这在处理大量问题时表现良好 —— 解释政策、起草回复、总结段落。但一旦答案需要依赖模型从未见过的数据时就会失败。例如询问"订单 #4471 的状态如何?",模型没有验证机制:这些信息存储在你的数据库中,而非训练数据中。它要么承认不知道,要么强行生成一个看似合理的答案,无论怎么提示都无法改变这一现状,因为没有任何提示能赋予模型访问未提供数据的权限。工具调用为模型提供了明确的请求数据方式,而非自行推断。
阅读《掌握 AI 代理工具调用路线图》了解更多内容。
为模型定义可请求的工具
为模型提供函数访问权限意味着编写该函数,并编写模型可读取的函数描述。描述至关重要,因为它告诉模型函数的作用以及何时应该调用它。
orders_db = {
"4471": {"status": "shipped", "carrier": "UPS", "eta": "2 days"},
"4472": {"status": "processing", "carrier": None, "eta": None},
}
def get_order_status(order_id):
return orders_db.get(order_id, {"error": "No order found with that ID"})get_order_status_schema = {
"name": "get_order_status",
"description": (
"通过订单ID查询客户订单的当前状态。"
"当问题依赖当前订单数据而非通用政策信息时使用此工具。"
),
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "要查询的订单ID,例如 '4471'"},
},
"required": ["order_id"],
},
}13 14 15 16 17 18 19 20 21 22 23
orders_db "4471" "status" "shipped" "carrier" "UPS" "eta" "2 days" "4472" "processing" None get_order_status order_id get "error" "No order found with that ID" get_order_status_schema "name" "get_order_status" "description" "通过订单ID查询客户订单的当前状态。 " "当问题依赖当前订单数据 " "而非通用政策信息时使用此工具。" "input_schema" "type" "object" "properties" "order_id" "string" "要查询的订单ID,例如 '4471'" "required"
该模式是结构化元数据:包含名称、描述以及函数预期参数的定义。工具调用在大多数提供商中工作方式相同:你将这些模式列表与消息一同传递给模型,模型自行决定是否需要调用其中一个工具来回答问题。
执行工具并反馈结果
传入模式后,情况会发生变化。响应将包含 tool_use 的 stop_reason 和描述模型想要调用的函数及其参数的内容块。此时模型尚未执行任何操作 —— 它处于暂停状态,并返回一个需要你处理的请求。
messages = [{"role": "user", "content": "What is the status of order #4471?"}] response = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, tools=[get_order_status_schema], messages=messages, ) if response.stop_reason == "tool_use": tool_call = next(block for block in response.content if block.type == "tool_use") result = get_order_status(**tool_call.input) messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": tool_call.id, "content": str(result), }], }) final = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, tools=[get_order_status_schema], messages=messages, ) print(final.content[0].text)
24
25
26
27
28
29
30
"What is the status of order #4471?"
tools
if
stop_reason
==
"tool_use"
tool_call
next
block
for
type
result
*
input
append
"assistant"
"tool_result"
"tool_use_id"
id
str
final
这里发生了两次往返交互。第一次交互是让模型决定要执行什么操作。你自己运行函数(例如在订单数据库中查询,或在生产系统中调用订单服务),将结果作为 tool_result 追加回对话中,然后重新发送所有内容。第二次往返是模型读取该结果并基于结果生成回答,而不是猜测。
直到模型获得所需信息的循环
一旦停止硬编码单个工具和单次往返交互,这种模式就可以泛化。更复杂的问题可能需要连续调用两个或三个工具——先查询订单,然后检查物流承运商的跟踪API,最后生成回复——你无法提前知道需要多少次调用。因此,你不会逐一编写每一步,而是循环直到模型不再请求工具。
def run_agent(user_input, tools, tool_map, max_iterations=6): messages = [{"role": "user", "content": user_input}] for _ in range(max_iterations): response = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, tools=tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": return response.content[0].text tool_results = [] for block in response.content: if block.type == "tool_use": function = tool_map[block.name] output = function(**block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(output), }) messages.append({"role": "user", "content": tool_results}) return "Stopped after max iterations without a final answer."
run_agent
user_input
tool_map
max_iterations
_
range
!=
tool_results
function
name
output
"Stopped after max iterations without a final answer."
max_iterations 的上限防止了特定的失败情况:如果没有这个限制,模型持续决定需要“再调用一次”工具,会导致循环直到你用完预算或耐心。对循环进行限制并返回清晰的备用消息,只需少量代码即可避免生产环境中出现令人困惑的故障。
剥离周围的代码后,这就是整个思路:一个模型、一个循环,以及一组它能请求你运行的函数。这里没有任何内容是专门针对订单查询的。你可以替换为搜索调用、内部API或自己的查询,只要模式清晰描述,相同的循环机制就能处理。
run_agent 如上所述在返回时会立即忘记所有内容。连续调用两次——第一次询问订单 #4471,第二次问“那什么时候能到?”——第二次调用完全不知道“它”指的是什么,因为每次调用都会从头开始构建全新的消息列表。这里的“记忆”指的是在调用之间保留该列表,而不是每次都丢弃它。
实现这一目标的最简单方法是停止每次传递新的消息列表,而是将其保存在对象上:
class Agent: def __init__(self, tools, tool_map, max_iterations=6): self.tools = tools self.tool_map = tool_map self.max_iterations = max_iterations self.messages = []
def run(self, user_input): self.messages.append({"role": "user", "content": user_input}) for _ in range(self.max_iterations): response = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, tools=self.tools, messages=self.messages, ) self.messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": return response.content[0].text tool_results = [] for block in response.content: if block.type == "tool_use": function = self.tool_map[block.name] output = function(**block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(output), }) self.messages.append({"role": "user", "content": tool_results}) return "Stopped after max iterations without a final answer."
31 32 33 34 35
class Agent __init__ self run
self.messages 在多次调用 run() 时会持续保留,因此模型每次都能看到完整的对话历史,包括之前的工具调用及其结果。先询问订单 #4471,再问“那什么时候能到?”,模型可以从 self.messages 中已有的历史记录中解析出“它”所指的内容。
这解决了单次运行过程中内存保持的问题。但未解决当消息列表超过模型上下文窗口容量时的情况,也未解决进程重启导致 self.messages 重置为空的情况。这些是该方法的真实限制,也正是为什么生产环境中的代理通常会增加一个步骤:修剪或总结较早的对话记录,并将对话历史保存在进程外部——例如数据库记录、文件或与会话 ID 关联的缓存键。
总结与下一步
目前我们已实现模型调用、描述函数的工具模式、直到模型具备足够信息回答问题的工具调用循环,以及跨轮次保留对话的类。这是一个可用的代理,其所有组件都是你编写的代码,可以逐行阅读。
以下是一些值得进一步探索的方向:
- 测试当多个工具争夺同一问题时,模型会选择哪个工具
- 持久化并修剪对话历史以防止超出上下文窗口
- 处理工具调用失败的情况(如超时或上游 API 返回错误响应),避免导致循环崩溃
- 在每个工具调用周围添加日志以便调试和后续审查
愉快地构建吧!
更多相关内容
- 从零构建并理解向量数据库…
- 如何用 Python 构建多代理研究助手
- 为什么从零实现机器学习算法
- 5 个用于构建优化 RAG 系统的 Python 库
- 如何用最少资源构建健壮的 RAG 系统
- 用 LLM 嵌入构建语义搜索
/.entry /.entry