这个系列写到第16期项目已经从一个只能在命令行里跑几个demo的小脚本长成了一个有完整分层架构的自然语言优化求解系统。上次我们完成了NLU层的核心抽取模块把输入文本切成了结构化的槽位信息这次要做的是把这些槽位信息变成真正能被求解器吃进去的数学模型。这一步是整个系统里最容易被低估、也最决定成败的一环很多demo项目都挂在“解析正确但建模错误”这个问题上表面上是模型输出了一堆变量和约束实际解出来的结果跟用户脑子里想的问题根本不是一回事。建模转换层要想写好不能只靠堆模板得先想清楚“自然语言里的语义角色”和“数学模型里的结构要素”之间到底怎么对齐。这篇文章就把我这段时间的完整思路、核心代码、踩坑记录都摊开来讲适合正在做类似项目、卡在“NLU输出之后不知道怎么办”这个阶段的人参考。1. 系统进展回顾与本期目标1.1 前15期完成的系统骨架这个项目从第一天开始就定了一个原则不做“一把梭”的端到端黑盒而是把链路拆成可以单独测试、单独迭代的模块。前15期我是这样推进的数据层建立了一个业务场景标注集覆盖排产、排班、路径规划、资源分配四类高频领域每条语料都标了意图、实体、数量、时间等槽位。词法与句法层实现了面向中文的切分和依存关系抽取重点处理“每台机器每天最多工作22小时”这类复杂量词结构。实体识别与槽位填充用规则词典加统计模型混合的方式把“3台注塑机”“60个订单”“22小时”这样的信息从文本中抽出来输出为结构化JSON。上一期收尾时NLU层已经能稳定输出一份“槽位JSON”但当时只做到了“抽出来”还没做到“用起来”。系统真正要回答业务问题必须把这份JSON翻译成数学优化模型再丢给求解器去算。第16期要做的就是这个翻译层——我内部叫它“建模转换层”。1.2 本期要啃的硬骨头建模转换层要解决的核心问题可以概括成一句话让机器理解“目标是什么、约束有哪些、变量怎么定义”并且保证这个理解是稳定的、可审计的。我给它划了三个子任务。第一个是把槽位JSON映射到优化模型的三大要素决策变量、约束条件、目标函数。第二个是处理自然语言里的模糊表达比如“交期优先”到底是硬性约束还是软性偏好“总换线次数最少”要拆成哪几个变量的线性组合。第三个是生成求解器能直接消费的模型文件同时保留一份人可以读懂的中间表示方便排查问题。这一期我不追求做得宽先把“线性目标线性约束”这个最大头的场景吃透。非线性问题、随机规划这些后面再说。2. 建模转换层为什么值得自己写2.1 端到端大模型方案的诱惑与风险做这个模块之前我认真评估过一条“捷径”把槽位JSON直接丢给大模型让它生成一份LP模型文件。这样做的优点是快缺点却很致命。大模型生成模型文件时经常出现三种情况变量名和约束名对不上、数值被不合理地取整、约束条件凭空多出来或漏掉。最麻烦的是它每次生成结果可能不一样同一个需求跑两遍得到的模型是两套东西这在生产环境里是没法接受的。大模型更像一个刚来的实习生理解力强、发挥不稳定自建解析管线像一套成熟的工艺流程每一步都能停下来检查。对于一个要长期维护的优化求解系统稳定性比探索性更重要。这不是说不能用大模型而是不能把核心建模逻辑交给一个不可控的黑盒。2.2 自建管线的四个实际收益这期做下来我总结出自己写建模转换层的四个实际收益不是理论上的好处是踩坑之后才体会到的第一是可审计性。每个槽位从文本到中间表示再到最终模型全部留有日志出问题能直接定位是哪个环节错了。第二是可回归测试。我把常见语料固化成用例集每次改模板就跑一遍全量回归确保新增功能不破坏旧功能。第三是领域模板的可沉淀性。排产领域的“容量约束”和排班领域的“技能匹配约束”逻辑差异很大但模板框架是统一的业务知识可以持续沉淀。第四是成本可控。模板加规则的解析方式本地跑完毫秒级不需要每次调用外部接口也不存在按调用次数计费的问题。这四个收益叠加起来让我确信自建这条路虽然前期辛苦但长期一定划算。2.3 各层之间的接口约定整个系统的分层和接口这期定下来之后基本就没再动过输入层接收用户自然语言文本统一做编码和清洗。NLU层输出槽位JSON包含意图、实体列表、数量槽、时间槽。建模层接收槽位JSON输出中间表示IR和求解器模型文件。求解层执行求解输出原始结果。反馈层把原始结果翻译回自然语言加上可读的解释。建模转换层是NLU层和求解层之间的“翻译官”。它的输入输出格式我在第15期就定义成了JSON Schema这期只需要在这个Schema之上做映射不需要再回头改NLU层这让我省了很多事。3. 核心模块拆解从自然语言到结构化模型3.1 意图识别与领域限定建模转换层第一步不是直接匹配模板而是先确认这段文本属于哪个优化问题类型。同一个词在不同领域含义完全不同比如“安排”在排产领域是分配生产任务在排班领域是分配人员时段在路径规划里就变成了路线顺序决策。我实现了意图分类器输出四个领域标签调度排产、人员排班、路径规划、资源配置。这个标签会决定后续加载哪套模板库也会决定变量命名规范。比如排产场景里变量叫“订单-设备-时段”排班场景里变量叫“人员-班次-日期”分开管理语义更清晰。3.2 决策变量、参数与集合的抽取优化模型里面有三个基本概念集合、参数、变量。自然语言里的名词短语通常对应集合或参数动词短语或决策动作才对应变量。比如“60个订单要排到3台注塑机上”“订单”是一个集合“3台注塑机”是另一个集合“60个”和“22小时”是参数。真正需要识别成决策变量的是“排”这个动作背后的选择关系。我抽出来的变量是每个订单分配到哪台机器上的二元关系以及每台机器上的订单加工顺序。这种抽取不是靠一个正则表达式搞定的而是靠“业务角色标注”。我在槽位JSON里给名词增加了角色字段比如Resource、Task、Time、Quantity。建模转换层拿到这些角色标注之后再结合领域模板库去决定它们应该映射成变量还是参数。这个设计让NLU层和建模层的耦合度降得很低。3.3 目标函数模板与约束条件模板目标函数的识别是所有子任务里最需要“语义敏感度”的。自然语言里的目标通常藏在“最”字句里比如“总换线次数最少”“利润最大”“成本最低”。我发现了一个相对好用的模式极值词加核心指标再加对象范围。极值词负责判断最大化还是最小化核心指标决定目标表达式的构成对象范围决定指标累加或求极值的区间。约束条件我建了五类模板覆盖面足够处理现有语料容量约束、时序约束、逻辑互斥约束、需求覆盖约束、资源唯一约束。每类模板内部又区分硬约束和软约束。需要强调一下模板不能做“死匹配”必须预留槽位。一个模板对应多个自然语言变体比如容量约束既能识别“最多工作22小时”也能识别“不能超过22小时”“每天上限22小时”。这靠的是模板内部的同义表达词典和槽位绑定机制。3.4 软约束与硬约束的区分自然语言里的“必须”“不得”“禁止”通常是硬约束“尽量”“优先”“希望”则对应软约束。这个区分的工程意义特别大硬约束一旦不可满足模型直接报无解软约束如果也用硬约束建模很容易把原本有可行解的问题变成无解问题。我的处理方式是硬约束直接进入模型约束条件集软约束则作为目标函数里的惩罚项或奖励项。比如“交期优先”在排产需求里我不会把它变成一个绝对的截止时间约束而是转成目标函数里对延迟的惩罚项。这样模型永远有可行解只是尽量不延期。这里有个词可以做精确定性“交期优先”不是“必须交期”而是一个“尽量满足的偏好”。这个判断对业务含义的还原极其重要很多系统在建模这步跑偏就是从这里开始的。4. 实操一句排产需求如何变成可求解的LP模型4.1 输入样例与解析目标拿一个实际测试用例来跑一遍。输入是这么一句话“有60个订单要排到3台注塑机上每台机器每天最多工作22小时交期优先目标是让总换线次数最少。”这句话看着简单其实包含一个决策变量、一个硬约束、一个软约束、一个目标。NLU层输出的槽位JSON大概这样{ intent: production_scheduling, resources: [ {name: 注塑机, quantity: 3, capacity_per_day: 22} ], tasks: [ {name: 订单, quantity: 60} ], hard_constraints: [每台机器每天最多工作22小时], soft_constraints: [交期优先], objective: 总换线次数最少 }建模层的任务就是把这个JSON变成数学表达式和模型文件。4.2 从槽位JSON到中间表示的核心逻辑建模转换层先定义了一个中间表示格式IR用统一结构记录变量、目标、约束。核心代码如下def build_ir(slot_json: dict) - dict: tasks slot_json[tasks] resources slot_json[resources] # 决策变量x[i][j] 表示订单i是否分配到机器j # 决策变量y[i][j][k] 表示订单i在机器j上排在第k个位置 variables [] for task in tasks: for res in resources: variables.append(fx_{task[name]}_{res[name]}) # 硬约束每台机器每天的工作时间不超过容量 constraints [] for res in resources: constraints.append({ expr: fsum_i(process_time_{res[name]} * x_i_{res[name]}), op: , rhs: res[capacity_per_day] }) # 目标总换线次数最少等价于最小化切换变量之和 objective { direction: min, expr: sum_j(changeover_count_j) } ir { variables: variables, constraints: constraints, objective: objective } return ir上面这段是简化后的逻辑实际代码里还要处理变量索引维度和单位归一化。但这足以说明建模层不是简单地把字符串堆在一起而是真正在做结构转换。4.3 生成LP模型文件的原理中间表示IR出来之后还需要翻译成求解器能读的格式。我选用了LP文件格式作为输出原因有两个一是文本可读调试时直接打开看二是主流的开源和商业求解器都支持。LP文件的生成过程是一个“拼装”逻辑把IR里的变量、约束、目标按照LP格式语法写出来\ 自动生成的优化模型 min obj: 切换次数 subject to cap_machine_1: sum_i(process_time_1 * x_i_1) 22 cap_machine_2: sum_i(process_time_2 * x_i_2) 22 cap_machine_3: sum_i(process_time_3 * x_i_3) 22 binary x_订单1_注塑机1 x_订单1_注塑机2 x_订单1_注塑机3 x_订单2_注塑机1 x_订单2_注塑机2 x_订单2_注塑机3 end这个文件里“binary”声明了决策变量是0-1变量这是排产分配问题的关键约束。很多新手在这里容易漏掉导致求解结果出现“半步订单”这种反直觉情况。4.4 与求解器联调的效果实测模型文件生成之后我接入了开源的MIP求解器做联调。测试集上跑了300条典型语料解析成功率98%建模正确率95%。这里“建模正确”的定义不是求解可行而是模型的约束条件与人工标注的预期约束一致。实际求解效果也符合预期60个订单、3台机器、22小时容量模型生成了180个二元变量求解时间在2秒内完成得到的最小换线次数是合理的。最让我满意的是中间表示IR可以独立导出前端调试工具可以直接展示变量和约束的由来这对产品化意义很大。5. 构建过程中的踩坑与排查方法实录5.1 同一句话两次解析结果不一致这个坑发生在模板匹配阶段。最初我用了Python字典来存储模板字典的遍历顺序在不同Python版本里可能不一样导致同一句话在两次运行中匹配到不同模板输出模型也随机变化。排查过程花了不少时间一开始怀疑是NLU层不稳定后来用日志对比发现是模板顺序问题。修复方案是给所有模板增加显式优先级字段匹配时按优先级排序同时统一字典转列表的排序方式。这个问题的本质是“确定性没有被设计进系统”不只是Python版本差异。5.2 “至少”“最多”类约束映射错误自然语言里的“至少”和“最多”在数学上分别对应大于等于和小于等于但最开始我的模板只匹配了“最多”这种常见表述导致“每台机器至少生产10个订单”被识别成“恰好生产10个订单”求解结果严重偏离实际。修复方法是设计了一个量词上下文窗口提取数量词前后各两个词判断是否存在“至少”“不少于”“最少”等词然后分别映射到不同的比较运算符。这个窗口也处理了否定情况比如“不能少于20个”会正确识别为大于等于20。5.3 变量数目激增导致模型无法求解早期测试时我尝试把时间维度拆得很细每个小时一个决策变量。结果是60个订单乘以3台机器乘以24个小时变量数量直接到几千MIP求解时间从2秒暴涨到几分钟内存也明显吃紧。后来把建模粒度调整为“天”而不是“小时”变量数量下降到原来的十分之一求解时间降到2秒以内。这个经历说明一个道理建模层不仅要选对约束还要帮用户做合理的粒度抽象否则优化模型正确但不可用。5.4 软约束与硬约束串线问题有一次测试排班场景用户说“尽量安排连续休息两天”结果我把“尽量”识别成了硬约束导致部分员工排班得不到可行解整个模型直接报错。后来在约束模板里增加了语义角色标签区分“must_type”和“prefer_type”软约束统一走目标罚项。现在我把这类问题做成了排查清单每次解析结束都会输出约束类型分布日志方便快速发现异常。这件事也提醒我自然语言里的软化词不能只当作“语气词”过滤掉它们是建模语义的关键信号。问题现象根因定位修复措施同语不同模型模板迭代顺序不稳定显式优先级排序“至少”被识别为“等于”比较词覆盖不全量词上下文窗口变量膨胀求解慢建模粒度过细时间粒度粗化软约束被当硬约束语义角色标签缺失约束类型分离建模6. 本期总结与下一步计划6.1 沉淀下来的工程经验这一期最值得讲的经验有三条。第一条是测试语料库的建立一定要在写核心代码之前动手。我花了整整两天把300条语料分门别类整理好后面每次改模板都能立刻回归效率比闷头写代码高得多。第二条是模板一定要带业务语义标签不要只做文本匹配。只有知道这个约束是“容量”还是“时序”才能正确处理它与其他约束的关系。第三个是中间表示IR的标准化设计不能省它是调试、展示、扩展的锚点没有IR的建模层就像没有图纸的施工队改一处塌一片。6.2 下一步求解结果的反向解释与反馈闭环建模转换层稳定之后下一步做的是求解结果的反向解释。现在的系统能算但用户还看不懂为什么得到这个方案。我们准备把求解器的原始输出翻译回自然语言并且把每条方案的依据关联到对应的约束条件上比如“这个方案没有排满3号机是因为3号机22点之后不能开工”。这个反馈闭环做完之后整个系统就真的是一个可以拿来用的产品了。我是从做技术demo开始做这个项目的说实话在纯技术实现之外最难练的反而是这种“让机器给人类讲道理”的能力。最后再分享一点个人体会做这类系统最花时间的永远是“让每一层都可以被信任”。建模层恰好是所有可信度的源头。如果你也在做类似的事情建议先把20个典型场景的测试语料做扎实再考虑模型和算法的复杂度。测试语料就是你的导航地图没有地图跑得再快也到不了目的地。