收藏必学!LangGraph实现人工干预的完整指南,让你的AI系统更安全可靠

为什么需要人工干预?

先说个真实案例。前段时间做了个自动化运维工具,本来想着能自动处理服务器故障多好啊。结果有一次,AI判断某个服务需要重启,二话不说就执行了。问题是,那是个正在跑批的数据库服务…

从那之后我就明白了,AI再聪明也需要人类把关,特别是:

  • 涉及金钱的操作(下单、转账、交易)
  • 数据的增删改
  • 发送消息给客户
  • 调用外部API

这不是说AI不行,而是有些决策需要人类的经验和直觉。比如AI可能不知道"虽然服务器CPU占用率高,但现在是月底结算日不能重启"这种业务潜规则。

LangGraph是怎么做的

LangGraph的思路很简单:让流程在某个地方暂停,等人确认后再继续。有点像你在玩游戏时按暂停键,处理完事情再继续玩。

核心就两个概念:

  1. interrupt - 暂停并等待人工输入
  2. Command - 恢复执行

中断实现方式

在这里插入图片描述
动态中断

最灵活的方式是动态中断,就是在代码执行过程中根据情况决定要不要暂停

from langgraph.types import interrupt, Command
from langgraph.graph import StateGraph
from langgraph.checkpoint.memory import InMemorySaver

def check_with_human(state):
    # 这里会暂停,等待人工输入, 人工输入的值会赋值给human_input
    human_input = interrupt({
        "question": f"要删除{state['count']}条数据,确定吗?",
        "data": state['data']
    })

    # 拿到人工输入后继续执行
    if human_input == "yes":
        return {"confirmed": True}
    return {"confirmed": False}

注意,你必须要有个checkpointer来保存状态,不然中断后就没法恢复了。我一开始就是忘了这个,调试了半天:

# 千万别忘了这个
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# 还需要指定thread_id
config = {"configurable": {"thread_id": "some-unique-id"}}

静态中断

如果你知道某些节点肯定需要人工确认,可以直接在编译时指定, 相当度提前设置关卡。

graph = builder.compile(
    checkpointer=checkpointer,
    interrupt_before=["dangerous_node"],  # 这个节点执行前必须人工确认
    # interrupt_after=["node1"],  执行node1后,中断!
)

说实话,静态中断我用得不多,主要用来调试。生产环境还是动态中断灵活。

典型场景

节点人工路由

这个最常见了,比如等待用户的选择继续走哪个节点。

from typing import Literal, TypedDict
import uuid

from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver

# Define the shared graph state
class State(TypedDict):
    llm_output: str
    decision: str

# Simulate an LLM output node
def generate_llm_output(state: State):
    return {"llm_output": "This is the generated output."}

# 人工中断审核节点
def human_approval(state: State) -> Command[Literal["approved_path", "rejected_path"]]:

    decision = interrupt({
        "question": "Do you approve the following output?",
        "llm_output": state["llm_output"]
    })

# 人工审核通过
    if decision == "approve":
        return Command(goto="approved_path", update={"decision": "approved"})
# 人工审核拒绝
    else:
        return Command(goto="rejected_path", update={"decision": "rejected"})

# Next steps after approval
def approved_node(state: State) -> State:
    print("✅ Approved path taken.")
    return state

# Alternative path after rejection
def rejected_node(state: State) -> State:
    print("❌ Rejected path taken.")
    return state

# Build the graph
builder = StateGraph(State)
builder.add_node("generate_llm_output", generate_llm_output)
builder.add_node("human_approval", human_approval)
builder.add_node("approved_path", approved_node)
builder.add_node("rejected_path", rejected_node)

builder.set_entry_point("generate_llm_output")
builder.add_edge("generate_llm_output", "human_approval")
builder.add_edge("approved_path", END)
builder.add_edge("rejected_path", END)

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# Run until interrupt
config = {"configurable": {"thread_id": uuid.uuid4()}}
result = graph.invoke({}, config=config)
print(result["__interrupt__"])
# Output:
# [Interrupt(value={'question': 'Do you approve the following output?', 'llm_output': 'This is the generated output.'}, resumable=True, ns=['human_approval:71743c02-e389-910f-7f94-21a6db33d824'])]

# 模拟人审核通过
# 测试拒绝, 则替换 resume="approve" with resume="reject"
final_result = graph.invoke(Command(resume="approve"), config=config)
print(final_result)

工具调用拦截

这个最重要!你绝对不想让AI随便调用危险的API。

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt
from langgraph.prebuilt import create_react_agent

from dotenv import load_dotenv  # 用于加载环境变量
load_dotenv()  # 加载 .env 文件中的环境变量

# An example of a sensitive tool that requires human review / approval
def book_hotel(hotel_name: str):
    """Book a hotel"""

    # 调用预订酒店API时中断点,等待用户确认
    response = interrupt(
        f"Trying to call `book_hotel` with args {{'hotel_name': {hotel_name}}}. "
        "Please approve or suggest edits."
    )
    if response["type"] == "accept":
        pass
    elif response["type"] == "edit":
        hotel_name = response["args"]["hotel_name"]
    else:
        raise ValueError(f"Unknown response type: {response['type']}")
    return f"Successfully booked a stay at {hotel_name}."

checkpointer = InMemorySaver()

model = ChatOpenAI(model="gpt-4o")
agent = create_react_agent(
    model=model,
    tools=[book_hotel],
    checkpointer=checkpointer,
)

config = {
   "configurable": {
      "thread_id": "1"
   }
}

for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "book a stay at Jinling hotel"}]},
    config
):
    print(chunk)

from langgraph.types import Command

for chunk in agent.stream(
    Command(resume={"type": "accept"}),
    # Command(resume={"type": "edit", "args": {"hotel_name": "Jinling Hotel"}}),
    config
):
    print(chunk)

以上在工具实现函数调用前进行中断,实现起来比较侵入性比较, 我的做法是包装一下工具函数。

from typing import Callable
from langchain_core.tools import BaseTool, tool as create_tool
from langchain_core.runnables import RunnableConfig
from langchain_openai import ChatOpenAI
from langgraph.types import interrupt
from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt

from dotenv import load_dotenv  # 用于加载环境变量

load_dotenv()  # 加载 .env 文件中的环境变量

# 提供一个函数,用于包装工具,以便工具在调用前中断

def add_human_in_the_loop(
        tool: Callable | BaseTool,
        *,
        interrupt_config: HumanInterruptConfig = None,
):
    """Wrap a tool to support human-in-the-loop review."""
    if not isinstance(tool, BaseTool):
        tool = create_tool(tool)

    if interrupt_config is None:
        interrupt_config = {
            "allow_accept": True,
            "allow_edit": True,
            "allow_respond": True,
        }

    @create_tool(
        tool.name,
        args_schema=tool.args_schema
    )
    def call_tool_with_interrupt(config: RunnableConfig, **tool_input):
        request: HumanInterrupt = {
            "action_request": {
                "action": tool.name,
                "args": tool_input
            },
            "config": interrupt_config,
            "description": "Please review the tool call"
        }
        response = interrupt([request])
        response = response[0]
        # approve the tool call
        if response["type"] == "accept":
            tool_response = tool.invoke(tool_input, config)
        # update tool call args
        elif response["type"] == "edit":
            tool_input = response["args"]
            tool_response = tool.invoke(tool_input, config)
        # respond to the LLM with user feedback
        elif response["type"] == "response":
            user_feedback = response["args"]
            tool_response = user_feedback
        else:
            raise ValueError(f"Unsupported interrupt response type: {response['type']}")

        return tool_response

    return call_tool_with_interrupt

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent

checkpointer = InMemorySaver()

def book_hotel(hotel_name: str):
    """Book a hotel"""
    return f"Successfully booked a stay at {hotel_name}."

model = ChatOpenAI(model="gpt-4o")
agent = create_react_agent(
    model=model,
    tools=[
        add_human_in_the_loop(book_hotel),  # 包装需要人工确认的工具
    ],
    checkpointer=checkpointer,
)

config = {"configurable": {"thread_id": "1"}}

# Run the agent
"""
{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_suoSSBNFuBKyosHvyp4Un0GL', 'function': {'arguments': '{"hotel_name":"McKittrick hotel"}', 'name': 'book_hotel'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_46bff0e0c8'}, id='run-12084617-ec66-4acf-b996-5c759faf5b3c-0', tool_calls=[{'name': 'book_hotel', 'args': {'hotel_name': 'McKittrick hotel'}, 'id': 'call_suoSSBNFuBKyosHvyp4Un0GL', 'type': 'tool_call'}])]}}
{'__interrupt__': (Interrupt(value=[{'action_request': {'action': 'book_hotel', 'args': {'hotel_name': 'McKittrick hotel'}}, 'config': {'allow_accept': True, 'allow_edit': True, 'allow_respond': True}, 'description': 'Please review the tool call'}], resumable=True, ns=['tools:61613f8d-70ba-c104-7a36-abfbb6e97aa0']),)}
"""
for chunk in agent.stream(
        {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]},
        config
):
    print(chunk)

from langgraph.types import Command
#
for chunk in agent.stream(
        # Command(resume=[{"type": "accept"}]),
        Command(resume=[{"type": "edit", "args": {"hotel_name": "Jinling Hotel"}}]),
        config
):
    print(chunk)

输入验证

有时候需要反复确认用户输入,直到符合要求:

def get_validated_input(state):
    prompt = "请输入手机号"

    while True:
        user_input = interrupt(prompt)

        # 验证格式
        if not user_input.startswith("1") or len(user_input) != 11:
            prompt = f"{user_input} 不是有效手机号,请重新输入"
            continue

        # 二次确认
        confirm = interrupt(f"确认手机号是 {user_input} 吗?")
        if confirm == "yes":
            break
        else:
            prompt = "请重新输入手机号"

    return {"phone": user_input}

审查和修改状态

用户可以查看和编辑图的状态。比如AI生成的内容经常需要人工润色,特别是对外的文案:有些营销部门特别喜欢这个功能,AI负责生成初稿,人工负责把关品牌调性。

from typing import TypedDict
import uuid

from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver

from typing import TypedDict
import uuid

from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from langgraph.checkpoint.memory import MemorySaver

# Define the graph state
class State(TypedDict):
    summary: str

# Simulate an LLM summary generation
def generate_summary(state: State) -> State:
    return {
        "summary": "The cat sat on the mat and looked at the stars."
    }

# 用户审查和修改节点函数
def human_review_edit(state: State) -> State:
    result = interrupt({
        "task": "Please review and edit the generated summary if necessary.",
        "generated_summary": state["summary"]
    })
    # 返回人工重新编辑修改summary
    return {
        "summary": result["edited_summary"]
    }

# Simulate downstream use of the edited summary
def downstream_use(state: State) -> State:
    print(f"✅ Using edited summary: {state['summary']}")
    return state

# Build the graph
builder = StateGraph(State)
builder.add_node("generate_summary", generate_summary)
builder.add_node("human_review_edit", human_review_edit)
builder.add_node("downstream_use", downstream_use)

builder.set_entry_point("generate_summary")
builder.add_edge("generate_summary", "human_review_edit")
builder.add_edge("human_review_edit", "downstream_use")
builder.add_edge("downstream_use", END)

# Set up in-memory checkpointing for interrupt support
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# Invoke the graph until it hits the interrupt
config = {"configurable": {"thread_id": uuid.uuid4()}}
result = graph.invoke({}, config=config)

# Output interrupt payload
print(result["__interrupt__"])
# Example output:
# Interrupt(
#   value={
#     'task': 'Please review and edit the generated summary if necessary.',
#     'generated_summary': 'The cat sat on the mat and looked at the stars.'
#   },
#   resumable=True,
#   ...
# )

# Resume the graph with human-edited input
edited_summary = "The cat lay on the rug, gazing peacefully at the night sky."
resumed_result = graph.invoke(
    Command(resume={"edited_summary": edited_summary}),
    config=config
)
print(resumed_result)

踩过的坑

  1. 忘记设置checkpointer - 这个最常见,没有checkpointer就无法恢复
  2. thread_id管理混乱 - 每个会话要用独立的thread_id,不然状态会串
  3. 超时处理 - 人工可能不及时响应,要有超时机制和默认处理
  4. 状态持久化 - InMemorySaver只适合测试,生产环境要用数据库

一些建议

经过这段时间的使用,我总结了几点:

  1. 不要过度使用 - 不是所有地方都需要人工确认,只在真正重要的地方用,不然用户会疯的。
  2. 提供足够的上下文 - 中断时要给用户足够的信息做决策,别就问"确认吗?",要说清楚确认什么。
  3. 有降级方案 - 如果人工长时间不响应怎么办?要有个兜底方案。
  4. 记录审计日志 - 谁在什么时候批准了什么操作,这些都要记录下来

最后

HITL不是什么高大上的概念,就是在合适的地方让人参与进来。LangGraph的实现方式挺优雅的,通过interrupt和Command就能实现复杂的人机协作流程。

现在我们的系统里,所有涉及数据修改和外部API调用的地方都加了HITL机制。虽然牺牲了一点自动化程度,但睡得踏实多了。

如果你也在用LangGraph,强烈建议从一开始就把HITL机制设计进去,后期再加会很麻烦,别问我怎么知道的…

普通人如何抓住AI大模型的风口?

领取方式在文末

为什么要学习大模型?

目前AI大模型的技术岗位与能力培养随着人工智能技术的迅速发展和应用 , 大模型作为其中的重要组成部分 , 正逐渐成为推动人工智能发展的重要引擎 。大模型以其强大的数据处理和模式识别能力, 广泛应用于自然语言处理 、计算机视觉 、 智能推荐等领域 ,为各行各业带来了革命性的改变和机遇 。

目前,开源人工智能大模型已应用于医疗、政务、法律、汽车、娱乐、金融、互联网、教育、制造业、企业服务等多个场景,其中,应用于金融、企业服务、制造业和法律领域的大模型在本次调研中占比超过 30%。
在这里插入图片描述

随着AI大模型技术的迅速发展,相关岗位的需求也日益增加。大模型产业链催生了一批高薪新职业:
在这里插入图片描述

人工智能大潮已来,不加入就可能被淘汰。如果你是技术人,尤其是互联网从业者,现在就开始学习AI大模型技术,真的是给你的人生一个重要建议!

最后

只要你真心想学习AI大模型技术,这份精心整理的学习资料我愿意无偿分享给你,但是想学技术去乱搞的人别来找我!

在当前这个人工智能高速发展的时代,AI大模型正在深刻改变各行各业。我国对高水平AI人才的需求也日益增长,真正懂技术、能落地的人才依旧紧缺。我也希望通过这份资料,能够帮助更多有志于AI领域的朋友入门并深入学习。

真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

在这里插入图片描述

大模型全套学习资料展示

自我们与MoPaaS魔泊云合作以来,我们不断打磨课程体系与技术内容,在细节上精益求精,同时在技术层面也新增了许多前沿且实用的内容,力求为大家带来更系统、更实战、更落地的大模型学习体验。

图片

希望这份系统、实用的大模型学习路径,能够帮助你从零入门,进阶到实战,真正掌握AI时代的核心技能!

01 教学内容

图片

  • 从零到精通完整闭环:【基础理论 →RAG开发 → Agent设计 → 模型微调与私有化部署调→热门技术】5大模块,内容比传统教材更贴近企业实战!

  • 大量真实项目案例: 带你亲自上手搞数据清洗、模型调优这些硬核操作,把课本知识变成真本事‌!

02适学人群

应届毕业生‌: 无工作经验但想要系统学习AI大模型技术,期待通过实战项目掌握核心技术。

零基础转型‌: 非技术背景但关注AI应用场景,计划通过低代码工具实现“AI+行业”跨界‌。

业务赋能突破瓶颈: 传统开发者(Java/前端等)学习Transformer架构与LangChain框架,向AI全栈工程师转型‌。

image.png

vx扫描下方二维码即可
在这里插入图片描述

本教程比较珍贵,仅限大家自行学习,不要传播!更严禁商用!

03 入门到进阶学习路线图

大模型学习路线图,整体分为5个大的阶段:
图片

04 视频和书籍PDF合集

图片

从0到掌握主流大模型技术视频教程(涵盖模型训练、微调、RAG、LangChain、Agent开发等实战方向)

图片

新手必备的大模型学习PDF书单来了!全是硬核知识,帮你少走弯路(不吹牛,真有用)
图片

05 行业报告+白皮书合集

收集70+报告与白皮书,了解行业最新动态!
图片

06 90+份面试题/经验

AI大模型岗位面试经验总结(谁学技术不是为了赚$呢,找个好的岗位很重要)图片
在这里插入图片描述

07 deepseek部署包+技巧大全

在这里插入图片描述

由于篇幅有限

只展示部分资料

并且还在持续更新中…

真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值