"""
Step 02: 理解 State - 状态管理
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎓 本节内容:
1. 什么是 Agent State?
2. 如何设计 State 结构?
3. State 如何在多步骤任务中传递?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""
from typing import TypedDict, List, Dict, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
import json
# ═══════════════════════════════════════════════════════════════════════════════
# 第一部分:理解为什么需要 State
# ═══════════════════════════════════════════════════════════════════════════════
"""
在开始写代码之前,我们先理解 State 的本质。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
场景:用户想生成一个报表,然后修改它
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ 没有 State 的实现(错误):
def handle_request(user_input):
if "生成" in user_input:
jrxml = generate_jrxml(user_input)
return jrxml # 生成后就"丢"了
if "修改" in user_input:
# 糟糕!我不知道当前报表是什么
# 只能让用户重新描述
return "请重新描述你想要修改的报表"
✅ 有 State 的实现(正确):
class Agent:
def __init__(self):
self.state = {} # 用一个字典存储状态
def handle_request(self, user_input):
if "生成" in user_input:
jrxml = generate_jrxml(user_input)
self.state["current_jrxml"] = jrxml # 保存到状态
return jrxml
if "修改" in user_input:
current = self.state.get("current_jrxml") # 从状态读取
if not current:
return "没有可修改的报表"
modified = modify_jrxml(current, user_input)
self.state["current_jrxml"] = modified # 更新状态
return modified
这就是 State 的作用:在多次交互中保持信息!
"""
# ═══════════════════════════════════════════════════════════════════════════════
# 第二部分:设计 State 的数据结构
# ═══════════════════════════════════════════════════════════════════════════════
"""
设计 State 时,我们使用 Python 的 TypedDict
这是因为:
1. 有类型提示,IDE 能帮你检查错误
2. 有代码补全,写代码更方便
3. 文档化,其他人知道 State 里有什么
"""
class AgentState(TypedDict, total=False):
"""
Agent 的状态定义
为什么用 TypedDict 而不是 dataclass?
因为 TypedDict 更直观,看起来就像一个字典
而且 LangGraph 直接支持 TypedDict
total=False 的含义:
所有字段都是可选的
这样初始化时可以只填需要的字段
每个字段的用途:
- user_input: 当前用户的输入
- current_jrxml: 当前正在编辑的报表代码
- conversation_history: 对话历史
- status: 当前状态(处理中/完成/错误)
- error_msg: 错误信息(如果有)
"""
# === 核心工作字段 ===
user_input: str # 用户当前输入
current_jrxml: str # 当前 JRXML 代码
status: str # 处理状态: "processing" / "success" / "error"
error_msg: str # 错误信息
# === 对话相关 ===
conversation_history: List[dict] # 对话历史 [{"role": "user", "content": "..."}]
full_conversation_history: List[dict] # 完整的对话历史(含时间戳)
# === 生成相关 ===
stage: str # 当前阶段: "initial" / "refine" / "mapping"
generated_jrxml: str # 生成的完整 JRXML
is_modified: bool # 是否有未保存的修改
# === 验证相关 ===
validation_result: dict # 验证结果
retry_count: int # 重试次数
# === 元信息 ===
session_id: str # 会话 ID
created_at: str # 创建时间
updated_at: str # 更新时间
# ═══════════════════════════════════════════════════════════════════════════════
# 第三部分:实际应用 - Jaspersoft 报表生成的完整状态
# ═══════════════════════════════════════════════════════════════════════════════
class JaspersoftAgentState(TypedDict, total=False):
"""
Jaspersoft 报表生成 Agent 的完整状态
这个状态设计对应了你实际项目中的需求
我们逐个解释每个字段的作用
"""
# ═══════════════════════════════════════════════════════════════════════
# 1. 基础信息
# ═══════════════════════════════════════════════════════════════════════
session_id: str # 会话唯一标识
session_name: str # 会话名称(用户友好)
created_at: str # 创建时间
updated_at: str # 最后更新时间
# ═══════════════════════════════════════════════════════════════════════
# 2. 用户输入
# ═══════════════════════════════════════════════════════════════════════
user_input: str # 用户当前的输入
uploaded_file_path: str # 上传的文件路径(如果有)
# ═══════════════════════════════════════════════════════════════════════
# 3. 对话历史
# ═══════════════════════════════════════════════════════════════════════
"""
对话历史的设计考虑:
为什么需要两种历史?
1. conversation_history:精简版,用于发送给 LLM(节省 token)
2. full_conversation_history:完整版,包含时间戳等元信息(用于审计)
为什么不直接保存所有消息?
因为 LLM 有上下文长度限制
当对话很长时,我们只能发送最近的几轮
所以需要"精简版"和"完整版"的区分
"""
conversation_history: List[dict] # 精简对话历史
full_conversation_history: List[dict] # 完整对话历史
compressed_history: str # 压缩后的早期对话
# ═══════════════════════════════════════════════════════════════════════
# 4. 报表相关
# ═══════════════════════════════════════════════════════════════════════
"""
报表相关的状态字段是最核心的部分
current_jrxml vs final_jrxml 的区别:
- current_jrxml:正在编辑的版本,可能还没验证通过
- final_jrxml:经过验证的最终版本,可以导出
为什么需要版本历史?
- 支持撤销操作
- 用户可能想回退到之前的某个版本
- 记录每次修改的轨迹
"""
current_jrxml: str # 当前正在编辑的 JRXML
final_jrxml: str # 最终确认的 JRXML(验证通过)
# 版本管理
jrxml_versions: List[dict] # 历史版本列表
history_states: List[dict] # 历史状态快照(用于撤销)
last_saved_version: int # 最后保存的版本号
# ═══════════════════════════════════════════════════════════════════════
# 5. 生成过程
# ═══════════════════════════════════════════════════════════════════════
"""
生成过程的中间状态
为什么要记录这些?
1. 用户可能想知道生成到哪一步了
2. 出错时可以定位问题在哪一步
3. 方便调试和优化
"""
stage: str # 当前阶段
"""
可能的阶段值:
- "initial_generation": 初始生成
- "layout_refine": 布局精调
- "field_mapping": 字段映射
- "validation": 验证
- "correction": 修正
"""
intent: str # 用户意图
"""
可能的意图值:
- "initial_generation": 生成新报表
- "modify_report": 修改现有报表
- "preview_report": 预览报表
- "consult_question": 咨询问题
"""
# 生成相关的中间结果
retrieved_context: str # RAG 检索到的上下文
layout_schema: dict # OCR 分析出的布局信息
ocr_extraction_result: dict # OCR 提取的字段
# ═══════════════════════════════════════════════════════════════════════
# 6. 验证和错误处理
# ═══════════════════════════════════════════════════════════════════════
"""
验证和错误处理是 Agent 可靠性的关键
retry_count 的设计:
- 每次生成失败后 +1
- 达到上限后停止重试
- 这样避免无限循环
"""
status: str # 状态:success / error / processing
error_msg: str # 错误信息
retry_count: int # 当前重试次数
max_retries: int # 最大重试次数
# 错误处理
pending_failure_context: dict # 待处理的失败上下文
"""
这个字段用于"失败恢复"
当重试耗尽时,我们保存失败信息
下次用户输入时,自动注入这个上下文
"""
# ═══════════════════════════════════════════════════════════════════════
# 7. 知识库相关
# ═══════════════════════════════════════════════════════════════════════
"""
知识库(KB)相关的状态
多租户设计:
- kb_id:当前会话绑定的知识库 ID
- 不同用户/项目可以使用不同的知识库
"""
kb_id: str # 当前知识库 ID
kb_fields: List[dict] # 知识库中的字段定义
kb_template_jrxml: str # 知识库中的模板 JRXML
# ═══════════════════════════════════════════════════════════════════════
# 8. 用户解释(让用户理解 Agent 在做什么)
# ═══════════════════════════════════════════════════════════════════════
"""
natural_explanation 是给用户看的解释
为什么需要这个?
- 用户不只是想知道结果,还想知道 Agent 是怎么想的
- 如果出错,用户想知道哪里出了问题
- 这增加了透明度和信任
"""
natural_explanation: str # 对用户的自然语言解释
# ═══════════════════════════════════════════════════════════════════════════════
# 第四部分:State 的操作工具函数
# ═══════════════════════════════════════════════════════════════════════════════
def create_initial_state(session_id: str) -> JaspersoftAgentState:
"""
创建初始状态
这是一个工厂函数,用于生成新的状态实例
所有必要的默认值在这里设置
为什么用工厂函数而不是直接初始化?
1. 确保所有必填字段有默认值
2. 统一初始化逻辑
3. 方便以后修改默认行为
"""
now = datetime.now().isoformat()
return JaspersoftAgentState(
# 基础信息
session_id=session_id,
session_name="新会话",
created_at=now,
updated_at=now,
# 对话历史
conversation_history=[],
full_conversation_history=[],
compressed_history="",
# 报表相关
current_jrxml="",
final_jrxml="",
jrxml_versions=[],
history_states=[],
last_saved_version=0,
# 生成过程
stage="initial",
intent="initial_generation",
retrieved_context="",
layout_schema={},
ocr_extraction_result={},
# 验证和错误
status="processing",
error_msg="",
retry_count=0,
max_retries=5,
# 知识库
kb_id="",
kb_fields=[],
kb_template_jrxml="",
# 解释
natural_explanation="",
)
def update_state(state: JaspersoftAgentState, **updates) -> JaspersoftAgentState:
"""
更新状态
这是一个辅助函数,用于安全地更新状态字段
为什么需要这个函数?
1. 自动更新时间戳
2. 类型检查(确保字段存在)
3. 记录更新历史(可选)
用法:
state = update_state(state, current_jrxml="new content", status="success")
"""
# 更新指定字段
for key, value in updates.items():
if key in state:
state[key] = value
else:
raise KeyError(f"State 没有字段: {key}")
# 自动更新时间戳
state["updated_at"] = datetime.now().isoformat()
return state
def save_state_snapshot(state: JaspersoftAgentState) -> dict:
"""
保存状态快照
这用于"撤销"功能
在执行重要操作前,保存当前状态的快照
如果操作失败,可以回滚到这个快照
返回的快照包含:
- 报表内容
- 对话历史
- 意图
- 用户请求
"""
return {
"current_jrxml": state.get("current_jrxml", ""),
"final_jrxml": state.get("final_jrxml", ""),
"status": state.get("status", ""),
"conversation_history": list(state.get("conversation_history", [])),
"user_input": state.get("user_input", ""),
"intent": state.get("intent", ""),
"timestamp": datetime.now().isoformat(),
}
def restore_state_snapshot(state: JaspersoftAgentState, snapshot: dict) -> JaspersoftAgentState:
"""
从快照恢复状态
用于"撤销"操作
从历史快照中恢复之前保存的状态
"""
state["current_jrxml"] = snapshot.get("current_jrxml", "")
state["final_jrxml"] = snapshot.get("final_jrxml", "")
state["status"] = snapshot.get("status", "")
state["conversation_history"] = snapshot.get("conversation_history", [])
state["updated_at"] = datetime.now().isoformat()
return state
# ═══════════════════════════════════════════════════════════════════════════════
# 第五部分:演示代码
# ═══════════════════════════════════════════════════════════════════════════════
def demo():
"""
演示 State 的使用
"""
print("=" * 60)
print("Step 02: 理解 State - 状态管理演示")
print("=" * 60)
# 1. 创建初始状态
print("\n📦 步骤 1: 创建初始状态")
state = create_initial_state("session_001")
print(f" 会话 ID: {state['session_id']}")
print(f" 创建时间: {state['created_at']}")
print(f" 当前状态: {state['status']}")
# 2. 更新状态
print("\n🔄 步骤 2: 更新状态")
state = update_state(
state,
user_input="生成一个销售报表",
current_jrxml='',
status="success",
)
print(f" 用户输入: {state['user_input']}")
print(f" 生成状态: {state['status']}")
print(f" JRXML 长度: {len(state['current_jrxml'])} 字符")
# 3. 保存快照
print("\n📸 步骤 3: 保存状态快照")
snapshot = save_state_snapshot(state)
print(f" 快照时间: {snapshot['timestamp']}")
print(f" 快照内容: JRXML ({len(snapshot['current_jrxml'])} 字符)")
# 4. 修改状态(模拟用户修改)
print("\n✏️ 步骤 4: 修改报表(模拟)")
state["current_jrxml"] = ''
state["status"] = "modified"
print(f" 新状态: {state['status']}")
print(f" 新 JRXML: {state['current_jrxml']}")
# 5. 撤销(恢复到快照)
print("\n↩️ 步骤 5: 撤销操作")
state = restore_state_snapshot(state, snapshot)
print(f" 恢复状态: {state['status']}")
print(f" 恢复 JRXML: {state['current_jrxml']}")
# 6. 模拟完整的生成流程
print("\n🔄 步骤 6: 模拟完整生成流程")
state = create_initial_state("session_002")
# 阶段 1: 用户输入
state["user_input"] = "生成一个采购单报表"
state["intent"] = "initial_generation"
print(f" [阶段1] 用户输入: {state['user_input']}")
# 阶段 2: 生成
state["current_jrxml"] = "...生成的 JRXML..."
state["stage"] = "initial_generation"
print(f" [阶段2] 生成完成,长度: {len(state['current_jrxml'])}")
# 阶段 3: 验证
state["status"] = "success"
state["final_jrxml"] = state["current_jrxml"]
print(f" [阶段3] 验证通过,状态: {state['status']}")
print("\n" + "=" * 60)
print("✅ State 管理演示完成")
print("=" * 60)
if __name__ == "__main__":
demo()