拆解 Tool Calling Agent:从裸写闭环到 LangGraph 安全状态机实现

拆解 Tool Calling Agent:从裸写闭环到 LangGraph 安全状态机实现

很多刚接触 Agent 开发的同学常有一个直觉误区:以为把函数"注册"给大模型后,模型就能自己运行代码并拿回结果。实际上,当前主流的大语言模型(无论 GPT-4o、Claude 还是开源的 Qwen、DeepSeek)本质上仍是一个纯文本概率生成器,它根本没有操作系统级的执行上下文。

模型不执行任何代码。它只负责在理解上下文后,生成一段符合约定的结构化调用意图(Function Calling / Tool Calls);真正搬砖跑 Python 代码、发 HTTP 请求、查数据库的,始终是我们的后端进程。

理解这一点,是写出生产可用 Agent 的分水岭。今天我从零手写一个工具调用闭环讲起,顺藤摸瓜拆解 LangGraph 的 ToolNode 与状态流转,最后落地一套包含白名单、异常兜底与可观测性的安全工具节点。


工具调用的三要素与底层消息契约

在 LangChain 体系下,我们通常用 @tool 装饰器定义一个工具:

python 复制代码
from langchain_core.tools import tool

@tool
def query_order(order_id: str) -> str:
    """根据订单号查询订单状态。
    当用户询问订单状态、物流进度或发货情况时调用此工具。
    """
    orders = {
        "1001": {"status": "已发货", "logistics": "顺丰速运"},
        "1002": {"status": "未发货", "logistics": None},
    }
    order = orders.get(order_id)
    if not order:
        return f"工具执行失败:订单 {order_id} 不存在,请核对订单号。"
    
    return f"订单 {order_id} 状态:{order['status']},物流:{order['logistics'] or '暂无'}。"

定义一个生产级工具时,三要素缺一不可:

  1. Name:函数的命名,语义必须清晰。
  2. Description(Docstring):模型判断"要不要调、何时调"的唯一先验知识。Prompt 中如果工具职责边界模糊,模型就会出现"幻觉乱调"或"视而不见"。
  3. Args Schema:参数类型与命名(基于 Pydantic / Type Hints),决定了模型输出 JSON 时的 Key 及格式。

当我们将工具绑定到模型:

python 复制代码
llm_with_tools = llm.bind_tools([query_order])

这步操作仅仅是在向模型发起 API 请求时,把工具的 JSON Schema 注入到了请求体中。

关键响应:tool_calls

模型如果判定需要调用工具,其返回的 AIMessage.content 往往为空(或仅有一句引言),核心载荷在 AIMessage.tool_calls 中:

json 复制代码
[
  {
    "name": "query_order",
    "args": {"order_id": "1001"},
    "id": "call_981ad07b_8e12"
  }
]

这里的 idtool_call_id)是模型调用的全局唯一追踪凭证。在模型并行发起多次工具调用时,后端必须凭借此 ID 把每个函数的执行结果精准对齐。


裸写一个工具调用闭环

在不依赖任何 Agent 框架的高级抽象(如 create_react_agent)之前,一个纯 Python 的工具调用循环长成这样:
本地工具函数 大语言模型 业务运行时 (Python) 本地工具函数 大语言模型 业务运行时 (Python) #mermaid-svg-1dhYG8c3uB7Vq6yB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1dhYG8c3uB7Vq6yB .error-icon{fill:#552222;}#mermaid-svg-1dhYG8c3uB7Vq6yB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1dhYG8c3uB7Vq6yB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1dhYG8c3uB7Vq6yB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1dhYG8c3uB7Vq6yB .marker.cross{stroke:#333333;}#mermaid-svg-1dhYG8c3uB7Vq6yB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1dhYG8c3uB7Vq6yB p{margin:0;}#mermaid-svg-1dhYG8c3uB7Vq6yB .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1dhYG8c3uB7Vq6yB text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1dhYG8c3uB7Vq6yB .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1dhYG8c3uB7Vq6yB .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1dhYG8c3uB7Vq6yB #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1dhYG8c3uB7Vq6yB .sequenceNumber{fill:white;}#mermaid-svg-1dhYG8c3uB7Vq6yB #sequencenumber{fill:#333;}#mermaid-svg-1dhYG8c3uB7Vq6yB #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1dhYG8c3uB7Vq6yB .messageText{fill:#333;stroke:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1dhYG8c3uB7Vq6yB .labelText,#mermaid-svg-1dhYG8c3uB7Vq6yB .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .loopText,#mermaid-svg-1dhYG8c3uB7Vq6yB .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1dhYG8c3uB7Vq6yB .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1dhYG8c3uB7Vq6yB .noteText,#mermaid-svg-1dhYG8c3uB7Vq6yB .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1dhYG8c3uB7Vq6yB .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1dhYG8c3uB7Vq6yB .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1dhYG8c3uB7Vq6yB .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1dhYG8c3uB7Vq6yB .actorPopupMenu{position:absolute;}#mermaid-svg-1dhYG8c3uB7Vq6yB .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1dhYG8c3uB7Vq6yB .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1dhYG8c3uB7Vq6yB .actor-man circle,#mermaid-svg-1dhYG8c3uB7Vq6yB line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1dhYG8c3uB7Vq6yB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 "查一下订单 1001"1发送 HumanMessage2返回 AIMessage (带 tool_calls 与 id)3解析 args 并执行 query_order("1001")4返回结果字符串5追加 ToolMessage (带 tool_call_id)6返回 AIMessage (最终总结文本,无 tool_calls)7返回最终回答8 用户

用代码还原这个状态转移过程:

python 复制代码
from typing import List
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage

def run_native_agent(question: str, tools: list, max_steps: int = 5) -> str:
    tools_by_name = {t.name: t for t in tools}
    llm_with_tools = llm.bind_tools(tools)
    
    # 显式维护的消息堆栈
    messages: List[BaseMessage] = [HumanMessage(content=question)]

    for step in range(max_steps):
        ai_message = llm_with_tools.invoke(messages)
        messages.append(ai_message)

        # 终止条件:模型认为信息已充足,不再产出工具调用
        if not ai_message.tool_calls:
            return str(ai_message.content)

        # 遍历执行所有调用的工具(支持模型单轮并行调用多个工具)
        for tool_call in ai_message.tool_calls:
            tool_name = tool_call["name"]
            tool_args = tool_call["args"]
            tool_call_id = tool_call["id"]

            selected_tool = tools_by_name.get(tool_name)
            if not selected_tool:
                tool_output = f"Error: Tool {tool_name} not found."
            else:
                tool_output = selected_tool.invoke(tool_args)

            # 核心:必须使用 ToolMessage 且附带对应的 tool_call_id 进行回传
            messages.append(
                ToolMessage(
                    content=str(tool_output),
                    tool_call_id=tool_call_id
                )
            )

    return "任务超出最大迭代步数,已中断。"

这段代码揭示了 Agent 的底层通信协议:

  1. HumanMessage 进入。
  2. 产出包含 tool_callsAIMessage
  3. 执行器产出带 tool_call_idToolMessage 并入栈。
  4. 再次携带全量上下文调用模型,直至模型输出终态文本。

使用 LangGraph 进行拓扑图重构

裸写循环在业务简单时很直接,但一旦引入条件路由、多 Agent 协作、状态持久化或人工介入(Human-in-the-loop),纯循环控制流会迅速恶化为不可维护的"面条代码"。

LangGraph 将上述过程抽象为有向状态图:
#mermaid-svg-8nhggp6XmdVRUFfL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8nhggp6XmdVRUFfL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8nhggp6XmdVRUFfL .error-icon{fill:#552222;}#mermaid-svg-8nhggp6XmdVRUFfL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8nhggp6XmdVRUFfL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8nhggp6XmdVRUFfL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8nhggp6XmdVRUFfL .marker.cross{stroke:#333333;}#mermaid-svg-8nhggp6XmdVRUFfL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8nhggp6XmdVRUFfL p{margin:0;}#mermaid-svg-8nhggp6XmdVRUFfL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8nhggp6XmdVRUFfL .cluster-label text{fill:#333;}#mermaid-svg-8nhggp6XmdVRUFfL .cluster-label span{color:#333;}#mermaid-svg-8nhggp6XmdVRUFfL .cluster-label span p{background-color:transparent;}#mermaid-svg-8nhggp6XmdVRUFfL .label text,#mermaid-svg-8nhggp6XmdVRUFfL span{fill:#333;color:#333;}#mermaid-svg-8nhggp6XmdVRUFfL .node rect,#mermaid-svg-8nhggp6XmdVRUFfL .node circle,#mermaid-svg-8nhggp6XmdVRUFfL .node ellipse,#mermaid-svg-8nhggp6XmdVRUFfL .node polygon,#mermaid-svg-8nhggp6XmdVRUFfL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8nhggp6XmdVRUFfL .rough-node .label text,#mermaid-svg-8nhggp6XmdVRUFfL .node .label text,#mermaid-svg-8nhggp6XmdVRUFfL .image-shape .label,#mermaid-svg-8nhggp6XmdVRUFfL .icon-shape .label{text-anchor:middle;}#mermaid-svg-8nhggp6XmdVRUFfL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8nhggp6XmdVRUFfL .rough-node .label,#mermaid-svg-8nhggp6XmdVRUFfL .node .label,#mermaid-svg-8nhggp6XmdVRUFfL .image-shape .label,#mermaid-svg-8nhggp6XmdVRUFfL .icon-shape .label{text-align:center;}#mermaid-svg-8nhggp6XmdVRUFfL .node.clickable{cursor:pointer;}#mermaid-svg-8nhggp6XmdVRUFfL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8nhggp6XmdVRUFfL .arrowheadPath{fill:#333333;}#mermaid-svg-8nhggp6XmdVRUFfL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8nhggp6XmdVRUFfL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8nhggp6XmdVRUFfL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8nhggp6XmdVRUFfL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8nhggp6XmdVRUFfL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8nhggp6XmdVRUFfL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8nhggp6XmdVRUFfL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8nhggp6XmdVRUFfL .cluster text{fill:#333;}#mermaid-svg-8nhggp6XmdVRUFfL .cluster span{color:#333;}#mermaid-svg-8nhggp6XmdVRUFfL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8nhggp6XmdVRUFfL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8nhggp6XmdVRUFfL rect.text{fill:none;stroke-width:0;}#mermaid-svg-8nhggp6XmdVRUFfL .icon-shape,#mermaid-svg-8nhggp6XmdVRUFfL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8nhggp6XmdVRUFfL .icon-shape p,#mermaid-svg-8nhggp6XmdVRUFfL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8nhggp6XmdVRUFfL .icon-shape .label rect,#mermaid-svg-8nhggp6XmdVRUFfL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8nhggp6XmdVRUFfL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8nhggp6XmdVRUFfL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8nhggp6XmdVRUFfL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有 tool_calls
无 tool_calls
START
agent 节点
should_continue?
tools 节点
END

1. 状态定义与 Reducer 语义

在 LangGraph 中,状态由 TypedDict 定义。消息列表绝不能被后续节点的返回值直接覆盖,必须使用 add_messages 进行追加合并:

python 复制代码
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage

class AgentState(TypedDict):
    # add_messages 告诉图:节点返回的 messages 切片要 append 到现有列表中,而不是 overwrite
    messages: Annotated[list[BaseMessage], add_messages]

2. 核心节点与条件边

python 复制代码
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode

# 1. Agent 节点:负责接收当前状态,调用 LLM,返回新的 AIMessage
def agent_node(state: AgentState):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

# 2. 条件路由边:负责判断是转向工具节点还是终结
def should_continue(state: AgentState) -> str:
    last_message = state["messages"][-1]
    if getattr(last_message, "tool_calls", None):
        return "tools"
    return END

# 3. 构建拓扑图
workflow = StateGraph(AgentState)

workflow.add_node("agent", agent_node)
workflow.add_node("tools", ToolNode(tools)) # 官方预置的 ToolNode

workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tools": "tools",
        END: END
    }
)
# 闭环:工具执行完毕后,控制权必须交还给 agent 节点继续推理
workflow.add_edge("tools", "agent")

app = workflow.compile()

走出实验室:构建工业级 SafeToolNode

官方提供的 ToolNode 虽好,但在真实生产环境直接使用仍存在隐患:

  • 模型幻觉风险:调用未经授权的函数。
  • 异常蔓延风险:工具抛出未捕获异常(如 DB 超时、网络波动),直接导致整个 Agent 链路 Crash。
  • 黑盒排障成本:缺乏调用延迟、参数详情与执行状态的结构化观测。

因此,我们需要手写一个 SafeToolNode 来接管工具的执行。

1. 错误契约设计:失败不要抛异常

如果工具内部报错直接 raise Exception,整个图执行就会中断退栈。正确的做法是将错误转化为字符串返回,给模型自我纠错或调整策略的机会。

  • 错误做法:raise ValueError("订单不存在") →\rightarrow→ 导致服务 500。
  • 正确做法:return "工具执行失败:未查询到该订单,请检查订单编号后重试。" →\rightarrow→ 模型收到后,可能会礼貌地回复用户:"我查询了库中未找到订单 1001,请问您的单号是否有误?"

2. 实现具备白名单与耗时监控的节点

python 复制代码
import time
from typing import Set
from langchain_core.messages import ToolMessage

ALLOWED_TOOLS: Set[str] = {"query_order", "create_ticket", "get_weather"}

def execute_tool_safely(tool_call: dict, tools_map: dict) -> ToolMessage:
    tool_name = tool_call.get("name", "")
    tool_args = tool_call.get("args", {})
    tool_call_id = tool_call.get("id", "")

    start_time = time.perf_counter()
    success = False
    content = ""

    try:
        # 安全防御:白名单拦截
        if tool_name not in ALLOWED_TOOLS:
            content = f"SecurityError: 工具 [{tool_name}] 不在允许调用的白名单内。"
        elif tool_name not in tools_map:
            content = f"LookupError: 运行时未找到工具 [{tool_name}] 的注册实例。"
        else:
            # 正常调用
            raw_result = tools_map[tool_name].invoke(tool_args)
            content = str(raw_result)
            success = not content.startswith("工具执行失败")
    except Exception as e:
        # 运行时兜底,防止崩溃
        content = f"RuntimeError: 执行工具 [{tool_name}] 时发生异常: {str(e)}"
        success = False
    finally:
        elapsed = time.perf_counter() - start_time
        # 生产环境中替换为结构化 loguru 或接入 APM 追踪
        print(f"[TOOL TRACE] ID: {tool_call_id} | Name: {tool_name} | "
              f"Success: {success} | Latency: {elapsed:.4f}s")

    return ToolMessage(
        content=content,
        tool_call_id=tool_call_id
    )

def safe_tool_node(state: AgentState):
    last_message = state["messages"][-1]
    tools_map = {t.name: t for t in tools}
    
    # 提取所有工具调用并并发/顺序执行
    tool_messages = [
        execute_tool_safely(tc, tools_map) 
        for tc in getattr(last_message, "tool_calls", [])
    ]
    
    return {"messages": tool_messages}

最后在建图时,将原有的预置节点替换为安全节点:

python 复制代码
workflow.add_node("tools", safe_tool_node)

防御性设计:死循环熔断机制

由于 Agent 状态图本质是一个循环有向图(Cyclic Graph),一旦模型遇到理解不了的工具输出,或者 Prompt 中的引导不够明确,模型可能陷入无限自言自语调工具的死锁:

agent -> tools -> agent -> tools -> ...

这不仅会快速消耗掉 API 配额,还会引发调用超时。在 LangGraph 中,调用时必须传入执行轮数上限(Recursion Limit):

python 复制代码
response = app.invoke(
    {
        "messages": [HumanMessage(content="帮我查一下订单 1001 的物流状态")]
    },
    config={
        # 设置整个图流转的最大步数阈值(节点跳转次数)
        "recursion_limit": 12 
    }
)

一旦内部跳转次数超过 recursion_limit,框架会直接抛出 GraphRecursionError,我们可以在最外层 API 网关处捕获该异常,对用户做降级或转人工处理。


思考与实践总结

从工程视角来看,Tool Calling Agent 的控制面与数据面界限非常清晰:

  1. 大模型负责决策(Control Plane):基于静态上下文和动态执行历史,推演下一跳应该走向终点还是走向外部执行。
  2. 状态图负责执行(Data Plane) :LangGraph 借助 Annotated[list, add_messages] 提供增量内存模型,利用 Conditional Edge 映射控制转移,借助外部代码拦截完成真正的 I/O 操作。

把工具调用当做一次跨系统 RPC 来看待:必须做白名单准入、必须做出入参序列化映射、必须做全局唯一 Trace 跟踪(tool_call_id),并且必须把所有错误吞吐在业务错误模型中,而不是任由异常击穿上下文。

顺着这个思路再往前走一步:如果某个工具的执行需要消耗极长的时间(例如跑一次大数据离线分析),或者该工具涉及高风险操作(例如转账、删库),我们该如何在 LangGraph 现有的图拓扑中挂起当前执行线程,等待外部信号唤醒?这将是状态持久化(Checkpointer)与人工确认(Human-in-the-loop)要解决的问题。

相关推荐
科技每日热闻1 小时前
初创企业从 MVP 迭代到规模化运营,什么样的云平台能够兼顾技术能力与成本管控?
ai
RobinDevNotes1 小时前
Palmier Pro:AI时代的Mac视频编辑器
人工智能·ceph·macos·ai·音视频·视频编辑·mcp
粉色大象1 小时前
handdrawn‑architecture‑video:开源SVG架构图转手绘4K动画视频|本地AI Agent Skill
人工智能·ai·ai作画·系统架构·开源·aigc·音视频
七夜zippoe3 小时前
Function Calling 深度解析:从参数定义到错误处理的完整实践
人工智能·ai·agent·function·calling
飞塔老梅子10 小时前
09. Codex 节省Token ❀ 老梅子学AI
人工智能·ai·token·模型·codex
前沿在线10 小时前
启元机器人亮相外滩大会,探索个人机器人健康服务新场景
人工智能·ai·大模型
Summer-Bright10 小时前
深度 | OpenAI 联合三星造芯:从模型层向底层算力延伸,AI 算力供应链开始重新洗牌
人工智能·ai·openai·芯片
slacker-kian12 小时前
[实践]-本地大模型 + MCP + Agent 对接 SAP OData API 实现Web Chart APP
ai·llm·sap·agent·mcp·odata·chart bot
龙骑士baby13 小时前
重建 AI 认知第 6 篇:RAG——答案不在"检索 + 生成"这四个字里
ai·llm·rag