彻底说清 Human-in-the-Loop:企业级 Agent 系统的关键挑战与LangGraph解法【上】
原创 秋山墨客 2025-05-28 08:31 江苏
企业级Agent 系统里的“人工干预”到底怎么做?HITL 完整指南。
点击上方
蓝字
关注我们
-
HITL的必要性与常见模式 -
HITL基础应用:核心机制(LangGraph) -
HITL基础应用:原理解析与注意点 -
HITL下的工具调用:两种模式详解 -
远程模式下的HITL:前后台如何协作 -
远程模式下的HITL:客户端的故障恢复 -
远程模式下的HITL:服务端的故障恢复
01
HITL的必要性与常见模式
-
LLM远非全能。甚至有相当大的可能会犯错(幻觉、推理错误),会不确定(答案信心不足),或不合规。 -
很多时候需要牺牲效率来换取可靠性。特别是在企业级应用场景下,一次关键错误可能会带来灾难性的影响。
-
审批确认:在某些Agent的关键动作后,或达到某个指标时,需要等待人类审核,并给予批准、拒绝、或者下一步行动指示。 -
信息注入:在流程运行的某些环节,某些条件可能会需要人类注入更多信息。包括:任务调整、信息补充、中间结果的反馈校正等。 -
安全管控:本质上与审批类似。主要是针对Agent的工具使用进行管控,特别是对破坏性较大的工具进行审核、参数检查,必要时终止。
02
HITL基础应用:核心机制(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客户端程序...
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}})
...
...
# 初始化 PostgreSQL 检查点保存器
with PostgresSaver.from_conn_string("postgresql://postgres:yourpassword@localhost/postgres?sslmode=disable") as checkpointer:
checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
03
HITL基础应用:原理解析与注意点
-
客户端调用invoke启动Agent工作流,指定thread_id和输入信息 -
工组流运行到人工节点的interrupt调用,发生中断,并携带了中断数据 -
中断发生。客户端收到Agent的返回状态,从中发现有中断,则提示用户 -
用户输入反馈后,调用invoke恢复工作流,指定thread_id和resume信息 -
再次进入人工节点,此时由于有resume信息,interrupt函数不会触发中断,直接返回resume信息;流程得以继续运行。至此,一次中断过程处理结束
04
HITL下的工具调用:两种管控模式
工具(Tools)使用是Agent最普遍的模式。当Agent准备调用外部工具或执行关键操作时,引入人工确认可以避免错误或高风险行为(特别是在MCP后大量共享工具的出现)。尽管在大的方法上和普通的审批没有质的不同,但在细节上有一些更灵活的控制要求。一个最常见的问题是
在哪里拦截工具调用的意图?如何更方便的管控工具是否需要审核?
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' 拒绝"
})
...
这种模式有一个细节问题:当工具被拒绝时,你不能简单的将请求路由回原节点。由于一些LLM要求在出现Tool_calls的AI消息后,必须有对应的工具结果(LangGraph中ToolMessage类型的消息),否则会导致API错误。因此,这里你可以人为的修改State,添加一条表明工具被拒绝的ToolMessage。
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}nn是否允许继续?n输入 'yes' 接受,'no' 拒绝,或 'edit' 修改查询关键词",
})
# 处理人工响应
if response["type"] == "accept":
pass
elif response["type"] == "edit":
query = response["args"]["query"]
else:
returnf"该工具被拒绝使用,请尝试其他方法或拒绝回答问题。"
...开始执行真正的工具逻辑...
@human_in_the_loop()
def tavily_search(query: str, search_depth: str = "basic"):
"""使用Tavily进行网络搜索"""
try:
......
-
集中看守模式更适合安全管控与审计要求较高;工具数量可控;需要快速接入第三方工具(比如MCP)的企业与场景 -
自我管理模式更适合分布式并行开发下希望工具能自治;低风险的常规调用;以自研工具为主的企业与场景
end
福利时间
为了帮助LLM开发人员更系统性与更深入的学习RAG应用,特别是企业级的RAG应用场景下,当前主流的优化方法与技术实现,我们编写了《基于大模型的RAG应用开发与优化 — 构建企业级LLM应用》这本长达500页的开发指南,与大家一起来深入到LLM应用开发的全新世界。
更多细节,点击如下链接了解