ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM+LangGraph重构报价审批:从规则引擎到智能工作流实战

LLM+LangGraph重构报价审批:从规则引擎到智能工作流实战 1. 报价审批为什么值得用 LLM 和工作流引擎重做一遍做过企业信息化的人大概都有同一个感受审批流这东西搭起来不难难的是让它真正聪明起来。传统的报价审批系统本质上就是一张表单加一串 if-else 判断——金额超过多少走哪个节点折扣低于几折触发哪级领导客户等级是什么就套哪套模板。规则写死了系统就只会机械执行一旦业务场景稍微复杂一点比如客户临时要求特殊付款条件、报价里混了非标产品、或者销售在备注里写了一段模棱两可的说明系统就彻底抓瞎只能把皮球踢给人工。我所在的团队去年接手了一个典型的报价审批改造项目。背景很朴素公司有几百个销售每天产生的报价单少则几十份多则上百份审批链路涉及销售主管、区域经理、财务、法务、甚至有时候要走到副总。原来的系统是采购的一套标准 OA 流程规则引擎配置得密密麻麻但实际跑起来问题一大堆。最典型的就是规则覆盖不全——业务部门每次遇到新情况就提需求加规则加了两年规则表膨胀到上千条维护的人自己都说不清哪条还有效。另一个痛点是审批意见质量参差不齐有的领导就写个同意有的写一大段但跟报价本身没关系财务想追溯依据都找不到。这个项目的核心思路就是用LLM来做理解层用工作流引擎来做编排层把原来那种规则匹配 人工兜底的模式改造成语义理解 动态路由 人工确认的模式。说白了让大模型去读报价单里的非结构化信息判断这单的风险点在哪、该走什么路径、需要谁来看然后由工作流引擎负责把审批任务准确地派发下去、跟踪状态、处理异常。这里面LangGraph是我们选的工作流编排框架后面会详细讲为什么选它而不是别的。这篇文章适合几类人看一是正在做企业流程自动化、想引入 LLM 但不知道从哪下手的工程师二是被审批流规则维护折磨过的产品经理或业务负责人三是对 LangGraph 感兴趣、想看看它在真实企业场景里怎么落地的开发者。我会把整个改造过程拆开讲包括架构设计、关键实现、踩过的坑以及那些文档里不会写的经验。代码会给关键片段但不会贴全量重点是思路和取舍逻辑。先说结论这套东西跑起来之后报价审批的平均处理时长从原来的 4.2 小时降到了 1.1 小时人工干预率从 68% 降到了 23%而且审批意见的完整度明显提升——因为 LLM 会在派发任务时自动附上这单为什么需要你审的摘要。当然过程中也踩了不少坑比如 LLM 输出不稳定、工作流状态丢失、并发审批冲突等等这些后面都会讲到。2. 整体架构设计与技术选型思路2.1 为什么是LLM 工作流引擎而不是纯 LLM 或纯规则一开始团队里有过争论既然 LLM 这么强能不能直接让模型端到端处理整个审批答案是能但不敢用。原因有三个。第一审批是有状态的长流程一份报价单可能在不同节点之间来回流转好几天LLM 的无状态特性天然不适合做状态管理。第二审批需要可追溯、可审计每一步谁做的决定、依据是什么必须落库纯 LLM 的黑盒输出满足不了合规要求。第三成本和延迟如果每次状态变更都调一次大模型token 消耗和响应时间都扛不住。反过来纯规则引擎的问题前面已经说了规则膨胀、覆盖不全、维护成本高。所以我们的思路是让两者各干各擅长的事LLM 负责读不懂的东西比如报价备注里的自然语言、非标产品的描述、客户特殊要求的语义工作流引擎负责管得住的东西比如状态机、任务派发、超时提醒、权限校验。两者之间通过一个结构化的决策结果来衔接——LLM 输出 JSON工作流引擎消费这个 JSON 来决定下一步走向。这个分工的关键在于边界要清晰。我们定了一条铁律LLM 只做建议和提取不做决定。也就是说模型可以判断这单折扣异常建议升级审批但最终是否升级、升级到谁由工作流引擎根据预设策略来执行。这样既利用了模型的语义理解能力又保留了流程的可控性。2.2 LangGraph 在其中的角色和选型理由工作流引擎的选择上我们评估过几个方案传统 BPMN 引擎如 Camunda、自己写状态机、以及 LangGraph。最后选 LangGraph主要基于几点考虑。Camunda 这类 BPMN 引擎功能很全但它的强项是人机协作流程和标准化建模跟 LLM 的集成需要额外写不少胶水代码而且它的流程定义是 XML改起来不够灵活。自己写状态机倒是轻量但一旦流程复杂起来状态转移的管理、异常处理、持久化都得自己造轮子维护成本高。LangGraph 的优势在于它天生就是为LLM 参与的流程设计的。它的核心抽象是图Graph节点Node可以是 LLM 调用、可以是普通函数、也可以是工具调用边Edge可以是条件跳转。这跟审批流的本质非常契合——审批流本身就是一张有向图每个审批节点是一个状态条件边决定下一步去哪。而且 LangGraph 内置了状态管理State和检查点Checkpoint机制天然支持长流程的持久化和恢复这正是审批场景需要的。提示LangGraph 的 Checkpoint 机制默认用内存存储生产环境一定要换成数据库后端比如 Postgres否则服务重启后流程状态全丢。这个坑我们踩过后面会细说。另外LangGraph 对人在回路Human-in-the-loop的支持比较友好可以通过interrupt机制在某个节点暂停等人工输入后再继续。审批场景里领导审批本质上就是一个人工节点这个特性直接能用。2.3 整体数据流和模块划分整个系统的数据流大致是这样的销售提交报价单 - 系统做预处理格式校验、字段提取- LLM 分析节点提取风险点、生成审批建议- 工作流引擎根据分析结果决定路由 - 派发审批任务给对应人员 - 审批人操作同意/驳回/加签- 工作流继续流转或结束 - 结果落库并通知销售。模块上我们分了四层接入层负责接收报价单数据做基础校验和格式化对外提供 REST API。智能分析层封装 LLM 调用包括 prompt 管理、输出解析、重试逻辑、缓存。流程编排层基于 LangGraph 构建的审批流程图管理状态和路由。持久化与通知层审批记录落库、消息推送、超时提醒。这四层之间通过明确的接口通信智能分析层不直接操作数据库流程编排层不直接调 LLM职责分离得很清楚。这样做的好处是后面如果要换模型或者换工作流框架改动范围可控。3. 核心细节解析与实操要点3.1 LLM 分析节点到底分析什么这是整个系统里最核心也最容易做砸的部分。很多人一上来就想让 LLM判断这单该不该批这个目标太模糊模型输出会很不稳定。我们的做法是把分析任务拆成几个具体的、可验证的子任务。第一个子任务是关键信息提取。报价单里有很多非结构化字段比如备注、特殊说明、客户附加要求这些字段里藏着关键信息。我们让 LLM 从中提取出结构化的风险要素比如是否涉及非标产品、是否有特殊付款条件、是否要求加急、是否涉及竞争对手比价等。输出格式是固定的 JSON schema每个要素是一个布尔值或枚举值。第二个子任务是风险评分。基于提取出的要素结合报价金额、折扣率、客户历史等结构化数据让 LLM 给出一个风险等级低/中/高和理由。这里要注意风险评分不是让模型拍脑袋而是在 prompt 里给出明确的评分规则和示例让模型按规则来。第三个子任务是审批建议生成。模型根据风险等级建议走哪条审批路径并生成一段给审批人看的摘要说明这单为什么需要你审、重点看什么。这段摘要直接附在审批任务里审批人打开就能看到不用自己去翻报价单。# 简化的 LLM 分析节点示意 from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import JsonOutputParser analysis_prompt ChatPromptTemplate.from_messages([ (system, 你是一个报价审批分析助手。请根据以下报价单信息 提取风险要素并给出风险等级。严格按照 JSON 格式输出。 风险要素包括is_nonstandard是否非标、has_special_payment特殊付款条件、 is_urgent是否加急、has_competitor是否涉及比价。 风险等级规则金额50万或折扣7折为高风险金额20万或折扣8折为中风险其余为低风险。), (human, 报价单信息{quote_info}) ]) parser JsonOutputParser() analysis_chain analysis_prompt | llm | parser注意prompt 里的规则一定要写得足够具体最好带几个 few-shot 示例。我们一开始规则写得太笼统模型经常把中风险判成高风险后来加了三个标注好的示例准确率明显提升。3.2 工作流图怎么设计才不容易乱LangGraph 的图设计直接决定了流程的可维护性。我们的原则是一个节点只做一件事不要把多个逻辑塞进一个节点。整个审批图大致有这些节点preprocess预处理校验数据完整性。analyze调 LLM 做分析。route_decision根据分析结果决定路由这是一个条件边。approve_manager主管审批节点。approve_finance财务审批节点。approve_legal法务审批节点。approve_vp副总审批节点。finalize结束节点落库并通知。节点之间的边用条件函数控制。比如route_decision之后根据风险等级和金额可能直接跳到approve_manager也可能跳到approve_finance高风险还要经过approve_vp。from langgraph.graph import StateGraph, END from typing import TypedDict class ApprovalState(TypedDict): quote_info: dict analysis_result: dict current_approver: str approval_history: list status: str def route_decision(state: ApprovalState) - str: risk state[analysis_result][risk_level] amount state[quote_info][amount] if risk high or amount 500000: return approve_vp elif risk medium or amount 200000: return approve_finance else: return approve_manager graph StateGraph(ApprovalState) graph.add_node(preprocess, preprocess_node) graph.add_node(analyze, analyze_node) graph.add_node(approve_manager, manager_node) # ... 其他节点 graph.add_conditional_edges(analyze, route_decision, { approve_vp: approve_vp, approve_finance: approve_finance, approve_manager: approve_manager })这里有个关键点状态State的设计要包含所有节点需要的信息。我们一开始 State 定义得太窄后来加节点时发现缺字段又得回头改很麻烦。建议一开始就把可能用到的字段都放进去宁可冗余。3.3 人在回路怎么实现才不别扭审批场景里人是绕不开的。LangGraph 提供了interrupt机制可以在节点执行前暂停等外部输入后再恢复。但实际用起来有几个细节要注意。首先是暂停后的状态持久化。如果审批人半天不处理服务重启了流程状态不能丢。这就要求 Checkpoint 必须用持久化后端。我们用的是 PostgresLangGraph 有现成的PostgresSaver。其次是恢复时的输入校验。审批人可能同意、驳回、或者加签不同操作对应不同的后续路径。恢复流程时要把审批人的操作作为输入传进去然后由条件边决定下一步。from langgraph.checkpoint.postgres import PostgresSaver checkpointer PostgresSaver.from_conn_string(postgresql://...) app graph.compile(checkpointercheckpointer, interrupt_before[approve_manager, approve_finance, approve_vp]) # 审批人操作后恢复 app.invoke({approval_action: approve, comment: 同意}, config{configurable: {thread_id: quote_id}})提示interrupt_before是在节点执行前暂停适合等人审批的场景。如果你需要节点执行到一半暂停用interrupt函数更灵活。我们两种都用过最后统一用interrupt_before逻辑更清晰。3.4 输出解析和容错处理LLM 输出不稳定是常态尤其是要求 JSON 格式时模型可能多写一句话、少一个括号、或者字段名拼错。我们的处理策略是三层防护。第一层是prompt 约束明确要求只输出 JSON不要任何额外文字并给出 schema 示例。第二层是解析容错用JsonOutputParser配合自定义的修复逻辑比如自动补全缺失的括号、去掉 markdown 代码块标记。第三层是重试机制如果解析失败把错误信息拼回 prompt 让模型重新生成最多重试两次。def safe_parse(raw_output: str, max_retry: int 2) - dict: for i in range(max_retry 1): try: cleaned raw_output.strip().removeprefix(json).removesuffix().strip() return json.loads(cleaned) except json.JSONDecodeError as e: if i max_retry: raise raw_output llm.invoke(f上次输出解析失败{e}请重新输出合法 JSON{raw_output})实测下来加了这三层之后解析成功率从 82% 提升到了 99% 以上。剩下那 1% 基本是模型彻底跑偏直接走人工兜底。4. 实操过程与核心环节实现4.1 环境准备和依赖安装先把基础环境搭起来。我们用的是 Python 3.11LangGraph 和 LangChain 的版本要匹配不然会有兼容性问题。建议用虚拟环境依赖锁版本。python -m venv venv source venv/bin/activate pip install langgraph0.2.x langchain0.3.x langchain-openai psycopg[binary] fastapi uvicorn数据库用 Postgres主要是为了 Checkpoint 持久化。建库建表不用手动做LangGraph 的PostgresSaver会自动建表但需要先确保数据库连接可用。from langgraph.checkpoint.postgres import PostgresSaver with PostgresSaver.from_conn_string(DB_URI) as checkpointer: checkpointer.setup() # 自动建表注意setup()只需要跑一次重复跑不会报错但也没必要。生产环境建议把这一步放到部署脚本里别每次启动都跑。4.2 报价单预处理和字段标准化报价单的来源可能很多样有的是系统导出的 JSON有的是 Excel 上传字段名五花八门。预处理节点的任务就是把这些统一成内部标准格式。我们定义了一个标准的QuoteInfo结构包含报价单号、客户名称、客户等级、报价金额、折扣率、产品列表、付款条件、备注、提交人、提交时间。预处理节点负责字段映射、类型转换、必填校验。如果关键字段缺失直接打回让销售补全不进审批流。这一步看起来简单但实际做的时候发现字段映射很麻烦因为不同来源的字段名差异很大。我们的做法是维护一张映射表配置化处理新增来源时改配置不改代码。4.3 LLM 分析节点的完整实现分析节点是整个流程的大脑实现上分几步。先组装 prompt把报价单信息和评分规则一起塞进去。然后调模型拿到原始输出。接着做解析和校验确保输出符合 schema。最后把结果写回 State。def analyze_node(state: ApprovalState) - ApprovalState: quote state[quote_info] prompt_input format_quote_for_prompt(quote) raw analysis_chain.invoke({quote_info: prompt_input}) validated validate_analysis(raw) state[analysis_result] validated return statevalidate_analysis里做了几件事检查必填字段是否存在、枚举值是否合法、风险等级和金额是否矛盾比如金额很小但判了高风险要标记出来人工复核。这一步是防止模型胡说八道的关键防线。4.4 工作流图的编译和运行图定义好之后编译成可执行的应用。编译时要传入 checkpointer否则没有持久化能力。app graph.compile( checkpointercheckpointer, interrupt_before[approve_manager, approve_finance, approve_vp] )运行时用thread_id来标识每个报价单的流程实例这个 ID 用报价单号就行保证唯一。config {configurable: {thread_id: quote_id}} result app.invoke({quote_info: quote_info}, configconfig)如果流程在某个审批节点暂停了result里会包含当前状态。审批人操作后再次invoke并传入操作结果流程继续。4.5 审批任务的派发和通知工作流暂停后需要通知对应的审批人。我们的做法是在暂停节点之后加一个通知逻辑或者用 LangGraph 的 stream 模式监听状态变化。实际实现上我们在interrupt_before触发后从 State 里读出current_approver然后调消息服务发通知。通知内容包含报价单摘要、LLM 生成的风险说明、审批链接。审批人点链接进入审批页面操作后调后端接口恢复流程。提示通知一定要带上下文别只发个你有个审批待处理。我们一开始通知太简略审批人还得自己去系统里翻体验很差。后来把 LLM 生成的摘要直接放进通知审批人扫一眼就知道要不要马上处理。4.6 审批结果的落库和流程收尾每次审批操作都要落库记录审批人、操作、意见、时间。这些记录既用于审计也用于后续优化模型——哪些单子被驳回了、驳回理由是什么都是宝贵的训练数据。流程走到finalize节点时汇总所有审批记录更新报价单状态通知销售结果。如果被驳回还要把驳回理由整理好发给销售方便修改后重新提交。5. 常见问题与排查技巧实录5.1 LLM 输出格式不稳定怎么办这是最高频的问题。表现是模型有时候输出纯 JSON有时候包在 markdown 代码块里有时候前面加一句好的以下是分析结果。解决办法前面提过核心是三层防护prompt 约束、解析容错、重试。另外一个小技巧是在 prompt 里明确说不要输出任何解释性文字比说只输出 JSON更有效。还有一个隐蔽的坑不同模型对 JSON 的遵循程度不一样。我们测试下来同等级别的模型里有些对格式要求响应更好。如果格式问题特别严重可以考虑换模型或者用支持 structured output 的接口。5.2 工作流状态丢失怎么排查状态丢失通常有几个原因。一是 checkpointer 没配或配错用了内存后端服务重启就丢。二是thread_id不一致恢复时用了不同的 ID找不到原来的状态。三是并发操作同一个thread_id导致状态覆盖。排查时先确认 checkpointer 类型再看日志里thread_id是否一致。并发问题比较隐蔽我们的做法是给每个thread_id加锁同一时间只允许一个操作。问题现象可能原因排查方法解决方案重启后流程消失用了内存 checkpointer检查 checkpointer 配置换成 PostgresSaver恢复时报找不到状态thread_id 不一致对比日志中的 thread_id统一用报价单号作为 ID状态被覆盖并发操作同一流程查操作日志时间戳加分布式锁5.3 审批路由不符合预期怎么调路由问题一般出在条件函数上。先检查条件逻辑是否覆盖了所有情况有没有漏掉的分支导致走了默认路径。再检查 LLM 输出的风险等级是否准确如果模型判错了路由自然就错了。我们的经验是条件函数要写得足够防御性。比如风险等级字段如果缺失或非法不能直接崩要有兜底逻辑默认走人工审批。另外路由决策最好记日志把输入和输出都打出来出问题时好追溯。5.4 模型调用超时或限流怎么处理生产环境调 LLM API超时和限流是家常便饭。我们的处理策略是设置合理的超时时间我们设的 30 秒超时后重试重试次数不超过 2 次。限流的话加一个令牌桶限流器控制调用频率。另外分析节点可以做成异步的不阻塞主流程分析结果出来后再触发路由。注意重试要有退避策略别一失败就立刻重试容易把限流搞得更严重。我们用指数退避第一次等 1 秒第二次等 2 秒。5.5 审批意见质量怎么保证LLM 生成的审批摘要质量参差不齐有时候太笼统有时候又太啰嗦。我们的优化方法是在 prompt 里明确摘要的格式要求比如用不超过 100 字说明风险点和审批重点并给出好的和坏的示例。另外摘要生成后可以再过一遍校验太短或太长的打回重生成。实测下来加了格式约束和示例后摘要的可用率从 70% 提升到了 90% 以上。剩下 10% 主要是模型对某些特殊业务场景理解不到位这种只能靠持续优化 prompt 和补充领域知识。6. 上线后的效果和一些真实体会系统上线跑了三个月数据上确实有改善。审批时长从平均 4.2 小时降到 1.1 小时人工干预率从 68% 降到 23%审批意见的完整度也有明显提升。但比数字更有意思的是一些意料之外的收获。比如LLM 分析节点积累了大量结构化的风险标注数据这些数据反过来帮业务部门发现了原来规则体系里的盲区。有一次我们发现模型频繁把某类产品标记为非标但业务上这类产品其实是标准的查下来是 prompt 里的定义跟业务口径不一致改了之后不仅模型准了连业务部门自己也把产品分类标准理清楚了。还有一个体会是这套系统的价值不在于替代人而在于让人把精力花在真正需要判断的地方。原来审批人大量时间花在这单是什么情况上现在 LLM 把情况摘要好了审批人直接进入该不该批的判断效率自然就上来了。当然也有做得不够好的地方。比如模型对某些长尾场景的理解还是不够偶尔会给出莫名其妙的建议。我们的应对是保留人工兜底通道任何环节审批人都可以打回让系统重新分析或者直接转人工处理。这个兜底机制很重要别指望模型 100% 靠谱。最后分享一个小技巧prompt 要版本化管理。我们一开始 prompt 直接写在代码里改一次要发一次版很麻烦。后来把 prompt 抽出来放到配置文件甚至数据库里改 prompt 不用动代码迭代速度快了很多。而且每次改 prompt 都记录版本和效果对比时间长了就能看出哪些改法有效、哪些是瞎折腾。
返回列表