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

资讯详情

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

LangChain Agent底层循环揭秘:用不可用工具看透推理-行动-反馈机制

LangChain Agent底层循环揭秘:用不可用工具看透推理-行动-反馈机制 在 LangChain 的组件体系里Agent 是最具备“自动决策感”的部分你给它一个任务它自己拆解步骤、调用工具、读取返回结果再决定下一步做什么。相比 Chain 的固定流程Agent 给人的感觉更像是一个“有手有脚”的 LLM。但如果你真的去追底层会发现 Agent 的工作方式其实不神秘核心就是一条循环推理 → 行动 → 反馈 → 再推理。本文不打算停留在概念层面而是用一个“自制造不可用工具”的演示把 Agent 底层这条循环暴露出来。通过观察工具不可用时的 Agent 表现我们会清楚看到模型如何被“反馈”驱动如何调整下一步动作以及为什么最终会停下或放弃。这篇文章适合两类读者刚开始接触 LangChain Agent想知道 Agent 到底怎么工作的新手。已经在用 Agent 开发但遇到“反复调用工具不收敛”“错误反馈丢失”“循环不终止”这类问题的开发者。学完之后你可以亲手复现完整的 Agent 循环过程并且对 LangChain AgentExecutor 的底层执行逻辑有一个直观认识。1. 从 Agent 的“自动决策”说起1.1 Agent 不是简单调用大模型很多人刚接触 LangChain 时会分不清 Chain 和 Agent 的区别。Chain 更像一条流水线输入先经过 Prompt A得到结果再传给 Prompt B流程是预先写死的。Agent 则不一样它允许模型在执行过程中自己决定“下一步该调用哪个工具、要不要继续尝试”。这样的设计解决了什么问题举一个真实场景假设你需要一个 AI 助手帮你查“某台服务器的 CPU 使用率”。如果只用一个 Prompt模型大概率会回答“我没有实时权限”或者干脆编造一个数字。但换成 Agent 后模型会意识到“我可以调用查询工具再根据工具返回的结果回答用户。”也就是说Agent 的价值在于把“知识”和“行动”连在一起。模型不再是单纯靠训练参数回答而是可以去外围系统“拿数据”来回答。1.2 推理-行动-反馈循环的本质ReAct 是 LangChain Agent 早期最经典的范式。它的名字由 Reasoning推理和 Acting行动组合而成核心思想可以浓缩成下面这条循环Thought推理 → Action行动 → Observation反馈 → Thought再次推理 ...每一轮循环可以做如下理解Thought推理模型根据任务和已有的 Observation判断自己当前掌握了什么信息还缺什么信息下一步该做什么。Action行动模型选择一个工具名称并给出调用参数。Observation反馈工具执行完返回一个结果。这个结果可能是成功数据、错误提示、甚至一段空文本。再推理模型把新的 Observation 当作新的输入决定是继续执行工具还是输出 Final Answer。整个过程会一直循环直到模型认为自己已经拿到足够的信息或者达到最大迭代次数上限。1.3 为什么用“不可用工具”观察底层最直观正常情况下Agent 调用工具两三次就能完成任务循环过程一闪而过用户很难察觉内部发生了什么。但如果你想真正理解 Agent 的底层机制就需要一个“慢动作”观察窗口。让工具“不可用”就是一个特别好的实验手段工具调用失败会形成一条错误反馈。模型拿到这条反馈后会重新推理甚至再次尝试相同动作。多次失败后模型要么更换策略要么放弃并给出一个兜底答案。这个“反复试错”的过程正好把推理-行动-反馈的循环拉长让内部逻辑变得可见。这也是本文用“不可用工具”做演示的原因。2. 环境准备与版本说明2.1 运行环境本文示例使用 Python 3.9 以上的版本操作系统不限Windows、macOS、Linux 都可以。示例项目结构如下langchain-agent-demo/ ├── main.py ├── tools.py ├── requirements.txt └── README.md在实际开发中可以根据项目大小自行调整。重点是先跑通最小示例再逐步扩展。2.2 安装 LangChain 相关依赖Agent 开发涉及的关键 Python 包包括langchain核心框架提供 Agent、Tool、Prompt 等组件。langchain-openaiOpenAI 风格的 Chat 模型封装也支持接 OpenAI 兼容的本地模型。langchain-community社区维护的各种工具和集成。安装命令pip install langchain langchain-openai langchain-community版本说明LangChain 0.2.x 和 0.3.x 中 Agent 相关 API 略有差异。本文示例以常用 API 为主重点是演示实现思路如果你用的是其他版本请先查看官方迁移文档对导入路径和参数做相应调整。2.3 模型接入方式示例中默认使用 OpenAI 兼容接口。如果你希望使用本地模型可以启动一个兼容 OpenAI 的服务然后通过base_url指向本地地址。from langchain_openai import ChatOpenAI llm ChatOpenAI( modelqwen2.5:7b, # 改成你自己的模型名称 base_urlhttp://localhost:8000/v1, # 本地兼容服务地址 api_keynot-needed, # 本地服务通常不需要真实 key temperature0 )如果你直接使用 OpenAI 官方模型可以不传base_url通过环境变量OPENAI_API_KEY配置密钥。3. ReAct 循环原理拆解3.1 一个循环内部的四步为了方便理解我们先用一个查询天气的小例子来说明循环内部发生了什么。假设 Agent 有两个工具get_weather(city)和get_city_code(city_name)。用户输入的问题是北京今天的天气怎么样第一轮循环Thought推理模型发现用户问的是“北京天气”而查询天气的工具可能需要城市编码。于是模型判断需要先调用get_city_code来获取编码。Action行动模型输出类似Action: get_city_code, Action Input: 北京。Observation反馈工具返回101010100。再推理模型拿到了北京的城市编码接下来会思考“现在我需要用这个编码查询天气。”第二轮循环Thought现在有城市编码了调用get_weather。ActionAction: get_weather, Action Input: 101010100。Observation工具返回晴25℃。再推理信息足够模型输出Final Answer: 北京今天晴气温25℃。可以看到Agent 的“决策”实际上是在每一轮循环里动态生成的不是一次性写死的。3.2 循环的终止条件循环不能无限执行下去否则任务永远结束不了还会消耗大量 token。LangChain Agent 的常见终止条件有以下几种终止方式说明模型输出 Final Answer正常终止Agent 认为已经拿到足够信息达到 max_iterations 上限AgentExecutor 强制终止工具执行抛异常且框架未捕获可能直接中断整个任务解析输出失败且配置不重试AgentExecutor 抛出解析错误在实际开发中max_iterations是一个非常重要但容易被忽略的参数。设置过小复杂任务可能没跑完就被强行打断设置过大模型可能陷入重复尝试白白消耗 token。3.3 工具异常如何进入反馈在 ReAct 循环里工具的返回结果统一作为 Observation 回到模型。这里有个关键问题如果工具执行失败错误信息算不算 Observation答案是看实现方式。如果工具内部捕获了异常只返回一段错误字符串那么模型会正常把这段字符串当作 Observation 继续推理。如果工具直接把异常抛出AgentExecutor 默认也会捕获异常文本并作为 Observation 传给模型。不过不同版本的行为有差异稍后的实战里我会给出具体的规避方案。如果错误信息过于模糊比如只返回Error模型很可能搞不清失败原因从而重复相同的动作。这条结论很重要。它说明反馈质量会直接影响 Agent 的决策质量。如果我们希望 Agent 在错误发生时快速调整就必须在工具层输出“可被推理的反馈”而不是简单抛一个空异常。4. 实战制造一个“不可用”工具观察 Agent 循环4.1 演示目标与场景设计为了看清 Agent 的循环我设计了一个“服务器监控助手”场景Agent 有两个工具list_servers和get_cpu_usage。list_servers正常运行返回服务器 ID 列表。get_cpu_usage对srv-01这台机器故意返回“查询失败”的字符串目的是模拟接口不可用。用户对 Agent 的提问是请查询 srv-01 的 CPU 使用率。正常情况下Agent 会先查服务器列表再查 CPU。但srv-01查询会一直失败于是我们可以观察 Agent 如何在“反馈”的驱动下反复尝试和最终收尾。4.2 构造自定义工具在 LangChain 中最方便的自定义工具方式是使用tool装饰器。# 文件路径tools.py from langchain.tools import tool tool def list_servers() - str: 返回当前可管理的服务器 ID 列表。 return srv-01, srv-02, srv-03 tool def get_cpu_usage(server_id: str) - str: 查询指定服务器的 CPU 使用率输入参数为服务器 ID例如 srv-01。 if server_id srv-01: # 故意模拟 srv-01 查询接口不可用 return ERROR: query failed with status_code500, reasonapi timeout, please check server status return fserver {server_id} cpu usage: 23.5%这里有一个细节值得注意我返回的是一串包含status_code和reason的字符串而不是简单返回Error。原因是让模型有足够的上下文去判断“失败是否与服务器本身有关”这会更接近真实故障场景。4.3 创建 Agent 并运行接下来在main.py中创建 ReAct Agent 并执行任务。# 文件路径main.py from langchain.agents import AgentExecutor, create_react_agent from langchain_core.prompts import PromptTemplate from langchain_openai import ChatOpenAI from tools import get_cpu_usage, list_servers # 1. 初始化模型 llm ChatOpenAI( modelqwen2.5:7b, base_urlhttp://localhost:8000/v1, api_keynot-needed, temperature0 ) # 2. 准备 ReAct 模板 prompt PromptTemplate.from_template( Answer the following questions as best you can. You have access to the following tools:\n {tools}\n\n Use the following format strictly:\n Question: the input question you must answer\n Thought: you should always think about what to do\n Action: the action to take, should be one of [{tool_names}]\n Action Input: the input to the action\n Observation: the result of the action\n ... (this Thought/Action/Action Input/Observation can repeat N times)\n Thought: I now know the final answer\n Final Answer: the final answer to the original input question\n\n Begin!\n\n Question: {input}\n Thought: {agent_scratchpad} ) # 3. 构建 ReAct Agent tools [list_servers, get_cpu_usage] agent create_react_agent(llmllm, toolstools, promptprompt) # 4. 包装成 AgentExecutor agent_executor AgentExecutor( agentagent, toolstools, verboseTrue, max_iterations6, handle_parsing_errorsTrue, return_intermediate_stepsTrue, ) # 5. 执行任务 if __name__ __main__: result agent_executor.invoke( {input: 请查询 srv-01 的 CPU 使用率} ) print(最终返回, result[output])代码说明verboseTrue会打印中间推理过程方便我们观察每一轮 Thought、Action、Observation。max_iterations6是防止 Agent 无限重试的安全阀。handle_parsing_errorsTrue表示当模型输出无法解析时框架尝试把解析错误传给模型让模型自行修正。4.4 运行结果解读不同模型的输出会有所差异但整体过程会类似下面这样 Entering new AgentExecutor chain... Thought: 用户想查询 srv-01 的 CPU 使用率。我需要先确认服务器列表确保该服务器存在。 Action: list_servers Action Input: Observation: srv-01, srv-02, srv-03 Thought: srv-01 在列表中现在调用 get_cpu_usage 查询。 Action: get_cpu_usage Action Input: srv-01 Observation: ERROR: query failed with status_code500, reasonapi timeout, please check server status Thought: 查询 srv-01 返回了 500 错误看起来是接口暂时不可用。我可以再次尝试查询确认是否是暂时性问题。 Action: get_cpu_usage Action Input: srv-01 Observation: ERROR: query failed with status_code500, reasonapi timeout, please check server status Thought: 连续两次查询都返回 500说明 srv-01 的 CPU 查询接口当前不可用。我不能编造 CPU 使用率因此应该告知用户该服务器接口暂时不可用。 Final Answer: srv-01 的 CPU 查询接口当前不可用建议稍后重试或检查服务器状态。 Finished chain.这个输出非常直观地展示了推理-行动-反馈的循环过程Agent 先调用list_servers获得反馈。Agent 再调用get_cpu_usage获得错误反馈。错误反馈驱动模型重新思考尝试再次调用同一个工具。再次失败后模型决定放弃并给出业务上合理 final answer。注意模型没有编造“23.5%”之类的假数据因为工具返回的反馈明确告诉它接口不可用。这说明反馈质量对最终答案的准确性非常关键。4.5 对比正常可用工具时的表现为了说明“不可用工具”背后发生了什么我们可以把get_cpu_usage改回可用状态tool def get_cpu_usage(server_id: str) - str: 查询指定服务器的 CPU 使用率输入参数为服务器 ID例如 srv-01。 return fserver {server_id} cpu usage: 23.5%重新运行后Agent 通常只需要两轮就能完成Thought: 用户想查询 srv-01 的 CPU 使用率我先查询服务器列表再查询具体使用率。 Action: list_servers Action Input: Observation: srv-01, srv-02, srv-03 Thought: srv-01 存在现在查询 CPU。 Action: get_cpu_usage Action Input: srv-01 Observation: server srv-01 cpu usage: 23.5% Thought: 已经拿到 srv-01 的 CPU 使用率可以给出最终答案。 Final Answer: srv-01 的 CPU 使用率为 23.5%。对比可以得出两个关键结论Agent 的循环次数是由反馈驱动的。反馈越顺利循环越短反馈越异常循环越容易拉长。模型在每一步推理时都依赖历史 Observation。如果把 Observation 从上下文里去掉Agent 就会“失忆”后续决策完全失去依据。如果想让 Agent 更早放弃重复尝试可以在 Prompt 中增加一条规则当某个工具连续返回同样错误时不要重复尝试超过两次请直接向用户说明故障。这样可以有效减少无效循环和 token 浪费。4.6 变体工具抛出异常时怎么处理真实项目中工具内部可能存在未捕获的异常。下面看一个变体tool def get_cpu_usage(server_id: str) - str: 查询指定服务器的 CPU 使用率输入参数为服务器 ID例如 srv-01。 if server_id srv-01: raise ValueError(srv-01 API endpoint refused connection) return fserver {server_id} cpu usage: 23.5%此时 AgentExecutor 在不同版本中的表现会有差异。更稳妥的做法是在工具内部统一捕获已知异常并返回结构化错误信息tool def get_cpu_usage(server_id: str) - str: 查询指定服务器的 CPU 使用率输入参数为服务器 ID例如 srv-01。 try: if server_id srv-01: raise ConnectionError(srv-01 api timeout) return fserver {server_id} cpu usage: 23.5% except Exception as e: return fERROR: {e}, params server_id{server_id}这种做法的好处是Agent 总能拿到反馈字符串循环可以继续模型可以根据错误内容决定下一步。5. 手写最简 ReAct 循环理解 LangChain 底层执行逻辑5.1 最小循环骨架看完上面的演示你可能会好奇LangChain 的 AgentExecutor 在底层到底做了什么其实核心循环逻辑并不复杂。我们可以用一段极简的伪代码模拟出来。# 文件路径minimal_react_loop.py from typing import Callable, Dict MAX_ITERATIONS 6 def build_react_prompt(task: str, history) - str: 把任务和此前的思考/观察结果拼进 Prompt prompt ( 你是一个有工具使用能力的 Agent。\n f用户任务{task}\n\n 可用工具\n - list_servers: 返回服务器列表\n - get_cpu_usage: 查询 CPU 使用率\n\n 历史执行记录\n ) for round_text in history: prompt round_text \n prompt 请继续输出你的下一步 Thought 和 Action。 return prompt def parse_llm_output(output: str): 简化解析如果包含 Final Answer视为结束否则从中抽取 Action 和 Action Input if Final Answer in output: return None, None, output # 实际项目中需要更稳健的解析这里只做演示 lines output.splitlines() action action_input for line in lines: if line.startswith(Action:): action line.replace(Action:, ).strip() if line.startswith(Action Input:): action_input line.replace(Action Input:, ).strip() return action, action_input, None def execute_tool(action: str, action_input: str) - str: 模拟工具执行 if action list_servers: return srv-01, srv-02, srv-03 if action get_cpu_usage: if action_input srv-01: return ERROR: query failed with status_code500, reasonapi timeout return fserver {action_input} cpu usage: 23.5% return ERROR: unknown tool def run_agent(task: str, llm_call: Callable[[str], str]): history [] for i in range(MAX_ITERATIONS): # 1. 推理把历史反馈交给模型 prompt build_react_prompt(task, history) llm_output llm_call(prompt) # 2. 解析输出 action, action_input, final_answer parse_llm_output(llm_output) if final_answer: return final_answer # 3. 行动执行工具 observation execute_tool(action, action_input) # 4. 反馈把本轮内容加入历史 history.append(fThought/Action: {llm_output}) history.append(fObservation: {observation}) return MAX_ITERATIONS reached, stop.这段代码虽然简陋但完整包含了一个 Agent 循环的四个关键部分build_react_prompt把历史 Observation 放回 Prompt让模型看到“反馈”。parse_llm_output从模型输出中解析动作指令。execute_tool执行工具并拿到反馈。history.append把反馈加入下一轮上下文。5.2 与 LangChain AgentExecutor 的对照LangChain 的 AgentExecutor 在底层做的事情本质上就是上面这段循环只是加了很多工程能力手写循环LangChain AgentExecutor手动拼接 Prompt内部封装 Prompt 模板和 agent_scratchpad简单字符串解析支持多种输出解析器并可配置错误重试手动限制迭代次数提供 max_iterations 参数手动把工具结果拼入历史自动维护 intermediate_steps异常要靠 try-except提供 handle_parsing_errors、handle_tool_error 等配置理解这个对照关系后再去看 LangChain 源码就不会觉得陌生。AgentExecutor 本质上是一个“循环执行器”而不是复杂的神话。5.3 手写循环暴露的常见问题手写 ReAct 循环时最容易暴露几个问题模型输出格式不稳定。真实模型经常不按模板输出解析器需要做容错。Observation 过长。工具返回内容太多几轮循环后上下文溢出。死循环。一旦模型反复生成相同 Action必须依赖 max_iterations 兜底。模型“看到”反馈但“听不进”反馈。某些模型在工具结果与用户问题冲突时更倾向相信自己的参数记忆需要 Prompt 中强调以 Observation 为准。这些坑在 LangChain AgentExecutor 里依然存在只是框架帮你解决了一部分并不能完全消除。6. 常见问题与排查思路6.1 问题排查表格在实际使用 LangChain Agent 时下面几个问题比较高频问题现象常见原因解决思路Agent 反复调用同一个工具一直不收敛工具返回的错误信息不够明确模型无法判断下一步优化工具返回内容添加上下文和失败原因Prompt 增加失败策略循环不终止持续输出 Thought/Action模型始终没有生成 Final Answer或解析失败设置 max_iterations配置 handle_parsing_errorsPrompt 增加 few-shot 示例工具抛异常导致任务整体中断未在工具内部捕获异常且框架策略不允许继续工具内部统一 try-except把异常转成结构化字符串反馈Agent 不调用工具直接给答案工具描述不清模型不知道当前任务需要调工具优化工具 name 和 description明确参数和返回格式工具返回内容过长导致上下文溢出工具结果太大历史 Observation 累积过多在工具内部做截断或摘要只保留最近几轮 ObservationAgent 最终答案与工具反馈矛盾模型依赖训练记忆忽视 ObservationPrompt 中显式强调“必须优先使用 Observation 中的信息回答”6.2 排查 CheckList如果你遇到了 Agent 行为异常可以按下面顺序排查打开verboseTrue观察每一轮 Thought、Action、Observation。确认第一轮 Action 是否合理模型是否理解了任务。检查工具返回内容是否包含足够信息错误信息是否明确。检查模型是否在拿到 Observation 后改变了 Action。确认max_iterations是否足够但也不要调得过大。查看是否发生了输出解析错误handle_parsing_errors是否开启。如果工具内部有网络请求、数据库访问先单独测试工具本身是否可用。7. 最佳实践与工程建议7.1 工具设计原则工具是 Agent 的“手”。工具设计得好不好直接决定 Agent 能不能稳定完成业务。建议遵循以下原则名称要像动词短语例如list_servers、get_cpu_usage、send_email避免含糊概念。description 要说明参数含义。模型通过 description 判断该什么时候调用工具写得太简单会让模型不会用。返回结构尽量清晰。推荐返回可解析的 JSON 字符串或包含明确字段的文本例如{status: ok, cpu_usage: 23.5%}。失败返回要带原因和上下文。例如ERROR: status_code500, reasonapi timeout, server_idsrv-01而不是Error。7.2 异常与反馈管理Agent 与普通程序最大的区别是错误信息也会成为决策输入。因此工具必须像“设计数据库 schema 一样”设计错误反馈。推荐做法import json from typing import Any, Dict def build_tool_response(status: str, data: Any None, message: str ) - str: body: Dict[str, Any] { status: status, data: data, message: message, } return json.dumps(body, ensure_asciiFalse)这样模型拿起反馈就能解析出状态、数据和错误消息后续行动会更准确。7.3 Prompt 与迭代次数控制ReAct Prompt 是 Agent 运行稳定性的关键。模板中至少要包含可用工具清单。必须遵循的输出格式。无法完成任务时的收尾规则。对 Observation 的信任声明。迭代次数建议结合任务复杂度设计查单条数据1~3 轮足够。多步操作5~6 轮。涉及多个工具的复杂任务可以设置到 8 轮但最好配合日志监控。另外在业务要求必须拿到结果时不要一味依赖 Agent 自行循环而应该增加重试、降级策略。7.4 日志与可观测性生产环境里verboseTrue不够用。建议记录intermediate_steps也就是完整链条。给每个请求一个 trace_id把入口参数、每轮 Thought/Action/Observation 记录下来。统计平均循环轮数、工具调用失败率用来评估 Agent 质量和模型表现。result agent_executor.invoke( {input: 请查询 srv-01 的 CPU 使用率}, return_only_outputsFalse, ) for step in result.get(intermediate_steps, []): action, observation step print(Action:, action.tool, action.tool_input) print(Observation:, observation)7.5 安全与权限边界Agent 能调用的工具必须经过授权评估。特别是涉及以下场景时需要格外谨慎删除、更新数据的工具。发送消息、邮件等对外操作。支付、转账、权限变更。生产环境配置修改。建议给高风险工具增加二次确认机制或者先将任务放到测试环境验证。任何时候都应遵循最小权限原则Agent 只拥有完成任务所需的最低权限。如果你的循环逻辑越来越复杂比如需要人工审批节点、条件分支、多 Agent 协作下一步可以考虑从AgentExecutor迁移到LangGraph。LangGraph 用显式状态图控制循环开发体验更像在“绘制一张流程图”。8. 总结与下一步学习路线通过本文的演示和手写循环你现在应该已经理解了 LangChain Agent 底层工作的核心Agent 不是一次调用大模型而是“推理 → 行动 → 反馈”的循环过程。工具返回的反馈会进入模型上下文影响下一步决策。当工具不可用时Agent 会表现出反复尝试的行为最终根据反馈决定放弃或更换策略。max_iterations是防止 Agent 失控的重要兜底参数。工具反馈的质量直接影响 Agent 最终回答的质量。如果你希望继续深入可以沿着以下几个方向学习阅读 LangChain 源码中AgentExecutor和BaseSingleActionAgent的实现对比本文手写的循环逻辑。学习 LangGraph用状态图的方式显式控制 Agent 循环、条件分支和人工审批。研究 Tool 自定义的更多细节比如 Pydantic 参数校验、异步工具、流式返回。实践 Agent 评估通过一组测试用例统计工具调用成功率、循环轮数、最终答案准确率。最后给你一个非常实用的建议在调试 Agent 时不要只盯着 Final Answer而要把中间步骤完整打印出来。如果你发现 Agent 反复调用同一个工具却不收敛优先去检查工具返回的反馈是否足够清晰而不是盲目调高max_iterations。Agent 的循环本身并不可怕可怕的是你看不到循环里发生了什么。
返回列表