复盘我的第一个 大模型Agent:从核心循环到模块化架构的演进之路
复盘我的第一个 大模型Agent从核心循环到模块化架构的演进之路引言从“对话”到“行动”的跃迁在接触大模型Agent之前我的认知止于“LLM 对话机器人”。直到我尝试构建第一个能调用工具、执行多步任务的Agent时才意识到真正的挑战在于如何让LLM从“被动回答”转向“主动规划与执行”。这篇文章将复盘我从零搭建Agent的过程剖析从“核心循环”到“模块化架构”的演进思路并用可运行的代码示例展示每个阶段的原理。## 一、核心循环Agent的“大脑”与“手脚”### 1.1 核心概念感知-思考-行动循环Agent的底层逻辑是循环接收输入感知→ LLM生成推理思考→ 执行工具行动→ 反馈结果。最简单的实现只需要一个while循环、一个LLM接口和一组工具函数。### 1.2 最小实现单循环Agent下面是一个纯Python实现的“思考-行动”循环使用OpenAI API和一个简单的计算器工具pythonimport openaiimport json# 工具函数一个简单的计算器def calculator(expression: str) - str: 安全计算数学表达式返回字符串结果 try: # 注意这里仅演示生产环境应使用更安全的eval result eval(expression, {__builtins__: {}}, {}) return str(result) except Exception as e: return f计算错误: {e}# 核心循环def agent_loop(user_input: str, max_steps: int 5): messages [ {role: system, content: 你是一个能调用工具的助手。工具格式: {\name\: \calculator\, \arguments\: {\expression\: \11\}}}, {role: user, content: user_input} ] step 0 while step max_steps: # 思考调用LLM生成响应 response openai.ChatCompletion.create( modelgpt-4, messagesmessages, functions[{ name: calculator, description: 执行数学计算, parameters: { type: object, properties: { expression: {type: string, description: 数学表达式如 23} }, required: [expression] } }], function_callauto ) message response[choices][0][message] # 如果LLM决定调用工具 if message.get(function_call): func_name message[function_call][name] args json.loads(message[function_call][arguments]) # 行动执行工具 result calculator(args[expression]) print(f[Step {step}] 调用工具 {func_name}({args}) - {result}) # 反馈将结果加入对话 messages.append(message) messages.append({ role: function, name: func_name, content: result }) else: # 否则直接输出最终答案 print(f[Step {step}] 最终回答: {message[content]}) break step 1# 运行示例agent_loop(计算 12345 * 6789 的结果)原理分析 -functions参数定义了工具规范LLM根据描述自主决定是否调用 - 每次循环LLM输出要么是工具调用包含名称和参数要么是自然语言回答 - 工具执行结果通过function角色注入对话历史形成连续推理上下文 这段代码只有30行但完整展示了Agent的核心循环。然而它存在明显问题工具扩展性差、错误处理薄弱、无法处理多步复杂任务。这促使我转向模块化设计。## 二、模块化架构从“硬编码”到“可插拔”### 2.1 为什么需要模块化随着任务复杂度提升单循环Agent暴露出三个痛点 1.工具管理混乱每个工具都需要在LLM调用中显式注册 2.状态维护困难无法暂停/恢复多步任务 3.可测试性差循环逻辑与业务逻辑耦合 模块化架构将Agent拆分为调度器Orchestrator、工具注册表Tool Registry、记忆管理器Memory Manager和策略引擎Policy Engine。### 2.2 模块化实现可扩展的Agent框架以下代码展示了一个模块化Agent的骨架支持动态注册工具、持久化记忆和可配置策略pythonfrom typing import Dict, Any, Callable, Listimport json# 1. 工具注册表管理所有可用工具class ToolRegistry: def __init__(self): self._tools {} def register(self, name: str, func: Callable, description: str, parameters: dict): self._tools[name] { function: func, spec: { name: name, description: description, parameters: parameters } } def execute(self, name: str, arguments: Dict[str, Any]) - str: tool self._tools.get(name) if not tool: return f错误工具 {name} 不存在 try: result tool[function](**arguments) return str(result) except Exception as e: return f执行错误: {e} def get_specs(self) - list: return [t[spec] for t in self._tools.values()]# 2. 记忆管理器维护对话历史class MemoryManager: def __init__(self): self.history [] def add_message(self, role: str, content: str): self.history.append({role: role, content: content}) def add_function_call(self, message: dict, result: str): self.history.append(message) self.history.append({ role: function, name: message[function_call][name], content: result }) def get_context(self) - list: return self.history# 3. 调度器协调循环class AgentOrchestrator: def __init__(self, tool_registry: ToolRegistry, memory: MemoryManager): self.tools tool_registry self.memory memory self.max_steps 10 def set_strategy(self, strategy: str auto): 设置工具调用策略auto/force/none self.strategy strategy def run(self, user_input: str): self.memory.add_message(user, user_input) for step in range(self.max_steps): # 调度器调用LLM此处用模拟代替实际API调用 llm_response self._simulate_llm(step) if llm_response[type] tool_call: tool_name llm_response[tool_name] args llm_response[arguments] result self.tools.execute(tool_name, args) print(f[Step {step}] 工具 {tool_name} 返回: {result}) self.memory.add_function_call(llm_response[message], result) else: print(f[Step {step}] 最终回答: {llm_response[content]}) return llm_response[content] return 达到最大步骤数 def _simulate_llm(self, step: int): 模拟LLM决策实际应替换为真实API调用 # 模拟第一步调用工具第二步回答 if step 0: return { type: tool_call, tool_name: calculator, arguments: {expression: 22}, message: { role: assistant, content: None, function_call: { name: calculator, arguments: {expression: 22} } } } else: return { type: answer, content: 根据计算224这是最终答案。 }# 使用示例registry ToolRegistry()registry.register( namecalculator, funclambda expression: str(eval(expression)), description执行数学计算, parameters{ type: object, properties: { expression: {type: string, description: 数学表达式} }, required: [expression] })memory MemoryManager()agent AgentOrchestrator(registry, memory)agent.run(帮我计算22)架构优势 -可扩展性通过ToolRegistry.register()动态添加工具无需修改循环逻辑 -可测试性每个模块独立测试例如单独测试工具执行、记忆回溯 -可配置性通过set_strategy()切换工具调用策略例如强制使用某个工具 ## 三、演进之路从原型到生产级架构### 3.1 第一阶段单循环原型-特征所有逻辑挤在一个while循环里 -教训当工具数量超过5个时代码难以维护LLM调用频率过高导致成本失控 -改进点引入工具注册表、添加调用频率限制 ### 3.2 第二阶段模块化重构-特征分离工具管理、记忆、调度 -关键调整 - 将工具描述从system prompt移到functions参数避免prompt爆炸 - 使用max_steps控制循环次数防止死循环 - 添加retry机制处理API调用失败 ### 3.3 第三阶段生产级增强-加入缓存层对重复的LLM请求如相同context使用LRU缓存 -异步化使用asyncio并发执行多个工具调用 -安全沙箱工具执行在隔离的docker容器中运行 ## 四、总结从单循环到模块化架构我的第一个Agent经历了从“能跑”到“能维护”的蜕变。核心循环是Agent的“心跳”——感知、思考、行动、反馈永不停歇而模块化架构则是“骨架”——让每个部件各司其职。给初学者的建议 1. 先用10行代码跑通核心循环理解“思考-行动”的本质 2. 当工具数量超过3个时立刻进行模块化重构 3. 始终关注可观测性记录每次LLM调用和工具执行的日志 Agent的本质不是“智能”而是“结构化的交互”。当你把LLM看作一个“会思考的调度器”将工具调用、记忆管理、错误处理封装为独立模块时Agent才真正从玩具变成工具。