掘金 人工智能 7小时前
彻底说清 Human-in-the-Loop:企业级 Agent 系统的关键挑战与LangGraph解法【上】
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了在LLM Agent自动化流程中,如何通过Human-in-the-Loop(HITL,人类参与闭环)模式来提高Agent的准确性、可信度和用户体验,尤其是在企业级应用场景下的实践。文章详细介绍了HITL的必要性、常见模式、核心机制(基于LangGraph框架),以及在工具调用和远程模式下的应用挑战与解决方案。通过两种工具管控模式的对比,为企业在Agent应用中实施HITL提供了有价值的参考。

🧐 HITL是提高Agent可靠性的关键,常见模式包括审批确认、信息注入和安全管控,这些模式可以混合使用以满足复杂的业务需求。

⚙️ LangGraph框架提供了Interrupt(中断)、Command Resume(命令恢复)和Checkpoint(检查点)三大机制,用于实现带有人类参与的Agent系统,支持流程的中断、恢复和状态持久化。

💡 在应用HITL时,需要注意Interrupt的本质是异常,断点续跑从中断节点开始,并且需要唯一的ID标识工作流运行过程,以便进行状态恢复。

🛠️ 工具调用管控是HITL的重要应用场景,文章介绍了集中看守模式和自我管理模式两种方案,分别适用于不同的企业需求。

🛡️ 集中看守模式通过审批节点管控高风险工具,而自我管理模式则允许工具内部自行决定是否需要人类审批,提高了灵活性。

作者:Ai大模型应用实战 (vx公众号)

在LLM Agent的自动化流程中,Human-in-the-Loop(HITL,人类参与闭环)是常见的设计模式之一。特别在要求较高的企业级场景中,HITL可以让人类在流程中对Agent运行进行适时的监督与接入,从而提高系统的准确性、可信度和用户体验。 不过在技术角度,HITL却常常成为很多人的梦魇:流程的中断、恢复、人类反馈、持久、前后台协作等,常常是HITL面临的众多难点。本文以LangGraph为框架,对企业级环境下的HITL关键挑战做解读与实践。全文内容将涵盖:

本篇首先介绍前四个部分。全部代码将在完结后一起提供。

01

HITL的必要性与常见模式

尽管在大部分时候我们都乐见高度自动化的AI带来的效率提高。但现实是:

HITL在企业的例子随处可见。比如:在智能客服的复杂问题回答中人工介入修正答案;企业OA中发送重要邮件与公告前需人工审批;Agent调用破坏性工具(Tool)前需要人工审批。随着Agent在B端的逐渐落地,HITL将是很多部署的标准配置。用一张图描述HITL的几种典型模式:这些不同的模式下,人工参与的方式与时机各有差异。可以做简单分类:

很多时候这些模式都非孤立存在,一个复杂的企业级流程可能混合着多种HITL模式。这很容易带来一些流程上的复杂性,也是一些技术难题的根源。

02

HITL基础应用:核心机制(LangGraph)

实现带有人类参与的Agent系统的关键在哪里?或许你可以想象到:流程中断与恢复,以及为了支持它所需要的状态持久化机制。简单说,就是需要一种机制,将流程“挂起”在特定节点(或步骤),等待人类参与和反馈,然后能从中断点恢复运行: 这要求系统能够记录中断时的上下文,并确保恢复后状态一致。很显然,你不能使用sleep等待或轮询这种糟糕的阻塞式方案。而LangGraph给出的解决方案是Interrupt(中断)、Command Resume(命令恢复)、Checkpoint(检查点)三大机制。Interrupt(中断)即暂停LangGraph工作流的执行,同时返回一个中断数据对象。其中含有给人类的信息,比如需要审核的内容,或者恢复时需要的元数据。典型的处理如下:

from langgraph.types import interrupt, Command...#这是一个Agent的某个人工参与的节点def human_review_node(state: State):   # 暂停执行,输出需人工审核的数据    review_data = {"question""请审核以下内容:""output": state["llm_output"]}    decision = interrupt(review_data)     # 恢复后将根据人工决策更新状态或跳转    if decision == "approve":        return Command(goto="approved_node")    else:        return Command(goto="rejected_node")...

在这个Agent节点中,interrupt的作用会暂时挂起这个Agent工作流,并将review_data返回给人类处理。一旦人类给予反馈,并要求工作流继续进行,该节点就会收到反馈信息(这里的decision),进而可以恢复运行。Command Resume(恢复)要恢复工作流,需要获得人类反馈并注入工作流状态(State),然后发出继续执行的命令:使用 Command(resume=value) 来反馈并恢复。这个工作通常是调用Agent的客户端来完成,比如:

...调用agent客户端程序...    result = graph.invoke(initial_state, config) #调用Agent启动工作流    interrupt_info = result['__interrupt__'][0].value    ...显示中断信息,人类交互与反馈...    # 假设 thread_id 标识此次任务,再次调用invoke恢复运行即可    user_decision"approve"  # 这里模拟用户最后的反馈    result = graph.invoke(Command(resume=user_decision), config={"configurable": {"thread_id": thread_id}})...

在这里通过Command对象的resume信息将人类反馈再送回Agent,变成之前Agent发起的interrupt调用的返回值(即上面代码中的decision)。Checkpoint(检查点)为了实现“断点续跑”,必须要实现Agent的状态持久化,用来在恢复时“重建现场”。这种机制也有利于Agent发生故障时的轨迹重放。这需要你首先创建一个检查点管理器:

...# 初始化 PostgreSQL 检查点保存器with PostgresSaver.from_conn_string("postgresql://postgres:yourpassword@localhost/postgres?sslmode=disable"as checkpointer:        checkpointer.setup()        graph = builder.compile(checkpointer=checkpointer)

这里创建了一个基于Postgres的checkpointer,并将其交给Agent。该checkpointer首次通过setup创建数据库对象;后续在每个node运行后将State序列化后并持久保存。

03

HITL基础应用:原理解析与注意点

尽管LangGraph处理HITL的核心机制貌似很简单。不过为了更好的掌控和应用它,有几个注意点需要特别了解:Interrupt的本质是Exception(异常)Interrupt的本质是什么?为什么它可以中断?原因很简单,因为它就是丢出了一个异常(Exception),异常信息就是中断时送出的数据。所以在发起Interrupt调用时不要做自定义异常捕获,否则可能无法中断。断点续跑是从中断所在的“节点”开始需要注意,“断点续跑”只是从中断的node重新开始,并不是从Interrupt函数调用处开始!所以不要在这个节点的Interrupt之前做改变状态(State)的动作!如果可能,尽量让人工节点只负责处理中断。要有唯一的ID标识一次工作流运行过程“断点续跑”依赖于首次Agent调用时的thread_id,所以如果需要处理HITL,就需要提供该信息。因为Checkpointer需要借助它做检查点,而恢复运行时则需要提供相同的thread_id来让Checkpointer找到对应的检查点。在理解了这几个注意点后,最后来总结与回顾整个处理过程:以一个本地SDK模式下直接调用Agent的客户端为例:

    客户端调用invoke启动Agent工作流,指定thread_id和输入信息工组流运行到人工节点的interrupt调用,发生中断,并携带了中断数据中断发生。客户端收到Agent的返回状态,从中发现有中断,则提示用户用户输入反馈后,调用invoke恢复工作流,指定thread_id和resume信息再次进入人工节点,此时由于有resume信息,interrupt函数不会触发中断,直接返回resume信息;流程得以继续运行。至此,一次中断过程处理结束

在实际生产中,一次的复杂流程运行可能会发生多次中断与人类参与,你需要更谨慎的设计Agent工作流以及客户端对多次中断的处理(借助于循环或者递归的方法。我们用一个例子演示上面的基础应用:一个借助AI润色文本的Agent,在每次润色以后会请求人工交互以获得修改意见;同时在最后输出前会再次确认成果。交互过程如下:

04

HITL下的工具调用:两种管控模式

工具(Tools)使用是Agent最普遍的模式。当Agent准备调用外部工具或执行关键操作时,引入人工确认可以避免错误或高风险行为(特别是在MCP后大量共享工具的出现)。尽管在大的方法上和普通的审批没有质的不同,但在细节上有一些更灵活的控制要求。一个最常见的问题是

在哪里拦截工具调用的意图?如何更方便的管控工具是否需要审核?

工具拦截,也就是工具的人工审批环节,应该设置在何处?我们以典型ReAct Agent为例,详细阐述两种人类管控模式。【集中看守模式】这种模式下,所有的工具调用会经过一个审批节点:这种模式中,前置的规划节点(一般是LLM Call)输出工具调用的需求(Tool_Calls)。人工审批节点进行判断,如果发现工具调用具有高风险,则通过 interrupt发起中断,提交给人类审核,否则直接通过。大致逻辑如下:

def human_approval_node(state: State):        .....从历史消息或者状态获得工具调用消息:tool_calls.....        tool_calls_info = []                # 获取工具调用信息(这里暂时只取第一个演示)        tc = tool_calls[0]        tool_id = tc.get("id""未知工具ID")        tool_name = tc.get("name""未知工具")        tool_args = tc.get("args", {})        tool_calls_info.append(f"{tool_name}({tool_args})")                    # 非高风险工具自动批准        if tool_name not in HIGH_RISK_TOOLS:            return {"human_approved": True}                tool_calls_str"\n - ".join(tool_calls_info)                # 高风险工具:中断并等待人工审批        value = interrupt({            "tool_calls": tool_calls_str,            "message": "请输入 'ok' 批准工具使用,或输入 'reject' 拒绝"        })...

很显然,通过设置这里的HIGH_RISK_TOOLS列表(高风险工具),就可以灵活调整哪些工具需要审核,哪些工具则可以自由放行。

这种模式有一个细节问题:当工具被拒绝时,你不能简单的将请求路由回原节点。由于一些LLM要求在出现Tool_calls的AI消息后,必须有对应的工具结果(LangGraph中ToolMessage类型的消息),否则会导致API错误。因此,这里你可以人为的修改State,添加一条表明工具被拒绝的ToolMessage。

【自我管理模式】这种模式下,工具的内部逻辑自行决定是否需要人类审批:比如一个数据库访问的工具,在执行SQL之前,自行判断并针对所有非只读的请求发起中断,要求人工审批。以下是一个需要审批的搜索工具内部:

async def tavily_search(query: str, search_depth: Optional[str] = "basic"):    ...    # 中断执行,等待人工审核    response = interrupt({        "tool""tavily_search",        "args": {            "query": query,            "search_depth": search_depth        },        "message"f"准备使用Tavily搜索:\n- 查询内容: {query}\n- 搜索深度: {search_depth}\n\n是否允许继续?\n输入 'yes' 接受,'no' 拒绝,或 'edit' 修改查询关键词",    })     # 处理人工响应    if response["type"] == "accept":        pass    elif response["type"] == "edit":        query = response["args"]["query"]    else:        returnf"该工具被拒绝使用,请尝试其他方法或拒绝回答问题。"        ...开始执行真正的工具逻辑...

这里的search工具在开始真正的工具逻辑之前,首先请求人工审核:人工可以选择同意、拒绝,或者修改搜索参数后继续执行。这种模式下,由于人类审批的请求由工具自我管理。因此,如果需要针对每个工具都增加这种中断代码,就会变得难以维护。一个较好的方法是:由于对不同工具的审核与反馈过程相对一致。因此可以设计一个Python的装饰器,来给普通的工具函数“偷偷的”加上人工审核功能(LangGraph官方提供了一个类似的Wrapper函数实现,不过装饰器似乎更简洁):现在,你在创建工具时无需考虑HITL。只需要在必要的时候加上装饰器,该工具就具备了人工审核的功能(@human_in_the_loop):

@human_in_the_loop()def tavily_search(query: str, search_depth: str"basic"):    """使用Tavily进行网络搜索"""    try:......

我们创建了一个ReAct Agent来验证这个工具的调用(客户端处理仍需自行实现):最后,简单总结两种管控模式的差异:

总之,通过引入Human-in-the-Loop,无论哪种模式,都可以对Agent运行过程中的工具风险进行充分管控,这对于在企业中推行Agent应用有重要的意义。在下篇中,我们将继续探讨与实践在远程模式下(即借助API与服务器上的Agent通信),Human-in-the-Loop所面临的不同挑战与应对方法。欢迎继续关注。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

LLM Agent Human-in-the-Loop LangGraph 工具调用 企业级应用
相关文章