中间件与人工介入(Human-in-the-Loop)

本文介绍 LangGraph 两个核心生产模式:让 Agent 在执行敏感操作前等待人类审批,以及通过中间件在 Agent 运行的关键节点插入自定义逻辑。


一、Human-in-the-Loop:让 Agent 暂停等待审批

为什么需要它?

Agent 可以自动发邮件、删数据库、下订单……但这些操作一旦执行就难以撤销。Human-in-the-Loop 让你在 Agent 执行敏感操作前强制暂停,等人类确认

核心原理

用户输入 → Agent 执行 → 遇到 interrupt() → 暂停 → 人类审批 → Command(resume) → 继续执行

第一步:在工具里加 interrupt()

from langgraph.types import interrupt
from langchain_core.tools import tool

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email to a recipient."""
    
    # 暂停,等待人类审批
    approval = interrupt({
        "action": "send_email",
        "to": to,
        "subject": subject,
        "body": body,
        "message": "Do you want to send this email?"
    })
    
    if approval.get("approved"):
        return f"Email sent to {to} with subject '{subject}'"
    else:
        return "Email cancelled by user"

interrupt() 传入的字典是展示给人类看的信息,内容自定义,没有固定格式。

第二步:创建带 checkpointer 的 Agent

from langchain.agents import create_agent
from langgraph.checkpoint.memory import MemorySaver

# 中断必须有 checkpointer,否则无法保存暂停状态
agent = create_agent(
    model=model,
    tools=[send_email],
    system_prompt="You are a helpful email assistant.",
    checkpointer=MemorySaver()
)

第三步:触发中断

import uuid
from langchain.messages import HumanMessage

config = {"configurable": {"thread_id": str(uuid.uuid4())}}

result = agent.invoke(
    {"messages": [HumanMessage(content="Send an email to alice@example.com with subject 'Meeting Tomorrow' and body 'Let\\'s meet at 3pm.'")]},
    config=config
)

# 检查是否触发了中断
if "__interrupt__" in result:
    info = result["__interrupt__"][0].value
    print(f"⏸️  Agent 已暂停")
    print(f"  收件人: {info['to']}")
    print(f"  主题: {info['subject']}")
    print(f"  正文: {info['body']}")
    print(f"  提示: {info['message']}")

输出结构:

{
    "messages": [...],
    "__interrupt__": [Interrupt(value={
        "action": "send_email",
        "to": "alice@example.com",
        "subject": "Meeting Tomorrow",
        "body": "Let's meet at 3pm.",
        "message": "Do you want to send this email?"
    })]
}

第四步:人类做出决定,恢复执行

from langgraph.types import Command

# 同意
result = agent.invoke(
    Command(resume={"approved": True}),
    config=config  # 必须用同一个 thread_id!
)

# 拒绝
result = agent.invoke(
    Command(resume={"approved": False}),
    config=config
)

thread_id 是关键:LangGraph 通过它找到暂停的 Checkpoint,从中断处继续执行。


二、进阶模式:中断 + 编辑

除了简单的同意/拒绝,还可以让人类修改内容后再执行

@tool
def send_email_v2(to: str, subject: str, body: str) -> str:
    """Send an email with edit support."""
    
    response = interrupt({
        "action": "send_email",
        "to": to, "subject": subject, "body": body,
        "message": "Review this email. You can approve, reject, or edit it."
    })
    
    if response["type"] == "approve":
        return f"Email sent to {to}"

    elif response["type"] == "reject":
        return "Email cancelled"

    elif response["type"] == "edit":
        # 人类改了哪个字段就用哪个,没改的保持原值
        to = response.get("to", to)
        subject = response.get("subject", subject)
        body = response.get("body", body)
        return f"Email sent with edits: To={to}, Subject={subject}"

人类携带修改内容恢复执行:

result = agent_v2.invoke(
    Command(resume={
        "type": "edit",
        "subject": "URGENT: Meeting Today at 2pm",  # 只改主题
        "body": "This is the updated body."
    }),
    config=config
)

三、Middleware(中间件)

什么是中间件?

中间件让你在 Agent 运行的每个关键节点前后插入自定义代码,而不改变 Agent 本身的逻辑。

类比:快递中转站 —— 包裹(请求)在发件人和收件人之间经过中转,中转站可以做检查、加急标记等操作。

Agent 循环

用户输入
  → [before_model]    ← 调用模型前
  → [wrap_model_call] ← 包装模型调用本身
  → 模型执行
  → [after_model]     ← 调用模型后
  → 工具执行
  → 循环...

两种钩子风格

Node-style Wrap-style
比喻 监控摄像头 安保人员
能否阻止执行
典型用途 日志、状态更新、验证 重试、缓存、模型替换
代表钩子 before_model, after_model wrap_model_call, wrap_tool_call

示例 1:动态系统提示(Node-style)

根据用户角色动态切换 system prompt:

from langchain.agents.middleware import dynamic_prompt, ModelRequest
from typing import TypedDict

class Context(TypedDict):
    user_role: str

@dynamic_prompt
def dynamic_prompt_middleware(request: ModelRequest) -> str:
    user_role = request.runtime.context.get("user_role", "general")
    
    if user_role == "expert":
        return "你是技术专家助手,提供详细的技术回答和代码示例。"
    elif user_role == "beginner":
        return "你是入门助手,用简单易懂的语言解释概念,避免专业术语。"
    else:
        return "你是通用助手。"

# 创建带中间件的 Agent
agent = create_agent(
    model=model,
    tools=[explain_concept],
    middleware=[dynamic_prompt_middleware],
    context_schema=Context
)

# 专家模式调用
result = agent.invoke(
    {"messages": [HumanMessage(content="解释异步编程")]},
    context={"user_role": "expert"}
)

# 入门模式调用
result = agent.invoke(
    {"messages": [HumanMessage(content="解释异步编程")]},
    context={"user_role": "beginner"}
)

示例 2:请求日志记录(Wrap-style)

在每个步骤打印调试信息:

from langchain.agents.middleware import AgentMiddleware, AgentState, ModelRequest, ModelResponse
from typing import Any, Callable

class RequestLoggerMiddleware(AgentMiddleware):
    """记录所有模型请求,便于调试。"""
    
    def before_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
        message_count = len(state.get("messages", []))
        print(f"[调用前] 当前消息数: {message_count}")
        return None  # 返回 None 表示不修改 state
    
    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse]
    ) -> ModelResponse:
        print(f"[模型调用] 可用工具数: {len(request.tools) if request.tools else 0}")
        response = handler(request)  # 实际调用模型
        return response
    
    def after_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
        last_message = state["messages"][-1]
        if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
            print(f"[调用后] 模型请求调用 {len(last_message.tool_calls)} 个工具")
        else:
            print(f"[调用后] 模型直接返回最终答案")
        return None

# 使用
agent = create_agent(
    model=model,
    tools=[explain_concept],
    middleware=[RequestLoggerMiddleware()]
)

四、中间件 + Human-in-the-Loop 组合

两者配合使用,构建生产级的安全 Agent:

# 危险工具:需要人工确认
@tool
def delete_database(database_name: str) -> str:
    """Delete a database."""
    response = interrupt({
        "action": "delete_database",
        "database_name": database_name,
        "warning": "这将永久删除数据库!",
        "message": "你确定吗?"
    })
    if response.get("confirmed"):
        return f"数据库 '{database_name}' 已删除"
    return "已取消删除"

# 安全中间件:检测危险操作并记录
class SafetyMiddleware(AgentMiddleware):
    def after_model(self, state: AgentState, runtime) -> dict[str, Any] | None:
        last_message = state["messages"][-1]
        if hasattr(last_message, 'tool_calls'):
            for tool_call in last_message.tool_calls:
                if "delete" in tool_call["name"].lower():
                    print(f"⚠️ [安全警告] 检测到危险操作: {tool_call['name']}")
                    print(f"   参数: {tool_call['args']}")
        return None

# 生产级 Agent:同时具备安全日志 + 人工审批
production_agent = create_agent(
    model=model,
    tools=[delete_database],
    middleware=[SafetyMiddleware()],
    checkpointer=MemorySaver()
)

执行流程:

用户请求删除数据库
  → SafetyMiddleware 检测到危险操作,打印警告日志
  → 工具执行,触发 interrupt() 暂停
  → 人类看到警告,决定是否确认
  → Command(resume={"confirmed": True/False})
  → 继续或取消执行

总结

模式 用途 关键 API
Human-in-the-Loop 敏感操作前等待人类审批 interrupt(), Command(resume=...)
Node-style 中间件 日志、验证、状态注入 before_model, after_model
Wrap-style 中间件 拦截、重试、模型替换 wrap_model_call, wrap_tool_call
组合使用 生产级安全 Agent 以上全部

使用场景:

  • 发邮件/删数据等不可逆操作 → Human-in-the-Loop
  • 调试 Agent 执行过程 → 日志中间件
  • 根据用户身份切换行为 → 动态 prompt 中间件
  • 生产环境敏感操作 → 两者组合
posted @ 2026-05-19 16:18  江鸟Dev  阅读(16)  评论(0)    收藏  举报