Python + Agent SDK构建可解释的业务自动化工作流 1. 项目概述这不是写个脚本而是给业务流程装上“自主神经系统”你有没有遇到过这种场景销售团队每天要从CRM导出300条线索手动筛选出高意向客户再挨个填进ERP系统做报价单最后发邮件通知客户——整个流程耗时2小时但95%的动作都是复制粘贴、点鼠标、等页面加载。我去年在一家做工业设备分销的公司做技术顾问亲眼看着一个资深销售经理把下午三点到五点固定设为“自动化屏蔽时段”就为了不被这些重复操作打断思考。这不是懒是人的认知带宽被低价值动作持续挤占。而“Python Agent SDK”这个组合本质上不是教你怎么写代码而是帮你把业务逻辑里那些“人脑默认执行但从未明确定义”的判断链条翻译成机器可理解、可调度、可追溯的自动化工作流。核心关键词Python、Agent SDK、自动化工作流这三个词必须放在一起理解才有意义。Python是通用胶水语言它不解决智能问题但它能稳稳托住所有AI组件Agent SDK比如LangGraph不是另一个大模型API封装它是专门为“多步骤、有状态、需协作”的复杂任务设计的编排框架而自动化工作流在这里特指那些需要跨系统、带条件分支、含人工审核节点、甚至要调用外部工具如查天气、发短信、读Excel的真实业务闭环。它和传统RPA有本质区别RPA模拟鼠标键盘Agent SDK驱动决策引擎。我试过用纯LangChain链式调用处理一个采购审批流程当审批人临时要求加一道“法务合规性校验”时整个链就断了——因为链是线性的而真实业务是网状的。LangGraph的图结构恰恰就是为这种“动态路由状态记忆工具调用”的需求而生。适合谁来学不是只面向AI工程师。如果你是业务分析师能用自然语言描述清楚“从客户询价到生成合同”的每一步判断依据如果你是IT支持常被业务部门拉着说“能不能让系统自动做XX”如果你是产品经理正在设计一个需要集成多个SaaS服务的新功能——那你就是这个项目的天然Owner。技术门槛其实不高你不需要自己训练模型不需要调参甚至不需要懂Transformer架构。你需要的是把业务规则拆解成“如果…那么…”、“先做A再根据结果决定做B还是C”这样的原子逻辑然后用Python把它喂给Agent SDK。我带过一个财务部的同事她连pip install都不会但用三天时间就把月度费用报销初审流程核对发票金额、匹配预算科目、识别异常报销人变成了一个可运行的LangGraph工作流。关键不在代码多难而在你是否真正理解业务里的“决策点”在哪里。2. 整体设计思路为什么必须用图Graph而不是链Chain2.1 业务场景的本质是“状态机”不是“流水线”我们先看一个具体案例某跨境电商平台的售后工单处理流程。用户提交退货申请后系统要走完以下路径步骤1检查订单是否在7天无理由退货期内步骤2若超期进入“特殊审批”分支若未超期继续步骤3查询库存确认该SKU是否仍有现货可换步骤4若有现货生成换货单并通知仓库若无现货触发“采购补货”子流程步骤5无论走哪条路最终都要更新CRM中的客户满意度标签这个流程里状态如“退货申请已提交”、“库存核查完成”、“换货单已生成”是核心分支条件时间、库存、客户等级是驱动器子流程采购补货是嵌套模块。传统Chain模式像一条笔直的水管数据从入口灌入依次经过Filter1→Filter2→Filter3最后流出。但现实业务中数据流会像河流遇到礁石一样分叉、汇合、回流。LangGraph的图结构就是把每个处理环节定义为一个节点Node把节点间的流转规则定义为边Edge而整个工作流就是一个有向无环图DAG。节点可以是函数、LLM调用、工具执行边则承载着条件判断逻辑。这种设计不是炫技而是对业务复杂度的诚实回应。2.2 Python作为“业务逻辑翻译器”的不可替代性有人会问既然有低代码平台为什么还要写Python答案很实在低代码平台的“积木块”永远跟不上业务变化的速度。比如某次促销活动要求新增一个“根据用户最近3次购买品类动态调整退货优先级”的规则。低代码平台可能需要等产品团队排期开发新组件而用Python你只需要在对应节点里加几行pandas代码def calculate_priority(state: dict) - dict: user_id state[user_id] # 从数据库获取该用户最近3次订单 recent_orders db.query(SELECT category FROM orders WHERE user_id ? ORDER BY created_at DESC LIMIT 3, user_id) categories [row[category] for row in recent_orders] # 计算品类集中度香农熵 from collections import Counter freq Counter(categories) entropy -sum((v/len(categories)) * math.log2(v/len(categories)) for v in freq.values()) # 集中度越高熵越低优先级越高 state[priority_score] 10 - (entropy * 2) return state这段代码的核心价值不在于它多精妙而在于它把一句模糊的业务需求“动态调整优先级”精准地锚定在了可执行、可测试、可版本管理的代码上。Python在这里扮演的角色是业务语言和机器指令之间的“实时翻译官”。它不负责思考但确保每一个业务判断都被无歧义地表达出来。我见过太多项目失败不是因为模型不够强而是因为业务规则在口头传递中层层失真最终写进代码的已是面目全非的“二手需求”。2.3 Agent SDK选型LangGraph为何成为当前最优解目前主流Agent框架有LangChain Agents、LlamaIndex Agents、以及专为图编排设计的LangGraph。选择LangGraph是基于三个硬性指标的权衡状态管理能力LangChain Agents的状态是隐式的、易丢失的。当你在一个长链中调用多个工具后想回溯到某个中间状态重新决策几乎不可能。LangGraph强制要求每个节点接收一个state字典并返回更新后的state所有中间结果都显式存于内存或Redis中。这直接解决了“流程中断后如何续跑”的痛点。调试友好性LangGraph提供app.get_graph().draw_mermaid_png()方法注意我们不生成图片但可输出mermaid文本供本地渲染能一键生成可视化流程图。我在调试一个涉及5个外部API调用的供应链预警流程时靠这张图3分钟就定位到是“物流信息查询节点”返回了空数据而非去翻几十行日志。而LangChain的调试往往需要在每个chain的verboseTrue下逐行看token流效率极低。与现有生态的兼容性LangGraph不是另起炉灶它深度兼容LangChain的Tool、LLM、PromptTemplate等组件。这意味着你不必抛弃已有的Prompt工程成果只需把原来的SequentialChain重构为StateGraph即可。我团队的一个项目原有30多个LangChain chain迁移至LangGraph仅用了2天核心改动就是把chain.run()替换为graph.invoke()其余Prompt、Tool、Memory全部复用。提示不要被“SDK”二字迷惑。LangGraph本身不提供大模型它只是一个编排引擎。你的LLM可以是OpenAI、Anthropic、本地部署的Qwen甚至是一个规则引擎。它的价值在于让你能把不同来源的“智能组件”像乐高一样严丝合缝地拼在一起。3. 核心细节解析从零搭建一个可落地的售后工单Agent3.1 环境准备避开Python包管理的“雷区”很多新手卡在第一步环境装不上。根本原因不是网络而是Python版本和包依赖的“蝴蝶效应”。我推荐一套经过20个项目验证的最小可行环境# 1. 创建独立虚拟环境强烈建议避免全局污染 python -m venv ./agent_env source agent_env/bin/activate # Linux/Mac # agent_env\Scripts\activate.bat # Windows # 2. 升级pip到最新版旧版pip安装复杂依赖时极易失败 pip install --upgrade pip # 3. 安装核心依赖按此顺序避免版本冲突 pip install langgraph0.1.20 # 固定版本避免API突变 pip install langchain0.1.22 pip install openai1.35.0 # OpenAI SDK稳定版 pip install pandas2.2.2 # 数据处理刚需 pip install redis4.6.0 # 可选用于生产环境状态持久化关键细节为什么固定版本LangGraph 0.1.x和0.2.x的API有重大变更如StateGraph类名改为CompiledGraph而网上90%的教程基于0.1.x。固定版本是节省时间的最有效方式。为什么先升级pippip 22.x以下版本在解析pydantic依赖时存在缓存bug会导致langgraph安装失败报错信息却是“找不到module typing_extensions”极具误导性。redis不是必须项开发阶段用内存存储完全够用只有当工作流需要支持多实例并发、或流程执行时间超过10分钟防止进程崩溃丢失状态时才引入redis。3.2 定义业务状态State让数据自己“说话”在LangGraph中“状态”不是抽象概念而是一个具体的Python字典它贯穿整个工作流生命周期。设计State是项目成败的关键一步。以售后工单为例一个健壮的State定义如下from typing import TypedDict, List, Optional, Dict, Any from datetime import datetime class TicketState(TypedDict): ticket_id: str # 工单唯一ID user_id: str # 用户ID order_id: str # 关联订单号 created_at: datetime # 工单创建时间 status: str # 当前状态pending, checking, approved, rejected, completed priority_score: float # 优先级分数0-10 inventory_check_result: Optional[Dict[str, Any]] # 库存检查原始结果 replacement_sku: Optional[str] # 换货SKU error_message: Optional[str] # 最近一次错误信息 audit_log: List[str] # 审计日志记录每一步操作 # 注意不要在这里放敏感数据如用户手机号、银行卡号应通过单独的加密服务获取这个定义的精妙之处在于audit_log字段不是为了监控而是为了“可解释性”。当业务方质疑“为什么这个工单被拒绝了”你直接输出state[audit_log]就能看到完整决策链“2024-05-20 14:22:03 - 库存检查SKU-12345无现货2024-05-20 14:22:05 - 触发采购补货流程...”。这比任何日志系统都直观。error_message字段专门用于错误传递。当某个节点如调用ERP接口失败时不抛异常中断流程而是将错误信息写入此字段并由后续的handle_error节点统一处理。这保证了工作流的韧性。类型提示TypedDict强制IDE和mypy进行静态检查避免state[user_id]写成state[user_idd]这类低级错误。我在一个项目中因少写了一个字母导致2000个工单被错误路由修复花了6小时——从此所有State都用TypedDict。3.3 构建核心节点Nodes每个节点只做一件事LangGraph的哲学是“单一职责”。一个节点不应既查库存又发邮件而应只做库存检查。以下是售后工单流程的四个核心节点实现# 节点1检查退货时效 def check_return_window(state: TicketState) - TicketState: from datetime import timedelta order get_order_by_id(state[order_id]) # 假设这是你的DB查询函数 days_since_order (datetime.now() - order[created_at]).days if days_since_order 7: state[status] special_approval state[audit_log].append(f{datetime.now()} - 超过7天无理由退货期进入特殊审批) else: state[status] checking_inventory state[audit_log].append(f{datetime.now()} - 在退货期内进入库存检查) return state # 节点2库存检查调用外部API def check_inventory(state: TicketState) - TicketState: try: # 调用WMS系统API response requests.get( fhttps://wms-api.example.com/stock/{state[order_id]}, timeout5 ) response.raise_for_status() stock_data response.json() state[inventory_check_result] stock_data if stock_data.get(available_quantity, 0) 0: state[replacement_sku] stock_data[sku] state[status] generate_replacement state[audit_log].append(f{datetime.now()} - 库存充足SKU {stock_data[sku]} 可换货) else: state[status] trigger_procurement state[audit_log].append(f{datetime.now()} - 库存不足触发采购补货) except Exception as e: state[error_message] f库存检查失败: {str(e)} state[status] error_handling state[audit_log].append(f{datetime.now()} - 库存检查异常: {str(e)}) return state # 节点3生成换货单调用ERP def generate_replacement_order(state: TicketState) - TicketState: try: # 构造ERP换货单数据 erp_payload { ticket_id: state[ticket_id], sku: state[replacement_sku], quantity: 1, warehouse_code: WH-MAIN } response requests.post(https://erp-api.example.com/replacement, jsonerp_payload) response.raise_for_status() state[status] notify_warehouse state[audit_log].append(f{datetime.now()} - 换货单已生成等待仓库执行) except Exception as e: state[error_message] f生成换货单失败: {str(e)} state[status] error_handling return state # 节点4错误处理兜底 def handle_error(state: TicketState) - TicketState: # 发送告警给运维群 send_alert_to_slack(f工单 {state[ticket_id]} 处理失败: {state[error_message]}) # 更新CRM标记为人工介入 update_crm_ticket_status(state[ticket_id], manual_intervention) state[status] manual_intervention state[audit_log].append(f{datetime.now()} - 错误已上报转人工处理) return state实操心得节点函数必须是纯函数只接收state只返回state不修改外部变量不产生副作用如发邮件、写文件应在节点内完成。这保证了可测试性和可重入性。超时控制是生命线所有外部API调用必须设置timeout否则一个慢接口会让整个工作流卡死。我吃过亏某次物流查询API响应长达45秒导致100个并发工单堆积最终OOM崩溃。审计日志要包含时间戳不要只写“库存检查完成”而要写“2024-05-20 14:22:03 - 库存检查完成”。时间戳是排查时序问题的唯一依据。3.4 定义流转边Edges用条件函数代替硬编码跳转边Edge决定了工作流的“血液流向”。LangGraph支持两种边无条件边always和条件边conditional。后者才是业务逻辑的灵魂。我们为售后工单定义条件边from langgraph.graph import END, START def route_after_window_check(state: TicketState) - str: 根据时效检查结果决定下一步 if state[status] special_approval: return special_approval_node # 路由到特殊审批节点 else: return check_inventory # 路由到库存检查节点 def route_after_inventory_check(state: TicketState) - str: 根据库存检查结果决定下一步 if state[status] generate_replacement: return generate_replacement_order elif state[status] trigger_procurement: return trigger_procurement_node else: # error return handle_error # 构建图 workflow StateGraph(TicketState) # 添加节点 workflow.add_node(check_return_window, check_return_window) workflow.add_node(check_inventory, check_inventory) workflow.add_node(generate_replacement_order, generate_replacement_order) workflow.add_node(handle_error, handle_error) # ... 其他节点 # 添加边START是内置起点 workflow.set_entry_point(check_return_window) # 添加条件边 workflow.add_conditional_edges( check_return_window, route_after_window_check, { special_approval_node: special_approval_node, check_inventory: check_inventory } ) workflow.add_conditional_edges( check_inventory, route_after_inventory_check, { generate_replacement_order: generate_replacement_order, trigger_procurement_node: trigger_procurement_node, handle_error: handle_error } ) # 添加无条件边 workflow.add_edge(generate_replacement_order, notify_warehouse) workflow.add_edge(handle_error, END) # END是内置终点 # 编译图 app workflow.compile()关键技巧条件函数返回字符串而非状态值route_after_window_check返回的是目标节点名如check_inventory不是state[status]。这解耦了业务状态和流程控制让状态可以更丰富如status为checking_inventory但流程仍可路由到check_inventory节点。END是真正的终点一旦走到END工作流就彻底结束。不要试图在END之后再做任何事所有收尾工作如发成功通知必须放在到达END前的最后一个节点里。set_entry_point必须明确不能假设第一个添加的节点就是起点。显式声明是避免歧义的最佳实践。4. 实操过程从本地测试到生产部署的全流程4.1 本地快速验证用一个工单跑通端到端在敲下app.invoke()之前先用一个最简化工单数据验证流程骨架# 构造测试数据 test_state TicketState( ticket_idTICKET-001, user_idUSER-123, order_idORDER-456, created_atdatetime(2024, 5, 15, 10, 0, 0), statuspending, priority_score0.0, inventory_check_resultNone, replacement_skuNone, error_messageNone, audit_log[] ) # 执行工作流 result app.invoke(test_state) print(最终状态:, result[status]) print(审计日志:) for log in result[audit_log]: print( , log)预期输出最终状态: notify_warehouse 审计日志: 2024-05-20 14:22:03 - 在退货期内进入库存检查 2024-05-20 14:22:05 - 库存充足SKU SKU-12345 可换货 2024-05-20 14:22:07 - 换货单已生成等待仓库执行如果输出不符合预期按以下顺序排查检查app.invoke()的输入是否符合TicketState定义如created_at必须是datetime对象不能是字符串查看audit_log中最后一条日志定位卡在哪个节点单独调用该节点函数传入当前state观察返回值检查该节点内的外部依赖如API URL、数据库连接是否在本地可达。注意本地测试时所有外部API调用应使用unittest.mock打桩避免真实调用。例如from unittest.mock import patch patch(requests.get) def test_check_inventory(mock_get): mock_get.return_value.json.return_value {available_quantity: 1, sku: SKU-12345} # 然后调用节点函数...4.2 集成真实工具让Agent“动手干活”Agent的价值在于调用工具。以“发送通知邮件”为例这不是一个节点而是一个LangChainToolfrom langchain.tools import BaseTool from pydantic import BaseModel, Field from typing import Optional class SendEmailInput(BaseModel): to: str Field(..., description收件人邮箱) subject: str Field(..., description邮件主题) body: str Field(..., description邮件正文) class SendEmailTool(BaseTool): name send_email description 向指定邮箱发送通知邮件。用于工单状态更新、审批结果告知等。 args_schema: type[BaseModel] SendEmailInput def _run(self, to: str, subject: str, body: str) - str: try: # 这里调用你的邮件服务如SendGrid、SMTP sendgrid_client.send( to_emailsto, subjectsubject, contentbody ) return f邮件已成功发送至 {to} except Exception as e: return f邮件发送失败: {str(e)} # 将工具注册到工作流中 tools [SendEmailTool()]然后在需要发邮件的节点如generate_replacement_order里调用它def generate_replacement_order(state: TicketState) - TicketState: # ... 前面的ERP调用逻辑 ... # 发送通知邮件 email_tool SendEmailTool() email_result email_tool._run( towarehousecompany.com, subjectf换货单待处理{state[ticket_id]}, bodyf请处理工单 {state[ticket_id]} 的换货请求SKU: {state[replacement_sku]} ) state[audit_log].append(f{datetime.now()} - {email_result}) return state经验之谈工具必须有清晰的description这是LLM决定是否调用它的唯一依据。描述要包含“用途”和“适用场景”如“用于工单状态更新、审批结果告知等”而不是“发送邮件”。工具返回值必须是字符串LangGraph要求所有工具返回str便于日志记录和LLM理解。不要返回dict或None。工具调用要加异常捕获邮件服务宕机是常态不能让一次发信失败导致整个工单流程中断。4.3 生产环境部署轻量级但可靠的方案生产环境不追求K8s、Service Mesh而要“小而韧”。我的标准部署栈是组件选型理由Web服务FastAPI轻量、异步、文档自动生成完美适配Agent的HTTP API暴露需求任务队列Celery Redis处理长耗时任务如批量工单处理避免Web请求超时状态存储Redis速度快、支持过期、原生支持分布式state存为JSON字符串日志Structured Logging (structlog)日志字段化便于ELK或Grafana分析FastAPI接口示例from fastapi import FastAPI, HTTPException from pydantic import BaseModel app_fastapi FastAPI() class TicketRequest(BaseModel): ticket_id: str user_id: str order_id: str app_fastapi.post(/process_ticket) async def process_ticket(request: TicketRequest): try: # 构建初始state initial_state TicketState( ticket_idrequest.ticket_id, user_idrequest.user_id, order_idrequest.order_id, created_atdatetime.now(), statuspending, priority_score0.0, inventory_check_resultNone, replacement_skuNone, error_messageNone, audit_log[] ) # 异步执行工作流 result await asyncio.to_thread(app.invoke, initial_state) return {success: True, final_status: result[status], audit_log: result[audit_log]} except Exception as e: raise HTTPException(status_code500, detailf处理失败: {str(e)})部署命令使用Uvicorn# 启动Web服务 uvicorn main:app_fastapi --host 0.0.0.0:8000 --workers 4 --reload # 启动Celery worker处理后台任务 celery -A tasks worker --loglevelinfo关键配置--workers 4Uvicorn工作进程数建议设为CPU核心数。过多进程会争抢Redis连接。Redis连接池在LangGraph配置中必须设置连接池避免高并发下连接耗尽import redis redis_client redis.Redis( connection_poolredis.ConnectionPool( hostlocalhost, port6379, db0, max_connections20 # 根据并发量调整 ) )5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “状态丢失”问题为什么我的state在节点间不更新现象node_a里给state[foo] bar但在node_b里state[foo]是None。根本原因Python的字典是可变对象但LangGraph要求每个节点返回一个新字典而不是修改原字典。如果你在节点里直接state[foo] bar然后return state看似正确实则危险——因为state是引用传递上游节点可能还在用这个字典。正确做法始终用copy()或构造新字典# ❌ 错误修改原字典 def bad_node(state: dict) - dict: state[foo] bar return state # 返回的是被修改的原字典 # ✅ 正确返回新字典 def good_node(state: dict) - dict: new_state state.copy() # 浅拷贝 new_state[foo] bar return new_state # ✅ 更推荐用TypedDict的构造方式如果定义了 def best_node(state: TicketState) - TicketState: return TicketState( **state, # 展开原有字段 foobar # 覆盖或新增字段 )排查技巧在每个节点开头加一行日志def debug_node(state: TicketState) - TicketState: print(进入节点前state:, {k: v for k, v in state.items() if k ! audit_log}) # 排除长日志干扰 # ... 节点逻辑 ...5.2 “循环调用”陷阱工作流像陀螺一样停不下来现象工作流启动后CPU飙升日志里反复出现同一节点的日志直到内存耗尽。典型诱因条件边的路由函数返回了错误的目标节点名导致流程在两个节点间无限跳转。例如# 错误示例路由函数逻辑有漏洞 def flawed_route(state: TicketState) - str: if state[status] checking_inventory: return check_inventory # 自己调自己 return handle_error解决方案强制要求所有路由函数有兜底返回即使业务上认为不会走到也要写else: return handle_error在图编译时启用循环检测app workflow.compile( checkpointerMemorySaver(), # 启用检查点 interrupt_before[handle_error], # 可选在特定节点前中断便于调试 )设置最大迭代次数LangGraph 0.1.x不直接支持需在节点内手动计数def safe_node(state: TicketState) - TicketState: # 从state中读取计数或用Redis全局计数器 step_count state.get(step_count, 0) 1 if step_count 10: # 防止死循环 state[error_message] 工作流执行超10步疑似死循环 state[status] error_handling return state state[step_count] step_count # ... 正常逻辑 ...5.3 “工具调用失败”LLM明明说要调用却没执行现象日志显示LLM输出了{action: send_email, action_input: {...}}但邮件没发出去。真相LangGraph的ToolNode需要显式添加到图中且必须与LLM的tool call schema严格匹配。常见错误工具名不一致LLM的tool_call里是send_email但你注册的工具name是email_sender参数名不匹配LLM传入{to_address: ab.com}但工具args_schema定义的是to: str缺少ToolNode只定义了Tool但没在图中添加workflow.add_node(tools, ToolNode(tools))。快速验证法在LLM节点后加一个print(LLM输出:, llm_output)手动解析llm_output确认tool_calls字段存在且格式正确检查ToolNode是否已添加且其输入state中包含messages字段这是LangGraph约定的LLM输出存放位置。5.4 “性能瓶颈”为什么100个并发就卡死数据本地测试单请求200ms但100并发时平均响应时间飙升到5s。根因分析表瓶颈环节表现检测方法解决方案Redis连接池耗尽redis.exceptions.ConnectionErrorredis-cli info clients查看connected_clients增大max_connections或改用连接池共享外部API限流WMS/ERP接口返回429查看API响应头X-RateLimit-Remaining在工具调用层加指数退避重试CPU密集型计算top显示Python进程CPU 100%py-spy record -o profile.svg --pid pid将pandas计算移到Celery异步任务中锁竞争多个工单同时更新同一订单状态日志中出现Order ORDER-456 is locked在DB层加行级锁或用乐观锁version字段终极优化技巧对高频、低延迟的节点如check_return_window用lru_cache缓存结果from functools import lru_cache lru_cache(maxsize1000) def get_order_by_id_cached(order_id: str) - dict: return get_order_by_id(order_id) # 原始DB查询函数 def check_return_window(state: TicketState) - TicketState: order get_order_by_id_cached(state[order_id]) # 缓存生效 # ... 其余逻辑6. 进阶应用让工作流具备“学习”和“进化”能力6.1 基于反馈的流程自优化一个真正智能的工作流不应是静态的。我们可以利用业务反馈数据动态调整流程参数。例如统计过去一周内“特殊审批”工单的最终通过率如果通过率 80%说明当前7天退货期规则过于保守可建议业务方延长至10天如果通过率 20%说明规则合理但“特殊审批”节点的处理效率低需优化。实现方式在END节点后添加一个post_process钩子def post_process(state: TicketState) - None: if state[status] completed: # 记录成功工单 redis_client.incr(ticket_success_count) elif state[status] manual_intervention: # 记录人工介入工单用于分析 redis_client.hincrby(manual_intervention_reasons, state[error_message], 1) # 每100个工单触发一次分析 total int(redis_client.get(total_tickets) or 0) if total % 100 0: trigger_analysis_job() # 在FastAPI接口中调用 result app.invoke(initial_state)