前言 上一篇的 interrupt() 是动态 人机协同:停在哪儿由代码运行时决定。 还有一类静态断点 :编译图时声明「进入某节点前 / 离开某节点后」一律暂停,适合调试或固定审批闸门。 工具侧则不必每次手写 tool_calls 循环;ToolNode 可直接当节点,并用 wrap_tool_call 做重试或缓存。 本文覆盖 interrupt_before / interrupt_after、手写工具节点与 ToolNode 对照,以及包装器要点。 示例对接 火山方舟 Coding Plan ,模型用 ark-code-latest。 下文需要 Python 3.12+ ,依赖用 uv 管理。
依赖 建议使用 Python 3.12 及以上。
1 2 3 4 uv init langgraph-static-tools cd langgraph-static-toolsuv venv --python 3.12 uv add "langgraph>=1.0,<2.0" "langchain>=1.0,<2.0" langchain-openai python-dotenv rich
在项目根目录配置 Coding Plan 的 .env(字段与上一篇相同)。 静态断点同样需要 checkpointer 与稳定的 thread_id。
静态断点 在 compile 时传入节点名列表。 首次 invoke 会在第一个断点停下;之后用 invoke(None, config=...) 推进到下一闸门,直到跑完。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from typing import TypedDictfrom langgraph.checkpoint.memory import InMemorySaverfrom langgraph.graph import END, START, StateGraphfrom rich import print as rprintclass OverAllState (TypedDict ): final_res: str def node_a (state: OverAllState ) -> OverAllState: return {"final_res" : "node_a" } def node_b (state: OverAllState ) -> OverAllState: return {"final_res" : "node_b" } def node_c (state: OverAllState ) -> OverAllState: return {"final_res" : "node_c" } builder = StateGraph(state_schema=OverAllState) builder.add_node("node_a" , node_a) builder.add_node("node_b" , node_b) builder.add_node("node_c" , node_c) builder.add_edge(START, "node_a" ) builder.add_edge("node_a" , "node_b" ) builder.add_edge("node_b" , "node_c" ) builder.add_edge("node_c" , END) graph = builder.compile ( checkpointer=InMemorySaver(), interrupt_before=["node_a" , "node_b" ], interrupt_after=["node_a" , "node_b" ], ) config = {"configurable" : {"thread_id" : "static-1" }} rprint(graph.invoke({}, config=config)) rprint(graph.invoke(None , config=config)) rprint(graph.invoke(None , config=config)) rprint(graph.invoke(None , config=config))
也可在单次 invoke(..., interrupt_before=..., interrupt_after=...) 里临时指定断点,不必写进 compile。 注意:临时列表与编译期配置混用时,某次调用若省略断点参数,后续行为可能与预期不一致 ;联调时固定一种写法更稳妥。 并行图上声明多个静态断点时,各分支会各自卡在闸门,恢复顺序仍按 invoke(None) 逐步推进。
手写工具循环 经典 Agent 环:LLM 节点 → 条件路由 → 手写 tool_node 解析 tool_calls → 再回 LLM。 下面用 Coding Plan 绑定两个本地工具。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import osfrom typing import Literal from dotenv import load_dotenvfrom langchain.chat_models import init_chat_modelfrom langchain.messages import HumanMessage, ToolMessagefrom langchain.tools import toolfrom langgraph.graph import END, START, MessagesState, StateGraphfrom rich import print as rprintload_dotenv() @tool(parse_docstring=True ) def get_weather (city: str ) -> str : """查询指定城市的当日天气。 Args: city: 城市名称 """ return f"{city} 天气晴朗,微风" @tool(parse_docstring=True ) def get_news (home_or_abroad: bool ) -> str : """查询国内外新闻。 Args: home_or_abroad: True 国内,False 国外 """ return "国内要闻摘要" if home_or_abroad else "国际要闻摘要" tools = [get_weather, get_news] tools_by_name = {t.name: t for t in tools} model = init_chat_model( "openai:ark-code-latest" , temperature=0 , api_key=os.environ["OPENAI_API_KEY" ], base_url=os.environ["OPENAI_BASE_URL" ], ).bind_tools(tools) def llm_node (state: MessagesState ) -> MessagesState: return {"messages" : [model.invoke(state["messages" ])]} def tool_node (state: MessagesState ) -> MessagesState: last = state["messages" ][-1 ] outs: list [ToolMessage] = [] for call in last.tool_calls: result = tools_by_name[call["name" ]].invoke(call["args" ]) outs.append( ToolMessage(name=call["name" ], content=result, tool_call_id=call["id" ]) ) return {"messages" : outs} def router (state: MessagesState ) -> Literal ["tool_node" , "__end__" ]: if state["messages" ][-1 ].tool_calls: return "tool_node" return END builder = StateGraph(state_schema=MessagesState) builder.add_node("llm_node" , llm_node) builder.add_node("tool_node" , tool_node) builder.add_edge(START, "llm_node" ) builder.add_conditional_edges("llm_node" , router, path_map=["tool_node" , END]) builder.add_edge("tool_node" , "llm_node" ) graph = builder.compile () res = graph.invoke( {"messages" : [HumanMessage("今天北京天气怎么样?国内有什么新闻?" )]} ) for msg in res["messages" ]: msg.pretty_print()
手写循环灵活,但要自己处理并行 tool_calls、异常与消息字段,容易漏。
把上面的 tool_node 换成预构建 ToolNode(tools=...),图结构不变。ToolNode 会批量执行调用并生成 ToolMessage。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import osfrom typing import Literal from dotenv import load_dotenvfrom langchain.chat_models import init_chat_modelfrom langchain.messages import HumanMessagefrom langchain.tools import toolfrom langgraph.graph import END, START, MessagesState, StateGraphfrom langgraph.prebuilt import ToolNodeload_dotenv() @tool(parse_docstring=True ) def get_weather (city: str ) -> str : """查询指定城市的当日天气。 Args: city: 城市名称 """ return f"{city} 天气晴朗,微风" tools = [get_weather] model = init_chat_model( "openai:ark-code-latest" , temperature=0 , api_key=os.environ["OPENAI_API_KEY" ], base_url=os.environ["OPENAI_BASE_URL" ], ).bind_tools(tools) def llm_node (state: MessagesState ) -> MessagesState: return {"messages" : [model.invoke(state["messages" ])]} def router (state: MessagesState ) -> Literal ["tool_node" , "__end__" ]: return "tool_node" if state["messages" ][-1 ].tool_calls else END builder = StateGraph(state_schema=MessagesState) builder.add_node("llm_node" , llm_node) builder.add_node("tool_node" , ToolNode(tools=tools)) builder.add_edge(START, "llm_node" ) builder.add_conditional_edges("llm_node" , router, path_map=["tool_node" , END]) builder.add_edge("tool_node" , "llm_node" ) graph = builder.compile () graph.invoke({"messages" : [HumanMessage("北京今天天气?" )]})
工具函数还可注入 ToolRuntime,返回 Command(update=...) 同时改自定义状态字段与 messages,适合把天气结果写到独立键里。
ToolNode(..., wrap_tool_call=fn) 可在真正 execute(request) 前后插入逻辑。 常见用途:有限次重试、统一打日志、按参数做结果缓存。
下面演示失败重试:工具随机抛 ConnectionError,包装器最多尝试 context.max_attempts 次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import osimport randomfrom dataclasses import dataclassfrom typing import Literal from dotenv import load_dotenvfrom langchain.chat_models import init_chat_modelfrom langchain.messages import HumanMessage, ToolMessagefrom langchain.tools import toolfrom langgraph.graph import END, START, MessagesState, StateGraphfrom langgraph.prebuilt import ToolNodeload_dotenv() @tool(parse_docstring=True ) def get_weather (city: str ) -> str : """查询指定城市的当日天气。 Args: city: 城市名称 """ if random.randint(1 , 10 ) < 8 : raise ConnectionError("网络波动" ) return f"{city} 天气晴朗,微风" tools = [get_weather] model = init_chat_model( "openai:ark-code-latest" , temperature=0 , api_key=os.environ["OPENAI_API_KEY" ], base_url=os.environ["OPENAI_BASE_URL" ], ).bind_tools(tools) @dataclass class UserContext : max_attempts: int def llm_node (state: MessagesState ) -> MessagesState: return {"messages" : [model.invoke(state["messages" ])]} def router (state: MessagesState ) -> Literal ["tool_node" , "__end__" ]: return "tool_node" if state["messages" ][-1 ].tool_calls else END def wrap_tool_call (request, execute ): max_attempts = request.runtime.context.max_attempts tool_call_id = request.runtime.tool_call_id tool_msg = None for _ in range (max_attempts): try : tool_msg = execute(request) break except ConnectionError: continue if tool_msg is None : tool_msg = ToolMessage( tool_call_id=tool_call_id, content="调用次数达到上限,调用失败" , ) return tool_msg builder = StateGraph(state_schema=MessagesState, context_schema=UserContext) builder.add_node("llm_node" , llm_node) builder.add_node("tool_node" , ToolNode(tools=tools, wrap_tool_call=wrap_tool_call)) builder.add_edge(START, "llm_node" ) builder.add_conditional_edges("llm_node" , router, path_map=["tool_node" , END]) builder.add_edge("tool_node" , "llm_node" ) graph = builder.compile () graph.invoke( {"messages" : [HumanMessage("今天北京天气怎么样?" )]}, context=UserContext(max_attempts=3 ), )
缓存版思路相同:用 (tool_name, json.dumps(args)) 做键,命中则直接构造 ToolMessage,未命中再 execute(request) 并写入进程内 dict。 注意缓存只适合幂等、短时效 工具;跨进程要换 Redis 等外部存储。
总结
静态断点:interrupt_before / interrupt_after + checkpointer;恢复用 invoke(None, config=...)。
动态 interrupt() 适合业务审批;静态断点适合固定闸门与调试。
优先用 ToolNode 替代手写工具循环,除非你要完全自定义消息形状。
wrap_tool_call 适合重试、观测与简单工具缓存,勿缓存有副作用的写操作。