1. 项目概述从“黑盒”到“白盒”的Agent认知之旅最近在社区里看到不少朋友在讨论LangChain的Agent感觉它被描述得有点“玄学”好像一个能自动调用工具、完成复杂任务的“魔法黑盒”。很多教程上来就是initialize_agent然后丢给OpenAI的API跑起来效果时好时坏出了问题也不知道从何查起。这让我想起了自己刚开始接触Agent开发时踩过的坑那时候总觉得Agent内部有一套高深莫测的调度逻辑。直到后来我决定抛开框架自己动手从零“手搓”一个最简单的工具调用流程才恍然大悟Agent最核心的执行逻辑本质上就是一个精心设计的while循环。这个认知上的转变让我对LangChain、AutoGen乃至所有Agent框架的理解都上了一个台阶。今天我就把这个“手搓”的过程和背后的思考分享出来希望能帮你拨开Agent的迷雾真正理解其运作的“第一性原理”。无论你是想深入理解LangChain还是计划自研轻量级Agent这篇文章都会带你走通最核心的那条路。2. 核心逻辑拆解Agent 循环 状态机为什么说Agent的核心是个while循环我们先把那些花哨的名词——ReAct、Plan-and-Execute、Tool Calling——都放一边回归到一个最本质的问题如何让一个大语言模型LLM使用工具来完成一个它自身无法直接完成的任务比如让模型查询今天的天气。2.1 最简模型思考、行动、观察的循环这个过程可以抽象为一个经典的“感知-思考-行动”循环在Agent领域常被称为ReActReasoning and Acting模式。我们用最朴素的代码来描述它思考将用户的问题和可用工具的描述交给LLM让它决定下一步该做什么是直接回答还是调用某个工具。行动如果LLM决定调用工具我们就解析它的输出找到要调用的工具名和参数然后执行对应的函数。观察获取工具执行的结果比如天气数据。再思考将工具执行的结果连同之前的对话历史再次交给LLM让它基于新的信息决定下一步是继续调用工具还是可以给出最终答案了。这个“思考 - 行动 - 观察 - 再思考”的过程会一直持续到LLM认为它已经收集到足够的信息可以给出最终答案为止。这个持续的过程就是一个典型的while循环。循环的继续条件就是“任务是否完成”。这个模型如此基础以至于几乎所有复杂的Agent框架在最底层都是这个模式的变体和增强。2.2 状态是关键循环体内流转的“上下文”光有循环不够循环内部必须有一个承载所有信息的“状态”对象。这个状态至少需要包含对话历史用户的问题、模型之前的回复、工具执行的结果。可用工具列表模型可以调用的函数及其描述。中间结果上一步模型输出的解析结果如要调用的工具名和参数。停止条件一个标志用于判断循环是否应该结束例如模型输出了一个特殊的“最终答案”标记。在循环的每一轮我们都会用当前的状态去“询问”LLM得到它的输出后更新状态例如添加工具执行结果然后判断是否进入下一轮。这个状态对象就是LangChain中AgentExecutor内部维护的上下文也是LangGraph这类框架用图来明确管理的核心。注意很多初学者容易混淆“Agent”和“AgentExecutor”。简单理解“Agent”是那个做决策的大脑LLM决策逻辑而“AgentExecutor”是驱动大脑运转的身体它负责运行我们上面描述的那个while循环管理状态流转、工具调用和异常处理。我们“手搓”的其实就是这个“Executor”的核心部分。3. 动手实现从零构建一个最小可行Agent理解了核心是循环和状态管理后我们抛开LangChain用最直接的代码实现一个能调用工具查询天气和计算器的超简易Agent。我们将使用OpenAI的Chat Completion API作为LLM。3.1 第一步定义工具与模型交互首先我们定义两个简单的工具函数并封装一个与OpenAI模型对话的函数。import openai import json import os # 假设已设置环境变量 OPENAI_API_KEY client openai.OpenAI(api_keyos.getenv(“OPENAI_API_KEY”)) # 1. 定义工具函数 def get_weather(city: str) - str: 获取指定城市的天气信息。 # 这里模拟一个天气API的返回 weather_data { “北京”: “晴15°C到25°C微风”, “上海”: “多云18°C到28°C东南风3级”, “深圳”: “阵雨22°C到30°C南风4级”, } return weather_data.get(city, f“未找到{city}的天气信息。”) def calculator(expression: str) - str: 计算一个数学表达式的结果。警告使用eval需确保输入安全此处仅作演示。 try: # 严重警告在实际生产环境中直接eval用户输入是极度危险的 # 这里仅为演示应使用更安全的表达式解析库如ast.literal_eval。 result eval(expression) return f“{expression} {result}” except Exception as e: return f“计算错误{e}” # 可用工具列表包含名称、函数对象和描述 TOOLS [ { “name”: “get_weather”, “function”: get_weather, “description”: “根据城市名称查询当前天气情况。” }, { “name”: “calculator”, “function”: calculator, “description”: “计算一个数学表达式的结果例如 ‘(35)*2’。” } ] # 2. 封装模型调用函数 def call_llm(messages, tools): 调用OpenAI模型并传入工具定义。 # 将我们的工具格式转换为OpenAI的tool格式 openai_tools [] for tool in tools: openai_tools.append({ “type”: “function”, “function”: { “name”: tool[“name”], “description”: tool[“description”], # 在实际中我们需要定义更严谨的parameters JSON schema # 这里为简化假设模型能理解自然语言指令 } }) response client.chat.completions.create( model“gpt-3.5-turbo”, # 或 “gpt-4” messagesmessages, toolsopenai_tools, # 关键告诉模型有哪些工具可用 tool_choice“auto”, # 让模型自行决定是否调用工具 ) return response.choices[0].message3.2 第二步实现核心的Agent执行循环现在我们来编写那个最关键的while循环——agent_executor。def manual_agent_executor(user_query: str, max_turns: int 5): 手搓的Agent执行器核心循环。 # 初始化状态 messages [{“role”: “user”, “content”: user_query}] # 对话历史 final_answer None turn_count 0 print(f“用户问题: {user_query}”) print(“-” * 30) while final_answer is None and turn_count max_turns: turn_count 1 print(f“\n第 {turn_count} 轮思考...”) # 1. 思考调用LLM获取决策 llm_message call_llm(messages, TOOLS) messages.append(llm_message) # 将模型回复加入历史 # 检查模型是否想调用工具 if llm_message.tool_calls: # 2. 行动处理每一个工具调用模型可能同时调用多个 for tool_call in llm_message.tool_calls: tool_name tool_call.function.name tool_args json.loads(tool_call.function.arguments) # 解析参数 print(f“ - 决定调用工具: {tool_name}, 参数: {tool_args}”) # 查找并执行对应的工具函数 tool_to_use None for tool in TOOLS: if tool[“name”] tool_name: tool_to_use tool break if tool_to_use: # 执行工具 try: # 注意这里简化了参数传递实际需根据工具定义处理 # 例如我们的weather工具期望一个‘city’参数 if tool_name “get_weather”: # 从模型返回的arguments中提取city参数 # 模型可能返回{“city”: “北京”}或{“location”: “北京”}这里需要适配 city tool_args.get(“city”) or tool_args.get(“location”) if not city: city list(tool_args.values())[0] # 简易处理 tool_result tool_to_use[“function”](city) elif tool_name “calculator”: expression tool_args.get(“expression”) or tool_args.get(“calculation”) if not expression: expression list(tool_args.values())[0] tool_result tool_to_use[“function”](expression) else: tool_result f“未知工具: {tool_name}” except Exception as e: tool_result f“工具执行出错: {e}” print(f“ - 工具执行结果: {tool_result}”) # 3. 观察将工具执行结果作为新的消息追加到历史中 # 格式需符合OpenAI的tool call结果格式 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “content”: tool_result, }) else: # 工具未找到 error_msg f“工具 {tool_name} 不存在。” print(f“ ! 错误: {error_msg}”) messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “content”: error_msg, }) else: # 模型没有调用工具直接给出了最终答案 final_answer llm_message.content print(f“ - 模型给出最终答案: {final_answer}”) break # 跳出循环 # 安全检查防止无限循环 if turn_count max_turns: final_answer “达到最大循环次数任务可能未完成。” print(f“ ! 警告: {final_answer}”) return final_answer, messages # 3. 测试我们的手搓Agent if __name__ “__main__”: query “北京和上海今天的天气怎么样然后计算一下两地的平均温度假设北京25度上海28度。” answer, history manual_agent_executor(query) print(“\n” “”*30) print(f“最终答案: {answer}”)运行这段代码你会看到控制台打印出类似以下的执行过程用户问题: 北京和上海今天的天气怎么样然后计算一下两地的平均温度假设北京25度上海28度。 ------------------------------ 第 1 轮思考... - 决定调用工具: get_weather, 参数: {‘city’: ‘北京’} - 工具执行结果: 晴15°C到25°C微风 - 决定调用工具: get_weather, 参数: {‘city’: ‘上海’} - 工具执行结果: 多云18°C到28°C东南风3级 第 2 轮思考... - 决定调用工具: calculator, 参数: {‘expression’: ‘(25 28) / 2’} - 工具执行结果: (25 28) / 2 26.5 第 3 轮思考... - 模型给出最终答案: 北京的天气是晴15°C到25°C微风上海的天气是多云18°C到28°C东南风3级。根据您提供的温度北京25度上海28度两地的平均温度是26.5度。看一个能自主调用工具、进行多轮对话的Agent就这样在一个清晰的while循环中跑起来了它先调用天气工具获取信息然后调用计算器工具进行计算最后综合所有信息给出了最终回答。4. 深入解析从“手搓版”到LangChain的桥梁我们“手搓”的版本虽然简陋但已经揭示了所有Agent框架共有的核心要素。现在让我们对比一下LangChain的AgentExecutor看看它在这个核心循环之上做了哪些重要的“工业化”增强。4.1 状态管理的标准化AgentState在我们的代码里状态是散落在messages、final_answer等变量中的。LangChain将其抽象为一个统一的、可扩展的状态字典通常是一个TypedDict比如包含input、chat_history、intermediate_steps存储工具调用和结果的元组、agent_outcome等键。这使得状态的读写和传递更加规范也为更复杂的流程如分支、循环奠定了基础。LangGraph更是将这种状态流通过图节点和边显式地定义出来。4.2 工具调用的规范化bind_tools与Tool Calling我们手动解析模型输出并调用工具步骤繁琐且容易出错。LangChain通过bind_tools方法将工具的定义以一种结构化的方式遵循OpenAI的Function Calling或Tool Calling标准“绑定”到LLM上。这样模型返回的就是一个结构化的ToolCall对象而不是需要复杂解析的文本。AgentExecutor会自动处理这些对象的解析和分发执行大大简化了代码。# LangChain风格的工具绑定与调用对比我们的手动解析 from langchain_openai import ChatOpenAI from langchain.tools import tool tool def get_weather(city: str) - str: “”“获取天气”“” return f”{city}的天气是...” llm ChatOpenAI(model“gpt-3.5-turbo”) llm_with_tools llm.bind_tools([get_weather]) # 绑定工具 # 当模型决定调用工具时返回的是 AIMessage其 tool_calls 属性是结构化的列表 response llm_with_tools.invoke(“北京天气”) if response.tool_calls: for tc in response.tool_calls: tool_name tc[‘name’] tool_args tc[‘args’] # 已经是解析好的字典 # 执行工具...4.3 循环控制与异常处理的强化我们的简单循环只有最大轮次限制。而AgentExecutor提供了更健壮的控制早期停止当模型输出特定标记如”Final Answer:”时停止。错误处理工具执行出错时可以选择重试、忽略或返回错误信息给模型。超时控制防止单个任务运行时间过长。中间步骤记录详细记录每一轮的工具调用和结果便于调试和追溯。4.4 支持多种Agent类型与策略我们实现的是一个简单的ReAct式Agent。LangChain内置了多种Agent类型如OPENAI_FUNCTIONS专为OpenAI函数调用优化、STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION等。它们之间的区别主要在于提示词模板Prompt Template和输出解析器Output Parser。不同的提示词引导模型以不同的方式如ReAct格式的Thought/Action/Observation进行推理和输出输出解析器则负责从模型的文本回复中提取出结构化的动作指令。我们的“手搓”版可以看作一个自定义的、输出解析器为“解析JSON”的Agent。5. 实战避坑与高级技巧理解了本质再用LangChain这类框架时就能知其所以然也能更好地排查问题。以下是一些从实战中总结的经验。5.1 工具描述是“第一生产力”模型决定是否调用、如何调用工具几乎完全依赖于你提供的工具描述。模糊的描述会导致模型不理解或错误调用。差描述“一个计算工具。”好描述“用于计算数学表达式。输入应为一个字符串格式的数学表达式如’(35)*2‘或’sqrt(16)‘。支持加减乘除、括号和常见数学函数。”技巧在描述中明确输入参数的名称、类型、格式和示例。对于复杂参数可以暗示模型以JSON格式提供。5.2 管理好对话历史与Token消耗我们的循环不断将整个历史对话传入模型这会导致Token数快速增长成本增加也可能触及模型上下文长度上限。策略1总结历史。在历史达到一定长度后让模型或另一个总结模型对之前的对话进行摘要然后用摘要替换掉冗长的原始历史。策略2使用有状态的LLM包装器。一些服务或框架提供了能自动管理长上下文的LLM。策略3选择性记忆。只保留与当前任务最相关的历史片段。5.3 处理模型的“固执”与错误有时模型会陷入死循环反复调用同一个工具或坚持一个错误的决定。设置硬性限制如我们代码中的max_turns这是最后的安全网。在提示词中加入约束明确告诉模型“你最多只能调用X次Y工具”或“如果工具返回错误请尝试换一种方式提问或放弃该操作”。实现验证层在工具执行前对模型解析出的参数进行简单验证如参数类型、范围如果明显不合理可以直接返回错误信息给模型让它重新思考。5.4 调试让Agent的执行过程可视化当Agent行为不符合预期时最有效的调试方法是查看每一轮的输入和输出。开启LangChain的调试输出设置环境变量LANGCHAIN_VERBOSETrue可以打印出详细的链式调用过程。手动记录像我们“手搓”代码中的print语句一样记录下每一轮的messages模型的输入和llm_message模型的输出。这能帮你判断是提示词问题、工具描述问题还是输出解析问题。5.5 超越简单循环引入规划与反思对于复杂任务简单的逐步执行ReAct可能不够高效或容易迷失。这时就需要在循环中引入更高级的“子循环”或“节点”。规划Plan在行动前先让模型制定一个分步骤的计划。这类似于LangChain的Plan-and-Execute代理或者CrewAI中的Crew协调多个Agent。反思Reflect在行动后让模型评估当前结果是否满意是否偏离目标是否需要调整策略。这可以通过在循环中插入一个“反思步骤”来实现让模型基于历史进行自我批评和修正。这些高级模式本质上是在主while循环内部又嵌套了其他决策循环或固定流程。LangGraph这类框架擅长用图来定义这种复杂的、带分支和循环的工作流。6. 从理解到创造自定义你的Agent工作流当你掌握了“循环状态”这个核心后就不再局限于使用框架预设的Agent。你可以为特定场景设计定制的工作流。场景一个客服Agent需要先查知识库如果没答案再查订单系统最后都找不到才转人工。自定义工作流设计初始化状态包含用户问题。循环开始。节点A调用“知识库查询工具”。判断结果是否满意如果满意跳转到最终回答节点如果不满意继续。节点B调用“订单查询工具”。判断结果是否满意如果满意跳转到最终回答节点如果不满意继续。节点C执行“转人工逻辑”。循环结束。这个工作流可以用if-else在循环内实现也可以用更直观的图如LangGraph来定义。其内核依然是一个受控的执行循环。回过头看Agent的神秘面纱被揭开了。它不是什么魔法而是一个将大语言模型的推理能力与外部工具的执行能力通过一个受控循环连接起来的编程范式。这个循环管理着状态的流转根据模型的决策调度工具的执行并处理各种边界情况。LangChain等框架的价值在于它们将这个模式标准化、模块化提供了丰富的工具集成、健壮的异常处理、以及多种预设的决策策略Agent类型。但万变不离其宗下次当你再使用或调试一个Agent时不妨在脑海中勾勒出那个正在运转的while循环以及在其中流动的状态数据很多问题都会变得清晰起来。