版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
一、调度台上最值钱的东西,不在模型里
1.1 模型给的方案,读起来永远是对的
把一段真实的排车场景丢给大模型:三个仓、八台车、十七个卸货点,要求明天 18 点前送完,其中两单是冷链。它会给你一份像模像样的方案——车号、顺序、时间窗,甚至算好总里程。问题不在文笔,在于你没法从文字上看出这方案跑不跑得通。
最常见的错有三类:某台车装了 12.4 吨,车型核载只有 11.8 吨;某台车第二段行程跨了限行区;某台车承诺 17:40 到,按它自己给的行车时间算要 19:10。三处错误的共同点是——它们在语言层面完全通顺。
这和「知识问答答错」是两回事:知识错了可以靠检索压住,方案不可行只能靠代码挡。这段判断本文叫校验器(一段独立代码,拿方案逐条比对规则,通过就放行,不通过就说清违反了哪条、差多少)。
1.2 你原来的三件本事,两件能直接搬
物流调度、运输调度这个岗位,日常处理的其实不是「怎么算最短路径」,而是「哪些条件绝对不能破」。这件事恰好是大模型应用里最缺的一块。
| 你现在做的事 | 对应到系统里的位置 | 复用程度 |
|---|---|---|
| 把车型、载重、限行、时间窗写成派车前提 | 硬约束清单与校验函数 | 直接复用 |
| 判断「先保哪一单、哪一单可以让」 | 降级策略与松绑顺序 | 直接复用 |
| 异常时的处置流程(改派、拆单、报备) | 违反后的三种处置分支 | 直接复用 |
| 用启发式规则快速排一版可执行方案 | 校验通过后的兜底排法 | 部分复用 |
| 调用外部系统取实时车况与在途量 | 工具调用(让模型去调具体函数,而不是靠猜) | 必须补 |
| 用样本证明「这套判断是对的」 | 评测集(一批带标准答案的样本) | 必须补 |
表格第三行值得单独看:调度员处理异常的顺序是先改派、再拆单、最后才找客户改时间——这就是处置策略,过去在人脑里,现在要写进代码。
1.3 六个词先对齐
- 硬约束:不能违反的规则,违反了方案直接不成立,没有商量的余地。
- 软偏好:尽量满足、可以牺牲的期望,比如「司机更愿意跑熟路」。
- 纯函数:同样的输入永远得到同样的输出,不读数据库、不看当前时间、不调用模型。
- 可行性校验:拿一份候选方案逐条对照硬约束,判断它能不能执行。
- 降级:在不违反硬约束的前提下,换一个更保守、覆盖面更小的方案。
- 转人工:停下来,把方案和违反原因一起交给人决定。
二、判据只有一条:能不能写成纯函数
2.1 三个条件,缺一条就不算硬约束
判断一条业务规则能不能进校验器,只用一条判据:它能不能写成「输入 → 判断 → 通过/不通过 + 违反原因」的纯函数。
拆开是三个条件。第一,输入能列全:判这条规则要哪些数据,必须能一项项写出来——车型核载、货物重量、卸货点坐标、限行时段。列不全,说明它依赖了某个人脑子里的信息。第二,判断不需要酌情:一旦出现「一般情况下」「视情况而定」,它就不是硬约束。第三,失败时说得出差多少:只说「不通过」,模型收到三个字只会瞎改;收到「装载 12.4 吨,超出上限 11.8 吨」,它才知道往哪改。这一条最容易被忽略,却决定了下游重试有没有意义。
2.2 写不成纯函数的,就当软偏好
「冷链车全程 2 到 8 摄氏度」——听着是硬约束,能不能写成纯函数取决于你有没有连续温度记录。只有装车和卸货两个采样点,你只能判「两个采样点在区间内」。这时正确做法是拆成两条:可判定的「采样点合规」进校验器,判不了的「全程温控」变成随车设备告警项,不由这套系统承担。
「客户不喜欢下午到货」是偏好,进排序;「危险品不能和食品同车」是硬约束,判断是两边货物属性的集合交集。
把软偏好硬塞进校验器,后果比想象中严重:它会频繁拦下本来可执行的方案,重试几次都过不了,最后所有人都学会绕过它。一个经常误拦的校验器,比没有校验器更快被弃用。
2.3 运筹工具的表达方式,给了同一个信号
OR-Tools 是 Google 开源的组合优化工具套件,官方仓库对它的描述是「Google’s software suite for combinatorial optimization」(面向组合优化问题的软件套件),其中包含车辆路径问题(VRP,给一组车辆安排访问顺序,使总成本最低)的算法。
截至 2026-10-19,其官方文档对带容量的车辆路径问题(CVRP)的定义里,有一句措辞很关键:
The problem is to pick up or deliver the items for the least cost, while never exceeding the capacity of the vehicles.
注意是never exceeding,不是「尽量不超过」。这是硬约束在工程语言里的样子:不是一个权重很大的惩罚项,而是一个永远不会被越过的边界。写校验器时也该用这种语气——不是打分,是判生死。
同一页文档还承认了一件更重要的事:带约束的路径问题可能根本没有可行解,比如总货量超过车队总运力;而且判断一个问题有没有解本身就可能很难——即便总需求不超过总容量,判断所有货能不能装进所有车,也是多背包问题的一个变体。它意味着校验器不该假装能判一切:能判的当场判,判不了的升级给人,而不是给个「大概可以」放过去。
| 规则举例 | 输入能否列全 | 是否需要酌情 | 归类 | 处置 |
|---|---|---|---|---|
| 单车装载不超核载 | 能(载重、核载) | 否 | 硬约束 | 重试或降级 |
| 限行时段不进限行区 | 能(时段、区域、车牌) | 否 | 硬约束 | 重试 |
| 对客承诺到达时间不得晚于约定 | 能(承诺时间、预计到达) | 否 | 硬约束 | 转人工 |
| 冷链全程温控 | 采样点能,全程不能 | 部分 | 拆成两条 | 采样点校验 + 设备告警 |
| 客户偏好上午收货 | 能,但只是偏好 | 是 | 软偏好 | 排序加权 |
| 司机想跑熟路 | 不能量化 | 是 | 软偏好 | 排序加权 |
三、位置:模型之后,写库之前
3.1 五个候选位置,只有一个对
校验放在哪里,比写得多漂亮更重要。
| 位置 | 能拦住什么 | 拦不住什么 | 结论 |
|---|---|---|---|
| 提示词里写「务必满足载重」 | 一部分明显违规 | 需要计算才能发现的违规 | 只能当辅助 |
| 工具函数体内部 | 该工具自己产出的错误 | 多个工具组合出来的冲突 | 必要但不够 |
| 模型输出之后、写库之前独立一层 | 结构错误 + 业务违规 + 跨字段冲突 | 输入数据本身错了 | 主战场 |
| 写库之后 | 已落库的错误数据 | 一切,只能事后补救 | 不可接受 |
| 纯事后审计 | 统计层面的问题 | 单次事故 | 只能当兜底 |
工具内部的校验保证「这个函数不产出垃圾」,独立一层的校验保证「几个函数合起来的结果能执行」——前者是局部纪律,后者是全局闸门。
3.2 规则写成数据,管线写成函数
先把规则写成数据,再写代码:业务方改阈值不用改代码,评审时也能逐条对着看。
⚠️代码待验证
{"rule_id":"capacity_not_exceeded","level":"hard","inputs":["plan.routes","ctx.vehicle","ctx.demand"],"check":"每台车的装载量 <= 该车核载","on_fail":"retry","max_retry":2,"fallback":"downgrade"}inputs这一项是给评审用的:扫一眼就知道这条规则依赖哪些数据;某个字段在系统里取不到,这条规则现在就不能上。
⚠️代码待验证
defhandle(plan,ctx):"""模型输出 -> 结构校验 -> 硬约束校验 -> 处置 -> 写库"""problems=[]forruleinHARD_RULES:# 每条规则都是纯函数ok,reason=rule.check(plan,ctx)ifnotok:problems.append({"rule":rule.id,"reason":reason})ifnotproblems:returncommit(plan)# 全部通过才落库returndecide(plan,problems,ctx)# 处置分支见第 4 章⚠️代码待验证
defcheck_vehicle_capacity(plan,ctx):forrouteinplan["routes"]:load=sum(ctx["demand"][stop]forstopinroute["stops"])cap=ctx["vehicle"][route["vehicle_id"]]["capacity"]ifload>cap:returnFalse,"车 %s 装载 %.1f 吨,超出核载 %.1f 吨"%(route["vehicle_id"],load,cap)returnTrue,""这个函数只有十行,但做了三件对的事:不调用模型、不读数据库(数据由ctx传进来)、失败时说清了差多少。最后一点是它能和重试配合的前提。
3.3 为什么不能让模型自证合规
最常见的偷懒写法,是在提示词末尾加一句「请确认你的方案满足所有约束」。它几乎必然失败,原因有三。
模型检查的是它自己刚写出来的那段文字,不是真实数据:它写「载重 11.2 吨」就认为没超,而真实装载要去算每一单的重量。模型还很擅长给违规找理由:你问它这单会不会晚,它能写出一段合理的解释,比如「该客户历史收货时间较宽松」——真假你无法从文字上区分。更根本的是上下文不对称:模型看到的只是你喂进去的那部分,校验器看到的是系统全量数据——让信息少的一方审信息多的一方,本来就审不出东西。
一句话:模型负责提方案,代码负责判可行性,这两件事不能合并到同一个主体上。
四、违反之后:重试、降级、转人工
4.1 三种处置的分界线
选哪一种,不看模型说了什么,看这条规则的违规代价落在谁身上。
| 处置 | 触发条件 | 次数上限 | 必须留痕 |
|---|---|---|---|
| 重试 | 违规只涉及可调字段,且原因能定位到具体数值 | 2 次 | 违反了哪条、改了什么、第几次改对 |
| 降级 | 重试后仍违反,或约束在现有资源下确实无解 | 1 次 | 放弃了哪些点、为什么放弃、降级后是否复检 |
| 转人工 | 涉及金额、对客时效承诺、合规(危险品、限行、载重法规) | 不限 | 完整方案 + 全部违反原因 + 建议动作 |
一句话:**只要这条规则被违反会让公司对外承诺或承担法律责任,就不许自动改,必须转人工。**运费算错、到达时间承诺、危险品混装,这三类建议一律不给自动处置权限。
4.2 处置决策写成函数
⚠️代码待验证
HUMAN_ONLY={"freight_amount","sla_commit","compliance"}defdecide(plan,problems,ctx):rules={p["rule"]forpinproblems}ifrules&HUMAN_ONLY:# 金额 / 时效承诺 / 合规returnescalate(plan,problems)ifctx["retry_used"]<MAX_RETRYandrepairable(problems):returnretry_with(plan,problems)# 只回灌原因与可调字段ifhas_conservative_variant(plan,ctx):returndowngrade(plan,problems)# 降级后必须重新校验returnescalate(plan,problems)这段代码的顺序不能调。先判转人工,再判重试,最后才降级——降级会改变对客户的交付内容(少送一个点、晚到半天),分量比重试大,必须放在重试确实无效之后。
4.3 重试只能改可调字段,转人工不是失败
方案里的字段分两类。事实字段:客户地址、货物重量、承诺到达时间、车牌,不许模型改,改了就是在编数据。可调字段:车型、路线顺序、装载分配、发车时间,这些可以改。重试时回灌给模型的,应该只有「违反了哪条、差多少、哪些字段可以动」,不要回灌整份方案让它重写——重写会把已经正确的部分也改掉。
降级同理。OR-Tools 处理无解的方式,是给每个点设一个罚金(放弃访问的代价),让求解器在必须放弃时选总代价最小的组合,罚金大小决定它会放弃多少个点。这个设计给的启示是:松绑顺序必须事先写死。先放弃哪个客户是业务决定的,不能让模型运行时现场决定。写进配置,降级就可复现;交给模型,同一个场景两次运行可能放弃两家不同的客户,没人解释得清。
转人工不是系统没做好的证据,拼命压低它的触发次数,方向就错了。它把模型的错误变成人的一次决策,而不是一次事故,也是唯一能兜住「校验器自己判不了」的办法——回到第 2.3 节,判断有没有可行解本身可能就是难题,算法要算很久,调度员看一眼货单就知道该拆哪一单。LangChain 官方文档里的人工介入机制正放在这个位置:截至 2026-10-19,其做法是在模型生成响应之后、任何工具调用执行之前插入检查点,由配置的策略决定要不要停下来等人批准,并把「写文件、执行 SQL」举为典型场景。调度里的「下发派车单」属于同一类动作。
五、读校验日志:分清提示词没写清和模型不够用
5.1 三个切面,一个归因表
只看拦截总数没有意义,要盯三个切面。
首次违反的规则分布:集中在一条规则上,说明这条规则的信息没给到位;均匀撒在所有规则上,说明任务整体超出了模型当前的处理能力。重试后修正成功的占比:改得动的多,说明模型知道怎么做、只是第一次没注意,这是提示词问题;改不动的多,说明它根本不会算,这是能力问题。按输入类型切片:如果违规只在「临时加单」高发,那多半不是提示词也不是模型,是校验器缺了「加单会挤占哪台车」这个上下文。
| 现象 | 更可能的归因 | 先改什么 | 怎么确认改对了 |
|---|---|---|---|
| 违规集中在单条规则,重试多数能改对 | 提示词缺字段说明或单位 | 补字段定义、给一个正例 | 该规则首次违反明显下降 |
| 违规分散在多条规则,重试多数改不动 | 任务需要计算,模型算不准 | 把计算挪进工具,模型只选参数 | 改为工具调用后同类违规消失 |
| 违规集中在某类输入 | 校验器缺上下文 | 补数据源,而不是改提示词 | 该切片违规下降,其他切片不变 |
| 某条规则几乎不触发 | 规则写得太松或数据缺失 | 补边界样本测一遍 | 构造反例能被拦住 |
| 转人工占比持续偏高 | 阈值过严或降级策略缺失 | 复核阈值,补降级分支 | 人工复核工作量下降且无事故 |
第四行是反直觉的检查项。一条从来不报错的规则,很可能是根本没生效——字段取不到、条件写反、被前面分支短路了。所以每条规则上线前,都要用反例把它打出来一次。
5.2 把违规样本固化成评测集
每次线上拦下来的真实违规,都该变成一条测试样本。三个类型各存一份:典型反例(那次真实出事的方案,脱敏后原样保存)、边界值(刚好等于上限的方案,大于号还是大于等于号只有边界样本能测出来)、脏输入(缺字段、单位不一致、地址重复,这类最容易让校验器自己崩掉)。有了这三类样本,改任何一条规则都能立刻回归;没有它们,改规则就是凭感觉。
5.3 日志聚合
⚠️代码待验证
fromcollectionsimportCounterdefsummarize(records):total=len(records)by_rule=Counter(r["rule"]forrinrecords)retried=[rforrinrecordsifr["retry_used"]>0]fixed=[rforrinretriedifr["final"]=="passed"]return{"拦截次数":total,"按规则":dict(by_rule),"重试后修正成功":len(fixed),"重试后仍违反":len(retried)-len(fixed),"转人工":sum(1forrinrecordsifr["final"]=="human"),}这段统计不需要模型参与,纯计数;它的价值在于让「改提示词还是改架构」变成看数字就能决定的事。
调度约束清单模板:把本章的三类样本和第二章的规则表做成了可填写的清单,含规则编号、输入字段、判定条件、处置方式四列。放在资料包里,扫码即可获取:
六、上线前要过的三道门
6.1 可测:每条规则都要有一个能打死它的反例
规则写完,先用三件事验它:给一个明显违规的方案,它必须拦;给一个刚好卡在上限的方案,它必须放;给一个缺字段的方案,它必须报「数据不足」而不是静默通过。
第三件最难也最重要。缺字段时返回通过,等于给脏数据开后门;返回不通过,又会让正常方案被误拦。正确做法是返回第三种状态——无法判定,并转人工。三态比两态多写十行代码,能省掉后面几个月的扯皮。
6.2 可读:违反原因是写给调度员看的
「校验失败,规则编号不匹配」这种话只有写代码的人看得懂。违反原因应该长这样:「车 6 第二段行程 09:00 进入中环路,该路段 07:00 到 09:30 限行」。
判断标准很简单:把这句话发给当班调度员,他能不能立刻知道该怎么办。能就是合格,还要追问一句就是没写完。给模型看「哪个字段可以动」,给人看「违反了哪条规定」,同一个结构体存两份文案,比写两套逻辑省事。
6.3 可独立:关掉模型,校验器还能判
这是我最看重的一条验收标准:**把模型那层摘掉,人工填一份方案进去,校验器能不能正常判定?**能,说明校验器和模型解耦,换模型、升级、降级,防线都还在;不能,说明校验逻辑长在了提示词里,换一次模型就要重做一遍。
这里还有个易混淆的点:框架自带的重试和业务硬约束的校验,是两层东西。
| 层次 | 检查什么 | 谁来做 | 失败后怎么办 |
|---|---|---|---|
| 格式层 | 输出是不是合法结构、字段类型对不对 | 框架的结构化输出校验 | 自动回灌错误让模型重试 |
| 语义层 | 方案在业务上能不能执行 | 你自己的校验器 | 重试/降级/转人工 |
截至 2026-10-19,LangChain 官方文档对第一层的描述是:模型生成结构化数据后会被捕获、校验,结果放进 agent 状态的structured_response键;校验失败时框架提供自动重试,handle_errors参数默认True,可配成固定文案、指定异常类型、自定义处理函数,或设为False直接抛出。文档举的例子是评分超出允许区间这类格式与取值范围问题。它管不到「这台车装不装得下」——只做格式层校验就上线,等于只检查报表填得规不规整,没检查数对不对。
七、你的调度经验,怎么变成能拿出手的东西
7.1 复用与要补,分清楚
第 1.2 节列过能力映射,这里只说结论:约束建模、时效优先级、异常处置流程这三项是真正的资产,分别对应校验器、降级顺序和处置分支。要补的是三样——工具调用、能跑起来的校验函数、一套带反例的评测集。
最后一样是多数转行者的短板:前两样能在项目里磨,唯独评测要主动建立习惯——任何一条规则,没有反例就不算写完。
7.2 一个最小可交付
不用等公司立项。找二十条你最熟的派车规则,按第 2.1 节过一遍,能写成纯函数的留下,写不了的降级成偏好。然后做三件事。
第一,写校验器:二十条规则大约两百行,输入是一份方案加一份上下文快照,输出是三态加原因。第二,造样本:每条规则三个——典型反例、边界值、脏输入,六十个样本跑一遍只要几分钟。第三,接一个模型:让它读订单和车辆列表,输出结构化方案,再喂给校验器。你会看到一个现象:模型的方案通过情况,和规则写得清不清楚高度相关——规则越具体,它第一次就对的概率越高。这时候你改的是提示词,不是模型。
7.3 简历和面试里怎么说
不要说「使用大模型实现了智能调度」,这句话信息量为零,面试官没法追问。
说这三句:「我把 20 条派车硬约束写成了纯函数校验器,模型输出后先过校验再落库;违反按金额、时效承诺、合规三类分流,前两类直接转人工;上线前用 60 个样本做了回归,其中包括 20 个线上真实违规案例。」
这三句里有角色、有位置、有判据、有验证方式。面试官接下来问的一定是「你怎么判断一条规则算硬约束」,而你手上正好有第 2.1 节那三个条件。
调度场景校验器骨架:把第 3.2 节的管线、第 4.2 节的分流和第 5.3 节的统计合成了一份可直接改的代码骨架,含规则表与样本表两个空表。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 事实 | 出处(文档名 + 发布方 + 链接) | 本文位置 |
|---|---|---|
| CVRP 定义原文含「while never exceeding the capacity of the vehicles」,即最小成本且永不超过车辆容量 | 《Capacity Constraints》,Google OR-Tools 官方文档,https://developers.google.com/optimization/routing/cvrp | 第 2.3 节(核验方式见本表末行) |
| 带约束的路径问题可能不存在可行解,例如总货量超过车队总容量 | 同上 | 第 2.3 节 |
| 判断一个问题是否有解本身可能很难;即便总需求不超过总容量,判断全部货物能否装车属于多背包问题的一个变体 | 同上 | 第 2.3、4.3 节 |
| 罚金大小决定求解器放弃多少个访问点;罚金越大越倾向于尽量多点送达 | 《Penalties and Dropping Visits》,Google OR-Tools 官方文档,https://developers.google.com/optimization/routing/penalties | 第 4.3 节 |
| OR-Tools 是「Google’s software suite for combinatorial optimization」,包含旅行商问题与车辆路径问题算法,采用 Apache License 2.0 | google/or-tools 仓库 README,Google,https://github.com/google/or-tools | 第 2.3 节 |
模型生成结构化数据后会被捕获、校验,结果放入 agent 状态的structured_response键 | 《Structured output》,LangChain 官方文档,https://docs.langchain.com/oss/python/langchain/structured-output | 第 6.3 节 |
结构化输出校验失败时框架提供自动重试;handle_errors默认True,支持固定文案、异常类型、异常元组、自定义函数与False | 同上 | 第 6.3 节 |
| 人工介入发生在模型生成响应之后、任何工具调用执行之前,由配置的策略决定是否暂停 | 《Human-in-the-loop》,LangChain 官方文档,https://docs.langchain.com/oss/python/langchain/human-in-the-loop | 第 4.3 节 |
| 国内各平台调度接口的载重、限行校验字段名与取值 | 待验证:未核到可公开引用的官方接口文档,本文不写具体字段名与取值 | 第 3.2 节 |
| 本文全部代码均未在本机执行,仅作结构示意 | 本机说明(如实标注,供读者复核) | 第 3.2、4.2、5.3 节 |
| OR-Tools 英文页核验情况 | WebFetch 直连 https://developers.google.com/optimization/routing/cvrp 返回抓取失败;改用官方中文镜像 https://developers.google.cn/optimization/routing/cvrp 与同一官方页面的检索片段交叉核对 | 第 2.3 节 |
附表 B:术语速查表
| 术语 | 一句话解释 | 在本文哪里用到 |
|---|---|---|
| 硬约束 | 不能违反的规则,违反了方案直接不成立 | 第 1.3、2.1 节 |
| 软偏好 | 尽量满足、可以牺牲的期望 | 第 1.3、2.2 节 |
| 纯函数 | 同样输入永远同样输出,不读库、不看时间、不调模型 | 第 1.3、2.1 节 |
| 校验器 | 拿方案逐条比对硬约束、给出通过与原因的独立代码 | 第 1.1、3.1 节 |
| 降级 | 在不违反硬约束的前提下换更保守的方案 | 第 1.3、4.1 节 |
| 转人工 | 停下来,把方案与违反原因交给人的决定 | 第 1.3、4.3 节 |
| VRP | 车辆路径问题,给一组车辆安排访问顺序使总成本最低 | 第 2.3 节 |
| CVRP | 带容量约束的车辆路径问题,装载不得超过车辆容量 | 第 2.3 节 |
| 罚金 | 放弃访问某个点时计入的成本,用来在无解时做取舍 | 第 4.3 节 |
| 评测集 | 一批带标准答案的样本,用来判断改完是否变好 | 第 1.2、5.2 节 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。