ARTICLE DETAIL

资讯详情

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

从零手搓AI Agent:揭秘LangChain底层核心的While循环原理

从零手搓AI Agent:揭秘LangChain底层核心的While循环原理 1. 项目概述从“黑盒”到“白盒”的Agent认知之旅如果你最近在折腾AI应用开发尤其是想搞点能自主调用工具、完成复杂任务的智能体Agent那么“LangChain”和“Agent”这两个词大概率已经在你耳边磨出了茧子。各种教程、框架、开源项目层出不穷概念讲得天花乱坠什么ReAct、Plan-and-Execute、Tool Calling听起来高大上但真到自己动手想实现一个最简单的“让AI根据问题决定是否查天气”的功能时却常常被一堆抽象的类、复杂的回调函数和看不太懂的文档给绕晕了。我自己在早期摸索时就有这种感觉Agent像是个封装好的“黑盒”我知道输入和输出但中间那套“思考-行动-观察”的循环到底是怎么转起来的总隔着一层纱。直到有一天我抛开所有框架尝试用最基础的代码去模拟这个过程才恍然大悟Agent最核心的执行逻辑本质上就是一个精心设计的while循环。这个认知突破就像捅破了一层窗户纸让我对LangChain、AutoGen乃至所有Agent框架的设计都有了更深的理解。今天我就带大家亲手“搓”一个极简版的工具调用Agent不用任何框架只用Python基础语法和OpenAI的API或其他你熟悉的大模型API把那个神秘的“循环”掰开揉碎看清楚。你会发现所谓的智能体其内核可能比你想象的要简洁、直观得多。这篇文章适合所有对AI Agent感兴趣但又被复杂框架吓退的开发者。无论你是想入门Agent开发还是已经在用LangChain但想更透彻地理解其底层机制这次“手搓”之旅都会让你有所收获。我们将从零开始构建一个能理解用户指令、自主选择并调用工具比如计算器、搜索引擎模拟、然后根据结果继续决策或给出最终答案的微型Agent系统。过程中我会详细解释每一个判断条件的设计缘由分享如何设计提示词Prompt来引导模型的“思考”过程并总结那些在真实开发中容易踩坑的细节。2. 核心逻辑拆解Agent 状态机 While循环在深入代码之前我们必须先建立正确的思维模型。一个能够调用工具的Agent其核心工作流程可以抽象为一个状态机而这个状态机的驱动引擎就是一个while循环。2.1 状态机的三个核心状态想象一下一个智能助理的工作过程思考听到你的问题“北京和上海的气温差多少”它需要决定下一步该做什么。是直接回答还是需要查天气如果需要查先查哪个城市行动根据思考的结果执行一个具体的动作。比如调用“获取北京天气”的工具。观察拿到工具执行的结果“北京25°C”然后结合之前的上下文再次进入“思考”状态决定下一步哦还需要上海的天气。这个过程会循环往复直到智能助理认为它已经收集到足够的信息可以给出最终答案为止。对应到我们的程序我们需要定义几个关键的状态变量来控制这个循环thought(思考/推理): 模型对当前状况的分析和下一步计划。例如“用户想比较温差。我需要先获取北京和上海的当前气温。我应该先调用天气查询工具获取北京的气温。”action(行动): 根据思考结果具体要执行的操作。通常包括一个tool_name工具名和tool_input工具输入参数。例如{“tool_name”: “get_weather”, “tool_input”: {“city”: “北京”}}。observation(观察): 工具执行后返回的结果。例如“北京当前气温为25摄氏度。”final_answer(最终答案): 当模型判断无需再调用工具时给出的最终回复。例如“北京气温25°C上海气温28°C两地温差为3°C。”2.2 While循环的运转流程有了这些状态我们的while循环骨架就清晰了# 伪代码展示核心逻辑 def run_agent(user_query): # 初始化将用户问题作为初始“观察” conversation_history [用户说: user_query] final_answer None max_steps 10 # 防止无限循环 step 0 while final_answer is None and step max_steps: step 1 # 1. 思考阶段让模型分析历史决定下一步 thought, action get_model_thought(conversation_history) # 2. 判断模型是想给出最终答案还是调用工具 if action is None: # 模型决定直接回答 final_answer thought break # 3. 行动阶段执行模型选择的工具 observation execute_tool(action.tool_name, action.tool_input) # 4. 观察阶段将本次“行动”和“观察”记录到历史中 conversation_history.append(模型决定: action) conversation_history.append(工具返回: observation) # 循环结束返回最终答案或提示未完成 return final_answer or “经过多轮尝试未能得出最终结论。”这个循环会一直运行直到模型主动返回final_answer将action设为None或者达到最大步数限制防止陷入死循环。这就是所有Agent框架最核心的调度逻辑。LangChain的AgentExecutor、AutoGen的GroupChat底层都是在管理这样一个复杂的循环只是它们封装了更多功能比如并行工具调用、多Agent协作、更复杂的状态管理等。关键理解这个循环的“智能”来源完全依赖于我们在get_model_thought函数中如何设计提示词Prompt以及模型自身的推理能力。我们的代码只是提供了一个严格遵循“思考-行动-观察”范式的执行环境。3. 手搓实战构建一个极简数学与天气查询Agent现在让我们把理论付诸实践。我们将创建一个拥有两个工具的Agent计算器 (calculator)用于执行数学计算。天气查询模拟 (get_weather)模拟查询城市气温由于是模拟我们返回一个固定值或随机值。我们将使用OpenAI的Chat Completion API模型如gpt-3.5-turbo或gpt-4作为我们的大脑get_model_thought函数。你需要准备一个OpenAI的API密钥。3.1 第一步定义工具与工具执行函数工具是Agent的手臂。我们先定义好工具的描述和执行函数。# 首先安装必要的库openai # pip install openai import openai import json import random # 配置你的OpenAI API密钥 openai.api_key ‘你的API密钥’ # 定义工具列表。描述非常重要模型靠它来决定是否以及如何使用工具。 TOOLS [ { “name”: “calculator”, “description”: “用于执行数学计算。输入一个包含数字和运算符 - * / **的数学表达式字符串返回计算结果。例如输入 ‘(3 5) * 2’ 将返回 16.0。”, “parameters”: { “type”: “object”, “properties”: { “expression”: {“type”: “string”, “description”: “数学表达式如 ‘3 5 * 2’} }, “required”: [“expression”] } }, { “name”: “get_weather”, “description”: “获取指定城市的当前气温模拟值。输入城市名返回该城市的模拟气温摄氏度。这是一个模拟工具返回固定或随机值。”, “parameters”: { “type”: “object”, “properties”: { “city”: {“type”: “string”, “description”: “城市名称例如 ‘北京’、‘上海’} }, “required”: [“city”] } } ] # 工具执行函数 def execute_calculator(expression: str) - str: “”“执行数学计算。注意使用eval有安全风险此处仅用于演示。生产环境应使用更安全的解析库如ast.literal_eval或计算库。”“” try: # 警告实际项目中严禁直接eval不可信的用户输入 result eval(expression) return f“计算结果: {result}” except Exception as e: return f“计算错误: {e}” def execute_get_weather(city: str) - str: “”“模拟天气查询。返回一个随机的气温值。”“” # 模拟数据实际应调用天气API temperature random.randint(15, 35) return f“{city}的当前模拟气温为{temperature}摄氏度。” # 工具执行路由函数 def execute_tool(tool_name: str, tool_input: dict) - str: “”“根据工具名调用对应的工具执行函数。”“” if tool_name “calculator”: return execute_calculator(tool_input[“expression”]) elif tool_name “get_weather”: return execute_get_weather(tool_input[“city”]) else: return f“错误: 未知工具 ‘{tool_name}’。”注意事项与心得工具描述是灵魂模型完全依赖你提供的description和parameters来理解工具的功能和使用方法。描述必须清晰、准确、无歧义。可以参考OpenAI的Function Calling描述格式这有助于模型更好地理解。安全第一示例中的calculator使用了eval()这在演示中方便但在真实、开放的环境中极其危险会带来代码注入漏洞。务必替换为安全的表达式求值库如ast.literal_eval仅支持简单表达式或专门的数学表达式解析库如numexpr。模拟与真实get_weather是模拟工具。在实际应用中你需要将其替换为真正的API调用并处理好网络请求、错误重试、API密钥管理等。3.2 第二步构建核心的“思考-决策”函数这是整个Agent的“大脑”。我们需要设计一个提示词Prompt让模型学会按照ReActReasoning and Acting格式进行思考并规范地输出它的决策。def get_model_thought_and_action(conversation_history: list) - tuple: “““ 让模型基于对话历史进行思考并决定下一步行动调用工具或直接回答。 返回一个元组 (thought, action)。 action为None表示直接给出最终答案。 “““ # 构建系统提示词这是指导模型行为的关键 system_prompt “”“你是一个智能助手可以调用工具来帮助用户解决问题。请遵循以下步骤思考 1. 分析用户的请求和当前的对话历史。 2. 决定是否需要调用工具来获取更多信息。如果需要请严格按照以下JSON格式输出你的行动 { “thought”: “你的推理过程解释为什么需要调用这个工具以及如何使用它。”, “action”: { “tool_name”: “工具名称必须是以下之一calculator, get_weather”, “tool_input”: {“参数名”: “参数值”} // 参数必须符合工具描述 } } 3. 如果你认为已经拥有足够的信息可以直接回答用户的问题请输出 { “thought”: “你的推理过程总结已有信息并给出答案。”, “action”: null, “final_answer”: “给用户的最终答案” } 请确保你的输出是有效的JSON且只包含这个JSON对象不要有其他任何文本。 可用的工具描述如下 {tools_descriptions} ”“”.format(tools_descriptionsjson.dumps(TOOLS, indent2, ensure_asciiFalse)) # 构建用户消息即整个对话历史 user_message “\n”.join([f“{msg[‘role’]}: {msg[‘content’]}” for msg in conversation_history]) try: response openai.ChatCompletion.create( model“gpt-3.5-turbo”, # 或 “gpt-4” messages[ {“role”: “system”, “content”: system_prompt}, {“role”: “user”, “content”: user_message} ], temperature0.1, # 低温度使输出更确定减少随机性 max_tokens500 ) model_output response.choices[0].message.content.strip() # 解析模型的JSON输出 parsed_output json.loads(model_output) thought parsed_output.get(“thought”, “”) action_dict parsed_output.get(“action”) final_answer parsed_output.get(“final_answer”) if action_dict is None: # 模型决定直接回答 return thought, None, final_answer else: # 模型决定调用工具 action {“tool_name”: action_dict[“tool_name”], “tool_input”: action_dict[“tool_input”]} return thought, action, None except json.JSONDecodeError as e: print(f“模型输出不是有效的JSON: {model_output}”) print(f“错误: {e}”) return “尝试解析模型输出时出错。”, None, “抱歉我处理您的请求时出现了内部错误。” except Exception as e: print(f“调用API或处理时出错: {e}”) return f“系统错误: {e}”, None, “抱歉服务暂时不可用。”核心技巧与避坑指南提示词工程系统提示词system_prompt是成功的关键。它必须明确指令告诉模型要遵循“思考-行动”的格式。提供范例通过清晰的JSON结构示例让模型学会如何输出。我们这里用了“伪范例”即在指令中描述格式。限定工具明确列出可用的工具名和描述防止模型“幻想”出不存在的工具。要求纯JSON强调“只包含JSON对象”这能极大提高输出格式的稳定性。对于更复杂的场景可以使用少样本Few-Shot提示在提示词中直接给出几个输入输出的例子。温度Temperature设置对于需要稳定、可解析输出的Agent任务建议将temperature设置为较低的值如0.1或0以减少模型的随机性确保JSON格式输出的可靠性。健壮的错误处理一定要捕获json.loads可能抛出的异常。模型有时可能会在JSON前后添加无关的标记或解释。更健壮的做法是使用正则表达式或尝试从响应文本中提取JSON块。3.3 第三步组装完整的Agent执行循环现在我们把大脑思考函数和手臂工具执行用while循环连接起来。def run_agent(user_query: str, max_steps: int 8) - str: “”“运行Agent的主函数。”“” # 初始化对话历史用户查询是第一条记录 conversation_history [ {“role”: “user”, “content”: user_query} ] final_answer None step 0 print(f“用户提问: {user_query}”) print(“-” * 40) while final_answer is None and step max_steps: step 1 print(f“\n[步骤 {step}]”) # 1. 获取模型的思考和决策 thought, action, direct_answer get_model_thought_and_action(conversation_history) if direct_answer is not None: # 模型决定直接给出最终答案 print(f“模型思考: {thought}”) print(f“模型决定直接回答。”) final_answer direct_answer break if action is None: # 理论上不会走到这里因为direct_answer和action至少一个非None但做安全处理 print(“模型未返回有效行动结束循环。”) final_answer “无法处理您的请求。” break # 2. 打印思考过程并执行工具 print(f“模型思考: {thought}”) print(f“模型行动: 调用工具 ‘{action[‘tool_name’]}’ 输入: {action[‘tool_input’]}”) observation execute_tool(action[‘tool_name’], action[‘tool_input’]) print(f“工具观察: {observation}”) # 3. 将本次“行动”和“观察”追加到对话历史中供下一轮思考参考 # 注意这里我们以“assistant”的角色记录模型的行动以“tool”或“user”的角色记录观察结果。 # 这是一种常见的记录方式有助于模型理解上下文。 conversation_history.append({“role”: “assistant”, “content”: json.dumps({“action”: action}, ensure_asciiFalse)}) conversation_history.append({“role”: “user”, “content”: f“工具调用结果: {observation}”}) # 循环结束 print(“-” * 40) if final_answer is not None: print(f“\n最终答案: {final_answer}”) return final_answer else: timeout_msg f“经过 {max_steps} 轮尝试仍未得出最终结论。可能问题过于复杂或需要其他工具。” print(timeout_msg) return timeout_msg # 让我们来测试一下 if __name__ “__main__”: # 测试用例1简单计算 print(“测试1: 简单计算”) run_agent(“请计算一下 (12 34) * 2 等于多少”) print(“\n” ““*50 “\n”) # 测试用例2需要多步推理和工具调用 print(“测试2: 多步查询天气与计算”) run_agent(“北京和上海的气温差多少度假设北京气温是模拟值。”) print(“\n” ““*50 “\n”) # 测试用例3无需工具调用 print(“测试3: 直接回答”) run_agent(“你好请介绍一下你自己。”)运行这段代码你将在控制台看到Agent完整的思考、行动和观察过程。对于测试用例2输出可能类似于用户提问: 北京和上海的气温差多少度假设北京气温是模拟值。 ---------------------------------------- [步骤 1] 模型思考: 用户想比较北京和上海的温差。我需要先获取这两个城市的当前气温。我有模拟天气查询工具。我应该先调用工具获取北京的气温。 模型行动: 调用工具 ‘get_weather’ 输入: {‘city’: ‘北京’} 工具观察: 北京的当前模拟气温为22摄氏度。 [步骤 2] 模型思考: 我已经获取了北京的气温22°C。现在我需要获取上海的气温来计算温差。 模型行动: 调用工具 ‘get_weather’ 输入: {‘city’: ‘上海’} 工具观察: 上海的当前模拟气温为28摄氏度。 [步骤 3] 模型思考: 现在我有了北京22°C和上海28°C的气温。要计算温差我需要用上海的气温减去北京的气温。我需要调用计算器工具。 模型行动: 调用工具 ‘calculator’ 输入: {‘expression’: ‘28 - 22’} 工具观察: 计算结果: 6.0 [步骤 4] 模型思考: 计算器显示温差是6°C。我现在拥有了所有必要信息可以直接回答用户的问题了。 模型决定直接回答。 ---------------------------------------- 最终答案: 根据模拟数据北京当前气温约为22摄氏度上海当前气温约为28摄氏度两地的气温差约为6摄氏度。看一个能够自主规划、调用工具、并最终给出答案的智能体就这样用几十行核心代码实现了。这个while循环清晰地展示了Agent从“感知”到“决策”再到“执行”的完整闭环。4. 从“手搓”理解LangChain的Agent设计通过我们自己的实现再回头去看LangChain的Agent很多概念就变得一目了然了。我们的get_model_thought_and_action函数本质上就是LangChain中的Agent类如ZeroShotAgent、OpenAIFunctionsAgent所做的事情它根据提示词和历史生成一个AgentAction或AgentFinish对象。我们的while循环和execute_tool函数对应的就是LangChain的AgentExecutor。这个执行器负责管理循环、调用工具、处理错误、管理中间步骤即我们的conversation_history。AgentExecutor还封装了许多我们简易版本没有的工业级特性错误处理与重试当工具调用失败或模型输出格式错误时进行重试或降级处理。步骤数限制与超时防止无限循环我们只用了简单的max_steps而LangChain有更细致的控制。中间步骤截断当对话历史很长时如何智能地压缩或总结历史以避免超出模型上下文长度。我们的简易版本会不断追加历史很快会超长。并行工具调用一些高级Agent可以同时调用多个工具这需要更复杂的状态管理。更丰富的工具定义LangChain的Tool类提供了更规范的接口、错误处理和元数据。bindTools等概念的理解在一些最新的框架或API中如OpenAI的Assistant API或LangChain的新接口你会看到bind_tools()或类似的方法。这其实是一种工具绑定机制。它的核心目的是将工具的描述信息以一种更结构化、更易于模型理解的方式“注入”到模型调用中。例如OpenAI的Function Calling功能就需要将工具的模式schema作为参数传递给API调用。bind_tools()做的就是这件事它把我们的工具列表转换成模型能识别的格式并确保每次模型调用时都能“看到”这些工具的定义。在我们的手搓版本中我们是通过在系统提示词里以JSON字符串的形式“硬编码”了工具描述来实现的。bind_tools()是一种更优雅、更通用的封装。5. 常见问题、调试技巧与进阶思考在实际操作中你肯定会遇到各种问题。下面是我在开发和调试这类Agent时积累的一些经验。5.1 模型不按格式输出怎么办这是最常见的问题。我们的代码严重依赖模型输出严格的JSON。强化提示词在系统提示词中更严厉地要求格式例如“你必须且只能输出一个JSON对象不要有任何其他前缀、后缀或解释。”使用少样本提示在提示词中提供1-2个完美的输入输出示例让模型模仿。后处理清洗如果模型总是在JSON外加了“json”标记可以写一个简单的函数来提取两个 之间的内容。降级方案如果多次尝试后仍无法解析可以设计一个fallback流程比如让模型用更简单的文本格式输出“工具名:参数”再由代码进行解析。考虑使用原生Function Calling如果使用OpenAI的模型强烈建议直接利用其内置的function_call或tools参数。这能从根本上保证输出格式的稳定性。我们的手搓方法更像是一个通用的、模型无关的原理演示。5.2 Agent陷入死循环或逻辑混乱设置明确的停止条件除了最大步数还可以在提示词中强调“当你认为信息足够时必须给出最终答案”。优化工具描述模糊或错误的工具描述会导致模型误用工具。确保描述精准并说明工具的局限性例如“此天气工具为模拟返回随机值”。在历史中提供负面反馈如果模型做出了错误决策你可以在下一轮的对话历史中以“用户”或“系统”的身份插入一条纠正信息例如“上一轮你调用的工具X并不适用于这个问题因为Y。请重新思考。”使用更强的模型对于复杂的多步推理GPT-4通常比GPT-3.5-Turbo表现更稳定、更少出现逻辑混乱。5.3 上下文长度爆炸我们的简易实现会无限制地增长conversation_history很快就会超过模型的上下文窗口。总结历史在历史达到一定长度后可以调用模型对之前的对话进行总结然后用总结文本替代冗长的原始历史。LangChain的ConversationSummaryBufferMemory就是干这个的。滑动窗口只保留最近N轮比如最近10条的对话消息。选择性记忆更高级的方案是只保留与当前任务最相关的历史片段。5.4 如何评估和提升Agent的性能“手搓”Agent的一个巨大优势是透明度和可调试性。你可以清晰地看到每一步的thought。构建测试集针对你的应用场景设计一系列有代表性的问题单步、多步、需要逻辑推理、存在歧义等。自动化测试编写脚本批量运行测试集自动检查最终答案是否正确并记录每一步的决策分析错误模式。迭代提示词根据错误分析不断优化你的系统提示词和工具描述。这是提升Agent性能性价比最高的方法。人工审查中间步骤对于复杂任务人工查看模型的thought字段能非常直观地发现它是如何“想歪”的从而进行针对性调整。通过这次“手搓”Agent的练习我希望你不再将Agent框架视为神秘的黑盒。它们提供的是一种强大的设计模式和一系列便利的工程化组件但最核心的“思考-行动”循环思想是朴素而通用的。理解了这个核心无论是使用LangChain、AutoGen还是未来新的框架你都能更快地上手并能更自信地进行定制和调试。当你遇到框架的限制或奇怪的bug时不妨回想一下这个最简单的while循环也许你就能自己找到问题的根源甚至动手实现一个更适合自己需求的轻量级解决方案。
返回列表