资讯详情

LangGraph Agent 可控性实战:Hook 与 Checkpointer 深度指南

📅 2026/9/29 15:54:48 | 华诺云谱 👁 阅读
LangGraph Agent 可控性实战:Hook 与 Checkpointer 深度指南
1. 项目概述当一个前端工程师开始给 Agent 装上“刹车”和“记事本”你有没有试过写完一个 LangGraph 流程跑起来后就像放飞的风筝——它自己思考、自己调用工具、自己重试、自己决定下一步直到某次 API 返回了个奇怪的 status code或者 LLM 突然开始胡言乱语整个流程直接卡死在某个节点连日志都只显示agent execution terminated due to error.我试过三次每次都是从头 rerun 整个对话重走一遍所有记忆、所有工具调用、所有中间状态。那感觉就像开车没刹车、没后视镜、没仪表盘全靠运气。这就是标题里说的“全自动”陷阱。很多前端同学刚从 React/Vue 转向 Agent 开发时第一反应是把useEffect换成graph.invoke()把useState换成StateGraph以为只要流程图画得漂亮Agent 就能稳稳落地。但现实是LangGraph 的默认执行模式尤其是CompiledGraph本质是个黑盒流水线它不暴露中间态不支持暂停不记录每一步的输入输出更不提供“回滚到上一步”的能力。一旦出错你不是在 debug是在考古。而Agent Hooks 与 Checkpointer就是给这个高速运转的智能体装上三样东西方向盘人为干预点、行车记录仪状态快照、以及可随时踩下的刹车执行暂停/恢复。它们不是锦上添花的功能而是把 Agent 从“自动贩卖机”升级为“可协作工作伙伴”的分水岭。尤其对前端出身的开发者——我们习惯于用户交互、状态管理、UI 可控性——这套机制天然契合我们的思维模型Hook 是事件监听器Checkpointer 是 localStorage Redux DevTools 的合体而整个流程终于可以像调试一个 React 组件一样逐帧 inspect、手动 step、甚至修改 state 后继续 run。这个项目不教你如何写一个炫酷的 PI Agent 或 Hermes Agent它聚焦在一个最朴素也最刚需的问题当你的 Agent 在生产环境里跑着跑着突然“失联”你怎么能在 30 秒内定位到是哪一次 tool call 失败怎么能让产品经理说“刚才第三步生成的文案我不满意换一种风格重来”而你不用删掉前面所有历史重新 invoke这就是 Hook 与 Checkpointer 的真实战场。它不解决“Agent 能不能思考”它解决的是“你能不能信任它、指挥它、修复它”。2. 核心设计思路为什么必须用 Hook Checkpointer而不是“加个 console.log”很多人会想既然出错了加个 try/catch 不就行了或者在每个 node 里console.log(entering node X, input)这确实能看见一点信息但离“可掌控”差了整整一个工程维度。我用一个真实场景对比说明2.1 单纯日志 vs. Hook Checkpointer 的能力鸿沟假设你正在开发一个“智能简历分析 Agent”流程是parse_pdf → extract_skills → match_jobs → generate_summary。某次运行中match_jobs节点因为招聘平台 API 限流返回了 429整个流程中断。仅靠 console.log你只能看到entering match_jobs和match_jobs failed: 429。你不知道parse_pdf输出的结构是否正确比如 PDF 解析漏掉了关键 sectionextract_skills提取的技能列表里有没有拼写错误导致匹配失败match_jobs的输入参数里城市字段是不是传成了ShangHai而不是Shanghai更重要的是你无法让 Agent “回到extract_skills后用另一个技能提取模型重跑再进match_jobs”。启用 Checkpointer Hook 后你可以查看checkpointer.get_state(config)拿到match_jobs节点失败前的完整 state包括pdf_text,skills_list,user_location等所有字段手动修改state.values.skills_list把Reactjs改成React.js调用graph.update_state(config, {skills_list: [React.js, TypeScript]})将修正后的 state 注入再次graph.invoke(..., configconfig)Agent 会从match_jobs节点继续执行跳过前面所有已成功步骤。这背后不是魔法而是 LangGraph 对“状态”和“生命周期”的彻底重构。它把 Agent 的每一次执行从一次性的函数调用变成了一个带版本号、可寻址、可编辑的持久化对象。而 Hook则是这个对象的“神经末梢”让你能在任何关键节点插入自己的逻辑——比如在generate_summary前弹出一个 UI 确认框前端场景或在tool_call后自动记录耗时并告警运维场景。2.2 为什么前端开发者特别需要这套机制前端同学转 Agent 开发最大的认知迁移不是学 Python而是从“声明式 UI”转向“状态驱动流程”。React 的useState让你随时setState({ loading: false, data: result })Vue 的ref让你myRef.value newValue。但 LangGraph 默认的graph.invoke()是“命令式”的——你发一个指令它跑完给你一个结果中间过程你无法介入。Hook 和 Checkpointer本质上就是把前端最熟悉的“响应式状态管理”范式移植到了 Agent 编排层on_chain_start/on_chain_endHook就像useEffect(() { ... }, [deps])监听节点生命周期Checkpointer就是Redux storelocalStorageget_state是store.getState()update_state是store.dispatch({ type: UPDATE, payload })config包含thread_id就是key确保每个用户会话的状态独立隔离就像 React 的key属性保证列表项唯一性。所以这不是“Python 工程师该学的东西”这是“前端工程师在构建下一代人机协作界面时必须掌握的底层协议”。当你用useAgentState(threadId)hook 在 React 中订阅 Agent 状态变化并在 UI 上实时显示currentStep: matching jobs...、progress: 65%、lastError: API rate limit exceeded时你写的就不是一个脚本而是一个真正意义上的“智能应用”。2.3 LangChain 与 LangGraph 的关键分野编排权的归属网上常有人问“LangChain 和 LangGraph 有什么区别”答案不能只停留在 API 差异。核心在于LangChain 的AgentExecutor把编排逻辑藏在框架内部你只能配置max_iterations或handle_parsing_errors而 LangGraph 把编排权完全交还给开发者通过StateGraph显式定义每一步再通过Checkpointer和Hooks让你拥有对每一步的绝对控制权。举个例子LangChain 的OpenAIFunctionsAgent你无法在tool_call和tool_response之间插入自定义逻辑比如做参数校验、加缓存、改 response 格式。它要么成功要么失败重试没有中间态。而 LangGraph 中你可以def tool_node(state: State): # 这里就是你的“中间态” tool_calls state[messages][-1].tool_calls # ✅ Hook 点在此处校验 tool_calls 参数合法性 if not validate_tool_params(tool_calls): return {messages: [AIMessage(content参数错误请检查输入)]} # ✅ Checkpoint 点记录本次 tool_call 的原始请求 checkpointer.save_checkpoint( configstate[config], checkpoint{tool_request: tool_calls}, stepstate[step] ) # 执行调用... response execute_tool(tool_calls) return {messages: [ToolMessage(contentstr(response), tool_call_id...)]}这种显式、可控、可插拔的设计正是前端工程师所珍视的“透明性”。它意味着你不再需要祈祷框架的默认行为符合你的业务逻辑而是可以像写组件一样精准地在每一个环节注入自己的判断和处理。3. 核心细节解析Agent Hooks 的类型、触发时机与实战用法LangGraph 的 Hooks 体系远比useEffect的mount/update/unmount三阶段复杂。它是一套覆盖整个 Agent 生命周期的“事件总线”每个事件都有明确的触发条件、携带的上下文数据以及严格的执行顺序。理解这些是写出健壮、可维护 Hook 的前提。3.1 六大核心 Hook 类型及其不可替代的用途LangGraph 定义了BaseCallbackHandler接口但实际使用中我们主要关注以下六类 Hook它们按执行顺序排列构成了 Agent 的“神经脉冲”Hook 名称触发时机关键参数前端类比典型用途on_chain_start任意节点node开始执行前run_id,name,inputscomponentWillMount日志记录、性能计时开始、权限校验如检查用户是否有权调用此工具on_chain_end任意节点执行成功后run_id,outputs,namecomponentDidUpdate结果后处理如格式化 JSON、状态聚合、发送通知邮件/SMSon_chain_error任意节点执行抛出异常时run_id,error,namecomponentDidCatch错误分类、降级策略如 fallback 到规则引擎、告警推送on_tool_startTool 调用发起前即tool_node开始run_id,name,tool_inputbeforeRequest(Axios interceptor)参数脱敏隐藏 API key、请求签名、缓存查询查本地 DB 是否有相同请求的缓存on_tool_endTool 调用返回后即tool_node结束run_id,output,nameafterResponse(Axios interceptor)响应校验检查 HTTP status、结果缓存写入、耗时统计end_time - start_timeon_event高级自定义事件需在 graph 中显式yield {event: xxx}event,data,metadatadispatchEvent(new CustomEvent(xxx))跨节点通信如parse_pdf成功后触发notify_frontend事件提示on_event是最容易被忽略但威力最大的 Hook。它打破了节点间的硬耦合。例如你不需要在parse_pdf节点里写if success: send_to_frontend()而是yield {event: pdf_parsed, data: {text: extracted_text}}然后在 Hook 中统一处理所有pdf_parsed事件——这正是前端 Event Bus 思维的完美复刻。3.2 实战一个“前端友好”的 Hook 集成方案作为前端开发者我们最关心的不是“如何注册 Hook”而是“如何让 Hook 的输出能被我的 React 组件消费”。这里给出一个经过生产验证的方案Step 1创建一个全局事件总线EventBus# hooks/event_bus.py from typing import Dict, List, Callable, Any import threading class EventBus: def __init__(self): self._handlers: Dict[str, List[Callable]] {} self._lock threading.Lock() def on(self, event: str, handler: Callable): with self._lock: if event not in self._handlers: self._handlers[event] [] self._handlers[event].append(handler) def emit(self, event: str, data: Any None): with self._lock: handlers self._handlers.get(event, []) for handler in handlers: try: handler(data) except Exception as e: print(fError in event handler {event}: {e}) # 全局实例 event_bus EventBus()Step 2编写一个专为前端服务的 Hook# hooks/frontend_hook.py from langgraph.callbacks.base import BaseCallbackHandler from hooks.event_bus import event_bus class FrontendHook(BaseCallbackHandler): def on_chain_start(self, run_id: str, name: str, inputs: dict, **kwargs): # 发送节点启动事件供前端 UI 显示加载状态 event_bus.emit(node_start, { run_id: run_id, node: name, timestamp: time.time(), inputs: {k: str(v)[:100] for k, v in inputs.items()} # 截断防过大 }) def on_chain_end(self, run_id: str, outputs: dict, name: str, **kwargs): # 发送节点结束事件含结果摘要 event_bus.emit(node_end, { run_id: run_id, node: name, timestamp: time.time(), outputs: {k: str(v)[:200] for k, v in outputs.items()} }) def on_chain_error(self, run_id: str, error: Exception, name: str, **kwargs): # 发送错误事件含可读错误码 error_code UNKNOWN if 429 in str(error): error_code RATE_LIMIT_EXCEEDED elif timeout in str(error).lower(): error_code REQUEST_TIMEOUT event_bus.emit(node_error, { run_id: run_id, node: name, error_code: error_code, message: str(error)[:500] })Step 3在 FastAPI 后端暴露 WebSocket 接口供前端连接# api/ws.py from fastapi import WebSocket, WebSocketDisconnect from hooks.event_bus import event_bus active_connections: List[WebSocket] [] router.websocket(/ws/agent-state) async def websocket_endpoint(websocket: WebSocket): await websocket.accept() active_connections.append(websocket) # 注册事件处理器 def send_to_ws(data): for conn in active_connections: try: await conn.send_json(data) except Exception: pass event_bus.on(node_start, send_to_ws) event_bus.on(node_end, send_to_ws) event_bus.on(node_error, send_to_ws) try: while True: # 心跳保活 await websocket.receive_text() except WebSocketDisconnect: active_connections.remove(websocket) # 清理事件监听器简化版实际需更严谨 event_bus._handlers.pop(node_start, None) event_bus._handlers.pop(node_end, None) event_bus._handlers.pop(node_error, None)Step 4前端 React 组件订阅状态// components/AgentStatus.tsx import { useEffect, useState } from react; import { io } from socket.io-client; const AgentStatus () { const [status, setStatus] useState{ currentStep: string; progress: number; lastError?: string }({ currentStep: idle, progress: 0 }); useEffect(() { const socket io(http://localhost:8000/ws/agent-state); socket.on(node_start, (data: any) { setStatus(prev ({ ...prev, currentStep: data.node, progress: prev.progress 10 })); }); socket.on(node_end, (data: any) { if (data.node generate_summary) { // 最终结果到达可更新 UI setSummary(data.outputs.summary); } }); socket.on(node_error, (data: any) { setStatus(prev ({ ...prev, lastError: data.message })); // 弹出错误提示框 showNotification(步骤 ${data.node} 失败: ${data.error_code}); }); return () { socket.close(); }; }, []); return ( div classNameagent-status div当前步骤: {status.currentStep}/div div进度: {status.progress}%/div {status.lastError div classNameerror⚠️ {status.lastError}/div} /div ); }; export default AgentStatus;这个方案的价值在于它把 LangGraph 的底层事件无缝映射到了前端的响应式 UI 生态中。你不再需要轮询/api/agent/status也不需要在每个节点里手动fetch(/api/update-ui)。Hook 是被动的监听者EventBus 是消息中枢WebSocket 是管道React 是最终的消费者——整条链路清晰、解耦、可测试。3.3 注意事项Hook 使用的三大陷阱陷阱一Hook 中的异步操作未 await导致状态错乱很多人在on_chain_end里写await save_to_db(outputs)却忘了on_chain_end本身是同步回调。LangGraph 不会等待你的await完成。正确做法在 Hook 中启动一个后台任务如asyncio.create_task(save_to_db(...))或使用线程池concurrent.futures.ThreadPoolExecutor。否则下一个节点可能在数据库写入完成前就开始执行读到脏数据。陷阱二过度依赖on_tool_end忽视on_chain_end的粒度on_tool_end只捕获工具调用而on_chain_end捕获所有节点包括llm_node,tool_node,conditional_node。如果你只想记录“LLM 生成了什么”用on_chain_end并过滤name llm如果只想记录“调用了哪个 API”才用on_tool_end。混用会导致日志重复或遗漏。陷阱三在 Hook 中修改state期望影响后续节点Hook 是观察者不是参与者。你在 Hook 里修改inputs或outputs字典不会改变实际流入/流出节点的数据。LangGraph 的 state 是 immutable 的通过StateGraph的add_node和add_edge定义Hook 只能“看”不能“改”。要修改 state必须在节点函数内部return {key: new_value}或用graph.update_state()。4. Checkpointer 深度实现从内存快照到分布式持久化如果说 Hook 是 Agent 的“感官系统”那么 Checkpointer 就是它的“记忆中枢”。没有 CheckpointerAgent 就像金鱼只有 7 秒记忆有了它Agent 才能成为真正的“长期伙伴”。但 Checkpointer 的配置绝不是checkpointer MemorySaver()一行代码那么简单。4.1 Checkpointer 的三种核心模式与选型逻辑LangGraph 提供了多种 Checkpointer 实现选择哪种取决于你的应用场景、数据规模和可靠性要求Checkpointer 类型存储位置优点缺点适用场景MemorySaver进程内存启动快、零配置、调试友好进程重启即丢失、不支持多实例、无持久化本地开发、单元测试、POC 验证SqliteSaver本地 SQLite 文件持久化、支持多会话、轻量级单机、不支持高并发写入、无备份机制小型内部工具、桌面应用如 Windows Hermes Agent 桌面版、单机部署的 MVPPostgresSaverPostgreSQL 数据库强一致性、高并发、支持备份/HA、成熟生态需额外 DB 运维、网络延迟、配置复杂生产环境、SaaS 应用、需要审计日志的金融/医疗场景提示不要被MemorySaver的简单迷惑。我在一个客户项目中用它跑了两周直到某次服务器重启所有用户的历史对话全部消失客户投诉电话打爆。MemorySaver只应出现在if DEBUG:分支里永远不要在 production 环境的requirements.txt中出现。这是血泪教训。4.2 从零搭建 PostgresSaver不只是填个连接字符串PostgresSaver的配置远不止PostgresSaver(connection_string...)。以下是生产环境必须配置的 5 个关键参数缺一不可from langgraph.checkpoint.postgres import PostgresSaver import asyncpg # 1. 连接池配置避免连接耗尽 async def create_pool(): return await asyncpg.create_pool( hostdb.example.com, port5432, userlanggraph_user, passwordyour_strong_password, databaselanggraph_db, min_size5, # 最小连接数避免冷启动延迟 max_size20, # 最大连接数防止 DB 过载 command_timeout30, # 查询超时防止慢 SQL 拖垮整个服务 ) # 2. 表名前缀多租户隔离 saver PostgresSaver( poolawait create_pool(), table_namelanggraph_checkpoints, # 默认是 checkpoints建议加前缀 ) # 3. 自定义序列化器处理非 JSON-serializable 类型 import pickle from langgraph.checkpoint import BaseCheckpointSaver class PicklePostgresSaver(PostgresSaver): def serialize(self, obj: Any) - bytes: # 对于 numpy array, datetime 等用 pickle return pickle.dumps(obj) def deserialize(self, data: bytes) - Any: return pickle.loads(data) # 4. TTLTime-To-Live自动清理防止磁盘爆满 # 在数据库中为 checkpoints 表添加 created_at 字段并定期执行 # DELETE FROM langgraph_checkpoints WHERE created_at NOW() - INTERVAL 30 days; # 5. 加密存储敏感字段如 API keys # 在 save_checkpoint 前对 state.values 中的敏感字段 AES 加密 # 在 get_state 后对敏感字段 AES 解密 # 注意加密密钥必须安全存储如 HashiCorp Vault4.3 实操一个可恢复的“简历重写”工作流让我们用一个具体案例展示 Checkpointer 如何让 Agent 真正“可掌控”。需求用户上传一份简历 PDFAgent 分析后生成三版不同风格的 Summary专业版、活泼版、极简版用户可任选其一或要求重写某版。Step 1定义 State关键State 结构决定 Checkpoint 的粒度from typing import Annotated, Sequence, TypedDict, Optional import operator class State(TypedDict): pdf_bytes: bytes # 原始 PDF只存一次 parsed_text: str # 解析后的文本 skills: list[str] # 提取的技能 summary_options: dict[str, str] # 三个版本的 summarykey: professional, casual, minimal selected_summary: Optional[str] # 用户最终选择 revision_request: Optional[str] # 用户要求重写的版本名如 casual thread_id: str # 用于 Checkpointer 的 configStep 2关键节点——支持“重写”的rewrite_summarydef rewrite_summary(state: State) - State: # 1. 从 Checkpointer 获取当前 state确保是最新的 # 实际中state 已由 LangGraph 自动注入此处为说明逻辑 # 2. 判断是首次生成还是重写 if state.get(revision_request): # 重写指定版本 target_style state[revision_request] # 用 LLM 重写提示词强调“保持原意只改变风格” new_summary llm.invoke( fRewrite this summary in {target_style} style:\n{state[summary_options][target_style]} ) # 更新对应版本 state[summary_options][target_style] new_summary.content # 清空 revision_request表示本次重写完成 state[revision_request] None else: # 首次生成三版并行 state[summary_options] { professional: generate_professional(state[parsed_text]), casual: generate_casual(state[parsed_text]), minimal: generate_minimal(state[parsed_text]) } return stateStep 3前端交互逻辑React// 当用户点击“重写活泼版”按钮时 const handleRewriteCasual async () { // 1. 调用后端 API设置 revision_request await fetch(/api/agent/revision, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ thread_id: currentThreadId, revision_request: casual }) }); // 2. 后端收到请求后执行 // graph.update_state( // config{configurable: {thread_id: thread_id}}, // values{revision_request: casual} // ) // 然后调用 graph.invoke(...)Agent 会从 rewrite_summary 节点继续执行 // 3. 前端监听 WebSocket 事件等待新 summary 到达 eventBus.on(summary_updated, (data) { if (data.style casual) { setCasualSummary(data.content); setIsRewriting(false); } }); };Step 4Checkpointer 的“救命”时刻假设rewrite_summary节点在调用 LLM 时因网络抖动超时失败。此时on_chain_errorHook 会被触发记录错误Checkpointer 已保存了失败前的 state含revision_request: casual用户刷新页面前端用thread_id重新连接后端graph.get_state(config)拿到 state发现revision_request仍为casual知道这是个未完成的重写任务无需用户任何操作Agent 自动重试rewrite_summary且只重试这一步前面的parsed_text和skills完全复用。这就是 Checkpointer 赋予的“韧性”。它让 Agent 不再是一次性的函数而是一个有记忆、有状态、可中断、可恢复的长生命周期服务。4.4 常见问题排查Checkpointer 的 5 个高频故障点问题现象可能原因排查命令/方法解决方案get_state返回Nonethread_id错误或该 thread_id 下无 checkpointSELECT * FROM langgraph_checkpoints WHERE thread_id xxx;检查前端传入的thread_id是否与graph.invoke(..., config{configurable: {thread_id: xxx}})一致确认save_checkpoint是否被调用加日志update_state后get_state仍是旧值update_state的config与get_state的config不一致如thread_id相同但checkpoint_id不同SELECT checkpoint_id, parent_checkpoint_id FROM langgraph_checkpoints WHERE thread_id xxx ORDER BY timestamp DESC LIMIT 5;update_state会创建新 checkpointget_state默认获取最新 checkpoint。若需获取特定版本用get_state(config, checkpoint_idxxx)PostgreSQL CPU 100%查询变慢checkpoints表数据量过大缺乏索引\d langgraph_checkpoints查看索引EXPLAIN ANALYZE SELECT * FROM langgraph_checkpoints WHERE thread_id xxx ORDER BY timestamp DESC LIMIT 1;为thread_id和timestamp字段创建复合索引CREATE INDEX idx_thread_ts ON langgraph_checkpoints (thread_id, timestamp DESC);多个用户会话状态互相污染thread_id生成逻辑有 bug导致不同用户共用同一thread_id在on_chain_startHook 中打印run_id和config[configurable][thread_id]thread_id必须全局唯一推荐用uuid.uuid4().hex绝对不要用用户 ID如user_123作为thread_id因为一个用户可能有多个并发会话MemorySaver在 FastAPI 重启后丢失状态MemorySaver实例被重新创建print(id(saver))在每次请求中查看 saver 实例地址将MemorySaver实例作为 FastAPI 的app.state.saver在startup事件中初始化一次而非每次请求都新建5. 前端集成实战用 React 构建一个“可掌控”的 Agent 控制台现在我们把前面所有的技术点整合成一个真实的前端界面。这不是一个 demo而是一个可直接用于生产环境的“Agent 控制台”最小可行产品MVP。5.1 核心功能模块设计一个真正“可掌控”的控制台必须包含以下四个核心区域状态概览区Top Bar显示thread_id、当前step、整体progress、last_error如果有流程图可视化区Center动态渲染 LangGraph 的节点和边高亮当前执行节点灰色化已完成节点红色标出失败节点状态检查器Right Panel以树形结构展示当前state的所有 key-value支持展开/折叠、搜索、编辑update_state操作控制台Bottom Console提供Resume从失败点继续、Rerun from here从当前节点重跑、Edit Continue修改 state 后继续等按钮。5.2 实现StateInspector组件让 state 可读、可查、可改这是控制台的灵魂。它必须能处理任意嵌套的dict/list/str/int并安全地调用update_state。// components/StateInspector.tsx import { useState, useEffect, useCallback } from react; import { useAgentState } from ../hooks/useAgentState; // 自定义 hook封装了 WebSocket 连接和 state 同步 import { updateAgentState } from ../api/agentApi; // 封装了 POST /api/agent/update-state interface StateNodeProps { path: string[]; value: any; depth: number; onEdit: (path: string[], newValue: any) void; } const StateNode ({ path, value, depth, onEdit }: StateNodeProps) { const [expanded, setExpanded] useState(true); const [editing, setEditing] useState(false); const [editValue, setEditValue] useState(JSON.stringify(value, null, 2)); const isObject typeof value object value ! null !Array.isArray(value); const isArray Array.isArray(value); const handleEditSubmit () { try { const parsed JSON.parse(editValue); onEdit(path, parsed); setEditing(false); } catch (e) { alert(JSON 格式错误); } }; return ( div style{{ marginLeft: depth * 20 }} div span style{{ color: #666 }}{path.join(.)}: /span {editing ? ( div textarea value{editValue} onChange{(e) setEditValue(e.target.value)} rows{3} style{{ width: 100%, fontFamily: monospace }} / button onClick{handleEditSubmit}✓ Save/button button onClick{() setEditing(false)}✗ Cancel/button /div ) : ( span {isObject || isArray ? ( span button onClick{() setExpanded(!expanded)} {expanded ? ▼ : ▶} {isObject ? Object : Array} ({Object.keys(value).length}) /button {expanded ( div {isObject Object.entries(value).map(([k, v]) ( StateNode key{k} path{[...path, k]} value{v} depth{depth 1} onEdit{onEdit} / ))} {isArray value.map((item: any, i: number) ( StateNode key{i} path{[...path, String(i)]} value{item} depth{depth 1} onEdit{onEdit} / ))} /div )} /span ) : ( span span style{{ color: #28a745 }}{typeof value string ? ${value} : String(value)}/span button onClick{() setEditing(true)} style{{ marginLeft: 8px }}✏️ Edit/button /span )} /span )} /div /div ); }; const StateInspector ({ threadId }: { threadId: string }) { const { state, loading } useAgentState(threadId); const [searchTerm, setSearchTerm] useState(); const filteredState useMemo(() { if (!searchTerm || !state) return state; // 简单的关键词搜索实际可用更强大的 jsonpath const search searchTerm.toLowerCase(); const filter (obj: any, path: string[] []): any { if (typeof obj object obj ! null) { if (Array.isArray(obj)) { return obj.map((item, i) filter(item, [...path, String(i)])); } else { const filtered: any {}; for (const [k, v] of Object.entries(obj)) { const fullPath [...path, k].join(.);
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑