ARTICLE DETAIL

资讯详情

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

LangGraph实战:LLM加工作流引擎实现智能报价审批

LangGraph实战:LLM加工作流引擎实现智能报价审批 1. 报价审批为什么成了企业流程里的“老大难”做过企业内部系统的人大概都有同感报销、采购、合同、报价这几类审批流表面上都是“填单-提交-审批-归档”四步走但真正落到报价审批上复杂度会陡然上升一个量级。原因不复杂——报价审批不是单纯走个签字流程它本质上是一次多角色、多约束、多版本的决策过程。销售要抢时间财务要控毛利法务要盯条款技术要评估交付可行性老板还要看整体折扣策略是否击穿底线。任何一个环节卡住单子就可能黄掉。我所在团队去年接手的一个项目就是典型的报价审批改造。改造前的状态是销售在CRM里填好报价单导出PDF发到审批群里一圈相关人谁有空谁看一眼回复“同意”或者“再降两个点”。财务事后补录台账月底对账经常发现折扣算错、税率用错、版本对不上。最夸张的一次同一个客户收到了三个不同版本的报价因为销售改了价格但没同步给技术评估成本。这种场景下传统工作流引擎能解决“流程流转”的问题但解决不了“内容理解”的问题。比如“这个报价的毛利率是否低于公司红线”“客户提到的账期要求是否触发了特殊审批规则”“技术方案里写的交付周期和报价单上的承诺是否一致”——这些判断依赖对非结构化文本的阅读理解传统规则引擎写死条件根本覆盖不全。而LLM恰好补上了这块能力它能读报价单、读客户需求、读历史合同把散落在文本里的关键信息抽出来再交给工作流引擎去做确定性的路由和审批。所以这个项目的核心思路就一句话LLM负责“看懂”工作流引擎负责“走对”。LangGraph在这个组合里扮演的是编排层的角色它把LLM调用、条件判断、人工审批节点、外部系统回调串成一张有状态的有向图让整个报价审批从“人找事”变成“事找人”。适合谁来参考这篇内容如果你正在做企业内部流程自动化尤其是涉及文档理解、多级审批、动态路由的场景或者你已经在用LangGraph做Agent编排但还没落到具体业务里那这篇实战记录应该能帮你省掉不少试错时间。下面我从整体设计、核心细节、实操过程、踩坑排查四个层面把这次改造的完整思路和落地细节拆开讲。2. 整体架构设计与技术选型背后的取舍2.1 为什么是LLM加工作流引擎而不是纯LLM或纯规则一开始团队里有过争论既然LLM这么强能不能直接让模型输出“同意/拒绝/转人工”的结论跳过工作流引擎我们试了一版原型结论是不行。原因有三个。第一审批路由需要确定性。报价金额超过50万必须走VP审批这是公司制度不能因为模型某次输出“建议直接通过”就跳过。LLM适合做信息抽取和风险提示但最终的路由决策必须由确定性代码执行。第二审批过程需要可追溯。每个节点谁审批的、什么时候审批的、依据是什么这些要落库。纯LLM对话式审批状态管理很混乱出了问题查不到链路。第三人工审批节点不可省略。报价审批里有一类“异常折扣”场景模型可以标记出来但最终是否放行必须由授权人决定。工作流引擎天然支持人工任务节点LLM只负责把异常信息整理好推给人。反过来纯规则引擎也不行。我们统计过历史报价单光是“折扣合理性判断”这一项如果写成规则需要覆盖客户等级、历史成交价、产品线、区域、季节、竞争对手报价等十几个维度规则条目超过200条维护成本极高且漏判率高。LLM读一遍报价单和客户背景几秒钟就能给出一个带解释的风险评分这是规则引擎做不到的。所以最终架构是分层的LLM层做理解和抽取工作流层做路由和状态管理人工层做最终决策。LangGraph正好是把这三层串起来的粘合剂。2.2 LangGraph在编排层解决了什么问题LangGraph的核心抽象是“状态图”你定义一个共享状态State然后定义若干节点Node和边Edge每个节点读取状态、执行逻辑、写回状态边决定下一步走哪个节点。这个模型和报价审批的流程天然匹配。我们定义的State大概长这样class QuoteApprovalState(TypedDict): quote_id: str raw_quote: dict # 销售提交的原始报价数据 extracted_info: dict # LLM抽取的关键信息 risk_score: float # LLM给出的风险评分 risk_reasons: list # 风险原因列表 approval_path: list # 已走过的审批节点 current_approver: str # 当前审批人 final_decision: str # 最终结论 audit_log: list # 审计日志节点包括parse_quote解析报价单、extract_with_llmLLM抽取、assess_risk风险评估、route_by_rules规则路由、human_approval人工审批、notify通知、archive归档。边则根据风险评分和金额动态决定走哪条审批路径。LangGraph相比自己写状态机的好处在于它内置了检查点Checkpoint机制每个节点执行完自动持久化状态审批中途服务重启也不会丢进度它还支持“中断-恢复”人工审批节点可以挂起几天审批人操作后再从断点继续。这些能力如果自己实现工作量至少多出两周。2.3 模型选型与成本控制的现实考量LLM选型上我们没有盲目追大参数模型。报价审批的核心任务是信息抽取和风险判断不需要创意生成所以选了一个中等规模的指令微调模型配合结构化输出约束。实测下来在抽取“客户名称、报价金额、折扣率、账期、交付周期、特殊条款”这六个字段时准确率能达到97%以上单次调用成本只有大模型的十分之一左右。成本控制还有一个关键点不是每个报价单都需要LLM全量分析。我们在工作流里加了一个前置判断节点如果报价金额低于某个阈值且折扣在标准范围内直接走快速审批通道不触发LLM调用。只有金额超标或折扣异常的报价单才进入LLM分析分支。这样整体LLM调用量下降了约60%审批时效反而提升了因为简单单子不再排队等模型推理。提示LLM调用一定要设超时和降级策略。我们设的是8秒超时超时后自动转人工审批并标记“模型分析未完成”避免因为模型服务波动导致整个审批流卡死。3. 核心细节拆解从报价单到审批结论的每一步3.1 报价单解析与LLM信息抽取的配合方式销售提交的报价单格式并不统一有的是CRM导出的结构化JSON有的是Excel模板还有的是邮件正文里贴的表格。我们的处理策略是能结构化的先结构化不能结构化的交给LLM。对于CRM导出的JSON直接映射字段不需要LLM介入。对于Excel和邮件正文先用解析库提取文本再拼成Prompt交给LLM做字段抽取。Prompt的设计很关键我们迭代了四版才稳定下来。核心技巧是给模型一个明确的输出Schema并要求它只输出JSON不要解释。EXTRACT_PROMPT 你是一个报价单信息抽取助手。请从以下文本中抽取指定字段以JSON格式输出。 字段定义 - customer_name: 客户公司全称 - quote_amount: 报价总金额数字单位元 - discount_rate: 折扣率小数如0.85表示85折 - payment_term: 账期描述 - delivery_days: 交付周期天数数字 - special_terms: 特殊条款列表 如果某个字段在文本中未提及值设为null。只输出JSON不要输出其他内容。 文本内容 {raw_text} 实测发现模型对“折扣率”的抽取最容易出错因为报价单里可能同时出现“折扣”“优惠”“让利”“折让”等多种表述而且有的是在总价上打折有的是在单价上打折。我们的解决办法是在Prompt里加了一个计算步骤要求模型先输出原始单价和折后单价再计算折扣率。这样即使表述不同模型也能通过数值关系推断出正确的折扣率。3.2 风险评估节点的规则与模型混合策略风险评估是整个审批流的核心判断点。我们采用的是规则兜底加模型评分的混合策略。规则部分负责硬性红线毛利率低于15%直接标记高风险账期超过90天直接标记高风险折扣率低于0.7直接标记高风险。这些规则不依赖模型代码里写死保证底线不被突破。模型部分负责软性判断综合客户历史合作情况、当前报价与历史均价的偏离度、特殊条款的潜在成本影响给出一个0到1的风险评分并列出风险原因。模型评分不直接决定审批路径而是作为路由的输入之一。路由逻辑用一张表说清楚风险评分金额范围审批路径0-0.3任意自动通过通知销售0.3-0.6低于50万销售主管审批0.3-0.650万及以上销售主管加财务审批0.6-0.8任意财务加法务审批0.8-1.0任意VP审批附模型风险报告这个表是动态可配置的不同产品线可以有不同的阈值。LangGraph的条件边读取State里的risk_score和quote_amount自动决定下一个节点。3.3 人工审批节点的交互设计与状态保持人工审批节点最容易被忽视的是上下文传递。审批人打开待办时不能只看到一个“同意/拒绝”按钮他需要知道这单为什么走到我这里、模型发现了什么风险、历史类似单子怎么处理的。我们在LangGraph的人工审批节点前加了一个prepare_approval_context节点把State里的关键信息整理成审批卡片报价摘要、风险评分、风险原因、模型建议、历史参考。审批人看到的是一个信息完整的页面而不是一个裸审批任务。状态保持方面LangGraph的Checkpoint机制帮了大忙。审批人可能隔天才处理这期间服务可能重启、可能发版但State持久化在数据库里审批人操作后从断点恢复继续走后续节点。我们用的是Postgres作为Checkpoint存储配合LangGraph的PostgresSaver实测下来稳定性很好。注意人工审批节点一定要设超时提醒和自动升级。我们设的是24小时未处理发提醒48小时未处理自动升级到上级。这个逻辑不要写在LangGraph节点里而是用外部定时任务扫描待办表触发后调用LangGraph的恢复接口。4. 实操过程从零搭建报价审批工作流的完整步骤4.1 环境准备与依赖安装先列一下我们实际用的技术栈和版本避免版本兼容问题python3.11 langgraph0.2.x langchain0.3.x fastapi0.115.x sqlalchemy2.0.x psycopg2-binary2.9.x pydantic2.x安装命令pip install langgraph langchain langchain-openai fastapi uvicorn sqlalchemy psycopg2-binary pydantic数据库用Postgres建两张表一张存审批实例的State快照一张存审计日志。LangGraph的Checkpoint表会自动创建不用手动建。4.2 定义State和节点函数State定义前面已经给了这里重点说节点函数的写法。每个节点函数接收State返回一个字典表示要更新的字段。LangGraph会自动合并。def extract_with_llm(state: QuoteApprovalState) - dict: raw_text state[raw_quote].get(text, ) if not raw_text: return {extracted_info: state[raw_quote], risk_score: 0.0} prompt EXTRACT_PROMPT.format(raw_textraw_text) response llm.invoke(prompt) extracted json.loads(response.content) return {extracted_info: extracted}风险评估节点def assess_risk(state: QuoteApprovalState) - dict: info state[extracted_info] reasons [] score 0.0 # 规则兜底 if info.get(discount_rate, 1.0) 0.7: reasons.append(折扣率低于0.7触发红线) score max(score, 0.9) if info.get(payment_term_days, 0) 90: reasons.append(账期超过90天) score max(score, 0.8) # 模型评分 model_score llm_risk_assess(info) score max(score, model_score) return {risk_score: score, risk_reasons: reasons}4.3 构建图与条件路由图构建是LangGraph的核心操作from langgraph.graph import StateGraph, END workflow StateGraph(QuoteApprovalState) workflow.add_node(parse_quote, parse_quote) workflow.add_node(extract_with_llm, extract_with_llm) workflow.add_node(assess_risk, assess_risk) workflow.add_node(route_by_rules, route_by_rules) workflow.add_node(human_approval, human_approval) workflow.add_node(notify, notify) workflow.add_node(archive, archive) workflow.set_entry_point(parse_quote) workflow.add_edge(parse_quote, extract_with_llm) workflow.add_edge(extract_with_llm, assess_risk) workflow.add_edge(assess_risk, route_by_rules) workflow.add_conditional_edges( route_by_rules, decide_next_node, { auto_approve: notify, supervisor: human_approval, finance: human_approval, vp: human_approval, } ) workflow.add_edge(human_approval, notify) workflow.add_edge(notify, archive) workflow.add_edge(archive, END) app workflow.compile(checkpointerPostgresSaver(conn))decide_next_node函数读取State里的risk_score和金额返回对应的路由键。这里有个细节人工审批节点是同一个但审批人不同。我们在State里存了current_approver字段人工审批节点根据这个字段决定推给谁。4.4 人工审批的挂起与恢复LangGraph支持在节点内调用interrupt来挂起图执行from langgraph.types import interrupt def human_approval(state: QuoteApprovalState) - dict: approval_data { quote_id: state[quote_id], risk_score: state[risk_score], risk_reasons: state[risk_reasons], approver: state[current_approver], } decision interrupt(approval_data) return {final_decision: decision[result], audit_log: state[audit_log] [decision]}外部系统比如审批中心拿到interrupt数据后展示给审批人审批人操作后调用app.invoke(Command(resumedecision), config)恢复执行。这个机制让审批流可以跨越很长时间而不丢状态。4.5 审计日志与可观测性每个节点执行完都要写审计日志。我们在State里维护一个audit_log列表每个节点追加一条记录包含节点名、时间戳、关键输入输出摘要。归档节点把完整State写入审计表供后续查询和合规检查。可观测性方面LangGraph支持回调Callback我们接入了LangSmith做链路追踪。每个报价单的审批过程在LangSmith里能看到完整的节点执行顺序、LLM调用耗时、Token消耗、路由决策依据。这对排查问题非常有用比如某单子卡住了一看链路就知道是LLM超时还是人工节点没恢复。5. 常见问题与排查技巧实录5.1 LLM输出格式不稳定导致解析失败这是最常见的问题。模型有时候会在JSON外面包一层解释文字或者字段名拼写错误。我们的解决办法是三层防护第一层Prompt里强调“只输出JSON”第二层用Pydantic做输出校验校验失败自动重试一次第三层重试仍失败则降级为人工处理标记“模型解析失败”。from pydantic import BaseModel, ValidationError class QuoteInfo(BaseModel): customer_name: str | None quote_amount: float | None discount_rate: float | None payment_term: str | None delivery_days: int | None special_terms: list[str] [] def safe_parse(content: str) - QuoteInfo | None: try: data json.loads(content) return QuoteInfo(**data) except (json.JSONDecodeError, ValidationError): return None5.2 人工审批节点恢复后状态不一致这个问题出现在并发场景审批人A和审批人B同时打开待办A先审批了B后审批B的恢复请求会覆盖A的结果。我们的解决办法是在恢复前检查State的版本号如果版本号已经变化拒绝B的请求并提示“该审批已被处理”。LangGraph的Checkpoint自带版本管理我们在恢复时传入checkpoint_id如果传入的ID不是最新版本LangGraph会报错我们捕获后返回友好提示。5.3 模型评分波动导致路由不稳定同一个报价单模型两次调用可能给出不同的风险评分导致路由结果不一致。这在审批场景里是不可接受的。我们的应对策略是模型评分只做参考路由决策以规则为主。具体来说模型评分映射到三个档位低/中/高路由表按档位设计而不是按精确分数。这样即使评分在0.55和0.58之间波动只要不跨档位路由结果就稳定。另外我们对模型调用做了缓存同一个报价单的文本哈希作为Key24小时内重复调用直接返回缓存结果。这既保证了稳定性又降低了成本。5.4 常见问题速查表问题现象可能原因排查方法解决措施审批流卡在LLM节点模型服务超时查看LangSmith链路耗时设超时降级转人工路由结果与预期不符风险评分跨档位打印State里的risk_score调整档位阈值或加规则兜底人工审批恢复失败Checkpoint版本冲突检查checkpoint_id加版本校验拒绝旧版本恢复审计日志缺失节点未写日志检查节点函数返回值统一在节点基类里写日志报价单字段抽取错误Prompt表述不清对比原文和抽取结果优化Prompt加计算步骤5.5 几个踩过的坑和实操心得第一个坑不要把所有逻辑都塞进LLM。我们一开始尝试让模型直接输出审批路径结果模型经常“自作主张”给出不合规的建议。后来改成模型只输出风险评分和原因路由由代码决定问题就消失了。第二个坑人工审批节点的上下文要精简。我们第一版把完整State推给审批人信息太多反而没人看。后来改成只推关键信息卡片审批效率明显提升。第三个坑LangGraph的State不要存大对象。我们一开始把报价单PDF的二进制内容存在State里导致Checkpoint写入很慢。后来改成只存文件路径需要时再读取。第四个心得先跑通再优化。我们第一版工作流只有四个节点LLM抽取、规则路由、人工审批、归档跑通之后才逐步加风险评估、通知、审计等节点。如果一开始就设计得很复杂调试成本会高很多。6. 这套方案还能怎么扩展报价审批跑通之后我们发现这套“LLM理解加工作流路由”的模式可以复用到很多类似场景。比如合同审批把报价单换成合同文本抽取字段换成合同金额、付款条件、违约责任风险评估换成法务风险评分整个工作流骨架几乎不用改。再比如采购申请审批逻辑也类似。扩展的时候有几个方向值得考虑。一是多模型路由简单抽取用小模型复杂风险评估用大模型按任务难度动态选择进一步降成本。二是审批知识沉淀把历史审批结论和对应的报价特征存下来作为Few-shot示例注入Prompt让模型评分越来越准。三是主动预警不等到审批提交才分析销售在CRM里填报价单的时候实时调用LLM做预检提前提示风险减少返工。我个人在实际操作中的体会是LLM加工作流引擎这个组合最难的不是技术实现而是边界划分哪些判断交给模型哪些判断留给代码哪些判断必须给人。这个边界划清楚了剩下的就是工程问题。划不清楚模型再强也白搭。
返回列表