尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零构建智能体操作系统:基于文件夹结构与核心循环的AI Agent开发实践

从零构建智能体操作系统:基于文件夹结构与核心循环的AI Agent开发实践 1. 项目概述从零构建你的智能体操作系统最近和几个做AI应用开发的朋友聊天大家普遍有个痛点手头攒了一堆大模型API、工具函数和数据处理脚本每次想快速拼凑一个能自动执行复杂任务的智能体Agent时都得从头搭建脚手架处理任务调度、状态管理和工具调用这些重复的“脏活累活”。结果往往是核心业务逻辑还没写几行光框架代码就折腾了半天。这让我想起了早期做Web开发时没有Spring Boot这类“约定大于配置”框架的日子。于是我琢磨着能不能用最朴素、最直观的方式——一套精心设计的文件夹结构加上一个核心的循环逻辑——来构建一个轻量级、可扩展的“AgentOS”智能体操作系统雏形。这不是一个要替代LangChain或AutoGPT的庞大框架而是一个属于你自己的、高度可控的起点。这个项目的核心思想是“结构化”与“自动化”。“文件夹结构”定义了智能体的组成部分大脑、工具、记忆、任务如何被清晰地组织和管理就像操作系统的目录规划了系统文件、应用程序和用户数据的存放位置。“循环构建”则是一个驱动智能体运转的核心引擎它不断地感知输入、调用工具、更新状态并决策下一步行动模拟了智能体持续思考与执行的过程。通过将这两者结合你就能从一个清晰的蓝图开始逐步迭代出一个功能强大、易于调试和维护的AI智能体系统。无论你是想做一个自动处理邮件的助手一个分析市场报告的机器人还是一个能链式调用多种API的复杂工作流这套方法都能为你提供一个坚实且灵活的基础。2. 核心设计思路像设计操作系统一样设计Agent为什么是文件夹和循环这背后是对智能体本质的一种抽象。一个能够自主行动的智能体无论其功能多么复杂通常都包含几个基本模块一个负责决策的“大脑”通常是LLM一套可供调用的“工具”函数或API一块记录历史和上下文的“记忆”以及待执行的“任务”队列。将这些模块用文件夹物理隔开带来的好处是显而易见的模块解耦、职责清晰、易于测试和替换。你可以单独优化提示词大脑新增一个工具函数而几乎不影响其他部分或者更换记忆存储后端从内存到数据库。而“循环”则是智能体的生命线。它不同于一次性的函数调用而是一个持续的、有状态的交互过程。一个典型的智能体循环Agentic Loop可以概括为以下几个阶段观察Perceive从环境用户输入、API返回、传感器数据或内部状态记忆、任务列表中获取信息。思考Think大脑LLM根据当前观察和记忆决定下一步要做什么调用哪个工具、直接输出答案、还是需要更多信息。行动Act执行思考后决定的动作比如运行一个工具函数、调用外部API。反思Reflect评估行动的结果更新内部状态和长期记忆为下一轮循环做准备。我们的目标就是用代码实现这个循环并用文件夹来妥善安置循环中涉及的每一个组件。这样构建出来的系统其扩展性极强。当你需要增加新功能时只需在对应的文件夹比如tools/下添加一个新文件然后在循环的“思考”阶段让大脑知道这个新工具的存在即可。2.1 为什么选择自建而非成熟框架你可能会问市面上已经有LangChain、LlamaIndex、AutoGen等优秀的框架为什么还要自己从头搭建原因主要有三点极致透明与可控自己搭建的每一个环节你都了如指掌没有黑盒。当出现诡异的问题时你可以精准地定位到是提示词的问题、工具函数的问题还是循环逻辑的问题调试效率极高。轻量无依赖你的系统只包含你需要的部分没有庞大的依赖树部署简单启动快速特别适合嵌入到现有项目或资源受限的环境中。深刻的学习过程亲手实现一遍核心循环你对智能体如何工作、大模型如何与外部世界交互的理解会深入骨髓。这为你后续无论是使用高级框架还是设计更复杂的多智能体系统都打下了坚实的基础。当然这并不意味着排斥成熟框架。你可以将这套自建的AgentOS视为“内核”或“原型”在验证想法后完全可以将其核心思想迁移到更大型的框架中或者反过来用成熟框架的某些组件比如更优秀的工具调用解析库来增强你的系统。3. 文件夹结构蓝图构建Agent的“五脏六腑”让我们来具体描绘这个AgentOS的物理形态。以下是我经过多次迭代后形成的一个推荐结构你可以根据项目的复杂程度进行裁剪或扩展。your_agentos_project/ ├── agent_brain/ # 智能体的“大脑”和决策核心 │ ├── __init__.py │ ├── llm_client.py # 封装与大模型API的交互OpenAI, Claude, 本地模型等 │ ├── prompt_templates/ # 存放各种任务的提示词模板 │ │ ├── task_planner.j2 │ │ ├── tool_selector.j2 │ │ └── response_formatter.j2 │ └── reasoning_engine.py # 核心决策逻辑解析LLM返回决定下一步 ├── tools/ # 智能体的“工具箱” │ ├── __init__.py │ ├── base_tool.py # 所有工具的基类定义统一接口 │ ├── web_search.py # 示例工具网络搜索 │ ├── calculator.py # 示例工具计算器 │ ├── file_ops.py # 示例工具文件读写 │ └── weather.py # 示例工具查询天气 ├── memory/ # 智能体的“记忆” │ ├── __init__.py │ ├── short_term.py # 短期记忆/对话上下文管理 │ ├── long_term.py # 长期记忆可向量化存储用于检索 │ └── knowledge_base/ # 知识库文件可选 │ └── company_faq.pdf ├── tasks/ # 任务定义与流程 │ ├── __init__.py │ ├── task.py # 任务基类 │ ├── email_processing.py # 具体任务处理邮件 │ └── data_analysis.py # 具体任务分析数据 ├── config/ # 配置文件 │ └── settings.yaml # API密钥、模型参数、开关配置 ├── logs/ # 运行日志 │ └── agent_20231027.log ├── main_loop.py # **核心主循环程序** └── requirements.txt # 项目依赖3.1 各目录职责详解与实操要点1.agent_brain/- 决策中枢这里是智能体的“CPU”。llm_client.py负责所有与LLM的通信关键是要做好错误重试、速率限制和token计数。一个常见的技巧是在这里实现一个“模型降级”策略当首选模型如GPT-4调用失败或超时时自动切换到备用模型如GPT-3.5-Turbo保证系统的鲁棒性。prompt_templates/目录下的Jinja2模板文件是核心资产。将提示词从代码中分离出来便于管理和A/B测试。例如tool_selector.j2模板可能长这样你是一个擅长使用工具解决问题的助手。你有以下工具可用 {% for tool in tools %} - {{ tool.name }}: {{ tool.description }}。输入参数示例{{ tool.args_example }} {% endfor %} 当前用户的问题是{{ user_query }} 当前的对话历史摘要{{ memory_summary }} 请分析问题并严格按照以下JSON格式回复且只回复JSON { thought: 你的思考过程, tool_to_use: 工具名或null如果无需工具, tool_input: {arg1: value1} // 如果使用工具这里是参数 }reasoning_engine.py则负责调用模板、与LLM客户端交互并解析和验证LLM的返回。这是最容易出错的环节。你必须假设LLM可能会返回任何格式的内容因此解析JSON时一定要用try...except包裹并设计一个“修复”机制当解析失败时可以将错误信息连同原始回复再次发给LLM要求它纠正格式。2.tools/- 能力扩展包每个工具都是一个独立的Python类继承自base_tool.py中定义的基类。基类通常会强制子类实现name、description、args_schema参数JSON Schema和run()方法。这种设计使得主循环可以通过反射动态加载所有工具并自动生成工具描述供大脑使用。以web_search.py为例from .base_tool import BaseTool import requests class WebSearchTool(BaseTool): name “web_search” description “在互联网上搜索信息。当用户需要最新、非你训练数据内的知识时使用此工具。” args_schema { “type”: “object”, “properties”: { “query”: {“type”: “string”, “description”: “搜索关键词”} }, “required”: [“query”] } def run(self, query: str): # 这里可以使用SerpAPI、Google Custom Search等 # 示例使用DuckDuckGo Instant Answer API简易版 try: response requests.get(f“https://api.duckduckgo.com/?q{query}formatjson”) data response.json() abstract data.get(‘AbstractText’, ‘No summary found.’) return f“搜索 ‘{query}’ 的结果摘要{abstract}” except Exception as e: return f“搜索过程中出错{str(e)}”注意工具函数内部必须做好异常处理并始终返回一个字符串结果。即使失败也要返回一个友好的错误信息让大脑能够理解并决定下一步例如重试或换一种方式。避免工具抛出未处理的异常导致整个循环崩溃。3.memory/- 状态与历史记录器短期记忆short_term.py通常用一个有长度限制的列表或队列来实现保存最近的几轮对话交互。这对于维持上下文连贯性至关重要。长期记忆long_term.py则更为复杂。对于简单的项目可以是一个JSON文件或SQLite数据库记录重要的交互摘要。对于需要基于历史进行语义检索的场景则需要引入向量数据库如Chroma、FAISS。这里的一个关键设计是摘要化不是存储完整的、冗长的对话而是定期或在关键节点让LLM对一段交互进行总结将摘要存入长期记忆。这能有效控制token消耗并提炼关键信息。4.tasks/- 目标与流程定义任务是对一个复杂目标的封装。一个Task类可能包含任务描述、成功标准、以及一系列的子步骤Step。main_loop.py可以处理一个顶级的“任务”并将其分解后逐步执行。例如data_analysis.py任务可能包含“读取数据文件”、“清洗数据”、“运行分析模型”、“生成报告”四个步骤每个步骤都可能触发智能体循环去调用相应的工具。5.config/与logs/- 保障系统可维护性将配置尤其是API密钥放在独立的配置文件中并通过环境变量或.gitignore确保其安全是工程化的基本要求。详细的日志记录则是调试复杂智能体行为的生命线。你不仅需要记录每轮循环的输入输出最好还能记录LLM的完整请求和响应注意脱敏敏感信息以及工具调用的耗时和结果。4. 核心循环引擎的实现与详解文件夹结构搭好了现在需要让它们“活”起来。main_loop.py就是这个系统的心脏。下面我们实现一个基础但功能完整的单智能体循环。4.1 主循环骨架代码import json import time from typing import Dict, Any from agent_brain.llm_client import LLMClient from agent_brain.reasoning_engine import ReasoningEngine from tools.manager import ToolManager from memory.short_term import ShortTermMemory from memory.long_term import LongTermMemory class AgentOS: def __init__(self, config_path: str “config/settings.yaml”): # 初始化所有组件 self.llm_client LLMClient(config_path) self.tool_manager ToolManager() # 负责加载和管理所有工具 self.short_memory ShortTermMemory(max_turns10) self.long_memory LongTermMemory() # 可能是向量存储接口 self.reasoning_engine ReasoningEngine(self.llm_client, self.tool_manager) self.max_iterations 20 # 防止无限循环 self.iteration 0 def run(self, initial_input: str): 运行智能体主循环 print(f“[AgentOS] 开始处理: {initial_input}”) current_state {“user_input”: initial_input, “result”: None, “need_more_info”: False} while self.iteration self.max_iterations: self.iteration 1 print(f“\n--- 循环迭代第 {self.iteration} 轮 ---”) # 1. 观察整合当前输入、记忆和上下文 observation self._make_observation(current_state) # 2. 思考让推理引擎决定下一步行动 decision self.reasoning_engine.think(observation, self.short_memory.get_recent()) print(f“[思考] 决策: {decision}”) # 3. 行动执行决策 action_result self._act(decision) print(f“[行动] 结果: {action_result[:100]}...”) # 打印前100字符 # 4. 反思更新记忆和状态 current_state self._reflect(decision, action_result, current_state) # 5. 检查终止条件 if self._should_stop(current_state, decision): print(f“[AgentOS] 任务完成或终止。”) break else: print(f“[警告] 达到最大迭代次数{self.max_iterations}强制终止。”) return current_state.get(“result”, “任务未完成”) def _make_observation(self, state: Dict[str, Any]) - str: 构建当前循环的观察信息 # 从长期记忆中检索相关历史基于当前问题的语义 relevant_memories self.long_memory.retrieve(state[“user_input”], top_k3) memory_context “\n”.join(relevant_memories) if relevant_memories else “无相关长期记忆。” # 整合短期记忆最近几轮对话 short_term_context self.short_memory.get_context_string() observation f 用户当前问题或目标{state[‘user_input’]} 短期对话历史{short_term_context} 相关长期记忆{memory_context} 上一轮结果{state.get(‘last_result’, ‘无’)} 是否需要更多信息{state.get(‘need_more_info’, False)} return observation.strip() def _act(self, decision: Dict[str, Any]) - str: 执行决策可能是调用工具或直接回应 if decision.get(“tool_to_use”): tool_name decision[“tool_to_use”] tool_input decision.get(“tool_input”, {}) try: # 通过工具管理器调用具体工具 result self.tool_manager.execute_tool(tool_name, tool_input) return f“调用工具 ‘{tool_name}’ 成功。结果{result}” except Exception as e: return f“调用工具 ‘{tool_name}’ 失败。错误{str(e)}” else: # 如果没有指定工具决策中的‘thought’可能就是直接回复 return decision.get(“thought”, “我决定直接回应。”) def _reflect(self, decision: Dict, action_result: str, old_state: Dict) - Dict: 根据行动结果更新状态和记忆 new_state old_state.copy() # 将本轮交互存入短期记忆 self.short_memory.add_turn( user_inputold_state.get(“user_input”), agent_thoughtdecision.get(“thought”), action_takenf“工具: {decision.get(‘tool_to_use’)}” if decision.get(“tool_to_use”) else “直接回应”, resultaction_result ) # 如果本轮有了最终答案更新状态 if not decision.get(“tool_to_use”) and “need_more_info” not in action_result.lower(): new_state[“result”] action_result new_state[“need_more_info”] False # 可选将重要结论摘要存入长期记忆 if len(action_result) 500: # 如果结果不长直接存 self.long_memory.store(f“Q: {old_state[‘user_input’]} A: {action_result}”) else: # 如果调用了工具或需要更多信息更新用户输入为行动结果继续循环 new_state[“user_input”] f“基于之前的上下文{old_state[‘user_input’]}。工具调用结果是{action_result}。请继续分析。” new_state[“last_result”] action_result # 检查结果是否暗示需要用户澄清 if “need_more_info” in action_result.lower() or “?” in action_result[-5:]: new_state[“need_more_info”] True return new_state def _should_stop(self, state: Dict, decision: Dict) - bool: 判断循环是否应该停止 # 条件1已经有了最终结果 if state.get(“result”) and not state.get(“need_more_info”): return True # 条件2大脑决定不再使用工具且未表明需要信息即准备直接给出最终回答 if not decision.get(“tool_to_use”) and not state.get(“need_more_info”): return True # 条件3用户输入明确包含终止信号在实际应用中可从交互中获取 if state[“user_input”].lower() in [“stop”, “exit”, “取消”]: return True return False # 使用示例 if __name__ “__main__”: agent AgentOS() # 模拟用户输入一个需要多步工具调用的复杂问题 final_answer agent.run(“请帮我查一下北京今天的天气然后告诉我这样的天气是否适合户外跑步并简单说明理由。”) print(f“\n最终答案{final_answer}”)4.2 循环中的关键机制解析观察阶段 (_make_observation) 的上下文构建这是决定智能体表现好坏的关键。我们不仅提供了原始的用户输入还拼接了短期记忆保证对话连贯、从长期记忆中检索出的相关片段提供背景知识、以及上一轮的结果。这形成了一个丰富的“情境板”供LLM决策。检索长期记忆时简单的项目可以用关键词匹配复杂的则推荐使用向量相似度检索这能让智能体“想起”过去相关的经历。思考阶段 (reasoning_engine.think) 的决策解析这是大脑的“前额叶皮层”。它接收观察信息填充提示词模板调用LLM并严格解析输出。如前所述必须防御性地处理LLM的输出。一个健壮的解析器会尝试将响应解析为JSON。如果失败尝试用正则表达式提取可能的JSON块。如果仍失败则向LLM发送一个“修复”请求附上错误信息和原始响应要求它重试。验证JSON中的字段是否符合预期如tool_to_use是否在工具列表中。行动阶段 (_act) 的工具执行与隔离工具管理器 (ToolManager) 的作用是解耦。主循环只知道要调用一个工具名和参数具体是哪个Python函数、如何执行由管理器负责。这允许你动态加载工具、进行权限检查、或添加工具调用前后的钩子如日志、计量。反思阶段 (_reflect) 的状态机与记忆更新这是智能体“学习”和“调整”的地方。它根据行动结果决定下一步是继续探索更新user_input继续循环还是终止设置result。短期记忆的更新是直接的。长期记忆的更新则需要策略不宜每轮都存那样会信息过载。通常只在任务完成、或产生重要洞察时让LLM生成一个简洁的摘要后再存储。终止条件 (_should_stop) 的设计防止智能体陷入死循环或无关对话至关重要。除了设置硬性的最大迭代次数更智能的停止条件包括检测到LLM连续多次决定不使用工具可能意味着它认为问题已解决检测到用户输入中的明确终止指令或者当行动结果多次返回相似内容可能陷入循环时。5. 进阶技巧与性能优化当基础循环跑通后你可以通过以下技巧大幅提升AgentOS的效率和智能程度。5.1 提示词工程与思维链Chain-of-Thought在agent_brain/prompt_templates/中设计引导LLM进行深度思考的模板至关重要。除了简单的工具选择提示可以引入多步推理。例如在task_planner.j2中可以要求LLM请按以下步骤思考 1. 解读用户请求的真正意图和隐含需求。 2. 分解完成任务所需的子步骤。 3. 为每个子步骤分配合适的工具或信息。 4. 评估潜在的风险或缺失信息。 请将你的完整思考过程写在“thought”字段中。这种显式的要求能极大提高LLM决策的可靠性和可解释性。你可以在reasoning_engine.py中解析出这个“思考过程”并把它记录到日志里这对于调试复杂任务异常有用。5.2 并行化工具调用与工作流有些任务中的子步骤是独立的可以并行执行以节省时间。例如用户问“比较一下Python和Go在Web后端的优缺点”你可以并行调用“搜索Python后端优势”、“搜索Python后端劣势”、“搜索Go后端优势”、“搜索Go后端劣势”四个搜索工具然后汇总结果。这需要在Task定义中支持“并行步骤”并在主循环或一个专门的“工作流引擎”中管理并行执行和结果合并。5.3 记忆的优化摘要、压缩与向量化长期记忆的管理是避免“上下文污染”和成本失控的关键。摘要化存储不要存储完整的对话。在任务结束时或每N轮对话后用一个单独的LLM调用使用一个专门的“摘要提示词”将一段交互浓缩成几句话只存储摘要。分层记忆实现不同“重要性”级别的记忆。普通对话存入短期记忆重要结论存入长期记忆的“重要事实”库而程序性的操作步骤如“如何配置某个API”可以存入“操作指南”库方便以后检索复用。向量检索的优化使用向量数据库时为记忆片段生成高质量的嵌入Embedding是关键。有时对原始文本进行轻微的改写或提炼使用LLM后再嵌入能提高检索的相关性。例如将“用户说他昨天用Python写了一个爬虫遇到了SSL错误”改写成“技术问题Python爬虫SSL证书错误”更利于被“如何解决Python SSL错误”这类问题检索到。5.4 成本控制与降级策略LLM API调用是主要成本。在llm_client.py中实现缓存对完全相同的提示词请求进行缓存可以使用functools.lru_cache或Redis。令牌预算为每个任务或会话设置token消耗上限接近上限时自动切换到更小、更便宜的模型或终止任务。选择性思考对于简单、明确的问题如“11等于几”可以配置一个“快速通道”绕过复杂的思考循环直接匹配预定义的答案或调用简单工具避免不必要的LLM调用。6. 常见问题排查与实战心得在实际搭建和运行这套AgentOS的过程中你一定会遇到各种问题。下面是我踩过的一些坑和解决方案。6.1 LLM不按格式返回或“胡言乱语”这是最常见的问题。症状json.decoder.JSONDecodeError或者解析出来的字段名不对。排查首先检查你的提示词是否明确要求了JSON格式并给出了清晰的示例。其次在reasoning_engine.py的解析函数中加入详细的日志打印出LLM的原始回复。很多时候LLM会在JSON前后加上解释性文字比如“好的我将以JSON格式回复...”。你需要用代码如正则表达式去剥离这些多余内容。解决实现一个“解析-修复”循环。如果第一次解析失败将错误信息和原始回复重新发送给LLM并说“你返回的内容无法解析为JSON请严格只输出JSON格式为...”。通常第二次就能成功。如果同一个提示词频繁出错可能需要重新设计提示词使其指令更绝对例如“你必须且只能输出一个JSON对象不要有任何其他文字。”6.2 智能体陷入无效循环或“鬼打墙”症状智能体在几个相似的工具调用或思考结论间来回切换无法推进。排查查看日志中decision和action_result的历史。是不是工具返回的结果没有提供新信息是不是LLM的思考过程没有利用历史结果解决增强观察阶段确保在_make_observation中将过去几轮的行动和结果清晰地传递给LLM。可以加入一句提醒“注意你已经尝试过以下方法[历史列表]请避免重复。”修改终止条件在_should_stop中增加对循环的检测。例如如果最近3轮的decision[‘thought’]高度相似可以用简单字符串匹配或嵌入向量计算相似度则触发终止并返回“无法取得进展”的提示。引入外部中断在循环中增加一个检查点每N轮后如果任务未完成可以主动询问用户如果是在交互场景中或尝试一个全新的、随机的工具调用探索策略来跳出局部最优。6.3 工具调用失败或结果不可用症状工具执行抛出异常或者返回的结果格式让LLM无法理解。排查首先检查工具函数本身的代码和网络连接。其次检查LLM生成的tool_input参数是否符合工具args_schema的定义。有时LLM会臆造一些不存在的参数。解决强化工具描述在工具的description和args_schema中使用极其精确的语言描述功能和每个参数的含义、类型、示例。例如query: (string) 搜索关键词例如‘人工智能最新进展’。结果后处理在工具run方法的最后对返回结果进行清洗和格式化。确保返回的是一个完整、通顺的句子或段落而不是一堆原始数据。例如一个数据库查询工具不应该直接返回SQL结果集而应该转换成“查询到X条记录其中...”这样的自然语言描述。工具验证层在ToolManager.execute_tool中在调用具体工具前先用jsonschema库验证tool_input是否符合预定义的schema如果不符合直接返回验证错误而不是传给工具。6.4 记忆检索不相关或丢失关键信息症状智能体表现得“健忘”记不住刚刚说过的话或者长期记忆检索出的内容与当前问题无关。排查检查短期记忆的容量设置是否太小。检查长期记忆的检索逻辑。如果是向量检索检查嵌入模型是否合适以及存储的文本片段是否过于冗长或信息稀疏。解决优化记忆存储粒度不要存入大段对话。存入的是“信息点”。例如将“用户说他的生日是1990年5月10日”存储为“用户生日1990-05-10”。这通常需要在存入前用一个LLM调用做一次信息提取。混合检索策略不要只依赖向量检索。可以结合关键词如实体名词检索。例如对于包含具体日期、人名、地点的问题先用关键词过滤再用向量排序。给记忆加“元数据”为每段记忆打上标签如topic: “hobby”, entity: “user”, type: “fact”。检索时可以先用标签进行粗筛再进行语义精排。6.5 系统响应速度慢症状完成一个简单问题需要数十秒。排查使用Python的cProfile或简单的time()记录每个阶段的耗时LLM调用、工具执行、记忆检索。解决LLM调用异步化如果循环中有多个不依赖的LLM调用比如并行任务使用asyncio和异步HTTP客户端如aiohttp可以大幅缩短总时间。工具超时设置为每个工具调用设置超时如requests.get(timeout10)防止一个缓慢的外部API拖垮整个循环。缓存记忆检索结果对于相同的查询短期内的记忆检索结果可以缓存起来避免重复的向量计算或数据库查询。这套基于文件夹结构和循环构建的AgentOS方法论其魅力在于它的简洁性和可塑性。它不是一个需要你费力学习的庞大框架而是一套你可以完全掌控、并随需求任意改造的乐高积木。从今天开始选一个你一直想自动化的任务按照这个结构创建你的第一个agent_brain、第一个tool然后运行main_loop看着你的智能体第一次自主地调用工具、思考、并完成任务那种成就感是无与伦比的。在这个过程中你会更深刻地理解智能体技术的精髓并最终打造出真正适合你自己业务的、独一无二的AI助手。
返回列表