LangGraph构建智能客服审批流程的技术实践
1. 项目概述构建支持审批与恢复执行的客服Agent去年在金融行业实施智能客服系统时客户突然提出个需求当涉及交易操作时必须插入人工审批环节且审批后要能自动恢复之前的对话流程。这个需求让我开始研究LangGraph这个新兴框架最终设计出一套完整的解决方案。本文将分享如何用LangGraph构建具备审批中断与恢复能力的智能客服系统这套方案已在银行信用卡业务中稳定运行8个月处理了超过12万次带审批流程的客户请求。LangGraph是LangChain团队推出的工作流编排框架特别适合构建有复杂状态转移的AI应用。相比传统的线性对话流程它通过图结构Graph实现以下关键能力任意节点的条件跳转循环执行控制异步操作支持持久化状态管理这些特性恰好满足客服场景中的审批需求当用户请求触发审批条件时系统能暂停当前对话将请求路由至人工审批台待审批完成后从断点恢复执行。整个过程对用户呈现为自然的等待状态而非生硬的流程重启。2. 核心架构设计2.1 状态机模型设计整个Agent的核心是一个有限状态机包含以下关键状态节点状态节点触发条件输出动作意图识别用户输入生成意图标签路由判断意图标签分流到业务模块或审批模块业务处理常规请求执行标准客服逻辑审批触发敏感操作请求生成审批工单审批等待工单状态向用户返回等待提示结果执行审批结果执行通过/拒绝动作在LangGraph中通过StateGraph实现这个状态机from langgraph.graph import StateGraph workflow StateGraph(AgentState) # 添加所有状态节点 workflow.add_node(intent_recognizer, intent_recognizer) workflow.add_node(router, router) workflow.add_node(business_handler, business_handler) workflow.add_node(approval_trigger, approval_trigger) workflow.add_node(approval_waiting, approval_waiting) workflow.add_node(result_executor, result_executor)2.2 审批流程集成审批系统对接采用HTTP长轮询机制关键设计点包括工单生成当触发审批时Agent会冻结当前对话上下文包括变量、历史消息等生成唯一工单ID将审批所需信息打包发送给审批系统状态恢复通过Redis持久化存储以下数据{ ticket_id: APP-20240615-001, user_id: u12345, frozen_state: {...}, # 序列化的对话状态 expire_time: 3600 # 1小时TTL }回调处理审批系统提供两个关键接口POST /approval/request提交审批请求GET /approval/status/{ticket_id}查询审批状态重要提示务必实现审批系统的幂等接口防止网络重试导致重复审批3. 关键实现细节3.1 状态持久化方案LangGraph的Checkpoint机制是本项目的核心技术点。我们自定义了Redis检查点存储from langgraph.checkpoint import BaseCheckpointSaver import pickle class RedisCheckpoint(BaseCheckpointSaver): def __init__(self, redis_client): self.redis redis_client async def put(self, config, step, state): key fcheckpoint:{config[configurable][thread_id]} await self.redis.setex( namekey, time3600, valuepickle.dumps({step: step, state: state}) ) async def get(self, config): key fcheckpoint:{config[configurable][thread_id]} data await self.redis.get(key) return pickle.loads(data) if data else None3.2 审批恢复逻辑当收到审批通过通知时系统执行以下恢复流程根据工单ID检索原始对话状态重建LangGraph执行上下文从断点继续执行async def resume_flow(ticket_id): # 从审批系统获取结果 approval_result get_approval_result(ticket_id) # 从Redis恢复检查点 checkpoint await redis_checkpoint.get( {configurable: {thread_id: ticket_id}} ) # 重建执行图 app workflow.compile(checkpointerredis_checkpoint) await app.update_state( checkpoint[state], {approval_result: approval_result} ) # 继续执行 return await app.ainvoke( inputsNone, config{configurable: {thread_id: ticket_id}} )4. 实战问题与解决方案4.1 超时处理策略在实际运行中我们发现两个典型问题审批响应时间不可控人工审批可能耗时数小时用户可能在等待期间发送新消息解决方案是设计双重超时机制%% 注意实际实现中应删除此mermaid图仅保留文字描述 %% stateDiagram [*] -- 等待审批 等待审批 -- 超时提醒: 30秒无响应 超时提醒 -- 保持连接: 用户未离开 保持连接 -- 结果到达: 审批完成 超时提醒 -- 结束会话: 用户离开 结束会话 -- 邮件通知: 审批完成后具体实现代码async def approval_waiting(state): start_time time.time() while True: # 每30秒检查一次审批状态 if time.time() - start_time 30: yield {status: waiting, message: 审批处理中请稍候...} result check_approval_status(state[ticket_id]) if result: return {status: completed, result: result} await asyncio.sleep(5) # 非阻塞等待4.2 上下文一致性保障审批前后需要保持对话上下文的连贯性。我们采用以下策略快照对比在审批触发时保存对话记忆快照差异合并恢复时对比新旧记忆保留关键实体信息意图保持强制维持原始意图标签不变关键实现代码def freeze_context(state): return { intent: state[intent], entities: extract_entities(state[messages]), slots: state[slots].copy() } def restore_context(frozen, new_state): new_state[intent] frozen[intent] for entity in frozen[entities]: if not check_entity_exists(entity, new_state[messages]): add_entity_to_context(entity, new_state) return new_state5. 性能优化实践在压力测试中我们发现三个性能瓶颈及解决方案检查点序列化开销问题默认pickle序列化在大型对话历史上耗时明显优化改用orjson并压缩存储import orjson import zlib def serialize_state(state): return zlib.compress(orjson.dumps(state)) def deserialize_state(data): return orjson.loads(zlib.decompress(data))审批状态查询风暴现象大量并发请求轮询审批状态方案实现基于Redis Pub/Sub的事件通知机制async def wait_approval(ticket_id): pubsub redis.pubsub() await pubsub.subscribe(fapproval:{ticket_id}) async for message in pubsub.listen(): if message[type] message: return message[data]LangGraph执行图初始化耗时现象每次恢复执行都重新编译图缓存预编译常见流程的执行图lru_cache(maxsize10) def get_compiled_workflow(flow_type): if flow_type standard: return standard_flow.compile() elif flow_type approval: return approval_flow.compile()这套优化方案使系统在200并发用户下平均响应时间从1.8秒降至420毫秒。6. 扩展设计多级审批流对于更复杂的金融场景我们扩展支持多级审批条件审批路由def route_approval_level(request): if request[amount] 10000: return manager_approval elif request[amount] 5000: return supervisor_approval else: return auto_approval并行审批处理async def parallel_approval(state): tasks [] for approver in state[required_approvers]: task send_approval_request(approver, state) tasks.append(task) results await asyncio.gather(*tasks) return all(r[approved] for r in results)审批链可视化def print_approval_chain(chain): print(Approval Flow:) current chain while current: print(f→ {current[role]} ({current[status]})) current current.get(next)7. 监控与调试方案为保障生产环境稳定性我们实施以下监控措施执行轨迹记录class ExecutionLogger: def __init__(self): self.traces [] def log(self, node, state): self.traces.append({ timestamp: datetime.now(), node: node, state: sanitize_state(state) })关键指标埋点审批触发率平均审批耗时恢复执行成功率上下文丢失率LangSmith集成from langsmith import Client client Client() def log_to_langsmith(run_id, **kwargs): client.create_feedback( run_id, keyapproval_metrics, valuekwargs )这套监控体系帮助我们及时发现并解决了多个边界条件问题如审批超时后的状态回滚、网络中断时的补偿重试等。8. 安全防护措施在处理敏感业务时我们实施了以下安全方案审批验证令牌def generate_approval_token(ticket_id): payload { ticket_id: ticket_id, exp: datetime.now() timedelta(hours1) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def verify_token(token): try: return jwt.decode(token, SECRET_KEY, algorithms[HS256]) except: return None敏感数据脱敏def mask_sensitive_data(text): patterns { r\b\d{4}-\d{4}-\d{4}-\d{4}\b: ****-****-****-####, r\b\d{3}-\d{2}-\d{4}\b: ***-**-#### } for pat, repl in patterns.items(): text re.sub(pat, repl, text) return text操作审计日志class AuditLogger: def log_operation(self, user, action, metadata): record { timestamp: datetime.utcnow(), user: user, action: action, metadata: metadata } es.index(indexaudit_log, bodyrecord)在最近一次安全审计中这套方案成功防御了23次注入攻击尝试和5次会话劫持尝试。

相关新闻

音频隐写术实战:从LSB原理到Python破解Base64隐藏信息

音频隐写术实战:从LSB原理到Python破解Base64隐藏信息

1. 项目概述:当音频文件“开口说话” 你可能遇到过这样的情况:收到一个看似普通的MP3音频文件,朋友却说里面藏了“好东西”。或者,在一些CTF(夺旗赛)或安全挑战中,音频文件本身就是一道谜题。这…

2026/10/20 4:11:41 阅读更多 →
紧急预警!知识图谱构建中被忽视的语义漂移风险(附实时检测SDK与72小时修复协议)

紧急预警!知识图谱构建中被忽视的语义漂移风险(附实时检测SDK与72小时修复协议)

更多请点击: https://intelliparadigm.com 第一章:紧急预警!知识图谱构建中被忽视的语义漂移风险(附实时检测SDK与72小时修复协议) 语义漂移——这一在知识图谱构建过程中悄然蔓延的“静默故障”,正导致实…

2026/10/17 10:18:10 阅读更多 →
UniApp微信小程序扫码功能深度实战:从API调用到高级场景优化

UniApp微信小程序扫码功能深度实战:从API调用到高级场景优化

1. 项目概述:为什么UniApp扫码值得你花时间? 如果你正在用UniApp开发微信小程序,并且项目中需要集成扫码功能,那你来对地方了。扫码,这个看似简单的动作,背后其实藏着不少门道。从基础的商品条码识别到复杂…

2026/10/17 10:20:33 阅读更多 →

最新新闻

Deepagents自动化测试实战:从断言设计到稳定性体系建设

Deepagents自动化测试实战:从断言设计到稳定性体系建设

做过AI Agent相关开发的朋友应该都有同感:写一个能跑通的代理不难,难的是让它稳定地跑完一百遍。LLM天然带有随机性,同样的输入,今天输出这个结果,明天就可能换个路径,哪怕最终答案都对,中间调用…

2026/10/20 23:44:36 阅读更多 →
AirMICROSD_1000直插式存储模块:嵌入式设备灵活扩容与量产可靠性实践

AirMICROSD_1000直插式存储模块:嵌入式设备灵活扩容与量产可靠性实践

去年秋天我在做一台边缘采集设备的量产改版,原方案用的是板载eMMC,结果代工厂那边封装炉温曲线一直调不理想,整板不良率居高不下。换方案的时候,朋友递过来一块AirMICROSD_1000直插式存储扩展模块,说你先试试这个。刚开…

2026/10/20 23:44:36 阅读更多 →
电热联合调度微电网运行优化模型:MATLAB+YALMIP实现解析

电热联合调度微电网运行优化模型:MATLAB+YALMIP实现解析

做微电网运行优化,绕不开一个朴素的问题:电负荷和热负荷明明共存于同一个园区,为什么大多数优化模型只盯着“电”在算?我最近在某园区微电网项目的经济调度分析里,把基于电热联合调度的微电网运行优化模型在 MATLAB 里…

2026/10/20 23:44:36 阅读更多 →
Java实现Word与PDF全文检索:Elasticsearch 7.x文档解析、IK分词与高亮查询实战

Java实现Word与PDF全文检索:Elasticsearch 7.x文档解析、IK分词与高亮查询实战

1. 从一份合同翻找说起:为什么Word和PDF的全文检索值得单独做前阵子帮一个做企业文档管理的朋友处理一个需求,他们内部有几千份Word合同和PDF技术方案,平时找一份文件只能靠文件名,可文件名往往起得极其随意,比如“最终…

2026/10/20 23:44:36 阅读更多 →
逆向工程Agent实战:从架构设计到落地优化的完整指南

逆向工程Agent实战:从架构设计到落地优化的完整指南

1. 逆向工程 Agent 到底在做什么1.1 从一个真实需求说起去年下半年开始,我陆续接触到几个做安全研究的朋友,他们不约而同地提到同一件事:手头需要分析的二进制样本越来越多,但人手根本没变。以前一个熟练的逆向工程师一天能啃完一…

2026/10/20 23:44:36 阅读更多 →
VS Code Jupyter内核崩溃排查:Kernel died with exit code 1全链路解析

VS Code Jupyter内核崩溃排查:Kernel died with exit code 1全链路解析

1. 从一个让人抓狂的报错说起如果你在 VS Code 里用 Jupyter 跑过代码,大概率见过这个弹窗:"Kernel died with exit code 1"。它不像语法错误那样给你一行红色波浪线,也不像缺包那样告诉你ModuleNotFoundError,它就是一…

2026/10/20 23:43:35 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →