自然语言优化求解系统实战:LLM语义解析、差分进化与KKT校验

发布时间:2026/10/10 7:25:41
自然语言优化求解系统实战:LLM语义解析、差分进化与KKT校验
自然语言优化求解这件事我最早是在一个排产项目里被逼着做的。当时业务方丢过来一句话“帮我把这周的产线排一下尽量别加班交期别拖。”听起来像人话但要把这句话变成求解器能吃的数学模型中间隔着一整套翻译工作。后来我干脆想能不能让系统自己完成这层翻译——用户说人话系统输出可求解的模型、跑出结果、再用人话解释回来。这就是我动手搭这套自然语言优化求解系统的起点。这篇是系列的第 0-1 篇重点不在堆功能而在把骨架立起来怎么把一句自然语言拆成结构化的优化问题怎么选求解策略怎么用 KKT 条件做结果校验以及怎么让 LLM 在其中扮演“翻译官”而不是“算命先生”。如果你做过运筹优化、碰过 LLM 应用或者单纯想搞明白“大模型到底能不能算数学题”这篇应该能给你一些能直接抄的工程思路。1. 先想清楚为什么不能让 LLM 直接给答案1.1 一个反直觉的实测结论我最初的做法很朴素把用户的自然语言描述直接丢给 LLM让它输出最优解。测试了二十来个线性规划小题结果惨不忍睹。不是格式错就是约束漏最要命的是它给出的“最优解”经常连可行域都不在。比如一个简单的两变量问题约束是 xy≤10、x≥0、y≥0让它最大化 3x2y它一本正经地告诉我 x7、y5加起来 12直接违反约束。这件事让我彻底放弃“LLM 当求解器”的幻想。LLM 的本质是概率语言模型它擅长的是模式匹配和语言生成不是数值迭代。你让它解方程它是在“回忆”见过的类似题目长什么样而不是真的在算。规模小、结构常见的时候它可能蒙对一旦变量多一点、约束绕一点它就开始编。所以正确的分工是LLM 负责把自然语言翻译成结构化问题求解器负责真正求解校验模块负责兜底。LLM 是翻译官不是数学家。1.2 系统要解决的三层问题把需求拆开看这套系统其实要解决三层问题一层比一层难第一层是语义解析。用户说“尽量别加班”系统得知道这是个软约束对应目标函数里的加班惩罚项“交期别拖”是硬约束对应每个任务的完成时间上限。这层靠 LLM 的语义理解能力但必须用结构化 schema 约束它的输出。第二层是模型构建与求解。解析出来的东西要变成标准的优化问题形式——目标函数、决策变量、约束条件。然后根据问题类型选求解器线性问题用 LP整数问题用 MILP非线性连续问题可能得上梯度类方法或者差分进化。第三层是结果校验与解释。求解器给出的解得验证它真的满足约束KKT 条件在这里派上用场然后翻译回自然语言告诉用户“为什么这么排”。这三层里第一层和第三层是 LLM 的主场第二层是传统优化的地盘。把边界划清楚系统才稳。1.3 为什么选差分进化做兜底关键词里出现了差分进化这不是随便选的。实际业务里的优化问题很多是非线性、非凸、甚至目标函数没有解析表达式的比如仿真优化。这类问题梯度类方法容易陷局部最优而差分进化Differential Evolution, DE作为一种群体智能算法不依赖梯度全局搜索能力不错实现也简单。我的策略是分层求解能用精确求解器的LP/MILP优先用精确方法保证最优性遇到非线性、非凸、或者求解器报 infeasible 但怀疑是数值问题的切到差分进化做启发式搜索给一个“够好”的可行解。DE 在这里的角色是兜底不是主力。2. 语义解析层把“人话”翻译成结构化问题2.1 定义一套中间表示 schemaLLM 输出最怕自由发挥所以第一步是定义一套严格的中间表示Intermediate Representation, IR。我用 JSON Schema 约束核心字段包括{ problem_type: LP | MILP | NLP | ..., variables: [ {name: x1, type: continuous|integer|binary, lower: 0, upper: null, desc: ...} ], objective: { sense: minimize|maximize, expression: 3*x1 2*x2, soft_terms: [{expr: ..., weight: 100, desc: 加班惩罚}] }, constraints: [ {expr: x1 x2 10, type: hard|soft, penalty: 0, desc: ...} ], metadata: {raw_text: ..., assumptions: [...]} }这套 schema 的关键设计点在于软约束和硬约束分开。硬约束是必须满足的软约束是尽量满足、违反要付代价的。业务语言里大量存在“尽量”“最好”“优先”这类词它们对应的都是软约束。把软约束单独拎出来转成目标函数里的惩罚项这是让模型贴近真实业务的关键。另外assumptions字段很重要。LLM 解析时一定会做假设比如“用户没说变量是否可负我假设非负”。把这些假设显式记录下来后面校验和解释的时候能追溯也方便用户纠正。2.2 用 few-shot 把解析准确率拉上来光给 schema 不够LLM 还是容易漏字段或者乱填。我的做法是配一组 few-shot 示例覆盖典型场景排产、装箱、路径、资源分配。每个示例都是“自然语言输入 → 标准 IR 输出”的配对。实测下来few-shot 的数量和质量比模型大小更影响解析准确率。我用一个中等规模的模型配 8 个精心设计的示例解析准确率字段完整且语义正确能从 60% 出头拉到 85% 以上。示例的设计要点是每个示例突出一种语言现象比如一个专门练“软约束识别”一个专门练“多目标权衡”一个专门练“隐含约束补全”。还有一个技巧是让 LLM 先输出推理过程再输出 JSON。虽然多花点 token但解析质量明显更稳。我一般要求它按“识别决策变量 → 识别目标 → 识别约束 → 标注软硬 → 输出 JSON”的顺序来。2.3 解析结果的自动校验LLM 输出完 IR不能直接信得先过一遍自动校验。我写了个校验器检查几件事变量引用一致性目标函数和约束里出现的变量必须在variables里定义过。量纲合理性如果变量有物理含义比如时间、数量检查上下界是否合理。约束可满足性预判把硬约束单独拎出来用松弛变量做个快速可行性检查如果明显矛盾比如 x≥10 且 x≤5直接报错让 LLM 重新解析。表达式语法把表达式字符串解析成符号表达式语法错的直接打回。这一步能拦掉相当一部分低级错误。校验不通过时把错误信息连同原始输入一起回喂给 LLM让它修正一般重试一两次就能过。提示校验器不要做得太“聪明”。它的职责是拦截明显错误不是替 LLM 做语义判断。语义层面的对错最终还是靠 few-shot 和人工抽检来保证。3. 求解层精确方法与差分进化的分工3.1 问题类型判定与求解器路由IR 拿到手第一件事是判定问题类型然后路由到对应的求解器。判定逻辑大致是这样特征判定类型求解器选择目标与约束全线性变量连续LP单纯形/内点法含整数/二进制变量线性MILP分支定界目标或约束非线性可求导NLPSLSQP/内点法非线性、非凸、无梯度黑箱优化差分进化含逻辑约束if-then需线性化MILP 大M法路由这块我踩过一个坑一开始想全用 MILP 统一处理把连续问题也离散化。结果精度和性能都崩了。后来老老实实按类型分流LP 走 LPMILP 走 MILP各用各的强项。对于含逻辑约束的情况需要先做线性化。比如“如果开机器 A 就必须开机器 B”可以引入二进制变量和大 M 法转成线性约束。这部分我放在 IR 到标准模型的转换层里做LLM 不参与纯规则处理。3.2 差分进化的参数怎么调差分进化用在兜底场景参数配置直接决定它能不能在合理时间内给出可用解。我常用的配置是种群规模 NP取 10 到 20 倍变量维度但不超过 200。维度高的时候种群太小会早熟太大又慢。缩放因子 F0.5 到 0.9 之间我一般取 0.7。F 大探索强F 小开发强。交叉概率 CR0.8 到 0.95取 0.9。CR 高有利于多样性。变异策略DE/rand/1/bin做通用DE/best/1/bin在收敛慢的时候切。停止条件最大迭代 1000 代或者种群适应度方差小于阈值或者连续 50 代无改进。约束处理是 DE 的难点。我用的是罚函数法违反硬约束的解适应度加上一个很大的惩罚项违反软约束的按权重加惩罚。罚系数需要调太小了约束守不住太大了搜索空间被压扁。我的经验是罚系数取目标函数量级的 100 到 1000 倍然后根据实际收敛情况微调。3.3 精确解与启发式解的取舍什么时候用精确解什么时候接受启发式解这个判断很关键。我的原则是如果问题规模在精确求解器能处理的范围内比如 MILP 变量几百个以内优先精确求解拿到最优性证明。如果精确求解器超时我设 60 秒上限或者报 infeasible 但怀疑是数值问题切 DE。如果问题本身就是黑箱、非凸直接上 DE不浪费时间。DE 给出的解我会再跑一遍可行性校验。如果连硬约束都不满足说明罚系数或者参数有问题需要调整重跑。如果满足硬约束、只是目标值比精确解差一点那就接受并在结果里标注“启发式解非全局最优”。4. 校验层用 KKT 条件给结果上保险4.1 KKT 条件到底在验什么KKTKarush-Kuhn-Tucker条件是带约束优化问题最优解的必要条件。对于连续可微的问题一个点要成为局部最优得满足梯度条件、原始可行性、对偶可行性、互补松弛。听起来抽象落到工程上其实就三件事原始可行性解满足所有约束。这个最基础必须查。互补松弛如果某个不等式约束没取等号即不紧那它对应的拉格朗日乘子必须为零。这能帮我们发现“解是不是真的在边界上最优”。对偶可行性不等式约束的乘子非负。我用 KKT 做校验主要不是为了证明最优性那需要凸性保证而是为了发现异常。比如求解器给了一个解原始可行性过了但互补松弛大面积不满足那很可能这个解有问题要么是求解器没收敛要么是模型本身有毛病。4.2 数值校验的容差设置KKT 是理论条件实际数值计算里不可能严格等于零得设容差。我的经验值原始可行性容差1e-6。约束违反量超过这个就判不可行。互补松弛容差1e-5。乘子乘以松弛量超过这个值就认为互补松弛被破坏。梯度条件容差1e-4。这个最松因为数值梯度本身有误差。容差不能设太严否则浮点误差会让你误判也不能太松否则真问题被放过。这几个值是我在多个项目里调出来的对大多数中小规模问题够用。对于 DE 给出的解KKT 校验要谨慎。DE 不保证满足 KKT所以它给出的解 KKT 不满足是正常的。这时候我只查原始可行性KKT 只作为参考信息记录不作为拒绝依据。4.3 校验失败时的排查链路校验失败时我有一套固定的排查顺序避免瞎猜先查原始可行性。不满足约束说明求解结果本身不可用。看是哪个约束违反、违反多少。再查模型本身。把 IR 拿出来人工看一遍约束和目标有没有写错。LLM 解析错误经常在这里暴露。然后查求解器状态。看求解器返回的状态码是 optimal、infeasible 还是 time limit。infeasible 的话用松弛变量找出是哪组约束冲突。最后查数值问题。如果模型没问题、求解器说 optimal但 KKT 校验异常可能是问题病态条件数大需要做变量缩放或者换求解器。这套链路走下来大部分问题都能定位。我遇到最多的是第 2 步——LLM 把某个约束的方向搞反了或者漏了一个隐含约束。5. 把三层串起来一次完整的端到端跑通5.1 一个排产场景的完整流程拿开头那个排产需求举例走一遍完整流程。用户输入“这周有 5 个订单要排到 3 条产线上每个订单有交期和工时尽量别加班交期别拖。”第一步语义解析。LLM 输出 IR决策变量是每个订单分配到哪条产线二进制变量 x[i][j]目标是最大化交期满足率、最小化加班时间软约束转惩罚约束包括每个订单必须分配一条产线、每条产线的总工时不超过可用工时硬约束、交期尽量满足软约束。第二步模型构建。这是个典型的指派问题加软约束转成 MILP。硬约束用等式和不等式表达软约束的违反量作为额外变量进目标函数。第三步求解。MILP 求解器跑几秒出结果。如果规模大或者有非线性切 DE。第四步校验。查原始可行性所有硬约束满足查 KKT互补松弛正常。通过。第五步解释。把求解结果翻译回自然语言“订单 1、3 分到产线 A订单 2、5 分到产线 B订单 4 分到产线 C。产线 A 周六需要加班 2 小时其余产线正常。订单 2 的交期比要求晚半天因为产线 B 产能紧张。”这一整套跑下来用户拿到的是能直接用的排产方案而不是一堆数字。5.2 各层之间的接口设计三层之间靠 IR 和求解结果两个数据结构衔接。IR 是解析层的输出、求解层的输入求解结果是求解层的输出、校验层和解释层的输入。接口设计的关键是信息不丢失IR 里要保留原始文本和假设求解结果里要保留求解器状态和 KKT 校验信息这样出问题能追溯。我一开始图省事IR 里只存结构化字段把原始文本丢了。结果解释层想引用用户原话的时候抓瞎。后来加上metadata.raw_text和assumptions整个链路才顺。5.3 实测中的意外情况跑通之后遇到几个没想到的情况记下来给后来人参考。一个是变量命名冲突。LLM 解析时可能给两个不同含义的变量起一样的名字比如两个场景都用 x1。校验器能查出来但报错信息不友好。后来我在 IR 里强制变量名带语义前缀比如order1_lineA冲突就少了。另一个是软约束权重难定。用户说“尽量别加班”这个“尽量”到底值多少我一开始让 LLM 猜权重结果它给的权重忽大忽小。后来改成让用户确认或者用一组默认权重先跑把结果给用户看再让用户调。权重这东西本质是业务偏好机器猜不准。还有一个是DE 的随机性。同样的输入DE 两次跑出来的解可能不一样。这在需要可复现的场景里是问题。我的处理是固定随机种子并且在结果里标注“启发式解存在随机性”。6. 工程落地时我踩过的几个坑6.1 LLM 输出的 JSON 解析失败LLM 输出 JSON 经常带点“私货”比如前后加解释文字、用单引号、末尾多个逗号。直接json.loads十有八九报错。我的处理是写一个健壮的解析器先用正则把 JSON 块抠出来再用宽容的解析库比如允许尾逗号的最后才用标准库。还不行就回喂给 LLM 让它重新输出纯 JSON。更稳的做法是用结构化输出能力。现在不少模型支持强制 JSON schema 输出能大幅降低解析失败率。如果模型支持优先用这个。6.2 求解器超时与降级策略精确求解器遇到大规模问题会超时。我的策略是设超时上限超时后不直接失败而是降级把当前找到的最好可行解拿出来用同时标注“未证明最优”。如果连可行解都没有才切 DE。降级策略要提前设计好不能等超时了才临时想。我在求解层封装了一个统一的solve接口内部处理超时、降级、重试上层不用关心。6.3 结果解释的“人话”程度解释层最容易犯的错是“翻译腔”——把求解结果机械地念一遍。好的解释应该回答用户真正关心的问题方案是什么、为什么这么排、哪里做了妥协。我一般让 LLM 按“结论 → 关键约束满足情况 → 妥协点 → 建议”的结构来组织解释并且要求它引用具体的订单号和产线名不要泛泛而谈。解释层还有个细节不要过度承诺。如果解是启发式的要明说“这是近似最优可能存在更好的方案”。如果某个软约束被违反了要明确告诉用户违反了多少、为什么。诚实比好听重要。7. 这套骨架还能往哪长骨架立起来之后扩展方向其实挺多的。我目前想到几个一是多轮交互。用户看到结果后说“产线 A 的加班能不能再少点”系统能理解这是调整软约束权重重新求解。这需要把对话状态和 IR 关联起来。二是模型库沉淀。把常见的优化问题模式指派、装箱、路径、排产做成模板LLM 解析时先匹配模板匹配上了直接套匹配不上再从头解析。这样准确率和速度都能提升。三是求解过程可视化。把 DE 的收敛曲线、MILP 的分支定界树画出来让用户看到求解过程增加信任感。四是约束学习。从用户对结果的反馈里学习隐含约束。比如用户反复调整某个约束系统可以主动问“是不是要把这个约束固化下来”。我个人在实际操作中的体会是这套系统最难的不是某个单点技术而是三层之间的衔接和容错。LLM 会犯错求解器会超时校验会误报每一层都得有兜底。把兜底做扎实系统才敢用。至于 LLM 和传统优化的边界我的经验是凡是涉及数值计算和严格逻辑的交给传统方法凡是涉及语言理解和人机交互的交给 LLM。这条线划清楚后面的事就顺了。