惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
SecWiki News
SecWiki News
Martin Fowler
Martin Fowler
T
Tor Project blog
N
Netflix TechBlog - Medium
C
Cybersecurity and Infrastructure Security Agency CISA
V
Vulnerabilities – Threatpost
V
Visual Studio Blog
GbyAI
GbyAI
PCI Perspectives
PCI Perspectives
D
DataBreaches.Net
Jina AI
Jina AI
H
Heimdal Security Blog
云风的 BLOG
云风的 BLOG
P
Privacy International News Feed
A
About on SuperTechFans
J
Java Code Geeks
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
News | PayPal Newsroom
有赞技术团队
有赞技术团队
MyScale Blog
MyScale Blog
博客园 - 司徒正美
C
Check Point Blog
T
Threat Research - Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
宝玉的分享
宝玉的分享
AI
AI
Simon Willison's Weblog
Simon Willison's Weblog
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
The Last Watchdog
The Last Watchdog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
Cisco Talos Blog
Cisco Talos Blog
W
WeLiveSecurity
Hacker News: Ask HN
Hacker News: Ask HN
Recent Commits to openclaw:main
Recent Commits to openclaw:main
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
D
Docker
博客园 - Franky
Security Archives - TechRepublic
Security Archives - TechRepublic

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
DeepAgents - Human in the loop
花酒锄作田 · 2026-06-14 · via 博客园_首页

前言

Human in the loop(人机协作)在企业级 Agent 应用中非常重要——AI 在执关键工具时必须经过人类审批,避免误操作影响业务。我之前用 LangGraph 0.3 裸写了一套(旧文),当时需要在 tool 函数里手动调 interrupt(),很啰嗦。如今有了 DeepAgents 和内置的 HumanInTheLoopMiddleware,只需配置一个 interrupt_on 字典,中断逻辑全自动——在执行前暂停图执行,保存状态到 checkpointer,等待人类决策后恢复。

不过官方文档的示例代码比较简单,只演示了基本用法,没有说明如何在真实应用中整合。本文以"允许 AI 执行 shell 命令,但每次执行前需用户确认"为需求,一步步实现完整的人机协作流程。

核心概念

在开始之前,先理清几个关键概念:

概念 说明
interrupt(中断) 当 Agent 准备调用某个被监控的 tool 时,HumanInTheLoopMiddleware 调用 LangGraph 的 interrupt() 暂停图执行,并抛出包含 action_requestsreview_configs 的请求
checkpoint(检查点) 中断时图状态会被持久化。必须配置 checkpointer,否则中断后无法恢复。生产环境建议用 AsyncPostgresSaver,测试用 InMemorySaver
version="v2" LangGraph 1.0 的 v2 模式,ainvoke() 返回 GraphOutput 对象(含 .interrupts 属性),astream()updates 流中会出现 __interrupt__ 事件
Command(resume=) 用户做出决策后,用 Command(resume={"decisions": [...]}) 从断点恢复执行
Decision(决策) 四种类型:approve(批准)、reject(拒绝并反馈)、edit(修改参数后执行)、respond(人类直接回答,跳过 tool 执行)

执行生命周期

用户提问 → Agent 调用 LLM 生成回复
  → LLM 决定调用 tool(如 execute_shell_command)
    → after_model 钩子:检查 tool 是否在 interrupt_on 中
      → 是:构建 HITLRequest → interrupt() → 暂停 ⌛
      → 否:继续执行
  → 人类做出决策(approve / reject / edit / respond)
    → 恢复执行 → 执行/拒绝 tool → LLM 生成最终回复 → 返回

流程逻辑

以 Chainlit 聊天应用为交互载体,消息处理流程如下:

  1. 用户在聊天页面发送消息(如"检查下系统负载")
  2. Agent 调用 LLM 生成回复,LLM 决定调用 execute_shell_command
  3. HumanInTheLoopMiddleware 检测到该 tool 在 interrupt_on 列表中,触发中断
  4. Chainlit 应用检测到中断,向用户展示审批提示
  5. 用户回复 批准 / 拒绝,应用用 Command(resume=) 恢复执行
  6. Agent 根据决策执行或拒绝 tool,最终返回结果给用户

配置中断

首先需要在创建 Agent 时配置 HumanInTheLoopMiddleware

from deepagents import create_deep_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware

agent = create_deep_agent(
    model=llm,
    tools=[execute_shell_command],
    checkpointer=checkpointer,  # 必须配置!
    system_prompt="你是一位智能助手...",
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                # 对 execute_shell_command 进行审批
                "execute_shell_command": {
                    "allowed_decisions": ["approve", "reject"]
                }
            }
        ),
    ],
)

interrupt_on 是一个字典,key 为 tool 名称,value 的可配置项:

  • True — 允许所有四种决策(approve / edit / reject / respond)
  • False — 不拦截该 tool(等同于不写)
  • {"allowed_decisions": [...]} — 只允许指定决策类型
  • 还可以配置 when 谓词按参数条件判断是否拦截、description 自定义中断提示文本

invoke 模式中的实现

v2 模式下的 ainvoke() 返回 GraphOutput 对象,可通过 .interrupts 属性直接获取中断数据,不需要去查 state。

检测中断

resp = await agent.ainvoke(
    input={"messages": [HumanMessage(content=query)]},
    config=config,
    version="v2",
)

if resp.interrupts:
    # 存在中断,resp.interrupts 是 Interrupt 对象的元组
    interrupt = resp.interrupts[0]
    # interrupt.value 是 HITLRequest,包含 action_requests 和 review_configs
    print(interrupt.value["action_requests"])

恢复中断

用户做出决策后,用 Command(resume=) 恢复:

from langgraph.types import Command

await agent.ainvoke(
    Command(resume={
        "decisions": [{"type": "approve"}]  # 或 {"type": "reject", "message": "..."}
    }),
    config=config,  # 必须用同一个 thread_id
    version="v2",
)

关键:如何分辨"新消息"还是"中断恢复"

在聊天应用中,用户发来的每条消息都走同一个 @cl.on_message 处理函数。用户说"检查负载"和回复"批准"都只是文本。解决方法是——调用前先检查是否有待处理的中断

# 检查当前会话是否有待处理的中断
state = await agent.aget_state(config)
if state.next:
    # 有待处理中断 → 本次消息是审批回复,构建 resume 命令
    cmd = Command(resume={"decisions": [{"type": "approve"}]})
    await agent.ainvoke(cmd, config=config, version="v2")
else:
    # 无中断 → 正常对话
    resp = await agent.ainvoke(
        {"messages": [HumanMessage(content=query)]}, config=config, version="v2"
    )

state.next 不为空表示图执行被暂停了(有中断等待处理)。

stream 模式中的实现

流式模式需要用 stream_mode=["messages", "updates"](官方推荐同时开启两种流):

  • messages 流:获取 LLM 的 token 级输出
  • updates 流:检测中断事件 __interrupt__
async for chunk in agent.astream(
    input=input_data,
    stream_mode=["messages", "updates"],
    version="v2",
    config=config,
):
    if chunk["type"] == "messages":
        msg, _meta = chunk["data"]
        # msg 是 AIMessageChunk,包含 content 和 tool_calls
        if isinstance(msg, AIMessageChunk) and msg.content:
            yield extract_text(msg)     # 流式输出文本
    elif chunk["type"] == "updates":
        if "__interrupt__" in chunk["data"]:
            interrupt = chunk["data"]["__interrupt__"][0]
            yield format_question(interrupt)  # 输出审批问题

stream 模式的恢复与 invoke 类似——在调用 astream() 之前同样要先检查 state.next 来判断是正常对话还是中断恢复。

完整示例

下面是核心代码。checkpointerllm 的配置函数、日志模块等非核心代码省略。

Agent 封装(internal/agent/agent.py 核心部分)

# --- 审批关键词匹配 ---
_APPROVE_KEYWORDS = frozenset(
    {"yes", "accept", "approve", "ok", "是", "允许", "同意", "批准"}
)

def _parse_decision(query: str) -> str:
    return "approve" if query.strip().lower() in _APPROVE_KEYWORDS else "reject"

def _build_resume_command(decision_type: str, actions_count: int) -> Command:
    item = {"type": decision_type}
    if decision_type == "reject":
        item["message"] = "user rejected this action"
    return Command(resume={"decisions": [item for _ in range(actions_count)]})

def _extract_text(message) -> str:
    """从消息中提取纯文本(兼容 str 和 list[dict] 两种 content 格式)。"""
    if not message or not hasattr(message, "content"):
        return ""
    content = message.content
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        return "".join(
            b.get("text", "")
            for b in content
            if isinstance(b, dict) and b.get("type") == "text"
        )
    return ""

def _format_interrupt_question(interrupt) -> str:
    """将中断数据格式化为用户的审批问题。"""
    action_requests = interrupt.value.get("action_requests", [])
    review_configs = interrupt.value.get("review_configs", [])
    allowed = (
        review_configs[0].get("allowed_decisions", ["approve", "reject"])
        if review_configs
        else ["approve", "reject"]
    )

    lines = []
    for req in action_requests:
        lines.append(
            "Do you approve me to execute this action?\n\n"
            f"- name: {req['name']}\n"
            f"- args: `{req['args']}`\n"
        )
    lines.append(f"Input your decision: {', '.join(allowed)}\n")
    return "\n".join(lines)


class AIAgent:
    # ... __init__, _init_deep_agent, _init_tools 省略 ...

    async def _has_pending_interrupt(self, config: RunnableConfig) -> bool:
        state = await self._agent.aget_state(config)
        return bool(state.next)

    # --- invoke 模式 ---
    async def ainvoke(self, query: str, config: RunnableConfig) -> str:
        if not self._agent:
            await self._init_deep_agent()

        # 优先处理中断恢复
        if await self._has_pending_interrupt(config):
            state = await self._agent.aget_state(config)
            actions_count = len(
                state.interrupts[0].value["action_requests"]
            )
            decision = _parse_decision(query)
            cmd = _build_resume_command(decision, actions_count)
            await self._agent.ainvoke(cmd, config=config, version="v2")

            # 恢复后取最新消息
            state = await self._agent.aget_state(config)
            if state.values and "messages" in state.values:
                return _extract_text(state.values["messages"][-1])
            return "Oops, something went wrong."

        # 正常对话
        resp = await self._agent.ainvoke(
            input={"messages": [HumanMessage(content=query)]},
            config=config,
            version="v2",
        )
        if resp.interrupts:
            return _format_interrupt_question(resp.interrupts[0])
        return _extract_text(resp.value["messages"][-1])

    # --- stream 模式 ---
    async def astream(self, query: str, config: RunnableConfig):
        if not self._agent:
            await self._init_deep_agent()

        # 判断是中断恢复还是正常对话
        state = await self._agent.aget_state(config)
        if state.next:
            actions_count = len(
                state.interrupts[0].value["action_requests"]
            )
            decision = _parse_decision(query)
            input_data = _build_resume_command(decision, actions_count)
        else:
            input_data = {"messages": [HumanMessage(content=query)]}

        async for chunk in self._agent.astream(
            input=input_data,
            stream_mode=["messages", "updates"],
            version="v2",
            config=config,
        ):
            if chunk["type"] == "messages":
                msg, _meta = chunk["data"]
                if isinstance(msg, AIMessageChunk) and msg.content:
                    yield _extract_text(msg)
            elif chunk["type"] == "updates" and "__interrupt__" in chunk["data"]:
                yield _format_interrupt_question(
                    chunk["data"]["__interrupt__"][0]
                )

Chainlit 应用层(chainlit_app.py 核心部分)

@cl.on_message
async def main(msg: cl.Message):
    config = RunnableConfig(
        configurable={"thread_id": cl.context.session.id},
    )

    # stream 模式(推荐)
    final_answer = cl.Message(content="")
    async for chunk in ai_agent.astream(msg.content, config=config):
        await final_answer.stream_token(chunk)
    await final_answer.send()

    # 或者 invoke 模式
    # resp = await ai_agent.ainvoke(msg.content, config)
    # await cl.Message(resp).send()

Chainlit 应用层的代码非常简洁——因为中断检测和恢复逻辑全部封装在 AIAgent 内部了。Chainlit 只需要流式输出 astream() / ainvoke() 的返回结果即可。

交互效果

[用户]: 检查下系统负载

[AI]: 🔧 正在调用工具: execute_shell_command...

[AI]: Do you approve me to execute this action?

       - name: execute_shell_command
       - args: `{"command": "cat /proc/loadavg && free -h", "timeout": 10}`

       Input your decision: approve, reject

[用户]: 批准

[AI]: 当前系统负载: 0.52 0.38 0.25,
       内存总容量 62Gi,已用 10Gi,剩余 46Gi,系统运行正常。

进阶:使用 interrupt_on 的 when 谓词

如果不想拦截所有 shell 命令,只想拦截危险操作(如 rmdd、写入系统目录等),可以用 when 谓词按参数条件判断:

from langgraph.prebuilt.tool_node import ToolCallRequest

def is_dangerous_command(request: ToolCallRequest) -> bool:
    """只拦截包含危险操作的命令。"""
    command = request.tool_call["args"].get("command", "")
    dangerous = {"rm ", "dd ", "mkfs", "shutdown", "reboot"}
    return any(d in command for d in dangerous)

HumanInTheLoopMiddleware(
    interrupt_on={
        "execute_shell_command": {
            "allowed_decisions": ["approve", "reject"],
            "when": is_dangerous_command,   # 只在危险命令时拦截
        }
    }
)

when 谓词返回 True 才触发中断,返回 False 则自动批准。注意 when 需要 langchain >= 1.3.3

改进点

  • 目前 reject 时用的是固定消息,实际产品中可以让用户输入拒绝原因,方便 LLM 调整后续行为
  • 审批提示目前是纯文本,可以用 Chainlit 的 AskActionMessage 做成按钮交互(不过受制于 Chainlit Action 的 payload 类型限制,需要额外处理)
  • 如果有多个 tool 同时被拦截,action_requests 列表中会有多项,本文为简化只取了第一个,生产环境应遍历处理

补充

DeepAgents 的 HumanInTheLoopMiddleware 把之前需要手写的中断逻辑全部封装好了。集成到真实应用的关键只有三步:

  1. 创建 Agent 时:配置 interrupt_on 字典 + 确保有 checkpointer
  2. 每次调用前:通过 state.next 判断是正常对话还是中断恢复
  3. 恢复时:用 Command(resume={"decisions": [...]}) 传入用户决策

ainvokeastream 两种模式的核心逻辑一致,只是检测中断的方式不同(.interrupts 属性 vs updates 流中的 __interrupt__)。