news 2026/10/19 9:41:00

解释器模式实战:从零构建可配置规则引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
解释器模式实战:从零构建可配置规则引擎

1. 从"每次改需求都要动核心代码"说起

如果你写过稍微复杂一点的业务规则引擎,或者处理过自定义查询条件的解析,大概率遇到过这种场景:产品经理跑过来说"这个折扣规则要改一下,满减门槛从300改成500,另外叠加一个会员等级判断",你打开代码一看,发现折扣计算逻辑是一棵巨大的 if-else 树,每个分支里还嵌套着各种条件组合。改一处,测试要回归十处,改完还得担心有没有漏掉某个边界情况。

这种"业务规则频繁变化、但结构高度相似"的问题,其实有一个非常经典的设计模式可以优雅地解决——解释器模式(Interpreter Pattern)。它的核心思路是:把一条业务规则或表达式,抽象成一棵由"终结符"和"非终结符"组成的语法树,每个节点都知道如何"解释"自己,最终整棵树的求值结果就是规则的执行结果。

听起来有点抽象?换个说法你就懂了。你平时写的1 + 2 * 3,编译器或计算器是怎么算的?它不会从左到右傻算,而是先构建一棵表达式树:乘法节点在下面,加法节点在上面,然后自底向上求值。解释器模式做的事情本质上就是这个——为一种特定的小语言(DSL)定义它的文法表示,并提供一个解释器来解释这些句子。

这篇文章我会从实际工程角度出发,把解释器模式拆开揉碎讲清楚:它到底解决什么问题、语法树怎么设计、终结符和非终结符怎么分工、上下文对象承担什么职责、什么时候该用它什么时候千万别用、以及我在实际项目中踩过的那些坑。适合有一定面向对象基础、正在做规则引擎、表达式解析、配置化系统或者想深入理解设计模式的开发者阅读。全文会配可运行的代码示例,你可以直接抄作业。

2. 解释器模式到底在解释什么:文法、语法树与求值链路

2.1 一句话定义与它的四个核心角色

解释器模式的标准定义是:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。这句话里藏着三个关键词:语言、文法、解释。

放到工程里,"语言"就是你自定义的那套规则表达方式,比如age > 18 AND level >= 3;"文法"就是这套表达方式的语法规则,规定了哪些符号合法、怎么组合;"解释"就是拿着一个具体的句子(比如age > 18 AND level >= 3配上age=20, level=5的数据),算出结果(true)。

它通常包含四个角色,我用一个"会员折扣规则"的例子来对应说明:

角色职责例子
抽象表达式(Abstract Expression)声明解释操作的接口Expression.interpret(Context)
终结符表达式(Terminal Expression)文法中的最小单位,不再拆分变量表达式、常量表达式
非终结符表达式(Nonterminal Expression)组合其他表达式,负责逻辑运算AndExpression、OrExpression、GreaterExpression
上下文(Context)存放解释器需要的全局信息存放age=20, level=5的数据容器

理解这四个角色的分工,是理解整个模式的关键。终结符是"叶子",非终结符是"树枝",上下文是"土壤",而 interpret 方法就是让整棵树从叶子往根部逐层求值的那条链路。

2.2 为什么是树形结构而不是线性判断

很多人第一次接触解释器模式会疑惑:我直接用 if-else 或者策略模式不也能实现规则判断吗?为什么非要搞一棵树?

答案在于组合的嵌套性。业务规则很少是平铺的,它天然是嵌套的。比如"(年龄大于18 且 会员等级大于等于3)或者(累计消费大于1000 且 不是黑名单用户)",这种规则用 if-else 写会变成多层嵌套,可读性和可维护性都很差。而用树来表示,每个括号就是一棵子树,逻辑运算符就是连接子树的父节点,结构一目了然。

更关键的是,树形结构让规则的动态组合成为可能。你可以把每个节点当成一个积木,运行时根据配置动态拼装出一棵新的树,而不需要重新编译代码。这就是为什么规则引擎、工作流引擎、查询解析器这些系统,底层几乎都用了类似解释器的思想。

2.3 一条表达式的完整求值链路

我用一个具体例子把求值链路走一遍。假设规则是age > 18 AND level >= 3,上下文里age=20, level=5。

第一步,解析阶段把字符串切成 token,构建出语法树:

AndExpression / \ GreaterExpression GreaterEqualExpression / \ / \ Variable Constant Variable Constant (age) (18) (level) (3)

第二步,求值阶段从根节点开始调用interpret(context)。AndExpression会先调用左子树的 interpret,左子树GreaterExpression再调用它下面Variable(age)的 interpret 拿到 20,和Constant(18)比较,返回 true;右子树同理返回 true;最后AndExpression对两个结果做逻辑与,返回 true。

整条链路是递归自底向上的,每个节点只关心自己的局部逻辑,组合逻辑由父节点负责。这种职责分离正是解释器模式优雅的地方——新增一种运算,只需要新增一个非终结符类,不用动其他任何代码。

3. 手写一个可运行的规则解释器:从文法定义到代码落地

3.1 先定义文法,再写代码

动手写代码之前,一定要先把文法用形式化的方式写出来。这一步很多人会跳过,结果写着写着发现运算符优先级处理不了、括号嵌套解析不了。文法定义清楚了,代码结构自然就出来了。

我们支持的规则文法用巴科斯范式(BNF)描述大致是这样:

expression ::= orExpr orExpr ::= andExpr ('OR' andExpr)* andExpr ::= comparison ('AND' comparison)* comparison ::= operand op operand operand ::= variable | constant | '(' expression ')' op ::= '>' | '>=' | '<' | '<=' | '==' | '!='

这个文法里,OR的优先级低于AND,所以orExpr在andExpr之上,这样解析出来的树天然满足"先算 AND 再算 OR"的优先级。括号通过operand里的递归引用实现嵌套。这就是为什么我说"文法先行"——优先级和嵌套关系在文法层面就解决了,代码只是忠实翻译。

3.2 抽象表达式与上下文对象的接口设计

先定义抽象表达式接口和上下文。上下文我设计成一个简单的键值容器,实际项目中它可能是一个请求对象、一个用户会话或者一个配置快照。

// 抽象表达式 public interface Expression { boolean interpret(Context context); } // 上下文:存放变量值 public class Context { private final Map<String, Object> variables = new HashMap<>(); public void set(String key, Object value) { variables.put(key, value); } public Object get(String key) { return variables.get(key); } }

这里有个设计决策值得说明:为什么interpret返回boolean而不是泛型Object?因为规则引擎的最终结果就是"通过/不通过"的布尔判断。如果你的场景需要返回数值(比如计算器),那就把返回类型改成泛型或double。接口的返回类型要由你的语言语义决定,不要盲目照搬模板。

3.3 终结符表达式:变量与常量

终结符是树的叶子,它们不包含子表达式,直接和上下文交互。

// 变量表达式:从上下文取值 public class VariableExpression implements Expression { private final String name; public VariableExpression(String name) { this.name = name; } @Override public boolean interpret(Context context) { Object value = context.get(name); if (value == null) { throw new IllegalArgumentException("变量未定义: " + name); } return Boolean.TRUE.equals(value); } } // 常量表达式:直接持有值 public class ConstantExpression implements Expression { private final Object value; public ConstantExpression(Object value) { this.value = value; } public Object getValue() { return value; } @Override public boolean interpret(Context context) { return Boolean.TRUE.equals(value); } }

注意VariableExpression里对 null 的处理。这是我在实际项目中踩过的坑:如果变量没在上下文里定义,直接返回 false 会掩盖配置错误,导致规则静默失效。更好的做法是抛出异常或记录告警,让问题暴露出来,而不是让一条本该报错的规则悄悄返回错误结果。

3.4 非终结符表达式:比较与逻辑运算

非终结符负责组合。比较表达式需要拿到左右操作数的实际值,逻辑表达式需要组合子表达式的结果。

// 比较表达式:支持 > >= < <= == != public class ComparisonExpression implements Expression { private final Expression left; private final Expression right; private final String operator; public ComparisonExpression(Expression left, String operator, Expression right) { this.left = left; this.operator = operator; this.right = right; } @Override public boolean interpret(Context context) { Object l = resolve(left, context); Object r = resolve(right, context); int cmp = compare(l, r); switch (operator) { case ">": return cmp > 0; case ">=": return cmp >= 0; case "<": return cmp < 0; case "<=": return cmp <= 0; case "==": return cmp == 0; case "!=": return cmp != 0; default: throw new IllegalArgumentException("未知运算符: " + operator); } } private Object resolve(Expression expr, Context context) { if (expr instanceof ConstantExpression) { return ((ConstantExpression) expr).getValue(); } return context.get(((VariableExpression) expr).getName()); } private int compare(Object l, Object r) { if (l instanceof Number && r instanceof Number) { return Double.compare(((Number) l).doubleValue(), ((Number) r).doubleValue()); } return String.valueOf(l).compareTo(String.valueOf(r)); } }

逻辑表达式更简单,就是把两个子表达式的结果做布尔运算:

public class AndExpression implements Expression { private final Expression left; private final Expression right; public AndExpression(Expression left, Expression right) { this.left = left; this.right = right; } @Override public boolean interpret(Context context) { return left.interpret(context) && right.interpret(context); } } public class OrExpression implements Expression { private final Expression left; private final Expression right; public OrExpression(Expression left, Expression right) { this.left = left; this.right = right; } @Override public boolean interpret(Context context) { return left.interpret(context) || right.interpret(context); } }

AndExpression用了短路求值(&&),这意味着如果左子树为 false,右子树根本不会被求值。这个特性在规则引擎里非常有用——你可以把开销大的判断放在右边,把简单的过滤条件放左边,性能自然就优化了。

3.5 组装语法树并跑通第一个用例

有了这些节点,手工组装一棵树来验证:

public class Demo { public static void main(String[] args) { // 规则: age > 18 AND level >= 3 Expression rule = new AndExpression( new ComparisonExpression(new VariableExpression("age"), ">", new ConstantExpression(18)), new ComparisonExpression(new VariableExpression("level"), ">=", new ConstantExpression(3)) ); Context ctx = new Context(); ctx.set("age", 20); ctx.set("level", 5); System.out.println(rule.interpret(ctx)); // true ctx.set("level", 2); System.out.println(rule.interpret(ctx)); // false } }

跑通之后你会发现,新增一条规则只需要重新组装树,完全不用改任何节点类的代码。这就是开闭原则的体现——对扩展开放(加新节点类型),对修改关闭(不改已有节点)。

4. 从手工组装到字符串解析:让规则真正可配置

4.1 手工组装树的致命短板

上面那段 Demo 能跑,但有个致命问题:树是硬编码在 Java 代码里的。产品经理要改规则,还是得找你改代码、重新编译、重新发布。这跟直接用 if-else 没本质区别,只是换了个优雅的写法而已。

解释器模式真正的威力,在于把规则的构建过程也自动化——让用户输入一个字符串,程序自动解析成语法树。这一步做完,规则就彻底配置化了,运营人员改规则不需要动代码。

4.2 词法分析:把字符串切成 token

解析的第一步是词法分析(Lexing),把age > 18 AND level >= 3切成一个个 token。我写一个简单的分词器:

public class Lexer { private final String input; private int pos = 0; public Lexer(String input) { this.input = input; } public List<String> tokenize() { List<String> tokens = new ArrayList<>(); while (pos < input.length()) { char c = input.charAt(pos); if (Character.isWhitespace(c)) { pos++; } else if (Character.isDigit(c)) { StringBuilder sb = new StringBuilder(); while (pos < input.length() && Character.isDigit(input.charAt(pos))) { sb.append(input.charAt(pos++)); } tokens.add(sb.toString()); } else if (Character.isLetter(c)) { StringBuilder sb = new StringBuilder(); while (pos < input.length() && Character.isLetterOrDigit(input.charAt(pos))) { sb.append(input.charAt(pos++)); } tokens.add(sb.toString()); } else if (c == '>' || c == '<' || c == '=' || c == '!') { // 处理双字符运算符 if (pos + 1 < input.length() && input.charAt(pos + 1) == '=') { tokens.add("" + c + "="); pos += 2; } else { tokens.add(String.valueOf(c)); pos++; } } else { tokens.add(String.valueOf(c)); pos++; } } return tokens; } }

分词器看着简单,但有几个坑必须注意。第一,双字符运算符的处理顺序:必须先判断>=再判断>,否则>=会被切成>和=两个 token。第二,关键字的大小写:AND和and要不要都支持?我建议统一转大写处理,减少用户输入错误。第三,数字和小数的区分:上面代码只处理了整数,实际项目要支持小数点,否则18.5会被切成18、.、5。

4.3 递归下降解析:把 token 流变成语法树

有了 token 列表,接下来用递归下降(Recursive Descent)解析器构建语法树。递归下降的好处是代码结构和文法几乎一一对应,可读性极强。

public class Parser { private final List<String> tokens; private int pos = 0; public Parser(List<String> tokens) { this.tokens = tokens; } public Expression parse() { return parseOr(); } private Expression parseOr() { Expression left = parseAnd(); while (pos < tokens.size() && "OR".equalsIgnoreCase(tokens.get(pos))) { pos++; Expression right = parseAnd(); left = new OrExpression(left, right); } return left; } private Expression parseAnd() { Expression left = parseComparison(); while (pos < tokens.size() && "AND".equalsIgnoreCase(tokens.get(pos))) { pos++; Expression right = parseComparison(); left = new AndExpression(left, right); } return left; } private Expression parseComparison() { Expression left = parseOperand(); if (pos < tokens.size() && isOperator(tokens.get(pos))) { String op = tokens.get(pos++); Expression right = parseOperand(); return new ComparisonExpression(left, op, right); } return left; } private Expression parseOperand() { String token = tokens.get(pos++); if (token.matches("\\d+")) { return new ConstantExpression(Integer.parseInt(token)); } return new VariableExpression(token); } private boolean isOperator(String token) { return Arrays.asList(">", ">=", "<", "<=", "==", "!=").contains(token); } }

这段代码的精髓在于每个解析方法对应文法里的一条产生式。parseOr对应orExpr,parseAnd对应andExpr,层层递进。因为parseOr先调用parseAnd,所以 AND 的优先级天然高于 OR,不需要额外处理优先级表。

4.4 完整链路串起来:字符串进,布尔值出

把分词器和解析器串起来,就得到了一个完整的规则引擎入口:

public class RuleEngine { public static Expression compile(String ruleText) { List<String> tokens = new Lexer(ruleText).tokenize(); return new Parser(tokens).parse(); } public static void main(String[] args) { Expression rule = compile("age > 18 AND level >= 3 OR vip == 1"); Context ctx = new Context(); ctx.set("age", 20); ctx.set("level", 2); ctx.set("vip", 1); System.out.println(rule.interpret(ctx)); // true,因为 vip==1 } }

到这里,一个可配置的规则引擎就成型了。运营人员只要在后台输入规则字符串,系统就能动态解析执行,完全不需要改代码。这就是解释器模式在工程上最有价值的落地形态。

5. 解释器模式的适用边界:什么时候该用,什么时候是过度设计

5.1 三个明确的适用信号

解释器模式不是银弹,用错了地方就是典型的过度设计。根据我的经验,出现下面三个信号时,才值得考虑它:

信号一:存在一种需要反复解释的简单语言。注意"简单"两个字。如果你的规则文法超过二三十条产生式,手写解析器会变得非常痛苦,这时候应该上 ANTLR、JavaCC 这类解析器生成工具,而不是硬扛解释器模式。

信号二:规则需要频繁变化且由非开发人员配置。如果规则一年改一次,直接写 if-else 最省事。只有当规则变化频繁、且希望业务人员自己维护时,配置化的价值才凸显出来。

信号三:规则结构高度相似,只是参数和组合方式不同。比如各种促销规则、风控规则、审批条件,它们都是"条件组合"的变体,用同一套语法树表达非常自然。

5.2 四个应该果断放弃的场景

反过来,下面这些场景千万别用解释器模式:

场景为什么不适合更好的选择
规则固定不变引入解析器和语法树纯属增加复杂度直接 if-else 或策略模式
文法极其复杂手写解析器维护成本爆炸ANTLR 等解析器生成器
性能要求极高每次求值都要遍历树,有开销编译成字节码或预编译表达式
只是简单条件判断杀鸡用牛刀策略模式、规则表

我见过一个团队,为了判断"用户是否成年"这一个条件,硬是搭了一套规则引擎,结果维护成本比直接写age >= 18高了十倍。设计模式的价值在于解决复杂度,而不是制造复杂度。

5.3 性能优化的几个实用手段

如果你的规则引擎确实要扛高并发,解释器模式的原生实现会有性能瓶颈,因为每次求值都要递归遍历整棵树。几个优化手段:

第一,缓存编译结果。规则字符串解析成语法树是有开销的,把ruleText -> Expression的映射缓存起来,同一规则只解析一次。用ConcurrentHashMap做缓存,注意设置合理的容量上限防止内存泄漏。

第二,短路求值。前面提到的AndExpression用&&、OrExpression用||,让高开销的判断靠后执行,能省下大量无用计算。

第三,把树编译成更高效的中间形式。如果性能要求极致,可以把语法树进一步编译成一组线性指令(类似字节码),用栈式虚拟机执行,避免递归调用的开销。这是进阶玩法,一般业务场景用不上。

第四,预编译常量表达式。像level >= 3这种两边都是常量的比较,可以在编译期直接算出结果,运行时就不用再算了。

6. 实战中踩过的坑与排查思路

6.1 运算符优先级搞反导致规则静默出错

我印象最深的一次事故:规则a > 1 OR b > 2 AND c > 3,按数学直觉应该是先算 AND 再算 OR,结果我们的解析器把 OR 和 AND 当成了同级,从左到右算,导致(a>1 OR b>2) AND c>3,规则结果完全错了,而且因为规则本身能跑通不报错,测试也没覆盖到这种组合,上线后才发现。

排查这类问题的思路是:构造边界用例,把优先级差异暴露出来。比如设a=2, b=0, c=0,正确结果应该是true OR (false AND false) = true,错误结果应该是(true OR false) AND false = false。一跑就能定位。修复方法就是前面说的,在文法层面把orExpr放在andExpr之上,让优先级由文法结构保证,而不是靠代码里的判断顺序。

6.2 变量未定义时的静默失败

前面提过,VariableExpression遇到未定义的变量,如果直接返回 false,会导致规则悄悄失效。我后来改成抛异常,但抛异常又带来新问题:如果规则里有个可选变量,用户没配,整个规则就崩了。

最终的方案是引入默认值机制:VariableExpression支持一个可选的默认值,未定义时用默认值,同时打一条 warn 日志。这样既不会静默失败,也不会因为一个可选变量导致规则不可用。日志是关键,它让你能追踪到哪些规则依赖了未配置的变量。

6.3 类型不匹配引发的比较异常

规则里age > "18"这种字符串和数字比较,如果处理不当会抛ClassCastException。我的做法是在ComparisonExpression里做类型归一化:如果两边都是数字,按数值比较;如果有一边是字符串,尝试解析成数字再比;解析失败就按字符串字典序比。同时记录一条日志,提示用户可能存在类型问题。

这里有个细节:浮点数比较不要用==。0.1 + 0.2 == 0.3在浮点世界里是 false。如果规则涉及金额、比例这类小数,比较时应该引入一个误差范围(epsilon),或者干脆用BigDecimal存储和比较。

6.4 递归深度过大导致栈溢出

如果用户写了一条超长的规则,比如几百个 AND 连在一起,递归下降解析器会递归几百层,可能触发StackOverflowError。防御手段有两个:一是限制规则字符串的最大长度和最大 token 数;二是在解析器里加深度计数,超过阈值就报错。我在生产环境里把阈值设成 200 层,超过就拒绝,并提示用户拆分规则。

6.5 规则注入与安全边界

规则字符串来自用户输入,就存在注入风险。比如用户输入一个超长的表达式,或者构造出解析器无法处理的畸形输入,可能导致解析器死循环或崩溃。防护措施包括:限制输入长度、对 token 做白名单校验(只允许字母、数字、运算符、括号)、解析超时保护。永远不要信任用户输入的规则字符串,这是安全底线。

7. 解释器模式与其他方案的横向对比

7.1 和策略模式的区别

很多人会把解释器模式和策略模式搞混,因为它们都涉及"多种算法可切换"。区别在于:策略模式是平级替换,每个策略是一个完整的算法,运行时选一个执行;解释器模式是组合嵌套,每个节点是算法的一部分,通过树形组合形成完整逻辑。

打个比方:策略模式像换挡,你一次只能挂一个挡;解释器模式像搭积木,你可以把任意积木组合成复杂结构。如果你的需求是"多种互斥的算法选一个",用策略模式;如果是"简单规则组合成复杂规则",用解释器模式。

7.2 和规则引擎框架的关系

市面上有很多成熟的规则引擎框架,它们底层大多用了类似解释器的思想,但在解析、优化、执行上做了大量工程化工作。自己手写解释器模式适合规则简单、可控性要求高的场景;如果规则复杂到需要脚本语言级别的表达能力,直接用成熟框架更划算。不要重复造轮子,除非轮子确实不适合你的车。

7.3 和表达式求值库的取舍

如果只是做数学表达式求值,现成的表达式求值库(比如各种 Expression Evaluator)开箱即用,没必要自己写。解释器模式的价值在于你可以完全掌控文法和求值逻辑,能针对业务做定制,比如支持自定义函数、支持业务变量、支持特殊的比较语义。当现成库满足不了你的定制需求时,才是自己动手的时机。

8. 我在实际项目中的几点体会

解释器模式是我在规则引擎项目里用得最多的模式之一,踩的坑也最多。最大的体会是:这个模式的难点从来不在模式本身,而在文法的设计和解析器的健壮性。模式的结构是固定的,但文法怎么定、边界怎么处理、错误怎么提示,这些才是真正决定项目成败的地方。

另一个体会是,不要一上来就追求大而全的文法。我早期设计规则引擎时,想一口气支持函数调用、算术运算、括号嵌套、类型转换,结果解析器写了一千多行,bug 层出不穷。后来砍到只支持比较和逻辑运算,稳定运行了半年,再按需逐步扩展,反而顺利得多。从最小可用文法开始,让需求驱动演进,这是我用血泪换来的经验。

还有一点,测试用例要覆盖文法的每一条产生式。解析器最容易出错的地方就是优先级和嵌套,而这些恰恰是普通测试容易漏掉的。我现在的习惯是,每加一条文法规则,就配套写一组边界测试,包括正常情况、优先级冲突、括号嵌套、畸形输入。这套测试后来帮我拦下了好几次回归问题。

最后分享一个小技巧:在解析器里加一个toString方法,把语法树还原成带括号的字符串。调试的时候,把用户输入的规则和解析后的树打印出来对比,一眼就能看出优先级有没有解析错。这个技巧帮我定位过好几次优先级 bug,比单步调试高效得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:40:59

跨交换机VLAN划分实战:Trunk与802.1Q标签配置详解

1. 从一个真实组网场景说起&#xff1a;为什么需要跨交换机划分VLAN很多刚接触企业级网络的朋友&#xff0c;第一次听到"跨交换机划分VLAN"这个词&#xff0c;脑子里冒出来的第一个疑问往往是&#xff1a;我在一台交换机上划好VLAN不就行了吗&#xff0c;为什么还要折…

作者头像 李华
网站建设 2026/10/19 9:40:04

语言模型如何赋能金融风控:从非结构化文本到结构化风险决策

语言模型和金融风险管理&#xff0c;这两个词放在一起&#xff0c;听起来像是又一篇技术趋势报告。但真做过的团队都明白&#xff0c;这句话背后不是搭个聊天机器人那么简单。它要回答的是两个非常硬的问题&#xff1a;哪些风险信号藏在非结构化文本里&#xff0c;以及怎么用语…

作者头像 李华
网站建设 2026/10/19 9:38:14

LangChain Tool Calling实战:让大模型学会调用外部工具

最近在做智能体项目的时候&#xff0c;团队里好几个同事都在同一个地方卡了壳&#xff1a;模型能聊天&#xff0c;但一让它查天气、算数学、查数据库&#xff0c;它就一本正经地开始“编”答案。直到我们把 LangChain 的 Tool Calling 真正用起来&#xff0c;这个问题才算彻底解…

作者头像 李华
网站建设 2026/10/19 9:37:43

Autoware Open Planner论文精读与参数调优实战指南

1. 从一份空白的输入说起&#xff1a;为什么我要啃Open Planner的论文和参数拿到这个标题的时候&#xff0c;项目正文、关键词、摘要描述全是空的&#xff0c;只有一行字——“Autoware Open Planner论文阅读 参数介绍”。这其实挺典型的&#xff0c;很多做自动驾驶规划控制方向…

作者头像 李华
网站建设 2026/10/19 9:37:07

自动驾驶感知落地实战:YOLOv8目标检测训练与TensorRT部署踩坑记录

做自动驾驶感知的第一天&#xff0c;带队的老工程师就跟我说过一句话&#xff1a;项目上没有花活&#xff0c;车辆、行人、红绿灯&#xff0c;这三类目标能不能稳定检出来&#xff0c;基本决定了一套系统能不能过路测。后来我在某量产L2辅助驾驶的感知模块里&#xff0c;真的把…

作者头像 李华
网站建设 2026/10/19 9:37:06

从零手搓C++子集编译器:LL(1)文法实战与避坑指南

1. 从零手搓一个C子集编译器&#xff1a;LL(1)文法到底能走多远编译原理这门课&#xff0c;很多人学完的感觉就是“听懂了&#xff0c;但让我写一个编译器&#xff0c;我连从哪下手都不知道”。词法分析、语法分析、语义分析、中间代码生成、目标代码生成——每个阶段单拎出来都…

作者头像 李华