从零开始用 Python 构建智能体
智能体不是魔法,而是工程。理解其本质,才能驾驭未来 AI 的无限可能。
本文以最小可运行代码为例,系统讲解如何用 Python 直接调用大型模型 API,逐步实现对话记忆、工具调用与执行循环,帮助你理解智能体的核心原理与工程边界。
什么是开始构建智能体系统的最佳方式?市面上有无数用于构建智能体的框架,比如 CrewAI、LangGraph 以及 OpenAI Agents SDK,选择其中之一可能让人无所适从。另一方面,Anthropic 建议先直接使用 LLM API 调用来理解基本原理,然后再依赖框架抽象。
本节采用自下而上的方式:不依赖任何智能体框架,直接调用大型模型 API 来搭建一个最小可用的智能体。目标是把核心概念内化 —— 明确「模型、记忆、工具、循环」之间的边界与交互,为后续接入更复杂的智能体框架或多智能体协同打好基础。
从零实现一个智能体
本节将通过逐步整合以下构件来实现一个 Agent() 类,这些构件是智能体的核心组成部分。
- LLM 与指令:驱动智能体推理与决策的 LLM,以及定义智能体行为的显式指令。
- 记忆:Agent 用于理解当前交互的会话历史(短期记忆)。
- 工具:Agent 可调用的外部函数或 API。
最后,我们将把这些部分放在一个循环中一起运行。
组件 1:模型与行为指令(Model & System Prompt)
任何智能体的决策能力都来自一个大语言模型(LLM)。本章以 Google Gemini 为示例,展示如何在初始化时指定模型、传入「系统指令(system prompt)」以限定智能体的行为风格与解决问题的策略。
下面通过伪代码演示如何初始化 Gemini 客户端并发送一次简单对话请求。
# 伪代码示例,按你使用的 Gemini SDK / HTTP API 做适配
from google_gemini import GeminiClient
import os
client = GeminiClient(api_key=os.getenv("GOOGLE_API_KEY"))
system_prompt = (
"You are an assistant that reasons step-by-step and prefers concise, checkable answers."
)
def call_model(prompt, context=None):
"""向 Gemini 发起对话请求(最简示例)"""
resp = client.chat(
model="gemini-pro",
system=system_prompt,
messages=(context or []) + [{"role": "user", "content": prompt}],
max_output_tokens=1024,
temperature=0.0,
)
return resp
现在智能体具备了简单的问答能力。我们来测试一下。
agent = Agent()
response = agent.chat("我有 4 个苹果。你有多少?")
print(response.content[0].text)
I don't have any apples - as an AI, I don't have a physical form, so I can't possess physical objects like apples. Only you have apples in this scenario (4 of them).
Is there something you'd like to do with this information, like a math problem involving your apples?
我没有实体,因此无法拥有实物(例如苹果)。在这个场景中,你有 4 个苹果。
你想用这些信息做什么?例如进行计算或其他操作?
很好。我们接着发送第二条消息。
response = agent.chat("我吃掉了 1 个苹果,还剩多少?")
print(response.content[0].text)
I don't have enough information to answer how many apples are left. To solve this, I would need to know:
**What I need:**
- How many apples you started with
**The calculation would be:**
Starting number of apples - 1 apple eaten = Apples remaining
Could you tell me how many apples you had before eating one?
我没有足够的信息来直接回答还剩多少个苹果。为了解题,我需要知道:
**所需信息:**
- 你最初有多少个苹果
**计算方法:**
起始数量 - 吃掉的数量 = 剩余数量
请告诉我你最开始有多少个苹果。
如你所见,Agent 丢失了第一条消息中的信息。这就是为什么我们需要让智能体可以访问会话历史。
组件 2:会话记忆(短期)
短期记忆就是把最近的对话作为上下文传给模型,以便模型能在多轮对话中保持一致性。最直观的实现是维护一个消息列表(user/assistant),并在每次调用模型时把该列表一并发送。
注意:对话越长会越占用上下文配额。常见做法包括窗口化(只保留最近 N 条)、对话抽象(rollup summary),或把长期信息存入向量数据库并在需要时检索。
下面给出一个简化的智能体类,用 Gemini 的 call_model 封装作为内部调用点:
class Agent:
def __init__(self, client, system_prompt):
self.client = client
self.system_prompt = system_prompt
self.messages = [] # short-term memory
def chat(self, user_text):
# 把用户消息放入会话历史
self.messages.append({"role": "user", "content": user_text})
# 调用模型(将 messages 作为上下文)
resp = call_model(user_text, context=self.messages)
# 假设 resp.content 是字符串形式的回复
assistant_text = resp.content
self.messages.append({"role": "assistant", "content": assistant_text})
return assistant_text
现在,再次用之前的例子测试智能体。
agent = Agent()
response = agent.chat("I have 4 apples. How many do you have?")
print(response.content[0].text)
response = agent.chat("I ate 1 apple. How many are left?")
print(response.content[0].text)
I don't have any apples - as an AI, I don't have a physical form and can't possess physical objects like apples. You have 4 apples, and I have 0 apples.
Is there something you'd like to do with your 4 apples, like a math problem or recipe suggestion?
Let me solve this step by step:
**Step 1:** Identify the starting amount
- You started with 4 apples
**Step 2:** Identify what was consumed
- You ate 1 apple
**Step 3:** Calculate the remaining amount
- Apples left = Starting amount - Apples eaten
- Apples left = 4 - 1 = 3
**Answer:** You have 3 apples left.
如你所见,智能体现在可以保持多轮会话并引用先前的信息。
但如果你让智能体去做更复杂的数学计算,会发生什么?
agent = Agent()
response = agent.chat("请计算 157.09 * 493.89 的结果。")
print(response.content[0].text)
我将按常规乘法分步计算:
157.09 × 493.89
步骤概览(示意):
- 先按位相乘并累计部分乘积
- 将所有部分乘积相加得到最终结果
部分乘积(示例):
1,413.81
12,567.2
47,127
14,138,100
62,836,000
-----------
77,035,208.01
因此,计算结果为 **157.09 × 493.89 = 77,035.2081**(注意:该手算示例用于说明步骤,实际使用工具或计算器更可靠)。
这个答案听起来很可信,但如果你校验它,你会发现即便是像 Gemini 这样的强大 LLM 在没有工具辅助时也可能在算术上出错。
157.09 * 493.89
77585.1801
组件 3:工具(外部功能)
智能体的强大之处在于把模型的“建议”变成可执行的操作:例如进行精确计算、查询数据库或调用外部 API。这里我们用一个极简的计算器工具来演示如何把工具暴露给模型。
关键做法如下:
- 每个工具实现实际的函数/方法(执行行为)。
- 同时给出工具的 schema(名字、描述、参数结构),以便把工具信息传给模型,让模型知道何时以及如何调用。
下面是一个非常简单的计算器工具与把工具信息加入智能体的方法(伪代码):
class CalculatorTool:
def get_schema(self):
return {"name": "calculator", "description": "Evaluate math expressions", "input_schema": {"expression": "string"}}
def execute(self, expression: str):
# 警告:示例使用 eval,仅用于学习,请在生产中使用安全解析器
try:
result = eval(expression)
return {"result": result}
except Exception as e:
return {"error": str(e)}
class AgentWithTools(Agent):
def __init__(self, client, system_prompt, tools):
super().__init__(client, system_prompt)
self.tools = tools
self.tool_map = {t.get_schema()["name"]: t for t in tools}
def chat(self, user_text):
self.messages.append({"role": "user", "content": user_text})
# 把工具 schema 传给模型(如果模型支持 tool-aware 接口)
resp = call_model(user_text, context=self.messages)
# 简化:检测 resp 是否请求工具执行(依赖于模型返回的结构)
if getattr(resp, "stop_reason", None) == "tool_use":
# 假设 resp.content 包含 tool_use 信息
tool_name = resp.tool_name
tool_input = resp.tool_input
tool = self.tool_map[tool_name]
tool_result = tool.execute(**tool_input)
# 将工具结果再作为用户消息继续对话
self.messages.append({"role": "user", "content": str(tool_result)})
return self.chat(str(tool_result))
assistant_text = resp.content
self.messages.append({"role": "assistant", "content": assistant_text})
return assistant_text
上面的示例展示了一个常见模式:模型负责决策(是否应该用工具、用哪个工具、如何填参数),系统负责执行工具并把结果回传给模型,从而完成一个闭环。
组件 4:执行循环(Agent Loop)
将模型决策与工具执行串联起来,就形成了智能体的主循环:模型提出动作,执行器(executor)运行动作并把结果回传,模型基于新状态继续决策。这个循环直到满足终止条件(例如达到目标或达到步数上限)。
下面是一个更紧凑的 run_agent 驱动函数(伪代码),演示如何处理工具调用并把工具结果回传给模型:
def run_agent(user_input, agent, max_turns=10):
i = 0
input_payload = user_input
while i < max_turns:
i += 1
print(f"Iteration {i}: input={input_payload}")
resp = agent.chat(input_payload)
# 假设 resp 包含结构化的 tool_use 指示
if getattr(resp, "stop_reason", None) == "tool_use":
for block in resp.content:
if getattr(block, "type", None) == "tool_use":
name = block.name
params = block.input
tool = agent.tool_map[name]
result = tool.execute(**params)
# 把工具结果作为下一轮的上下文
input_payload = {"type": "tool_result", "tool_use_id": block.id, "content": result}
break
continue
# 否则,模型返回最终文本回复
return resp.content
return None
测试已实现的智能体
下面用一些示例测试该智能体的实现。
测试 1:普通问题(无需工具)
该测试展示智能体能够回答不需要外部工具的简单一般性问题。
response = run_agent("我有 4 个苹果。你有多少?")
Iteration 1:
User input: 我有 4 个苹果。你有多少?
Agent output: 我没有实体,因此无法拥有实物。但我可以帮你计算或处理关于你 4 个苹果的问题。
你想用这些信息做什么,比如计算数量或提供食谱建议?
测试 2:工具调用
该测试演示智能体在遇到需要工具才能解决的任务时,如何使用 CalculatorTool 获取正确结果。
response = run_agent("请计算 157.09 * 493.89 的结果。")
Iteration 1:
User input: 请计算 157.09 * 493.89 的结果。
Agent output: 我将为你计算 157.09 * 493.89。
Using tool calculator with input {'expression': '157.09 * 493.89'}
Tool result: {'result': 77585.1801}
Iteration 2:
User input: [{'type': 'tool_result', 'tool_use_id': 'toolu_01FC9yLWt2Cf6a8zLGhj7ZJz', 'content': '{"result": 77585.1801}'}]
Agent output: 157.09 * 493.89 的结果是 **77,585.1801**。
测试 3:分步工具调用
该测试展示智能体能将更复杂的问题分解成更小的步骤,并在一个会话中多次调用 CalculatorTool 来得到最终答案。
response = run_agent("如果我 20 岁,我妈妈比我大 30 岁,而我弟弟比我妈妈小 32 岁,我弟弟多少岁?")
Iteration 1:
User input: 如果我 20 岁,我妈妈比我大 30 岁,而我弟弟比我妈妈小 32 岁,我弟弟多少岁?
Agent output: 我将按步骤计算这个问题。
已知条件:
- 你:20 岁
- 你妈妈比你大 30 岁
- 你弟弟比你妈妈小 32 岁
先计算你妈妈的年龄:
Using tool calculator with input {'expression': '20 + 30'}
Tool result: {'result': 50}
Iteration 2:
User input: [{'type': 'tool_result', 'tool_use_id': 'toolu_01WPMQRzCi4roua9vQ7qXeCR', 'content': '{"result": 50}'}]
Agent output: 所以你妈妈 50 岁。
现在计算你弟弟的年龄:
Using tool calculator with input {'expression': '50 - 32'}
Tool result: {'result': 18}
Iteration 3:
User input: [{'type': 'tool_result', 'tool_use_id': 'toolu_01UL7n7a85XJUn7Tgk8kiHhX', 'content': '{"result": 18}'}]
Agent output: 你弟弟 18 岁。
总结:
- 你:20 岁
- 你妈妈:50 岁(比你大 30 岁)
- 你弟弟:18 岁(比你妈妈小 32 岁)
总结
通过将模型(决策器)、短期记忆、工具(执行器)与主循环逐步拆解并手工实现,你可以清晰理解每一层的责任边界。以下是一些实用建议:
- 开发初期建议直接用 API 调用理解底层原理(如本文示例的 Gemini 调用),后续再考虑引入框架以节省工程成本。
- 工具执行要有明确的接口和安全边界:不要在生产中直接用 eval;对外部调用需做超时与错误处理。
- 会话增长时采用窗口、摘要或检索记忆方案,避免超出 context 限制。