ARTICLE DETAIL

资讯详情

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

本地任务拆分实战:L0硬规则+L1模型两级流水线

本地任务拆分实战:L0硬规则+L1模型两级流水线 1. 为什么要在本地做任务拆分1.1 从一次线上事故说起去年年底我接手了一个内部工单系统的自动化改造需求本身不复杂把用户提交的自然语言工单自动拆成结构化的子任务再分派给对应的处理人。一开始我图省事直接调了一个云端大模型的接口把整段工单文本丢进去让它一次性输出拆分结果。测试阶段一切正常上线第三天出了事——那天网络抖动接口超时率飙到 40%积压的工单直接把队列堵死值班同事半夜打电话过来我爬起来手动补数据补到凌晨四点。那次之后我就下定决心把任务拆分这条链路搬到本地来做。原因有三个第一工单内容涉及内部业务信息走云端接口本身就有合规风险第二拆分逻辑其实有大量固定模式比如帮我改一下登录页面的按钮颜色顺便把文案也更新一下这种完全可以用规则命中没必要每次都调模型第三本地推理虽然单次慢一点但胜在稳定可控不会因为外部服务抖动就把整条链路拖垮。这就是L0硬规则前置 L1模型兜底两级流水线的由来。L0 层用确定性规则处理高频、格式固定的输入L1 层用本地模型处理规则覆盖不到的复杂语义。两级串联既保证了吞吐又保住了准确率。1.2 这套方案到底解决什么问题说白了它解决的是**用规则太死、用模型太贵**这个经典矛盾。纯规则方案的问题是泛化能力差用户换个说法就命中不了纯模型方案的问题是成本和延迟都不可控尤其是本地部署场景下模型推理本身就吃资源如果每个请求都走模型GPU 排队能排到你怀疑人生。两级流水线的核心思路是分流让简单请求走快车道复杂请求走慢车道。我实测下来在一个日均 8000 条工单的场景里L0 层能拦下大约 62% 的请求剩下 38% 才进 L1。这意味着模型的实际负载只有全量请求的三分之一多一点单卡就能扛住不需要堆硬件。适合参考这套方案的人我总结了几类一是做企业内部自动化工具、对数据不出域有要求的开发者二是本地部署了模型但发现推理压力大、想优化吞吐的同行三是刚接触任务拆分这个概念、想找个能落地的入门架构的新手。如果你属于第三类建议先把 L0 层吃透规则引擎玩明白了再上模型也不迟。1.3 两级流水线的整体架构长什么样架构本身不复杂我用文字描述一下数据流向。请求进来之后先经过一个预处理模块做文本清洗、编码统一、长度截断这些杂活。清洗完的文本送进 L0 规则引擎引擎里挂了一组按优先级排序的规则每条规则包含匹配条件和对应的拆分模板。命中规则就直接产出结果打上source: L0的标记没命中就转交 L1。L1 层是一个本地部署的模型服务接收原始文本输出结构化的拆分结果打上source: L1的标记。两层的结果最终汇入同一个输出格式下游消费方不需要关心这个结果是哪一层产出的。这里有个设计细节值得说一下L0 和 L1 的输出格式必须严格一致。我一开始偷懒L0 输出的字段名和 L1 不一样结果下游解析代码里写了一堆 if-else 判断来源维护起来极其痛苦。后来统一成同一套 schema下游代码干净了一大截。2. L0 硬规则层的设计与实现2.1 规则怎么定从真实语料里挖模式规则不是拍脑袋想出来的得从真实数据里挖。我的做法是先收集最近一个月的工单文本大概两万条做一轮词频统计和句式聚类。具体操作上我用 jieba 做分词把高频动词和名词组合提取出来再人工过一遍归纳出几类典型模式。挖下来发现工单文本其实高度模板化主要集中在这几类模式类型典型表达占比单动作单对象修改登录页按钮颜色28%多动作并列改按钮颜色顺便更新文案19%带条件约束如果用户未登录就跳转首页9%纯咨询类这个功能怎么用6%复杂语义其他38%前四类加起来 62%正好对应我前面说的 L0 拦截率。这个数字不是巧合是规则覆盖能力的直接体现。你要做的第一件事就是拿自己的数据跑一遍这个统计看看你的 L0 理论上能覆盖多少。如果低于 40%说明你的数据太发散得重新考虑架构。2.2 规则引擎的选型为什么我没用现成的市面上规则引擎不少Drools、Easy Rules 这些我都试过。最后我选择自己写一个轻量级的原因很实际现成引擎功能太全配置复杂学习成本高而我需要的只是按优先级匹配 输出模板这两个功能。为了这两个功能引入一个几万行的依赖不划算。我自己写的引擎核心就三个类Rule规则定义、RuleEngine匹配调度、TemplateRenderer模板渲染。规则用 YAML 配置方便非开发同事也能改。一条规则的配置长这样- id: rule_001 priority: 100 pattern: (修改|调整|改一下).*(颜色|样式|配色) template: | { action: modify_style, target: {target}, attribute: color } enabled: truepriority越大越先匹配pattern是正则template是输出模板。匹配到之后用正则的捕获组填充模板占位符。这套东西加起来不到 300 行代码但覆盖了我 90% 的规则需求。提示正则写规则有个坑中文的贪婪匹配很容易吃掉不该吃的内容。建议在关键位置用非贪婪模式.*?并且给每条规则写单元测试用真实语料验证。2.3 规则优先级与冲突处理规则一多冲突就来了。比如修改登录页按钮颜色这句话可能同时命中修改样式和修改页面元素两条规则。这时候优先级就起作用了数字大的先匹配匹配到就返回不再往下走。但光靠优先级不够因为有些规则是互斥的有些是可以叠加的。我的处理方式是给规则加一个exclusive字段标记这条规则是否独占。独占规则命中后直接返回非独占规则命中后继续往下匹配最后合并结果。def match(self, text): results [] for rule in sorted(self.rules, keylambda r: -r.priority): if rule.pattern.search(text): results.append(rule.render(text)) if rule.exclusive: break return results这段逻辑很简单但实际跑起来效果很好。我踩过的坑是非独占规则的结果合并顺序很重要。一开始我用列表追加结果下游拿到的字段顺序是乱的。后来改成按 priority 排序后再合并问题解决。2.4 规则的可维护性让非开发也能改规则这东西业务变化快今天加一条明天改一条。如果每次都要开发介入效率太低。我的做法是把规则配置抽成独立的 YAML 文件放在一个共享目录里业务同事改完提交走一个简单的审核流程就能生效。为了防止改错我加了一个规则自检脚本每次配置变更后自动跑一遍检查正则语法是否合法、模板占位符是否和捕获组数量匹配、优先级是否有重复。这个脚本帮我拦下了好几次低级错误强烈建议你也加一个。import re, yaml def validate_rule(rule): try: pattern re.compile(rule[pattern]) except re.error as e: return f正则语法错误: {e} groups pattern.groups placeholders re.findall(r\{(\w)\}, rule[template]) if len(placeholders) groups: return f占位符数量({len(placeholders)})超过捕获组数量({groups}) return None3. L1 模型兜底层的落地细节3.1 本地模型选型不是越大越好L1 层的核心是本地模型。选型的时候我对比了几个维度参数量、推理速度、显存占用、中文能力。最后选了一个 7B 级别的指令微调模型量化到 4bit 之后显存占用大概 5GB单张消费级显卡就能跑。为什么不选更大的因为任务拆分这个场景对模型的指令遵循能力要求高于知识储备。7B 模型在指令微调之后处理结构化输出完全够用。我实测过 13B 和 7B 在同一个测试集上的表现准确率差距不到 3 个百分点但推理速度差了将近一倍。在吞吐优先的场景下7B 是更划算的选择。部署方式上我用的是 Ollama一条命令就能拉起来省去了自己配环境的麻烦。启动命令很简单ollama run qwen2.5:7b-instruct-q4_K_M跑起来之后它会暴露一个本地 HTTP 接口默认在 11434 端口。Python 侧用 requests 直接调就行不需要额外的 SDK。3.2 提示词设计结构化输出的关键模型能不能稳定输出结构化结果全看提示词怎么写。我试过好几种写法最后稳定下来的版本包含四个部分角色设定、任务描述、输出格式、示例。角色设定要具体不能只说你是一个助手要说你是一个任务拆分专家负责把用户的自然语言需求拆成结构化的子任务列表。任务描述要明确边界告诉它什么该拆什么不该拆。输出格式用 JSON Schema 描述越详细越好。示例给两到三个覆盖典型场景。PROMPT_TEMPLATE 你是一个任务拆分专家。请把用户输入拆分成子任务列表。 要求 1. 每个子任务包含 action动作、target对象、detail细节三个字段 2. 如果无法拆分返回包含单个子任务的列表 3. 只输出 JSON不要输出任何解释文字 输出格式 {{tasks: [{{action: ..., target: ..., detail: ...}}]}} 示例输入改一下登录页按钮颜色顺便更新文案 示例输出{{tasks: [{{action: modify, target: 登录页按钮, detail: 颜色}}, {{action: update, target: 文案, detail: }}]}} 用户输入{user_input} 这里有个细节示例的质量直接决定输出质量。我一开始给的示例太简单模型遇到复杂输入就开始自由发挥。后来把示例换成带条件约束的复杂案例输出稳定性明显提升。3.3 输出解析与容错模型输出 JSON 这件事理想很丰满现实很骨感。即使提示词写得再好偶尔还是会冒出多余的解释文字、markdown 代码块标记、或者干脆 JSON 格式错误。所以解析层必须做容错。我的解析逻辑分三步先尝试直接json.loads失败了就用正则提取第一个完整的 JSON 对象再失败就返回一个降级结果标记source: L1_failed让下游决定怎么处理。import json, re def parse_model_output(text): try: return json.loads(text) except json.JSONDecodeError: pass match re.search(r\{.*\}, text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass return {tasks: [], error: parse_failed}这个降级机制救过我好几次。有一次模型升级后输出格式变了解析全挂但因为降级逻辑在系统没崩只是准确率掉了给了我缓冲时间修。3.4 推理性能优化批处理与缓存本地模型推理性能是绕不开的话题。我做了两件事来优化批处理和缓存。批处理是指把多个请求攒一批一起送进模型。Ollama 本身支持 batch但需要你在请求时指定。我设的批大小是 8实测下来吞吐能提升 2 倍多。不过批大小不是越大越好太大显存扛不住而且单次延迟会变高。8 是我在 5GB 显存下的甜点值。缓存是指对相同或相似的输入直接返回历史结果。我用了一个简单的 LRU 缓存key 是输入文本的哈希value 是拆分结果。命中率大概 15%虽然不高但省下来的推理时间很可观。from functools import lru_cache lru_cache(maxsize1000) def split_with_model(text): # 调用模型推理 ...注意缓存要设过期时间不然业务规则变了缓存还返回旧结果会出问题。我设的是 24 小时。4. 两级流水线的串联与调度4.1 请求分发逻辑两级串联的核心是分发逻辑。请求进来先走 L0命中就返回没命中转 L1。听起来简单但实际写的时候有几个细节要注意。第一L0 的匹配要有超时保护。正则匹配虽然快但如果规则写得不好遇到超长文本可能会卡住。我加了一个 50ms 的超时超时就当没命中转 L1。第二L1 的调用要异步。模型推理慢如果同步等会把整个请求线程堵死。我用了一个简单的线程池L1 调用丢进池子里异步执行主线程轮询结果。第三结果要统一格式。前面说过L0 和 L1 的输出 schema 必须一致。我定义了一个TaskResult类两层都往这个类里填下游只认这个类。class TaskResult: def __init__(self, tasks, source, latency): self.tasks tasks self.source source # L0 or L1 self.latency latency4.2 降级与熔断策略本地部署虽然稳定但也不是铁板一块。模型服务可能因为显存不足崩掉规则文件可能因为误改加载失败。这些情况都要有降级方案。我的降级策略是分层的L1 挂了就返回 L0 的结果哪怕 L0 没命中也返回一个空结果加错误标记而不是直接抛异常。L0 挂了就直接全量走 L1。两层都挂了返回一个兜底结果标记source: fallback同时触发告警。熔断用的是经典的滑动窗口算法统计最近 100 次调用的失败率超过 30% 就熔断 30 秒期间所有请求直接走降级路径。这个机制在模型服务重启的时候特别有用避免了大量请求堆积。4.3 监控指标与日志没有监控的流水线就是黑盒。我埋了几个关键指标L0 命中率、L1 平均延迟、L1 失败率、端到端延迟 P99。这些指标用 Prometheus 采集Grafana 展示。日志方面每个请求都记一条结构化日志包含请求 ID、输入文本哈希、走的哪一层、耗时、结果状态。出问题的时候拿请求 ID 一查就能定位。import logging, json logger logging.getLogger(pipeline) def log_request(req_id, text, source, latency, status): logger.info(json.dumps({ req_id: req_id, text_hash: hash(text), source: source, latency_ms: latency, status: status }))这套监控上线之后我发现了一个之前没注意到的问题L1 的延迟在每天下午三点左右会有一个尖峰。查下来是因为那个时间段有批量任务在跑抢了 GPU 资源。后来把批量任务挪到凌晨问题解决。5. 实操中踩过的坑与排查技巧5.1 规则误命中一个真实的案例有一次业务同事反馈说删除用户这个工单被拆成了修改用户信息。查下来发现L0 里有一条规则是(修改|调整|改).*(用户)而删除用户里的除字被正则的.*吃掉了导致误命中。这个坑的根源是正则写得太宽泛。修复方式是把动作词限定得更死用(修改|调整|改一下)而不是(修改|调整|改)并且在动作词后面加一个边界断言。改完之后误命中率从 3% 降到了 0.2%。提示写规则的时候一定要用真实语料做回归测试。我现在的做法是维护一个 500 条的测试集每次改规则都跑一遍看命中率和准确率的变化。5.2 模型输出不稳定温度参数的调节模型输出时好时坏是新手最容易遇到的问题。我一开始用的是默认温度 0.7结果同样的输入有时候输出两个子任务有时候输出三个。后来把温度调到 0.1输出就稳定多了。温度这个参数本质是控制采样的随机性。任务拆分这种需要确定性输出的场景温度越低越好。我实测下来0.1 到 0.2 之间是比较合适的区间再低会显得死板再高就开始飘。除了温度top_p和repeat_penalty也值得调。我的配置是temperature0.1, top_p0.9, repeat_penalty1.1这套参数在我的场景下表现最稳。5.3 显存不足量化与显存管理本地跑模型显存不足是家常便饭。我的显卡是 12GB 的跑 7B 模型 4bit 量化后占用 5GB 左右看起来够用但实际上跑一段时间就会 OOM。查下来是因为 Ollama 默认会缓存多个模型实例显存没及时释放。解决办法有两个一是设置OLLAMA_MAX_LOADED_MODELS1限制同时加载的模型数量二是定期重启模型服务我写了个定时任务每天凌晨重启一次释放碎片显存。如果显存实在紧张可以考虑用更激进的量化比如 3bit 或者 2bit。但量化越低模型能力损失越大需要你自己权衡。我的建议是优先保证 4bit实在不行再降。5.4 常见问题速查表问题现象可能原因排查方向解决方案L0 命中率骤降规则文件加载失败检查日志中规则加载记录修复 YAML 语法重新加载L1 延迟飙升GPU 被其他任务占用查看 GPU 利用率错峰调度或限制并发输出 JSON 解析失败模型输出格式漂移检查原始输出文本加强提示词约束增加解析容错端到端超时L1 同步调用阻塞检查线程池状态改为异步调用加超时保护结果不一致缓存未过期检查缓存命中日志缩短缓存过期时间这张表是我从实际运维中总结出来的基本上覆盖了 80% 的常见问题。遇到问题先查表能省不少时间。6. 性能实测与调优记录6.1 测试环境与数据集测试环境是一台带 12GB 显存的机器CPU 是 8 核内存 32GB。模型用的是 7B 指令微调版4bit 量化。数据集是从真实工单里抽的 2000 条覆盖了前面说的五类模式。测试指标有三个L0 命中率、L1 平均延迟、端到端 P99 延迟。测试方法是把 2000 条数据灌进流水线记录每条的处理路径和耗时。6.2 调优前后的对比调优前L0 命中率 58%L1 平均延迟 1.2 秒端到端 P99 是 3.5 秒。这个成绩不算差但 P99 偏高说明有长尾请求。调优做了三件事一是把 L0 的规则重新梳理了一遍合并了冗余规则命中率提到 62%二是给 L1 加了批处理平均延迟降到 0.8 秒三是加了缓存命中缓存的请求直接返回P99 降到 1.8 秒。指标调优前调优后L0 命中率58%62%L1 平均延迟1.2s0.8s端到端 P993.5s1.8s模型负载占比42%38%这个提升幅度不算惊艳但胜在稳定。尤其是 P99 从 3.5 秒降到 1.8 秒用户体验的改善是肉眼可见的。6.3 还能怎么继续优化如果还想继续压榨性能有几个方向可以试。一是把 L0 的规则引擎换成更高效的数据结构比如用 Trie 树做前缀匹配理论上能把匹配耗时再降一半。二是给 L1 加一个更激进的缓存策略比如用语义相似度做缓存而不是精确匹配命中率能提到 30% 以上。三是考虑模型蒸馏用大模型生成一批标注数据蒸馏一个小模型专门做任务拆分推理速度能再快一倍。不过这些优化都有成本要不要做取决于你的实际瓶颈在哪。我的建议是先把监控做好找到真正的瓶颈再动手别为了优化而优化。7. 一些个人体会这套流水线我从去年年底跑到现在快一年了中间迭代了七八个版本。最大的体会是规则和模型不是对立的而是互补的。很多人一上来就想用模型解决所有问题结果发现成本和延迟都扛不住也有人死守规则结果泛化能力差维护成本高。两级流水线的价值就在于让合适的东西做合适的事。另一个体会是监控比优化更重要。我见过太多人花大力气调优结果上线之后连哪里慢都不知道。先把指标埋好让数据告诉你瓶颈在哪再动手效率高得多。最后分享一个小技巧L0 的规则不要一次写太多先从最高频的几条开始跑一段时间看命中率再逐步补充。我一开始一口气写了 50 条规则结果维护起来极其痛苦后来砍到 20 条核心规则命中率反而更高了。规则这东西质量比数量重要。
返回列表