从零构建AI智能体:理解大语言模型自主决策的核心原理与实现
1. 项目概述从零构建智能体理解AI自主决策的基石最近在GitHub上看到一个挺有意思的项目叫“agents-from-scratch”。光看名字很多朋友可能就明白了这是一个关于“智能体”的项目而且强调“从零开始”。在AI领域尤其是大语言模型应用开发中“智能体”这个概念已经火了好一阵子了。但说实话市面上很多教程和框架要么是直接调用现成的API把底层逻辑封装得严严实实要么就是概念讲得天花乱坠但一落到代码实现就语焉不详。对于想真正理解智能体如何运作、其内部决策循环是怎样构建的开发者来说总感觉隔着一层纱。这个项目恰好击中了这个痛点。它不是一个庞大的、功能齐全的框架而更像是一个精心设计的“教学标本”或“最小可行产品”。它的核心目标很明确剥离复杂的工程化包装用最简洁、清晰的代码展示一个基于大语言模型的智能体是如何被构建、如何思考、如何执行任务并持续学习的。无论你是刚接触AI应用开发的新手还是想深入理解智能体底层机制的老手通过亲手实现一遍这个项目都能获得远超阅读文档的深刻理解。它解决的不仅仅是“怎么用”的问题更是“为什么这么用”以及“还能怎么变”的问题。简单来说pguso/agents-from-scratch项目为你提供了一个绝佳的实验室让你能亲手搭建并观察一个AI大脑的“思考-行动-观察”循环。接下来我们就一起深入这个项目的内部看看它是如何设计的我们又该如何利用它来构建自己的智能体。2. 核心架构与设计哲学拆解2.1 什么是“从零开始”的智能体在讨论架构之前我们得先统一对“智能体”和“从零开始”的理解。在这个项目的语境里“智能体”特指一种能够感知环境、根据目标制定计划、执行动作并从中学习的软件实体。它通常围绕一个大语言模型构建利用LLM强大的理解和生成能力作为其“大脑”。而“从零开始”并不意味着我们要从零训练一个大模型那是不现实的。这里的“从零”指的是最小化外部依赖不依赖LangChain、AutoGPT等重型框架仅使用必要的核心库如OpenAI SDK、请求库等。透明化核心循环将智能体的核心决策循环感知-思考-行动-学习用最直白的代码呈现出来每一行代码你都知道它在干什么。可插拔的组件设计将工具调用、记忆管理、规划生成等模块设计成接口清晰的独立组件方便你替换和实验。这种设计哲学的好处是巨大的。它强迫你去理解每个环节的必要性而不是被框架的“魔法”所迷惑。当你自己写了一遍agent.think()和agent.act()的逻辑后你对ReAct、CoT这些模式的理解会完全不同。2.2 项目核心组件与数据流一个典型的智能体系统通常包含以下几个核心组件agents-from-scratch项目也围绕这些展开大脑即大语言模型。负责处理输入信息进行推理、规划和生成下一步动作的指令。项目通常会封装一个统一的模型调用接口方便切换不同的模型提供商如OpenAI、Anthropic、本地模型等。记忆智能体的“经验库”。分为短期记忆当前会话的上下文和长期记忆向量数据库存储的过往经验。记忆模块决定了智能体能否从历史中学习避免重复错误。工具智能体的“手和脚”。是一组可供调用的函数或API如网络搜索、计算器、读写文件、执行代码等。智能体通过模型生成的指令来调用合适的工具。规划器负责将复杂目标拆解为可执行的子任务序列。有时这个功能直接集成在“大脑”的提示词中有时会是一个独立的模块。执行器负责安全地调用工具处理工具返回的结果并将其反馈给大脑进行下一轮思考。数据流通常是循环的用户目标 - 大脑规划 - 选择并调用工具 - 观察工具结果 - 更新记忆 - 判断目标是否达成 - 若未达成则进入下一轮循环。这个项目的代码会清晰地展示这个循环是如何在while循环或递归函数中实现的你会看到提示词如何组装、工具返回结果如何格式化、循环终止条件如何判断等关键细节。注意很多初学者会混淆“智能体”和“简单函数调用”。关键区别在于智能体有多步推理和自主决策的能力。它不会一次性给出所有答案而是像人一样遇到复杂问题会先查资料调用搜索工具再计算调用计算器最后综合信息给出答案。这个动态的过程才是智能体的精髓。3. 环境搭建与核心依赖解析3.1 基础环境准备开始之前你需要一个Python环境建议3.8以上。首先创建一个干净的虚拟环境是个好习惯能避免依赖冲突。# 创建并激活虚拟环境以venv为例 python -m venv venv_agent # Windows venv_agent\Scripts\activate # Linux/Mac source venv_agent/bin/activate接下来安装最核心的依赖。由于项目强调“从零开始”它的依赖列表会非常精简。# 核心依赖用于调用大模型API pip install openai # 可选但常用用于网络请求如果工具包含网页抓取 pip install requests # 可选但常用用于处理环境变量管理API密钥 pip install python-dotenv这就是最基础的核心包了。对比那些动辄安装几十个依赖的框架是不是清爽多了openai库是我们与GPT模型交互的桥梁requests用于构建自定义的网络工具python-dotenv则帮助我们安全地管理敏感的API密钥。3.2 模型API密钥配置智能体的“大脑”需要燃料也就是大模型API的访问权限。这里以OpenAI为例。前往OpenAI平台注册并获取API密钥。在项目根目录创建一个名为.env的文件。在.env文件中写入你的密钥OPENAI_API_KEY你的实际api密钥在Python代码中通过dotenv加载这个密钥from dotenv import load_dotenv import os load_dotenv() # 加载.env文件中的环境变量 api_key os.getenv(OPENAI_API_KEY) # 初始化OpenAI客户端 from openai import OpenAI client OpenAI(api_keyapi_key)重要安全提示务必把.env文件添加到你的.gitignore中绝对不要将包含真实密钥的文件提交到GitHub等公开仓库。这是开发中的基本安全守则。3.3 项目结构初探一个典型的“从零开始”智能体项目其代码结构可能如下所示根据项目实际略有不同agents-from-scratch/ ├── core/ │ ├── __init__.py │ ├── agent.py # 智能体主类包含核心循环逻辑 │ ├── brain.py # 模型封装与调用逻辑 │ └── memory.py # 记忆管理可能包含短期/长期记忆 ├── tools/ │ ├── __init__.py │ ├── base_tool.py # 工具基类定义接口 │ ├── calculator.py # 计算器工具示例 │ └── web_search.py # 网络搜索工具示例 ├── prompts/ # 存放各类提示词模板 │ └── agent_prompt.txt ├── utils/ # 工具函数 │ └── helpers.py ├── .env # 环境变量文件本地不上传 ├── .gitignore # 忽略.env等文件 ├── requirements.txt # 项目依赖 ├── main.py # 主程序入口示例运行脚本 └── README.md这个结构清晰地将智能体的不同功能模块解耦。agent.py里的Agent类会是你的主要操作对象它内部引用了brain和tools。这种结构让你可以轻松地替换其中一个组件比如把OpenAI的“大脑”换成Claude的或者添加一个自己写的数据库查询工具。4. 核心代码实现构建你的第一个智能体4.1 定义工具赋予智能体“手脚”工具是智能体与外界交互的媒介。我们先从定义一个工具基类开始确保所有工具都有统一的调用接口。# tools/base_tool.py from abc import ABC, abstractmethod from typing import Any, Dict class BaseTool(ABC): 所有工具的基类。 def __init__(self, name: str, description: str): self.name name # 工具名称如“calculator” self.description description # 工具描述用于提示词 abstractmethod def run(self, **kwargs) - str: 执行工具的核心方法。 参数: kwargs - 工具运行所需的参数。 返回: str - 工具执行结果的文本描述。 pass def to_dict(self) - Dict[str, Any]: 将工具信息转换为字典方便传递给模型。 return { name: self.name, description: self.description, parameters: self._get_parameters_schema() # 可选获取参数模式 } def _get_parameters_schema(self): 定义工具所需的参数模式JSON Schema格式。用于高级的模型函数调用。 # 这是一个简化版实际项目可能需要更详细的模式定义 return {}接下来实现一个具体的工具比如一个简单的计算器# tools/calculator.py import ast import operator as op from tools.base_tool import BaseTool # 支持的操作符用于安全评估 _operators { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, ast.USub: op.neg, } def _safe_eval(node): 安全地评估一个AST表达式节点。 if isinstance(node, ast.Num): return node.n elif isinstance(node, ast.BinOp): left_val _safe_eval(node.left) right_val _safe_eval(node.right) return _operators[type(node.op)](left_val, right_val) elif isinstance(node, ast.UnaryOp): operand_val _safe_eval(node.operand) return _operators[type(node.op)](operand_val) else: raise TypeError(f不支持的表达式类型: {node}) class CalculatorTool(BaseTool): 一个安全的数学表达式计算器工具。 def __init__(self): super().__init__( namecalculator, description计算一个数学表达式的结果。输入应该是一个像 2 3 * 4 这样的字符串。 ) def run(self, expression: str) - str: try: # 使用ast解析表达式比eval安全因为它限制了可用的操作 tree ast.parse(expression, modeeval) result _safe_eval(tree.body) return f计算器结果: {expression} {result} except (SyntaxError, TypeError, KeyError) as e: return f计算错误: 无法解析表达式 {expression}。错误: {e}为什么这么设计基类抽象BaseTool确保了所有工具都有name,description和run方法这样智能体就能以统一的方式管理和调用它们。安全计算我们使用了Python的ast模块来解析数学表达式而不是直接使用危险的eval()函数。这可以防止恶意代码注入是构建可靠工具的关键。清晰的返回工具返回一个格式化的字符串结果这个结果会被拼接到智能体的上下文中供下一轮思考使用。4.2 构建大脑封装模型调用“大脑”模块负责与LLM API对话并将对话内容格式化为智能体能理解的指令。# core/brain.py from openai import OpenAI from typing import List, Dict, Any class OpenAIBrain: 封装OpenAI API调用的智能体大脑。 def __init__(self, model: str gpt-3.5-turbo, temperature: float 0.1): 初始化大脑。 参数: model: 使用的模型名称。 temperature: 生成文本的随机性越低越确定。 self.client OpenAI() self.model model self.temperature temperature def think(self, messages: List[Dict[str, str]], tools: List[Dict] None) - Dict[str, Any]: 让模型根据历史消息和可用工具进行思考。 参数: messages: 对话历史消息列表。 tools: 可用工具的描述列表。 返回: 模型的完整响应对象。 params { model: self.model, messages: messages, temperature: self.temperature, } # 如果提供了工具启用函数调用或工具调用功能 if tools: # 注意不同模型版本和API版本参数名可能为 functions 或 tools params[tools] tools params[tool_choice] auto # 让模型决定是否调用工具 response self.client.chat.completions.create(**params) return response def parse_response(self, response) - tuple: 解析模型的响应。 返回一个元组(回复文本, 是否决定调用工具, 工具调用信息) choice response.choices[0] message choice.message # 检查模型是否想调用工具 tool_calls getattr(message, tool_calls, None) if tool_calls: # 模型决定调用工具 # 通常第一个工具调用是主要的 tool_call tool_calls[0] tool_name tool_call.function.name # 注意参数是JSON字符串需要解析 import json try: tool_args json.loads(tool_call.function.arguments) except json.JSONDecodeError: tool_args {} return , True, {name: tool_name, args: tool_args} else: # 模型直接回复文本 return message.content, False, {}关键点解析消息格式messages参数遵循OpenAI的聊天格式是一个字典列表包含role(如user,assistant,system) 和content。工具调用我们使用了OpenAI的tools参数。当模型认为需要调用工具时它不会在content中回复文本而是返回一个tool_calls结构指示要调用哪个工具以及参数是什么。这比让模型在文本中输出“我要调用计算器参数是23”要规范和安全得多。温度参数对于智能体任务通常设置较低的temperature(如0.1-0.3)以确保其决策和行为更加稳定、可预测减少随机性带来的不确定性。4.3 实现智能体主类串联循环现在我们将工具和大脑组合起来形成智能体的核心循环。# core/agent.py from typing import List, Optional from .brain import OpenAIBrain from tools.base_tool import BaseTool class Agent: 智能体主类管理核心的思考-行动循环。 def __init__(self, brain: OpenAIBrain, tools: List[BaseTool], system_prompt: str, max_iterations: int 10): 初始化智能体。 参数: brain: 智能体的大脑模型封装。 tools: 可用的工具列表。 system_prompt: 定义智能体角色和能力的系统提示词。 max_iterations: 最大循环次数防止无限循环。 self.brain brain self.tools {tool.name: tool for tool in tools} # 用字典方便按名查找 self.system_prompt system_prompt self.max_iterations max_iterations # 对话历史记忆 self.messages [ {role: system, content: system_prompt} ] def run(self, user_input: str) - str: 执行智能体任务。 参数: user_input: 用户的任务指令。 返回: 智能体的最终回答。 # 添加用户输入到历史 self.messages.append({role: user, content: user_input}) iteration 0 while iteration self.max_iterations: iteration 1 print(f\n--- 迭代第 {iteration} 次 ---) # 1. 思考让大脑基于当前历史决定下一步 # 准备工具描述给大脑 tool_descriptions [tool.to_dict() for tool in self.tools.values()] response self.brain.think(self.messages, tool_descriptions) # 2. 解析响应 text_response, wants_to_use_tool, tool_info self.brain.parse_response(response) # 3. 处理响应 if wants_to_use_tool: # 模型决定使用工具 tool_name tool_info[name] tool_args tool_info[args] print(f智能体决定使用工具: {tool_name}, 参数: {tool_args}) # 查找并执行工具 if tool_name in self.tools: tool self.tools[tool_name] try: # 执行工具 tool_result tool.run(**tool_args) print(f工具执行结果: {tool_result}) # 将工具调用和结果添加到历史中格式很重要 # 添加工具调用消息assistant的角色但包含tool_calls # 注意这里简化处理实际需按API要求构造完整的tool_calls消息 # 更简单的做法将工具结果作为一条新消息附加 self.messages.append({ role: tool, content: tool_result, # 实际API调用中这里可能需要关联tool_call_id }) except Exception as e: error_msg f调用工具 {tool_name} 时出错: {e} print(error_msg) self.messages.append({role: tool, content: error_msg}) else: error_msg f未知工具: {tool_name} print(error_msg) self.messages.append({role: tool, content: error_msg}) else: # 模型直接给出了文本回答 print(f智能体回复: {text_response}) # 将助手的回复添加到历史 self.messages.append({role: assistant, content: text_response}) # 检查是否任务完成这里可以根据回复内容或预设条件判断 # 例如可以检测回复中是否包含特定结束语或者由用户判断。 # 为简化我们假设当模型不调用工具而直接给出较长回复时任务完成。 if len(text_response) 10: # 一个简单的启发式条件 print(任务似乎已完成。) return text_response # 否则继续循环例如模型可能说“我需要更多信息” # 如果达到最大迭代次数 return f达到最大迭代次数({self.max_iterations})仍未完成任务。最后上下文:\n{self.messages[-5:]} # 返回最后几条消息供调试核心循环逻辑详解初始化智能体被赋予一个系统提示词、一个大脑和一组工具。系统提示词至关重要它定义了智能体的角色、能力和行为准则。循环while循环模拟了智能体的“思考-行动”周期。每次迭代智能体都会根据完整的对话历史self.messages来决定下一步。思考brain.think()方法将历史消息和工具描述发送给LLM。LLM会分析当前情况决定是直接回答还是调用某个工具来获取更多信息。行动如果LLM决定调用工具代码会找到对应的工具对象传入参数并执行。工具执行的结果成功或失败会被格式化成一条新的消息添加到历史中。这条消息的角色通常是tool。观察与学习工具执行结果成为历史的一部分在下一轮循环中LLM就能“看到”这个结果并基于此进行下一步推理。这就是智能体从环境中学习的方式。终止条件循环在两种情况下结束a) LLM给出了一个看似最终答案的文本回复这里用简单的长度判断实际项目可能需要更复杂的逻辑如检测特定关键词或由另一个模型判断b) 达到最大迭代次数防止陷入死循环。4.4 设计系统提示词定义智能体人格系统提示词是智能体的“宪法”它从根本上指导模型的行为。一个好的提示词能极大提升智能体的效率和可靠性。# 一个示例系统提示词 SYSTEM_PROMPT 你是一个有帮助的AI助手并且可以使用工具来完成任务。 你的目标是以准确和高效的方式回应用户的查询。 # 能力 - 你可以进行对话和回答问题。 - 当你需要计算、获取实时信息或执行其他无法仅凭内部知识完成的任务时你应该使用提供的工具。 - 你一次只能使用一个工具。 # 工具使用规范 1. 仔细分析用户的问题判断是否需要使用工具。 2. 如果需要在你的回复中**只**输出工具调用不要包含任何其他解释文本。 3. 工具调用必须以特定格式输出系统会识别这个格式。 4. 工具执行后你会看到结果。基于结果继续思考下一步是回答问题还是再次使用工具。 # 输出格式 当决定使用工具时你必须严格按照以下JSON格式输出且不要有任何其他文字 { tool: 工具名称, args: { 参数名1: 值1, 参数名2: 值2 } } # 示例 用户计算一下345乘以678是多少 助手{tool: calculator, args: {expression: 345 * 678}} 用户今天的天气怎么样 助手{tool: web_search, args: {query: 今日天气 [用户所在城市]}} 记住如果你能直接回答就不要使用工具。直接给出友好、准确的回答。 提示词设计心得角色明确开宗明义告诉模型“你是谁”。能力界定清晰说明你能做什么用工具不能做什么纯知识截止到某时间点。规则具体工具使用的触发条件、调用格式必须极其明确。示例是最有效的方式。格式强制要求模型输出严格的JSON或特定格式这便于代码解析减少错误。在实际使用OpenAI的tools参数时格式由API控制提示词可以更侧重于任务规划。安全边界可以加入“如果问题涉及XX你应该拒绝回答”等条款为智能体设定安全护栏。5. 运行与调试让你的智能体动起来5.1 组装并运行智能体让我们写一个主程序把上面所有的部件组装起来并测试一个简单任务。# main.py from core.brain import OpenAIBrain from core.agent import Agent from tools.calculator import CalculatorTool # 假设我们还有一个搜索工具 # from tools.web_search import WebSearchTool import os from dotenv import load_dotenv load_dotenv() def main(): # 1. 初始化大脑 brain OpenAIBrain(modelgpt-3.5-turbo, temperature0.1) # 2. 准备工具 calculator CalculatorTool() # web_searcher WebSearchTool() # 需要实现这个工具 tools [calculator] # 暂时只有计算器 # 3. 系统提示词 (使用上面定义的或微调) system_prompt SYSTEM_PROMPT # 这里需要将上面定义的提示词字符串赋值给SYSTEM_PROMPT变量 # 4. 创建智能体 agent Agent(brainbrain, toolstools, system_promptsystem_prompt, max_iterations5) # 5. 运行任务 user_query 请帮我计算 (15 27) * 3 的结果然后告诉我这个结果除以6是多少 print(f用户提问: {user_query}) final_answer agent.run(user_query) print(f\n 最终答案 \n{final_answer}) # 打印完整对话历史以供分析 print(f\n 完整对话历史 ) for msg in agent.messages: print(f{msg[role].upper()}: {msg.get(content, N/A)[:100]}...) # 截断显示 if __name__ __main__: main()运行这个程序你可能会看到类似以下的输出用户提问: 请帮我计算 (15 27) * 3 的结果然后告诉我这个结果除以6是多少 --- 迭代第 1 次 --- 智能体决定使用工具: calculator, 参数: {expression: (15 27) * 3} 工具执行结果: 计算器结果: (15 27) * 3 126 --- 迭代第 2 次 --- 智能体回复: 第一步计算结果是126。现在我需要计算126除以6。 --- 迭代第 3 次 --- 智能体决定使用工具: calculator, 参数: {expression: 126 / 6} 工具执行结果: 计算器结果: 126 / 6 21.0 --- 迭代第 4 次 --- 智能体回复: 所以(15 27) * 3 126然后126除以6等于21。 最终答案 所以(15 27) * 3 126然后126除以6等于21。看智能体成功地将一个复杂问题分解成了两个步骤并正确地调用了两次计算器工具最终给出了答案。这就是智能体多步推理能力的直观体现。5.2 调试技巧与常见问题在开发智能体时你会遇到各种问题。以下是一些常见坑点和调试技巧智能体陷入循环不断调用同一个工具原因工具返回的结果没有给模型提供新的、有用的信息或者提示词没有引导模型根据结果进行决策。排查打印每一轮迭代的完整消息历史 (agent.messages)。检查工具返回的结果是否清晰。模型是否误解了结果解决优化工具返回结果的格式使其更易于理解。在系统提示词中强调“基于工具结果决定下一步行动”。可以设置更严格的最大迭代次数。模型不按格式输出导致解析失败原因提示词中对输出格式的要求不够严格或者模型“自由发挥”了。排查打印模型在调用工具前的原始回复 (response.choices[0].message.content)。解决使用OpenAI的tools或functions参数让API来强制管理工具调用格式这比依赖模型的文本输出要稳定得多。这也是我们之前在OpenAIBrain中使用tools参数的原因。工具调用参数错误原因模型生成的参数格式或类型与工具期望的不匹配。排查打印tool_info[args]检查其值。例如计算器期望一个字符串表达式但模型可能传了一个数字。解决在工具的描述 (description) 中更详细地说明参数格式。可以使用JSON Schema在_get_parameters_schema中精确定义参数类型并在调用大脑时传入这样模型能更好地理解。API调用开销与速率限制问题智能体每轮思考都需要调用一次API复杂任务可能调用多次成本和时间会增加。优化缓存对相同的查询进行缓存。任务规划在提示词中鼓励模型在第一次思考时就规划多个步骤减少来回次数但需注意模型上下文长度限制。选择模型对于简单工具调用gpt-3.5-turbo通常比gpt-4成本低且速度更快且足够胜任。设置超时和重试在网络不稳定或API限流时代码应有重试机制。上下文长度爆炸问题对话历史包含所有工具调用和结果会越来越长可能超过模型的上下文窗口。解决实现“记忆管理”。只保留最近N轮对话或者将早期不重要的对话总结成一段摘要后再存入历史。这就是长期记忆和短期记忆模块需要处理的问题。6. 进阶扩展从玩具到实用工具一个基础的智能体跑通后你可以考虑以下方向进行扩展使其能力更强大、更实用6.1 增强记忆能力基础版本只使用了简单的对话列表作为短期记忆。一个成熟的智能体需要向量记忆使用如ChromaDB、Pinecone或FAISS等向量数据库将历史对话或知识库片段转换为向量存储。当遇到新问题时可以检索最相关的记忆片段注入上下文实现长期记忆和知识增强。记忆总结当对话历史过长时调用另一个LLM对之前的对话进行总结将冗长的历史压缩成一段精炼的摘要然后继续使用摘要进行后续对话从而突破上下文长度限制。6.2 实现更复杂的工具计算器只是开始。你可以集成网络搜索通过调用SerpAPI、Google Search API或爬虫遵守robots.txt获取实时信息。代码执行在一个安全的沙箱环境中执行Python代码片段谨慎有安全风险。文件操作读取、写入、分析本地文件。第三方API连接天气预报、股票、翻译等各类Web API。每个工具都需要仔细设计其输入、输出和错误处理机制。6.3 引入规划与反思高级智能体不是走一步看一步而是能“三思而后行”。任务规划在开始执行前让模型先输出一个完整的计划Plan。例如“步骤1搜索X信息步骤2用Y公式计算步骤3综合结果给出答案。” 然后按计划执行。行动后反思在一次工具调用后不立即进行下一步而是让模型先“反思”一下结果是否合理、是否达成了子目标、下一步是否需要调整计划。这能显著提高复杂任务的完成率。6.4 多智能体协作这是最前沿也最有趣的方向。你可以创建多个具有不同专长如研究员、写手、校对员的智能体让它们通过一个“协调者”或直接通过共享的“黑板”来通信协作共同完成一个宏大任务。这涉及到智能体间的通信协议、冲突解决和整体工作流设计。从pguso/agents-from-scratch这样的项目出发亲手实现每一个基础组件理解每一行代码背后的逻辑是你深入AI智能体世界的绝佳路径。它带给你的不是某个框架的API使用技巧而是构建智能系统的底层思维和能力。当你再去看那些复杂的框架时你会一眼看穿其本质并能根据自己的需求进行定制和优化。这才是“从零开始”的真正价值。