LangGraph Human-in-the-loop 全解

LangGraph Human-in-the-loop 全解

一、什么是 Human-in-the-loop(人在回路)?

Human-in-the-loop(简称 HITL)是在自动化工作流的关键节点插入人工干预的机制,让 Agent 在执行到高风险、高不确定性或需要人类决策的步骤时自动暂停,等待人类确认、修改或补充信息后再继续执行。

为什么必须要有 HITL?

在生产级 Agent 系统中,纯自动化执行存在三大致命问题:

  1. 高风险操作不可控:比如退款、删数据、发邮件、调用付费 API,一旦出错会造成不可逆的损失
  2. 模型幻觉无法避免:LLM 偶尔会生成错误或不符合要求的内容,需要人类审核兜底
  3. 信息缺失无法自行解决:Agent 可能缺少只有人类知道的上下文或敏感信息

LangGraph 的 HITL 机制完美解决了这些问题,它基于中断 + 恢复的模式,让你可以在工作流的任意节点插入人工干预,同时保证状态的完整持久化。

二、LangGraph HITL 核心原理与 API

1. 三大核心组件

表格

组件 作用 核心说明
interrupt() 函数 暂停工作流执行 在节点内部调用,触发中断,传递需要人类查看的信息
Command(resume=...) 对象 恢复工作流执行 人类输入完成后,用 Command 将输入传递给 Agent,继续执行
Checkpointer(检查点) 持久化中断状态 必须配置,否则无法支持中断和恢复,中断时自动保存完整状态

2. 标准执行流程

plaintext

复制代码
1. Agent正常执行,到达关键节点
2. 节点内部调用 `interrupt(payload)`,触发中断
3. LangGraph自动保存当前完整状态到Checkpointer
4. 工作流暂停,将payload返回给调用方(前端/控制台)
5. 人类查看payload,输入确认/修改/补充信息
6. 调用方用 `Command(resume=人类输入)` 重新调用Agent
7. LangGraph从Checkpoint恢复状态,`interrupt()` 函数返回人类输入
8. 节点继续执行剩余代码,工作流恢复正常运行

3. 关键注意事项

  • 必须配置 Checkpointer:中断依赖状态持久化,没有 Checkpointer 无法工作
  • 不要用 try/except 包裹 interrupt:interrupt 通过抛出特殊异常实现,包裹会导致中断失效
  • payload 必须可序列化:interrupt 的参数必须是能转成 JSON 的类型(字符串、字典、列表等)
  • 中断是节点内的暂停:不是停止整个图,而是暂停在节点内部的 interrupt 行,恢复后从该行继续执行

三、实战:给双智能体系统添加人工干预

我们基于你之前的研究员 + 作家双智能体系统,添加两个最关键的人工干预点:

  1. 研究员完成研究笔记后:等待人类确认是否需要补充资料
  2. 作家完成初稿后:等待人类审核通过,或提出修改意见

第一步:核心 API 导入

复制代码
# 必须导入这两个核心API
from langgraph.types import interrupt, Command

第二步:完整可运行代码

复制代码
import os
from typing import TypedDict, Annotated, Sequence, Literal
from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langgraph.graph import add_messages, StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command  # ✅ 核心HITL API

# ==================== 1. 配置管理 ====================
load_dotenv()

class Settings(BaseSettings):
    ZHIPU_API_KEY: str
    ZHIPU_BASE_URL: str
    LLM_MODEL: str = "glm-4.6"
    LLM_BACKUP_MODEL: str = "glm-4-flash"
    LLM_TIMEOUT: int = 30
    MAX_TURNS: int = 5

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore"
    )

settings = Settings()

# ==================== 2. 全局状态定义 ====================
class MultiAgentState(TypedDict):
    task: str
    topic: str
    research_notes: str
    draft: str
    final_article: str
    current_agent: Literal["researcher", "writer", "human"]
    turn_count: int
    max_turns: int
    messages: Annotated[Sequence[BaseMessage], add_messages]
    error: str | None
    human_feedback: str  # ✅ 新增:存储人类反馈

# ==================== 3. LLM初始化 ====================
llm = ChatOpenAI(
    api_key=settings.ZHIPU_API_KEY,
    base_url=settings.ZHIPU_BASE_URL,
    model_name=settings.LLM_MODEL,
    temperature=0.7,
    timeout=settings.LLM_TIMEOUT
)

# ==================== 4. 智能体1:研究员(带人工确认)====================
def create_researcher_agent():
    researcher_prompt = ChatPromptTemplate.from_messages([
        ("system", """
你是专业的资深研究员,擅长围绕特定主题收集、整理、提炼准确、全面的资料。
如果有人类提供的反馈,请优先根据反馈补充或修改研究笔记。
"""),
        ("human", """
写作任务:{task}
写作主题:{topic}
人类反馈:{human_feedback}
请提供详细的结构化研究笔记。
""")
    ])

    def researcher_node(state: MultiAgentState):
        print("\n-- 🔬 研究员开始工作 --")
        try:
            # 调用LLM生成研究笔记
            response = llm.invoke(researcher_prompt.format(
                task=state["task"],
                topic=state["topic"],
                human_feedback=state.get("human_feedback", "无")
            ))
            research_notes = response.content
            print("✅ 研究员完成研究笔记")

            # ✅ 关键:触发人工干预,等待人类确认
            print("\n⏸️  等待人类确认研究笔记...")
            human_approval = interrupt({
                "type": "research_approval",
                "topic": state["topic"],
                "research_notes": research_notes,
                "prompt": "请确认研究笔记是否合格?输入 '通过' 继续,或输入修改意见让研究员补充:"
            })

            # 当人类输入后,代码从这里继续执行
            print(f"\n📝 收到人类反馈:{human_approval}")

            # 如果人类输入"通过",进入下一步
            if human_approval.strip() == "通过":
                return {
                    "research_notes": research_notes,
                    "current_agent": "writer",
                    "turn_count": state["turn_count"] + 1,
                    "human_feedback": "",
                    "messages": [AIMessage(content=f"研究员已完成研究笔记:\n{research_notes}")]
                }
            # 否则,研究员根据人类反馈修改研究笔记
            else:
                print("🔄 研究员根据人类反馈修改研究笔记...")
                return {
                    "human_feedback": human_approval,
                    "turn_count": state["turn_count"] + 1,
                    "current_agent": "researcher"  # 回到研究员节点,重新生成
                }

        except Exception as e:
            print(f"❌ 研究员工作失败:{str(e)}")
            return {
                "error": str(e),
                "turn_count": state["turn_count"] + 1
            }

    builder = StateGraph(MultiAgentState)
    builder.add_node("researcher", researcher_node)
    builder.set_entry_point("researcher")
    return builder.compile()

# ==================== 5. 智能体2:作家(带人工审核)====================
def create_writer_agent():
    writer_prompt = ChatPromptTemplate.from_messages([
        ("system", """
你是专业的资深作家,擅长基于研究员提供的资料,创作高质量、结构清晰、语言流畅的文章。
如果有人类提供的修改意见,请优先根据意见修改文章。
"""),
        ("human", """
写作任务:{task}
写作主题:{topic}
研究笔记:{research_notes}
人类修改意见:{human_feedback}
请写一篇完整的文章初稿。
""")
    ])

    def writer_node(state: MultiAgentState):
        print("\n-- ✍️ 作家开始工作 --")
        try:
            # 调用LLM生成初稿
            response = llm.invoke(writer_prompt.format(
                task=state["task"],
                topic=state["topic"],
                research_notes=state["research_notes"],
                human_feedback=state.get("human_feedback", "无")
            ))
            draft = response.content
            print("✅ 作家完成文章初稿")

            # ✅ 关键:触发人工干预,等待人类审核
            print("\n⏸️  等待人类审核文章初稿...")
            human_approval = interrupt({
                "type": "draft_approval",
                "topic": state["topic"],
                "draft": draft,
                "prompt": "请审核文章初稿:输入 '通过' 生成最终文章,或输入修改意见让作家修改:"
            })

            # 人类输入后继续执行
            print(f"\n📝 收到人类审核意见:{human_approval}")

            if human_approval.strip() == "通过":
                return {
                    "draft": draft,
                    "current_agent": "finalize",
                    "turn_count": state["turn_count"] + 1,
                    "human_feedback": "",
                    "messages": [AIMessage(content=f"作家已完成文章初稿:\n{draft}")]
                }
            else:
                print("🔄 作家根据人类意见修改文章...")
                return {
                    "human_feedback": human_approval,
                    "turn_count": state["turn_count"] + 1,
                    "current_agent": "writer"  # 回到作家节点,重新生成
                }

        except Exception as e:
            print(f"❌ 作家工作失败:{str(e)}")
            return {
                "error": str(e),
                "turn_count": state["turn_count"] + 1
            }

    builder = StateGraph(MultiAgentState)
    builder.add_node("writer", writer_node)
    builder.set_entry_point("writer")
    return builder.compile()

# ==================== 6. 主图:智能体调度器 ====================
def build_multi_agent_system():
    researcher_agent = create_researcher_agent()
    writer_agent = create_writer_agent()

    builder = StateGraph(MultiAgentState)

    # 把子图作为节点加入主图
    builder.add_node("researcher", researcher_agent)
    builder.add_node("writer", writer_agent)

    # 最终汇总节点
    def finalize_node(state: MultiAgentState):
        print("\n-- 🎉 任务完成,生成最终文章 --")
        return {
            "final_article": state["draft"],
            "messages": [AIMessage(content=f"最终文章:\n{state['draft']}")]
        }

    builder.add_node("finalize", finalize_node)

    # 条件路由
    def router(state: MultiAgentState) -> Literal["researcher", "writer", "finalize", "end"]:
        if state.get("error"):
            print(f"❌ 系统出错:{state['error']},任务结束")
            return "end"
        
        if state["turn_count"] >= state["max_turns"]:
            print("⚠️ 超过最大交互轮次,强制结束任务")
            return "finalize"
        
        current_agent = state["current_agent"]
        if current_agent == "researcher":
            return "researcher"
        elif current_agent == "writer":
            return "writer"
        elif current_agent == "finalize":
            return "finalize"
        else:
            return "finalize"

    builder.add_edge(START, "researcher")

    builder.add_conditional_edges(
        "researcher",
        router,
        {
            "writer": "writer",
            "finalize": "finalize",
            "end": END
        }
    )

    builder.add_conditional_edges(
        "writer",
        router,
        {
            "researcher": "researcher",
            "finalize": "finalize",
            "end": END
        }
    )

    builder.add_edge("finalize", END)

    # ✅ 必须配置Checkpointer,否则中断无法工作
    checkpointer = MemorySaver()
    # 生产环境用RedisSaver,支持服务重启后恢复
    # from langgraph.checkpoint.redis import RedisSaver
    # checkpointer = RedisSaver("redis://localhost:6379/0")

    return builder.compile(checkpointer=checkpointer)

# ==================== 7. 运行与人工交互逻辑 ====================
def run_with_human_interaction(agent, initial_state, config):
    """带人工交互的运行函数"""
    print("===== 🤝 带人工干预的双智能体协作系统 启动 =====")
    
    # 第一次运行,直到遇到第一个中断
    result = agent.invoke(initial_state, config=config)
    
    # 循环处理中断,直到任务完成
    while True:
        # 检查是否有中断
        if "__interrupt__" in result:
            interrupt_info = result["__interrupt__"][0]
            payload = interrupt_info["value"]
            
            # 打印中断信息和提示
            print("\n" + "="*80)
            print(f"⚠️  触发人工干预:{payload['type']}")
            print(f"主题:{payload['topic']}")
            print("-"*80)
            print(f"内容预览:\n{payload['content'][:500]}..." if len(payload['content']) > 500 else f"内容:\n{payload['content']}")
            print("-"*80)
            
            # 获取人类输入
            human_input = input(payload["prompt"])
            
            # 用Command恢复执行
            print("\n▶️  恢复执行...")
            result = agent.invoke(
                Command(resume=human_input),
                config=config
            )
        else:
            # 没有中断,任务完成
            break
    
    return result

# ==================== 8. 测试运行 ====================
if __name__ == "__main__":
    # 初始化多智能体系统
    multi_agent = build_multi_agent_system()

    # 会话配置:同一个thread_id支持服务重启后恢复中断
    config = {
        "configurable": {
            "thread_id": "multi_agent_hil_session_001",
            "user_id": "user_001"
        }
    }

    # 测试写作任务
    test_task = "写一篇关于LangGraph多智能体系统的技术文章"
    test_topic = "LangGraph多智能体系统的原理、架构与实现"

    # 初始化状态
    initial_state = {
        "task": test_task,
        "topic": test_topic,
        "research_notes": "",
        "draft": "",
        "final_article": "",
        "current_agent": "researcher",
        "turn_count": 0,
        "max_turns": settings.MAX_TURNS,
        "messages": [HumanMessage(content=test_task)],
        "error": None,
        "human_feedback": ""
    }

    # 运行带人工交互的系统
    result = run_with_human_interaction(multi_agent, initial_state, config)

    # 输出最终结果
    print("\n" + "="*80)
    print("📄 最终完成的文章:")
    print("="*80)
    print(result["final_article"])
    print("="*80)
    print(f"\n✅ 任务完成,共交互 {result['turn_count']} 轮")

四、运行效果演示

plaintext

复制代码
===== 🤝 带人工干预的双智能体协作系统 启动 =====

-- 🔬 研究员开始工作 --
✅ 研究员完成研究笔记

⏸️  等待人类确认研究笔记...

================================================================================
⚠️  触发人工干预:research_approval
主题:LangGraph多智能体系统的原理、架构与实现
--------------------------------------------------------------------------------
内容:
# LangGraph多智能体系统研究笔记
## 一、核心概念
LangGraph多智能体系统是基于状态机构建的,由多个独立智能体组成,通过共享状态进行通信...
--------------------------------------------------------------------------------
请确认研究笔记是否合格?输入 '通过' 继续,或输入修改意见让研究员补充:请补充多智能体的应用场景

📝 收到人类反馈:请补充多智能体的应用场景
🔄 研究员根据人类反馈修改研究笔记...

-- 🔬 研究员开始工作 --
✅ 研究员完成研究笔记

⏸️  等待人类确认研究笔记...
请确认研究笔记是否合格?输入 '通过' 继续,或输入修改意见让研究员补充:通过

📝 收到人类反馈:通过

-- ✍️ 作家开始工作 --
✅ 作家完成文章初稿

⏸️  等待人类审核文章初稿...
请审核文章初稿:输入 '通过' 生成最终文章,或输入修改意见让作家修改:通过

📝 收到人类审核意见:通过

-- 🎉 任务完成,生成最终文章 --

================================================================================
📄 最终完成的文章:
================================================================================
# LangGraph多智能体系统的原理、架构与实现
...(完整文章)
================================================================================

✅ 任务完成,共交互 4 轮

五、核心要点总结

  1. 三大核心 APIinterrupt() 暂停、Command(resume=...) 恢复、Checkpointer 持久化
  2. 中断是节点内的暂停:不是停止整个图,而是暂停在 interrupt 行,恢复后从该行继续
  3. 必须配置 Checkpointer:没有 Checkpointer,中断和恢复功能完全无法工作
  4. payload 设计要清晰:给人类的提示要明确,包含足够的上下文信息
  5. 支持无限期等待 :中断状态会永久保存在 Checkpointer 中,服务重启后可以通过同一个thread_id恢复

六、进阶扩展方向

1. 工具调用前的人工审批

给工具调用节点添加人工干预,在工具执行前让人类确认是否允许调用,避免高风险操作:

复制代码
def tool_node(state):
    tool_call = state["tool_calls"][0]
    # 触发人工审批
    approval = interrupt({
        "type": "tool_approval",
        "tool_name": tool_call["name"],
        "tool_args": tool_call["args"],
        "prompt": "是否允许执行这个工具调用?输入 '允许' 或 '拒绝':"
    })
    if approval == "允许":
        return execute_tool(tool_call)
    else:
        return {"error": "工具调用被人类拒绝"}

2. 超时自动处理

给中断添加超时机制,如果超过指定时间人类没有输入,自动执行默认操作(比如拒绝工具调用、继续执行等)。

3. 集成 Web 界面

将中断信息通过 API 暴露给前端,前端渲染审核界面,人类在网页上输入反馈后,后端调用Command(resume=...)恢复执行。

4. 人工干预历史记录

将所有人工干预的记录(时间、输入、操作人)存入数据库,方便审计和追溯。

5. 批量人工审核

对于需要处理大量任务的场景,实现批量审核界面,让人类可以一次性审核多个中断任务。

LangGraph 的 HITL 机制是生产级 Agent 系统的必备功能,它让你在自动化和人工监督之间找到了完美的平衡点,既提高了效率,又保证了系统的安全性和可靠性。

相关推荐
倒霉熊dd1 小时前
Python 学习(第二部分:函数、模块与面向对象编程)
前端·数据库·python
铁皮哥1 小时前
【力扣题解】LeetCode 25. K 个一组翻转链表
java·数据结构·windows·python·算法·leetcode·链表
lbb 小魔仙2 小时前
告别腾讯会议40分钟限制:用ToDesk协作版开在线会议,免费不限时远程会议新方案
python·langchain·jenkins
凯瑟琳.奥古斯特2 小时前
PyTorch动态计算图详解
人工智能·pytorch·python·深度学习
一个数据大开发2 小时前
企业知识工程的三条路线:Neo4j 知识中台、Agent + Action 与本体原生 Runtime
大数据·python·neo4j
没有感情的robot2 小时前
网页录制方法总结
python
m0_738120722 小时前
ctfshow靶场SSRF部分——基础绕过到协议攻击解题思路与技巧(二)
python·网络协议·tcp/ip·安全·网络安全
JavaEdge.2 小时前
用 LangChain 克隆一个 ChatGPT:LLMChain + Memory 实战
人工智能·chatgpt·langchain
人工智能培训3 小时前
如何定义和测量“通用具身智能”
大数据·人工智能·机器学习·prompt·agent