LangChain Agent循环机制拆解:从不可用工具看推理-行动-反馈闭环 LangChain 的 Agent 模块一直被当作“让大模型调用工具”的入口但很多人只记住了怎么用现成工具没想清楚它底层的执行方式。一旦自己写一个自定义工具并且这个工具在运行中报错整个调用链就会从一次简单的问答变成一个多轮循环模型先基于用户问题做推理选出一个行动执行工具后拿到反馈再把反馈交给模型做下一次推理。如果问题没有得到解决这个循环会继续直到模型给出最终答案或者 Agent 的迭代次数被强制截断。今天这篇文章就用“自造工具不可用”这个例子把 LangChain Agent 的推理-行动-反馈循环完整地拆开看看这个循环是如何跑起来、如何犯错、如何被限制的。这篇文章适合刚接触 LangChain、想自定义工具但还没完全理解 Agent 运行机制的开发者。学完后你不光能写自己的工具还能在工具失败时快速判断问题出在推理环节、工具执行环节还是 Agent 配置环节。如果以后要往 LangGraph、多 Agent 协作方向发展这篇文章里的“循环”理解也能直接迁移过去。1. 推理-行动-反馈循环Agent 看似聪明本质是循环决策1.1 为什么工具失败反而能放大 Agent 的底层机制很多教程都告诉你“用 tool 装饰一个函数Agent 就能调用它”。这句话给初学者留下一个错觉调用工具是自动发生的就像普通函数调用一样。实际上Agent 并不是把用户的提问直接发给你写的函数而是先由大模型理解问题再生成一段“思考文本”这段文本里会包含一个工具名和一个输入参数LangChain 再把这段文本解析出来去调用对应工具。当工具正常返回时循环不明显。模型调用工具拿到结果说一句“答案是 128”整个过程看起来就像一次问答。但当工具不可用时也就是函数内部抛出异常或返回一段错误信息模型的下一轮推理就会被迫出现。这个被误认为是“出错”的瞬间恰好是整个 Agent 机制最容易观察的地方模型必须面对一段失败反馈。模型不能简单地假装问题已经解决。模型必须重新思考选择是否换一个工具、换一个参数还是直接给出最终答案。换句话说工具失败不是 Agent 的 bug而是理解 Agent 循环的最佳调试窗口。1.2 循环的最小组成单元ReAct 模式把 Agent 的一轮执行拆成三个核心节点推理模型根据用户问题和已有的上下文写出当前判断通常是“Thought: 我需要先查询库存”。行动模型指定要调用的工具和参数格式通常是“Action: inventory_query”和“Action Input: {sku: ABC123}”。反馈工具执行后返回结果。结果可能是正常数据也可能是错误文本LangChain 会把这段结果作为 Observation 放回上下文。一轮推理-行动-反馈结束后如果模型还拿不出最终答案它就会再次进入推理环节。这就是循环。理解这个结构后再看 LangChain 里那些提示词模板、AgentExecutor 配置思路会清晰很多。循环节点常见格式作用推理Thought: ...让模型解释当前判断行动Action: tool_name / Action Input: {...}指定要调用哪个工具及参数反馈Observation: result把工具执行结果交回模型终止Final Answer: ...模型认为可以回答原始问题这里的“反馈”不是简单的成功回传。Observation 里可以放正常返回值也可以放异常信息、错误提示、超时提示。模型不会自动区分“这是正确答案”还是“这是报错”它只会把这段文本当成新的上下文继续推理。所以工具开发者如果想控制 Agent 的行为核心工作就变成了“设计反馈文本”。2. 准备实验环境版本、模型和项目结构2.1 安装 LangChain 及配套依赖现阶段 LangChain 的 API 变化很快网上很多老代码用的是initialize_agent这类接口在新版本里已经很难直接使用。下面示例按 LangChain 0.2.x 的常见写法给出。落地前先确认你要用的版本再决定代码怎么写。建议使用虚拟环境避免把不同项目的依赖混在一起python -m venv .venv source .venv/bin/activate然后安装依赖pip install --upgrade pip pip install langchain0.2,0.4 langchain-core langchain-openai langchain-ollama python-dotenv依赖用途langchainAgent、工具调用等核心编排能力langchain-coreBaseTool、PromptTemplate 等基础抽象langchain-openai调用 OpenAI 兼容 APIlangchain-ollama调用本地 Ollama 模型python-dotenv从 .env 文件加载密钥如果使用的是钉钉、智谱、DeepSeek 等国内模型平台的 OpenAI 兼容接口langchain-openai也能通过base_url指向对应地址并不一定非要langchain-ollama。2.2 选择模型本地 Ollama 或云 API演示 Agent 循环需要一个真正支持函数调用的模型但模型能力不必很强。本地 Ollama 是最容易起步的方式。先安装 Ollama然后拉取一个中文能力较好的模型ollama pull qwen2.5:7b代码里用ChatOllama连接from langchain_ollama import ChatOllama llm ChatOllama(modelqwen2.5:7b)如果使用云厂商的 OpenAI 兼容接口则用ChatOpenAI并配置环境变量import os from langchain_openai import ChatOpenAI llm ChatOpenAI( modelos.getenv(LLM_MODEL, qwen-plus), api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL, https://api.example.com/v1), )这里能看到一个很关键的取舍本地模型可以反复调试出错成本低但生成格式可能不稳定云 API 更稳定但连续循环会明显增加 token 消耗。所以本文推荐先在本地跑通再换云模型验证。注意不同模型的 ReAct 文本生成格式存在差异。如果模型输出的 Action Input 格式不标准LangChain 解析时会报错这个问题会在第 6 节重点排查。2.3 准备一个最小的项目结构实验项目只需要两个文件agent_debug/ ├── .env # 存放 API Key 等环境变量 ├── main.py # Agent 与工具代码 └── requirements.txt # 依赖清单把依赖写进requirements.txt防止以后升级时需要翻历史记录langchain0.2,0.4 langchain-core langchain-ollama langchain-openai python-dotenv如果只用 Ollama 本地模型langchain-openai可以暂时不装。3. 故意制造一个不可用的自定义工具3.1 用 tool 声明一个业务函数给 Agent先创建一个正常工作的“商品信息查询”工具用于对比。这个工具返回一段结构化文本逻辑非常简单from langchain_core.tools import tool tool def product_lookup(sku: str) - str: 查询指定 SKU 的商品基础信息包括商品名、分类和品牌。 return fSKU {sku} 对应商品名LangChain 实战手册分类技术图书这里有几个关键点函数名product_lookup会成为工具名Agent 在生成 Action 时必须使用这个名字。参数sku会被 LangChain 自动解析成工具的输入 schema。函数注释就是工具的 description。模型主要依靠这段描述来决定“什么情况下该调用这个工具”。再创建一个“库存查询”工具。这个工具的代码里故意加了一个环境变量开关模拟外部库存服务不可用的情况import os def _call_inventory_api(sku: str) - str: if os.environ.get(INVENTORY_FORCE_ERROR) 1: raise ConnectionError(finventory service unavailable, sku{sku}) return fSKU {sku} 当前库存数量128 tool def inventory_query(sku: str) - str: 查询指定 SKU 的实时库存数量只有业务系统才能返回准确结果。 return _call_inventory_api(sku)3.2 让工具失败抛异常与返回错误信息是两条路上面代码里_call_inventory_api在条件满足时直接抛异常。工具内部抛异常后LangChain 会根据版本和 AgentExecutor 配置把异常包装成错误文本或者直接中断流程。为了让行为可控生产环境中更推荐让工具返回错误信息而不是抛出异常tool def inventory_query(sku: str) - str: 查询指定 SKU 的实时库存数量只有业务系统才能返回准确结果。 if os.environ.get(INVENTORY_FORCE_ERROR) 1: return ERROR: inventory service unavailable return fSKU {sku} 当前库存数量128这条路径的差异很重要实现方式反馈给模型的内容适用场景抛异常由框架按策略处理可能变成错误文本适合测试框架错误处理不适合直接给模型看返回错误字符串错误直接成为 Observation可预期、可日志记录生产环境更推荐返回 JSON 结构模型容易提取错误码和消息需要后续代码根据错误码做判断时更合适本文演示选择抛异常因为它的不可用感更强也更能看出 Agent 的反馈环节。理解后再切换成返回错误字符串你就能清楚两种设计的差别。3.3 先单独验证工具本身别急着接 Agent直接接 Agent 调试时问题会混在一起你很难分清是工具代码错了还是 Agent 没有正确解析模型输出。稳妥做法是先单独调用工具# 单独验证工具 print(inventory_query.invoke({sku: ABC123}))这里要注意一点tool生成的对象可以直接用invoke调用参数保持 dict 形式。可以先不加INVENTORY_FORCE_ERROR看到正常返回值SKU ABC123 当前库存数量128再模拟失败INVENTORY_FORCE_ERROR1 python main.py如果抛异常你会在控制台看到ConnectionError。这时再继续组装 Agent就能确定“工具本身确实报错”而不是 Agent 配置问题。4. 组装最小 Agent观察循环如何跑起来4.1 使用 ReAct 提示词构建 AgentLangChain 中常见做法是使用create_react_agent配合一段带{tools}、{tool_names}、{input}、{agent_scratchpad}的提示词。agent_scratchpad是核心变量它保存前面已经执行过的推理、行动、反馈内容让模型在做下一轮思考时能看到自己刚才的行动过程。下面这段提示词是从 ReAct 的经典格式简化来的from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import PromptTemplate prompt PromptTemplate.from_template( 你是一个电商库存助手。请使用下面的工具回答问题。 你可以使用这些工具 {tools} 请严格按以下格式输出 Question: 用户输入的问题 Thought: 你应该先思考要做什么 Action: 要调用的工具名只能是 [{tool_names}] 中的一个 Action Input: 调用工具时输入的参数必须是 JSON 格式 Observation: 工具返回的结果 Thought: 根据结果继续思考 ... 可以重复多轮 Thought: 我现在知道最终答案了 Final Answer: 对原始问题的最终回答 开始。 Question: {input} Thought: {agent_scratchpad} ) tools [inventory_query, product_lookup] agent create_react_agent(llmllm, toolstools, promptprompt) executor AgentExecutor( agentagent, toolstools, verboseTrue, handle_parsing_errorsTrue, max_iterations5, )这里每个参数都有明确目的verboseTrue会把 Agent 每一步的思考文本显示在控制台是观察循环的最重要开关。handle_parsing_errorsTrue允许模型输出格式不符合 ReAct 模板时把解析错误文本返回给模型让模型重新修正。这个设置也会推动循环继续而不是直接崩溃。max_iterations5是循环的安全阀。没有它碰到一个反复失败的问题时模型可能会无限循环白白消耗 token。4.2 带着“坏工具”执行一次查询现在执行一个看起来非常简单的问题result executor.invoke({input: SKU ABC123 目前还有多少库存}) print(result[output])这次执行不是从 Agent 里去掉工具而是把失败开关打开。运行命令INVENTORY_FORCE_ERROR1 python main.py当verboseTrue时终端会打印类似下面的过程。不要把它看成普通日志这是 Agent 完整的推理轨迹 Entering new AgentExecutor chain... Thought: 用户需要查询库存数量我需要先调用库存查询工具。 Action: inventory_query Action Input: {sku: ABC123} Error: ConnectionError: inventory service unavailable, skuABC123 Thought: 库存接口报错了。库存服务可能暂时不可用但商品本身可能仍然存在我先查一下商品信息。 Action: product_lookup Action Input: {sku: ABC123} Observation: SKU ABC123 对应商品名LangChain 实战手册分类技术图书 Thought: 商品存在但库存服务当前不可用无法返回具体数量。 Final Answer: SKU ABC123 的商品存在但库存服务当前不可用无法返回库存数量。 Finished chain.打印的具体文案会因模型和版本不同而变化但关键结构一致第一轮工具失败后Agent 没有直接终止而是把错误作为 Observation 再次送给模型模型继续推理并决定换一个工具。4.3 对照组关闭失败开关循环明显缩短为了确认循环不是因为代码写错才多跑几轮可以关闭失败开关再执行一次python main.py正常情况下输出会变短模型调用一次库存工具就给出答案不再查询商品信息。对比两种输出你能直观看到“不可用工具”如何把循环次数从 1 轮推到 2 轮甚至 3 轮。循环不是语言模型自己开启的而是“模型生成文本 - 执行工具 - 反馈回上下文 - 模型再次生成文本”这个结构天然产生的。5. 逐行拆解执行轨迹认识循环的边界5.1 第一个 Thought模型在推理不是在命令执行轨迹的第一行Thought: 用户需要查询库存数量我需要先调用库存查询工具。很多初学者会把这行当成“模型在自言自语”其实它是 Agent 循环的第一阶段。大模型是一个文本生成器它需要先“生成”一个计划再“生成”行动指令。如果没有 Thought模型就缺少把用户问题映射到工具选择的中间步骤。站在工程角度看Thought 文本是很有价值的调试信息。如果 Agent 始终不调用你希望的工具第一件事不是改 Agent 代码而是看它的 Thought 是否提到了你的工具描述。如果模型根本没理解工具用途那就要调整工具的 description。5.2 工具报错后的 Observation反馈不只是正常结果轨迹里最醒目的一段是Error: ConnectionError: inventory service unavailable, skuABC123这段内容之所以能触发模型重新推理是因为 LangChain 把工具执行结果原样放回了agent_scratchpad。模型能看到“我刚才调用了inventory_query它的结果是 ConnectionError”。于是第二轮推理里出现了“库存接口报错了可以先查商品信息”的判断。这正是把“推理-行动-反馈”称为循环系统的原因第一次反馈是库存服务不可用。模型把反馈当成新的上下文继续推理。第二次行动是查询商品信息。第二次反馈是商品存在。模型判断已经拿到足够信息输出 Final Answer。如果第一次反馈是正常库存数字循环会在第二轮直接结束。如果第一次反馈是一堆乱码模型大概率会再尝试调用一次同一个工具或者干脆告诉用户“输入有误”。反馈文本的质量直接决定循环的质量。5.3 Final Answer循环的退出条件模型输出 Final Answer 后AgentExecutor 会停止继续调用 LLM 和工具把该字段作为最终结果返回。循环不是永远转下去它有两个常见退出条件模型认为已经可以回答用户问题主动输出 Final Answer。循环次数达到max_iterationsAgentExecutor 强制终止。max_iterations是一个非常实用的参数。实际项目中如果设置过小复杂任务会在还没拿到关键信息时就被截断如果设置过大模型在失败反馈中陷入反复尝试时会产生大量 token 消耗。建议先设一个较小值观察行为再根据任务复杂度逐步加大。注意max_iterations限制的是 Agent 循环的完整轮数不是 LLM 调用次数。每轮推理都会调用 LLM一轮工具执行也算一次循环。调试前最好先看官方文档确认版本对这组参数的解释。5.4 工具成功与失败时循环的差异用一个简单表格总结上面的观察场景Thought 轮数工具调用Observation最终输出工具可用1inventory_query库存数量 128直接给出库存工具不可用至少 2inventory_query 后换 product_lookup错误后商品信息说明库存服务不可用工具反复失败且无其他工具多次同一工具反复调用持续错误被 max_iterations 截断或最终输出错误说明第三个场景非常容易测试上面的示例里如果把product_lookup也改成失败模型就会在同样的工具上来回尝试。这就是那些“Agent 卡住了”现象的本质不是模型卡住了而是循环没有外部手段让它收敛。6. 常见问题排查工具不被调用、异常被吞掉、重复调用6.1 工具没有被调用现象Agent 最终直接回答“我是一个语言模型无法查询库存”但控制台完全没有出现 Action 输出。可能原因工具的 description 不清晰模型不知道什么情况该调用它。提示词模板里没有正确渲染{tools}Agent 根本看不到工具。模型没有经过指令微调不擅长输出 ReAct 格式。检查方式print(prompt.format(inputSKU ABC123 目前还有多少库存, toolstools, tool_names, .join([t.name for t in tools]), agent_scratchpad))看控制台输出里是否出现工具名和描述。如果工具名没有出现在 prompt 中问题在提示词模板不是模型能力问题。处理建议加强 description明确“什么时候调用、参数有什么用、返回什么”。换一个能力更强的模型再试。调低temperature避免模型自由发挥格式。6.2 工具抛异常后 Agent 整体崩溃现象工具内部raise ConnectionError后程序直接报错退出而不是继续循环。常见原因某些 LangChain 版本里工具异常需要显式配置handle_tool_error。使用了不兼容的自定义 BaseTool 子类错误没有被统一捕获。检查方式查看堆栈异常是否指向 AgentExecutor 的_execute_tool方法。检查工具是否用tool正常声明没有被 try/except 提前吞掉。处理建议先改成“返回错误字符串”的方式让错误直接成为 Observation这是最可控的路径。如果必须抛异常再研究当前版本的handle_tool_error参数。6.3 同一个失败工具被反复调用直到 max_iterations现象模型反复调用inventory_query每次都得到同样的错误但还是不放弃。原因模型只看到了“工具返回错误”的文本没有看到新的线索所以它可能认为换个参数或者重试一次会成功。处理建议给错误反馈里补充更明确的提示例如“该服务 30 秒内不可重试”。增加一个可用的备份工具让模型有别的路径可走。设置max_iterations3之类的较小上限避免无限消耗 token。问题现象可能原因检查方式处理建议工具没被调用提示词没渲染工具或 description 不清晰打印格式化后的 prompt修模板、改写描述异常导致崩溃工具异常没有被 AgentExecutor 捕获看堆栈是否在工具执行阶段改用返回错误字符串同一工具反复调用反馈文本没有给模型新信息查看 verbose 日志里的 Observation增加更明确的错误提示模型输出格式无法解析ReAct 模板约束不够看 parsing error 日志打开 handle_parsing_errors换更强的模型6.4 新版本 API 变化LangChain 0.3 之后官方推荐使用新的create_agent部分旧代码需要迁移。不要直接照抄网上代码先跑通当前版本的官方示例再替换自己的工具。版本升级时最容易出问题的三类地方BaseTool导入路径变化。handle_parsing_errors行为变化。agent_scratchpad变量是否还需要在 prompt 中声明。7. 生产环境的 Agent 工具设计不能只写一个会返回字符串的函数7.1 让工具失败信息变成结构化反馈把工具变成不可用是最容易的调试手段但生产环境需要完全不同的思路。建议工具内部不要直接抛裸异常而是返回结构化错误tool def inventory_query(sku: str) - str: 查询指定 SKU 的实时库存数量只有业务系统才能返回准确结果。 try: return fSKU {sku} 当前库存数量128 except Exception as exc: return fERROR: codeINVENTORY_SERVICE_ERROR, message{exc}这样 Agent 能在 Observation 里看到错误码外围日志也能把这段文本完整记下来。更重要的是模型不需要去理解堆栈跟踪它可以直接在下一轮推理中决定要不要换工具。7.2 给不稳定工具加重试、限流和降级Agent 背后的工具不一定是稳定的内部接口可能是外部 HTTP API、数据库查询或另一个模型调用。对于真实业务系统至少要考虑重试配置指数退避最多重试 2 到 3 次。限流控制 Agent 循环里的工具调用频率避免把下游系统打挂。降级当主工具失败时返回缓存数据或一个明确错误码。这些策略应该放在工具内部实现而不是放在 Agent 的 prompt 里。因为 prompt 只是文本不能保证模型遵守而工具内部逻辑是确定的能按代码顺序执行。7.3 从 AgentExecutor 走向 LangGraph 的显式循环AgentExecutor 帮我们隐藏了很多循环细节学习和调试都很方便。但它把循环控制都封装在框架内部在复杂多步骤、需要人工中断、需要并行调用工具的场景里控制力不够。LangGraph 是 LangChain 生态里更适合做状态机编排的方案。它可以把 Agent 的 Thought、Action、Observation 建模成节点和边循环路径一目了然也方便加入人工审批、错误恢复、条件分支。理解本文的循环机制后再去看 LangGraph 会轻松很多因为核心概念仍然是“状态在节点之间流转每个节点根据反馈决定下一步”。7.4 可复用的 Agent 工具发布检查清单发布一个自定义工具进生产环境前按这个清单逐项检查description 是否说明了触发场景、参数含义和返回结构。工具内部是否捕获了所有第三方异常并返回可读错误码。是否设置了超时时间避免 Agent 一直等待一个不响应的服务。是否配置了max_iterations防止循环失控。是否开启verboseFalse后仍然有关键日志输出到日志系统。Agent 执行时间是否可控是否记录了每轮 LLM 调用 token 数。是否有对应回滚方案比如把 Agent 切换回固定工作流。这个清单可以贴在项目文档里每次新增工具时逐项勾选。很多 Agent 问题在工具上线阶段就能被发现而不是等到模型在线上反复调用后才暴露。工具失败并不可怕可怕的是无法从反馈文本中看出失败原因。本文用“自造不可用工具”演示的底层逻辑最终要落地成一句话Agent 是一个循环系统Thought 决定方向Action 执行动作Observation 提供反馈作为开发者你的核心职责不是帮模型思考而是设计好反馈内容让它每一轮循环都能得到足够的信息。下一步建议先把自己的业务接口做成工具再故意注入一个异常用同样的方法观察循环这套经验会很快变成你的 Agent 调试直觉。