1. 项目概述:当确定性规则遇上非确定性智能

最近在做一个挺有意思的项目,核心是把传统的规则引擎和现在火热的混元大模型给“焊”在了一起。听起来有点抽象,对吧?简单来说,就是让一个做事一板一眼、绝对靠谱的“老员工”(规则引擎),和一个思维活跃、能举一反三的“天才实习生”(大模型)搭档干活。这个组合的目标很明确:既要保证业务核心流程的绝对稳定和可控(这是规则引擎的强项),又要能灵活处理那些规则写不完、甚至根本预料不到的复杂场景(这是大模型的舞台)。

为什么非得这么干?因为纯靠规则,系统会越来越笨重。业务每变一次,就得吭哧吭哧改一堆 if-else ,维护成本高,响应还慢。而纯靠大模型,心里又没底,它今天这么答,明天可能就那么答了,用在生产环境的关键决策上,谁敢打包票?所以,我们琢磨出的这条路子,本质上是在寻找一种“确定性骨架”与“智慧大脑”的平衡。骨架撑起主体,保证不走形;大脑填充血肉,让系统更聪明、更人性化。这个实践,特别适合风控审核、智能客服决策、个性化推荐策略等既要求严谨流程,又需要智能判断的场景。

2. 架构设计与核心思路拆解

2.1 为什么是“规则引擎+大模型”?

这个组合不是拍脑袋想出来的,而是为了解决实际工程中的几个核心痛点。

痛点一:规则的“长尾效应”与维护噩梦。 在复杂的业务系统中,比如金融信贷审批,核心规则(如“年龄需大于22岁”)可能只占20%,但为了覆盖各种边界情况和特殊场景,你需要编写80%的复杂、琐碎甚至相互冲突的规则。这些规则相互嵌套,形成一个“规则网”,任何改动都可能引发意想不到的连锁反应。Drools这类规则引擎虽然通过将业务规则从代码中分离(实现了逻辑与数据的解耦),并提供了高效的Rete算法进行匹配,但规则的编写和维护依然高度依赖领域专家,且无法处理规则未定义的“未知情况”。

痛点二:大模型的“黑盒”与不确定性。 混元这类大模型能力强大,能理解自然语言,进行推理和生成。但你若让它独自处理一个严谨的审批流程,它可能会因为提示词(Prompt)的细微差别、模型本身的随机性(temperature参数影响)或训练数据偏差,给出不一致甚至错误的判断。在生产环境中,这种不确定性是不可接受的。

解决方案:分层决策与职责分离。 我们的架构核心思想是“分层”。将整个决策流程视为一个管道(Pipeline):

  1. 规则层(确定性骨架) :处理所有明确的、已定义的、必须严格执行的逻辑。例如,硬性合规检查、基础数据校验、流程路由。这一层输出是二元的(通过/拒绝)或结构化的(如评分、标签),且100%可预测、可审计。
  2. 大模型层(智慧大脑) :接管规则层无法覆盖或处理不好的部分。例如:
    • 复杂信息抽取与理解 :从用户冗长的描述中,精准提取关键事件、情绪和诉求。
    • 模糊场景裁决 :规则判断结果为“边缘案例”或“需人工复核”时,由大模型提供辅助决策建议。
    • 自然语言生成与交互 :根据规则执行结果和大模型分析,生成个性化、人性化的答复或报告。
    • 异常模式发现 :分析历史决策数据,发现潜在的新规则模式,反哺给规则层进行优化。

这样,规则引擎确保了基础效率和稳定性,大模型则提供了灵活性和智能上限。两者通过清晰的接口和数据契约进行协作。

2.2 技术选型与组件考量

规则引擎选型:Drools的得与失 我们选择了Drools,主要是看中其成熟度和强大的社区生态。它的核心优势在于:

  • 高效的Rete算法 :对于大量规则的匹配,性能优于简单的顺序判断。
  • DSL(领域特定语言) :允许业务人员用接近自然语言的格式编写规则,提升了可读性。
  • 决策表 :对于条件组合固定的规则(如费率表),用Excel格式管理非常方便。
  • 与Java生态无缝集成 :作为Java项目,集成成本低。

注意 :Drools的学习曲线并不平缓,特别是理解其工作内存(Working Memory)、议程(Agenda)和执行流程需要时间。另外,对于超大规模、动态更新的规则集,其规则文件的版本管理和热更新需要精心设计,通常需要结合Git和配置中心。

可视化规则编辑 :这是提升运营效率的关键。我们并没有完全自己造轮子,而是基于开源组件进行二次开发,实现了一个Web版的规则编辑器。运营人员可以在界面上通过拖拽条件节点、配置参数来定义规则,后端将其转换为Drools原生的DRL文件。这一步极大地降低了业务人员参与规则维护的门槛。

大模型层选型:混元与备选方案 混元大模型在中文场景、知识问答和逻辑推理上表现不错,且提供了稳定的API。选择它主要基于:

  1. API易用性与稳定性 :文档清晰,SDK完善,对于企业级应用,稳定性比追求极致性能更重要。
  2. 成本可控 :有明确的计价模式,便于进行项目预算和成本核算。
  3. 功能对齐 :其多轮对话、函数调用(Function Calling)能力与我们的架构契合。

当然,这不是唯一选择。架构上我们保持了对大模型的抽象。通过定义统一的 LLMService 接口,我们可以轻松切换后端模型。例如,对于需要本地部署、数据隐私要求极高的场景,可以集成 Ollama 部署的 Llama 3 Qwen 模型;对于简单的任务,也可以使用 OpenAI Anthropic 的API。 LangChain LlamaIndex 这类框架可以帮助我们管理不同的模型提供商和构建复杂的链(Chain),但在我们的架构中,由于规则引擎已经承担了主要流程控制,我们对大模型的调用相对直接,因此选择了更轻量级的自定义封装。

3. 核心细节解析与实操要点

3.1 规则引擎的“骨架”如何搭建

规则引擎不是简单地把 if-else 搬出来,而是需要一套工程化的设计方法。

领域模型设计是基石 你的规则作用于什么数据?这需要定义一个清晰的、富含业务语义的领域对象模型。例如,在信贷审批场景,你需要一个 LoanApplication (贷款申请)对象,里面包含 Applicant (申请人)信息、 Income (收入)证明、 CreditReport (信用报告)等嵌套对象。这些对象将被插入到Drools的“工作内存”中,供规则匹配。

// 示例领域对象
public class LoanApplication {
    private String id;
    private Applicant applicant; // 包含年龄、职业等
    private double amount;
    private int term;
    private List<IncomeRecord> incomes;
    private CreditReport creditReport;
    private String status; // 规则引擎会修改这个状态
    // getters and setters
}

规则的组织与分类 不要把所有规则写在一个文件里。我们按业务域和决策阶段进行拆分:

  • rules/validation/ :基础校验规则(如数据完整性、格式)。
  • rules/compliance/ :合规性硬规则(如反洗钱名单检查)。
  • rules/risk/ :风险评分规则(输出一个风险分数)。
  • rules/routing/ :流程路由规则(决定下一步是自动通过、转人工还是触发大模型分析)。

这种分类便于管理、测试和权限控制(比如合规规则只能由法务人员修改)。

规则的编写技巧

  • 善用 salience 属性 :控制规则执行优先级。通常,否决性规则(如黑名单检查)优先级最高。
  • 避免规则循环激活 :规则右部( then 部分)修改了工作内存中的事实,可能导致其他规则被重新激活,设计不当会引起死循环。要谨慎修改触发当前规则的事实。
  • 使用逻辑插入( insertLogical :在需要基于一定条件临时推导出新事实时使用,当条件不再成立时,Drools会自动收回该事实,有助于管理内存和逻辑状态。

3.2 大模型“大脑”的接入与提示工程

大模型不是魔法,你需要清晰地告诉它要做什么。这就是提示工程。

设计系统角色(System Role) 每次调用大模型API时,我们都会在消息列表开头赋予它一个明确的角色,这能极大地稳定其输出。

你是一个专业的金融风控分析助手。你的任务是仔细分析用户贷款申请中的软性信息,并结合给定的基础规则判断结果,提供是否批准的辅助建议。你必须严格基于提供的事实进行分析,不得捏造信息。你的输出必须是结构化的JSON格式。

这个角色指令限定了模型的回答范围和风格。

构建高质量的上下文(Context) 大模型需要信息才能工作。我们会把规则引擎处理后的结构化信息,以及从原始文本中提取的关键片段,作为上下文喂给模型。

## 申请信息
- 申请人:张三,35岁,自由职业者。
- 申请金额:20万元,期限3年。
- 收入情况:近半年银行流水显示月均收入约1.5万元,但波动较大。
- 信用报告:无逾期记录,信用分680(中等)。
- **规则引擎初步判断**:规则校验通过,但风险评分模块给出“中等风险”标签,触发人工复核规则。

## 用户补充说明(来自申请表单)
“我是一名独立设计师,项目收入有季节性,但年度总收入稳定。本次借款用于购置专业设备,预计能提升未来收入。”

## 你的任务
请分析申请人的职业稳定性和还款意愿,给出是否建议批准贷款的简要理由(不超过200字),并按以下JSON格式输出:
{
  "suggestion": "approve" | "reject" | "need_more_info",
  "confidence": 0.8, // 置信度,0-1之间
  "reasoning": "你的分析理由..."
}

函数调用(Function Calling)的妙用 对于需要触发具体业务动作的场景,我们利用大模型的函数调用能力。例如,当模型分析认为“需要补充收入证明”时,它可以调用一个预定义的 requestAdditionalDocument() 函数,我们的系统接收到这个调用请求后,就会自动向用户发起材料补交通知。这实现了从“分析”到“行动”的闭环。

3.3 两者之间的数据流转与协同协议

这是整个架构的“粘合剂”,设计好坏直接决定系统是否顺畅。

状态标志位设计 我们在核心的领域对象(如 LoanApplication )中设计了一系列状态标志位,作为两个引擎之间的“通信信号”。

public class LoanApplication {
    // ... 其他字段
    private String ruleEngineStatus; // “PASSED”, “REJECTED”, “NEED_REVIEW”
    private String reviewTriggerReason; // 规则引擎触发复核的原因码
    private LLMReviewResult llmReviewResult; // 大模型分析结果的封装对象
    private String finalDecision; // 最终决策
}

规则引擎执行完毕后,会设置 ruleEngineStatus reviewTriggerReason 。下游的决策协调服务根据这些状态,决定是否调用、以及如何调用大模型。

决策协调服务 这是一个轻量的服务层,它不包含业务逻辑,只负责流程编排:

  1. 接收业务请求,初始化领域对象,调用规则引擎。
  2. 解析规则引擎输出。
  3. 如果状态为 NEED_REVIEW ,则根据 reviewTriggerReason 组装对应的提示词模板,调用大模型服务。
  4. 接收大模型返回的结构化结果(如JSON),将其解析并更新到领域对象。
  5. 执行最终的决策逻辑 :这里可能是一个简单的规则(如“规则拒绝则最终拒绝,规则通过且大模型建议批准则最终批准”),也可能是一个更复杂的加权投票机制。这个最终逻辑本身,也可以配置成一条高优先级的规则,放在Drools中执行,实现逻辑的集中管理。

异步与降级考虑 大模型调用可能耗时较长(几秒),对于实时性要求高的场景,可以采用异步策略:规则引擎快速返回“已提交复核”,后续结果通过消息队列或WebSocket推送。同时,必须设计降级方案:当大模型服务不可用时,系统能自动 fallback 到纯规则流程或直接转人工,保障核心业务不中断。

4. 实操过程与核心环节实现

4.1 环境搭建与基础配置

我们以Spring Boot为基础框架进行集成。

Drools集成 pom.xml 中添加依赖:

<dependency>
    <groupId>org.kie</groupId>
    <artifactId>kie-spring</artifactId>
    <version>7.73.0.Final</version>
</dependency>

配置 KieContainer Bean,让它从类路径或指定的Git仓库加载我们的规则文件( .drl , .xlsx 决策表)。

@Configuration
public class DroolsConfig {
    @Bean
    public KieContainer kieContainer() {
        KieServices ks = KieServices.Factory.get();
        KieFileSystem kfs = ks.newKieFileSystem();
        // 从指定目录加载所有规则文件
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resourcePatternResolver.getResources("classpath*:rules/**/*.drl");
        for (Resource resource : resources) {
            kfs.write(ResourceFactory.newUrlResource(resource.getURL()));
        }
        KieBuilder kieBuilder = ks.newKieBuilder(kfs).buildAll();
        KieModule kieModule = kieBuilder.getKieModule();
        return ks.newKieContainer(kieModule.getReleaseId());
    }
}

大模型服务抽象 定义 LLMService 接口:

public interface LLMService {
    LLMResponse chatCompletion(LLMRequest request);
    JsonNode functionCall(LLMRequest request, List<Tool> tools);
}

实现 HunyuanServiceImpl ,内部使用混元官方SDK或封装HTTP客户端。同时,可以写一个 OllamaServiceImpl 作为备用或用于本地测试。

4.2 核心决策流水线实现

以下是决策协调服务中核心方法 processApplication 的简化伪代码:

@Service
public class DecisionOrchestrationService {
    @Autowired
    private KieContainer kieContainer;
    @Autowired
    private LLMService llmService;
    @Autowired
    private ApplicationRepository appRepo;

    public LoanApplication processApplication(LoanApplication app) {
        // 阶段1: 规则引擎执行
        KieSession kieSession = kieContainer.newKieSession();
        kieSession.insert(app);
        kieSession.fireAllRules(); // 触发所有匹配规则
        kieSession.dispose();

        // 阶段2: 根据规则结果判断是否需要大模型介入
        if ("NEED_REVIEW".equals(app.getRuleEngineStatus())) {
            // 根据复核原因码,选择提示词模板
            String promptTemplate = selectPromptTemplate(app.getReviewTriggerReason());
            // 填充模板,构建完整的LLM请求
            LLMRequest request = buildLLMRequest(app, promptTemplate);
            // 调用大模型
            LLMResponse response = llmService.chatCompletion(request);
            // 解析响应,更新申请对象
            LLMReviewResult reviewResult = parseLLMResponse(response);
            app.setLlmReviewResult(reviewResult);
            // 触发最终决策规则(可能是一条单独的规则)
            executeFinalDecisionRule(app);
        } else {
            // 规则引擎已有明确结论(通过/拒绝),直接作为最终决策
            app.setFinalDecision(app.getRuleEngineStatus());
        }

        // 保存并返回结果
        appRepo.save(app);
        return app;
    }

    private void executeFinalDecisionRule(LoanApplication app) {
        // 可以再次创建一个简单的KieSession,只包含最终决策的规则
        // 或者,直接在这里编写简单的Java逻辑
        if ("REJECTED".equals(app.getRuleEngineStatus())) {
            app.setFinalDecision("REJECTED");
        } else if ("APPROVE".equals(app.getLlmReviewResult().getSuggestion())) {
            app.setFinalDecision("APPROVED");
        } else {
            app.setFinalDecision("MANUAL_REVIEW");
        }
    }
}

4.3 规则可视化编辑器的前端对接

前端使用React/Vue,通过组件库构建规则画布。当用户拖拽配置完一个规则节点(例如:“申请金额 > 10万”),前端会将其序列化为一个JSON结构。这个结构描述了规则的条件(Left Hand Side, LHS)和动作(Right Hand Side, RHS)。

后端提供一个 /rule/compile 接口,接收这个JSON,将其转换为Drools DRL语法。转换器需要处理各种操作符、字段类型和内置函数。生成的DRL文件会被保存到规则仓库(如Git),并触发一个规则重载的机制(例如,通过Drools的 KieScanner 或发送一个刷新事件到 KieContainer )。

实操心得 :规则可视化不是为了取代DRL,而是为了降低入门门槛。复杂的、性能关键的规则,仍然建议直接编写DRL。可视化编辑器产出的DRL,一定要有回显和预览功能,让高级用户能够看到并微调生成的代码,这是一个很好的“兜底”和学习机制。

5. 常见问题与排查技巧实录

在实际开发和上线过程中,我们踩了不少坑,也积累了一些排查经验。

5.1 规则引擎侧典型问题

问题1:规则不生效或执行顺序不符合预期。

  • 排查步骤
    1. 检查事实插入 :用日志或调试器确认你的领域对象是否被正确插入到 KieSession 中。常见错误是插入了对象,但对象内部为null的字段没有初始化,导致规则条件匹配不上。
    2. 检查规则条件语法 :DRL语法严格,特别是字段类型和操作符。 Customer(age > 18) Customer(age > “18”) 天差地别。
    3. 检查 salience agenda-group :高 salience 的规则优先执行。如果使用了 agenda-group ,需要显式调用 kieSession.getAgenda().getAgendaGroup(“group-name”).setFocus() 来激活该组规则。
    4. 查看规则匹配情况 :启用Drools的日志级别为DEBUG,可以看到哪些规则被激活(Activation),这非常有用。
  • 技巧 :为重要的规则在RHS部分添加日志输出,如 System.out.println(“规则[高风险检查]被触发,应用于客户:” + $customer.getName()); ,这是最直接的调试方式。

问题2:性能瓶颈,规则匹配慢。

  • 排查步骤
    1. 分析规则复杂度 :规则条件中是否包含大量的 eval() 或复杂的函数调用?这些会严重拖慢匹配速度。尽量将计算移到规则外部,以事实属性(field)的形式插入。
    2. 检查事实数量 :是否向工作内存中插入了过多不必要的事实?只插入与当前会话相关的事实。
    3. 审视Rete网络 :过于复杂的规则交叉引用会导致Rete网络膨胀。考虑使用 ruleflow-group agenda-group 对规则进行分段执行,减少单次匹配的规则数量。
  • 技巧 :使用Drools提供的 StatelessKieSession 替代 StatefulKieSession 如果场景允许。前者在执行后不保留会话状态,开销更小。

5.2 大模型侧典型问题

问题1:大模型响应不稳定,时好时坏。

  • 排查步骤
    1. 固定随机种子和温度 :在API调用中,明确设置 temperature=0.1 (甚至0)和 seed 参数。这能极大减少生成结果的随机性,使输出更确定。
    2. 审查提示词 :提示词是否模糊、有歧义?是否提供了足够的上下文和示例?尝试使用“少样本学习”(Few-shot Learning)在提示词中给出几个输入输出的例子。
    3. 检查上下文长度 :是否超过了模型的最大上下文窗口?超长的上下文可能导致模型忽略前面的关键指令。做好文本摘要和精炼。
    4. 模型本身波动 :接受大模型服务本身可能存在的不稳定性。对于关键应用,实现重试机制和多个模型的fallback策略。
  • 技巧 :建立“提示词版本库”。每次对提示词的修改都记录版本和测试结果。当模型行为异常时,首先回滚到最近一个稳定的提示词版本进行对比测试。

问题2:大模型输出格式不符合要求,JSON解析失败。

  • 排查步骤
    1. 强化格式指令 :在提示词中明确要求,并使用三重反引号标记JSON部分。例如:“你的输出必须是如下JSON格式,不要有任何其他解释: json {...} ”。
    2. 使用函数调用 :如果API支持,优先使用函数调用功能。模型会返回一个结构化的函数调用请求,其参数就是你定义的JSON Schema,解析成功率接近100%。
    3. 后处理与容错 :在解析JSON的代码中加入健壮的容错逻辑。例如,使用 JsonNode 进行宽松解析,或者使用正则表达式从返回文本中提取JSON片段。记录解析失败的原始响应,用于优化提示词。
  • 技巧 :在开发阶段,用一个“提示词测试沙盒”页面,快速验证不同提示词下模型的输出格式稳定性,这是提高效率的利器。

5.3 协同与系统集成问题

问题:规则与大模型判断冲突,如何裁决? 这是架构设计之初就要想好的。我们的策略是“规则优先,大模型辅助”。规则引擎的硬性拒绝(如黑名单)是最终裁决,没有商量余地。对于规则引擎标记为“需复核”的灰色地带,大模型的建议会与规则引擎的风险评分等输出一起,呈现给最终决策者(可能是另一个规则,也可能是人工)。我们设计了一个“置信度加权”机制:大模型输出的 confidence 分数,会作为一个权重因子,影响最终决策分数。同时,所有决策路径都必须完整记录日志,包括规则触发轨迹和大模型的输入输出,以满足审计和可解释性要求。

问题:如何对这套混合系统进行测试? 测试需要分层进行:

  1. 单元测试 :单独测试每一条核心DRL规则。使用Drools的测试工具,给定输入事实,断言输出结果。
  2. 集成测试 :测试规则集与大模型服务的集成。这里需要Mock大模型服务,模拟其返回各种预定好的响应(如批准、拒绝、需要更多信息),来验证整个决策流水线的逻辑是否正确。
  3. 端到端测试 :使用历史数据或构造的测试用例,运行完整的系统,验证最终业务结果。
  4. 大模型专项测试 :构建一个涵盖边界案例、对抗性问题的测试集,定期(如每周)用生产环境的提示词跑一遍,监控大模型输出质量的变化(漂移)。

这套“规则引擎+大模型”的架构,本质上是在追求一种“可控的智能”。它承认大模型的不完美,用规则的确定性为其划定舞台和底线;同时也利用大模型的泛化能力,突破规则系统的僵化边界。在实施过程中,最深的体会是,技术融合的成功,一半在于技术选型和架构设计,另一半在于对业务场景的深度理解——你必须非常清楚,业务的哪些部分必须“铁板一块”,哪些部分可以“灵活应变”。这个分界点的把握,才是项目成败的关键。

更多推荐