Human-in-the-loop中间件在我们智能体开发中使用还是比较重要的,因此会重点进行介绍。
什么是人工审核
人工循环(HITL)中间件允许你为代理工具调用添加人工监督审核。当模型提出可能需要审查的动作——例如写入文件或执行 SQL——中间件可以暂停执行并等待决策。
它通过将每个工具调用与可配置策略进行检查来实现这一点。如果需要干预,中间件会发出中断,从而停止执行。图状态通过 LangGraph 的持久层保存,因此执行可以安全地暂停,稍后继续。
随后由人工决策决定下一步发生什么:动作可以按原样批准(approve)、修改后执行(edit)、或通过反馈拒绝(reject)。
人工审核简单实现
1 | # @Time:2025/12/31 13:18 |
执行结果
1 | /Users/jinglv/PycharmProjects/learn-ai/.venv/bin/python /Users/jinglv/PycharmProjects/learn-ai/example/langchain_middleware/human_loop_demo.py |
注意:这只是智能体中的一个简单示例,这种方式并不常用,主要是要使用Human-in-the-loop中间件的方式
中断决策类型
中间件定义了三种内置的人类响应中断的方式:
| 决策类型 | 描述 | 示例用例 |
|---|---|---|
| ✅ approve | 该行动按现状批准并执行,无需更改。 | 按原文发送邮件草稿 |
| ✏️ edit | 工具调用执行时经过修改。 | 发送邮件前请更换收件人 |
| ❌ reject | 工具调用被拒绝,并在对话中添加了解释。 | 拒绝邮件草稿并说明如何重写 |
每个工具可用的决策类型取决于你在 interrupt_on 中配置的策略。当多个工具调用同时暂停时,每个动作都需要独立的决策。决策必须按照中断请求中动作出现的顺序提供。
注意: 编辑工具参数时,应保守地进行修改。对原始参数的重大修改可能导致模型重新评估其方法,并可能多次执行该工具或采取意外动作。
中断配置
要使用 HITL,创建代理时将中间件添加到代理列表。
你通过对工具动作的映射来配置它,映射到每个动作允许的决策类型。当工具调用与映射中的动作匹配时,中间件会中断执行。
1 | # @Time:2025/12/31 13:18 |
执行结果
1 | {'messages': [HumanMessage(content='帮我查询 user 表中所有的数据', additional_kwargs={}, response_metadata={}, id='fe6bd9b7-b794-45fc-a8fa-4c658bdf02e8'), AIMessage(content='我来帮您查询 user 表中的所有数据。首先让我获取数据库的连接配置。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 47, 'prompt_tokens': 533, 'total_tokens': 580, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 512}, 'prompt_cache_hit_tokens': 512, 'prompt_cache_miss_tokens': 21}, 'model_provider': 'deepseek', 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_eaab8d114b_prod0820_fp8_kvcache', 'id': 'd7782e31-15b8-4596-856b-718dac89eb45', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--aadae469-70a0-4e20-8f65-169b74b6ee88-0', tool_calls=[{'name': 'get_database_connect_config', 'args': {}, 'id': 'call_00_RRvibzp2LDaKFLOq53YdfAb9', 'type': 'tool_call'}], usage_metadata={'input_tokens': 533, 'output_tokens': 47, 'total_tokens': 580, 'input_token_details': {'cache_read': 512}, 'output_token_details': {}}), ToolMessage(content="host='82.157.193.65' port=3366 user='root' password='12345678' database='test'", name='get_database_connect_config', id='eb82e1e2-8874-4af9-8800-bd7cc97d02b0', tool_call_id='call_00_RRvibzp2LDaKFLOq53YdfAb9'), AIMessage(content='现在我已经获取到数据库的连接配置,接下来我将执行查询 user 表中所有数据的 SQL 语句。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 121, 'prompt_tokens': 625, 'total_tokens': 746, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 576}, 'prompt_cache_hit_tokens': 576, 'prompt_cache_miss_tokens': 49}, 'model_provider': 'deepseek', 'model_name': 'deepseek-chat', 'system_fingerprint': 'fp_eaab8d114b_prod0820_fp8_kvcache', 'id': '261ac061-f72e-416b-8d27-d6b5a2f6e8d0', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--4909536f-8654-47e8-8a94-ace558fec578-0', tool_calls=[{'name': 'database_handler', 'args': {'data_config': {'host': '82.157.193.65', 'port': 3366, 'user': 'root', 'password': '12345678', 'database': 'test'}, 'sql': 'SELECT * FROM user'}, 'id': 'call_00_yNotwUOxBG73hkyPabdTFamf', 'type': 'tool_call'}], usage_metadata={'input_tokens': 625, 'output_tokens': 121, 'total_tokens': 746, 'input_token_details': {'cache_read': 576}, 'output_token_details': {}})], '__interrupt__': [Interrupt(value={'action_requests': [{'name': 'database_handler', 'args': {'data_config': {'host': '82.157.193.65', 'port': 3366, 'user': 'root', 'password': '12345678', 'database': 'test'}, 'sql': 'SELECT * FROM user'}, 'description': "⚠️ 工具执行待人工审核\n\nTool: database_handler\nArgs: {'data_config': {'host': '82.157.193.65', 'port': 3366, 'user': 'root', 'password': '12345678', 'database': 'test'}, 'sql': 'SELECT * FROM user'}"}], 'review_configs': [{'action_name': 'database_handler', 'allowed_decisions': ['approve', 'reject', 'edit']}]}, id='21a5098333f394459b241f5bfa85e114')]} |
响应中断
当你调用代理时,它会运行,直到完成或中断被触发。当工具调用与你配置的策略匹配时,中断就会被触发。在这种情况下,调用结果会包含一个 __interrupt__ 字段,其中包含需要复审的动作。然后你可以将这些动作展示给审稿人,并在决策提供后继续执行。
从上面的执行结果来看,可以明确的看到中断触发的字段内容:
1 | '__interrupt__': [Interrupt(value={'action_requests': [{'name': 'database_handler', 'args': {'data_config': {'host': '82.157.193.65', 'port': 3366, 'user': 'root', 'password': '12345678', 'database': 'test'}, 'sql': 'SELECT * FROM user'}, 'description': "⚠️ 工具执行待人工审核\n\nTool: database_handler\nArgs: {'data_config': {'host': 'localhost', 'port': 3306, 'user': 'root', 'password': '12345678', 'database': 'test'}, 'sql': 'SELECT * FROM user'}"}], 'review_configs': [{'action_name': 'database_handler', 'allowed_decisions': ['approve', 'reject', 'edit']}]}, id='21a5098333f394459b241f5bfa85e114')] |
执行中断,HumanInTheLoopMiddleware中间件中如果工具执行需要审批的时候,会中断整个Agent任务的执行,中断之后,是可以手动恢复从上次中断的位置继续执行
中断之后的操作:
- 获取中断的原因,需要审核的具体事项
- 人工输出审批的结果
- 恢复执行状态(从中断的位置继续执行)
Langchain的Agent中断恢复的实现原理(人工审核是基于这个机制去实现):
- 基于Langgraph的检查点去实现的,所以在创建Agent一定要配置检查点
- 运行Agent的时候,必须通过config参数传入线程id(thread_id)
- Langgraph会用线程id作为唯一标识,在检查点中保存Agent的执行进度状态(快照)
- 在人工审批,恢复执行的时候,会使用线程id在检查点中找到之前执行的进度状态,然后从中断的位置继续执行
下面则介绍三种状态下如何进行决策的
中断决策类型示例
批准(approve)
用于按原样批准工具调用,并执行它,不做更改。
1 | # @Time:2025/12/31 13:18 |
执行结果
1 | -----执行步骤:调用大模型 |
人工拒绝(reject)
1 | # @Time:2025/12/31 13:18 |
执行结果
1 | -----执行步骤:调用大模型 |
人工修改再执行(edit)
1 | # @Time:2025/12/31 13:18 |
执行结果
1 | -----执行步骤:调用大模型 |