尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手写AI Agent:深入理解核心架构与Python实现

从零手写AI Agent:深入理解核心架构与Python实现 1. 项目概述为什么我们要“徒手”造一个AI Agent最近AI Agent这个概念火得不行好像不提Agent就落伍了。但打开各种教程和框架动辄就是LangChain、AutoGen、CrewAI配置复杂概念一堆对新手来说光是理解这些框架的抽象层就得花上好几天。这让我想起早年学编程从理解指针到写出第一个链表那种“从无到有”的掌控感是直接调用现成库无法替代的。所以我决定做一次“返璞归真”的尝试不使用任何现成的AI Agent框架仅用纯Python从零开始手写一个具备基础能力的AI Agent。这个Agent的核心目标很简单它能理解我的自然语言指令调用我预先赋予它的工具比如计算器、网络搜索模拟并基于结果进行简单的推理和决策最后用自然语言回复我。你可能会问有轮子为什么不用原因有三第一为了彻底理解。框架封装了太多细节就像开自动挡车虽然快但你不明白离合器、变速箱是如何协同工作的。亲手实现一遍你会对Agent的“感知-规划-执行-学习”循环有刻骨铭心的理解。第二为了极致的定制与控制。当你的需求非常特殊或者需要对Agent的每一个决策环节进行审计和干预时一个 stripped-down精简到核心的自建系统是无可替代的。第三为了学习与教学。这是理解AI Agent架构最直观、最深刻的方式没有之一。这个项目适合所有对AI感兴趣具备基础Python语法知识想窥探AI Agent内部奥秘的开发者。我们不需要GPU不需要复杂的深度学习知识核心是逻辑与架构设计。接下来我将带你一步步拆解、构建并分享其中每一个“坑”与“闪光点”。2. 核心架构设计一个AI Agent的“五脏六腑”在动手写代码之前我们必须先想清楚一个最简单的AI Agent应该由哪些核心部件构成。抛开那些花哨的名词我们可以将其抽象为四个基本模块它们构成了Agent的“大脑”和“四肢”。2.1 大脑核心大语言模型接口层这是Agent的“认知中心”。虽然我们说“从零手写”但并非要自己训练一个LLM那是另一个维度的工程。我们这里指的是如何与一个现成的LLM API进行交互。我们将封装一个统一的类来处理与LLM的对话、管理上下文记忆、解析返回结果。关键在于设计一个灵活的提示词模板系统让LLM能按照我们设定的角色和格式进行思考与输出。注意选择LLM API时优先考虑其响应的结构化能力如支持JSON格式输出和上下文长度。对于实验和学习OpenAI的GPT-3.5-Turbo或 Anthropic 的 Claude Haiku 都是成本与效果平衡的好选择。国内的一些平台如百度文心、阿里通义千问也提供了类似的API。2.2 技能仓库工具调用系统Agent的“四肢”就是它能使用的各种工具。一个只会聊天的AI不是Agent能调用工具完成任务才是。我们需要设计一个工具注册与发现机制。每个工具都是一个Python函数我们需要用一种方式比如装饰器来告诉Agent“嗨我这里有一个新工具它的功能是XXX调用时需要A、B两个参数”。当LLM大脑决定使用某个工具时我们的系统要能准确地找到对应的函数传入正确的参数并执行它。2.3 决策循环推理与执行引擎这是Agent的“中枢神经系统”负责调度大脑和四肢。它的工作流程是一个经典循环感知接收用户输入。规划LLM大脑分析输入决定是否需要调用工具、调用哪个工具、参数是什么。执行工具调用系统执行对应的函数。观察获取工具执行的结果。再规划/输出LLM大脑结合工具结果和对话历史决定是继续调用工具还是已经可以生成最终答案回复用户。这个循环可能执行多次直到任务完成。如何设计这个循环的状态机如何让LLM的每次输出都能被稳定地解析成“思考”或“行动指令”是这里的核心挑战。2.4 记忆模块对话上下文管理Agent不能得鱼忘筌它需要记住之前的对话和工具执行结果。这就是记忆模块。最简单的实现就是一个列表按顺序存放每轮用户输入、AI思考、工具调用和结果。更高级的可以引入摘要记忆、向量数据库记忆等。在我们的初版中一个能自动修剪长度的对话历史列表就足够了重点是理解记忆在推理中的关键作用。3. 分步实现从零搭建每一块积木理论说得再多不如一行代码。让我们开始动手我会详细解释每一行关键代码的意图和可能遇到的坑。3.1 第一步搭建与LLM对话的桥梁首先我们需要一个LLMClient类。这里以OpenAI API为例但设计上要保持接口通用以便日后切换模型。import openai import json from typing import Dict, List, Optional, Any class LLMClient: def __init__(self, api_key: str, model: str gpt-3.5-turbo, temperature: float 0.1): 初始化LLM客户端。 :param api_key: API密钥 :param model: 使用的模型名称 :param temperature: 温度参数越低输出越确定越高越有创造性。对于工具调用建议调低。 self.client openai.OpenAI(api_keyapi_key) self.model model self.temperature temperature self.conversation_history: List[Dict[str, str]] [] # 存储对话历史 def add_message(self, role: str, content: str): 向对话历史中添加一条消息。 self.conversation_history.append({role: role, content: content}) def get_completion(self, messages: List[Dict[str, str]]) - str: 调用LLM API获取补全结果。 try: response self.client.chat.completions.create( modelself.model, messagesmessages, temperatureself.temperature, ) return response.choices[0].message.content except Exception as e: print(f调用LLM API失败: {e}) return fError: {e} def clear_history(self): 清空对话历史。 self.conversation_history.clear()关键点解析temperature0.1对于需要稳定解析、执行工具调用的Agent较低的温度值能减少输出的随机性让模型更“听话”。conversation_history我们用列表存储所有消息。一个更健壮的实现需要考虑上下文窗口长度当历史消息太长时需要智能地摘要或丢弃最早的消息否则会触发API的token限制错误。3.2 第二步构建工具系统工具系统的核心是一个“工具注册表”和一个统一的“工具执行器”。我们使用装饰器来优雅地注册工具。class ToolRegistry: def __init__(self): self._tools: Dict[str, Dict] {} # 工具名 - 工具信息函数、描述、参数模式 def register(self, name: str, description: str): 装饰器用于注册一个工具。 def decorator(func): # 获取函数的参数信息用于后续生成提示词和参数验证 import inspect sig inspect.signature(func) params list(sig.parameters.keys()) self._tools[name] { function: func, description: description, parameters: params } return func return decorator def get_tool(self, name: str) - Optional[Dict]: 根据名称获取工具信息。 return self._tools.get(name) def list_tools(self) - List[str]: 列出所有可用的工具名称和描述。 return [f{name}: {info[description]} for name, info in self._tools.items()] # 全局工具注册表实例 tool_registry ToolRegistry() # 示例注册一个计算器工具 tool_registry.register(namecalculator, description执行简单的数学计算支持加减乘除。) def calculator(expression: str) - str: 计算数学表达式。注意使用eval有安全风险此处仅用于演示。 try: # 警告在生产环境中直接eval用户输入是极度危险的 # 这里应使用安全的表达式解析库如 ast.literal_eval 配合自定义解析器。 result eval(expression, {__builtins__: None}, {}) return f计算结果: {result} except Exception as e: return f计算错误: {e} # 示例注册一个获取天气的模拟工具真实情况需调用API tool_registry.register(nameget_weather, description获取指定城市的模拟天气信息。) def get_weather(city: str) - str: # 这里模拟一个API调用 weather_data { 北京: 晴25°C, 上海: 多云28°C, 深圳: 雷阵雨30°C } return weather_data.get(city, f未找到{city}的天气信息。)实操心得与避坑指南安全安全安全calculator工具中使用了eval这在任何生产环境或接收不可信用户输入的场景下都是绝对禁止的这里仅为了演示工具调用的流程。一个安全的计算器应该使用ast.literal_eval并严格限制可用的操作符或者使用像numexpr这样的专用库。工具描述的魔力给工具写一个清晰、准确的description至关重要。LLM大脑完全依赖这段描述来决定是否以及如何调用该工具。描述应包含功能、输入参数格式和输出示例。参数验证当前实现只是简单存储了参数名。一个完善的系统应该在调用前验证参数的类型和值这能极大减少LLM“幻觉”调用导致的错误。3.3 第三步设计Agent的核心推理循环这是最核心的部分。我们需要设计一个Agent类它整合LLM客户端和工具系统并运行思考-行动循环。class SimpleAgent: def __init__(self, llm_client: LLMClient, max_iterations: int 5): self.llm llm_client self.max_iterations max_iterations # 防止无限循环 self.system_prompt self._build_system_prompt() def _build_system_prompt(self) - str: 构建系统提示词定义Agent的角色和能力。 tools_list \n.join(tool_registry.list_tools()) prompt f 你是一个有帮助的AI助手可以调用工具来解决问题。 你可以使用的工具如下 {tools_list} 你的思考过程必须遵循以下格式 思考[你的推理过程分析用户问题决定是否需要以及使用哪个工具] 行动如果需要工具则格式为 工具名:参数1,参数2,...。如果不需要工具则直接输出最终答案。 答案[只有当不需要工具或得到工具结果后才给出给用户的最终答案] 示例1需要工具 用户计算一下123乘以456。 思考用户需要计算乘法我应该使用计算器工具。 行动calculator:123*456 ...系统执行工具并返回结果... 思考工具返回了结果56088。我可以将这个结果告知用户。 答案123乘以456等于56088。 示例2不需要工具 用户你好 思考这是一个简单的问候不需要调用工具。 答案你好我是你的AI助手有什么可以帮你的吗 现在请开始你的任务。记住每次只输出一个“思考”或“行动”或“答案”块。 return prompt def run(self, user_input: str) - str: 运行Agent处理用户输入。 print(f\n用户: {user_input}) self.llm.add_message(user, user_input) for iteration in range(self.max_iterations): # 1. 准备对话上下文系统提示 完整历史 messages [{role: system, content: self.system_prompt}] self.llm.conversation_history # 2. LLM生成响应 llm_response self.llm.get_completion(messages) print(fAI原始响应: {llm_response}) # 3. 解析响应 thought, action, answer self._parse_response(llm_response) if thought: print(f思考: {thought}) self.llm.add_message(assistant, f思考: {thought}) if action: # 解析行动指令 tool_name, *args action.split(:) tool_info tool_registry.get_tool(tool_name.strip()) if not tool_info: error_msg f错误未知工具 {tool_name} self.llm.add_message(system, error_msg) print(error_msg) continue # 执行工具 tool_func tool_info[function] try: # 简单处理假设参数是以逗号分隔的字符串 # 更复杂的实现需要根据工具参数定义进行类型转换 args_list [arg.strip() for arg in args[0].split(,)] if args else [] result tool_func(*args_list) except Exception as e: result f工具执行出错: {e} print(f执行工具 {tool_name}结果: {result}) # 将工具执行结果作为系统消息加入历史供LLM下一轮参考 self.llm.add_message(system, f工具{tool_name}返回: {result}) if answer: print(f答案: {answer}) self.llm.add_message(assistant, f答案: {answer}) return answer # 任务完成返回最终答案 # 循环超过最大次数 timeout_msg 抱歉我尝试了多次仍未解决问题。 self.llm.add_message(assistant, timeout_msg) return timeout_msg def _parse_response(self, response: str) - (str, str, str): 解析LLM的响应提取思考、行动、答案。 thought action answer lines response.strip().split(\n) for line in lines: if line.startswith(思考:): thought line[3:].strip() elif line.startswith(行动:): action line[3:].strip() elif line.startswith(答案:): answer line[3:].strip() return thought, action, answer核心逻辑深度解析系统提示词工程_build_system_prompt方法是Agent的“灵魂注入”。它定义了Agent的角色、可用工具、最重要的输出格式以及示例。清晰的格式要求思考/行动/答案是让LLM稳定输出的关键这比让它自由发挥可靠得多。循环与状态机run方法实现了一个简单的状态机。只要没有输出答案:并且迭代次数未超限就会持续进行“思考-行动-观察”的循环。每次循环都将之前的对话和工具结果作为上下文喂给LLM。解析器的脆弱性_parse_response函数非常简陋它依赖于LLM严格遵守格式。在实际中LLM偶尔会“不听话”输出格式混乱的文本。更健壮的做法是使用LLM的“函数调用”功能如果API支持或者要求LLM输出严格的JSON格式然后用json.loads解析容错性会高很多。3.4 第四步组装并运行你的第一个Agent现在让我们把所有部件组装起来看看这个“手搓”的Agent能否工作。def main(): # 1. 初始化LLM客户端请替换为你的真实API Key API_KEY your-openai-api-key-here # 务必替换 llm_client LLMClient(api_keyAPI_KEY, modelgpt-3.5-turbo) # 2. 创建Agent agent SimpleAgent(llm_clientllm_client) # 3. 运行测试 test_queries [ 你好请介绍一下你自己。, 请问北京今天的天气怎么样, 帮我计算一下(15 27) * 3 等于多少, 先查一下深圳的天气然后计算如果温度是30度相当于多少华氏度公式是 F C * 9/5 32 ] for query in test_queries: final_answer agent.run(query) print(f最终回复: {final_answer}\n{-*50}) if __name__ __main__: main()运行这段代码你会看到类似以下的输出具体内容因模型随机性略有不同用户: 帮我计算一下(15 27) * 3 等于多少 AI原始响应: 思考用户需要一个数学计算我可以使用计算器工具。 行动calculator:(15 27) * 3 思考: 用户需要一个数学计算我可以使用计算器工具。 执行工具 calculator结果: 计算结果: 126 AI原始响应: 思考工具返回了结果126。我可以直接给出答案。 答案: (15 27) * 3 的计算结果是126。 答案: (15 27) * 3 的计算结果是126。 最终回复: (15 27) * 3 的计算结果是126。看它成功地识别了计算需求调用了正确的工具并给出了答案对于最后一个需要多步推理先查天气再换算温度的复杂问题一个设计良好的Agent应该能通过多次循环来完成。4. 进阶优化与问题深度排查一个能跑起来的Demo只是起点。要让这个手写Agent真正可用、健壮我们还需要解决一系列工程问题。4.1 如何让LLM的输出更稳定—— 结构化输出的艺术我们之前依赖文本解析这很脆弱。更优解是使用LLM API的结构化输出功能如OpenAI的JSON Mode或函数调用功能。方案升级使用JSON Mode修改LLMClient.get_completion和系统提示词要求LLM始终返回一个JSON对象。def get_structured_completion(self, messages, response_format{type: json_object}): try: response self.client.chat.completions.create( modelself.model, messagesmessages, temperatureself.temperature, response_formatresponse_format # 指定JSON格式 ) json_str response.choices[0].message.content return json.loads(json_str) # 直接解析为字典 except json.JSONDecodeError as e: print(fJSON解析失败: {e}, 原始响应: {json_str}) return {error: Invalid JSON response}同时系统提示词要明确要求输出JSON例如请始终以以下JSON格式回复 { thought: 你的推理过程, action: {tool_name: 工具名, parameters: [参数1, 参数2]}, answer: 给用户的最终答案 } 其中action和answer不会同时存在。这样在_parse_response中我们直接处理字典彻底告别字符串解析的噩梦。4.2 工具调用参数如何更智能—— 从字符串到类型化目前的工具参数传递是简单的字符串分割无法处理复杂参数如列表、字典。我们可以结合Python的inspect模块和LLM的函数调用描述来实现。思路为每个工具生成一个符合OpenAI函数调用规范的描述包含参数类型。然后在调用LLM时将这些描述传入并启用function_call功能。LLM会返回一个结构化的函数调用请求其中参数已经是解析好的。这需要更深入地集成特定LLM API的高级功能是通往生产级Agent的必经之路。4.3 记忆管理如何突破上下文长度限制随着对话和工具调用轮次增加conversation_history会越来越长最终超过模型的上下文窗口如GPT-3.5的16K。解决方案有几种滑动窗口只保留最近N轮对话。简单但会丢失早期关键信息。智能摘要当历史达到一定长度让LLM自己生成一个当前对话的简短摘要然后用“系统消息之前的对话摘要...” “最近几轮实际对话”来替代冗长的完整历史。这需要额外的LLM调用和提示词设计。向量数据库记忆将每轮对话的关键信息如事实、用户偏好转化为向量存入数据库如ChromaDB。每次需要回忆时用当前问题去检索最相关的记忆片段。这是实现“长期记忆”的先进方式但架构复杂度陡增。对于我们的手写Agent可以先实现滑动窗口并设置一个最大历史长度阈值。4.4 常见问题排查速查表在实际运行中你几乎一定会遇到以下问题。这里提供一个快速排查指南问题现象可能原因解决方案Agent陷入死循环不停调用同一个工具。1. 工具结果未能帮助LLM推进任务。2. 系统提示词未明确“何时停止”。3.max_iterations设置过高。1. 检查工具返回的结果是否清晰、有用。2. 在系统提示词中强调“当你认为拥有足够信息回答用户时就输出答案”。3. 加入更严格的循环退出条件如检测到重复动作。LLM不按指定格式输出解析失败。1. 提示词中的格式指令不够清晰或强制。2. Temperature参数过高导致输出随机。3. 模型能力不足。1. 使用更严厉的措辞如“你必须严格遵守以下格式”。2. 将temperature降至0.1或0。3. 升级到更强大的模型如GPT-4或采用前述的JSON Mode/函数调用。工具执行出错参数不对。1. LLM“幻觉”出了不存在的参数。2. 参数类型不匹配如需要数字却传了字符串。1. 在工具描述中极其精确地说明参数名称、类型和示例。2. 在执行工具前增加参数验证和清洗逻辑。处理复杂、多步骤任务时失败。Agent的“规划”能力不足无法将大任务分解为子步骤。引入更复杂的提示工程技术如“思维链”提示在系统提示中教导LLM先制定分步计划。或者实现一个更高阶的“规划器”模块专门负责任务分解。API调用频繁失败或超时。网络问题或API服务不稳定。增加重试机制如tenacity库、设置合理的超时时间、加入退避策略。5. 从玩具到工具扩展你的手写Agent掌握了基础架构后你可以像搭乐高一样为你的Agent添加更多强大功能多模态能力让Agent不仅能处理文本还能“看”图“听”音。这需要在工具系统中集成图像识别如CLIP或语音转文本如Whisper的API调用。网络搜索能力注册一个web_search工具内部调用Serper API或SearxNG让Agent能获取实时信息不再局限于训练数据。持久化与状态管理将Agent的对话历史、学到的知识如用户偏好保存到文件或数据库中下次启动时可以加载。多Agent协作创建多个具有不同专长如研究员、写手、校对员的Agent实例让它们通过一个“协调者”Agent来共同完成复杂项目。这本质上就是构建一个微型的CrewAI或AutoGen。手写一个AI Agent的过程就像在显微镜下观察一个生命体的运作。每一个循环每一次工具调用每一次记忆的存取都清晰可见。你可能会为它偶尔的“愚蠢”而抓狂也会为它灵光一现完成复杂任务而欣喜。这种深度的理解和掌控感是使用高级框架无法给予的。这个项目的代码只是一个起点它简陋但完整地揭示了一个AI Agent的核心骨架。当你亲手调试它、扩展它、看着它从踉跄学步到逐渐稳健时你对AI Agent的理解将不再停留在概念和API文档层面。你会明白所谓智能体其内核无非是在确定性的工具世界与概率性的语言模型之间搭建起一座可靠沟通的桥梁。而这座桥的每一块砖都由你亲手烧制。
返回列表