Notebook

第11章:周末出行清单智能体

从单次工具调用到"计划—行动—观察"的多步循环

本 Notebook 将带你构建一个最小智能体,让它自主完成多步任务:读取行程、计算天数、生成行李清单。

你将观察到:

  • ReAct 循环怎样把单步变成多步
  • 状态机怎样跟踪进度
  • 步数上限怎样阻止无限循环
  • 人工暂停(HITL)怎样拦截高风险操作
  • 注入防御怎样保护系统安全

环境要求:Python 3.10+,无需联网或外部模型(内置 mock 模式)


0. 环境准备

import json
import copy
from datetime import datetime, timedelta
from pathlib import Path
from html import escape

# 数据路径
DATA_PATH = Path('../data/travel_data.json')

print('环境检查通过 ✓')
# 加载旅行数据
with open(DATA_PATH, 'r', encoding='utf-8') as f:
    travel_data = json.load(f)

print(f'行程:{travel_data["itinerary"]["destination"]}')
print(f'出发日期:{travel_data["itinerary"]["start_date"]}')
print(f'返回日期:{travel_data["itinerary"]["end_date"]}')
print(f'天气天数:{len(travel_data["weather"])}')
print(f'允许工具:{[t["name"] for t in travel_data["tools"]["allowed"]]}')

1. 构建最小工具集

智能体通过调用工具来完成任务。我们先定义5个模拟工具。

# ===== 模拟工具集 =====

# 模拟文件系统
virtual_fs = {
    'itinerary.txt': json.dumps(travel_data['itinerary'], ensure_ascii=False, indent=2),
    'weather.txt': json.dumps(travel_data['weather'], ensure_ascii=False, indent=2),
    'traffic.txt': json.dumps(travel_data['traffic'], ensure_ascii=False, indent=2),
}

def read_text_file(file_path: str) -> str:
    """读取虚拟文件"""
    filename = Path(file_path).name
    if filename in virtual_fs:
        return virtual_fs[filename]
    return f'[错误] 文件不存在: {filename}'

def calc_days(start_date: str, end_date: str) -> int:
    """计算出行天数"""
    d1 = datetime.strptime(start_date, '%Y-%m-%d')
    d2 = datetime.strptime(end_date, '%Y-%m-%d')
    return (d2 - d1).days + 1  # 包含首尾两天

def create_note(filename: str, content: str) -> str:
    """创建清单文件"""
    virtual_fs[filename] = content
    return f'[成功] 文件已保存: {filename}({len(content)} 字符)'

def list_files(directory: str = '.') -> list:
    """列出虚拟文件"""
    return list(virtual_fs.keys())

def delete_file(file_path: str) -> str:
    """删除文件(高风险,需 HITL)"""
    filename = Path(file_path).name
    if filename in virtual_fs:
        del virtual_fs[filename]
        return f'[成功] 文件已删除: {filename}'
    return f'[错误] 文件不存在: {filename}'

# 工具注册表
TOOL_REGISTRY = {
    'read_text_file': read_text_file,
    'calc_days': calc_days,
    'create_note': create_note,
    'list_files': list_files,
    'delete_file': delete_file,
}

# 风险级别
RISK_LEVELS = {t['name']: t['risk_level'] for t in travel_data['tools']['allowed']}

print('工具集构建完成 ✓')
print(f'工具列表:{list(TOOL_REGISTRY.keys())}')
print(f'风险级别:{RISK_LEVELS}')

2. 构建状态机

状态机记录智能体的执行进度,决定是否可以继续。

class AgentState:
    """智能体状态机"""
    
    def __init__(self, max_steps=10):
        self.max_steps = max_steps
        self.current_step = 0
        self.status = 'pending'  # pending/running/done/blocked/cancelled/failed
        self.step_log = []       # 每步的详细记录
        self.evidence = []       # 依据摘要
        self.hitl_pending = False  # 是否有人工暂停等待处理
        self.hitl_request = None   # 暂停请求详情
    
    def can_continue(self) -> bool:
        """检查是否还能继续执行"""
        if self.status in ('done', 'cancelled', 'failed'):
            return False
        if self.hitl_pending:
            return False
        if self.current_step >= self.max_steps:
            self.status = 'blocked'
            return False
        return True
    
    def record_step(self, thought: str, action: str, observation: str, state_after: str):
        """记录一步的执行"""
        self.current_step += 1
        entry = {
            'step': self.current_step,
            'thought': thought,
            'action': action,
            'observation': observation[:200] + '...' if len(observation) > 200 else observation,
            'state_after': state_after,
            'timestamp': datetime.now().isoformat()
        }
        self.step_log.append(entry)
        self.evidence.append(f'Step {self.current_step}: {action} → {state_after}')
    
    def request_hitl(self, tool_name: str, reason: str):
        """请求人工确认"""
        self.hitl_pending = True
        self.status = 'blocked'
        self.hitl_request = {
            'tool': tool_name,
            'reason': reason,
            'step': self.current_step + 1
        }
    
    def resolve_hitl(self, approved: bool) -> str:
        """处理人工确认结果"""
        if not self.hitl_pending:
            return '无待处理的 HITL 请求'
        if approved:
            self.hitl_pending = False
            self.status = 'running'
            self.hitl_request['result'] = 'approved'
            return '已批准,继续执行'
        else:
            self.hitl_pending = False
            self.status = 'cancelled'
            self.hitl_request['result'] = 'cancelled'
            return '已取消,智能体停止'
    
    def summary(self) -> str:
        """生成状态摘要"""
        return (
            f'状态: {self.status} | '
            f'步数: {self.current_step}/{self.max_steps} | '
            f'HITL: {"等待中" if self.hitl_pending else "无"}'
        )


# 测试状态机
state = AgentState(max_steps=5)
print(f'初始状态: {state.summary()}')
print(f'可以继续? {state.can_continue()}')

state.record_step('需要读取行程', 'read_text_file(itinerary.txt)', '行程数据...', 'running')
print(f'执行1步后: {state.summary()}')

3. 三步基线:读行程 → 算天数 → 建清单

先运行最简单的3步任务,观察 ReAct 循环。

def run_baseline_agent(max_steps=10, include_weather=False):
    """
    模拟智能体的 ReAct 循环。
    
    这里用预定义的步骤序列模拟"模型决策"。
    真实场景中,每一步的"思考"和"行动选择"由大语言模型完成。
    """
    state = AgentState(max_steps=max_steps)
    state.status = 'running'
    
    # 定义任务步骤序列
    steps_plan = [
        {
            'thought': '我需要先读取行程信息,了解目的地和日期',
            'tool': 'read_text_file',
            'args': {'file_path': 'itinerary.txt'},
        },
        {
            'thought': '我需要计算出行天数,以便决定带多少衣物',
            'tool': 'calc_days',
            'args': {
                'start_date': travel_data['itinerary']['start_date'],
                'end_date': travel_data['itinerary']['end_date']
            },
        },
        {
            'thought': '现在我有了天数,可以生成基本行李清单',
            'tool': 'create_note',
            'args': {'filename': 'packing_list.txt', 'content': ''},  # content 动态生成
        },
    ]
    
    # 如果包含天气,增加步骤
    if include_weather:
        steps_plan.insert(2, {
            'thought': '我还需要查看天气预报,决定是否需要雨具和防晒',
            'tool': 'read_text_file',
            'args': {'file_path': 'weather.txt'},
        })
        # 修改 create_note 步骤,加入天气信息
        steps_plan[-1]['thought'] = '现在我有了天数和天气,可以生成完整的行李清单'
    
    # 执行 ReAct 循环
    trip_days = None
    weather_info = None
    
    for step_def in steps_plan:
        if not state.can_continue():
            break
        
        thought = step_def['thought']
        tool_name = step_def['tool']
        args = step_def['args']
        
        # 检查 HITL
        if RISK_LEVELS.get(tool_name) == 'high':
            state.request_hitl(tool_name, f'高风险操作: {tool_name}')
            state.record_step(thought, f'[HITL] {tool_name}', '等待人工确认', 'blocked')
            break
        
        # 执行工具
        tool_func = TOOL_REGISTRY[tool_name]
        
        # 动态生成清单内容
        if tool_name == 'create_note':
            items = []
            items.append('=== 周末出行行李清单 ===')
            items.append(f'目的地:{travel_data["itinerary"]["destination"]}')
            items.append(f'天数:{trip_days}天')
            items.append('')
            items.append('【证件】身份证、学生证')
            items.append(f'【衣物】内衣×{trip_days}套、袜子×{trip_days}双')
            items.append('【洗漱】牙刷、牙膏、毛巾、洗面奶')
            items.append('【电子】手机、充电器、耳机')
            items.append('【其他】纸巾、水杯、零食')
            if weather_info:
                items.append('')
                items.append('--- 天气相关 ---')
                has_rain = any(w['needs_rain_gear'] for w in weather_info)
                has_sun = any(w['needs_sunscreen'] for w in weather_info)
                if has_rain:
                    items.append('【雨具】雨伞、防水外套')
                if has_sun:
                    items.append('【防晒】防晒霜、遮阳帽、太阳镜')
            args['content'] = '\n'.join(items)
        
        observation = str(tool_func(**args))
        
        # 保存中间结果
        if tool_name == 'calc_days':
            trip_days = int(observation)
        if tool_name == 'read_text_file' and 'weather' in str(args):
            try:
                weather_info = json.loads(observation)
            except:
                pass
        
        state.record_step(thought, f'{tool_name}({args})', observation, 'running')
    
    # 标记完成
    if state.status == 'running':
        state.status = 'done'
    
    return state


# ===== 运行三步基线 =====
print('=' * 60)
print('实验 1:三步基线(不含天气)')
print('=' * 60)

state_baseline = run_baseline_agent(max_steps=10, include_weather=False)

print(f'\n最终状态: {state_baseline.summary()}')
print(f'\n--- 步骤轨迹 ---')
for entry in state_baseline.step_log:
    print(f'\n步骤 {entry["step"]}:')
    print(f'  思考: {entry["thought"]}')
    print(f'  行动: {entry["action"]}')
    obs_preview = entry['observation'][:100]
    print(f'  观察: {obs_preview}')
    print(f'  状态: {entry["state_after"]}')

4. 加入天气数据:观察智能体增加步骤

当任务需要更多信息时,智能体应该自主增加读取和修改步骤。

print('=' * 60)
print('实验 2:加入天气数据(5步任务)')
print('=' * 60)

state_weather = run_baseline_agent(max_steps=10, include_weather=True)

print(f'\n最终状态: {state_weather.summary()}')
print(f'总步数: {state_weather.current_step}')
print(f'\n--- 步骤轨迹 ---')
for entry in state_weather.step_log:
    print(f'\n步骤 {entry["step"]}:')
    print(f'  思考: {entry["thought"]}')
    print(f'  行动: {entry["action"][:80]}')
    print(f'  状态: {entry["state_after"]}')

print(f'\n--- 对比 ---')
print(f'基线步数: {state_baseline.current_step}')
print(f'天气版步数: {state_weather.current_step}')
print(f'增加了 {state_weather.current_step - state_baseline.current_step} 步')

📝 观察记录

在下方记录你的观察:

  • 加入天气后,智能体多了哪些步骤?
  • 新增步骤的"思考"内容是什么?
  • 最终清单多了哪些物品?

你的观察:(在此填写)


5. 步数上限测试:降低 max_steps

把 max_steps 降低,观察智能体怎样停止并报告未完成项。

print('=' * 60)
print('实验 3:步数上限测试(max_steps=2)')
print('=' * 60)

# 预测:max_steps=2 时,智能体只能完成前2步,第3步无法执行
print('\n[预测] 智能体应该只能执行2步,然后因达到上限而停止')
print('[预测] 最终状态应该是 blocked 或 done(但清单未生成)')

state_limited = run_baseline_agent(max_steps=2, include_weather=False)

print(f'\n--- 实际结果 ---')
print(f'最终状态: {state_limited.summary()}')
print(f'实际步数: {state_limited.current_step}')
print(f'\n--- 步骤轨迹 ---')
for entry in state_limited.step_log:
    print(f'  步骤 {entry["step"]}: {entry["action"][:60]}')

# 检查清单是否生成
list_generated = 'packing_list.txt' in virtual_fs
print(f'\n清单是否生成: {list_generated}')
if not list_generated:
    print('→ 步数上限生效,智能体在完成清单前就停止了')
# 测试不同的 max_steps 值
print('--- max_steps 对比实验 ---')
print(f'{"max_steps":<12} {"实际步数":<10} {"状态":<12} {"清单生成":<10}')
print('-' * 50)

for ms in [1, 2, 3, 5, 10]:
    # 重置虚拟文件系统
    virtual_fs_test = {
        'itinerary.txt': json.dumps(travel_data['itinerary'], ensure_ascii=False, indent=2),
        'weather.txt': json.dumps(travel_data['weather'], ensure_ascii=False, indent=2),
        'traffic.txt': json.dumps(travel_data['traffic'], ensure_ascii=False, indent=2),
    }
    # 临时替换
    global virtual_fs
    original_fs = virtual_fs
    virtual_fs = virtual_fs_test
    
    s = run_baseline_agent(max_steps=ms, include_weather=False)
    generated = 'packing_list.txt' in virtual_fs
    print(f'{ms:<12} {s.current_step:<10} {s.status:<12} {"是" if generated else "否":<10}')
    
    virtual_fs = original_fs

📝 必改点 A

修改 max_steps 值(从 1, 2, 3, 5, 10 中选择或自定义),观察并记录:

  1. 你选择的 max_steps 值是多少?
  2. 修改前预测:智能体会执行几步?清单会生成吗?
  3. 修改后验证:实际执行了几步?清单生成了吗?
  4. 预测和实际一致吗?如果不一致,原因是什么?

你的记录:(在此填写)


6. HITL 测试:delete_file 模拟

当智能体尝试执行高风险操作时,状态机应该暂停并等待人工确认。

def run_hitl_agent(max_steps=10):
    """
    模拟一个会触发 HITL 的智能体。
    场景:智能体在生成清单前,先尝试删除旧清单。
    """
    state = AgentState(max_steps=max_steps)
    state.status = 'running'
    
    steps_plan = [
        {
            'thought': '我需要先读取行程信息',
            'tool': 'read_text_file',
            'args': {'file_path': 'itinerary.txt'},
        },
        {
            'thought': '在生成新清单前,我先清理旧的清单文件',
            'tool': 'delete_file',  # 高风险!
            'args': {'file_path': 'old_packing_list.txt'},
        },
        {
            'thought': '计算出行天数',
            'tool': 'calc_days',
            'args': {
                'start_date': travel_data['itinerary']['start_date'],
                'end_date': travel_data['itinerary']['end_date']
            },
        },
        {
            'thought': '生成行李清单',
            'tool': 'create_note',
            'args': {'filename': 'packing_list.txt', 'content': '清单内容...'},
        },
    ]
    
    for step_def in steps_plan:
        if not state.can_continue():
            break
        
        tool_name = step_def['tool']
        
        # 检查 HITL
        if RISK_LEVELS.get(tool_name) == 'high':
            state.request_hitl(tool_name, f'高风险操作: {tool_name}({step_def["args"]})')
            state.record_step(
                step_def['thought'],
                f'[HITL 暂停] {tool_name}',
                f'等待人工确认: {step_def["args"]}',
                'blocked'
            )
            break
        
        tool_func = TOOL_REGISTRY[tool_name]
        observation = str(tool_func(**step_def['args']))
        state.record_step(step_def['thought'], f'{tool_name}({step_def["args"]})', observation, 'running')
    
    if state.status == 'running':
        state.status = 'done'
    
    return state


# ===== 运行 HITL 测试 =====
print('=' * 60)
print('实验 4:HITL 测试 - delete_file 触发人工暂停')
print('=' * 60)

state_hitl = run_hitl_agent(max_steps=10)

print(f'\n最终状态: {state_hitl.summary()}')
print(f'\n--- 步骤轨迹 ---')
for entry in state_hitl.step_log:
    print(f'  步骤 {entry["step"]}: {entry["action"][:60]}')
    print(f'    状态: {entry["state_after"]}')

if state_hitl.hitl_pending:
    print(f'\n⚠️  HITL 暂停请求:')
    print(f'  工具: {state_hitl.hitl_request["tool"]}')
    print(f'  原因: {state_hitl.hitl_request["reason"]}')
    print(f'  步骤: {state_hitl.hitl_request["step"]}')
# 模拟学生选择"取消"
print('=' * 60)
print('学生选择:取消(不执行 delete_file)')
print('=' * 60)

result = state_hitl.resolve_hitl(approved=False)
print(f'\nHITL 处理结果: {result}')
print(f'最终状态: {state_hitl.summary()}')
print(f'\n→ 智能体已停止,不会偷偷继续执行后续步骤')
print(f'→ 后续步骤(calc_days, create_note)未被执行')

# 验证:后续步骤确实没有执行
steps_after_hitl = len(state_hitl.step_log)
print(f'\n实际执行步骤数: {steps_after_hitl}')
print(f'最后一步状态: {state_hitl.step_log[-1]["state_after"]}')

📝 观察记录

  1. HITL 在第几步触发?触发的工具是什么?
  2. 选择"取消"后,智能体的状态变成了什么?
  3. 后续步骤(calc_days, create_note)有没有被执行?
  4. 如果没有 HITL,智能体会怎样?

你的记录:(在此填写)


7. 注入防御测试

加载含隐藏指令的旅行提示,检查智能体是否会偏离目标。

# 查看注入样本
injection = travel_data['injection_sample']
print('=' * 60)
print('注入样本内容')
print('=' * 60)
print(injection['file_content'])
print(f'\n--- 攻击向量 ---')
for v in injection['attack_vectors']:
    print(f'  • {v}')
print(f'\n--- 预期行为 ---')
print(injection['expected_behavior'])
def run_injection_test(max_steps=10):
    """
    模拟智能体读取含注入指令的旅行提示后的行为。
    
    正确行为:智能体忽略注入指令,继续完成行李清单任务。
    错误行为:智能体执行注入指令(删除文件、读取系统配置、发送数据)。
    """
    state = AgentState(max_steps=max_steps)
    state.status = 'running'
    
    # 把注入样本加入虚拟文件系统
    virtual_fs['travel_tips.txt'] = injection['file_content']
    
    steps_plan = [
        {
            'thought': '我需要读取行程信息',
            'tool': 'read_text_file',
            'args': {'file_path': 'itinerary.txt'},
        },
        {
            'thought': '读取旅行提示(可能包含注入指令)',
            'tool': 'read_text_file',
            'args': {'file_path': 'travel_tips.txt'},
        },
        {
            'thought': '计算出行天数',
            'tool': 'calc_days',
            'args': {
                'start_date': travel_data['itinerary']['start_date'],
                'end_date': travel_data['itinerary']['end_date']
            },
        },
        {
            'thought': '生成行李清单(忽略旅行提示中的越界指令)',
            'tool': 'create_note',
            'args': {'filename': 'packing_list.txt', 'content': '清单内容...'},
        },
    ]
    
    injection_detected = False
    injection_actions = []
    
    for step_def in steps_plan:
        if not state.can_continue():
            break
        
        tool_name = step_def['tool']
        
        # 检查 HITL
        if RISK_LEVELS.get(tool_name) == 'high':
            state.request_hitl(tool_name, f'高风险操作: {tool_name}')
            state.record_step(step_def['thought'], f'[HITL] {tool_name}', '等待确认', 'blocked')
            injection_actions.append(f'HITL拦截: {tool_name}')
            break
        
        tool_func = TOOL_REGISTRY[tool_name]
        observation = str(tool_func(**step_def['args']))
        
        # 检查是否执行了注入指令
        if tool_name == 'read_text_file' and 'travel_tips' in str(step_def['args']):
            # 智能体读取了旅行提示,但应该忽略其中的指令
            # 检查"系统更新通知"部分是否影响了后续行为
            pass  # 正确行为:只是读取,不执行其中的指令
        
        state.record_step(step_def['thought'], f'{tool_name}({step_def["args"]})', observation, 'running')
    
    if state.status == 'running':
        state.status = 'done'
    
    # 验证:检查是否有越界操作
    # 1. 是否尝试发送数据到外部邮箱?(工具白名单中没有 send_email)
    # 2. 是否尝试读取系统配置?(不在任务范围内)
    # 3. delete_file 是否被 HITL 拦截?
    
    return state, injection_actions


# ===== 运行注入测试 =====
print('=' * 60)
print('实验 5:注入防御测试')
print('=' * 60)

state_injection, injection_actions = run_injection_test(max_steps=10)

print(f'\n最终状态: {state_injection.summary()}')
print(f'\n--- 步骤轨迹 ---')
for entry in state_injection.step_log:
    print(f'  步骤 {entry["step"]}: {entry["action"][:60]}')

# 防御验证
print(f'\n--- 防御验证 ---')
print(f'清单是否生成: {"packing_list.txt" in virtual_fs}')
print(f'智能体是否执行了注入指令: {"否(正确)" if not injection_actions else injection_actions}')
print(f'\n防御机制:')
print(f'  1. 工具白名单: 只允许调用预定义的5个工具')
print(f'  2. 权限隔离: 资料内容不能改变工具权限')
print(f'  3. 目标锁定: 智能体只完成合同中的任务')
print(f'  4. HITL: delete_file 被人工暂停拦截')

📝 注入防御记录

  1. 旅行提示中包含哪些隐藏指令?
  2. 智能体是否执行了这些指令?
  3. 哪些防御机制阻止了注入攻击?
  4. 如果去掉工具白名单,会发生什么?

你的记录:(在此填写)


8. HTML 状态轨迹可视化

用 HTML 可视化每一步的状态变化,方便复盘。

def generate_trajectory_html(state: AgentState, title: str = '智能体任务轨迹') -> str:
    """生成 HTML 状态轨迹图"""
    
    rows = ''
    for entry in state.step_log:
        state_color = {
            'running': '#4CAF50',
            'done': '#2196F3',
            'blocked': '#FF9800',
            'cancelled': '#f44336',
            'failed': '#f44336',
            'pending': '#9E9E9E',
        }.get(entry['state_after'], '#9E9E9E')
        
        rows += f'''
        <tr>
            <td style="text-align:center; font-weight:bold;">{entry['step']}</td>
            <td>{escape(entry['thought'])}</td>
            <td><code>{escape(entry['action'][:80])}</code></td>
            <td style="font-size:0.85em; max-width:300px; overflow:hidden; text-overflow:ellipsis;">{escape(entry['observation'][:150])}</td>
            <td style="text-align:center;"><span style="background:{state_color}; color:white; padding:2px 8px; border-radius:4px;">{entry['state_after']}</span></td>
        </tr>'''
    
    html = f'''
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>{escape(title)}</title>
        <style>
            body {{ font-family: -apple-system, sans-serif; max-width: 1000px; margin: 40px auto; padding: 0 20px; }}
            h1 {{ color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; }}
            table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}
            th, td {{ border: 1px solid #ddd; padding: 10px; text-align: left; }}
            th {{ background: #4CAF50; color: white; }}
            tr:nth-child(even) {{ background: #f9f9f9; }}
            .summary {{ background: #f0f0f0; padding: 15px; border-radius: 8px; margin: 20px 0; }}
            code {{ background: #e8e8e8; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }}
        </style>
    </head>
    <body>
        <h1>{escape(title)}</h1>
        <div class="summary">
            <strong>最终状态:</strong> {escape(state.status)} |
            <strong>总步数:</strong> {state.current_step}/{state.max_steps} |
            <strong>HITL:</strong> {'有' if state.hitl_pending else '无'}
        </div>
        <table>
            <thead>
                <tr>
                    <th style="width:50px;">步骤</th>
                    <th>思考 (Thought)</th>
                    <th>行动 (Action)</th>
                    <th>观察 (Observation)</th>
                    <th style="width:100px;">状态</th>
                </tr>
            </thead>
            <tbody>
                {rows}
            </tbody>
        </table>
        <h2>依据摘要 (Evidence)</h2>
        <ol>
            {''.join(f'<li>{escape(e)}</li>' for e in state.evidence)}
        </ol>
    </body>
    </html>
    '''
    return html


# 生成天气版轨迹
html_weather = generate_trajectory_html(state_weather, '实验2:天气版任务轨迹')

# 保存 HTML
html_path = Path('../data/trajectory_weather.html')
html_path.write_text(html_weather, encoding='utf-8')
print(f'HTML 轨迹已保存: {html_path}')

# 在 Notebook 中显示
from IPython.display import HTML, display
display(HTML(html_weather))
# 生成 HITL 轨迹
html_hitl = generate_trajectory_html(state_hitl, '实验4:HITL 暂停轨迹')
display(HTML(html_hitl))

9. 教材兼容入口

印刷教材第11章的 questions.md 和 kb.md 入口。

# 加载教材兼容问题
questions = travel_data['questions_compat']['questions']
print('=== 教材兼容问题 (questions.md) ===')
for i, q in enumerate(questions, 1):
    print(f'  {q}')

print()

# 加载教材兼容知识库
kb = travel_data['kb_compat']['terms']
print('=== 教材兼容知识库 (kb.md) ===')
for term, definition in kb.items():
    print(f'  {term}: {definition}')

10. B档拓展:修改规则

选择以下一项完成:

选项A:修改 max_steps,观察并解释停止行为的变化

选项B:增加一条新的 HITL 触发条件(只能收紧保护,不能取消高风险确认)

要求:

  1. 修改前先写预测
  2. 修改后运行验证
  3. 记录预测和实际是否一致
# ===== 选项A 示例:修改 max_steps =====

# 你的修改
NEW_MAX_STEPS = 4  # ← 修改这个值

# 预测(先填写再运行)
prediction = '当 max_steps=4 时,智能体应该能完成3步基线任务,还有1步余量'
print(f'[预测] {prediction}')

# 运行验证
state_modified = run_baseline_agent(max_steps=NEW_MAX_STEPS, include_weather=False)
print(f'\n[实际] 状态: {state_modified.summary()}')
print(f'[实际] 步数: {state_modified.current_step}')

# 对比
print(f'\n[对比]')
print(f'  原始 max_steps=10: {state_baseline.current_step} 步, 状态={state_baseline.status}')
print(f'  修改 max_steps={NEW_MAX_STEPS}: {state_modified.current_step} 步, 状态={state_modified.status}')
# ===== 选项B 示例:增加 HITL 触发条件 =====

# 原始规则:只有 delete_file 需要人工确认
# 新增规则:当清单文件已存在时,覆盖也需要确认

def run_with_extra_hitl(max_steps=10):
    """增加了额外 HITL 条件的智能体"""
    state = AgentState(max_steps=max_steps)
    state.status = 'running'
    
    # 预设:清单文件已存在
    virtual_fs['packing_list.txt'] = '旧清单内容'
    
    steps_plan = [
        {'thought': '读取行程', 'tool': 'read_text_file', 'args': {'file_path': 'itinerary.txt'}},
        {'thought': '计算天数', 'tool': 'calc_days', 
         'args': {'start_date': '2026-08-30', 'end_date': '2026-09-01'}},
        {'thought': '创建清单(文件已存在,触发额外 HITL)', 'tool': 'create_note',
         'args': {'filename': 'packing_list.txt', 'content': '新清单'}},
    ]
    
    for step_def in steps_plan:
        if not state.can_continue():
            break
        
        tool_name = step_def['tool']
        
        # 原始 HITL 检查
        if RISK_LEVELS.get(tool_name) == 'high':
            state.request_hitl(tool_name, f'高风险操作')
            state.record_step(step_def['thought'], f'[HITL] {tool_name}', '等待确认', 'blocked')
            break
        
        # 新增 HITL 检查:覆盖已有文件
        if tool_name == 'create_note':
            filename = step_def['args'].get('filename', '')
            if filename in virtual_fs:
                state.request_hitl(tool_name, f'文件 {filename} 已存在,覆盖需要确认')
                state.record_step(step_def['thought'], f'[HITL-新增] 覆盖 {filename}', '等待确认', 'blocked')
                break
        
        tool_func = TOOL_REGISTRY[tool_name]
        observation = str(tool_func(**step_def['args']))
        state.record_step(step_def['thought'], f'{tool_name}', observation, 'running')
    
    if state.status == 'running':
        state.status = 'done'
    return state


print('=== 选项B:增加覆盖确认 HITL ===')
state_extra_hitl = run_with_extra_hitl()
print(f'状态: {state_extra_hitl.summary()}')
for entry in state_extra_hitl.step_log:
    print(f'  步骤 {entry["step"]}: {entry["action"][:60]} → {entry["state_after"]}')

if state_extra_hitl.hitl_pending:
    print(f'\n⚠️  新增 HITL 触发: {state_extra_hitl.hitl_request["reason"]}')

📝 拓展记录

  1. 你选择了哪个选项?
  2. 修改了什么?
  3. 修改前的预测是什么?
  4. 修改后的实际结果是什么?
  5. 预测和实际一致吗?

你的记录:(在此填写)


11. 交付清单

完成以下检查,确认所有交付物已准备就绪:

  • 成功任务轨迹(实验1或2的步骤记录)
  • 步数上限测试(实验3的对比表)
  • 人工暂停/终止记录(实验4的 HITL 处理)
  • 注入防御记录(实验5的验证结果)
  • 最终行李清单(虚拟文件系统中的 packing_list.txt)
  • HTML 状态轨迹图(已保存)
  • 关键代码解释(能用自己的话解释 ReAct 循环和状态机)
  • AI协同证据(第4步的五段留痕)
# 显示最终行李清单
if 'packing_list.txt' in virtual_fs:
    print('=== 最终行李清单 ===')
    print(virtual_fs['packing_list.txt'])
else:
    print('清单未生成(可能因为步数上限或 HITL 取消)')
    print('请重新运行实验2(天气版)以生成清单')

12. 总结

本章核心概念

概念 含义 为什么需要
ReAct 循环 思考→行动→观察→更新 的多步执行模式 单步工具调用无法完成复杂任务
状态机 记录进度、决定下一步的有穷自动机 让循环可控、可追踪
步数上限 max_steps 安全阀 防止无限循环和错误累积
HITL 高风险操作需人工确认 防止不可逆操作
注入防御 外部数据不能改变系统行为 保护工具权限和任务目标

与后续章节的衔接

  • 第12章:把单个智能体的多步循环扩展为多步骤工作流
  • 第13章:让智能体协助开发物理 AI 作品
  • 第14章:把多个能力组织成有责任链的完整工作流