【由浅入深探究langchain】第二十集-SQL Agent+Human-in-the-loop

前言

上一集解读了官方提供的SQL Agent的demo,这一集跟着官方的文档进入了 Agent 落地最核心的安全课题:Human-in-the-loop (HITL, 人在回路)。

在处理数据库(尤其是生产环境)时,AI 可能会生成效率极低、消耗资源巨大,甚至带有潜在风险的 SQL。引入"人在回路"机制,本质上是在 Agent 的"思考"与"执行"之间加了一个人工审批按钮。

官方文档中的位置如下:

编码

我们沿用上一集的代码,只修改核心创建agent的部分

python 复制代码
# 创建 SQL Agent
# 1. 使用 LangGraph 方式创建 Agent (替代之前的 create_sql_agent)
# 注意:这里我们直接把中文系统提示词传进去
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware 
from langgraph.checkpoint.memory import InMemorySaver

# 配置拦截中间件
middleware = [
    HumanInTheLoopMiddleware(
        # 只针对执行 SQL 的工具进行拦截
        interrupt_on={"sql_db_query": True},
        description_prefix="[安全审计] 准备执行 SQL,请审核"
    )
]

agent = create_agent(
    kimi_model, 
    tools, 
    system_prompt=system_prompt_zh ,
    middleware=middleware,
    checkpointer=InMemorySaver(), # 必须提供存储,否则无法在中断后恢复
)
# 2. 运行官方的流式循环
question = "哪个音乐类型的平均歌曲长度最长?"
config = {"configurable": {"thread_id": "1"}}

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    config,
    stream_mode="values",
):
    if "__interrupt__" in step:
        print("INTERRUPTED:")
        interrupt = step["__interrupt__"][0]
        for request in interrupt.value["action_requests"]:
            print(request["description"])
    elif "messages" in step:
        step["messages"][-1].pretty_print()
    else:
        pass
    
from langgraph.types import Command 

for step in agent.stream(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config,
    stream_mode="values",
):
    if "messages" in step:
        step["messages"][-1].pretty_print()
    elif "__interrupt__" in step:
        print("INTERRUPTED:")
        interrupt = step["__interrupt__"][0]
        for request in interrupt.value["action_requests"]:
            print(request["description"])
    else:
        pass
1. 配置刹车:中间件与存档点

定义了HumanInTheLoopMiddleware,规则是Agent 想要调用 sql_db_query 工具(即真正去跑 SQL),中间件就会强行挂起程序。

InMemorySaver:这是 Agent 的存档,之前内容详解过,不做过多赘述。

python 复制代码
# 配置拦截中间件
middleware = [
    HumanInTheLoopMiddleware(
        interrupt_on={"sql_db_query": True}, # 核心:只拦截执行 SQL 的动作
        description_prefix="[安全审计] 准备执行 SQL,请审核"
    )
]

agent = create_agent(
    # ...
    middleware=middleware,
    checkpointer=InMemorySaver(), # 存档点:没有它,Agent 停下后就"失忆"了
)
2. 触发拦截:第一次运行循环

thread_id:告诉 InMemorySaver 当前的"进度"

python 复制代码
config = {"configurable": {"thread_id": "1"}} # 存档位 ID

for step in agent.stream(
    {"messages": [{"role": "user", "content": question}]},
    config,
    stream_mode="values",
):
    if "__interrupt__" in step:
        # 捕获拦截信号
        interrupt = step["__interrupt__"][0]
        # 打印 Agent 打算干什么(即生成的 SQL 描述)
        for request in interrupt.value["action_requests"]:
            print(request["description"])
    elif "messages" in step:
        step["messages"][-1].pretty_print()

"__interrupt__" in step:当 Agent 跑到 sql_db_query 这一步时,循环会检测到这个信号。此时 Agent 会停止思考,程序进入 if 分支,打印出 AI 刚刚写好的、还没运行的 SQL。

3. 人工放行:第二次运行循环

Command(resume=...):这是**"唤醒指令"**。它告诉 Agent:"我看过了,你刚才写的 SQL 没问题,现在去执行吧(type: approve)。"

python 复制代码
from langgraph.types import Command 

for step in agent.stream(
    # 发送一个"批准"命令,唤醒挂起的 Agent
    Command(resume={"decisions": [{"type": "approve"}]}),
    config, # 必须带上 thread_id: "1",否则找不到之前的存档
    stream_mode="values",
):
    # 恢复运行后的处理逻辑
    if "messages" in step:
        step["messages"][-1].pretty_print()
结果解读

当我们运行这段代码的时候,前期的输出和上一集的一致,

直到第一次AI准备好了sql语句,即将使用sql_db_query这个工具去执行的时候,停住了,需要确认,而在我们给了approve信号后,它接下去执行sql,并整理结果了。

在我们的middleware 配置中,有一行核心定义: interrupt_on={"sql_db_query": True}

这意味着 Agent 在整个任务执行过程中,只有在伸手去碰数据库写权限/查询工具(即调用 sql_db_query)时,系统才会强行拉下闸门。

之前的步骤: Agent 调用 sql_db_list_tables 查看表名、调用 sql_db_schema 查看结构。这些都是"只读"且"低风险"的探测行为,所以系统没有拦截,流程顺滑通过。

拦截点: 当 Agent 准备好 SQL 语句,正式发起 sql_db_query 请求时,中间件识别到了匹配动作,于是立刻触发 INTERRUPTED

在屏幕上看到 INTERRUPTED 时,程序实际上已经运行完了第一个 for 循环。此时进程并没有结束,它只是退出了第一个循环。

状态已存档:由于你配置了 InMemorySaver,AI 的这条 SELECT 语句和当前的上下文已经保存在了 thread_id: "1" 对应的内存块里。

等待指令:直到你运行下半段代码(发送 Command(resume=...)),Agent 才会重新"活"过来,跳过拦截器去执行 SQL。

如果日志里出现了多次INTERRUPTED的打印,那也是可能发生的,因为可能会发生:AI 第一次执行 SQL 失败了(比如语法错),它会尝试修复并再次调用 sql_db_query。因为那是第二次调用工具,所以会再次触发拦截。这在生产环境中非常有意义------每一次写操作,人类都必须过目。

总结

在日志中我们清晰地看到,Agent 的自动化流程在涉及核心数据查询的 sql_db_query 环节精准'刹车'。这种设计确保了 AI 既能利用强大的推理能力完成复杂的 SQL 逻辑编写(如多表 Join 和聚合计算),又将最终的'扣动板机'权牢牢掌握在人类手中。

相关推荐
大江东去浪淘尽千古风流人物1 天前
【cuVSLAM】GPU 加速、多相机、实时视觉/视觉惯性 SLAM设计优势
c++·人工智能·数码相机·ubuntu·计算机视觉·augmented reality
weixin_458580121 天前
如何在 Go 中直接将 AST 编译为可执行二进制文件?
jvm·数据库·python
Elastic 中国社区官方博客1 天前
Elasticsearch:使用 Agent Builder 的 A2A 实现 - 开发者的圣诞颂歌
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
2301_816660211 天前
PHP怎么处理Eloquent Attribute Inference属性推断_Laravel从数据自动推导类型【操作】
jvm·数据库·python
第一程序员1 天前
数据工程 pipelines 实践
python·github
chools1 天前
【AI超级智能体】快速搞懂工具调用Tool Calling 和 MCP协议
java·人工智能·学习·ai
知行合一。。。1 天前
Python--05--面向对象(属性,方法)
android·开发语言·python
郝学胜-神的一滴1 天前
深度学习必学:PyTorch 神经网络参数初始化全攻略(原理 + 代码 + 选择指南)
人工智能·pytorch·python·深度学习·神经网络·机器学习
leobertlan1 天前
好玩系列:用20元实现快乐保存器
android·人工智能·算法
笨笨饿1 天前
#58_万能函数的构造方法:ReLU函数
数据结构·人工智能·stm32·单片机·硬件工程·学习方法