十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建AIAgent框架:基于状态-决策-执行循环的Python实战

从零搭建AIAgent框架:基于状态-决策-执行循环的Python实战 1. 项目概述从零到一理解AIAgent的核心最近“AIAgent”这个词在圈子里火得不行无论是技术论坛还是产品讨论似乎不提Agent就落伍了。但说实话很多文章要么讲得太玄乎要么直接甩出几百行代码对新手极不友好。今天我们不谈那些高大上的概念就从一个开发者的实操视角出发动手搭建一个最简易、可运行的AIAgent框架。这个框架的目标很明确让你在喝杯咖啡的时间里理解AIAgent是如何“思考”和“行动”的并拥有一个可以继续添砖加瓦的代码基底。简单来说AIAgent就是一个能感知环境、进行决策并执行动作的智能体。它不像传统的聊天机器人那样一问一答就结束而是具备一定的目标导向和自主性。比如你可以让它“帮我查一下明天北京的天气如果下雨就提醒我带伞”它会自动分解任务先调用天气API查询再根据返回结果判断是否触发提醒动作。我们这次要搭建的就是实现这套“感知-思考-行动”循环的最小可行系统。无论你是前端开发者想了解后端智能逻辑还是对AI应用感兴趣的爱好者这个案例都能让你绕过复杂的理论直接触摸到核心。2. 框架核心设计拆解“感知-思考-行动”循环要搭建一个AIAgent首先得把它的大脑工作流程搞清楚。别看市面上框架五花八门其核心都绕不开一个经典模型感知Perception、思考Reasoning、行动Action的循环有些框架还会加上一个学习Learning环节。对于我们这个简易框架我们先聚焦于前三个这是Agent能动起来的根本。2.1 为什么是“状态-决策-执行”三层结构我选择采用“状态State- 决策Decision- 执行Execution”三层结构来具象化这个循环。这比直接套用抽象术语更贴近代码实现。状态State这是Agent的“感知”部分。它不仅仅是用户当前输入的一句话而是一个包含了上下文、历史对话、环境参数比如当前时间、可用工具列表的集合。在设计上我们用一个Python字典dict来承载状态因为它灵活可以随时扩展字段。例如初始状态可能是{“user_input”: “明天北京天气如何”, “conversation_history”: [], “available_tools”: [“get_weather”]}。决策Decision这是Agent的“思考”核心。给定当前状态Agent需要决定下一步做什么。是直接回答用户还是调用某个工具API这里就是大型语言模型LLM发挥作用的地方。我们会将状态信息精心编排成一段提示词Prompt发送给LLM比如OpenAI的GPT-3.5/4或者开源的ChatGLM、通义千问请求它输出一个结构化的决策。这个决策通常是一个JSON例如{“action”: “call_tool”, “tool_name”: “get_weather”, “tool_input”: {“city”: “北京”, “date”: “明天”}}或者{“action”: “final_answer”, “answer”: “….”}。执行Execution根据决策执行具体操作。如果是调用工具就找到注册好的对应函数传入参数执行并将执行结果更新到状态中如果是最终回答就将结果返回给用户。执行完成后产生新的状态进入下一个循环。这个结构清晰地将LLM的“思考”与外部世界的“行动”解耦LLM只负责规划和决策具体活儿由可靠的、预设的函数来干大大提升了系统的确定性和安全性。2.2 工具Tools的设计与管理Agent的强大之处在于它能使用工具。在我们的框架里工具就是一个普通的Python函数但需要被标准化地描述和注册。工具定义每个工具需要三个关键元素1) 名称name用于决策时指定2) 描述description用于告诉LLM这个工具是干什么的这部分描述会写入提示词直接影响LLM是否以及如何调用它3) 函数本体func包含具体的执行逻辑。注册机制我们会维护一个全局的“工具注册表”一个字典框架启动时将工具注册进去。当决策环节输出要调用某个工具时执行环节就能从这个注册表里按名索骥找到对应的函数并运行。设计心得工具的描述description至关重要。它必须清晰、无歧义并最好说明输入输出的格式。例如get_weather工具的描述可以是“根据城市名和日期查询天气情况。输入参数city字符串城市名date字符串格式‘今天’、‘明天’或‘YYYY-MM-DD’。返回一个包含天气状况和温度的字符串。” 清晰的描述能极大提高LLM调用的准确性。3. 简易框架搭建实战一行行代码实现核心理论说再多不如动手。我们使用Python来构建因为它生态丰富与各种AI模型交互方便。这个框架不依赖任何复杂的重型库核心逻辑大约100行代码就能实现。3.1 环境准备与依赖安装首先确保你的Python环境在3.8以上。我们需要的核心库并不多openai用于调用OpenAI的API。如果你使用其他模型可能是requests或相应的SDK。python-dotenv管理环境变量安全地存储API密钥。通过pip安装pip install openai python-dotenv创建一个.env文件来存放你的OpenAI API密钥或其他模型的密钥OPENAI_API_KEY你的密钥3.2 核心类与循环实现我们创建一个名为SimpleAgent的类。import json import openai from typing import Dict, Any, Callable, List from dotenv import load_dotenv import os load_dotenv() openai.api_key os.getenv(“OPENAI_API_KEY”) class SimpleAgent: def __init__(self, model: str “gpt-3.5-turbo”): self.model model self.tools: Dict[str, Dict] {} # 工具注册表 self.conversation_history: List[Dict] [] # 维护对话历史 def register_tool(self, name: str, description: str, func: Callable): “”“注册一个工具”“” self.tools[name] { “description”: description, “func”: func } def _build_prompt(self, state: Dict[str, Any]) - str: “”“构建给LLM的提示词这是核心中的核心”“” # 1. 系统角色设定 system_message “””你是一个专业的任务执行助手。你需要根据用户请求和当前状态决定下一步行动。 你可以选择两种行动 1. 调用工具当你需要获取信息或执行操作时使用。 2. 直接回答当你拥有足够信息回答用户时使用。 请严格按照以下JSON格式输出你的决策 {“action”: “call_tool”, “tool_name”: “工具名”, “tool_input”: {“参数名”: “参数值”}} 或 {“action”: “final_answer”, “answer”: “你的回答内容”} 当前可用工具如下 “”” # 2. 拼接工具描述 tools_desc “\n”.join([f”- {name}: {info[‘description’]}” for name, info in self.tools.items()]) # 3. 拼接当前状态和历史 history_str “\n”.join([f”{msg[‘role’]}: {msg[‘content’]}” for msg in self.conversation_history[-5:]]) # 最近5轮历史 current_state f”用户最新输入: {state.get(‘user_input’)}” # 4. 组合成完整提示 full_prompt f”{system_message}\n{tools_desc}\n\n对话历史\n{history_str}\n\n{current_state}\n\n你的决策JSON” return full_prompt def _make_decision(self, prompt: str) - Dict[str, Any]: “”“调用LLM进行决策”“” try: response openai.ChatCompletion.create( modelself.model, messages[{“role”: “user”, “content”: prompt}], temperature0.1, # 低温度保证输出稳定性 max_tokens500 ) decision_text response.choices[0].message.content.strip() # 尝试解析JSON return json.loads(decision_text) except json.JSONDecodeError: # 如果LLM没有返回合法JSON降级处理 print(f”LLM返回非JSON内容: {decision_text}”) return {“action”: “final_answer”, “answer”: “系统处理出现异常请稍后再试。”} except Exception as e: print(f”调用API失败: {e}”) return {“action”: “final_answer”, “answer”: “服务暂时不可用。”} def _execute_action(self, decision: Dict[str, Any], state: Dict[str, Any]) - Dict[str, Any]: “”“执行决策更新状态”“” action decision.get(“action”) if action “call_tool”: tool_name decision.get(“tool_name”) tool_input decision.get(“tool_input”, {}) if tool_name in self.tools: try: # 执行工具函数 result self.tools[tool_name][“func”](**tool_input) # 将执行结果更新到状态 state[“last_tool_result”] result # 将本次“工具调用-结果”加入历史供下一轮决策参考 self.conversation_history.append({“role”: “assistant”, “content”: f”调用了工具 {tool_name}输入为 {tool_input}。”}) self.conversation_history.append({“role”: “system”, “content”: f”工具 {tool_name} 返回结果: {result}”}) except Exception as e: state[“last_tool_result”] f”工具执行错误: {e}” else: state[“last_tool_result”] f”未知工具: {tool_name}” elif action “final_answer”: state[“final_answer”] decision.get(“answer”) # 将最终答案加入历史 self.conversation_history.append({“role”: “assistant”, “content”: decision.get(“answer”)}) else: state[“error”] f”无法识别的动作: {action}” return state def run(self, user_input: str) - str: “”“运行Agent的主循环”“” # 初始化状态 state { “user_input”: user_input, “final_answer”: None, “last_tool_result”: None } # 将用户输入加入历史 self.conversation_history.append({“role”: “user”, “content”: user_input}) # 最大循环次数防止死循环 max_steps 5 for step in range(max_steps): # 1. 构建提示 prompt self._build_prompt(state) # 2. 决策 decision self._make_decision(prompt) # 3. 执行 state self._execute_action(decision, state) # 4. 检查是否结束 if state.get(“final_answer”): return state[“final_answer”] # 如果上一步是调用工具则循环继续LLM将基于工具结果进行下一轮决策 return “Agent经过多次尝试未能得出最终结论请简化您的问题或重试。”3.3 定义并注册工具现在我们定义两个简单的工具来测试框架。# 定义一个模拟天气查询工具 def get_weather(city: str, date: str) - str: “”“模拟天气查询实际项目中应调用真实API”“” # 这里模拟一个简单的响应 weather_map {“北京”: “晴朗”, “上海”: “多云”, “广州”: “阵雨”} return f”{date}{city}的天气是{weather_map.get(city, ‘未知’)}温度20-25度。” # 定义一个计算器工具 def calculator(expression: str) - str: “”“简易计算器注意实际使用中要对表达式做安全过滤”“” try: # 警告此处使用eval仅用于演示生产环境必须禁用或严格过滤 result eval(expression) return f”{expression} {result}” except Exception as e: return f”计算错误: {e}” # 创建Agent实例并注册工具 if __name__ “__main__”: agent SimpleAgent() agent.register_tool( name“get_weather”, description“根据城市名和日期查询天气情况。输入参数city字符串城市名date字符串格式‘今天’、‘明天’或‘YYYY-MM-DD’。返回天气描述字符串。”, funcget_weather ) agent.register_tool( name“calculator”, description“执行数学表达式计算。输入参数expression字符串例如‘35*2’。返回计算结果字符串。”, funccalculator ) # 测试运行 test_query “明天北京天气怎么样” answer agent.run(test_query) print(f”用户: {test_query}”) print(f”Agent: {answer}”) print(“\n — 下一个问题 — \n”) test_query2 “那如果我去上海顺便帮我算一下(157)*3等于多少” answer2 agent.run(test_query2) print(f”用户: {test_query2}”) print(f”Agent: {answer2}”)4. 核心环节深度解析与调优要点框架跑起来只是第一步要让Agent真正“聪明”可用以下几个环节的细节处理至关重要。4.1 提示词Prompt工程的艺术我们的框架中_build_prompt方法是大脑的“信息输入通道”其质量直接决定决策水平。结构化指令必须清晰在系统消息中必须明确列出可用的行动类型call_tool,final_answer和严格的输出格式JSON。LLM会严格遵守格式要求模糊的指令会导致输出不可预测。工具描述的技巧工具描述要像给一个新员工写说明书一样明确功能、输入参数名称、类型、示例、输出示例。例如好的描述是“查询城市天气。输入{“city”: “string”, “date”: “string”}。输出字符串格式的天气报告。” 避免使用“处理数据”、“获取信息”等模糊词汇。历史上下文的裁剪我们将最近的对话历史例如5轮放入提示词这给了Agent短期记忆。但要注意上下文长度Token数是有限的历史太长会挤占当前指令的空间也可能导致API调用成本上升。需要根据模型的上限做动态裁剪或摘要。温度Temperature参数在_make_decision中我们设置temperature0.1。这个值越接近0输出越确定、可重复值越高创造性越强但越不稳定。对于需要稳定执行逻辑的Agent决策环节通常建议使用较低的温度0-0.3。4.2 状态管理的演进策略我们目前的状态管理比较简单。在实际复杂场景中状态可能需要更精细的设计长期记忆与短期记忆conversation_history是短期工作记忆。对于需要跨会话记忆的信息如用户偏好需要引入长期记忆机制例如将关键信息向量化后存入数据库如ChromaDB, Pinecone在需要时通过检索增强生成RAG的方式召回。状态验证与清洗从LLM决策中解析出的tool_input在传给工具函数前必须进行类型验证和安全性清洗。特别是对于像calculator这样使用eval的函数仅为演示生产环境绝对禁止直接使用eval必须严格限制可用的字符和操作或使用安全的库如ast.literal_eval。4.3 工具执行的可靠性与错误处理工具是Agent与真实世界交互的手脚必须可靠。超时与重试工具调用尤其是网络API可能失败。在执行环节应为工具调用添加超时机制和有限次数的重试逻辑。结果标准化不同工具返回的结果格式各异。最好能设计一个统一的工具响应格式例如{“success”: bool, “data”: Any, “error”: str}这样在更新状态和构造下一轮提示时会更加一致。依赖工具有时一个工具的执行需要另一个工具的结果。这需要在状态中妥善管理中间数据并在提示词中清晰地告知LLM可用的信息。5. 常见问题排查与进阶优化在实际搭建和运行过程中你肯定会遇到各种问题。这里记录几个典型场景和我的解决思路。5.1 Agent陷入死循环或无效调用现象Agent反复调用同一个工具或者在不该调用工具时调用。排查检查提示词首先打印出_build_prompt方法生成的完整提示词。看看工具描述是否清晰历史上下文是否包含了导致混淆的信息系统指令是否足够强硬地规定了输出格式检查决策输出打印_make_decision返回的decision_text。LLM是否输出了合法的JSON是否完全遵循了你定义的action类型有时LLM会在JSON外加一层Markdown代码块标记需要预处理。简化测试移除所有工具只测试final_answer路径。确保基础循环是通的。然后一次只添加一个工具逐步测试。解决强化指令在系统提示中加入更明确的约束如“你必须根据已有信息判断如果信息不足再调用工具不要重复调用已提供相同参数的工具。”改进状态设计在状态中显式加入一个last_action字段记录上一次的动作和结果并在提示词中说明帮助LLM避免重复。设置最大步数正如我们在run方法中所做必须设置一个循环上限如5-10步这是防止死循环的最后防线。5.2 LLM不理解工具或参数错误现象LLM决策时tool_name错误或tool_input中的参数名、类型不对。排查与解决工具描述口语化用LLM能理解的自然语言描述工具而不是内部函数签名。对比一下差def get_weather(city: str, date: str): …好工具名get_weather。功能查询指定城市在指定日期的天气。你需要提供两个参数city城市名称例如“北京”date日期可以是“今天”、“明天”或“2023-10-01”这样的格式。提供示例在系统提示中直接给出1-2个完整的、正确的决策JSON示例。Few-shot learning少样本学习对LLM遵循格式非常有效。后处理校验在_execute_action中增加参数校验逻辑。如果发现参数缺失或类型不符可以尝试用LLM进行修正或者直接返回一个错误信息更新到状态让LLM在下一轮决策中纠正。5.3 性能与成本考量上下文长度每次循环都会将整个对话历史发送给LLMToken消耗会线性增长。对于长对话需要考虑历史摘要不发送原始历史而是发送一个由LLM生成的、浓缩的摘要。滑动窗口只发送最近N轮对话。选择性记忆只将与当前任务高度相关的历史片段放入上下文。异步执行如果工具调用是IO密集型如网络请求可以考虑使用异步asyncio来并行执行多个独立工具减少整体响应时间。缓存对于相同参数的查询如天气可以在工具层实现缓存避免重复调用外部API和LLM节省成本和时间。这个简易框架就像一辆能开动的“原型车”它具备了AIAgent最核心的引擎和底盘。你可以在此基础上为它装上更强大的工具搜索引擎、数据库操作、代码解释器、更智能的记忆系统、更美观的交互界面用Vue3.0Element Plus做个前端甚至引入多Agent协作机制。动手搭建一遍的意义在于当你在使用LangChain、AutoGen等成熟框架时你能清楚地知道它们在底层帮你解决了哪些问题从而能更得心应手地使用和定制它们。
返回列表