[DeepAgents:LangChain的Harness-06]通过HumanInTheLoopMiddleware引入人机交互
某些较为敏感工具在执行之前需要引入人工审理此时就需要使用到HumanInTheLoopMiddleware。HumanInTheLoopMiddleware旨在为Agent增加一道安全护栏它通过LangGraph的中断实现了人机交互。在工具调用前引入人工干预确保敏感操作如发送邮件、删除数据、大额转账必须经过人类审核方可执行。1.Agent的中断与恢复执行LangGraph的中断与恢复执行功能建立在基于Checkpointing的持久化机制上我在拆解LangChain执行引擎这个长系列中利用多篇文章对此做过系统介绍。为了让大家对HumanInTheLoopMiddleware的实现原理有一个直观的认识我们先通过一个简单的实例演示来体验一下Agent的中断与恢复执行功能。如下的演示程序创建的Agent注册了一个用于转账的transfer工具工具函数中会调用interrupt函数通过触发一个中断等待用户的确认来决定是否继续执行转账操作。fromlangchain.agentsimportcreate_agentfromlanggraph.typesimportinterrupt,Commandfromlangchain.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportMemorySaverfromlangchain_core.runnablesimportRunnableConfigfromdotenvimportload_dotenvimportasyncio,uuid load_dotenv()toolasyncdeftransfer(account_from:str,account_to:str,amount:float):Execute a bank transfer between two accounts immediatelyifinterrupt(fDo you want to transfer{amount}from{account_from}to{account_to}?)yes:returnfTransfer of{amount}from{account_from}to{account_to}has been completed.returnTransfer request declined.agentcreate_agent(modelChatOpenAI(modelgpt-5.2-chat),tools[transfer],checkpointerMemorySaver(),)asyncdefperform_transfer(decision:str):config:RunnableConfig{configurable:{thread_id:uuid.uuid4().hex}}resultawaitagent.ainvoke(input{messages:[{role:user,content:Transfer $100 from 4242 4242 4242 4242 to 5555 5555 5555 4444.,},]},configconfig,versionv2)interrupt_idresult.interrupts[-1].idinterrupt_valueresult.interrupts[-1].valueprint(f\ Interrupt received during transfer process: ID:{interrupt_id}, Value:{interrupt_value})resultawaitagent.ainvoke(inputCommand(resumedecision),configconfig)print(result[messages][-1].content)asyncio.run(perform_transfer(yes))asyncio.run(perform_transfer(no))调用Agent提供转账的功能实现在perform_transfer函数中参数decision代表用户对于转账操作的确认批准approve或拒绝reject为两种可用的选项。由于中断会触发持久化而持久化是基于Thread进行的所以调用Agent的时候需要利用RunnableConfig提供thread_id相同的RunnableConfig被用于后续的恢复调用。调用Agent的ainvoke方法时我们显式指定了version参数为v2这样我们才可以执行结果中利用interrupts字段来获取中断列表由于节点的并发执行所以可能在同一个推理步骤会产生多个中断。中断通过具有如下定义的Interrupt类型表示id字段是中断的标识符value字段则是我们在工具函数中调用interrupt函数时提供的内容。演示程序会将中断对象的id和value打印出来。当我们以恢复执行的方式再次调用Agent时我们通过Command对象的resume字段提供了用户的审批决定这样Agent就可以根据这个决定来继续被中断的操作。finaldataclass(initFalse,slotsTrue)classInterrupt:value:Anyid:strdef__init__(self,value:Any,id:str_DEFAULT_INTERRUPT_ID,**deprecated_kwargs:Unpack[DeprecatedKwargs],)-Noneclassmethoddeffrom_ns(cls,value:Any,ns:str)-Interrupt:returncls(valuevalue,idxxh3_128_hexdigest(ns.encode()))我们提供这两种决定先后调用了perform_transfer函数最终会得到如下的输出结果Interrupt received during transfer process: ID: 519f2744bc91d0af06c90b924814c91f, Value: Do you want to transfer 100.0 from 4242 4242 4242 4242 to 5555 5555 5555 4444? ✅ **Transfer Successful!** Ive completed the transfer of **$100** with the following details: - **From:** 4242 4242 4242 4242 - **To:** 5555 5555 5555 4444 - **Amount:** $100.00 If you need a receipt, want to make another transfer, or have any other banking requests, just let me know!Interrupt received during transfer process: ID: 2d4d3358c895de0bf15c1bb94e076ffa, Value: Do you want to transfer 100.0 from 4242 4242 4242 4242 to 5555 5555 5555 4444? ❌ **Transfer Failed** The transfer of **$100** from account **4242 4242 4242 4242** to **5555 5555 5555 4444** could not be completed because the request was **declined** by the system. ### What you can do next: - ✅ Double‑check the account numbers for accuracy - Ensure the source account has sufficient funds - Confirm there are no restrictions or holds on either account - Try the transfer again later If you’d like, I can help you retry the transfer or look into possible reasons for the decline. Just let me know!2. 人机交互在HumanInTheLoopMiddleware中的实现HumanInTheLoopMiddleware旨在Agent增加一道安全护栏。它的作用是在模型调用某个工具之前向用户发出通知等待用户做出如下的决定批准Approve用户确认无误工具继续执行编辑Edit用户可以修改模型生成的参数例如修改邮件正文或接收人后再执行拒绝Reject用户拒绝执行该操作。通常可以附带反馈信息让模型根据反馈重新思考或修正后续行动。HumanInTheLoopMiddleware引入人为干预是基于某个工具。它的__init__方法利用interrupt_on参数来提供需要中断的工具和对应的配置另一个参数description_prefix的作用是为人工审核请求提供的说明性文本前缀。当程序暂停并询问人类是否允许执行这个工具时description_prefix决定了用户看到的提示文字开头是什么。classHumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT,ResponseT]):def__init__(self,interrupt_on:dict[str,bool|InterruptOnConfig],*,description_prefix:strTool execution requires approval,)-Noneinterrupt_on参数是一个字典Key为工具名称Value可以是一个布尔值或一个通过InterruptOnConfig对象表示的中断配置True: 代表对于这个工具的调用会被中断并等待用户的批准False: 代表对于这个工具的调用不会被中断InterruptOnConfig对象: 可以通过该对象提供更详细的配置例如允许的决策类型、描述信息和参数模式等。InterruptOnConfig是一个具有如下定义的TypedDict类型classHumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT,ResponseT]):def__init__(self,interrupt_on:dict[str,bool|InterruptOnConfig],*,description_prefix:strTool execution requires approval,)-NoneclassInterruptOnConfig(TypedDict):allowed_decisions:list[DecisionType]description:NotRequired[str|_DescriptionFactory]args_schema:NotRequired[dict[str,Any]]DecisionTypeLiteral[approve,edit,reject]class_DescriptionFactory(Protocol):def__call__(self,tool_call:ToolCall,state:AgentState[Any],runtime:Runtime[ContextT])-str:...它提供了更详细的中断配置选项allowed_decisionsDecisionType类型的列表代表允许的决策类型可以是批准approve、**编辑edit或拒绝reject**中的一种或多种description一个字符串或者返回字符串的函数用于提供中断的描述信息args_schema一个可选的字典用于定义工具调用参数的模式。当用户选择编辑决策时这个参数模式可以被用来验证用户提供的修改后的参数是否符合预期的格式和要求。某个工具总是在LLM执行之后并且其返回的AIMessage包含针对该工具的ToolCall对象情况下才会被调用。HumanInTheLoopMiddleware重写了wrap_model_call/awrap_model_call方法它在模型调用结束后分析生成的AIMessage中的ToolCall列表并根据配置的针对工具的中断规则来实施中断和等待用户决策的逻辑。classHumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT,ResponseT]):defafter_model(self,state:AgentState[Any],runtime:Runtime[ContextT])-dict[str,Any]|Noneasyncdefaafter_model(self,state:AgentState[Any],runtime:Runtime[ContextT])-dict[str,Any]|None如果AIMessage中的ToolCall对象匹配了interrupt_on参数中配置的工具HumanInTheLoopMiddleware会按照如下的逻辑来处理如果配置的Value为False代表对于这个工具的调用不会被中断HumanInTheLoopMiddleware会直接跳过不进行任何处理如果配置的Value为True或InterruptOnConfig对象代表对于这个工具的调用会被中断并等待用户的决策。HumanInTheLoopMiddleware会根据InterruptOnConfig中的allowed_decisions来确定允许的决策类型并且利用description或描述工厂函数来生成展示给用户的描述信息。然后它会触发一个中断等待用户做出批准、编辑或拒绝的决定当用户做出决策后HumanInTheLoopMiddleware会根据用户的决策来继续执行后续的操作批准原封不动地保留该工具调用编辑用人类修改后的edited_action新参数或新工具名替换掉模型原始的调用拒绝丢弃该调用并伪造一个ToolMessage状态为error或自定义消息告诉LLM这个操作被拒绝了。HumanInTheLoopMiddleware调用interrupt函数指定的参数是一个HITLRequest对象两个字段action_requests和review_configs分别返回一组ActionRequest和ReviewConfig列表。这几个TypedDict定义了人机交互协议的数据结构。它们共同描述了Agent想干什么、人类能怎么审查以及整个请求包长什么样。ActionRequest描述了Agent想要执行的操作包含操作名称、参数和可选的描述信息。ReviewConfig描述了针对某个操作的审查政策包括允许的决策类型、参数模式和可选的描述信息。classHITLRequest(TypedDict):action_requests:list[ActionRequest]review_configs:list[ReviewConfig]classActionRequest(TypedDict):name:strargs:dict[str,Any]description:NotRequired[str]classReviewConfig(TypedDict):action_name:strallowed_decisions:list[DecisionType]args_schema:NotRequired[dict[str,Any]]由于可能涉及多个工具的中断所以在调用Agent恢复执行时指定Command的resume字段需要提供一组按照中断顺序提供一组Decision具体为具有{decisions: [Decision]}格式的字典。Decision是一个联合类型具体类型可用是ApproveDecision、EditDecision或RejectDecision中的一种它们分别代表批准、编辑和拒绝三种决策类型。这几个TypedDict利用公共字段type来区分不同的决策类型RejectDecision进一步利用message字段提供拒绝的理由EditDecision利用edited_action字段返回的Action对象修改调用的工具和参数。DecisionApproveDecision|EditDecision|RejectDecisionclassApproveDecision(TypedDict):type:Literal[approve]classEditDecision(TypedDict):type:Literal[edit]edited_action:ActionclassRejectDecision(TypedDict):type:Literal[reject]message:NotRequired[str]classAction(TypedDict):name:strargs:dict[str,Any]3. 利用HumanInTheLoopMiddleware重写前面演示的实例在前面演示的实例中我们在工具函数中调用interrupt函数来触发中断来引入人机交互。下面的引入HumanInTheLoopMiddleware的版本两个版本基本是等效的。fromlangchain.agentsimportcreate_agentfromlanggraph.typesimportinterrupt,Commandfromlangchain.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportMemorySaverfromlangchain.agents.middlewareimportHumanInTheLoopMiddleware,InterruptOnConfigfromlangchain.agents.middleware.human_in_the_loopimportDecision,ApproveDecision,RejectDecisionfromlangchain_core.runnablesimportRunnableConfigfromdotenvimportload_dotenvimportasyncio,uuid load_dotenv()toolasyncdeftransfer(account_from:str,account_to:str,amount:float)-str:Execute a bank transfer between two accounts immediatelyreturnfTransfer of{amount}from{account_from}to{account_to}has been completed.agentcreate_agent(modelChatOpenAI(modelgpt-5.2-chat),tools[transfer],checkpointerMemorySaver(),middleware[HumanInTheLoopMiddleware(interrupt_on{transfer:InterruptOnConfig(allowed_decisions[approve,reject],descriptionfDo you want to conduct such a transfer?)})],)asyncdefperform_transfer(decision:Decision):config:RunnableConfig{configurable:{thread_id:uuid.uuid4().hex}}resultawaitagent.ainvoke(input{messages:[{role:user,content:Transfer $100 from 4242 4242 4242 4242 to 5555 5555 5555 4444.,}]},configconfig,versionv2)interrupt_idresult.interrupts[-1].idinterrupt_valueresult.interrupts[-1].valueprint(f\ Interrupt received during transfer process: ID:{interrupt_id}, Value:{interrupt_value})resultawaitagent.ainvoke(inputCommand(resume{decisions:[decision]}),configconfig)print(result[messages][-1].content)asyncio.run(perform_transfer(ApproveDecision(typeapprove)))asyncio.run(perform_transfer(RejectDecision(typereject,messageI dont feel safe about this transfer, so I want to reject it.)))