835 lines
26 KiB
Plaintext
835 lines
26 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# 23 人机协作\n",
|
||
"\n",
|
||
"## 学习目标\n",
|
||
"1. 理解人机协作(Human-in-the-loop)在智能体中的重要性\n",
|
||
"2. 掌握 LangGraph 中暂停(`interrupt`)和恢复(`resume`)机制\n",
|
||
"3. 能够设计需要人工确认或补充输入的智能体流程\n",
|
||
"4. 理解检查点在暂停与恢复中的作用\n",
|
||
"5. 能够构建带人工审批环节的基础智能体工作流"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 1. 什么是人机协作\n",
|
||
"\n",
|
||
"人机协作,也叫 Human-in-the-loop,意思是:**智能体不是从头到尾完全自动执行,而是在关键步骤暂停下来,让人参与判断、确认或补充信息。**\n",
|
||
"\n",
|
||
"普通自动流程可能是这样:\n",
|
||
"\n",
|
||
"```\n",
|
||
"输入 -> 智能体处理 -> 自动执行 -> 输出结果\n",
|
||
"```\n",
|
||
"\n",
|
||
"人机协作流程更像这样:\n",
|
||
"\n",
|
||
"```\n",
|
||
"输入 -> 智能体处理 -> 暂停等待人工确认 -> 继续执行 -> 输出结果\n",
|
||
"```\n",
|
||
"\n",
|
||
"也就是说,人不是在流程外面旁观,而是成为流程中的一个关键环节。\n",
|
||
"\n",
|
||
"在人机协作中,AI 适合做:\n",
|
||
"\n",
|
||
"- 整理信息\n",
|
||
"- 生成草稿\n",
|
||
"- 给出建议\n",
|
||
"- 自动处理低风险步骤\n",
|
||
"\n",
|
||
"人更适合做:\n",
|
||
"\n",
|
||
"- 最终确认\n",
|
||
"- 风险判断\n",
|
||
"- 补充缺失信息\n",
|
||
"- 决定是否继续执行\n",
|
||
"\n",
|
||
"这就是人机协作的核心思想:**让 AI 提高效率,让人把控关键决策。**"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 2. 为什么智能体需要人工介入\n",
|
||
"\n",
|
||
"很多智能体任务不能完全交给模型自动完成。原因主要有三类。\n",
|
||
"\n",
|
||
"### 2.1 有些操作风险较高\n",
|
||
"例如:\n",
|
||
"\n",
|
||
"- 发送正式邮件\n",
|
||
"- 删除数据\n",
|
||
"- 提交订单\n",
|
||
"- 执行付款\n",
|
||
"- 修改线上配置\n",
|
||
"\n",
|
||
"这些操作一旦执行,可能造成真实影响,所以最好先让人确认。\n",
|
||
"\n",
|
||
"### 2.2 有些信息 AI 不知道\n",
|
||
"例如用户说:\n",
|
||
"\n",
|
||
"```text\n",
|
||
"帮我给客户写一封邮件。\n",
|
||
"```\n",
|
||
"\n",
|
||
"但 AI 可能不知道客户姓名、邮件目的、语气要求、是否要附带报价。\n",
|
||
"\n",
|
||
"这时流程应该暂停,让用户补充信息。\n",
|
||
"\n",
|
||
"### 2.3 有些判断需要人负责\n",
|
||
"例如 AI 生成了一个方案,但最终是否采用,需要人做业务判断。\n",
|
||
"\n",
|
||
"所以,人机协作不是因为 AI 不够强,而是因为很多场景天然需要人工负责。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 3. LangGraph 中的人机协作机制\n",
|
||
"\n",
|
||
"在 LangGraph 中,人机协作通常依赖两个核心动作:\n",
|
||
"\n",
|
||
"| 动作 | 含义 | 作用 |\n",
|
||
"| --- | --- | --- |\n",
|
||
"| `interrupt(...)` | 暂停流程 | 把需要人工处理的信息抛出来 |\n",
|
||
"| `Command(resume=...)` | 恢复流程 | 把人工输入传回图中,继续执行 |\n",
|
||
"\n",
|
||
"可以这样理解:\n",
|
||
"\n",
|
||
"- `interrupt` 像是智能体举手说:‘这里需要人来决定一下’\n",
|
||
"- `Command(resume=...)` 像是人回复:‘我决定好了,你继续吧’\n",
|
||
"\n",
|
||
"不过要注意:暂停和恢复必须依赖检查点(checkpoint)。\n",
|
||
"\n",
|
||
"因为图暂停后,需要记住自己停在了哪里。等人给出答案后,才能从正确的位置继续执行。\n",
|
||
"\n",
|
||
"所以,人机协作一般会和 `MemorySaver` 一起使用。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 4. 第一个例子:人工确认后再继续\n",
|
||
"\n",
|
||
"先从最简单的例子开始:智能体准备执行一个操作,但在执行前需要人工确认。\n",
|
||
"\n",
|
||
"流程如下:\n",
|
||
"\n",
|
||
"```\n",
|
||
"START -> request_approval -> perform_action -> END\n",
|
||
"```\n",
|
||
"\n",
|
||
"其中 `request_approval` 会暂停,等待用户输入 `yes` 或 `no`。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "7085b2d5",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing_extensions import TypedDict\n",
|
||
"from langgraph.graph import StateGraph, START, END\n",
|
||
"from langgraph.types import interrupt, Command\n",
|
||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||
"\n",
|
||
"class ApprovalState(TypedDict):\n",
|
||
" task: str\n",
|
||
" approved: bool\n",
|
||
" result: str\n",
|
||
"\n",
|
||
"def request_approval(state: ApprovalState):\n",
|
||
" answer = interrupt({\n",
|
||
" 'question': '是否允许执行这个任务?',\n",
|
||
" 'task': state['task'],\n",
|
||
" 'options': ['yes', 'no']\n",
|
||
" })\n",
|
||
" return {'approved': answer == 'yes'}\n",
|
||
"\n",
|
||
"def perform_action(state: ApprovalState):\n",
|
||
" if state['approved']:\n",
|
||
" return {'result': f'任务已执行:{state[\"task\"]}'}\n",
|
||
" return {'result': f'任务已取消:{state[\"task\"]}'}\n",
|
||
"\n",
|
||
"builder = StateGraph(ApprovalState)\n",
|
||
"builder.add_node('request_approval', request_approval)\n",
|
||
"builder.add_node('perform_action', perform_action)\n",
|
||
"\n",
|
||
"builder.add_edge(START, 'request_approval')\n",
|
||
"builder.add_edge('request_approval', 'perform_action')\n",
|
||
"builder.add_edge('perform_action', END)\n",
|
||
"\n",
|
||
"memory = MemorySaver()\n",
|
||
"graph = builder.compile(checkpointer=memory)\n",
|
||
"\n",
|
||
"config = {'configurable': {'thread_id': 'approval-demo'}}\n",
|
||
"\n",
|
||
"first_result = graph.invoke(\n",
|
||
" {'task': '发送项目周报邮件', 'approved': False, 'result': ''},\n",
|
||
" config=config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(first_result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "07b99ca8",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这个例子第一次使用了 `interrupt`,所以需要重点理解。\n",
|
||
"\n",
|
||
"#### `ApprovalState`\n",
|
||
"状态里有三个字段:\n",
|
||
"\n",
|
||
"- `task`:准备执行的任务\n",
|
||
"- `approved`:人工是否批准\n",
|
||
"- `result`:最终执行结果\n",
|
||
"\n",
|
||
"也就是说,整张图围绕一个任务展开,先确认,再执行或取消。\n",
|
||
"\n",
|
||
"#### `request_approval` 节点\n",
|
||
"这个节点的核心代码是:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"answer = interrupt({...})\n",
|
||
"```\n",
|
||
"\n",
|
||
"当代码执行到这里时,图不会继续往下走,而是暂停下来。\n",
|
||
"\n",
|
||
"`interrupt` 里面传入的是要展示给人的信息。这里包括:\n",
|
||
"\n",
|
||
"- 问题:是否允许执行这个任务\n",
|
||
"- 任务内容:`state['task']`\n",
|
||
"- 可选项:`yes` 或 `no`\n",
|
||
"\n",
|
||
"#### 为什么 `interrupt` 后面还有 return\n",
|
||
"这行代码:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"return {'approved': answer == 'yes'}\n",
|
||
"```\n",
|
||
"\n",
|
||
"不是第一次暂停时立刻执行完成的。\n",
|
||
"\n",
|
||
"第一次运行到 `interrupt` 时,图会停住。等我们用 `Command(resume='yes')` 恢复时,`interrupt` 才会把 `'yes'` 作为返回值交给 `answer`,然后继续执行后面的 `return`。\n",
|
||
"\n",
|
||
"#### `perform_action` 节点\n",
|
||
"这个节点根据 `approved` 决定结果:\n",
|
||
"\n",
|
||
"- 如果 `approved=True`,说明人工批准,任务执行\n",
|
||
"- 如果 `approved=False`,说明人工拒绝,任务取消\n",
|
||
"\n",
|
||
"#### 为什么需要 `MemorySaver`\n",
|
||
"因为图会暂停。暂停之后,系统必须记住:\n",
|
||
"\n",
|
||
"- 当前执行到哪个节点\n",
|
||
"- 当前状态是什么\n",
|
||
"- 后面应该从哪里继续\n",
|
||
"\n",
|
||
"这些信息都需要检查点保存。`MemorySaver` 就是一个内存版检查点保存器。\n",
|
||
"\n",
|
||
"#### 第一次运行会得到什么\n",
|
||
"第一次 `invoke` 不会直接得到最终结果,而是会得到一个包含中断信息的结果。\n",
|
||
"\n",
|
||
"这表示图已经暂停,正在等待人工输入。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "185c4df9",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 5. 恢复流程:用 `Command(resume=...)` 传回人工决定\n",
|
||
"\n",
|
||
"上一步图已经暂停。现在我们模拟人工选择 `yes`,让流程继续执行。\n",
|
||
"\n",
|
||
"恢复时要使用同一个 `config`,尤其是同一个 `thread_id`。\n",
|
||
"\n",
|
||
"因为 LangGraph 需要根据 `thread_id` 找回刚才暂停的那一次运行。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "0c18357d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"second_result = graph.invoke(\n",
|
||
" Command(resume='yes'),\n",
|
||
" config=config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(second_result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "31abd5f8",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这一段代码负责恢复刚才暂停的流程。\n",
|
||
"\n",
|
||
"#### `Command(resume='yes')`\n",
|
||
"它的意思是:把人工输入 `'yes'` 传回给刚才的 `interrupt`。\n",
|
||
"\n",
|
||
"也就是说,前面这行代码:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"answer = interrupt(...)\n",
|
||
"```\n",
|
||
"\n",
|
||
"会在恢复后得到:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"answer = 'yes'\n",
|
||
"```\n",
|
||
"\n",
|
||
"然后节点继续执行:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"return {'approved': answer == 'yes'}\n",
|
||
"```\n",
|
||
"\n",
|
||
"于是 `approved` 会变成 `True`。\n",
|
||
"\n",
|
||
"#### 为什么还要传 `config=config`\n",
|
||
"因为恢复流程必须知道要恢复哪一次暂停。\n",
|
||
"\n",
|
||
"`thread_id='approval-demo'` 就像这次流程的编号。\n",
|
||
"\n",
|
||
"如果换了另一个 `thread_id`,LangGraph 就找不到刚才暂停的位置。\n",
|
||
"\n",
|
||
"#### 恢复后的执行路径\n",
|
||
"恢复后流程会继续:\n",
|
||
"\n",
|
||
"```\n",
|
||
"request_approval -> perform_action -> END\n",
|
||
"```\n",
|
||
"\n",
|
||
"最终会得到:\n",
|
||
"\n",
|
||
"```text\n",
|
||
"任务已执行:发送项目周报邮件\n",
|
||
"```\n",
|
||
"\n",
|
||
"这就是最基本的人机协作流程。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1fd00b84",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 6. 第二个例子:人工补充缺失信息\n",
|
||
"\n",
|
||
"人机协作不只是审批,也可以用来补充信息。\n",
|
||
"\n",
|
||
"例如用户想让智能体写邮件,但没有提供收件人。\n",
|
||
"\n",
|
||
"流程可以这样设计:\n",
|
||
"\n",
|
||
"```\n",
|
||
"START -> check_info -> draft_email -> END\n",
|
||
"```\n",
|
||
"\n",
|
||
"如果缺少收件人,`check_info` 就暂停,让用户补充。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "4000512e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing_extensions import TypedDict\n",
|
||
"from langgraph.graph import StateGraph, START, END\n",
|
||
"from langgraph.types import interrupt, Command\n",
|
||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||
"\n",
|
||
"class EmailState(TypedDict):\n",
|
||
" topic: str\n",
|
||
" recipient: str\n",
|
||
" email_draft: str\n",
|
||
"\n",
|
||
"def check_info(state: EmailState):\n",
|
||
" if state['recipient']:\n",
|
||
" return {}\n",
|
||
"\n",
|
||
" recipient = interrupt({\n",
|
||
" 'question': '请补充邮件收件人',\n",
|
||
" 'current_topic': state['topic']\n",
|
||
" })\n",
|
||
" return {'recipient': recipient}\n",
|
||
"\n",
|
||
"def draft_email(state: EmailState):\n",
|
||
" draft = f'''收件人:{state[\"recipient\"]}\n",
|
||
"主题:{state[\"topic\"]}\n",
|
||
"\n",
|
||
"你好,\n",
|
||
"这里是一封关于“{state[\"topic\"]}”的邮件草稿。\n",
|
||
"请根据具体情况补充正文细节。\n",
|
||
"'''\n",
|
||
" return {'email_draft': draft}\n",
|
||
"\n",
|
||
"builder = StateGraph(EmailState)\n",
|
||
"builder.add_node('check_info', check_info)\n",
|
||
"builder.add_node('draft_email', draft_email)\n",
|
||
"\n",
|
||
"builder.add_edge(START, 'check_info')\n",
|
||
"builder.add_edge('check_info', 'draft_email')\n",
|
||
"builder.add_edge('draft_email', END)\n",
|
||
"\n",
|
||
"email_graph = builder.compile(checkpointer=MemorySaver())\n",
|
||
"email_config = {'configurable': {'thread_id': 'email-demo'}}\n",
|
||
"\n",
|
||
"first_result = email_graph.invoke(\n",
|
||
" {'topic': '下周项目会议安排', 'recipient': '', 'email_draft': ''},\n",
|
||
" config=email_config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(first_result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "84056ad6",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这个例子展示了人机协作的另一种常见用途:补充缺失信息。\n",
|
||
"\n",
|
||
"#### `EmailState`\n",
|
||
"状态包含三个字段:\n",
|
||
"\n",
|
||
"- `topic`:邮件主题\n",
|
||
"- `recipient`:邮件收件人\n",
|
||
"- `email_draft`:生成的邮件草稿\n",
|
||
"\n",
|
||
"#### `check_info` 节点\n",
|
||
"这个节点负责检查信息是否完整。\n",
|
||
"\n",
|
||
"如果 `recipient` 已经有值,就返回 `{}`,表示不需要修改状态。\n",
|
||
"\n",
|
||
"如果 `recipient` 为空,就执行:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"recipient = interrupt({...})\n",
|
||
"```\n",
|
||
"\n",
|
||
"这会暂停流程,并向用户请求收件人信息。\n",
|
||
"\n",
|
||
"#### 为什么这里不直接报错\n",
|
||
"在真实智能体中,缺少信息不一定是错误。\n",
|
||
"\n",
|
||
"更好的做法是:停下来问人。\n",
|
||
"\n",
|
||
"这就是人机协作比普通异常处理更适合智能体流程的地方。\n",
|
||
"\n",
|
||
"#### `draft_email` 节点\n",
|
||
"这个节点依赖完整信息生成邮件草稿。\n",
|
||
"\n",
|
||
"它会读取:\n",
|
||
"\n",
|
||
"- `state['recipient']`\n",
|
||
"- `state['topic']`\n",
|
||
"\n",
|
||
"然后写入 `email_draft`。\n",
|
||
"\n",
|
||
"#### 第一次运行为什么会暂停\n",
|
||
"因为初始状态中:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"'recipient': ''\n",
|
||
"```\n",
|
||
"\n",
|
||
"所以流程会停在 `check_info`,等待人补充收件人。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "cf5e9b4b",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 7. 恢复并补充收件人\n",
|
||
"\n",
|
||
"现在模拟人工输入收件人,例如:`张经理`。\n",
|
||
"\n",
|
||
"恢复后,流程会继续进入 `draft_email`,并生成邮件草稿。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "081941b0",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"final_result = email_graph.invoke(\n",
|
||
" Command(resume='张经理'),\n",
|
||
" config=email_config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(final_result['email_draft'])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "8c7dade8",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这一段代码把人工补充的信息传回给图。\n",
|
||
"\n",
|
||
"#### `Command(resume='张经理')`\n",
|
||
"它会让刚才暂停的 `interrupt` 返回 `'张经理'`。\n",
|
||
"\n",
|
||
"于是这行代码:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"recipient = interrupt(...)\n",
|
||
"```\n",
|
||
"\n",
|
||
"恢复后就相当于:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"recipient = '张经理'\n",
|
||
"```\n",
|
||
"\n",
|
||
"然后节点返回:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"return {'recipient': recipient}\n",
|
||
"```\n",
|
||
"\n",
|
||
"状态中的收件人就被补上了。\n",
|
||
"\n",
|
||
"#### 后续为什么能生成邮件草稿\n",
|
||
"恢复后,图会继续执行 `draft_email`。\n",
|
||
"\n",
|
||
"这时状态已经包含:\n",
|
||
"\n",
|
||
"- `topic='下周项目会议安排'`\n",
|
||
"- `recipient='张经理'`\n",
|
||
"\n",
|
||
"所以就可以生成完整的邮件草稿。\n",
|
||
"\n",
|
||
"#### 这个例子的重点\n",
|
||
"人机协作不是只能做‘同意/不同意’。\n",
|
||
"\n",
|
||
"它也可以用来让人补充缺失信息,然后让图继续完成后续任务。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6090d74f",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 8. 第三个例子:人工修改 AI 草稿\n",
|
||
"\n",
|
||
"在很多真实场景中,AI 不一定直接执行最终结果,而是先生成一个草稿,让人修改或确认。\n",
|
||
"\n",
|
||
"例如:\n",
|
||
"\n",
|
||
"- AI 生成邮件草稿,人修改后再发送\n",
|
||
"- AI 生成计划,人确认后再执行\n",
|
||
"- AI 生成 SQL,人审核后再运行\n",
|
||
"\n",
|
||
"下面我们用一个简单的任务计划示例。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "88a81e02",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from typing_extensions import TypedDict\n",
|
||
"from langgraph.graph import StateGraph, START, END\n",
|
||
"from langgraph.types import interrupt, Command\n",
|
||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||
"\n",
|
||
"class PlanState(TypedDict):\n",
|
||
" goal: str\n",
|
||
" draft_plan: str\n",
|
||
" final_plan: str\n",
|
||
"\n",
|
||
"def create_draft_plan(state: PlanState):\n",
|
||
" draft = f'''目标:{state[\"goal\"]}\n",
|
||
"步骤 1:明确当前需求\n",
|
||
"步骤 2:拆分任务并安排执行顺序\n",
|
||
"步骤 3:检查结果并总结经验\n",
|
||
"'''\n",
|
||
" return {'draft_plan': draft}\n",
|
||
"\n",
|
||
"def review_plan(state: PlanState):\n",
|
||
" revised_plan = interrupt({\n",
|
||
" 'question': '请审核并修改计划草稿',\n",
|
||
" 'draft_plan': state['draft_plan']\n",
|
||
" })\n",
|
||
" return {'final_plan': revised_plan}\n",
|
||
"\n",
|
||
"builder = StateGraph(PlanState)\n",
|
||
"builder.add_node('create_draft_plan', create_draft_plan)\n",
|
||
"builder.add_node('review_plan', review_plan)\n",
|
||
"\n",
|
||
"builder.add_edge(START, 'create_draft_plan')\n",
|
||
"builder.add_edge('create_draft_plan', 'review_plan')\n",
|
||
"builder.add_edge('review_plan', END)\n",
|
||
"\n",
|
||
"plan_graph = builder.compile(checkpointer=MemorySaver())\n",
|
||
"plan_config = {'configurable': {'thread_id': 'plan-demo'}}\n",
|
||
"\n",
|
||
"review_result = plan_graph.invoke(\n",
|
||
" {'goal': '准备一次 LangGraph 分享', 'draft_plan': '', 'final_plan': ''},\n",
|
||
" config=plan_config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(review_result)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "88b97a79",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这个例子展示的是‘AI 先生成草稿,人再修改确认’的模式。\n",
|
||
"\n",
|
||
"#### `PlanState`\n",
|
||
"状态中有三个字段:\n",
|
||
"\n",
|
||
"- `goal`:目标\n",
|
||
"- `draft_plan`:AI 或程序生成的草稿计划\n",
|
||
"- `final_plan`:人工确认后的最终计划\n",
|
||
"\n",
|
||
"#### `create_draft_plan` 节点\n",
|
||
"这个节点根据目标生成一个初步计划。\n",
|
||
"\n",
|
||
"这里为了教学简单,计划是用模板字符串生成的。\n",
|
||
"\n",
|
||
"在真实项目中,这一步也可以换成大模型生成。\n",
|
||
"\n",
|
||
"#### `review_plan` 节点\n",
|
||
"这个节点调用 `interrupt`,把草稿交给人审核。\n",
|
||
"\n",
|
||
"传给人的信息包括:\n",
|
||
"\n",
|
||
"- 问题:请审核并修改计划草稿\n",
|
||
"- 当前草稿:`state['draft_plan']`\n",
|
||
"\n",
|
||
"人可以直接接受,也可以修改后再返回。\n",
|
||
"\n",
|
||
"#### 为什么返回 `final_plan`\n",
|
||
"AI 生成的是草稿,不一定等于最终结果。\n",
|
||
"\n",
|
||
"人工修改后的内容才写入 `final_plan`。\n",
|
||
"\n",
|
||
"这能清楚地区分:\n",
|
||
"\n",
|
||
"- AI 草稿是什么\n",
|
||
"- 人工最终确认的版本是什么\n",
|
||
"\n",
|
||
"这在真实工作流中非常重要,因为最终责任通常属于人工确认后的结果。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f0da01c5",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 9. 恢复并提交人工修改后的计划\n",
|
||
"\n",
|
||
"现在模拟人工审核后,提交一个修改版计划。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "31d862b2",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"human_revised_plan = '''目标:准备一次 LangGraph 分享\n",
|
||
"步骤 1:先介绍图结构、状态和边的概念\n",
|
||
"步骤 2:演示一个最小可运行的 LangGraph 示例\n",
|
||
"步骤 3:重点讲解人机协作中的 interrupt 和 resume\n",
|
||
"步骤 4:最后总结适合人机协作的真实业务场景\n",
|
||
"'''\n",
|
||
"\n",
|
||
"final_plan_result = plan_graph.invoke(\n",
|
||
" Command(resume=human_revised_plan),\n",
|
||
" config=plan_config\n",
|
||
")\n",
|
||
"\n",
|
||
"print(final_plan_result['final_plan'])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 代码解释\n",
|
||
"\n",
|
||
"这段代码模拟人工修改草稿后,把最终版本交回给图。\n",
|
||
"\n",
|
||
"#### `human_revised_plan`\n",
|
||
"这是人工修改后的计划。\n",
|
||
"\n",
|
||
"它和原始草稿相比,更具体,也更贴近分享主题。\n",
|
||
"\n",
|
||
"#### `Command(resume=human_revised_plan)`\n",
|
||
"这表示把人工修改后的计划传回给 `interrupt`。\n",
|
||
"\n",
|
||
"恢复后,`review_plan` 节点会继续执行:\n",
|
||
"\n",
|
||
"```python\n",
|
||
"return {'final_plan': revised_plan}\n",
|
||
"```\n",
|
||
"\n",
|
||
"于是最终状态中就有了人工确认后的 `final_plan`。\n",
|
||
"\n",
|
||
"#### 这个模式有什么用\n",
|
||
"这类流程非常适合高风险或高要求任务。\n",
|
||
"\n",
|
||
"例如:\n",
|
||
"\n",
|
||
"- AI 生成合同条款,人审核后确认\n",
|
||
"- AI 生成客户邮件,人修改后发送\n",
|
||
"- AI 生成执行计划,人确认后落地\n",
|
||
"\n",
|
||
"核心思想是:AI 负责初稿,人负责把关。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 10. 人机协作流程的关键点\n",
|
||
"\n",
|
||
"设计人机协作流程时,要特别注意下面几点。\n",
|
||
"\n",
|
||
"### 10.1 暂停点要放在关键位置\n",
|
||
"不是每一步都需要人确认。\n",
|
||
"\n",
|
||
"一般只在这些地方暂停:\n",
|
||
"\n",
|
||
"- 信息缺失\n",
|
||
"- 风险较高\n",
|
||
"- 需要业务判断\n",
|
||
"- 执行前需要最终确认\n",
|
||
"\n",
|
||
"### 10.2 给人的信息要清楚\n",
|
||
"`interrupt` 传出的内容应该让人一眼看懂:\n",
|
||
"\n",
|
||
"- 当前要做什么\n",
|
||
"- 为什么要暂停\n",
|
||
"- 希望人提供什么\n",
|
||
"- 可选项有哪些\n",
|
||
"\n",
|
||
"### 10.3 恢复时必须使用同一个线程\n",
|
||
"恢复流程时,必须使用同一个 `thread_id`。\n",
|
||
"\n",
|
||
"否则图找不到之前暂停的位置。\n",
|
||
"\n",
|
||
"### 10.4 不要滥用人工确认\n",
|
||
"如果每一步都让人确认,智能体就会变得很繁琐。\n",
|
||
"\n",
|
||
"合理做法是:低风险步骤自动执行,高风险步骤人工把关。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 11. 人机协作和普通 input() 的区别\n",
|
||
"\n",
|
||
"你可能会问:为什么不直接用 Python 的 `input()`?\n",
|
||
"\n",
|
||
"因为 LangGraph 的 `interrupt` 不只是简单输入,它更适合智能体工作流。\n",
|
||
"\n",
|
||
"| 对比项 | `input()` | `interrupt` |\n",
|
||
"| --- | --- | --- |\n",
|
||
"| 是否适合 notebook / 服务端流程 | 不太适合 | 更适合 |\n",
|
||
"| 是否能保存暂停状态 | 不能 | 可以,依赖检查点 |\n",
|
||
"| 是否能恢复到图中原位置 | 不能 | 可以 |\n",
|
||
"| 是否适合多用户多会话 | 弱 | 强,配合 `thread_id` |\n",
|
||
"| 是否和 LangGraph 状态集成 | 否 | 是 |\n",
|
||
"\n",
|
||
"所以在人机协作智能体中,推荐使用 `interrupt`,而不是普通 `input()`。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 12. 本节小结\n",
|
||
"\n",
|
||
"本节最重要的内容有五点:\n",
|
||
"\n",
|
||
"1. **人机协作是让人在关键节点参与智能体流程**\n",
|
||
"2. **`interrupt` 用于暂停流程,并把需要人工处理的信息交出去**\n",
|
||
"3. **`Command(resume=...)` 用于把人工输入传回图中,并恢复执行**\n",
|
||
"4. **暂停和恢复必须依赖检查点,通常会配合 `MemorySaver` 使用**\n",
|
||
"5. **人工介入适合审批、补充信息、修改草稿和高风险操作确认**\n",
|
||
"\n",
|
||
"掌握人机协作后,智能体就不再只是自动执行脚本,而可以变成一个真正能和人配合工作的流程系统。"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## 13. 本节练习\n",
|
||
"\n",
|
||
"1. 修改第一个示例,把任务改成‘删除临时文件’,观察批准和拒绝的结果\n",
|
||
"2. 修改第二个示例,让缺少邮件主题时也触发一次人工补充\n",
|
||
"3. 修改第三个示例,让人工可以输入 `accept` 表示直接接受草稿\n",
|
||
"4. 思考:哪些智能体操作必须经过人工确认?\n",
|
||
"5. 思考:如果一个系统有多个用户同时使用,为什么 `thread_id` 很重要?"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"language_info": {
|
||
"codemirror_mode": {
|
||
"name": "ipython",
|
||
"version": 3
|
||
},
|
||
"file_extension": ".py",
|
||
"mimetype": "text/x-python",
|
||
"name": "python",
|
||
"nbconvert_exporter": "python",
|
||
"pygments_lexer": "ipython3",
|
||
"version": "3.14.4"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|