ToolNode 并发超限
LangGraph 设置 max_concurrency=1,ToolNode 异步工具仍同时执行。核对节点内部 gather,用共享 awrap_tool_call gate 约束实际操作。
给 LangGraph 传了 config={"max_concurrency": 1},同一 ToolNode 中的两个异步工具却同时开始访问外部服务。先确认使用的是自建 StateGraph 里的直接多工具调用路径,不能只看这份配置就认定异步工具已经被串行化。
已有反馈使用 LangGraph 1.2.10、langgraph-prebuilt 1.1.0:同步调用的同时活跃工具数为一,异步调用却为二。已核对的 1.2.15 对应 ToolNode 异步实现仍会直接收集并等待全部调用,不能把主包版本增大当作这一限制已自动生效。
图任务上限不等于节点内部工具上限
一个 ToolNode 可以从一条 AIMessage 中取出多个 tool_calls。同步路径使用配置的 executor,异步路径则把这些工具协程交给 asyncio.gather。
因此,图调度器限制同时运行的节点,与这个节点内部一次派发多少工具,是两层约束。通过 create_agent 的 Send 派发路径表现正常,也不能证明自建图中直接执行多调用 ToolNode 的路径相同。
记录实际的 LangGraph、langgraph-prebuilt 与 langchain-core 版本、节点构造方式,以及一次进入节点的调用数。观察需要的是实际同时活跃的外部操作数,工具列表顺序或结果列表顺序都不能证明没有并发。
在异步执行边界共享一份 gate
确实需要限制这条路径时,可在支持 awrap_tool_call 的 ToolNode 上包住实际工具执行。下面的工厂接收项目已有的工具列表,创建一个同时只允许一项执行的节点:
import asyncio
from langgraph.prebuilt import ToolNode
def limited_tool_node(tools):
gate = asyncio.Semaphore(1)
async def limit_tool(request, execute):
async with gate:
return await execute(request)
return ToolNode(tools, awrap_tool_call=limit_tool)
在原图构造时用返回的节点替代需要受限的 ToolNode,保留已有工具定义、认证和运行配置。这是异步执行包装示例,不会自动执行工具;参数 tools 必须来自项目原来获准使用的工具列表。
同一个节点中的调用共享同一份 gate。若在每次回调内部都新建 Semaphore(1),每个调用都有自己的许可,就无法限制彼此。已有 wrapper、错误处理或自定义 messages key 的节点,需要把这些设置正确组合,不能用示例覆盖掉原有逻辑。
async with 会在退出时释放许可,返回值沿原执行路径保留,包括 ToolMessage 或 Command。不要把包装器改成重新拼一份结果,也不要吞掉取消或业务错误,随后让模型以为工具成功。
许可数量与速率限制不同
gate 限制的是同一份包装器保护的、正在等待完成的异步执行。它不是每秒请求配额,也不自动跨进程、跨事件循环或跨其他 ToolNode 共享。服务若有全局配额,需要按实际部署设计共享协调,不能让每个 worker 各限一个就称为全站只执行一个。
工具内部若另起后台任务、不等待实际外部操作结束就返回,许可会提前释放。应把需要保护的真实资源使用放在受限生命周期内,不能只包住一个立即返回的派发函数。
把上限设为一,也不建立任意复杂图的业务依赖顺序。需要先后依赖的操作,应由图或业务流程明确表达,不能只依靠谁先拿到 semaphore。
在获准的测试工具中观察同时活跃数、正常结果、异常和取消后的许可释放,再检查同步调用与其他节点。不要只看配置仍写着 max_concurrency: 1,也不要用已经可能产生副作用的业务工具反复调用来估计并发。


